Skip to content

v0.35.0

Choose a tag to compare

@Kewton Kewton released this 12 Sep 15:33
· 15 commits to main since this release
09115b8

[0.35.0] - 2026-09-12

Highlight: スマホ・弱電波での操作性をまとめて立て直したリリース。通信が数秒切れただけで worktree 詳細画面が全画面エラーに落ちてポーリングごと止まる、通信断の chunk 読み込み失敗を「サーバ更新」と誤判定して入力中の内容ごと自動リロードする、WebSocket に heartbeat が無く half-open 接続のまま送信が黙って消える——という 3 つの「固まる」経路を塞ぎ、スマホに接続状態を表示する useConnectivity を新設した。あわせてファイル面を広げ、2MB を超えるテキストファイルが 413 で閲覧すらできなかったのを読み取り専用で開けるようにしたうえで .txt を編集可能にし、スマホからも .md 以外を編集できるようにした。実測では heartbeat が 30 秒間隔で届き、pong を返さない接続はサーバが 52 秒で切断、クライアントの half-open 検知は 75.1 秒、オンライン復帰からの再接続は 5ms である。

Added

  • feat(mobile): スマホのファイル画面で .txt / .yaml / .yml も編集・保存できるようにした (#2507): モバイルの全画面ファイル面(FileViewer の unified screen)に入る条件が content.extension === 'md' 決め打ちで、PC ファイルパネルが isEditableExtension() でエディタに回している .txt/.yaml/.yml がスマホからは閲覧専用のままだった(#2506 で .txt を編集可能にしても、この分岐を広げない限りスマホには届かない)ため、判定を isEditableExtension('.' + content.extension) に置き換え、実態に合わせて isUnifiedMarkdown → isUnifiedEditable、markdownMode → screenMode にリネームした。プレビューを持つのは .md だけなので閲覧/編集の切替 UI(markdown-file-mode-switch)は .md のときだけ描画し、他の拡張子は編集モード直行(ファイル内検索の間だけ、行アンカーが要る行番号つきソース表示に切り替わる)。全画面表示・閉じる導線・未保存時の確認(requestClose())・アクションシートは拡張子によらず従来どおり動く。.html/.htm は判断のうえ既存の HtmlPreviewMobile(Source/Preview タブ+サンドボックス信頼度セレクタを持つ閲覧面)に据え置き、スマホでの HTML 編集は PC の HtmlPreview に委ねる(切替 UI の二重ネストとレンダリング済み HTML を見る手段の喪失を避けるため)。#2505 の readOnly が真のファイルはエディタにも保存導線にも入れず、理由(readOnlyReason.message)を警告バナーとしてモーダル/全画面の両方に表示する
  • feat(files): .txt をブラウザ上で編集・保存できるようにした (#2506): EDITABLE_EXTENSIONS(src/config/editable-extensions.ts)に .txt を追加し、EXTENSION_VALIDATORS に 2MB 上限(TEXT_MAX_SIZE_BYTES)のエントリを足した。この 1 行で PUT の書き込み許可(isEditableFile())・PC ファイルパネルのエディタ分岐(FilePanelContent)・新規ファイルダイアログの拡張子候補(NewFileDialog)の 3 つが同時に .txt を受け入れる。.md 以外は MarkdownEditor が text モード(プレビュー無し)で開くため、.txt はプレーンテキストエディタになる。2MB を超える .txt は #2505 の経路に乗り、本文を 200 で返したうえで読み取り専用(readOnly: true)として仮想化ビューアで描画されるので、大きなログやダンプが textarea へ一括描画される崖には落ちない。.env.txt のような機微パスは findSensitivePathSegment() が全 HTTP メソッドで拒否したまま(#2014)で、NULL バイト検出も validateContent() の共通処理として効く。あわせて MarkdownWithSearch がファイル内検索中の CodeViewer に extension="md" を固定で渡していたのを content.extension に直し、.txt/.yaml が Markdown としてハイライトされる崩れを解消した
  • feat(chat): 圏外で送ったメッセージを「送信待ち」として保持し、サーバ到達を確認できた時点で1回だけ自動再送する (#2503): 通信が測定上落ちている間は送信失敗を error にせず 30 秒タイムアウトも止め、復帰時は再取得でサーバ側に届いていないことを確かめてから再送する(届いていた・送信中・応答済みのものは送らない)。自動再送は1メッセージ1回で、失敗すれば従来どおり手動の再試行/破棄に委ねる。PC 分割(TerminalSplitPaneContent)とスマホのチャット面(MobileTerminalTab)の両方に配線。 既知の問題: 実機受入テストで、オンライン復帰時の自動再送が発火しないことを確認している(#2535)。オフライン中に失敗表示にしない・タイムアウトで error に落とさない部分は期待どおり動作する。
  • feat(files): サイズ上限を超えたテキストファイルを読み取り専用で開けるようにした (#2505): GET /api/worktrees/:id/files/:path は編集可能拡張子が上限(.html/.htm は 5MB、その他は 2MB)を超えると 413 FILE_TOO_LARGE を返し閲覧すらできなかったが、本文を 200 で返したうえで readOnly: true と理由(readOnlyReason: code/message/limitBytes/sizeBytes)を添える形に変えた。上限は「読めない」ではなく「保存できない」の意味になり、PUT 側の上限拒否(validateContent() の maxFileSize)は従来どおり。FilePanelContent は編集分岐(HTML プレビュー・Markdown/テキストエディタ)に入る前に readOnly を見て仮想化ビューア CodeViewer に回すため、数万行のファイルが textarea へ一括描画されてスマホがフリーズする経路も塞がる。画面には理由が警告バナーとして表示される
  • feat(mobile): スマホに接続状態(オフライン/再接続中)を表示 (#2501): useConnectivity フックが navigator.onLine・共有WebSocketの status・サーバ到達性プローブの3信号を1つの判定にまとめ(onLine === true は「オンラインの確定」には使わず、キャプティブポータル等で「オンライン」と誤表示しない)、モバイルシェル先頭に MobileConnectionBanner(flex列内の細いバー。composer や GlobalMobileNav を覆わない)を、デスクトップは既存 ConnectionStatusIndicator を同フックへ載せ替えて表示する。接続中は何も表示しない

Fixed

  • fix(detection): Command Code の折り返し説明つき AskUserQuestion を multiple_choice として読み、status/履歴・通知/respond/Auto-Yes が同じ質問と選択肢を扱えるようにする (#2522): #2521 が認識だけして手動操作 fallback を出していた footer なしの質問画面を、新設の専用リーダ src/lib/detection/tools/command-code/dialog.ts が読むようにした。罫線・タブ帯・厳密連番・単一 ❯ の領域判定は #2521 の readCommandCodeQuestionRegion をそのまま再利用し、オプション本体は共有の findNumberedOptionBlock に渡して画面で直上の option へ説明行を空白 1 個で畳み込む(インデント 0/1/2・最終 option の下も同様)。質問はタブ行の直後から最初の option の直前までで、?/? は必須にしない — 罫線・タブ帯・上の TODOS 行が混入しない。Type something... は表示上の選択肢として残したうえで requiresTextInput:true にし(汎用 TEXT_INPUT_PATTERNS は変更していない)、Auto-Yes の既定回答も手動の数字回答も拒否する。罫線が消える経路が核心で、detectPromptWithOptions・Auto-Yes の captureAndCleanOutput・/prompt-response はいずれも stripBoxDrawing を通すため領域が常に null になっていた(stripBoxDrawing は 200 桁の U+2500 行をまさに消す)。そこで 1 tick の同じ capture から raw と clean の両方を保持して伝搬する配線に変更した(capturePollerFrame() 新設、detectPromptOnCleanFrame(clean, tool, precomputedLines, rawFrame) に raw 引数追加、precomputedLines は従来どおり cleanOutput の split のまま)。送信は既存 submitMode:'answer_only'(数字で即選択される質問に遅延 Enter を足すと次画面まで確定してしまうため)。読めない質問画面(欠番・重複・上限超過・最終 option 直下の説明過多・複数選択チェックボックス)は #2521 と同じ waiting / command_code_selection_list / hasActivePrompt:false の手動操作 fallback へ落とし、promptData も自動回答も generic parser の部分リストも生成しない。手動 /prompt-response は HTTP 200・success:false・unsupported_dialog_layout でキー送信 0 回、質問が消えていれば従来の prompt_no_longer_active。COMMAND_CODE_VERIFIED_AGAINST は据え置き(根拠は報告された 1.53.0 の匿名化 capture と 1.53.1 パッケージの静的確認のみで、実機 probe は未実施)。
  • fix(api): クライアントの fetch にタイムアウト・リトライ・オフライン即時失敗を入れた (#2499): fetchApi() / 新設 fetchApiResponse() が GET 10秒・送信系 30秒・ポーリング 8秒で打ち切り、AbortError を kind(timeout/network/offline/aborted/http)つきの ApiError に正規化する。冪等 GET のみ jitter 付き指数バックオフで最大2回リトライし(総予算20秒で打ち切り)、POST などの送信系はリトライ対象外なので二重送信しない。navigator.onLine === false のときは fetch を出さず即失敗。詳細画面・ファイル取得・ターミナル出力の各ポーリングと FilePanelContent のファイル読み込みを共通ラッパへ寄せ、各リクエストの成否を #2501 の reportServerReachability() へ報告して接続バナーの精度を上げた。タイムアウト値とリトライ定数は src/config/api-timeout-config.ts。
  • fix(ui): 通信断による ChunkLoadError を「サーバ更新」と誤判定して自動リロードするのをやめた (#2500): ChunkLoadError を navigator.onLine で network / build に分類し(classifyChunkError())、オフライン時は自動リロードせず「接続が切れています」を表示したうえで、#2501 の subscribeServerReachability() / probeServerReachable() を使ってサーバ復帰時にユーザー操作なしで reset() 再試行する。オンライン時の本物の版ズレは従来どおり CHUNK_RELOAD_GUARD_MS つきで1回だけ自動リロードする
  • fix(detection): Command Code の折り返し説明付き AskUserQuestion が ready と誤判定され、チャットに操作カードが出ず wait が偽完了する問題を修正 (#2521): AskUserQuestion はフッタ(ヒントバー)を描かないため /usage・/model の既存 2 分岐はどちらも一致せず、さらに選択肢 1 の説明が折り返されると継続行が空白 1 個で始まる( answer).)ため共有 detectPrompt はそこでスキャンを止めて選択肢 1 に到達しない。結果フレームは step 3 の汎用 composer チェックへ落ち、COMMAND_CODE_PROMPT_PATTERN(=^❯(\s*$|\s+\S)) がダイアログ自身のカーソル行 ❯ 1. Prepare worktrees + dispatch (Recommended) に一致して ready / input_prompt を publish していた(2026-09-12 実測・Command Code 1.53.0 と報告されたセッション・200x1000 の pane で選択画面が約 12.1 分間未検出)。そこで browser-safe leaf の selection-shape.ts に領域判定 readCommandCodeQuestionRegion() を追加し、「最後の適格な罫線(純粋 U+2500・40 桁以上)を上端」「その直下がタブ列 ● Dispatch | ◯ Review(固有名ではなく選択/未選択マーカーと区切りの構造で読む)」「空でない質問本文」「1. からの厳密な連番 2 個以上」「領域内に ❯ が 1 個だけかつ選択肢行の上」「検索/filter 入力・/model footer・dismiss-only footer・他 turn の生成/完了 UI を含まない」の全成立でのみ陽性とした。status(waiting / high / command_code_selection_list / hasActivePrompt:false / evidence:positive)・extractCommandCodeSelectionListFrame() の crop・ChatSurface の数字キー抑止が同一の読み取りを共有するので、カードの行・待機判定・ボタンが食い違わない。commandmate wait は既存の選択リスト経路で exit 10 / type=selection_list / options=[]、--on-prompt human は応答待ちを継続する。汎用 prompt-detect-multiple-choice.ts の条件は緩めていないし、readSelectionListShape / shouldOfferOptionNumbers の全 CLI 共通規則も不変(claude /model の s 扱い、codex picker の 7 個の数字ボタンは維持)。promptData の生成・respond / Auto-Yes の復旧・COMMAND_CODE_VERIFIED_AGAINST は本件の対象外(#2522)で、この画面の数字キーは「押せる」と実測できていないためカードには出さない。fixture は tests/fixtures/command-code-askuserquestion-2521/(実キャプチャ由来の匿名化 200x1000 版+合成の最小例+採取日・geometry・加工箇所の README)。リポジトリの全 .txt を走査する dialog-frame-2326.test.ts の等値ピンは、この 2 件だけが増えることを毎回再導出する。
  • fix(ws): WebSocket に heartbeat を入れ、half-open 接続を検知して再接続する (#2502): サーバが 30 秒ごとに ping と application-level heartbeat フレームを送り、pong の無い接続を次の周期で terminate() してルーム・ターミナル購読ごと解放するようにした。クライアントは受信の途絶(75 秒)で half-open と判定して自ら close() し status を disconnected に落とすため、ターミナル以外の面(サイドバー・履歴・版不一致検知)も通常のポーリング間隔へフォールバックする。あわせて再接続バックオフに ±20% の jitter を入れてサーバ再起動時の同時殺到を防ぎ、online イベントで即時再接続するようにした(従来は最大 30 秒待ち)。間隔・タイムアウト・jitter 比・close code は src/config/websocket-config.ts に集約。
  • fix(ui): worktree詳細画面がポーリング1回の通信失敗で全画面エラーに落ちる問題を修正 (#2498): fetchWorktree() の失敗を初回ロード失敗(従来どおり ErrorDisplay +再試行ボタン)とポーリング失敗(画面を保持したまま連続3回で控えめな再接続バナー)に分離し、ポーリング effect の if (loading || error) return から error を外してエラー中も回り続けるようにした。初回ロード失敗には useWorktreesCache の INITIAL_LOAD_RETRY_DELAYS_MS 相当のバックオフラダー(2s/5s/10s)を移植し、成功1回で全ての失敗判定を解除する。あわせて detectAuthRedirect() / detectNonJsonBody() を通し、セッション期限切れ(401 / /login リダイレクト)を Unexpected token '<' ではなく再ログイン導線として表示するようにした。電波が途切れても画面とcomposerの下書きが失われず、復帰後はユーザー操作なしにポーリングが再開する。