feat: select & copy — cherry-pick parts of a session for export#201
feat: select & copy — cherry-pick parts of a session for export#201AgarwalPragy wants to merge 6 commits into
Conversation
Useful for session handoffs and debugging write-ups: rather than copying the whole chat, pick the user turns, Claude responses, thinking, and tool calls (or specific tool fields) to compose a clean Markdown summary. Notable choices: - Tool output is kept full — no preview truncation — so large file reads and bash dumps survive the copy. - "Expand All" propagates via a per-tab signal counter rather than pre-computing item IDs in ChatHistory; each AIChatGroup reacts and expands its own enhanced display items.
There was a problem hiding this comment.
Code Review
This pull request introduces a 'Select & Copy' feature for chat conversations, enabling users to selectively export content via a new selection mode and toolbar. Key additions include the ExportSelectionContext for state management, the conversationExtractor utility for Markdown formatting, and granular checkboxes for messages and tool fields. Feedback recommends deduplicating the TriStateCheckbox component, optimizing mass expansion performance by batching store updates, and properly managing timeouts to prevent state updates on unmounted components.
| const TriStateCheckbox = ({ | ||
| checked, | ||
| indeterminate, | ||
| onChange, | ||
| className, | ||
| title, | ||
| }: { | ||
| checked: boolean; | ||
| indeterminate: boolean; | ||
| onChange: () => void; | ||
| className?: string; | ||
| title?: string; | ||
| }): React.JSX.Element => { | ||
| const ref = useRef<HTMLInputElement>(null); | ||
| useEffect(() => { | ||
| if (ref.current) ref.current.indeterminate = indeterminate; | ||
| }, [indeterminate]); | ||
| return ( | ||
| <input | ||
| ref={ref} | ||
| type="checkbox" | ||
| checked={checked} | ||
| onChange={onChange} | ||
| className={className} | ||
| title={title} | ||
| /> | ||
| ); | ||
| }; |
| enhanced.displayItems.forEach((item, i) => { | ||
| let itemId = ''; | ||
| switch (item.type) { | ||
| case 'thinking': | ||
| itemId = `thinking-${i}`; | ||
| break; | ||
| case 'output': | ||
| itemId = `output-${i}`; | ||
| break; | ||
| case 'tool': | ||
| itemId = `tool-${item.tool.id}-${i}`; | ||
| break; | ||
| case 'subagent': | ||
| itemId = `subagent-${item.subagent.id}-${i}`; | ||
| expandSubagentTrace(item.subagent.id); | ||
| break; | ||
| case 'slash': | ||
| itemId = `slash-${item.slash.name}-${i}`; | ||
| break; | ||
| case 'teammate_message': | ||
| itemId = `teammate-${item.teammateMessage.id}-${i}`; | ||
| break; | ||
| case 'subagent_input': | ||
| itemId = `input-${i}`; | ||
| break; | ||
| case 'compact_boundary': | ||
| itemId = `compact-${i}`; | ||
| break; | ||
| } | ||
| if (itemId) expandDisplayItem(aiGroup.id, itemId); | ||
| }); |
There was a problem hiding this comment.
This loop triggers multiple store updates by calling expandDisplayItem and expandSubagentTrace for each item. Since each update involves cloning state objects (Maps/Sets) in the store, this can cause performance issues in large conversations when 'Expand All' is triggered. Consider implementing a batch expansion action in the store that handles multiple IDs in a single state transition.
| try { | ||
| await navigator.clipboard.writeText(text); | ||
| setCopyConfirmed(true); | ||
| setTimeout(() => setCopyConfirmed(false), 2000); |
📝 WalkthroughWalkthroughThis PR introduces an export-selection mode that allows users to selectively copy conversation items to clipboard. Users open it via "Select & Copy…" and get a toolbar with category toggles, per-item checkboxes, per-tool-field controls (name/summary/input/output), Expand All, and a Copy action that assembles markdown. ChangesExport Selection and Copy Feature
Possibly related PRs
Suggested labelsfeature request 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/components/chat/ChatHistory.tsx`:
- Around line 1026-1035: The All/None buttons only call setSelectedExportIds and
leave toolItemFields out of sync; update the handlers for the "All" and "None"
buttons (the onClick that currently calls setSelectedExportIds(new
Set(exportItems.map(i => i.id))) and setSelectedExportIds(new Set())) to also
setToolItemFields accordingly so the invariant between selectedExportIds and
toolItemFields is preserved (mirror selected ids into toolItemFields for all
selected items, and clear toolItemFields when clearing selection); use the
existing toolItemFields state updater function and exportItems to construct the
new toolItemFields mapping.
In `@src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx`:
- Around line 44-54: The export checkboxes in EditToolViewer.tsx (the input
checkbox using inputChecked and toggleToolItemField(exportId!, 'input') and the
corresponding output checkbox around lines 79-87) lack accessible labels; wrap
each checkbox and its adjacent <span> text in a <label> (preferred) or add a
clear aria-label on the <input> so screen readers announce context (e.g.,
"Include diff in copy" and the output equivalent), ensuring the span remains
visually the same but is programmatically associated with the checkbox.
In `@src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx`:
- Around line 66-78: The checkbox in ReadToolViewer lacks a programmatic label
for screen readers; update the JSX around showCheckbox so the input is
associated with a label (e.g., wrap the span and input in a <label> or add
aria-label="Include file content in copy") and ensure the accessible text
matches the existing title string; keep existing bindings
(checked={outputChecked}, onChange={() => toggleToolItemField(exportId!,
'output')}) and do not change exportId or toggleToolItemField behavior.
In `@src/renderer/components/chat/items/linkedTool/SkillToolViewer.tsx`:
- Around line 42-54: The export checkbox in SkillToolViewer is missing an
accessible label; update the checkbox (the input rendered when showCheckbox is
true) to be accessible by either wrapping it in a <label> or adding an
aria-label attribute (e.g., aria-label="Include result in copy") so screen
readers can identify it; ensure the change is applied to the input that uses
checked={outputChecked} and onChange={() => toggleToolItemField(exportId!,
'output')} so behavior remains unchanged.
In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx`:
- Around line 35-47: The checkbox input in WriteToolViewer (controlled by props
showCheckbox, inputChecked, exportId and toggled via toggleToolItemField) lacks
an accessible label; update the input to be screen-reader-accessible by either
wrapping it with a <label> that describes "Include file content in copy" or by
adding an aria-label="Include file content in copy" (and ensure the onChange
still calls toggleToolItemField(exportId!, 'input')); prefer using a label for
visible association or aria-label if a visible label isn't desired.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 55bf4dc5-596e-4404-a8ea-45a9e55d198a
📒 Files selected for processing (20)
src/renderer/components/chat/AIChatGroup.tsxsrc/renderer/components/chat/ChatHistory.tsxsrc/renderer/components/chat/DisplayItemList.tsxsrc/renderer/components/chat/LastOutputDisplay.tsxsrc/renderer/components/chat/UserChatGroup.tsxsrc/renderer/components/chat/items/BaseItem.tsxsrc/renderer/components/chat/items/LinkedToolItem.tsxsrc/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsxsrc/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/EditToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/ReadToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/SkillToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/WriteToolViewer.tsxsrc/renderer/components/layout/MoreMenu.tsxsrc/renderer/contexts/ExportSelectionContext.tsxsrc/renderer/hooks/useTabUI.tssrc/renderer/store/slices/tabUISlice.tssrc/renderer/store/slices/uiSlice.tssrc/renderer/utils/conversationExtractor.tstest/renderer/utils/conversationExtractor.test.ts
Each AIChatGroup previously called expandDisplayItem (and expandSubagentTrace) once per display item, with each call cloning the tabUIStates map and its nested Map/Set entries. For sessions with many items per group, that's O(N) store updates and re-render cycles per group. Adds expandManyForTab in tabUISlice that takes batches of item + subagent IDs and applies them in a single state transition.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx (1)
24-27: ⚡ Quick winUse an
isXxxtype guard instead of assertingtoolUseResultshape.The direct cast at Line 24 bypasses runtime validation. Add a named guard (e.g.,
isToolUseResult) before readingfilePath/content/type.Proposed change
- const toolUseResult = linkedTool.result?.toolUseResult as Record<string, unknown> | undefined; + const isToolUseResult = ( + value: unknown + ): value is { filePath?: string; content?: string; type?: string } => + typeof value === 'object' && value !== null; + + const rawToolUseResult = linkedTool.result?.toolUseResult; + const toolUseResult = isToolUseResult(rawToolUseResult) ? rawToolUseResult : undefined;As per coding guidelines, "Use TypeScript type guards with isXxx naming convention for runtime type checking".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx` around lines 24 - 27, The code currently force-casts linkedTool.result?.toolUseResult to Record<string, unknown> and reads filePath/content/type directly; replace this with a runtime type guard named isToolUseResult that checks the expected shape (presence and types of filePath, content, type) and use it to narrow toolUseResult before deriving filePath, content, and isCreate in WriteToolViewer (use isToolUseResult(linkedTool.result?.toolUseResult) to decide whether to read properties or fallback to linkedTool.input.* defaults).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx`:
- Around line 24-27: The code currently force-casts
linkedTool.result?.toolUseResult to Record<string, unknown> and reads
filePath/content/type directly; replace this with a runtime type guard named
isToolUseResult that checks the expected shape (presence and types of filePath,
content, type) and use it to narrow toolUseResult before deriving filePath,
content, and isCreate in WriteToolViewer (use
isToolUseResult(linkedTool.result?.toolUseResult) to decide whether to read
properties or fallback to linkedTool.input.* defaults).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 56575542-b48e-4bf5-9bd9-3b26e202a460
📒 Files selected for processing (15)
src/renderer/components/chat/AIChatGroup.tsxsrc/renderer/components/chat/ChatHistory.tsxsrc/renderer/components/chat/DisplayItemList.tsxsrc/renderer/components/chat/LastOutputDisplay.tsxsrc/renderer/components/chat/UserChatGroup.tsxsrc/renderer/components/chat/items/BaseItem.tsxsrc/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsxsrc/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/EditToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/ReadToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/SkillToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/WriteToolViewer.tsxsrc/renderer/components/common/TriStateCheckbox.tsxsrc/renderer/hooks/useTabUI.tssrc/renderer/store/slices/tabUISlice.ts
🚧 Files skipped from review as they are similar to previous changes (10)
- src/renderer/components/chat/UserChatGroup.tsx
- src/renderer/components/chat/LastOutputDisplay.tsx
- src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
- src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx
- src/renderer/components/chat/ChatHistory.tsx
- src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
- src/renderer/components/chat/items/BaseItem.tsx
- src/renderer/hooks/useTabUI.ts
- src/renderer/components/chat/AIChatGroup.tsx
- src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
|
The |
Summary
When a Claude Code session is worth sharing — a debugging handoff, a write-up of how something was fixed, a snippet to drop into an issue — the current options are "copy the whole chat" or "screenshot." Both are blunt: most sessions have tangential exploration, large tool outputs, and side trips that aren't worth pasting.
This PR adds an in-place selection mode: a new "Select & Copy…" entry in the MoreMenu activates checkboxes inline next to every selectable item in the chat view. Pick exactly the user turns, Claude responses, thinking, and tool calls (with per-field control over name / intent / input / output), and copy the result as Markdown.
Validation
pnpm typecheck,pnpm lint:fix,pnpm build— all pass (no new errors)pnpm test— 762/762 pass, including 40 new tests forconversationExtractorNotes
commandrather than the full input JSON.tabUISlicerather than pre-computing item IDs inChatHistory. EachAIChatGroupalready has the enhanced display items, so it's the one that reacts and expands them.Summary by CodeRabbit