-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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で展開ボタンの左隣)
- UserMessageSection: 展開/折りたたみボタンが存在しないため、シンプルに右上配置が可能(例: ヘッダー行の右端、または
- アイコン: lucide-reactの
CopyまたはClipboardCopyアイコンを使用(プロジェクトで既にlucide-reactを採用済み)
MessageList(レガシー表示)
各メッセージバブル(MessageBubble)にも同様のコピーボタンを追加する。ただし、現在の page.tsx は WorktreeDetailRefactored.tsx を使用しており、MessageList.tsx はレガシーコンポーネント(WorktreeDetail.tsx)で使用されているため、ConversationPairCardの対応を主要な変更対象とし、MessageListは副次的な対応とする。
コピー内容の仕様
message.contentの生テキストをそのままコピーするMessageList.tsxではANSIエスケープコードを含むメッセージが存在するため(hasAnsiCodes/convertAnsiToHtml関数の存在から確認)、コピー時にはANSIエスケープコードを除去してからコピーする- ANSIコード除去のアプローチ: 既存の
stripAnsi()関数(src/lib/cli-patterns.tsL205-207)を再利用する。この関数は SGR、OSC、CSI シーケンスに対応した包括的な正規表現パターン(ANSI_PATTERN)を使用しており、プロジェクト内で既に広く利用されている(assistant-response-saver.ts、claude-session.ts、status-detector.ts、response-poller.ts等、少なくとも8ファイルがインポート)。clipboard-utils.tsではimport { stripAnsi } from '@/lib/cli-patterns'としてインポートし、ANSI除去ロジックの重複実装は行わない。既存のAnsiToHtmlライブラリ(MessageList.tsxL18)はANSIをHTML変換する用途であり、プレーンテキスト出力のコピー用途には適さないため使用しない
- ANSIコード除去のアプローチ: 既存の
- Markdownソースはそのままコピーする(レンダリング後のテキストではなく、生のMarkdownテキスト)
Toast通知の統合方針
既存の WorktreeDetailRefactored.tsx(L1309付近)が既に useToast を使用して showToast を内部で利用している。以下の方針でToast機能を統合する:
推奨方針(C案): 親コンポーネントからのprops伝搬
WorktreeDetailRefactored.tsxのshowToastをHistoryPaneにpropsとして渡す- 重要:
WorktreeDetailRefactored.tsxには HistoryPane の呼び出し箇所が2箇所存在する(モバイルレイアウト用:renderMobileLeftPaneContent内 L809付近、デスクトップレイアウト用:WorktreeDesktopLayout内 L1573付近)。両方の呼び出し箇所にshowToastpropsを追加すること。片方だけ対応するとレイアウトによってコピー機能が動作しない不具合となる。
- 重要:
HistoryPaneは受け取ったshowToastを使い、useCallbackでonCopyコールバックを作成してConversationPairCardにpropsとして渡すConversationPairCard内でコピーボタンがクリックされるとonCopyを呼び出す
この方針は既存のパターンとの一貫性が最も高い。
showToast 型シグネチャとデータフロー
showToast の型シグネチャは以下の通り(Toast.tsx L249-261 の useToast フックの返り値):
showToast?: (message: string, type?: 'success' | 'error' | 'info', duration?: number) => string型定義は @/types/markdown-editor.ts の ToastType 型を参照する。
データフローの詳細:
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 フックは showToast を useCallback([], []) で定義しており、コンポーネントの全ライフサイクルで参照が安定している。そのため、HistoryPane での useCallback([showToast]) の依存配列も安定し、onCopy の参照安定性は保たれる(確認済み)。
Props設計方針
新たに追加するprops(showToast、onCopy 等)は全てオプショナル(?:)として定義する。これにより:
- 既存のテストコードや呼び出し元を壊さず段階的に対応できる
showToastが提供されていない場合、コピーボタンはクリップボードへのコピーのみ実行し、Toast通知は省略する(コピーボタン自体は表示する)
コールバック参照の安定化(メモ化考慮)
ConversationPairCard およびそのサブコンポーネント(UserMessageSection、AssistantMessageItem、AssistantMessagesSection)は全て React.memo でラップされている。コピーコールバックを新たにpropsとして追加する場合、各render時に新しいコールバック参照が生成されると memo 化の効果が無効化される。
対応方針: 既存の handleToggle / handleFilePathClick と同様のパターンに従い、useCallback でラップした onCopy ハンドラーをサブコンポーネントに渡す。
MessageList.tsx の React.memo カスタム比較関数について
MessageList.tsx の MessageBubble は React.memo のカスタム比較関数(L362-371)を使用しているが、この比較関数は message.id、message.content、promptData.status、promptData.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.ts(stripAnsi()の再利用に合わせたテスト内容とする。ANSI除去ロジック自体のテストではなく、clipboard-utils が stripAnsi を正しく呼び出してクリップボードに書き込むことの統合テストを中心とする)
Props型定義変更
-
HistoryPanePropsにshowToastコールバックをオプショナルで追加(型:(message: string, type?: ToastType, duration?: number) => string) -
ConversationPairCardPropsにonCopyコールバックをオプショナルで追加(型:(content: string) => void)
UI実装
-
WorktreeDetailRefactored.tsxからHistoryPaneへshowToastをprops伝搬(モバイルレイアウト用 L809付近とデスクトップレイアウト用 L1573付近の2箇所の両方に対応すること) -
HistoryPane.tsxでshowToast+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- フィードバック表示(既存活用、変更不要)。showToastはuseCallback([], [])で参照安定性が保証済み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([], []) により保証されていることを注記