Home

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.tsAbortController を受け取れるように関数を修正
frontend/src/hooks/useSSEStream.tsAbortError のハンドリング追加
frontend/src/app/page.tsxAbortController の管理、handleAbort 関数の追加
frontend/src/components/ProgressContainer.tsx中断ボタンの props 追加と表示

3. 実装ステップ

Step 1: api.ts の修正

  • 対象ファイル: frontend/src/lib/api.ts
  • 変更内容: executeCommandStreamcontinueSessionStreamsignal?: 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 に onAbortcanAbort を渡す
  • 注意点: ref を使う理由は、abort は最新の controller を参照する必要があるため

Step 4: ProgressContainer に中断ボタンを追加

  • 対象ファイル: frontend/src/components/ProgressContainer.tsx
  • 変更内容:
    • props に onAbortcanAbort を追加
    • LoadingIndicator の直後に赤い「Abort」ボタンを配置
    • canAbort が true のときのみボタンを表示
  • 注意点: PlanApproval のボタンスタイルを参考にする(赤色系)

4. 設計判断とトレードオフ

判断選択した方法理由他の選択肢
中断方法fetch の abortバックエンドの Context がキャンセルされ、プロセスも停止する。専用 API 不要専用 Abort API(過剰)
AbortController の管理useRefabort 時に最新の 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 を実行
  • ctxc.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" を送信(ただし接続切断後なので届かない)

確認事項

実装前に確認が必要

  1. ボタンテキスト: 「Abort」でよいか?(「Stop」「Cancel」等の選択肢もあり)
  2. 中断後メッセージ: 「Execution aborted」でよいか?

決定済み

  • 専用の Abort API は不要(fetch abort で十分)
  • バックエンド側の追加実装は不要

次回実装(MVP外)

以下はMVP範囲外とし、次回以降に実装:

  • 中断確認ダイアログ
  • 中断理由の入力・ログ記録

実装完了レポート

実装サマリー

  • 実装日: 2026-01-25
  • 変更ファイル数: 4 files

変更ファイル一覧

ファイル変更内容
frontend/src/lib/api.tsexecuteCommandStreamcontinueSessionStreamsignal?: AbortSignal パラメータを追加
frontend/src/hooks/useSSEStream.tsAbortError を検出し、エラーとして扱わず onComplete を呼ぶ処理を追加
frontend/src/app/page.tsxabortControllerRef の管理、handleAbort 関数の追加、ProgressContainer への props 追加
frontend/src/components/ProgressContainer.tsxonAbortcanAbort 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 操作が正しく動作し、バックエンドプロセスが停止することを確認
  • 中断後に新しいコマンドを正常に実行できることを確認