-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Note: このIssueは 2026-02-15 にレビュー結果を反映して更新されました。
詳細: dev-reports/issue/287/issue-review/
概要
モバイル画面の「Claudeからの確認」ダイアログで選択肢(例: 1. Yes)を選んで「送信」しても、Claude Codeが入力を認識せずプロンプトが残り続ける。
再現手順
- Claude Codeセッションで複数選択肢プロンプト(AskUserQuestion)が表示される
- CommandMateのモバイルUIで選択肢を選択
- 「送信」ボタンをタップ
- → Claude Codeが応答せず、プロンプトが残り続ける
promptCheck再検証が失敗する条件
上記手順のステップ3で route.ts:L72-89 の captureSessionOutput() が例外を発生させる場合に本バグが発現する。具体的な失敗条件は以下の通り:
- tmuxセッション出力のキャプチャが5000msタイムアウトする場合
- tmuxセッション名が不正または存在しない場合
- セッションが既に終了している場合
補足: 本バグは上記条件下でのみ発生し、captureSessionOutput() が正常に完了する場合は既存のカーソルキー方式が正しく適用される。
原因
src/app/api/worktrees/[id]/prompt-response/route.ts の72〜89行目にあるプロンプト再検証処理がサイレント失敗した場合、promptCheck が null のままとなり、96〜98行目の isClaudeMultiChoice 判定が常に false になる。
結果、149〜157行目のテキスト入力フォールバック(sendKeys("1") + Enter)が実行されるが、Claude Codeの選択肢プロンプトはカーソルキーベースのナビゲーション(Arrow Up/Down + Enter)を要求するため、文字列入力は無視される。
処理フロー
UI: selectedOption.toString() → "1" → onRespond("1")
↓
API (prompt-response/route.ts):
[L72-89] captureSessionOutput() → 例外発生 → promptCheck = null
[L96-98] promptCheck?.promptData?.type → undefined → isClaudeMultiChoice = false
[L149-157] sendKeys("1") + Enter → Claude Codeは認識しない ✗
期待される動作 vs 実際の動作
| 期待される入力 | 実際に送られる入力 |
|---|---|
sendSpecialKeys(["Enter"]) (デフォルト選択時) |
sendKeys("1") + sendKeys("", Enter) |
| カーソルキー操作 → Enter | 文字列 "1" → Enter |
影響範囲
- Claude Codeの複数選択肢プロンプト(AskUserQuestion)のみ
- Yes/Noプロンプトはテキスト送信で動作するため影響なし
- Codexには影響なし
src/lib/auto-yes-manager.tsには影響なし(L318-321でpromptDetection.isPromptがfalseの場合は早期リターンする設計であり、promptDetectionがnullになるケースは発生しない。route.tsとは異なる構造のため本バグの影響範囲外)- 注記:
route.tsとauto-yes-manager.ts(L343-399)のカーソルキー送信ロジックには重複が存在する。本Issueでは影響範囲外のため対応しないが、重複解消のリファクタリングは別Issueでの検討を推奨する
- 注記:
影響を受けるAPIクライアント
prompt-response APIを呼び出すクライアントは以下の2箇所であり、いずれもアプローチBの修正対象となる:
WorktreeDetailRefactored.tsxのhandlePromptRespond: ユーザー手動応答パス(PromptMessage.tsx,PromptPanel.tsx,MobilePromptSheet.tsxのonRespondコールバック経由)useAutoYes.tsの fetch呼び出し(L85-89): Auto-Yes自動応答パス。現在{ answer, cliTool }のみをリクエストボディに含めており、promptType/defaultOptionNumberが含まれていない。promptDataは関数パラメータとして受け取っている(L30:promptData: PromptData | null)ため修正は容易
修正方針案
アプローチ比較
promptCheck が null(再検証失敗)の場合にカーソルキー方式で送信するための2つのアプローチ:
アプローチA: API側のみでフォールバック
cliToolId === 'claude' かつ answer が数値文字列の場合にカーソルキー方式を仮定する。
- メリット: UI側の変更不要。修正範囲が小さい
- デメリット: promptTypeが不明なため、Yes/Noプロンプトの再検証失敗時にも誤ってカーソルキー方式が適用されるリスクがある
- リスク: answer="1" がYes/Noの"Yes"なのか、選択肢の1番なのかを区別できない
アプローチB: UI側からpromptData情報を送信(推奨)
UI側(handlePromptRespond)からリクエストボディに promptData 情報(type, defaultOptionNumber)を含め、API側でその情報を利用してカーソルキー方式を決定する。
- メリット: 正確なpromptType判定が可能。誤送信リスクが低い
- デメリット:
PromptResponseRequest型の拡張とUI側の修正が必要 - 修正箇所: 下記「アプローチBの修正箇所」セクション参照
アプローチBの修正箇所
推奨する実装戦略: 非破壊的アプローチ
onRespond のシグネチャ (answer: string) => Promise<void> は変更せず、handlePromptRespond の内部で state.prompt.data から直接 promptType/defaultOptionNumber を取得してリクエストボディに含める。この戦略により、MobilePromptSheet.tsx, PromptPanel.tsx, PromptMessage.tsx の onRespond インターフェースは不変となり、UI側の修正は handlePromptRespond の内部修正のみで済む。
根拠: WorktreeDetailRefactored.tsx の handlePromptRespond は useCallback で定義されており、関数内部で state.prompt.data にアクセスすることが可能(dependency配列への追加が必要)。シグネチャを変更する破壊的アプローチでは PromptMessage.tsx (L18), MobilePromptSheet.tsx (L44), PromptPanel.tsx (L46) の全ての onRespond 型定義も同期が必要となり、修正範囲が不必要に拡大する。
実装時の注意点: useCallback のdependency配列(現在: [worktreeId, actions, fetchCurrentOutput, activeCliTab])に state.prompt.data を追加すると、promptData が変化するたびに関数が再生成される。promptData はポーリング(2秒/5秒間隔)の度に更新される可能性があるため、理論的には関数の再生成頻度が上がる。ただし、React.memo で保護されている PromptPanel/MobilePromptSheet は他のpropsも同時に変わるため、実質的なパフォーマンス影響は無視できるレベルである。
defaultOptionNumber の導出ロジック
リクエストボディに含める defaultOptionNumber は、PromptData の型に応じて以下のように導出する:
multiple_choice型の場合:MultipleChoicePromptData.options配列からoptions.find(o => o.isDefault)?.numberで導出する。MultipleChoiceOption.isDefaultはboolean型であり、デフォルト選択肢のnumber(number型)を取得する必要があるyes_no型の場合:YesNoPromptData.defaultOptionは'yes' | 'no'型(文字列)であり、カーソルキー方式は不要なためdefaultOptionNumberは送信しない(undefined)
注意: Issueの以前の記載では defaultOption?: number としていたが、これは multiple_choice 型専用のフィールドであり、YesNoPromptData.defaultOption('yes' | 'no' 型)とは異なる。混同を避けるため、リクエストボディのフィールド名を defaultOptionNumber とする。
フォールバック判定ロジックの疑似コード
route.ts 内での promptCheck と body.promptType の優先順位:
// promptCheck が存在する場合はそちらを優先し、null の場合のみリクエストボディにフォールバック
const isClaudeMultiChoice =
cliToolId === 'claude' &&
(
// 通常パス: promptCheck が正常に取得できた場合
promptCheck?.promptData?.type === 'multiple_choice' ||
// フォールバックパス: promptCheck が null(再検証失敗)の場合
(promptCheck === null && body.promptType === 'multiple_choice')
) &&
/^\d+$/.test(answer);
// カーソルキーのoffset計算
if (isClaudeMultiChoice) {
// 通常パス: promptCheck からデフォルト選択肢番号を取得
// フォールバックパス: body.defaultOptionNumber を使用(undefined の場合はフォールバック値 1)
const defaultNum = promptCheck
? (promptCheck.promptData?.options?.find(o => o.isDefault)?.number ?? 1)
: (body.defaultOptionNumber ?? 1); // ★ ?? 1 が必要(後方互換性のため)
const offset = selectedNum - defaultNum;
// ... カーソルキー送信
}defaultOptionNumber ?? 1 フォールバックの根拠: route.ts の既存コード(L103-104)では defaultNum = defaultOption?.number ?? 1 としてフォールバック値 1 を使用している。フォールバックパスでも同様の ?? 1 が必要である。特に、UIが古いバージョン(promptType/defaultOptionNumber を送信しない)でAPIが新バージョンの場合、body.defaultOptionNumber は undefined となるため、このフォールバック値が後方互換性を担保する。
修正ファイル一覧
| ファイル | 修正内容 | 変更タイプ |
|---|---|---|
src/app/api/worktrees/[id]/prompt-response/route.ts |
PromptResponseRequest に promptType?: string, defaultOptionNumber?: number を追加(オプショナル。後方互換性維持)。promptCheck=null 時にリクエストボディの promptType を参照してカーソルキー方式を判定。フォールバックパスでは body.defaultOptionNumber ?? 1 でデフォルト値を使用。PromptResponseRequest 型はこのIssueではroute.ts内のローカル定義のまま拡張し、src/types/models.ts への共有化は別Issueとする |
modify |
src/components/worktree/WorktreeDetailRefactored.tsx |
handlePromptRespond 内部で state.prompt.data から promptType を取得し、multiple_choice 型の場合は options.find(o => o.isDefault)?.number で defaultOptionNumber を導出。リクエストボディの JSON.stringify() に含める。useCallback のdependency配列に state.prompt.data を追加 |
modify |
src/hooks/useAutoYes.ts |
L85-89の fetch 呼び出しで { answer, cliTool } に promptType(promptData.type)と defaultOptionNumber(multiple_choice 型の場合のみ promptData.options.find(o => o.isDefault)?.number)を追加 |
modify |
tests/unit/api/prompt-response-verification.test.ts |
promptCheck 失敗時のテストケース更新。promptType 付きリクエストのテスト追加。defaultOptionNumber が undefined のケースのテスト追加(?? 1 フォールバック動作の検証) |
modify |
tests/integration/worktree-detail-integration.test.tsx |
prompt-response API呼び出しのリクエストボディ内容(answer, cliTool, promptType, defaultOptionNumber)を検証するテストケースを追加(現状はAPI呼び出しの存在確認のみ) |
modify |
UI側で変更不要なファイル(非破壊的アプローチの場合)
以下のファイルは onRespond シグネチャが不変のため、修正不要:
src/components/worktree/PromptMessage.tsx(onRespond: (answer: string) => Promise<void>不変)src/components/mobile/MobilePromptSheet.tsx(同上)src/components/worktree/PromptPanel.tsx(同上)
推奨方針
アプローチBを推奨(非破壊的実装戦略)。UIコンポーネント(MobilePromptSheet.tsx, PromptPanel.tsx)は既に promptData を保持しているため、handlePromptRespond 内部で state.prompt.data から情報を取得するアプローチが最も安全かつ正確であり、かつ修正範囲が最小限に抑えられる。
受け入れ条件
-
promptCheck再検証が失敗した場合(promptCheck=null)でも、Claude Codeの複数選択肢プロンプト(AskUserQuestion)にカーソルキー方式で正しく応答できること - Yes/Noプロンプトの既存動作に影響がないこと
- Codexの既存動作に影響がないこと
- 既存テスト(
tests/unit/api/prompt-response-verification.test.ts)が引き続きパスすること(必要に応じてテスト更新) -
promptCheck再検証が失敗するケースのテストが追加されていること -
useAutoYes.tsの prompt-response API呼び出しにpromptType/defaultOptionNumberが含まれること(Auto-Yesパスでの同一バグ再発防止) - 結合テスト(
worktree-detail-integration.test.tsx)で prompt-response リクエストボディの内容(answer,cliTool,promptType,defaultOptionNumber)が検証されること -
promptType/defaultOptionNumberフィールドはオプショナル(?)として設計され、フィールド未送信時の後方互換性が保たれること -
defaultOptionNumberがundefinedの場合にフォールバック値1が使用されること(?? 1)。UIが古いバージョンでAPIが新バージョンの場合のテストを含むこと
関連ファイル
src/app/api/worktrees/[id]/prompt-response/route.ts(主要修正対象)src/lib/cli-session.ts(captureSessionOutputの実装元。例外発生パターンの理解に必要)src/components/worktree/WorktreeDetailRefactored.tsx(handlePromptRespond修正対象)src/hooks/useAutoYes.ts(prompt-response API呼び出し修正対象)src/components/mobile/MobilePromptSheet.tsx(非破壊的アプローチでは変更不要)src/components/worktree/PromptPanel.tsx(同上)src/components/worktree/PromptMessage.tsx(同上。onRespondコールバック経由でAPIを呼び出す)src/lib/tmux.ts(sendKeys/sendSpecialKeys)src/types/models.ts(PromptResponseRequest,PromptData,MultipleChoiceOption型定義)tests/unit/api/prompt-response-verification.test.ts(テスト更新対象)tests/integration/worktree-detail-integration.test.tsx(リクエストボディ検証追加対象)
関連Issue
- Claude Codeからの複数選択肢に対し、回答を送信出来ない #193 (Claude Code AskUserQuestion のカーソルキー方式導入)
- auto yes 実行時、それなりの頻度で"1"が送信される #161 (プロンプト再検証の導入)
スクリーンショット
レビュー履歴
イテレーション 1 - Stage 3: 影響範囲分析 (2026-02-15)
- MF-1:
useAutoYes.tsの prompt-response API呼び出し(L85-89)を影響範囲・修正箇所リストに追加。Auto-Yesパスでの同一バグ再発防止 - SF-1:
PromptMessage.tsxのonRespondコールバックパスを影響範囲に明記。非破壊的アプローチでは変更不要であることを明示 - SF-2:
onRespondシグネチャの実装戦略(非破壊的アプローチ推奨)を明記。handlePromptRespond内部でstate.prompt.dataから取得する戦略の根拠を記載 - SF-3: 受け入れ条件に結合テストのリクエストボディ検証を追加。修正ファイル一覧に
worktree-detail-integration.test.tsxを追加
イテレーション 2 - Stage 5: 整合性レビュー (2026-02-15)
- SF-1:
defaultOptionフィールド名をdefaultOptionNumberに変更し、multiple_choice型専用であることを明確化。MultipleChoiceOption.isDefault(boolean)からoptions.find(o => o.isDefault)?.numberで導出するロジックを「defaultOptionNumberの導出ロジック」セクションとして追加。YesNoPromptData.defaultOption('yes' | 'no'型)との混同を防止 - SF-2:
useAutoYes.tsの影響範囲説明で「propsとして受け取っている(L76)」を「関数パラメータとして受け取っている(L30)」に修正。L76はpromptKey生成行であり、promptData受け取り箇所ではない - NTH-1:
route.tsのフォールバック判定ロジックの疑似コードを「フォールバック判定ロジックの疑似コード」セクションとして追加。promptCheckが存在する場合はそちらを優先し、nullの場合のみリクエストボディのpromptTypeにフォールバックする判定順序を明示 - NTH-2:
PromptResponseRequest型の配置方針を明記。本Issueではroute.ts内のローカル定義のまま拡張し、src/types/models.tsへの共有化は別Issueとするスコープ判断を修正ファイル一覧に追記
イテレーション 2 - Stage 7: 影響範囲分析 (2026-02-15)
- SF-1: フォールバック判定ロジックの疑似コードに
defaultOptionNumber ?? 1フォールバックを明記。body.defaultOptionNumberがundefinedの場合(UIが古いバージョン)にデフォルト値1を使用する根拠と、既存コード(L103-104)との一貫性を記載。受け入れ条件にdefaultOptionNumberがundefinedの場合のテストを追加。テスト修正ファイル一覧にもdefaultOptionNumber=undefinedケースの追加を明記 - NTH-1:
auto-yes-manager.tsとroute.tsのカーソルキーロジック重複(L343-399 vs L96-148)のリファクタリングについて、影響範囲セクションに別Issueでの検討を推奨する注記を追加 - NTH-2:
handlePromptRespondのuseCallbackdependency配列にstate.prompt.dataを追加した場合のパフォーマンス影響に関する注意点を「推奨する実装戦略」セクションに追加。ポーリング頻度による関数再生成とReact.memoによる保護の関係を記載
