Skip to content

feat: select & copy — cherry-pick parts of a session for export#201

Open
AgarwalPragy wants to merge 6 commits into
matt1398:mainfrom
AgarwalPragy:feat/select-and-copy
Open

feat: select & copy — cherry-pick parts of a session for export#201
AgarwalPragy wants to merge 6 commits into
matt1398:mainfrom
AgarwalPragy:feat/select-and-copy

Conversation

@AgarwalPragy
Copy link
Copy Markdown

@AgarwalPragy AgarwalPragy commented May 14, 2026

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 for conversationExtractor
  • Manually exercised in dev: select / deselect across categories, per-field tool selection, partial-state tristate, Expand All, Copy output verified in a paste target

Notes

  • Tool output is kept full (no preview truncation), so large file reads and bash dumps survive the copy. Bash specifically renders its raw command rather than the full input JSON.
  • "Expand All" is implemented as a per-tab signal counter in tabUISlice rather than pre-computing item IDs in ChatHistory. Each AIChatGroup already has the enhanced display items, so it's the one that reacts and expands them.

Summary by CodeRabbit

  • New Features
    • "Select & Copy…" menu option to enter export-selection mode and pick items/fields to copy
    • Per-item "Include in copy" checkboxes for user messages, AI outputs, and tool results with per-field controls (name, summary, input, output)
    • Tri-state checkboxes and batch-select controls for tool fields (per-item and apply-to-all)
    • "Expand All" in selection mode and a copy action that assembles selected content to clipboard with confirmation

Review Change Stack

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.
@coderabbitai coderabbitai Bot added the feature request New feature or request label May 14, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +127 to +154
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}
/>
);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TriStateCheckbox component is duplicated here and in DisplayItemList.tsx. To improve maintainability and follow the DRY principle, consider moving this component to a shared location, such as src/renderer/components/common/TriStateCheckbox.tsx.

Comment on lines +471 to +501
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);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The setTimeout used for resetting copyConfirmed should be cleared if the component unmounts to prevent state updates on an unmounted component. You can manage this by storing the timeout ID in a useRef and clearing it in a useEffect cleanup function.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Export Selection and Copy Feature

Layer / File(s) Summary
Export-selection infrastructure: context, store, and hooks
src/renderer/contexts/ExportSelectionContext.tsx, src/renderer/store/slices/uiSlice.ts, src/renderer/store/slices/tabUISlice.ts, src/renderer/hooks/useTabUI.ts
ExportSelectionContext provides the selection API (isActive, isSelected, toggle, getToolFields, toggleToolItemField, setToolItemFieldsAll). UISlice adds exportSelectionMode state and open/close actions. TabUISlice adds per-tab expand-all signal and expandManyForTab. useTabUI exposes expand-all and expandMany wrappers to components.
Conversation extraction utility and item types
src/renderer/utils/conversationExtractor.ts, test/renderer/utils/conversationExtractor.test.ts
Defines ExportItem, ToolFieldKey, and ToolExportFields; implements extractExportItems to flatten conversations into exportable items with turn indexing and stable IDs, and assembleToolContent to build markdown from enabled fields. Tests cover formatting, edge cases, and ordering.
ChatHistory export orchestration and menu integration
src/renderer/components/chat/ChatHistory.tsx, src/renderer/components/layout/MoreMenu.tsx
ChatHistory derives exportItems from conversation, manages selectedExportIds and per-tool enabled fields, implements toggles and clipboard copy, and renders the export toolbar wrapped in ExportSelectionContext.Provider. MoreMenu adds "Select & Copy…" to open the mode.
DisplayItemList and tri-state selection
src/renderer/components/chat/DisplayItemList.tsx, src/renderer/components/common/TriStateCheckbox.tsx
DisplayItemList assigns deterministic exportId to extractable items, renders boolean checkboxes for non-tool items and TriStateCheckbox for tool items (driven by per-tool field sets), and forwards exportId to LinkedToolItem when selection is active.
User/AI/Last-output UI integration
src/renderer/components/chat/UserChatGroup.tsx, src/renderer/components/chat/AIChatGroup.tsx, src/renderer/components/chat/LastOutputDisplay.tsx
UserChatGroup shows an "Include in copy" checkbox for user messages. AIChatGroup adds a LastOutputCheckbox (tri-state for tool last-outputs) and an expandAllSignal effect that expands all display items/subagent traces. LastOutputDisplay accepts exportId? and conditionally renders name/output field checkboxes.
Base item and LinkedToolItem wiring
src/renderer/components/chat/items/BaseItem.tsx, src/renderer/components/chat/items/LinkedToolItem.tsx
BaseItem exports BaseItemExportSelection and renders inline field checkboxes (name/summary) when provided, with event-stopping to prevent collapse. LinkedToolItem accepts exportId?, derives exportSelection from context, passes it to BaseItem, and forwards exportId into all tool viewers.
Tool-specific viewers with field selection
src/renderer/components/chat/items/linkedTool/{CollapsibleOutputSection,DefaultToolViewer,EditToolViewer,ReadToolViewer,SkillToolViewer,WriteToolViewer}.tsx
Each viewer accepts an optional exportId and conditionally renders field-level checkboxes (input/output/name/summary) wired to getToolFields/toggleToolItemField so clipboard assembly includes only enabled fields.

Possibly related PRs

Suggested labels

feature request

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 16cc3c8 and b19378e.

📒 Files selected for processing (20)
  • src/renderer/components/chat/AIChatGroup.tsx
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/DisplayItemList.tsx
  • src/renderer/components/chat/LastOutputDisplay.tsx
  • src/renderer/components/chat/UserChatGroup.tsx
  • src/renderer/components/chat/items/BaseItem.tsx
  • src/renderer/components/chat/items/LinkedToolItem.tsx
  • src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
  • src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/SkillToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
  • src/renderer/components/layout/MoreMenu.tsx
  • src/renderer/contexts/ExportSelectionContext.tsx
  • src/renderer/hooks/useTabUI.ts
  • src/renderer/store/slices/tabUISlice.ts
  • src/renderer/store/slices/uiSlice.ts
  • src/renderer/utils/conversationExtractor.ts
  • test/renderer/utils/conversationExtractor.test.ts

Comment thread src/renderer/components/chat/ChatHistory.tsx
Comment thread src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx
Comment thread src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
Comment thread src/renderer/components/chat/items/linkedTool/SkillToolViewer.tsx
Comment thread src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx (1)

24-27: ⚡ Quick win

Use an isXxx type guard instead of asserting toolUseResult shape.

The direct cast at Line 24 bypasses runtime validation. Add a named guard (e.g., isToolUseResult) before reading filePath/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

📥 Commits

Reviewing files that changed from the base of the PR and between b19378e and 3932f58.

📒 Files selected for processing (15)
  • src/renderer/components/chat/AIChatGroup.tsx
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/DisplayItemList.tsx
  • src/renderer/components/chat/LastOutputDisplay.tsx
  • src/renderer/components/chat/UserChatGroup.tsx
  • src/renderer/components/chat/items/BaseItem.tsx
  • src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
  • src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/SkillToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
  • src/renderer/components/common/TriStateCheckbox.tsx
  • src/renderer/hooks/useTabUI.ts
  • src/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

@AgarwalPragy
Copy link
Copy Markdown
Author

The toolUseResult cast in WriteToolViewer is pre-existing on main, not introduced by this PR. If it needs a type guard, that should be a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant