Skip to content

5 development

Claude Lin & Lay edited this page Oct 18, 2026 · 1 revision

開発・テスト・配布

フロントエンドの責務(#379)

src/main.ts は画面の組み立てと部屋・トピック・発言の連携を持つ。席の表示語と使用量の判定は src/seat-status.ts の純粋関数が持ち、部屋が返事を待っているかは呼び出し側が渡す。端末、選択中の席、トピックごとの選択、席の読み戻し、PTY の購読と破棄、出力の静けさを測るタイマー、起動・再開・終了、席の統計と活動通知の適用は src/session-controller.ts の factory が所有する。外へ渡すのは照会と操作であり、内部の Map は渡さない。破棄済みの席へ遅れて届く PTY の callback と、購読待ちの間に破棄された席の listener もここで拒否・解除する。

src/account-dialog.ts は編集中の draft、環境変数の描画、avatar の仮編集、character ファイルの競合確認、起動プレビュー、MCP の欄とフォームの wiring を所有する。保存するアカウント一覧は main の設定であり、必要な読み書きと再描画は明示した依存で渡す。表示倍率と文字サイズの保存・復元・キー操作は src/display-settings.ts が持ち、端末の文字サイズは session 側の操作へ渡す。共通の型は src/contracts.ts に置く。各 factory は main を import せず、import 自体は DOM に触らない。wiring は main の既存の初期化順で呼ぶ。

sidecar/test/claude-limit-ui.test.mjs は tsx の tsImport で状態判定の実モジュールを読む。frontend-lifecycle.test.mjs も実モジュールを読み、明示した端末・イベント・DOM の境界を代役にして、起動中のトピック切替、購読の順序と破棄、古い PTY の通知拒否、キャンセル後の環境変数読み戻しの拒否、表示設定の復元と保存を確かめる。キーと描画の実機確認は別に必要であり、このテストは実 CLI や Tauri を起動しない。

Codex の再現チェック(#272)

通常 CI はモデル/利用者認証を使わない。mcp-config は Codex の argv、設定の保持、hook、native ID 照合、履歴探索、profile/cwd/linked worktree 境界を検証する。codex-session.test.mjs は callback と起動直後の retry、親 ID を使わないことを確認する。sidecar 往復テストは同一アカウントの別トピック登録を接続しないケースも含む。旧 PULLCEPT_CHARACTER に長い自由文が残っていても MCP に入らないこと、SDK initialize の instructions の先頭512文字に部屋への投稿方法と返信/履歴の二道具が入ること、作法全文が 1024 字以内であること(#273、#276)を確認する(公式 MCP 案内)。

実バイナリの discovery は opt-in である。PULLCEPT_TEST_CODEX_BIN に隔離 0.160.0 以上の実行ファイルを指定し、node --test sidecar/test/codex-runtime.test.mjs を実行する。新しい一時 CODEX_HOME と fixture のみを使い、thread/turn を作らずモデルを呼ばない。Windows では空白を含むパスの .cmd shim 経由の起動も確認する。通常の npm run sidecar:test では skip する。

キャラ選択の native 検証は同じ PULLCEPT_TEST_CODEX_BIN で node --test sidecar/test/codex-character-native.test.mjs を実行する。Windows で PULLCEPT_TEST_CODEX_PTY=1 も設定すると crates/codex-character-probe をビルドし、アプリと同じ patched portable-pty / cmd /C call の配送を実測する。新しい一時 config と既知の指示だけを使い、通常 CI は両方を skip する。sentinel/文法/TOML同値/8191超拒否/Claude・native ID の回帰と sidecar initialize は通常テストで確認する。プレビューのnative探索は async command の spawn_blocking で UI 処理から外す。Codexのみ250ms debounceと同時実行一件を使い、実行中の編集は最新入力で再探索する。

sidecar/test/codex-live.mjs は明示実行する実モデルの手動 fixture である。PULLCEPT_PROBE_BIN に隔離 CLI、必要なら PULLCEPT_PROBE_AUTH にコピー元 auth.json、PULLCEPT_PROBE_ROOT に専用出力先を指定する。node sidecar/test/codex-live.mjs を対話 PTY で実行し、通常の folder trust と専用 handler 一件の信頼を行う。コピー元を変えず、検証用コピー先だけを使う。PULLCEPT_PROBE_RESUME に取得 UUID を指定すると明示 resume を検証できる。相手は mock 部屋であり、実 Pullcept listener と区別する。

生成物の既定先 .codex-live-evidence/ は gitignore で除外する。検証用 auth コピーを含みうるため PR に含めない。evidence.json は fixture の部屋通信と native ID だけを保存し、端末出力・資格情報を収録しない。結果の範囲は 4-status.md に記載する。

テストの配置

HTML プレビューの境界試験(#436)

npm run preview:test は独立した headless Edge/Chromium と一時プロファイルで、実装の Markdown renderer、プレビュー controller、文書 builder と独立ウィンドウ用 host を動かす。実行ファイルは Windows の通常の Edge 配置から探し、別の配置では PREVIEW_BROWSER を指定する。ブラウザが無ければ skip せず失敗する。通常 CI でも Windows Edge で実行する。投稿の処理を始める前から要求を監視し、HTML/CSS の表示、root/body 属性、横の領域が本体のページに枠を置かず子ビューへ文書と表示領域の物理ピクセルの位置・大きさを渡すこと(コード表示・<dialog> の間は隠し、ウィンドウの大きさの変化に追従し、文書は一度だけ送る。閉じる・作り直しで空にして隠す。#466)、子ビュー用の html-preview.html?pane が見出しと注記を出さないこと、独立ウィンドウ用の host の帯(ファイル名・プレビュー|コード・メインウィンドウで開く・窓の三つのボタンの並び、コード表示が投稿の元のコードを文字としてだけ出すこと、窓の操作と帯の押下・ダブルクリックが固定のナビゲーション /html-preview-window/… になりボタンの上では掴まないこと、最大化の名前の入れ替え。#468)、子ビュー用と独立ウィンドウ用の host の枠でインラインスクリプトとイベント属性が枠の DOM を書き換えること・fetch とスクリプトが作る画像/frame の CSP 拒否・親/top の document に届かないこと・外部 src/import map/speculation rules の script の除去・枠の自己移動が host の frame-src で止まること(#443)、meta refresh/base/URL/form/SVG/外部資源の除去、CSS 資源の CSP 拒否、開閉・切替・焦点・狭幅・元コードの保持を確かめる。DevTools に現れる CSP 遮断の試行を実通信の成立とは数えず、外部 scheme は CSP による失敗、local file は opaque frame による失敗であることを別に検査する。PREVIEW_SCREENSHOTS を指定すると同じ試験の最後に横の領域と独立 host の画像を保存する。

src-tauri/examples/html-preview-check.rs と tests/html-preview.native.test.mjs は実際の native 窓を調べる opt-in harness である。部屋・設定・relay・アカウント・PTY を起動せず、テスト専用識別子、一時 WebView2 profile、隠した窓と専用 debug port を使い、production のコマンドを通す。cargo build --manifest-path src-tauri/Cargo.toml --example html-preview-check --target x86_64-pc-windows-gnu 後、node --test tests/html-preview.native.test.mjs で再現する。2026-10-17 のローカル GNU 環境ではビルド成功後、Rust main より前に STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) で停止した。example には manifest が埋め込まれず、Common Controls 6 を読めないためであり、試験は実行ファイルの横に Common Controls 6 を求める manifest(html-preview-check.exe.manifest)を書いてから起動する(#468)。2026-10-18 のローカル環境ではこれで起動し、独立ウィンドウの生成・枠でのスクリプト実行・独立ウィンドウからの invoke の ACL 拒否・再利用・最上位の遷移の拒否・独立ウィンドウの帯の操作(標準の枠が無いこと、窓の題、最大化・ダブルクリックでの元に戻す・最小化・閉じる、#468)・横の領域の子ビュー(#466)の生成と拒否までが通った。最後の「本体の破棄で harness のプロセスが終わる」の待ちだけが 30 秒で打ち切られ、これは #468 の変更を外した main の html_preview.rs でも同じだった(未解決)。通常 CI へは入れていない。帯の掴んでの移動と画面端への引き寄せは実際のマウスの押下が要るため、この harness では確かめられない。

.mcp.json への登録と起動フラグの検査、会話の記録の在り処(「トピック」の決定3)、ステータスラインのアドレスとその読み戻し・行に載せてよい語の判定・statusLine の組み立て・Claude の起動行が hooks を宣言しないこと(#155 / #161)、および種別が持つ CLI の作法とその移行(起動コマンドからの判定、セッション id の付与と取り外し、再開の一行、--settings の報告を載せるかどうか、保存済みアカウントの種別の書き換え。#156)は crates/mcp-config/、床の判定と刻印は crates/room-floor/、トピックの保存(索引とディレクトリの照合、削除、題の生成、一行の解析、添付の保存とその名前の整え方・番号付け・削除で一緒に消えること。#223)は crates/topic-index/、アカウントの環境変数(欄の解析、マスク、DPAPI による暗号化と復号、出たままの行の保持。#163)は crates/account-env/、アカウントのキャラのファイルの場所・検査・読み書き(#100)は crates/character-file/、発言のリンクが開いてよいもの(http:// / https:// だけであること、ドライブと UNC のパスだけであること、開くと実行されるファイルを場所を示すだけにすること、存在しないパスを「見つかりません」と言うこと。#349)は crates/post-link/ という、いずれも tauri 非依存の crate に置く。Codex の席の rollout の読み方(行の読み方と末尾の追記分の読み進め。#283)も crates/mcp-config/(codex::status)に置き、src-tauri 側(codex_status.rs)は席と見張りの寿命だけを持つ。Codex の席の利用上限(rollout の停止の行と解除時刻、app-server の答えの読み方、停止・問い合わせの予定・預かり・解除の状態、知らせと一通の文面。#294)も crates/mcp-config/(codex::limit)に置き、src-tauri 側(codex_limit.rs)は問い合わせのプロセスと部屋への知らせだけを持つ。許可待ちの Claude Code の席へ打たずに預ける列と、その渡し方(#346)も crates/mcp-config/(prompt_hold)に置き、src-tauri 側(hook_activity.rs)は席ごとの錠と端末だけを持つ。

移行がここに在るのは、この規則の一番の対象だからである。人が保存したアカウントを書き換える手続きであり、間違えれば失うのは利用者の設定である。src-tauri 側の config.json の読み書きからそこだけを切り出し、種別の名前は呼ぶ側から渡す——名前は画面と共有する列挙のものであり、この crate に二つ目の綴りを置かないためである。

webhook の通知(#169)のうち、ブリッジへ書く三つの行、ブリッジから来る一行の読み方(イベント・答え・その他の区別、isError の答えを失敗と読むこと、id か本文を欠く push を出さないこと)、通知を入れる部屋の判定は crates/webhook-bridge/ に置く。ブリッジそのものについて頼っている性質——クライアントを問わず claude/channel を宣言すること、WebSocket に来たイベントが meta.message_id を持つ channel push として出ること、起動形が標準入力の終わりで終わること——は sidecar/test/webhook-bridge.test.mjs が、インストール済みのブリッジを実際に走らせて持つ。偽の Worker をループバックに立て、空の home に置いたトークンファイルで WebSocket を張らせるため、ネットワークにも本物のトークンにも触れない。三つめはブリッジ単体では落ちる(標準入力を閉じても 5 秒後に走っている。2026-09-27 に対照として確かめた)。

発言の端末への入力(#183 / #195)のうち、打つ文字列(一行目の札とその role、送信キーを持たない本文)、role の決め方(admin は画面のコマンドだけであること、ソケットが admin と mcp を名乗れないこと)、どの端末へ打つか(発言者の端末を除くこと、端末の無い接続を打たないこと、同じ端末へ一度だけ打つこと)、端末ペインのキーから打ちかけを読むこと(Unsent)は crates/terminal-input/ に置く。作法が名指す札の綴りと admin の綴りがその crate の定数と一致することは、sidecar/test/round-trip.test.mjs が Rust のソースを読んで持つ。保留の列そのもの(src-tauri/src/pty.rs)は PTY の状態に触れるため境界の側に残る。

アプリ自身が起動する MCP サーバ(#172)のうち、設定ファイルの読み書き(Desktop の形の読み、既定の書き出し、当該エントリの三欄だけを書き換えること)、画面の欄の読み方(一行一引数、名前=値 の読みと拒否)、ログの保持、エントリごとのアカウントの作成(#193。既に在るアカウントと名前・色を残すこと、エントリの消えたアカウントを残すこと、使われている id を二度与えないこと)、フォームから作るエントリの名前の決め方と、削除でエントリだけを外すこと(#200)は crates/mcp-servers/ に置く。人が手で書き換えるファイルを書き直す手続きであり、壊せば失うのはその人の設定である——読んで確かめるより走らせて見る側に置く。

ステータスラインのスクリプトそのものは sidecar/test/status-line.test.mjs が持つ(npm run sidecar:test)。部屋の在る場所に受け口を立て、CLI と同じ形で走らせて、アプリへ届く body と端末へ出る 1 行の両方を読む。片方だけでは足りない——送る側が壊れてもバーは出るし、バーが空でも body は届くためである。

発言と入力欄の ``` の枠(#348)のうち、どこが枠になるか(findCodeFences)と、本文を地の文とコードに分けること(splitCodeFences)は src/composer.ts に置く。入力欄のエディター(#354)が送る文字列と読み戻し(textFromBlocks / docFromText)、貼り付けとコピー(pasteText / textFromSlice)、キーに結ぶコマンド(lineBreak / leaveCodeDown / enterCodeUp / backspaceInCode)も同じファイルに、ページの無い所で走る形で置く。キーのコマンドは ProseMirror のコマンド(状態を受け取り、変更を返す関数)であり、カーソルが見えている最後・最初の行にあるかどうか(折り返しを数えるため画面が要る)だけは呼ぶ側から渡す。sidecar/test/composer.test.mjs と sidecar/test/code-fence.test.mjs は tsx の tsImport で src/composer.ts をそのまま読み込んで走らせる(npm run sidecar:test)。閉じていない枠の中の 添付: を添付として読まないこと(splitAttachments)は src/main.ts に残り、code-fence.test.mjs がそれを取り出し、src/composer.ts の関数を横に置いて走らせる(TypeScript の AST から関数を取り出して実行する)。Enter で送ること、``` で枠になる入力規則、言語のボタンと一覧、日本語入力はエディターの画面が要るため、実機で見る。

画面に入れるライブラリを足したり版を上げたりしたら、node scripts/third-party-notices.mjs で THIRD-PARTY-NOTICES.txt を作り直す(#354)。ファイルは src/composer.ts が読み込むパッケージを起点に、package-lock.json の依存(peer を含む)をたどり、各パッケージが node_modules に持つ LICENSE の本文を写す。許すライセンス(MIT・BSD・ISC・Apache-2.0)の外のものや LICENSE の無いものがあれば、作らずに止まる。sidecar/test/third-party-notices.test.mjs は、ファイルが今のロックファイルから作ったものと一致しないとき、src/composer.ts が一覧に無いパッケージを読み込んだとき、インストーラーの同梱物(src-tauri/tauri.conf.json の bundle.resources)から外れたときに落ちる。

理由は依存の正しさと、テストが実行できることの両方である。これらのロジックが tauri を必要とする理由はそもそも無い。加えて src-tauri 側に置くと、テストバイナリが tauri の依存ツリー全体をリンクするため、GNU ターゲットでは STATUS_ENTRYPOINT_NOT_FOUND(0xc0000139)でプロセスが起動せず、アサーションが一度も実行されない。これはローカル環境固有ではなく CI でも再現する(run 32431917979)。特定の依存クレートまでは切り分けていない。

したがって src-tauri/src/ に残すのは、tauri の State / AppHandle に触れる部分だけとする。純粋ロジックをそちらへ書き足すと、書いた時点で検証不能になる。床の順序付けは読んでも確かめられない性質のもの(二人が同時に呼んだとき片方だけが通る)であり、この方針が最も効く箇所である。src-tauri/src/room.rs が持つのは、その判定を既存ロックの内側で呼ぶことと、結果をフレームとイベントへ載せることだけである。

部屋のログ(src-tauri/src/room_log.rs)は境界の側である。パスの解決は AppHandle の app_data_dir に依存し、索引の read-modify-write のロックと画面へのイベントもここに在るため、そこは crate へは出せない。出したのは、部屋のディレクトリさえ渡れば済む部分すべてである——形(LoggedPost / Topic / TopicIndex)、索引の読み書き、照合、削除、題の生成、一行の解析。room_log.rs に残るのは、そのどれに触るかを決める側と、Post から六欄への写像(Post は部屋の形であり、crate へは行けない)と、旧 logs/{部屋名}.jsonl の引き継ぎ(部屋のディレクトリの一つ上を読むため、ディレクトリだけを渡す crate の外に在る)である。

#115 の時点でこれは行われておらず、その状態はここに書かれていた——「照合のほうは読んで確かめられる範囲を出ている。AppHandle への依存を外して crate へ出すのが正しい形である。この issue の範囲では行っていない」。#119 がそれを load-bearing にした。トピックの削除は照合の裏返しであり、索引の項目だけを消す削除は照合がそのまま元へ戻す——読んで確かめられる性質のものではなく、走らせて見るものである。crates/topic-index/ のテストの中心はそれであり、項目だけを消す形を実際に走らせて、次の読み出しでトピックが戻ることを見る。

追記の順序については、この面に確かめるものは無い——順序は床の判定と同じロックの内側で決まっており、その順序付け自体は既に crates/room-floor/ が持っている。

CI が実行するもの:

コマンド 対象
npm run build フロントエンドの型検査とビルド
npm run sidecar:check サイドカーの型検査
npm run sidecar:test サイドカーの往復ハーネス(channel を宣言しないこと、部屋からの post フレームを会話へ流さないこと、札と role を述べる作法。#195。say_to_room の to の一つの名前と名前の並び、宛先の作法。#204。本文の @名前 が宛先になることを述べる作法。#206。名前を差し込んだ instructions が 1024 字以内であること、道具の説明へ移した作法の全文。#273)、webhook の受信に使うブリッジの宣言と push の形と終わり方(#169)、発言の本文から URL とパスを切り出す画面の関数(src/main.ts の linkPieces を取り出して実行する。scheme、パスの形、末尾の句読点と括弧、改行と空白がそのまま返ること。#349)
cargo check --target x86_64-pc-windows-gnu アプリのコンパイル
cargo run --locked --manifest-path scripts/window-icon-check/Cargo.toml Windows の large icon(#381)。既存 ICO を独立した検証用 exe に埋め込み、本番 helper の呼び出し後、100 回の表示・最小化・復帰・非表示・題変更と窓の破棄後にハンドルが有効なこと、small icon と foreground を変えないこと。稼働中の Pullcept には触れない。resource compiler と linker の形式を揃えるため、target を指定せず native Windows host で走らせる
cargo test(crates/mcp-config) .mcp.json マージ保全、起動フラグ検査(端末を読まなくするフラグの拒否、利用者の channel フラグを拒否も追加もしないこと。#195)、起動しないサーバの名指し、会話の記録の在り処(実機で確認した二例、英数字でない文字の畳み方、CLI が名前を切り詰める長さ)、app-server 方式の Codex の席の様子(許可の flag が item に勝つこと、並列の item、ターン終了と idle での消去、待機中・仕事の途中の切断で送る事象、スレッドが決まる前の知らせを失わないこと、本文や引数を出さないこと。#326)、Claude Code の席の hook による様子(許可待ちがツールに勝つこと、ツールとサブエージェントの集合、拒否されたツールの片付け、ターンの区切りでの片付けと背景のサブエージェントを残すこと、切れた本文の扱い、引数や結果を出さないこと。#331)
cargo test(crates/room-floor) 床の判定(未読による拒否、自分の発言の除外、解決できない last_seen、席の位置、同時発話の順序付け、拒否が運ぶ宣言色とアカウント)、宛先の並び(空白と空の名前と二度目の名前を除くこと、拒否が運ぶ宛先が並びであること。#204)、本文の @名前 の照合と除去(部屋の名前に当たるものだけを取ること、最長一致、名前の直後の境界、大文字と小文字、全角の @、書いた順と重複、消した後の空白と行、to 引数の後ろへ足すこと、@名前 だけの発言を取らないこと、通知の本文と to に手を付けないこと。#206)
cargo test(crates/account-env) アカウントの環境変数(欄の解析と拒否、マスクの形と隠れる量、文字単位の切り出し、DPAPI の暗号化と復号の往復、改ざんされた暗号文の拒否、出たままの行が暗号文をそのまま保つこと、入れ直し・削除・追加、値を囲む引用符の除去と片側だけの引用符の拒否、保存済みの名前の隠し表示を書き換えた値の拒否と ... を含む普通の値の受け入れ、#165 の実機の手順の再現、開けない値の描画と保持と起動の拒否、アプリ自身の名前の拒否)
cargo test(crates/webhook-bridge) webhook の通知(ブリッジへ書く行、ブリッジから来る行の読み分け、通知を入れる部屋の判定。#169)
cargo test(crates/mcp-servers) アプリ自身の MCP サーバ(設定ファイルの読みと既定、他の欄とエントリを残す書き込み、引数と環境変数の欄の読み方と拒否、ログの上限。#172。エントリごとのアカウントの作成と既存のアカウントの保持。#193。アカウントの名前から決めて使われている名前と id を避けるエントリの名前、他のエントリとキーを残す取り外し。#200)
cargo test(crates/terminal-input) 発言の端末への入力(札の形と message_id と role、札の at が年月日から分までの現地時刻を時差付きで持ち、読めない時刻では付かないこと(#219)、宛先の無いときに to を持たないこと、宛先を名前の並びで持ち名前で札を書き換えられないこと(#204)、送信キーを持たない本文と札、本文に置いた偽の札が本文のままであること、制御文字を落とすこと、admin が画面からだけ来ること、発言者の端末を打たないこと、端末の無い接続を打たないこと、同じ端末へ一度だけ打つこと、打ちかけの読み方。#183 / #195)
cargo test(crates/topic-index) トピックの保存(索引に無いファイルの採録、削除がファイルと項目の両方を取ること、項目だけを消した場合の復活、他のトピックを乱さないこと、ファイルの無い項目の削除、一ファイル名でない id の拒否、題の生成、千切れた行の読み飛ばしと計数、セッション id の取り消しが項目と他アカウントを残すことと id 一致でのみ効くこと、行のアカウントの読み戻しとそれを持たない旧い行、一つの名前の to を持つ旧い行を並びとして読むこと。#204)
cargo test(crates/character-file) アカウントのキャラのファイル(#100。種別・作業ディレクトリ・キャラクターからの場所、名前の検査と Codex の文法、作業ディレクトリの外へ出る junction の拒否と中の junction の書き通し、新規作成、空の本文と変わらない本文を書かないこと、開いた後の変更を上書きしないこと、改行と BOM の保持、128 KiB の上限、UTF-8 でないファイルの拒否、frontmatter の name:、同じファイルを着る他のアカウント)
cargo test(crates/post-link) 発言のリンクが開いてよいもの(#349。http:// / https:// だけを大文字小文字を問わず通すこと、ほかの scheme と host の無い URL と空白を含む URL を拒むこと、ドライブと UNC のパスだけを通し相対パス・装置の形・verbatim の形・ドライブの後のコロンを拒むこと、開くと実行される拡張子を名前の末尾の . と空白を除いて読むこと、フォルダ・ファイル・実行されるファイル・プログラムの名前のフォルダの開き方、存在しないパスの「見つかりません」)

往復が成立しないときの切り分け

上流から順に確認する。各段は下流の前提であるため、順序を飛ばさない。

  1. 部屋ソケットが待ち受けているか。 右の参加者パネルの部屋ソケットの行を見る。127.0.0.1:<port> が accent 色で出ていなければ以降はすべて不成立。
  2. セッションが生きているか。 同じパネルのセッション行を見る。終了(終了コード N) なら CLI が落ちている。この行は参加者のリストで選択されている端末のアカウントのものであるため、複数動かしている場合はまず対象の行を選ぶ。理由はタイトルバーの「端末」を開いた先に残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。
  3. サイドカーが接続したか。 パネルの参加者のリストで、そのアカウントの行から「未起動」が消え、薄い表示が戻る。リストにはアカウントが起動していなくても並ぶため、名前が出ていること自体は接続の根拠にならない。自分(人間)も発言前から載っている。接続していない場合は端末に room socket: connected as "<name>" があるかを見る。room socket: not joining: が出ていれば、そのサイドカーを起動した CLI はそのアカウントとして起動されていない(「セッションは自分以外の部屋の登録を起動しない」、#208)。
  4. サイドカー単体が壊れていないか。 npm run sidecar:test を実行する。偽の部屋ソケットを立てて両面を駆動するため、通ればサイドカーではなくアプリ側の問題に絞れる。
  5. .mcp.json の登録が効いているか。 セッションの作業ディレクトリの .mcp.json に pullcept-room-<slug>-<hash> が入っているかを見る。鍵はアカウント id から決まるため名前では引けない。どのアカウントのものかはエントリ内の PULLCEPT_AGENT_NAME で読む。固定の pullcept-room は前回起動の残りとして掃除される側である。走る行の --settings の enabledMcpjsonServers がその鍵を名指していなければ、CLI は「New MCP server found」の確認で止まる。他のアカウントのエントリはファイルに残っているのが正しい——消すのではなく、走る行の --settings の disabledMcpjsonServers で起動を止めている(「セッションは自分以外の部屋の登録を起動しない」)。名簿に同じ参加者が二人出るときは、ファイルではなくその行を見る。
  6. 端末を読まなくするフラグが無いか。 アカウントの起動オプションに --print / --input-format / --output-format がある場合、セッション起動はエラーで止まる(黙って外すと動いたように見えるため)。部屋の発言は端末へ打たれるため、端末の入力を読まない起動では何も届かない(#195)。
  7. 発言が端末へ打たれているか。 端末ペインに一行目が [pullcept] の入力が現れるかを見る。現れないときは、その席のアカウントがその部屋で走っているか(参加者パネル)と、人が入力欄に打ちかけていないか(打ちかけの間、発言は保留される。送信か消去で打たれる)を見る。

配布

段階を分ける。第二段階は実装ではなく手続きのトラックであり、第一段階の完了条件には含めない。

  • 第一段階: 開発者自身の環境で動かす。
  • 第二段階: 一般配布。部屋の入力路が channel だった間は、plugin としてマーケットプレイス経由で channel allowlist に載せることが条件だった(plugin:<name>@<marketplace>、--dangerously-load-development-channels は起動ごとに警告バナーを出すローカル開発専用のフラグだったため)。#195 で部屋は channel を使わなくなり、この条件は外れた。

アプリのアイコン

タスクバー・窓の左上・インストーラーに出るアイコンは、src-tauri/app-icon.svg の一枚から作る(#246)。絵は「部屋の吹き出し」である。吹き出しの枠が部屋の壁を、中に並ぶ同じ大きさの三つの点が区別なく並ぶ参加者を表す。地は透明で、青は #1e9be8 である。画面の強調色(#185fa5、#235)より明るい、画面の色とは別の値であり、強調色を変えてもアイコンは追随しない。

Windows の実行中の窓には、埋め込んだ同じ icon.ico の large icon も明示して渡す(#381、src-tauri/src/window_icon.rs)。Tauri 2.11.5 / tauri-runtime-wry 2.11.4 / Tao 0.35.3 の既定の icon 経路は small icon だけを設定し、Tao の taskbar icon(ICON_BIG)は空のままである。開発版も配布版も、tauri-build が実行ファイルへ付ける resource 32512 から、Windows の標準サイズを LoadImageW で読み、窓の setup で WM_SETICON / ICON_BIG を設定する。LR_SHARED を使うため、ハンドルは Windows が管理し、setup のローカル変数の終わりで破棄されない。ファイルの実行時読み込みや、定期的な復元は行わない。small icon は Tauri に任せる。窓の取得や icon の読み込みが失敗した場合は診断を出し、部屋の起動は続ける。

2026-10-11 の稼働中の開発版への読み取りでは、WM_GETICON の small は有効なハンドル、large と class icon はゼロだった。起動直後は正しい絵で、その後に汎用アイコンへ変わるという報告があるが、変化を引き起こす操作と Explorer 側の取得経路までは再現していない。この修正が確かめるのは native large icon の欠落の解消であり、継続使用の見え方は修正版を起動して別途確かめる。調査中は既存の部屋を止めず、稼働版を入れ替えない。

各サイズは Tauri CLI の tauri icon で SVG から直接作る(PNG を間に挟まない)。リポジトリの根で次を実行する。

npm ci
npx tauri icon src-tauri/app-icon.svg -o src-tauri/icons

src-tauri/icons/ の全部(bundle.icon が指す 32x32.png・128x128.png・128x128@2x.png・icon.icns・icon.ico と、Windows ストア用の Square* / StoreLogo.png、android/、ios/)が同じ名前のまま上書きされる。名前が変わらないので tauri.conf.json は触らない。PNG と icon.ico の各サイズは透明地のまま出る。ios/ だけは白地で塗られる——tauri icon が iOS 用の地に --ios-color(既定 #fff)を敷くためである。吹き出しの色は SVG の根の color 属性の一か所にあり、塗りと縁はそれを currentColor で引く。色だけを変えるときはその値を書き換える。絵を変えるときは SVG を直して同じコマンドを流し直し、出た全部を一緒にコミットする。

窓の枠は画面が描き、題は残す

窓には Windows の標準の枠が無い(#252)。src-tauri/tauri.conf.json の窓に decorations: false を置き、窓の最上段は画面のタイトルバー(index.html の #titlebar)が受け持つ。アイコン・最小化・最大化(元に戻す)・閉じるの置き方と見た目は「タイトルバーは窓の最上段である」。

窓の題は Pullcept のまま残す(app.windows[].title)。タスクバーのサムネイルと Alt+Tab は題を読むためである(#248)。題を描く最上段がもう無いので、#248 で最上段の文字だけを描かせなかった仕組み(src-tauri/src/lib.rs で uxtheme の SetWindowThemeAttribute に WTNCA_NODRAWCAPTION を渡す処理)は外した。その呼び出しのためだけに置いていた windows クレートの依存も src-tauri/Cargo.toml から外した。Tauri 自身は同じクレートを使い続けるので、Cargo.lock から消えたのは pullcept からの一本の線だけである。

index.html の <title>Pullcept</title> は窓の題に届かない。Tauri 2(2.11.5)が文書の題を窓の題へ写すのは、窓を作る側が on_document_title_changed で受け手を置いたときだけであり、設定ファイルから作る窓にも src-tauri/src にもその受け手は無い。窓の題は設定ファイルの値で決まる。

#248 が独自の枠を採らなかった理由と、#252 で採った理由。 #248 の時点で消したかったのは最上段の文字だけであり、標準の枠のままそれだけを消せた。独自の枠にすると、三つのボタン、窓を掴んで動かす帯、縁での大きさ変え、Windows 11 のスナップを画面の側で作り直すことになり、それを抱える理由が無かった。#252 では、標準の枠の最上段と画面の二段目の帯を一段にまとめることが求められた(Master「あとアプリの二段目のメニューバーを最上段に入れつつ最上段の全高をちょっとだけ上げて、色もサイドバーに合わせられる?」、続けて「じゃあ全高はそのままで」、2026-10-03)。標準の枠の最上段に画面の操作を置く手は無いため、枠を外して画面の帯を最上段にした。作り直しの多くは Tauri が持っている(下記「確かめたこと」)。画面の側で足したのは三つのボタンと掴む帯の指定だけである。

確かめたこと(#252)。 いずれも src-tauri/Cargo.lock と package-lock.json が指す版のソースを読んで確かめた(tauri 2.11.5、tauri-runtime-wry 2.11.4、tao 0.35.3、tauri-utils 2.9.3、@tauri-apps/api 2.11.1)。実機ではまだ確かめていない(「実装状況」の未実装)。

  • 縁を引いて大きさを変えられる。 tao は枠の無い窓にも窓の型(WS_CAPTION・WS_SIZEBOX・WS_MAXIMIZEBOX)を残し、WM_NCCALCSIZE で見える枠を消す(platform_impl/windows/window_state.rs の to_window_styles、event_loop.rs)。影を持つ窓(shadow は既定で true)は左・右・下に枠の厚みだけの見えない縁を残すので、そこを引けば OS がそのまま大きさを変える。上の縁は webview が覆うため、tauri-runtime-wry が decorations: false で大きさを変えられる窓に限って、上の端に枠の厚み(SM_CYFRAME)だけの細い子窓を webview の上に重ね、そこを HTTOP として返す(undecorated_resizing.rs、lib.rs の attach_resize_handler)。最大化している間はその子窓を大きさ 0 にする。手当てを足す必要は無かった。帯の上端の数ピクセルはボタンではなく上の縁であり、これは標準の枠でも同じである。
  • 掴んで動かす・ダブルクリックで最大化。 data-tauri-drag-region は Tauri が webview に入れる window/scripts/drag.js が見る。マウスを押したところから上へ辿り、属性を持たないボタン・リンク・欄などに当たれば掴まない。値 deep は、属性を持つ要素の中のどこを押しても掴む。一度押しで start_dragging、二度押しで internal_toggle_maximize を呼ぶ。start_dragging は tao の drag_window で、HTCAPTION を押した扱いを OS に渡すので、画面の端へ引くスナップは標準の枠と同じ経路を通る。窓の型に WS_MAXIMIZEBOX と WS_SIZEBOX が残るので、Win+矢印も効くはずである。最大化ボタンにカーソルを乗せて出るスナップの配置の一覧は失われる(受け入れた損失、#252)。Microsoft の文書(「Support snap layouts for desktop apps on Windows 11」)によれば、独自のタイトルバーでこの一覧を出すには、最大化ボタンの上で WM_NCHITTEST に HTMAXBUTTON を返す必要がある。画面のボタンは webview の中の要素であり、その返事をする窓を持たない。同じ文書は一覧を Win+Z でも開けると言うが、この窓で開くかは確かめていない。
  • 権限。 core:default の束に入るのは問い合わせ系と internal_toggle_maximize だけであり(tauri の build.rs の既定の表と permissions/window/autogenerated/reference.md)、close・minimize・toggle_maximize・start_dragging は入らない。そこで src-tauri/capabilities/default.json に core:window:allow-close・allow-minimize・allow-toggle-maximize・allow-start-dragging を足した。is_maximized と、大きさの変化の購読(core:event)は既定の束に在る。
  • 閉じるは今の終了の流れを通る。 @tauri-apps/api の close() は plugin:window|close を呼び、tauri-runtime-wry はそれを on_close_requested で受けて、OS の閉じると同じ CloseRequested を出す。画面はそれを onCloseRequested で受けている(「アプリの終了」)。destroy() は問わずに壊すので使わない。
  • 角の丸みと影。 shadow が true(既定)の枠の無い窓は、tao が上の 1 ピクセル(Windows 11、DPI に連れる)と左右下の枠の厚みを残し、DWM がその枠に影と縁を描く。tauri-utils の設定の説明は、このとき Windows 11 では角が丸くなり、1 ピクセルの縁が付くと言う。shadow は既定のまま書いていない。縁の色がライト / ダークの切り替えで地に馴染むかは実機で見る。

アプリの command の権限(#449)

アプリの自作 command(src-tauri/src/lib.rs の invoke_handler に並ぶもの)は、main の webview からだけ呼べる。app manifest が無いと、Tauri はローカルのオリジンからの invoke を capability に関係なく全部の自作 command へ通す(tauri 2.11.5 の src/webview/mod.rs の on_message)。WebView2 では Tauri の注入スクリプトが子フレームにも入るため(wry 0.55.1 の src/webview2/mod.rs)、この既定のままでは main 以外の文書からも spawn_pty や write_pty に届く。

  • src-tauri/build.rs は tauri_build::try_build に AppManifest::new().commands(APP_COMMANDS) を渡す。APP_COMMANDS は invoke_handler の全 command の名前である。build はそれぞれの allow-<command> / deny-<command> を src-tauri/permissions/autogenerated/ に書き出す。この生成物はコミットする。
  • 許すのは src-tauri/capabilities/default.json(webviews: ["main"])だけであり、全 command の allow-<command> を並べる。main から呼べる command は manifest を入れる前と同じである。html-preview の窓と、本体ウィンドウにはめ込む横のプレビューの子ビュー(html-preview-pane、#466)は IPC を使わず(src-tauri/src/html_preview.rs は eval で内容を渡す)、capability を一つも持たない。
  • capability はウィンドウ(windows)ではなく webview(webviews)で指す(#466)。Tauri は「webview が一致、または window が一致」で許すため(tauri 2.11.5 の src/ipc/authority.rs)、windows: ["main"] は本体ウィンドウにいる子ビューにも届く。マルチビューでは webviews を使うよう tauri-utils も書いている(src/acl/capability.rs)。子ビューを作る Window::add_child のため、tauri に unstable feature を付けた。
  • command を足すときは、invoke_handler、APP_COMMANDS、default.json の三か所に同じ変更で足す。sidecar/test/app-command-acl.test.mjs が三つの一致と、default.json 以外の capability がアプリの command を許していないこと、どの capability もウィンドウ main と独立ウィンドウ、子ビューのラベルに当たらないことを確かめる。default.json に足し忘れた command は main からも ACL に拒否される。

開発環境とビルド手順

README.md を参照。本仕様では再記述しない。

@xterm/addon-webgl へのパッチ(#278)

@xterm/addon-webgl は、インストールのたびにローカルのパッチを当てて使う。package.json の postinstall が scripts/patch-xterm-webgl.mjs を走らせ、node_modules/@xterm/addon-webgl/lib/ の二つの束(addon-webgl.mjs と addon-webgl.js)の中で、ブロック要素を塗る fillRect の呼び出しを、四辺を整数ピクセルへ丸めてから塗る形に書き換える。何を直すかは「診断面」にある。

  • 版を固定する。 package.json は 0.19.0 をそのまま書き(^ を付けない)、スクリプトも版を確かめる。書き換える箇所は、その版の圧縮済みの文字列そのものを目印にして探す。版が違う、または目印がちょうど一つ見つからなければ、スクリプトは失敗し、npm install / npm ci も失敗する。版を上げたときに線が黙って戻ることは無く、新しい版の描き方を読み直してスクリプトを直すまで入らない。既に当たっていれば(印 /*pullcept#278*/ がある)何もしない。
  • この手段にした理由。 塗り方はアドオンの内部の関数であり、xterm.js の公開の API からは差し替えられない。フォークを持つと版の追随と配布の手間が常に付いてくる。patch-package のような道具を足すと依存が一つ増えるが、書き換えるのは二つのファイルの一か所ずつであり、短いスクリプトで足りる。postinstall に置けば npm ci でも走るため、CI と別の機械でも同じ状態になる。DOM の描画へ戻る経路(useWebglRenderer)には触れていない。
  • --ignore-scripts では当たらない。 その場合は線が戻る。CI はスクリプトを止めずに npm ci を走らせている。

この変更を取り込んだクローンで

npm install を一度走らせる(パッチはインストールの時に当たる)。開発サーバが動いていた場合は、Vite が依存を事前に束ねた控え(node_modules/.vite)に古いアドオンが残っている。Vite がこの控えを作り直すかは node_modules/.package-lock.json の中身などで決まり、パッチの有無はそこに現れないため、開発サーバを止めて node_modules/.vite を消してから起動し直す。

src-tauri/Cargo.toml の改行

src-tauri/Cargo.toml は、作業ツリーでも LF で取り出す(#262)。リポジトリの根の .gitattributes が src-tauri/Cargo.toml text eol=lf を置き、core.autocrlf=true の Windows でもこのファイルだけは CRLF に変わらない。ほかのファイルの改行の扱いは変えていない。

理由は、Tauri CLI が tauri dev / tauri build のたびにこのファイルを書き直すことである。CLI は src-tauri/Cargo.toml を読んで tauri / tauri-build の features を設定に合わせ、組み直した文字列が読んだものと一字でも違えば書き戻す(tauri-cli 2.11.4、crates/tauri-cli/src/interface/rust/manifest.rs の rewrite_manifest)。書き戻しは LF で出る。CRLF で取り出していると、中身は同じでも改行だけが違う文字列になって書き戻され、git status が M src-tauri/Cargo.toml を出した。それが git pull を止め、戻す操作(git checkout -- src-tauri/Cargo.toml)はファイルを書き換えるので、動いているアプリの作り直しと再起動を引き起こした。LF で取り出していれば組み直した文字列は読んだものと同じで、CLI は書き戻さない。

2026-10-03 に作業ツリーで確かめた(npx tauri build --debug --no-bundle。CLI はフロントエンドのビルドより前、起動から 1 秒でこのファイルを書く):

  • 属性の無い CRLF の取り出しでは、ファイルが LF に書き換わり M が出た。git diff --ignore-cr-at-eol は空だった。
  • 属性を置いて取り出し直すと LF で出て、ビルドしてもファイルは書かれず(内容も更新時刻も同じ)、git status は空だった。
  • 属性を置いたまま CRLF のファイルが残っている場合は、次の書き直しで LF になり、git status は空に戻った。

この変更を取り込んだクローンで

.gitattributes を取り込んだ直後は、手元の src-tauri/Cargo.toml が CRLF のまま、または書き直された LF のままで、git status が M src-tauri/Cargo.toml を出し続けることがある。中身は索引と同じであり、git が記録している更新時刻だけが古い。次を実行する。

git add --renormalize src-tauri/Cargo.toml

これは索引だけを書き直し、作業ツリーのファイルには触れない。動いているアプリは作り直されない。git checkout -- src-tauri/Cargo.toml は使わない——ファイルを書き換えるので、作り直しと再起動が起きる。

Native relay の回帰境界(#419)

cargo test --manifest-path crates/codex-remote/Cargo.toml は実際の control transport をローカル偽 WebSocket backend に接続する。Bearer header と experimental initialize、応答 ID の照合、通知と承認要求を応答と取り違えないこと、timeout 後に socket を捨てること、遅延応答と席終了の拒否、起動時 enable より前/後に届く明示 disable の順序、非対応 API をサニタイズして再試行可能に保つことを検証する。実 Codex・認証・モデルは使わない。

mcp-config の codex::remote は安定して衝突しない保存先、最後の global sqlite_home 設定、ephemeral 操作、純粋な ID 相関と秘密を含まない失敗、期限の正規化を検証する。sidecar/test/codex-remote-ui.test.mjs は実画面モジュールへ invoke / QR renderer / DOM / 時計を代入し、閉じた後の private reply、QR の遅延完了、期限切れ、非表示、席終了、epoch 変更、非対応の再試行を検証する。frontend-lifecycle.test.mjs は旧アカウントの既定 OFF と opt-in の保存・再読込、app-server 解除時の remote 解除を含む。

これらは合成試験である。Tauri invoke と実 CLI の組合せ、分離 SQLite での既存会話 resume と writer lock、DB 再構築の一覧への影響、公式アプリでの複数席選択・入力・承認・再接続は、隔離した実環境と実機で別途確認する。製品の稼働席はこの PR の検証で更新しない。

専用SQLite保存先の回帰(#426)

mcp-config の codex::remote 試験は、完全長SHA-256の固定vector・安定性・安全なcomponent・topic/accountの境界・空文字・UTF-8 IDを検証する。標準WindowsアプリデータrootとUUID、native0.162.1の全DB名と -journal / -wal / -shm を含むUTF-16長を検証し、同じ予算で旧hex二階層が落ちることも確かめる。長いユーザー名、非ASCIIroot、サロゲート対が予算を超える境界を含む。上限超過は sqlite_home が返し、launcherはdiscoveryやbackend起動前に伝播する。

隔離native試験はdeveloper fixtureのinjectだけで履歴を永続化し、モデルturnは送らない。251/252字の比較と初期化条件、旧失敗fixtureから新SQLiteへの同一thread cold resumeを観測した。修正関数から得た保存先でも成功したが、製品の本来の会話・スマホ・relayはこの試験に含まない。

アカウント設定の型は src-tauri/src/config/account.rs に置き、Tauri 非依存の crates/mcp-config/tests/account_config.rs が同じソースを読み込んで、app-server 設定の欠落・明示 false・明示 true と保存後の再読み取りを確かめる(#299)。画面の新規・種別切り替え・編集・プレビュー・保存は sidecar/test/codex-account-default.test.mjs が実際のアカウントダイアログを動かして確かめる。

Clone this wiki locally