2026-01-25 実行中断ボタン plan
実行中断ボタン 実装計画
概要
Claude CLI 実行中に即座に強制終了できる「実行中断ボタン」機能を実装する。
要件:
- フロントエンドから「中断」操作で即座に Claude CLI プロセスを Kill する
- 完全終了のみ(graceful shutdown は不要)
フロントエンド計画
1. 仕様サマリー
機能要件:
- 実行中に「中断ボタン」を表示する
- クリックで即座に実行を中断する
- 中断後はUIを適切な状態にリセット
技術的背景:
- バックエンドは
exec.CommandContextを使用しており、HTTPリクエストのコンテキストがキャンセルされると Claude CLI プロセスも停止する - フロントエンドで
AbortControllerを使ってfetch を中断すれば、SSE接続が切断され、バックエンドのコンテキストもキャンセルされる - 専用の Abort API は不要 - fetch の abort で十分
2. 変更ファイル一覧
| ファイル | 変更内容 | 影響度 |
|---|---|---|
frontend/src/lib/api.ts | AbortController を受け取れるように関数を修正 | 中 |
frontend/src/hooks/useSSEStream.ts | AbortError のハンドリング追加 | 中 |
frontend/src/app/page.tsx | AbortController の管理、handleAbort 関数の追加 | 中 |
frontend/src/components/ProgressContainer.tsx | 中断ボタンの props 追加と表示 | 低 |
3. 実装ステップ
Step 1: api.ts の修正
- 対象ファイル:
frontend/src/lib/api.ts - 変更内容:
executeCommandStreamとcontinueSessionStreamにsignal?: AbortSignalパラメータを追加 - 注意点: オプショナルパラメータとして追加し、既存の呼び出しに影響を与えない
// 修正後のイメージ
export async function executeCommandStream(
request: CommandRequest,
signal?: AbortSignal // 追加
): Promise<Response> {
const response = await fetch(`${API_BASE}/api/command/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
signal, // 追加
});
return response;
}
export async function continueSessionStream(
request: ContinueRequest,
signal?: AbortSignal // 追加
): Promise<Response> {
const response = await fetch(`${API_BASE}/api/command/continue/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
signal, // 追加
});
return response;
}
Step 2: useSSEStream.ts の修正
- 対象ファイル:
frontend/src/hooks/useSSEStream.ts - 変更内容:
processStreamの try-catch で AbortError を検出- AbortError の場合は
onErrorを呼ばずに静かに終了(onCompleteは呼ぶ) - 判定:
error.name === 'AbortError'
// 修正後のイメージ
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// ...
}
} catch (error) {
// AbortError は中断による正常な終了なので無視
if (error instanceof Error && error.name === 'AbortError') {
return; // onError を呼ばない
}
onError(error instanceof Error ? error.message : "Stream error");
} finally {
onComplete();
}
Step 3: page.tsx の状態管理追加
- 対象ファイル:
frontend/src/app/page.tsx - 変更内容:
abortControllerRefを useRef で管理handleSubmit/handleAnswerで AbortController を作成し、api 関数に渡すhandleAbort関数を追加(controller.abort() を呼び、状態をリセット)- ProgressContainer に
onAbortとcanAbortを渡す
- 注意点: ref を使う理由は、abort は最新の controller を参照する必要があるため
Step 4: ProgressContainer に中断ボタンを追加
- 対象ファイル:
frontend/src/components/ProgressContainer.tsx - 変更内容:
- props に
onAbortとcanAbortを追加 LoadingIndicatorの直後に赤い「Abort」ボタンを配置canAbortが true のときのみボタンを表示
- props に
- 注意点: PlanApproval のボタンスタイルを参考にする(赤色系)
4. 設計判断とトレードオフ
| 判断 | 選択した方法 | 理由 | 他の選択肢 |
|---|---|---|---|
| 中断方法 | fetch の abort | バックエンドの Context がキャンセルされ、プロセスも停止する。専用 API 不要 | 専用 Abort API(過剰) |
| AbortController の管理 | useRef | abort 時に最新の controller を参照する必要がある。useState だと stale closure の問題 | useState(問題あり) |
| ボタン配置 | LoadingIndicator の直後 | 実行中に目立つ場所。既存の ProgressContainer レイアウトに自然に収まる | CommandForm 内(分散して分かりにくい) |
| AbortError 処理 | useSSEStream 内で静かに終了 | abort は正常な中断操作なので、エラーとして表示しない | page.tsx 側でハンドリング(責務分散) |
5. 懸念点と対応方針
| 懸念点 | 対応方針 |
|---|---|
| abort 後の状態リセット | handleAbort で全状態をリセット(詳細は下記参照) |
| 連続クリック防止 | abort 後は isLoading=false になるのでボタンが消える。追加対策不要 |
| SSE 切断時のエラーハンドリング | useSSEStream で AbortError を検出し、エラーとして扱わない |
handleError との重複 | handleAbort は専用処理とし、handleError は呼ばない(中断とエラーは異なる状態) |
6. 状態リセット詳細
handleAbort で設定する状態:
const handleAbort = useCallback(() => {
// AbortController で接続を切断
abortControllerRef.current?.abort();
abortControllerRef.current = null;
// 状態をリセット
setIsLoading(false);
setIsSubmitting(false);
setLoadingText("");
// 実行ログ(events)は保持し、中断イベントを追加
addEvent("info", "Execution aborted");
// 結果表示
setResultOutput("Execution aborted by user");
setResultType("error");
// 質問・承認UIは非表示
setShowQuestions(false);
setShowPlanApproval(false);
setQuestions([]);
}, [addEvent]);
設計方針:
events(実行ログ)は保持し、最後に「中断」イベントを追加(実行経過を確認できるようにする)questions,showQuestions,showPlanApprovalはリセット(中断後は不要)sessionId,totalCostは保持(セッション情報は残す)
7. UI設計詳細
中断ボタンの表示条件:
canAbort = isLoading && !showQuestions && !showPlanApproval
isLoading中のみ表示- 質問待ち・承認待ち状態では非表示(それぞれ専用UIがある)
ボタンのスタイル:
- PlanApproval の Reject ボタンと同じスタイルを適用:
className="py-3.5 px-6 bg-red-500 text-white rounded-lg font-semibold text-base cursor-pointer transition-colors hover:bg-red-600 border-none"
- テキスト: 「Abort」
配置:
LoadingIndicatorの直後、独立した行として配置- 中央揃え、適切なマージン
バックエンド計画
1. 仕様サマリー
技術的背景:
- バックエンドは
exec.CommandContext(ctx, "claude", ...)で Claude CLI を実行 ctxはc.Request.Context()から取得しているため、HTTP接続が切断されるとコンテキストがキャンセルされる- コンテキストキャンセル時、
exec.CommandContextは自動的にプロセスを Kill する
結論:
- フロントエンドが fetch を abort すると、バックエンドのコンテキストもキャンセルされ、Claude CLI プロセスが自動的に終了する
- バックエンド側の追加実装は不要
2. 既存の動作確認
backend/internal/service/claude.go の該当部分:
func (s *claudeServiceImpl) executeCommandStream(ctx context.Context, project, prompt, sessionID string, eventCh chan<- StreamEvent) error {
// タイムアウト付きコンテキストを作成
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
// claude コマンドを実行
cmd := exec.CommandContext(ctx, "claude", cmdArgs...)
// ...
if err != nil {
// コンテキストキャンセルの場合
if ctx.Err() == context.Canceled {
eventCh <- StreamEvent{Type: EventTypeError, Message: "Execution canceled"}
return fmt.Errorf("execution canceled")
}
// ...
}
}
exec.CommandContextを使用しているため、コンテキストキャンセル時にプロセスが自動終了- キャンセル時は
EventTypeErrorで "Execution canceled" を送信(ただし接続切断後なので届かない)
確認事項
実装前に確認が必要
- ボタンテキスト: 「Abort」でよいか?(「Stop」「Cancel」等の選択肢もあり)
- 中断後メッセージ: 「Execution aborted」でよいか?
決定済み
- 専用の Abort API は不要(fetch abort で十分)
- バックエンド側の追加実装は不要
次回実装(MVP外)
以下はMVP範囲外とし、次回以降に実装:
- 中断確認ダイアログ
- 中断理由の入力・ログ記録
実装完了レポート
実装サマリー
- 実装日: 2026-01-25
- 変更ファイル数: 4 files
変更ファイル一覧
| ファイル | 変更内容 |
|---|---|
frontend/src/lib/api.ts | executeCommandStream と continueSessionStream に signal?: AbortSignal パラメータを追加 |
frontend/src/hooks/useSSEStream.ts | AbortError を検出し、エラーとして扱わず onComplete を呼ぶ処理を追加 |
frontend/src/app/page.tsx | abortControllerRef の管理、handleAbort 関数の追加、ProgressContainer への props 追加 |
frontend/src/components/ProgressContainer.tsx | onAbort と canAbort props を追加、赤い「Abort」ボタンを LoadingIndicator の直後に配置 |
計画からの変更点
特になし - 計画通りに実装完了
実装時の課題
特になし
残存する懸念点
- 連続クリック時の挙動: abort 後は
isLoading=falseになりボタンが消えるため問題ないが、極めて短い時間に連続クリックされた場合の挙動は未検証 - ネットワーク遅延時のUX: abort 後にバックエンドからのレスポンスが遅れて届く可能性があるが、AbortError ハンドリングにより無視される
動作確認フロー
1. フロントエンドを起動: cd frontend && npm run dev
2. ブラウザで http://localhost:3000 を開く
3. Project Path にプロジェクトパスを入力
4. コマンド(例: /plan)と引数を入力して「Execute」をクリック
5. 実行中に LoadingIndicator の下に表示される赤い「Abort」ボタンをクリック
6. 以下を確認:
- 実行が即座に中断される
- イベントログに「Execution aborted」が追加される
- 結果表示エリアに「Execution aborted by user」が赤背景で表示される
- 「Abort」ボタンが非表示になる
デプロイ後の確認事項
- 本番環境で Abort ボタンが正しく表示されることを確認
- 本番環境で Abort 操作が正しく動作し、バックエンドプロセスが停止することを確認
- 中断後に新しいコマンドを正常に実行できることを確認