Skip to content

履歴から過去の入力やレスポンスをコピー可能にしたい #211

@Kewton

Description

@Kewton

Note: このIssueは 2026-02-10 にレビュー結果(Stage 7: 影響範囲レビュー2回目)を反映して更新されました。
詳細: dev-reports/issue/211/issue-review/

概要

履歴タブ(HistoryPane)に表示される過去のユーザー入力やAssistant応答を、個別にクリップボードへコピーできるボタンを追加する。

背景・課題

  • Assistantの回答(コード断片、コマンド、説明文等)を別の場所で再利用したいケースが多いが、現在はテキストを手動で選択・コピーする必要がある
  • 特にコードブロックを含む長い応答は、手動選択ではコピー範囲が不正確になりやすい
  • ChatGPTなど類似ツールではメッセージごとのコピーボタンが標準的なUXとして提供されている

提案する解決策

ConversationPairCard(現行表示)

各メッセージセクション(UserMessageSection / AssistantMessageItem)にコピーアイコンボタンを常時表示し、クリックでメッセージ内容をプレーンテキストとしてクリップボードにコピーする。コピー成功時にはToast通知で「コピーしました」を表示する。

  • コピーボタンの配置(セクション別方針):
    • UserMessageSection: 展開/折りたたみボタンが存在しないため、シンプルに右上配置が可能(例: ヘッダー行の右端、または absolute top-2 right-2
    • AssistantMessagesSection: 既存の展開/折りたたみボタン(absolute top-2 right-2)が存在するため、そのボタンと重ならない位置に配置する(例: right-10 top-2 で展開ボタンの左隣)
  • アイコン: lucide-reactの Copy または ClipboardCopy アイコンを使用(プロジェクトで既にlucide-reactを採用済み)

MessageList(レガシー表示)

各メッセージバブル(MessageBubble)にも同様のコピーボタンを追加する。ただし、現在の page.tsxWorktreeDetailRefactored.tsx を使用しており、MessageList.tsx はレガシーコンポーネント(WorktreeDetail.tsx)で使用されているため、ConversationPairCardの対応を主要な変更対象とし、MessageListは副次的な対応とする。

コピー内容の仕様

  • message.content の生テキストをそのままコピーする
  • MessageList.tsx ではANSIエスケープコードを含むメッセージが存在するため(hasAnsiCodes / convertAnsiToHtml 関数の存在から確認)、コピー時にはANSIエスケープコードを除去してからコピーする
    • ANSIコード除去のアプローチ: 既存の stripAnsi() 関数(src/lib/cli-patterns.ts L205-207)を再利用する。この関数は SGR、OSC、CSI シーケンスに対応した包括的な正規表現パターン(ANSI_PATTERN)を使用しており、プロジェクト内で既に広く利用されている(assistant-response-saver.tsclaude-session.tsstatus-detector.tsresponse-poller.ts 等、少なくとも8ファイルがインポート)。clipboard-utils.ts では import { stripAnsi } from '@/lib/cli-patterns' としてインポートし、ANSI除去ロジックの重複実装は行わない。既存の AnsiToHtml ライブラリ(MessageList.tsx L18)はANSIをHTML変換する用途であり、プレーンテキスト出力のコピー用途には適さないため使用しない
  • Markdownソースはそのままコピーする(レンダリング後のテキストではなく、生のMarkdownテキスト)

Toast通知の統合方針

既存の WorktreeDetailRefactored.tsx(L1309付近)が既に useToast を使用して showToast を内部で利用している。以下の方針でToast機能を統合する:

推奨方針(C案): 親コンポーネントからのprops伝搬

  1. WorktreeDetailRefactored.tsxshowToastHistoryPane にpropsとして渡す
    • 重要: WorktreeDetailRefactored.tsx には HistoryPane の呼び出し箇所が2箇所存在する(モバイルレイアウト用: renderMobileLeftPaneContent 内 L809付近、デスクトップレイアウト用: WorktreeDesktopLayout 内 L1573付近)。両方の呼び出し箇所に showToast propsを追加すること。片方だけ対応するとレイアウトによってコピー機能が動作しない不具合となる。
  2. HistoryPane は受け取った showToast を使い、useCallbackonCopy コールバックを作成して ConversationPairCard にpropsとして渡す
  3. ConversationPairCard 内でコピーボタンがクリックされると onCopy を呼び出す

この方針は既存のパターンとの一貫性が最も高い。

showToast 型シグネチャとデータフロー

showToast の型シグネチャは以下の通り(Toast.tsx L249-261 の useToast フックの返り値):

showToast?: (message: string, type?: 'success' | 'error' | 'info', duration?: number) => string

型定義は @/types/markdown-editor.tsToastType 型を参照する。

データフローの詳細:

WorktreeDetailRefactored (showToast を保持)
  └─ HistoryPane (showToast を受け取り、onCopy を useCallback で作成)
       └─ ConversationPairCard (onCopy を受け取り、コピーボタンから呼び出す)

HistoryPane 内での onCopy コールバック作成イメージ:

import { stripAnsi } from '@/lib/cli-patterns';

const onCopy = useCallback(async (content: string) => {
  const cleanText = stripAnsi(content);
  await navigator.clipboard.writeText(cleanText);
  showToast?.('コピーしました', 'success');
}, [showToast]);

このパターンにより、クリップボード操作とToast呼び出しのロジックは HistoryPane に集約され、ConversationPairCard はコピー対象のテキストを onCopy に渡すだけの責務となる。

showToast の参照安定性について: Toast.tsx 内の useToast フックは showToastuseCallback([], []) で定義しており、コンポーネントの全ライフサイクルで参照が安定している。そのため、HistoryPane での useCallback([showToast]) の依存配列も安定し、onCopy の参照安定性は保たれる(確認済み)。

Props設計方針

新たに追加するprops(showToastonCopy 等)は全てオプショナル(?:)として定義する。これにより:

  • 既存のテストコードや呼び出し元を壊さず段階的に対応できる
  • showToast が提供されていない場合、コピーボタンはクリップボードへのコピーのみ実行し、Toast通知は省略する(コピーボタン自体は表示する)

コールバック参照の安定化(メモ化考慮)

ConversationPairCard およびそのサブコンポーネント(UserMessageSectionAssistantMessageItemAssistantMessagesSection)は全て React.memo でラップされている。コピーコールバックを新たにpropsとして追加する場合、各render時に新しいコールバック参照が生成されると memo 化の効果が無効化される。

対応方針: 既存の handleToggle / handleFilePathClick と同様のパターンに従い、useCallback でラップした onCopy ハンドラーをサブコンポーネントに渡す。

MessageList.tsx の React.memo カスタム比較関数について

MessageList.tsxMessageBubbleReact.memo のカスタム比較関数(L362-371)を使用しているが、この比較関数は message.idmessage.contentpromptData.statuspromptData.answer の4プロパティのみを比較しており、onFilePathClick などのコールバック系propsは比較対象に含まれていない。onCopy 系propsも既存の onFilePathClick と同様に比較関数の対象外とする方針で問題ない(onFilePathClick が比較対象外で問題なく動作している実績がある)。そのため、MessageBubble の React.memo カスタム比較関数の修正は不要である。

実装タスク

コピーユーティリティ

  • コピーユーティリティ関数の作成: src/lib/clipboard-utils.ts を新規作成(navigator.clipboard.writeText ラッパー)。ANSIコード除去は既存の stripAnsi() 関数(src/lib/cli-patterns.ts)を import { stripAnsi } from '@/lib/cli-patterns' でインポートして利用する(重複実装しない)。既存の AnsiToHtml ライブラリは使用しない
  • コピーユーティリティ関数の単体テスト作成: src/lib/__tests__/clipboard-utils.test.tsstripAnsi() の再利用に合わせたテスト内容とする。ANSI除去ロジック自体のテストではなく、clipboard-utils が stripAnsi を正しく呼び出してクリップボードに書き込むことの統合テストを中心とする)

Props型定義変更

  • HistoryPanePropsshowToast コールバックをオプショナルで追加(型: (message: string, type?: ToastType, duration?: number) => string
  • ConversationPairCardPropsonCopy コールバックをオプショナルで追加(型: (content: string) => void

UI実装

  • WorktreeDetailRefactored.tsx から HistoryPaneshowToast をprops伝搬(モバイルレイアウト用 L809付近とデスクトップレイアウト用 L1573付近の2箇所の両方に対応すること
  • HistoryPane.tsxshowToast + stripAnsi@/lib/cli-patterns からインポート)+ クリップボード操作を組み合わせた onCopy コールバックを useCallback で作成し、ConversationPairCard に渡す
  • ConversationPairCard.tsx にコピーボタンUI追加:
    • UserMessageSection: 展開ボタンが存在しないため右上にシンプル配置
    • AssistantMessageItem: 既存の展開ボタン(absolute top-2 right-2)との共存を考慮した配置(例: right-10 top-2
    • コピーハンドラーは useCallback で参照安定化すること
  • MessageList.tsx にコピーボタンUI追加(副次的対応)。React.memo カスタム比較関数(L362-371)は修正不要(onCopy は既存の onFilePathClick と同様に比較対象外で問題ない)
  • コピー成功/失敗のToast通知実装
  • lucide-reactの Copy / ClipboardCopy アイコンの選定・配置

テスト

  • ConversationPairCardのコピーボタン表示・クリックイベントのテスト作成
  • 既存テストの修正確認(propsをオプショナルにすることで破損は回避される見込みだが、以下の既存テストファイルを確認し必要に応じて修正):
    • src/components/worktree/__tests__/HistoryPane.integration.test.tsx - HistoryPaneProps変更の影響確認
    • tests/unit/components/HistoryPane.test.tsx - HistoryPaneProps変更の影響確認
    • tests/unit/components/worktree/MessageListOptimistic.test.tsx - MessageBubbleの React.memo カスタム比較関数はonCopy系propsについて修正不要を確認
    • src/components/worktree/__tests__/ConversationPairCard.test.tsx - onCopy propsの追加に伴う既存テスト確認
    • tests/integration/conversation-pair-card.test.tsx - onCopy propsの追加に伴う既存テスト確認。propsはオプショナルのため破損しない見込み

受入条件

Phase 1: ConversationPairCard(必須 - 主要目的の達成)

  • HistoryPaneの各メッセージセクション(UserMessageSection / AssistantMessageItem)にコピーボタンが表示されること
  • コピーボタンクリックでメッセージ内容がクリップボードにプレーンテキストとしてコピーされること
  • ANSIエスケープコードを含むメッセージの場合、既存の stripAnsi() 関数(src/lib/cli-patterns.ts)を使用してコード除去後のテキストがコピーされること
  • コピー成功時にToast通知が表示されること
  • コピー失敗時(Clipboard API非対応等)にエラーToastが表示されること
  • UserMessageSectionではコピーボタンが右上に配置されていること
  • AssistantMessagesSectionではコピーボタンが既存の展開/折りたたみボタンと重ならないこと
  • モバイルレイアウトとデスクトップレイアウトの両方でコピー機能が動作すること
  • コピーコールバックが useCallback で参照安定化されており、不要な再レンダリングが発生しないこと
  • コピーユーティリティ関数の単体テスト(src/lib/__tests__/clipboard-utils.test.ts)が存在し、パスすること
  • ConversationPairCardのコピーボタン表示とクリックイベントのテストが存在し、パスすること
  • 既存テスト(HistoryPane、ConversationPairCard、MessageListOptimistic、conversation-pair-card結合テスト)が全てパスすること

Phase 2: MessageList(副次的 - レガシーコンポーネント対応)

  • MessageList(レガシー表示)にもコピーボタンが追加されていること

影響範囲

変更対象ファイル

ファイル 変更内容
src/components/worktree/ConversationPairCard.tsx コピーボタンUI追加(UserMessageSection: 右上シンプル配置、AssistantMessageItem: 展開ボタンとの共存配置)、Props型に onCopy コールバック追加(オプショナル)、useCallback でハンドラー参照安定化
src/components/worktree/HistoryPane.tsx Props型に showToast コールバック追加(オプショナル、型: (message: string, type?: ToastType, duration?: number) => string)、showToast + stripAnsi@/lib/cli-patterns からインポート)+ クリップボード操作を組み合わせた onCopy コールバックを useCallback で作成しConversationPairCardへ中継
src/components/worktree/WorktreeDetailRefactored.tsx HistoryPaneへ showToast props伝搬。モバイルレイアウト用(renderMobileLeftPaneContent 内 L809付近)とデスクトップレイアウト用(WorktreeDesktopLayout 内 L1573付近)の2箇所の HistoryPane 呼び出しの両方に対応
src/components/worktree/MessageList.tsx コピーボタンUI追加(副次的対応)。React.memo カスタム比較関数(L362-371)は修正不要(onCopy は onFilePathClick と同様に比較対象外で問題ない)
新規: src/lib/clipboard-utils.ts navigator.clipboard.writeText ラッパー。ANSIコード除去は既存の stripAnsi()src/lib/cli-patterns.ts)を再利用(重複実装しない)
新規: src/lib/__tests__/clipboard-utils.test.ts コピーユーティリティ関数の単体テスト(stripAnsi 再利用に合わせたテスト内容)

修正確認が必要な既存テスト

テストファイル 確認内容
src/components/worktree/__tests__/ConversationPairCard.test.tsx onCopy propsの追加に伴う既存テスト確認。propsはオプショナルのため破損しない見込み
src/components/worktree/__tests__/HistoryPane.integration.test.tsx showToast propsの追加に伴う既存テスト確認。propsはオプショナルのため破損しない見込み
tests/unit/components/HistoryPane.test.tsx 同上
tests/unit/components/worktree/MessageListOptimistic.test.tsx MessageBubbleの React.memo カスタム比較関数はonCopy系propsについて修正不要を確認
tests/integration/conversation-pair-card.test.tsx onCopy propsの追加に伴う既存テスト確認。propsはオプショナルのため破損しない見込み

関連コンポーネント(変更不要)

  • src/components/common/Toast.tsx - フィードバック表示(既存活用、変更不要)。showToastuseCallback([], []) で参照安定性が保証済み
  • src/lib/cli-patterns.ts - stripAnsi() 関数を変更なしで再利用。clipboard-utils.ts からインポートするのみ
  • src/types/conversation.ts - ConversationPair型定義(変更不要)
  • src/types/markdown-editor.ts - ToastType型定義(参照のみ、変更不要)

レビュー履歴

Stage 1-2 イテレーション 1 (2026-02-10)

  • 初回レビュー結果を反映

Stage 3 影響範囲分析 (2026-02-10)

  • MF-1: WorktreeDetailRefactored.tsxにおける2箇所のHistoryPane呼び出し(モバイル用L809/デスクトップ用L1573)への対応を明記
  • SF-1: useCallbackによるコピーコールバック参照安定化の考慮を「コールバック参照の安定化」セクションおよび実装タスクに追加
  • SF-2: 既存テストファイル(HistoryPane.integration.test.tsx、HistoryPane.test.tsx、MessageListOptimistic.test.tsx、ConversationPairCard.test.tsx)の修正確認を実装タスク・影響範囲テーブルに追加
  • SF-3: Props設計方針セクションを追加。新規propsは全てオプショナル(?:)として定義し、既存テスト・呼び出し元の破壊的変更を回避

Stage 5-6 イテレーション 2 (2026-02-10)

  • SF-1: showToastの型シグネチャを明記((message: string, type?: ToastType, duration?: number) => string)。HistoryPaneでshowToast + クリップボード操作を組み合わせた onCopy コールバックを作成しConversationPairCardに渡すデータフローを明確化
  • SF-2: UserMessageSection(展開ボタンなし、シンプル右上配置)とAssistantMessagesSection(展開ボタンとの共存配置)でコピーボタン配置方針が異なることを明記
  • NTH-1: ANSIコード除去は正規表現ベースの新規実装であり、既存のAnsiToHtmlライブラリは使用しないことを明記
  • NTH-2: 受入条件をPhase 1(ConversationPairCard、必須)とPhase 2(MessageList、副次的)に分割

Stage 7-8 影響範囲分析 2回目 (2026-02-10)

  • MF-1: ANSIコード除去を新規実装から既存の stripAnsi() 関数(src/lib/cli-patterns.ts)の再利用に変更。重複実装を回避し保守性を向上
  • SF-1: tests/integration/conversation-pair-card.test.tsx を「修正確認が必要な既存テスト」テーブルおよび実装タスクのテストセクションに追加
  • NTH-1: MessageList.tsx の React.memo カスタム比較関数は onCopy 系 props について修正不要であることを明記(onFilePathClick と同様のパターン)
  • NTH-2: showToast の参照安定性が useCallback([], []) により保証されていることを注記

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions