<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Firefox on ざわらぼ</title><link>https://w1.zawa-lab.net/tags/firefox/</link><description>Recent content in Firefox on ざわらぼ</description><generator>Hugo -- gohugo.io</generator><language>ja</language><lastBuildDate>Fri, 09 Oct 2026 21:07:24 +0900</lastBuildDate><atom:link href="https://w1.zawa-lab.net/tags/firefox/index.xml" rel="self" type="application/rss+xml"/><item><title>Claude DesktopのEnter誤送信が鬱陶しいので、Ctrl+Enterで送信できるようにしてみた</title><link>https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/</link><pubDate>Fri, 09 Oct 2026 21:07:24 +0900</pubDate><guid>https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/</guid><description>&lt;img src="https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/image.png" alt="Featured image of post Claude DesktopのEnter誤送信が鬱陶しいので、Ctrl+Enterで送信できるようにしてみた" />&lt;!-- qiita-sync:slug=20261009_claude-ctrl-enter sha=7e6071fa78a3 -->
&lt;h1 id="claude-desktopのenter誤送信が鬱陶しいのでctrlenterで送信できるようにしてみた日本語ime対応2026年10月">Claude DesktopのEnter誤送信が鬱陶しいので、Ctrl+Enterで送信できるようにしてみた【日本語IME対応／2026年10月】
&lt;/h1>&lt;p>&lt;img src="https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/image.png"
width="1290"
height="765"
srcset="https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/image_hu_6f2415f2142f752d.png 480w, https://w1.zawa-lab.net/p/20261009_claude-ctrl-enter/image_hu_1f67f035d3d0a8f.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="168"
data-flex-basis="404px"
>&lt;/p>
&lt;blockquote>
&lt;p>この記事はChatGPTなどの生成AIを利用して執筆しています。ツールの調査・実装もAIの支援を受けていますが、IMEの動作やClaude Desktop上での操作は手元の環境で検証しました。&lt;/p>
&lt;p>&lt;strong>2026年10月9日時点の記録です。&lt;/strong> Claude Desktopの非公開実装に依存した非公式ツールであり、今後の更新への対応を保証するものではありません。&lt;/p>&lt;/blockquote>
&lt;p>日本語でClaudeを使っていると、Enterキーでやらかしませんか。&lt;/p>
&lt;p>日本語変換を確定しようとしてEnterを押したら、そのままメッセージまで送信してしまう。変換の途中だったり、まだ文章が完成していなかったりするので、地味に面倒くさいんですよね。&lt;/p>
&lt;p>Microsoft TeamsやChatGPTなどでは、普段からCtrl+Enterで送信する操作に慣れています。なので、Claudeだけ入力欄の挙動が違うのが、どうにも気持ち悪い。&lt;/p>
&lt;p>ということで、&lt;strong>Enterで改行、Ctrl+Enterで送信&lt;/strong>するツールを作りました。日本語IMEの変換確定は邪魔しません。ついでにClaudeの画面内でキーを変更できるようにもしています。&lt;/p>
&lt;p>最初はAutoHotkeyでどうにかするつもりだったのですが、IMEの状態判定が予想以上に厄介で、最終的にはElectron内部のWeb画面へ拡張機能を読み込ませる方法に落ち着きました。&lt;/p>
&lt;p>この記事では、まず導入方法と結論を掲載し、そのあとでAutoHotkeyによる調査、Electronの&lt;code>app.asar&lt;/code>解析、WebExtensionの読み込み方法、Tiptapのキーイベント処理まで書いていきます。&lt;/p>
&lt;h2 id="最初に結論">最初に結論
&lt;/h2>&lt;p>作成したのは &lt;strong>Claude Ctrl+Enter&lt;/strong> という非公式のClaude Desktop向けツールです。&lt;/p>
&lt;p>&lt;strong>GitHub：&lt;/strong> &lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">zawa356/claude_ctrl-enter&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;br>
&lt;strong>ダウンロード：&lt;/strong> &lt;a href="https://github.com/zawa356/claude_ctrl-enter/releases/latest" target="_blank" rel="noopener">最新のReleases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;br>
&lt;strong>詳しい導入方法：&lt;/strong> &lt;a href="https://github.com/zawa356/claude_ctrl-enter#readme" target="_blank" rel="noopener">README（日本語）&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter#readme" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>操作&lt;/th>
&lt;th>導入後の初期設定&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Enter&lt;/code>&lt;/td>
&lt;td>改行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Ctrl+Enter&lt;/code>&lt;/td>
&lt;td>送信&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IME変換中の&lt;code>Enter&lt;/code>&lt;/td>
&lt;td>変換候補の確定。送信も改行もしない&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/&lt;/code>コマンドやメンション候補表示中の&lt;code>Enter&lt;/code>&lt;/td>
&lt;td>Claude本来の候補選択&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>サイドバーの「⌨ キー設定」から有効・無効を切り替えたり、送信キーと改行キーを&lt;code>Enter&lt;/code>、&lt;code>Ctrl+Enter&lt;/code>、&lt;code>Shift+Enter&lt;/code>、&lt;code>Alt+Enter&lt;/code>の中から選び直したりできます。設定は再起動後も保持されます。&lt;/p>
&lt;p>実際の操作感としては、特別な機能が増えたというより、&lt;strong>ほかのチャットアプリと同じ感覚で普通に文章を入力できる&lt;/strong>ようになった、というものです。今回ほしかったのはまさにそれなので十分です。&lt;/p>
&lt;h3 id="対応環境">対応環境
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>項目&lt;/th>
&lt;th>2026年10月9日時点の状況&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Windows 10 / 11&lt;/td>
&lt;td>Claude DesktopのMSIX版向け。Windows実機で検証&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>検証したClaude&lt;/td>
&lt;td>&lt;code>2.26454.2&lt;/code>、&lt;code>2.31226.0&lt;/code>（共通ローダー移行後は後者で確認）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Linux&lt;/td>
&lt;td>Claude Desktopベータ向けスクリプトあり。&lt;strong>実機未確認&lt;/strong>、CIテストのみ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>macOS&lt;/td>
&lt;td>未対応&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>インストール権限&lt;/td>
&lt;td>Windowsでは管理者権限不要&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Claude本体の&lt;code>claude.exe&lt;/code>や&lt;code>app.asar&lt;/code>、Microsoft Storeアプリのインストール先には手を加えません。&lt;/p>
&lt;h2 id="導入方法windows">導入方法（Windows）
&lt;/h2>&lt;ol>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/releases/latest" target="_blank" rel="noopener">最新のReleases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
から&lt;code>claude-ctrl-enter-&amp;lt;バージョン&amp;gt;.zip&lt;/code>をダウンロードして展開します。&lt;/li>
&lt;li>中にある&lt;code>install.bat&lt;/code>を実行します。診断情報を確認し、続行を求められたら&lt;code>Y&lt;/code>を入力します。&lt;/li>
&lt;li>Claude Desktopを&lt;strong>タスクトレイから完全に終了&lt;/strong>させます。単にウィンドウを閉じるだけでは不十分な場合があります。&lt;/li>
&lt;li>いつものClaudeのアイコンから起動し直します。&lt;/li>
&lt;li>左サイドバーのプロフィール欄の上に「⌨ キー設定　有効」と出ているか確認します。&lt;/li>
&lt;/ol>
&lt;p>これでEnterは改行、Ctrl+Enterは送信になります。インストール後も専用ランチャーやショートカットは不要です。&lt;/p>
&lt;p>Windowsが未署名のbatファイルについて警告する場合があります。内容を確認したうえで実行してください。インストーラーやPowerShellスクリプトもリポジトリで公開しています。&lt;/p>
&lt;p>&lt;strong>元に戻す：&lt;/strong> &lt;code>uninstall.bat&lt;/code>を実行し、Claude Desktopを完全終了して起動し直します。キーだけ一時的に元へ戻したい場合は「キー設定を有効にする」をオフにする方法もあります。&lt;/p>
&lt;p>&lt;strong>診断：&lt;/strong> &lt;code>diagnose.bat&lt;/code>は読み取り専用です。動かないときは、まずこれで導入状況を確認できます。&lt;/p>
&lt;p>配布版の導入・更新手順は今後変わる可能性があるため、実際にインストールするときは&lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">README&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
を優先してください。&lt;/p>
&lt;hr>
&lt;h2 id="なぜautohotkeyではなくelectronの中に実装したのか">なぜAutoHotkeyではなく、Electronの中に実装したのか
&lt;/h2>&lt;p>ここから先は技術的な話です。&lt;/p>
&lt;p>「Ctrl+Enterを押したら送信するだけ」なら、Windowsのキーリマップで終わるように思えます。自分も最初はそう考えてAutoHotkey v2を使い始めました。&lt;/p>
&lt;p>しかし、問題の中心にあったのは&lt;strong>Ctrl+Enterではなく日本語IMEの変換確定&lt;/strong>でした。&lt;/p>
&lt;h3 id="1-enterには別の役割がある">1. Enterには別の役割がある
&lt;/h3>&lt;p>日本語入力中のEnterは、状況によって違う意味になります。&lt;/p>
&lt;ul>
&lt;li>変換候補の選択・確定&lt;/li>
&lt;li>未確定文字列の確定&lt;/li>
&lt;li>変換が終わったあと、通常の入力欄で行う改行&lt;/li>
&lt;li>Claudeが割り当てている送信&lt;/li>
&lt;/ul>
&lt;p>さらに、句読点や「＠」のような全角記号、候補ウィンドウ、キー入力のタイミングも関係します。&lt;/p>
&lt;p>Windows側で単純にEnterを別のキーへ変換すると、&lt;strong>IMEが必要としているEnterまで奪ってしまう&lt;/strong>可能性があります。&lt;/p>
&lt;p>なので、まずはWindowsからIMEの変換状態をどこまで取得できるかを調べました。&lt;/p>
&lt;h2 id="2-autohotkey--ui-automationでimeイベントを調べる">2. AutoHotkey + UI AutomationでIMEイベントを調べる
&lt;/h2>&lt;p>AutoHotkey v2とUI Automationを使い、Claude Desktopの入力欄に対して、次のイベントを監視する試作を作りました。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>イベント&lt;/th>
&lt;th>調査上の意味&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>TextEditTextChanged&lt;/code> / &lt;code>Composition(2)&lt;/code>&lt;/td>
&lt;td>IME変換中に観測されたイベント&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>TextEditTextChanged&lt;/code> / &lt;code>Finalized(3)&lt;/code>&lt;/td>
&lt;td>確定を検出できるか試すためのイベント&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>TextChanged(20015)&lt;/code>&lt;/td>
&lt;td>テキストが変更されたことを示すイベント&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>調査スクリプトはAutoHotkey v2のUIAライブラリを使用したもので、&lt;strong>入力キー自体は変更せず、イベントの時刻と種類だけをログに残す&lt;/strong>診断用でした。変換文字列や入力内容は記録していません。&lt;/p>
&lt;p>実際に使用したスクリプトでは、おおむね次のようにハンドラーを登録しています（コードは要点のみ。実行にはAutoHotkey v2とUIA-v2ライブラリ、およびコールバック・後始末の実装が必要です）。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-autohotkey" data-lang="autohotkey">&lt;span class="line">&lt;span class="cl">&lt;span class="n">#Requires&lt;/span> &lt;span class="n">AutoHotkey&lt;/span> &lt;span class="n">v2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">#Include&lt;/span> &lt;span class="n">Lib&lt;/span>\&lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ahk&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">el&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">GetFocusedElement&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">; 変換・確定関連の通知を観測&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">hComp&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CreateTextEditTextChangedEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OnComposition&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">hFinal&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CreateTextEditTextChangedEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OnComposition&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">hText&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CreateAutomationEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OnTextChanged&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AddTextEditTextChangedEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hComp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AddTextEditTextChangedEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hFinal&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="n">UIA&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AddAutomationEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hText&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20015&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>ここで期待していたのは「Compositionで変換中へ入り、Finalizedで変換終了が分かる」という素直な状態遷移です。&lt;/p>
&lt;p>しかし、実際の通知はそう簡単ではありませんでした。&lt;/p>
&lt;h3 id="2-1-観測できたイベントの順序">2-1. 観測できたイベントの順序
&lt;/h3>&lt;p>2026年10月8日のログから、代表的な部分を抜粋すると次のような形です。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">2719 ms | RAW Composition(2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2719 ms | STATE=COMPOSING (Composition observed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2719 ms | RAW TextChanged(20015)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2719 ms | MATCH Composition -&amp;gt; TextChanged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3328 ms | RAW TextChanged(20015)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3328 ms | STATE=PENDING_TEXT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3500 ms | UNMATCHED TextChanged
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3500 ms | STATE=IDLE_INFERRED (Unpaired TextChanged (NOT guaranteed))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>※ログの&lt;code>STATE=...&lt;/code>は診断スクリプト自身が推定した状態です。Windowsが「IMEは終了した」と保証したものではありません。&lt;/p>
&lt;p>&lt;code>Composition&lt;/code>と&lt;code>TextChanged&lt;/code>が同時、または十数ミリ秒の差で届く例がありました。一方で&lt;code>TextChanged&lt;/code>だけが単独で届く場面もあります。&lt;/p>
&lt;p>そこで、&lt;code>Composition&lt;/code>と&lt;code>TextChanged&lt;/code>を150ms以内の時間差で照合し、対応しない&lt;code>TextChanged&lt;/code>があったら変換終了を&lt;strong>推測&lt;/strong>する分類器まで作っています。&lt;/p>
&lt;p>しかし、これでは「変換終了を確実に検出できた」とは言えません。実際のログでも状態名は&lt;code>IDLE_INFERRED&lt;/code>、理由は&lt;code>NOT guaranteed&lt;/code>です。&lt;/p>
&lt;p>この結果は、AutoHotkeyやUI Automation全般でIME判定が絶対にできない、という証明ではありません。少なくとも&lt;strong>今回のイベントと判定方式だけでは、誤送信を防ぐための十分に確実な状態判定にならなかった&lt;/strong>、という話です。&lt;/p>
&lt;p>本来の目的は、キー操作を楽にすることです。判定器のタイミング調整を延々と続けるより、もう少し入力欄に近いところで処理した方がよさそうです。&lt;/p>
&lt;h2 id="3-claude-desktopの中身はelectron">3. Claude Desktopの中身はElectron
&lt;/h2>&lt;p>ということで、別の方向から攻めることにしました。&lt;/p>
&lt;p>Claude DesktopのUIはElectron上で動いています。つまり、Webページ側のDOMやキーボードイベントにアクセスできれば、WindowsのIME状態を外側から推定するより、入力欄が直接受け取っているイベントに基づいて判断できます。&lt;/p>
&lt;p>ただし、公式のClaudeアプリを壊してまでキー操作を直したいわけではありません。&lt;/p>
&lt;p>ここでの制約は最初から次のとおりでした。&lt;/p>
&lt;ul>
&lt;li>&lt;code>claude.exe&lt;/code>や&lt;code>app.asar&lt;/code>を改変しない&lt;/li>
&lt;li>Microsoft Storeの署名付きMSIXを壊さない&lt;/li>
&lt;li>いつものアイコンから普通に起動できる&lt;/li>
&lt;li>新しい専用ランチャーを使わせない&lt;/li>
&lt;li>不要になったら解除できる&lt;/li>
&lt;/ul>
&lt;p>この条件を満たす方法を探します。&lt;/p>
&lt;h3 id="3-1-appasarを読み取り専用で調べる">3-1. &lt;code>app.asar&lt;/code>を読み取り専用で調べる
&lt;/h3>&lt;p>最初にWindows側でパッケージ情報を確認しました。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Get-AppxPackage&lt;/span> &lt;span class="n">-Name&lt;/span> &lt;span class="s1">&amp;#39;*Claude*&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">Select-Object&lt;/span> &lt;span class="n">Name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PackageFamilyName&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">InstallLocation&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>調査した&lt;code>2.26454.2&lt;/code>では、&lt;code>app/resources/app.asar&lt;/code>が存在しました。&lt;code>app.asar&lt;/code>はElectronでよく使われるアーカイブ形式です。&lt;/p>
&lt;p>本体を変更しないよう、&lt;strong>作業用のフォルダーへコピーしてから&lt;/strong>解析しました。実例として、コピー後の作業フォルダーで次を実行すれば、パッケージ情報を読み出せます。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># ここでは作業用フォルダーに app.asar をコピー済みとする&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">npx&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-yes&lt;/span> &lt;span class="nv">@electron&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">asar&lt;/span> &lt;span class="nb">extract-file&lt;/span> &lt;span class="p">.\&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">asar&lt;/span> &lt;span class="n">package&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Get-Content&lt;/span> &lt;span class="p">.\&lt;/span>&lt;span class="n">package&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">json&lt;/span> &lt;span class="n">-Raw&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="nb">ConvertFrom-Json&lt;/span> &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">Select-Object&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">version&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">main&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当時の結果は次のようなものでした。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">name version main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---- ------- ----
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">@ant/desktop 2.26454.2 .vite/build/index.pre.js
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>npx&lt;/code>を使う場合はNode.js/npmが必要です。また、ソースはminifyされた大きなバンドルなので、何かを検索すればすぐ意味が分かるというものでもありません。&lt;/p>
&lt;p>起点の&lt;code>index.pre.js&lt;/code>から呼び出し先を追い、&lt;code>mainView.js&lt;/code>、&lt;code>mainWindow.js&lt;/code>、各&lt;code>index.chunk-*.js&lt;/code>を調べました。外側のウィンドウとチャット本体が別に構成され、通常のチャットは&lt;code>https://claude.ai&lt;/code>を読み込むことが分かりました。&lt;/p>
&lt;p>この時点で重要なのは、&lt;strong>入力欄の送信処理そのものは、MSIXのネイティブ部分を改造しなくても操作できそう&lt;/strong>だったことです。&lt;/p>
&lt;h3 id="3-2-開発者ツールで入力欄を確認">3-2. 開発者ツールで入力欄を確認
&lt;/h3>&lt;p>調査時にはClaudeの「ヘルプ」→「トラブルシューティング」から開発者モードを有効にし、入力欄にフォーカスした状態で&lt;code>Ctrl+Alt+I&lt;/code>を押してDevToolsを開きました。&lt;/p>
&lt;p>※これは&lt;strong>開発時にDOMを調べるための操作&lt;/strong>です。公開版のインストールに開発者モードを有効化する必要はありません。初期試作と現在の導入要件を混同しないようにします。&lt;/p>
&lt;p>コンソールで入力欄を見ると、次のような要素が見つかりました。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;tiptap ProseMirror&amp;#34;&lt;/span> &lt;span class="na">contenteditable&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span> &lt;span class="na">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;textbox&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>説明用に属性を抜粋しています。実際のDOMにはほかにも属性があります。&lt;/p>
&lt;p>さらに、次のようなコマンドで確認できます。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.tiptap.ProseMirror[contenteditable=&amp;#34;true&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;editor:&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">!!&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">editor&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;hardBreak:&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">typeof&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">editor&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">commands&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setHardBreak&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;view composing:&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">editor&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">view&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">composing&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当時の環境では&lt;code>el.editor&lt;/code>が存在し、&lt;code>editor.commands.setHardBreak()&lt;/code>を利用できました。&lt;/p>
&lt;p>つまり、単にEnterというキーイベントを合成するのではなく、&lt;strong>入力エディターの正規の改行コマンドを直接呼び出せる&lt;/strong>わけです。&lt;/p>
&lt;h3 id="3-3-送信は内部apiよりもボタンを利用する">3-3. 送信は内部APIよりもボタンを利用する
&lt;/h3>&lt;p>続いて送信処理も調べました。&lt;/p>
&lt;p>Tiptap/ProseMirrorのプラグインやキーマップを見れば内部の送信コマンドも見つかるかと思いましたが、バンドルされた関数や閉じたスコープが多く、確実な呼び出し経路を特定できませんでした。&lt;/p>
&lt;p>そこで無理に内部APIを追うのはやめて、Claudeが表示している送信ボタンを利用する方式にしました。&lt;/p>
&lt;p>実機で確認したのは次の属性です。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span> &lt;span class="na">data-testid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;chat-input-send&amp;#34;&lt;/span> &lt;span class="na">aria-label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;メッセージを送信&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>aria-label&lt;/code>の日本語はローカライズで変わるかもしれませんが、&lt;code>data-testid&lt;/code>は当時のDOMで識別に使える値でした。&lt;/p>
&lt;p>とはいえ、画面のどこかにある送信ボタンを単純に&lt;code>querySelector()&lt;/code>して押すのは危険です。過去のメッセージ編集欄など、別の入力欄が開いている可能性があります。&lt;/p>
&lt;p>現在の実装は、&lt;strong>操作対象のエディターに対応する、表示中の送信ボタンが一意に特定できた場合だけクリック&lt;/strong>します。複数候補があったり、ボタンが無効だったりする場合は送信しません。&lt;/p>
&lt;p>この「曖昧だったら送らない」は、誤送信を防ぐツールとして重要な判断でした。&lt;/p>
&lt;h2 id="4-キー操作の実装">4. キー操作の実装
&lt;/h2>&lt;p>ここからは現在の実装を見ていきます。主要部分は&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/keys.js" target="_blank" rel="noopener">extension/keys.js&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/keys.js" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
にあります。&lt;/p>
&lt;h3 id="4-1-keydownをキャプチャ段階で処理する">4-1. &lt;code>keydown&lt;/code>をキャプチャ段階で処理する
&lt;/h3>&lt;p>拡張機能は&lt;code>window&lt;/code>に&lt;code>keydown&lt;/code>のリスナーを登録し、&lt;code>capture: true&lt;/code>で受け取ります。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// extension/keys.jsの構造を抜粋
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">controller&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AbortController&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">capture&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">signal&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">controller&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">signal&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">selector&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;.tiptap.ProseMirror[contenteditable=&amp;#34;true&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keydown&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span> &lt;span class="k">instanceof&lt;/span> &lt;span class="nx">Element&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">key&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s1">&amp;#39;Enter&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ここでIME状態・キー設定・候補メニューを判定する
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">},&lt;/span> &lt;span class="nx">options&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>キャプチャ段階で判定することで、Claude本来のEnter送信処理よりも前に介入できます。&lt;/p>
&lt;p>なお、キーが入力欄以外にある場合は基本的に何もしません。ダイアログ操作や別のUIにまで干渉するためのツールではないからです。&lt;/p>
&lt;h3 id="4-2-imeの変換中はブラウザー本来の確定操作を残す">4-2. IMEの変換中はブラウザー本来の確定操作を残す
&lt;/h3>&lt;p>特に重要なのが次の判定です。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// extension/keys.jsより要点を抜粋
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">composing&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isComposing&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keyCode&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">229&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">editor&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">view&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">composing&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stopImmediatePropagation&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>判定には複数の情報を使っています。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>判定&lt;/th>
&lt;th>役割&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>compositionstart&lt;/code> / &lt;code>compositionend&lt;/code>&lt;/td>
&lt;td>入力欄単位でIMEの合成状態を追跡&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>event.isComposing&lt;/code>&lt;/td>
&lt;td>キーイベント自身が持つ合成中フラグ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>keyCode === 229&lt;/code>&lt;/td>
&lt;td>IME処理を示すことがある互換情報&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>editor.view.composing&lt;/code>&lt;/td>
&lt;td>Tiptap/ProseMirror側の変換状態&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>変換中はClaude側の送信ハンドラーへの伝播を止めますが、&lt;strong>&lt;code>preventDefault()&lt;/code>は呼びません&lt;/strong>。IME自身による候補確定はそのまま動かしたいからです。&lt;/p>
&lt;p>加えて、&lt;code>compositionend&lt;/code>直後の100ms以内に届いたEnterは抑止する処理もあります。これは変換完了直後のイベントで意図せず改行・送信しないようにするための安全側の対策です。&lt;/p>
&lt;p>100msという値があらゆる日本語IMEで最適だと証明したわけではありません。あくまで今回の実装における保護策であり、IMEやバージョンの組み合わせによって追加検証の余地はあります。&lt;/p>
&lt;h3 id="4-3-改行はtiptap送信は対応するボタン">4-3. 改行はTiptap、送信は対応するボタン
&lt;/h3>&lt;p>IME以外の通常のキー入力であれば、設定に応じて処理を分岐します。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 実装から処理の要点を再構成した例
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">action&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;newline&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">editor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">commands&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setHardBreak&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">action&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;send&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">button&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">sendButtonFor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">button&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disabled&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;aria-disabled&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>改行に&lt;code>setHardBreak()&lt;/code>を使うため、Webエディターの内部状態を無視したDOM直書きや、合成Enterキーの投げ直しをしなくて済みます。&lt;/p>
&lt;p>送信は送信ボタンに任せるので、Claudeの内部送信関数の名前や引数を推測する必要がありません。ただし、今後DOM構造や送信ボタンの識別子が変われば動作しなくなる可能性はあります。&lt;/p>
&lt;h3 id="4-4-コマンドの候補選択を壊さない">4-4. &lt;code>/&lt;/code>コマンドの候補選択を壊さない
&lt;/h3>&lt;p>試作中にもう1つ問題が出ました。&lt;/p>
&lt;p>スラッシュコマンドやメンションの候補一覧を出しているときは、&lt;strong>Enterが「選択する」という操作に使われます&lt;/strong>。何も考えずにすべてのEnterを改行にすると、候補を選べません。&lt;/p>
&lt;p>公開版では、この点を修正しました。TiptapのSuggestion系プラグインの状態と、実際に表示されているメニューを両方確認し、候補が開いているときの通常EnterはClaudeへ渡すようにしています。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// extension/keys.js より
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">active&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">editor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">some&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">suggestion&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="nx">mention&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">editor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">state&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">active&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>実際のコードではこの判定に加えて、&lt;code>[role=&amp;quot;menu&amp;quot;],[role=&amp;quot;listbox&amp;quot;]&lt;/code>の表示状態も確認しています。&lt;/p>
&lt;p>このような細部は、最初にEnterを改行にできた時点では見落としがちです。「想定したキーだけ動く」と「普段の文章入力を邪魔せず使える」は、なかなか別物でした。&lt;/p>
&lt;h2 id="5-一時パッチを毎回実行するわけにもいかない">5. 一時パッチを毎回実行するわけにもいかない
&lt;/h2>&lt;p>ここまでの挙動は、当初DevToolsのConsoleへJavaScriptを入力する一時的な方法で検証しました。&lt;/p>
&lt;p>しかし、ページを読み込み直したら消えてしまいます。毎回コンソールへ貼り付けて使うのでは、さすがに本末転倒です。&lt;/p>
&lt;p>そこで、Claude Desktopに&lt;strong>通常起動時からWebExtensionを読み込ませる方法&lt;/strong>を探しました。&lt;/p>
&lt;h3 id="5-1-使わなかった方式">5-1. 使わなかった方式
&lt;/h3>&lt;p>Electronなら何でもできそうですが、今回のClaudeでは使えない、あるいは使いたくない方法がありました。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>方法&lt;/th>
&lt;th>採用しなかった理由&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>app.asar&lt;/code>の直接改造&lt;/td>
&lt;td>MSIXの署名・整合性や今後の更新を考えると避けたい。公式アプリを改変したくない&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--remote-debugging-port&lt;/code>等を使う起動&lt;/td>
&lt;td>Claude側に関連引数の検査があり、通常起動を拒否する経路を解析で確認。専用ランチャーも必要になる&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>NODE_OPTIONS&lt;/code>による起動フック&lt;/td>
&lt;td>対象バイナリではElectron fuseによって関連機能が無効&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claudeメニューの「拡張機能をインストール」&lt;/td>
&lt;td>MCP/DXT系の仕組みであり、任意のChrome content scriptを読み込む入口とは確認できなかった&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>重要なのは、Electron一般の機能と&lt;strong>このClaude Desktopで実際に使える機能&lt;/strong>を混同しないことです。&lt;/p>
&lt;h3 id="5-2-react_profile1という読み込み経路を発見">5-2. &lt;code>REACT_PROFILE=1&lt;/code>という読み込み経路を発見
&lt;/h3>&lt;p>&lt;code>app.asar&lt;/code>内のJavaScriptを調べていたところ、React DevToolsに関するコードを見つけました。&lt;/p>
&lt;p>当時の対象ビルドでは、起動時に次の環境変数を確認しています。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">REACT_PROFILE=1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>この値が設定されていると、メインウィンドウ生成前に&lt;code>loadReactDevTools()&lt;/code>が実行され、特定の拡張フォルダーを&lt;code>session.defaultSession.loadExtension()&lt;/code>へ渡す構造でした。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">%APPDATA%\Claude\extensions\fmkadmapgofadopljbjfkapdkoienihi\
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>fmkadmapgofadopljbjfkapdkoienihi&lt;/code>はReact Developer Toolsの拡張IDとして使われる文字列です。ただし、今回の仕組みではその名前の&lt;strong>フォルダーを読み込み先として利用している&lt;/strong>だけで、React DevToolsそのものを入れる必要はありません。また、そのフォルダー名と実際に割り当てられるChromium内部の拡張IDが同一であると証明した、という話でもありません。&lt;/p>
&lt;p>この経路のポイントは、&lt;strong>Claudeの実行ファイルを変更しなくて済む&lt;/strong>こと。Windowsのユーザー環境変数と、ユーザーデータに置いた拡張だけで済みます。&lt;/p>
&lt;p>ただし、これは非公開の実装を利用した方法です。Anthropicが将来この経路を削除・変更すれば、当然使えなくなります。&lt;/p>
&lt;h3 id="5-3-mv3拡張にする">5-3. MV3拡張にする
&lt;/h3>&lt;p>最終的な拡張はManifest V3で、2つのcontent scriptを使います。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;manifest_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content_scripts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matches&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://claude.ai/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;js&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;keys.js&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;document_start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;world&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;MAIN&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;all_frames&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matches&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;https://claude.ai/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;js&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;settings-ui.js&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;document_start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;all_frames&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;permissions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;storage&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>※拡張全体の&lt;code>name&lt;/code>、&lt;code>version&lt;/code>、&lt;code>description&lt;/code>などは省略しています。完全な定義は&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/manifest.json" target="_blank" rel="noopener">extension/manifest.json&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/manifest.json" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
を参照してください。&lt;/p>
&lt;p>&lt;code>keys.js&lt;/code>はWebページのJavaScriptと同じ&lt;code>MAIN&lt;/code> worldで動き、TiptapのエディターAPIを呼びます。一方&lt;code>settings-ui.js&lt;/code>は通常の拡張用の実行環境で動き、&lt;code>chrome.storage.local&lt;/code>を使って設定を保存します。&lt;/p>
&lt;p>両者の設定は、ページルートの属性&lt;code>data-claude-enter-settings&lt;/code>とカスタムイベント&lt;code>claude-enter-settings-changed&lt;/code>で受け渡します。保存済みの設定を読み込むまで、未知の送信キーで誤って送信しないようにするガードもあります。&lt;/p>
&lt;p>実装は&lt;code>document_start&lt;/code>から開始します。試作版の&lt;code>document_idle&lt;/code>では起動直後に元のEnter処理が動く余地があったため、公開版で早期読み込みに変更しました。&lt;/p>
&lt;h2 id="6-設定uiもclaudeの画面に置く">6. 設定UIもClaudeの画面に置く
&lt;/h2>&lt;p>キーの割り当てを変えるたびにスクリプトを修正するのは面倒なので、設定UIも作りました。&lt;/p>
&lt;p>最初はClaudeの「ファイル」「編集」のようなネイティブメニューへ項目を追加したかったのですが、今回使うWebExtension側から素直に操作できるものではありません。&lt;/p>
&lt;p>そこで、左サイドバーのプロフィール欄の上に「⌨ キー設定」を追加しています。&lt;/p>
&lt;p>右下へ固定のバッジを表示する試作もありましたが、モデル選択などと重なって邪魔だったので撤去しました。やっぱり、自分用のツールでも普段の画面を邪魔するのは嫌です。&lt;/p>
&lt;p>設定画面では次のことができます。&lt;/p>
&lt;ul>
&lt;li>送信・改行キーの変更&lt;/li>
&lt;li>有効／無効の切り替え&lt;/li>
&lt;li>初期値への復帰&lt;/li>
&lt;li>保存済み設定の読み込み、再起動後の復元&lt;/li>
&lt;li>想定外の画面構造や送信ボタンの判定失敗を知らせる状態表示&lt;/li>
&lt;/ul>
&lt;p>設定画面はShadow DOMを使っており、Claude本体のCSSと衝突しにくいようにしています。また、送信と改行に同じキーを設定しないよう検査しています。&lt;/p>
&lt;p>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/settings-ui.js" target="_blank" rel="noopener">設定UIの実装&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/settings-ui.js" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
を見ると、サイドバーの&lt;code>data-testid=&amp;quot;user-menu-button&amp;quot;&lt;/code>などを手掛かりに挿入位置を確認しています。この部分も当然Claudeの画面構造に依存するため、変更されれば追従が必要です。&lt;/p>
&lt;h2 id="7-実機検証とテストの範囲">7. 実機検証とテストの範囲
&lt;/h2>&lt;p>開発時点では、主にWindowsのClaude Desktopで次を確認しています。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>検証項目&lt;/th>
&lt;th>結果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>IME変換中の確定Enter&lt;/td>
&lt;td>変換確定を維持、誤送信なし&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>全角記号・句読点を含む日本語入力&lt;/td>
&lt;td>実機で基本動作を確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Enterによる改行&lt;/td>
&lt;td>動作確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ctrl+Enterによる送信&lt;/td>
&lt;td>動作確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>別チャットへの移動&lt;/td>
&lt;td>設定したキー操作を維持&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>サイドバーからのキー変更／有効・無効&lt;/td>
&lt;td>動作確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>完全終了・通常起動後の設定保持&lt;/td>
&lt;td>動作確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/&lt;/code>コマンド等の候補選択&lt;/td>
&lt;td>初期の問題を修正し、公開版で対応&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Linux Desktop&lt;/td>
&lt;td>自動試験のみで、実機検証なし&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>また、Node.js上の模擬環境でキーイベント処理をテストし、Playwrightを使って設定UIの表示や保存の振る舞いも確認しています。公開用インストーラーはPowerShellのテストも整備しました。&lt;/p>
&lt;p>ただし、&lt;strong>模擬テストが通ったことと、すべてのIME・すべてのClaudeバージョンで正常に動くことは別&lt;/strong>です。テスト対象外の画面や複数ウィンドウなどでは、想定外の挙動が残る可能性があります。&lt;/p>
&lt;h2 id="8-もう1つツールを作ったら読み込み先が競合した">8. もう1つツールを作ったら、読み込み先が競合した
&lt;/h2>&lt;p>ここまででCtrl+Enter自体は実用になりました。&lt;/p>
&lt;p>ところが、その後に&lt;strong>ChatとCoworkの分離UIを戻す拡張&lt;/strong>も開発しました。&lt;/p>
&lt;p>そのツールも、同じ&lt;code>REACT_PROFILE&lt;/code>経由でClaude DesktopにChrome拡張を読み込ませたい。つまり、&lt;strong>1つしかないReact DevTools用の拡張枠を取り合う&lt;/strong>ことになります。&lt;/p>
&lt;p>せっかく作ったのに、一方をインストールするともう一方が消えるのは困ります。&lt;/p>
&lt;h3 id="8-1-共通ローダーを作る">8-1. 共通ローダーを作る
&lt;/h3>&lt;p>そこで、別プロジェクトとして&lt;a href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">claude-desktop-webext&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-desktop-webext" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-desktop-webext&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
を作りました。&lt;/p>
&lt;p>各ツールは普通のMV3拡張フォルダーをそれぞれ保持し、共通ローダーが1つのManifestにまとめてClaude Desktopへ読ませる構造です。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">claude-ctrl-enter（キー操作） ──┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─&amp;gt; claude-desktop-webext
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">claude-split-ui（分離UI） ─────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1つの生成MV3 manifest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Claude DesktopのReact DevTools読み込み枠
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Windowsでの保存先は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">%LOCALAPPDATA%\ClaudeDesktopWebExt\web-extensions\claude-ctrl-enter\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%LOCALAPPDATA%\ClaudeDesktopWebExt\web-extensions\claude-split-ui\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%APPDATA%\Claude\extensions\fmkadmapgofadopljbjfkapdkoienihi\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↑ ローダーが生成する読み込み先
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Ctrl+Enterは&lt;code>order: 50&lt;/code>、先に起動時の通信をフックするSplit UIは&lt;code>order: 10&lt;/code>にしています。&lt;/p>
&lt;p>ローダーは導入・更新・解除と、バックアップ・ロールバックを担当します。複数拡張のマニフェストや権限を安全に統合できる範囲だけ対応しており、Chrome拡張なら何でも使えるわけではありません。&lt;/p>
&lt;p>似た目的で&lt;a href="https://github.com/lugia19/Claude-WebExtension-Launcher" target="_blank" rel="noopener">Claude-WebExtension-Launcher&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/lugia19/Claude-WebExtension-Launcher" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/lugia19/Claude-WebExtension-Launcher" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">lugia19/Claude-WebExtension-Launcher&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
というプロジェクトもありましたが、あちらは改変したClaudeを使う方式です。こちらは公式のMSIX版Claudeをそのまま使うことを優先しました。&lt;/p>
&lt;h3 id="8-2-既存の設定を引き継げるか">8-2. 既存の設定を引き継げるか
&lt;/h3>&lt;p>Ctrl+Enter &lt;code>0.4.0&lt;/code>から共通ローダーへ移行しています（2026年10月9日の&lt;code>0.4.1&lt;/code>は主にドキュメントとローダーのリリース版への更新です）。&lt;/p>
&lt;p>WindowsのClaude &lt;code>2.31226&lt;/code>では、旧&lt;code>0.3.0&lt;/code>の環境から更新し、保存していたキー設定が残ることを確認しました。さらにSplit UIを同時導入した状態で、分離UI、サイドバーのキー設定、Enter改行、Ctrl+Enter送信がすべて動作しています。&lt;/p>
&lt;p>これで普段使いのClaudeに必要な2つの不満を、同時に解消できました。&lt;/p>
&lt;h2 id="9-aiを使って開発した感想">9. AIを使って開発した感想
&lt;/h2>&lt;p>今回、コードの実装やテスト、調査メモの整理にはChatGPTなどのAIをかなり使いました。&lt;/p>
&lt;p>特に印象的だったのは、&lt;strong>一度で正解の実装が出てくるわけではない&lt;/strong>ことです。&lt;/p>
&lt;p>AutoHotkeyのIME状態判定では、イベントが取得できているというだけでは十分ではありません。&lt;code>IDLE_INFERRED&lt;/code>を「確定している状態」と扱ってしまえば、一番避けたい誤送信につながります。&lt;/p>
&lt;p>Electronの調査でも、最初に検索したバンドルで拡張読み込み経路が見つからなかったからといって、存在しないわけではありませんでした。別のchunkまで調べることで&lt;code>loadReactDevTools()&lt;/code>が見つかっています。&lt;/p>
&lt;p>AIが候補を出し、スクリプトを組み、検証項目を増やす。その結果を実機で確かめて、ダメなら原因を戻って調べる。この繰り返しです。&lt;/p>
&lt;p>Ctrl+Enter自体は小さな機能ですが、入力中のIMEを壊さず、誤送信せず、Claude更新時にも戻せるようにするには、意外と検討することがありました。&lt;/p>
&lt;h2 id="10-注意点と今後">10. 注意点と今後
&lt;/h2>&lt;p>本ツールはClaude Desktop内部の非公開の読み込み経路と、Web画面のDOM・エディターAPIに依存しています。&lt;/p>
&lt;ul>
&lt;li>Anthropic公式の機能ではなく、将来の互換性は保証できません。&lt;/li>
&lt;li>Claudeの更新で&lt;code>REACT_PROFILE&lt;/code>の読み込み経路が使えなくなる可能性があります。&lt;/li>
&lt;li>&lt;code>data-testid&lt;/code>やTiptapのAPIが変更されればキー操作が動かなくなる可能性があります。&lt;/li>
&lt;li>&lt;code>REACT_PROFILE&lt;/code>はユーザー環境変数なので、他の開発環境と競合する場合があります。&lt;/li>
&lt;li>本物のReact DevToolsや未知の拡張が既存の読み込み先にある場合、インストーラーは上書きせず停止する設計です。&lt;/li>
&lt;li>Linux版は配布していますが実機未検証、macOSは未対応です。&lt;/li>
&lt;li>未送信のメッセージがある場合は、導入・解除後の再起動前に保存してください。インストーラーはClaudeを強制終了しません。&lt;/li>
&lt;/ul>
&lt;p>万一動かなくなった場合に備えて、&lt;code>uninstall.bat&lt;/code>と&lt;code>diagnose.bat&lt;/code>を用意しています。最新状況や既知の制約は&lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">GitHub&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
で確認してください。&lt;/p>
&lt;h2 id="まとめ">まとめ
&lt;/h2>&lt;p>「ClaudeでもCtrl+Enterで送信したい」。それだけのつもりで始めたのに、AutoHotkeyでIMEイベントを追い、ElectronのASARを調べ、React DevToolsの読み込み経路を見つけ、WebExtensionを作り、最後は複数拡張を共存させる共通ローダーまで作ることになりました。&lt;/p>
&lt;p>なかなか遠回りした気もしますが、いまは日本語変換も、普通のEnter改行も、Ctrl+Enter送信も違和感なく使えています。&lt;/p>
&lt;p>便利な追加機能というより、&lt;strong>マイナスだった使い勝手をゼロに戻すためのツール&lt;/strong>ですね。&lt;/p>
&lt;p>とはいえ、毎日使う入力欄に余計なストレスがなくなるのは、やっぱり大事だと思います。&lt;/p>
&lt;h3 id="関連リンク">関連リンク
&lt;/h3>&lt;ul>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">Claude Ctrl+Enter（ソース・ダウンロード）&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG：試作から共通ローダー移行まで&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/CHANGELOG.md" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/docs/AI_HANDOFF_JA.md" target="_blank" rel="noopener">初期調査・判断の技術記録&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/docs/AI_HANDOFF_JA.md" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/keys.js" target="_blank" rel="noopener">keys.js：IME判定と送信処理&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/keys.js" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/settings-ui.js" target="_blank" rel="noopener">settings-ui.js：設定画面と永続化&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter/blob/main/extension/settings-ui.js" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">Claude Split UI&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">共通ローダー claude-desktop-webext&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-desktop-webext" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-desktop-webext&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://www.electronjs.org/docs/latest/api/extensions" target="_blank" rel="noopener">Electron公式：Chrome拡張のサポート&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://www.electronjs.org/docs/latest/api/extensions" target="_blank" rel="noopener">
&lt;div class="link-card__thumb link-card__thumb--icon">
&lt;img src="https://www.google.com/s2/favicons?sz=128&amp;amp;domain=electronjs.org" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">Electron公式：Chrome拡張のサポート&lt;/div>
&lt;div class="link-card__desc">electronjs.org&lt;/div>
&lt;div class="link-card__host">www.electronjs.org&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;/ul>
&lt;!-- スクリーンショット候補：サイドバーの「キー設定」画面。本文は画像なしでも理解できる構成です。 --></description></item><item><title>ClaudeのChatとCoworkが統合されて不便なので、分離UIを復活させてみた</title><link>https://w1.zawa-lab.net/p/20261009_claude-split-ui/</link><pubDate>Fri, 09 Oct 2026 21:06:57 +0900</pubDate><guid>https://w1.zawa-lab.net/p/20261009_claude-split-ui/</guid><description>&lt;img src="https://w1.zawa-lab.net/p/20261009_claude-split-ui/image.png" alt="Featured image of post ClaudeのChatとCoworkが統合されて不便なので、分離UIを復活させてみた" />&lt;!-- qiita-sync:slug=20261009_claude-split-ui sha=1f5d3a48406d -->
&lt;h1 id="claudeのchatとcoworkが統合されて不便なので分離uiを復活させてみたfirefoxchromedesktop2026年10月">ClaudeのChatとCoworkが統合されて不便なので、分離UIを復活させてみた【Firefox・Chrome・Desktop／2026年10月】
&lt;/h1>&lt;blockquote>
&lt;p>この記事はChatGPTなどの生成AIを利用して執筆しています。調査・実装にもAIを利用していますが、実際の画面操作や動作検証は手元の環境で行い、確認できた範囲を区別して記載しています。&lt;/p>
&lt;p>&lt;strong>2026年10月9日時点の記録です。&lt;/strong> Anthropic非公式の実験であり、Claudeのアップデートで動作しなくなる可能性があります。&lt;/p>&lt;/blockquote>
&lt;p>ClaudeのChatとCowork、いつの間にか統合されましたね。&lt;/p>
&lt;p>これ、個人的には結構困っていました。普通に質問したいだけなのにCowork側で作業が始まったり、意図していないファイル整理に話が進んだり。単に文章で回答してくれればいいのに、作業環境を立ち上げて調査を始められると、時間も使用量も余計にかかります。スマートフォンからも作業を継続できるのは便利ですが、必要ないときまでそういうセッションを作ってほしいわけではありません。&lt;/p>
&lt;p>ChatはChat、CoworkはCowork。使うかどうかは自分で決めたい。&lt;/p>
&lt;p>ということで、統合前の切り替えUIを戻せないか調べていたら、&lt;strong>Feature Flagを1つ変更するだけで、分離表示に戻せる&lt;/strong>ことが分かりました。拍子抜けするほど簡単でした。……発見した時点では。&lt;/p>
&lt;p>その後、Firefoxでの検証からChrome対応、Claude Desktopへの組み込み、別の自作拡張との共存までやったので、今回はその記録です。&lt;/p>
&lt;p>まず使い方をまとめ、後半で通信解析・実装方法・実機検証について詳しく書きます。とりあえず動かしたいだけの方は前半だけで十分です。&lt;/p>
&lt;p>&lt;img src="https://w1.zawa-lab.net/p/20261009_claude-split-ui/image.png"
width="894"
height="375"
srcset="https://w1.zawa-lab.net/p/20261009_claude-split-ui/image_hu_43d18970466615b7.png 480w, https://w1.zawa-lab.net/p/20261009_claude-split-ui/image_hu_ffefd3171e5fd3d.png 1024w"
loading="lazy"
alt="coworkボタン復活！"
class="gallery-image"
data-flex-grow="238"
data-flex-basis="572px"
>&lt;/p>
&lt;h2 id="最初に結論">最初に結論
&lt;/h2>&lt;p>作成したのは &lt;strong>Claude Split UI&lt;/strong> という非公式の拡張機能です。&lt;/p>
&lt;ul>
&lt;li>統合されていたChat / Coworkの切り替えを、確認済みの環境で分離表示へ戻す&lt;/li>
&lt;li>Firefox・ChromeのWeb版、およびWindows版Claude Desktopで基本動作を確認&lt;/li>
&lt;li>既存のClaudeアカウントやサブスクリプションはそのまま使用&lt;/li>
&lt;li>Claude本体の実行ファイルや&lt;code>app.asar&lt;/code>を書き換えず、無効化・解除できる&lt;/li>
&lt;li>Claude Desktopでは別に作成した&lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">Ctrl+Enter送信ツール&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
とも共存可能&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>GitHub：&lt;/strong> &lt;a href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">zawa356/claude-split-ui&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;br>
&lt;strong>ダウンロード：&lt;/strong> &lt;a href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">最新のReleases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;br>
&lt;strong>日本語README：&lt;/strong> &lt;a href="https://github.com/zawa356/claude-split-ui/blob/main/docs/i18n/README.ja.md" target="_blank" rel="noopener">導入手順と対応状況&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/blob/main/docs/i18n/README.ja.md" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/p>
&lt;p>これは&lt;strong>画面の切り替え方を戻すツール&lt;/strong>です。権限のない機能を有効化するものでも、以前のCoworkのバックエンドや実行環境を完全に復活させるものでもありません。そこは最初に明確にしておきます。&lt;/p>
&lt;h3 id="確認した環境">確認した環境
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>対象&lt;/th>
&lt;th>2026年10月9日時点の確認結果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Firefox 157.0.1&lt;/td>
&lt;td>一時拡張の導入で分離、削除して再読込すると統合表示に復帰&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chrome&lt;/td>
&lt;td>WXT版拡張の手動導入・無効化による分離／復帰を確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude Desktop / Windows 11&lt;/td>
&lt;td>Microsoft Store（MSIX）版 &lt;code>2.31226.0.0&lt;/code> で分離／復帰を確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude Desktop / Linux&lt;/td>
&lt;td>配布・CI上の検査はあるが、実機動作は未確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>macOS&lt;/td>
&lt;td>今回のDesktop用配布の対象外&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>すべてのアカウントや将来のバージョンで動くという意味ではありません。&lt;/p>
&lt;h2 id="導入方法">導入方法
&lt;/h2>&lt;h3 id="firefoxの場合">Firefoxの場合
&lt;/h3>&lt;ol>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">Releases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
から&lt;code>claude-split-ui-&amp;lt;バージョン&amp;gt;-firefox.zip&lt;/code>を取得して展開します。&lt;/li>
&lt;li>アドレスバーに&lt;code>about:debugging#/runtime/this-firefox&lt;/code>と入力します。&lt;/li>
&lt;li>「一時的なアドオンを読み込む」から、展開したフォルダー内の&lt;code>manifest.json&lt;/code>を選択します。&lt;/li>
&lt;li>&lt;a href="https://claude.ai/new" target="_blank" rel="noopener">Claude Web&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://claude.ai/new" target="_blank" rel="noopener">
&lt;div class="link-card__thumb link-card__thumb--icon">
&lt;img src="https://www.google.com/s2/favicons?sz=128&amp;amp;domain=claude.ai" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">Claude Web&lt;/div>
&lt;div class="link-card__desc">claude.ai&lt;/div>
&lt;div class="link-card__host">claude.ai&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
を再読み込みします。&lt;/li>
&lt;/ol>
&lt;p>うまくいけば、ChatとCoworkの切り替えが表示されます。&lt;/p>
&lt;p>&lt;strong>注意：&lt;/strong> 現在のFirefox版は一時インストールによる開発者向けの配布です。Firefoxを再起動すると一時アドオンは解除されます。AMOで署名された正式なアドオンではありません。&lt;/p>
&lt;p>解除するときは&lt;code>about:debugging&lt;/code>から一時アドオンを削除し、Claudeのページを再読み込みしてください。&lt;/p>
&lt;h3 id="chromeの場合">Chromeの場合
&lt;/h3>&lt;ol>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">Releases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
から&lt;code>claude-split-ui-&amp;lt;バージョン&amp;gt;-chrome.zip&lt;/code>を取得し、展開します。&lt;/li>
&lt;li>&lt;code>chrome://extensions/&lt;/code>を開き、デベロッパーモードを有効にします。&lt;/li>
&lt;li>「パッケージ化されていない拡張機能を読み込む」で、&lt;code>manifest.json&lt;/code>があるフォルダーを選択します。&lt;/li>
&lt;li>Claude Webを再読み込みします。&lt;/li>
&lt;/ol>
&lt;p>元に戻す場合は拡張を無効化または削除して再読み込みします。Chrome Web Storeでの配布ではありません。&lt;/p>
&lt;h3 id="claude-desktopwindowsの場合">Claude Desktop（Windows）の場合
&lt;/h3>&lt;ol>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">Releases&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/releases/latest" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
から&lt;code>claude-split-ui-&amp;lt;バージョン&amp;gt;-desktop.zip&lt;/code>を取得して展開します。&lt;/li>
&lt;li>中の&lt;code>install.bat&lt;/code>を実行します。管理者権限は不要です。&lt;/li>
&lt;li>Claudeを&lt;strong>タスクトレイから完全に終了&lt;/strong>させ、通常のアイコンから起動し直します。&lt;/li>
&lt;li>入力欄の「＋」付近にChat / Coworkの切り替えが出るか確認します。&lt;/li>
&lt;/ol>
&lt;p>&lt;code>uninstall.bat&lt;/code>で解除できます。解除後もClaudeを完全終了・再起動してください。診断は&lt;code>diagnose.bat&lt;/code>、読み込み枠の再生成は&lt;code>repair.bat&lt;/code>です。&lt;/p>
&lt;p>導入スクリプトの詳細や最新の制約はREADMEを優先してください。特に以前のバージョンから更新する場合は、古い解除スクリプトではなく新しい配布物を使用します。&lt;/p>
&lt;hr>
&lt;h2 id="そもそもなぜ戻したかったのか">そもそも、なぜ戻したかったのか
&lt;/h2>&lt;p>今回の不満は「新しい画面が気に入らない」という見た目だけの話ではありません。&lt;/p>
&lt;p>自分の使い方だと、普通のChatは技術的な質問や調べ物、文章の相談をする場所。一方Coworkは、ファイルを扱わせたりPC上で作業させたりする必要があるときに、明示的に利用したい機能です。&lt;/p>
&lt;p>この2つが統合されると、ちょっとした質問でもCowork的な作業に移行することがありました。場合によっては頼んでいないファイル整理の話まで進んでしまい、「いや、そこまでしなくていいんだけど……」という状態になります。&lt;/p>
&lt;p>Cowork自体が嫌いなわけではありません。実際、必要なときには便利です。ただ、使い分けるための境界がほしい。&lt;/p>
&lt;p>それに、たまたま別の&lt;strong>Claude Teamアカウント&lt;/strong>で確認したところ、そちらには従来の分離UIが残っていました。&lt;/p>
&lt;p>同じClaudeなのに、アカウントによって画面が違う。だったら旧UIのコードが完全に削除されたわけではなく、機能フラグで表示を切り替えている可能性があるのでは？&lt;/p>
&lt;p>ここから調査が始まります。&lt;/p>
&lt;h2 id="1-まずfirefoxで通信を調べる">1. まずFirefoxで通信を調べる
&lt;/h2>&lt;p>ElectronのDesktopアプリを最初から改造するよりも、Web版をブラウザーの開発者ツールで調べた方が楽です。ということで、Firefoxから始めました。&lt;/p>
&lt;p>Firefoxの開発者ツールに慣れていたわけではないので、このあたりはChatGPTに調査を手伝ってもらいながら進めています。&lt;/p>
&lt;h3 id="1-1-network-monitorで起動時の通信を取得">1-1. Network Monitorで起動時の通信を取得
&lt;/h3>&lt;p>再現したい場合は、次のような手順です。&lt;/p>
&lt;ol>
&lt;li>統合UIになっているアカウントで&lt;code>https://claude.ai/new&lt;/code>を開きます。&lt;/li>
&lt;li>&lt;code>F12&lt;/code>で開発者ツールを開き、「ネットワーク」タブへ移動します。&lt;/li>
&lt;li>必要に応じてキャッシュを無効化し、通信ログをクリアしてページを再読み込みします。&lt;/li>
&lt;li>&lt;code>app_start&lt;/code>という文字列を含むリクエストを探します。&lt;/li>
&lt;li>レスポンスのJSONと、ページHTMLの起動時スクリプトを確認します。&lt;/li>
&lt;/ol>
&lt;p>実際の検証では、次のような同一オリジンのAPIが呼ばれていました。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">GET https://claude.ai/edge-api/bootstrap/&amp;lt;organization-uuid&amp;gt;/app_start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ?statsig_hashing_algorithm=djb2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;amp;growthbook_format=sdk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;amp;...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>ここでの&lt;code>&amp;lt;organization-uuid&amp;gt;&lt;/code>は説明用の伏せ字です。実際のアカウントのIDを公開する必要はありません。また、URLに付くパラメーターは将来変更される可能性があります。&lt;/p>
&lt;p>最初はHARも取得して内容をAIと一緒に分析しました。ただし、&lt;strong>HARにはCookie、認証ヘッダー、各種IDなどが含まれる可能性があります&lt;/strong>。そのまま公開したり、Issueへ貼ったりするのは避けてください。AIへ渡す場合も、サービスや利用環境の取り扱い条件を確認し、必要な範囲に絞るべきです。&lt;/p>
&lt;h3 id="1-2-growthbookのfeature-flagを見つける">1-2. GrowthBookのFeature Flagを見つける
&lt;/h3>&lt;p>起動時のJSONを調べると、GrowthBook形式のFeature定義の中に、数値のキー&lt;code>1174351393&lt;/code>が見つかりました。&lt;/p>
&lt;p>実データから対象部分だけを抜き出して簡略化すると、構造は次のようなものです。&lt;strong>これは説明用のサンプルであり、実際のレスポンス全文ではありません。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;1174351393&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;defaultValue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;force&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>一見すると、&lt;code>defaultValue&lt;/code>だけ&lt;code>false&lt;/code>にすればよさそうです。ただし、&lt;code>rules&lt;/code>には&lt;code>force&lt;/code>という値があります。デフォルトを変更しても、適用中のルールによって別の値が選ばれる可能性があります。&lt;/p>
&lt;p>そこで&lt;code>defaultValue&lt;/code>だけでなく、Boolean型の&lt;code>rules[].force&lt;/code>も変更して挙動を見ることにしました。&lt;/p>
&lt;h3 id="1-3-network-overrideで本当に切り替わるか試す">1-3. Network Overrideで本当に切り替わるか試す
&lt;/h3>&lt;p>FirefoxのNetwork Monitorには、サーバーのレスポンスをローカルファイルに差し替える&lt;strong>Network Override&lt;/strong>があります。Firefox 137以降で使える機能です。&lt;/p>
&lt;p>調査時には次の流れで試しました。&lt;/p>
&lt;ol>
&lt;li>Network Monitorで対象の&lt;code>app_start&lt;/code>リクエストを右クリックします。&lt;/li>
&lt;li>&lt;strong>Set Network Override&lt;/strong>を選び、レスポンス内容をローカルファイルへ保存します。&lt;/li>
&lt;li>そのファイルの対象Featureだけを編集します。&lt;/li>
&lt;li>&lt;code>defaultValue&lt;/code>と&lt;code>rules[].force&lt;/code>を&lt;code>false&lt;/code>にして保存します。&lt;/li>
&lt;li>ページを再読み込みしてUIを確認します。&lt;/li>
&lt;/ol>
&lt;p>Firefoxの操作自体は&lt;a href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/network_overrides/index.html" target="_blank" rel="noopener">MozillaのNetwork Overrides解説&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/network_overrides/index.html" target="_blank" rel="noopener">
&lt;div class="link-card__thumb link-card__thumb--icon">
&lt;img src="https://www.google.com/s2/favicons?sz=128&amp;amp;domain=firefox-source-docs.mozilla.org" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">MozillaのNetwork Overrides解説&lt;/div>
&lt;div class="link-card__desc">firefox-source-docs.mozilla.org&lt;/div>
&lt;div class="link-card__host">firefox-source-docs.mozilla.org&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
にも記載があります。Overrideはタブ単位で、開発者ツールを閉じれば永続化されません。&lt;/p>
&lt;p>対象部分は次の状態にします。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;1174351393&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;defaultValue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;force&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>もちろん実際は、&lt;strong>レスポンス全体を保ったまま対象部分だけ書き換えます&lt;/strong>。サンプルJSONだけをレスポンスとして返してもClaudeは起動できません。&lt;/p>
&lt;p>すると、分離されていた頃のChat / Cowork切り替えが表示されました。&lt;/p>
&lt;p>えっ、これだけ？&lt;/p>
&lt;p>と思いましたが、偶然の表示崩れではないかを確認する必要があります。&lt;/p>
&lt;h3 id="1-4-ababで検証する">1-4. A/B/A/Bで検証する
&lt;/h3>&lt;p>同じアカウント・同じブラウザーで値を切り替え、結果を記録しました。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>試行&lt;/th>
&lt;th>&lt;code>defaultValue&lt;/code>&lt;/th>
&lt;th>&lt;code>rules[].force&lt;/code>&lt;/th>
&lt;th>画面&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A：変更前&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;td>統合UI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B：両方変更&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>&lt;strong>分離UI&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A相当：ルールを戻す&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;td>統合UI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B：再度変更&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>&lt;strong>分離UI&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>この結果から、少なくとも&lt;strong>当時のアカウントとビルドでは、対象Featureのルール設定がUI切り替えに効いている&lt;/strong>と判断できます。&lt;/p>
&lt;p>ちなみに&lt;code>classic_mode_available&lt;/code>や&lt;code>showCoworkTab&lt;/code>といった、それらしい名前の値もありました。しかし、それらが今回の表示を決める要因だと立証できたわけではありません。成功した変更はあくまで&lt;code>1174351393&lt;/code>のFeature定義に限定しています。&lt;/p>
&lt;p>Coworkを選ぶと&lt;code>/cowork/&lt;/code>へ移動し、Cowork用の画面やタスク一覧が表示されました。簡単なMarkdownファイル作成も実行できました。ただし、この結果だけで「統合前のCowork実行基盤まで完全に戻った」とは言えません。&lt;strong>確認できたのは主にUIの分離です。&lt;/strong>&lt;/p>
&lt;h2 id="2-一時的な差し替えからwebextensionへ">2. 一時的な差し替えからWebExtensionへ
&lt;/h2>&lt;p>Network Overrideでは、開発者ツールを開いてファイルを管理し続ける必要があります。毎回使うものとしてはさすがに面倒です。&lt;/p>
&lt;p>そこで、レスポンスをブラウザー内で動的に加工するWebExtensionを作りました。&lt;/p>
&lt;h3 id="2-1-なぜdocument_startなのか">2-1. なぜ&lt;code>document_start&lt;/code>なのか
&lt;/h3>&lt;p>最初に引っかかったのが、差し込むタイミングです。&lt;/p>
&lt;p>HTMLを調べると、Claudeはアプリ本体の大きなJavaScriptが起動する前に、インラインスクリプトからbootstrapの取得を始めていました。&lt;/p>
&lt;p>概念的には次の構造です。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Claudeの動作を説明するための擬似コード（原文の転載ではありません）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">__BOOTSTRAP_PRELOAD__&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">promise&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/edge-api/bootstrap/.../app_start&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">credentials&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;include&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>DOMContentLoaded&lt;/code>を待ってから&lt;code>fetch&lt;/code>をフックしても、もう遅い。そこでManifest V3のcontent scriptを、&lt;strong>&lt;code>document_start&lt;/code>かつ&lt;code>MAIN&lt;/code> world&lt;/strong>で実行します。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// apps/browser-extension/entrypoints/claude.content.ts より
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="nx">defineContentScript&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">matches&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;https://claude.ai/*&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">runAt&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;document_start&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">world&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;MAIN&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">allFrames&lt;/span>: &lt;span class="kt">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">main() {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">installFetchHook&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>普通の拡張機能のcontent scriptは、WebページとJavaScriptの実行環境が分離されています。今回はページ本体が呼ぶ&lt;code>window.fetch&lt;/code>を置き換える必要があるため、同じ実行環境となる&lt;code>MAIN&lt;/code>を使用しています。&lt;/p>
&lt;h3 id="2-2-何でも書き換えない">2-2. 何でも書き換えない
&lt;/h3>&lt;p>ここでやりたかったのは、Claudeの通信を横取りすることではありません。&lt;strong>最新の正規レスポンスから、目的のFeatureだけをその場で変更すること&lt;/strong>です。&lt;/p>
&lt;p>現在の実装は、おおむね次の手順で動きます。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Claudeのページがfetch()を呼ぶ
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ app_start以外 → 元のfetchをそのまま実行
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 同一オリジンのapp_start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 正規のレスポンスを取得
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> JSONをコピーしてFeature 1174351393を探す
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 形式が違う／対象なし／取得失敗 → 元のレスポンスを返す
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 対象あり
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> defaultValue=false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Boolean型のrules[].force=false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 変更したJSONをResponseとして返す
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>実装の核は&lt;a href="https://github.com/zawa356/claude-split-ui/blob/main/packages/core/src/index.mjs" target="_blank" rel="noopener">packages/core/src/index.mjs&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/blob/main/packages/core/src/index.mjs" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
です。&lt;/p>
&lt;p>たとえばFeatureの変更部分は、実際には次のような処理になっています（要点を抜粋）。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">copy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">structuredClone&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">root&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">visitDefinitions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">copy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">definition&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">definition&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defaultValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">rule&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">definition&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rules&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">rule&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="k">typeof&lt;/span> &lt;span class="nx">rule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;boolean&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">rule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rule&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>visitDefinitions&lt;/code>側でも対象キーとデータ型を検査しています。変更対象でない通信・項目はそのまま通し、想定外のJSONなら加工しません。取得した認証情報やレスポンス全文を保存・外部送信する機能も設けていません。&lt;/p>
&lt;p>余談ですが、Network Override用に取得した実際のbootstrap JSONを、拡張機能の中へ固定データとして同梱するのは避けています。ユーザーや時点によって異なる設定を巻き込むうえ、認証情報が混ざる危険もあるからです。&lt;/p>
&lt;h3 id="2-3-firefoxからchromeへ">2-3. FirefoxからChromeへ
&lt;/h3>&lt;p>最初にFirefox用のManifest V3によるPoCを作り、&lt;strong>拡張を入れる→分離する→外す→統合に戻る&lt;/strong>というA/B/Aを確認しました。Network Overrideを解除した状態で試験しているので、前段の差し替えが残っていたわけではありません。&lt;/p>
&lt;p>その後、WXTを使ってFirefoxとChromeのビルドを共通化。変換処理は&lt;code>packages/core&lt;/code>に切り出し、ブラウザーごとの拡張から同じ処理を呼び出す形にしました。&lt;/p>
&lt;p>単体テストや模擬ブラウザーでの確認に加えて、Firefox・Chromeの両方で実際に拡張を読み込み、分離表示と解除後の復帰を確認しています。&lt;/p>
&lt;p>なお、CIが通ったからといってClaude Webとの組み合わせが保証されるわけではありません。この点は実機試験と区別しています。&lt;/p>
&lt;h2 id="3-desktop版にも適用する">3. Desktop版にも適用する
&lt;/h2>&lt;p>さて、ここまで来るとDesktop版でも使いたくなります。&lt;/p>
&lt;p>実は、この少し前に別の不満から&lt;strong>Claude DesktopのEnterキー動作を変更するツール&lt;/strong>を開発していました。その過程で、Claude Desktop内部にあるReact DevTools用の拡張読み込み経路を見つけていたのです。&lt;/p>
&lt;p>ここが今回のDesktop対応に効きました。&lt;/p>
&lt;h3 id="3-1-web版のclaudeがそのまま載っている">3-1. Web版のClaudeがそのまま載っている
&lt;/h3>&lt;p>Windows版Claude DesktopはElectron製です。&lt;code>app.asar&lt;/code>を&lt;strong>コピーして読み取り専用で解析&lt;/strong>したところ、通常のチャット画面では&lt;code>https://claude.ai&lt;/code>を表示する構成が確認できました。&lt;/p>
&lt;p>したがって、Claude Desktopのメインプロセスを改造するより、実際のWeb画面へChrome用のcontent scriptを読み込ませる方が変更範囲を小さくできます。&lt;/p>
&lt;h3 id="3-2-react_profile1とreact-devtoolsの読み込み枠">3-2. &lt;code>REACT_PROFILE=1&lt;/code>とReact DevToolsの読み込み枠
&lt;/h3>&lt;p>対象ビルドには、起動時に&lt;code>REACT_PROFILE&lt;/code>環境変数を確認し、&lt;code>1&lt;/code>なら&lt;code>loadReactDevTools()&lt;/code>を実行する分岐がありました。&lt;/p>
&lt;p>さらに、所定の拡張フォルダーが存在すると、Electronの&lt;code>session.defaultSession.loadExtension()&lt;/code>で読み込む実装になっています。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ユーザー環境変数 REACT_PROFILE=1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Claude Desktopの起動処理
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">React DevTools用の拡張フォルダーを確認
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%APPDATA%\Claude\extensions\fmkadmapgofadopljbjfkapdkoienihi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Electron session.defaultSession.loadExtension()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">claude.ai のWebContentsへcontent scriptを適用
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>これはClaudeの&lt;strong>非公開の開発用読み込み経路を利用したもの&lt;/strong>で、公式の「任意のChrome拡張機能をインストールする機能」ではありません。React DevToolsのIDは読み込み先のフォルダー名として利用していますが、React DevToolsそのものを同梱しているわけではありません。&lt;/p>
&lt;p>また、&lt;code>REACT_PROFILE&lt;/code>はユーザー環境変数です。他の用途と競合する場合があるため、既存設定や本物のReact DevToolsがある環境では、インストーラーが勝手に上書きしないようにしています。&lt;/p>
&lt;h3 id="3-3-windows実機でabaを確認">3-3. Windows実機でA/B/Aを確認
&lt;/h3>&lt;p>Windows 11、Claude Desktop &lt;code>2.31226.0.0&lt;/code>（Electron &lt;code>44.4.3&lt;/code>）で検証しました。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>状態&lt;/th>
&lt;th>結果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>元の状態（Ctrl+Enter拡張のみ）&lt;/td>
&lt;td>統合UI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Split UIのcontent scriptを追加&lt;/td>
&lt;td>&lt;strong>分離UI&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>元の構成に戻す&lt;/td>
&lt;td>統合UI&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>差し替え時には、フックの内部カウンターで&lt;code>intercepted: 1&lt;/code>、&lt;code>patched: 1&lt;/code>、&lt;code>modifiedRules: 1&lt;/code>も確認しています。Ctrl+Enter側の動作は維持されていました。&lt;/p>
&lt;p>これでWebとDesktopの両方で、同じ基本処理を利用できると確認できました。&lt;/p>
&lt;h2 id="4-ところが拡張を2つ入れられない">4. ところが、拡張を2つ入れられない
&lt;/h2>&lt;p>ここで別の問題が発覚します。&lt;/p>
&lt;p>ClaudeのReact DevTools用の読み込み先は、実質的に&lt;strong>1つの拡張フォルダー&lt;/strong>です。つまりSplit UIとCtrl+Enterがそれぞれ好き勝手に同じ場所へ書き込むと、後から入れたツールが前のツールを上書きしてしまいます。&lt;/p>
&lt;p>かといってClaudeの実行ファイルや&lt;code>app.asar&lt;/code>を書き換える方式にはしたくありません。Microsoft Store版のMSIXは署名や配置の都合もあり、公式アプリを改造して別物を常用するのは避けたかったのです。&lt;/p>
&lt;p>そこで作ったのが、3つ目のリポジトリである**&lt;a href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">claude-desktop-webext&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-desktop-webext" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-desktop-webext&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
**です。&lt;/p>
&lt;h3 id="4-1-共通ローダーの仕組み">4-1. 共通ローダーの仕組み
&lt;/h3>&lt;p>各ツールは、自分の拡張だけを個別のフォルダーで保持します。ローダーはそれらの&lt;code>manifest.json&lt;/code>を検査し、&lt;code>content_scripts&lt;/code>や必要な権限をまとめた&lt;strong>1つの生成マニフェスト&lt;/strong>を作成します。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">%LOCALAPPDATA%\ClaudeDesktopWebExt\web-extensions\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ claude-split-ui\ ← 個別のMV3拡張
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ claude-ctrl-enter\ ← 個別のMV3拡張
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓ 検証・統合（順序制御）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%APPDATA%\Claude\extensions\fmkadmapgofadopljbjfkapdkoienihi\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ manifest.json ← ローダーが生成
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ ext\... ← 各拡張のスクリプト
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Claude Desktop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Split UIは起動直後の&lt;code>fetch&lt;/code>を先回りしてフックしなければならないため、実行順を&lt;code>10&lt;/code>、Ctrl+Enterを&lt;code>50&lt;/code>に設定しています。&lt;/p>
&lt;p>スロットのバックアップ、導入・解除時の状態管理、失敗時のロールバックもローダー側で行います。ツール同士が相手のファイルを直接触らずに済むのが大きいところです。&lt;/p>
&lt;p>すでに似た目的で&lt;a href="https://github.com/lugia19/Claude-WebExtension-Launcher" target="_blank" rel="noopener">Claude-WebExtension-Launcher&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/lugia19/Claude-WebExtension-Launcher" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/lugia19/Claude-WebExtension-Launcher" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">lugia19/Claude-WebExtension-Launcher&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
という先行プロジェクトもあります。ただし、こちらは改変したClaudeアプリを使う別方式。今回は公式のClaude Desktopをそのまま使いたかったので、読み込み先を共有するローダーを別に作りました。&lt;/p>
&lt;p>ちなみに、このローダーは&lt;strong>任意のChrome拡張を無条件に読み込める万能ランチャーではありません&lt;/strong>。安全に1つのManifestへ統合できる、対応したMV3 content scriptに範囲を限定しています。&lt;/p>
&lt;h3 id="4-2-共存まで検証">4-2. 共存まで検証
&lt;/h3>&lt;p>WindowsのClaude &lt;code>2.31226&lt;/code>では、旧版のCtrl+Enter単独構成から共通ローダーへ移行したうえで、Split UIを追加しました。&lt;/p>
&lt;p>その後、実機で次を確認しています。&lt;/p>
&lt;ul>
&lt;li>Chat / Coworkの分離切り替えが表示される&lt;/li>
&lt;li>Ctrl+Enterの「キー設定」がサイドバーに表示される&lt;/li>
&lt;li>保存してあったキー設定が残る&lt;/li>
&lt;li>Enter改行とCtrl+Enter送信が引き続き動作する&lt;/li>
&lt;/ul>
&lt;p>これで、自分の使いたかった環境が一通り揃いました。&lt;/p>
&lt;h2 id="5-aiをどう使ったか">5. AIをどう使ったか
&lt;/h2>&lt;p>今回、実装の多くはAIの支援を受けて進めています。いわゆるバイブコーディングに近い進め方です。&lt;/p>
&lt;p>ただし「AIに一言頼んだら完成した」という感じではありません。&lt;/p>
&lt;p>調査の入口ではFirefoxの操作やHARの整理を手伝ってもらい、実装段階ではFeatureの変換ロジックやWebExtension化、Windows版との共通化、テストの構築などにAIを使いました。一方で、&lt;strong>分離したか、解除して元に戻るか、Ctrl+Enterと同時に使えるか&lt;/strong>は実際の環境で確認しています。&lt;/p>
&lt;p>また、途中には「Desktop版は&lt;code>app.asar&lt;/code>へパッチを当てればよいのでは」という設計案もありました。しかし、すでに別プロジェクトで見つけていた拡張の読み込み経路を使う方が、目的にも制約にも合っていました。技術的に可能そうな方法と、常用したい方法は別です。&lt;/p>
&lt;p>今回の実装で大切だったのは、コードを書く量よりも&lt;strong>何を変更したら何が変わったのかを切り分けること&lt;/strong>だった気がします。&lt;/p>
&lt;h2 id="6-制約注意点">6. 制約・注意点
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>項目&lt;/th>
&lt;th>注意点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>非公式実装&lt;/td>
&lt;td>Anthropicとは無関係。非公開のフラグとDesktopの読み込み経路に依存&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>動作範囲&lt;/td>
&lt;td>検証できたのは主に分離UIと画面遷移。旧バックエンドの完全復活ではない&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Feature Flag&lt;/td>
&lt;td>&lt;code>1174351393&lt;/code>が変更・削除されれば動かなくなる可能性あり&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Desktop&lt;/td>
&lt;td>確認済みはWindows版&lt;code>2.31226&lt;/code>。Linux実機は未確認&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ブラウザー配布&lt;/td>
&lt;td>現在は手動インストール。ストア署名済み拡張ではない&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>既存の拡張&lt;/td>
&lt;td>React DevToolsなど同じ読み込み枠を使うものとは競合し得る&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>認証情報&lt;/td>
&lt;td>HARやbootstrapレスポンス原本をそのまま公開・共有しない&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>利用する場合は配布物の内容を確認し、動かなければ解除してください。将来のメンテナンスは続けるつもりですが、動作を保証できるものではありません。&lt;/p>
&lt;h2 id="まとめ">まとめ
&lt;/h2>&lt;p>個人アカウントとClaude TeamでUIが違うことから調べ始め、FirefoxのNetwork OverrideでFeature Flagを変更したら、Chat / Coworkを分離できました。&lt;/p>
&lt;p>WebExtensionへ移してFirefox・Chromeに対応させ、さらに以前作ったCtrl+Enter拡張の仕組みを利用してWindows版Claude Desktopでも動かせました。最後は1つしかない拡張読み込み枠を共有するためのローダーまで作ることに。ちょっとした不満の解消にしては、ずいぶん大掛かりです。&lt;/p>
&lt;p>とはいえ、使ってみた感想は「すごく便利になった！」というより、&lt;strong>やっと普通に使えるようになった&lt;/strong>という感じです。自分が使いたいときだけCoworkを選べる。それだけの話ですが、自分にとってはそこが重要でした。&lt;/p>
&lt;p>同じように統合UIが合わない方の参考になれば幸いです。&lt;/p>
&lt;h3 id="関連リンク">関連リンク
&lt;/h3>&lt;ul>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">Claude Split UI（ソース・ダウンロード）&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui/blob/main/docs/research/2026-10-09-feature-flag-analysis.md" target="_blank" rel="noopener">Feature Flagの調査記録&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/blob/main/docs/research/2026-10-09-feature-flag-analysis.md" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude-split-ui/blob/main/docs/research/2026-10-09-desktop-poc.md" target="_blank" rel="noopener">DesktopでのA/B/A検証記録&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-split-ui/blob/main/docs/research/2026-10-09-desktop-poc.md" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-split-ui" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-split-ui&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">WebExtension共通ローダー&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude-desktop-webext" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude-desktop-webext" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude-desktop-webext&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">Claude Ctrl+Enter&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://github.com/zawa356/claude_ctrl-enter" target="_blank" rel="noopener">
&lt;div class="link-card__thumb">
&lt;img src="https://opengraph.githubassets.com/1/zawa356/claude_ctrl-enter" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">zawa356/claude_ctrl-enter&lt;/div>
&lt;div class="link-card__desc">GitHub Repository&lt;/div>
&lt;div class="link-card__host">github.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/network_overrides/index.html" target="_blank" rel="noopener">Firefox公式：Network Overrides&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/network_overrides/index.html" target="_blank" rel="noopener">
&lt;div class="link-card__thumb link-card__thumb--icon">
&lt;img src="https://www.google.com/s2/favicons?sz=128&amp;amp;domain=firefox-source-docs.mozilla.org" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">Firefox公式：Network Overrides&lt;/div>
&lt;div class="link-card__desc">firefox-source-docs.mozilla.org&lt;/div>
&lt;div class="link-card__host">firefox-source-docs.mozilla.org&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;li>&lt;a href="https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts" target="_blank" rel="noopener">Chrome公式：Content scripts&lt;/a>&lt;div class="link-card">
&lt;a class="link-card__inner" href="https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts" target="_blank" rel="noopener">
&lt;div class="link-card__thumb link-card__thumb--icon">
&lt;img src="https://www.google.com/s2/favicons?sz=128&amp;amp;domain=developer.chrome.com" alt="Preview image" loading="lazy" />
&lt;/div>
&lt;div class="link-card__body">
&lt;div class="link-card__title">Chrome公式：Content scripts&lt;/div>
&lt;div class="link-card__desc">developer.chrome.com&lt;/div>
&lt;div class="link-card__host">developer.chrome.com&lt;/div>
&lt;/div>
&lt;/a>
&lt;/div>
&lt;/li>
&lt;/ul>
&lt;!-- スクリーンショット候補：統合UIと分離UIの比較。文章だけでも読めるため必須ではありません。 --></description></item></channel></rss>