Skip to content

feat(chat-thread): copy a turn from its footer, a message from its menu - #3831

Open
adamleithp wants to merge 5 commits into
mainfrom
claude/chat-copy-buttons
Open

feat(chat-thread): copy a turn from its footer, a message from its menu#3831
adamleithp wants to merge 5 commits into
mainfrom
claude/chat-copy-buttons

Conversation

@adamleithp

@adamleithp adamleithp commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Stacked on #3829 (claude/chat-message-minimap) — retarget to main once that lands. #3829 carries the minimap and the thread gutter this builds on.

What

Every message row carried a hover copy button floating in its right rail, plus 36px of reserved padding (pr-9) so the button had somewhere to sit. Both are gone. Copy now lives in two places:

  • "Copy turn" in each completed turn's hover footer, beside the timestamp. Copies that turn as plain text — the prompt and the agent's prose; tool calls, thoughts and status rows are skipped (buildTurnCopyText, 5 tests). The windowed body carries the same text on the row that already carries the turn timestamp, so both renderers behave the same.
  • "Copy message" in each user message's own footer, and on right-click for any user or agent message (MessageContextMenu).

The message context menu also carries SessionView's raw-logs toggle. That menu sits inside SessionView's own and wins the event, so without it, right-clicking a message was the one spot in the session where "Show raw logs" went missing. It reads the same sessionViewStore state, so it stays in sync with the outer menu.

Styling

Both buttons are one shared CopyButton: quill Button (variant="default" size="icon-xs") + quill Tooltip, text-muted-foreground in every state. No Radix IconButton, no color="green"@radix-ui/themes is no longer imported by ChatThread.tsx at all.

Confirmation is an anchored quill toast ("Copied!") above the button, fired off the copied flag rather than the click — a rejected clipboard write (unfocused document, blocked permission) never claims success. It dismisses on unmount so a toast can't outlive its anchor.

Row padding: with the gutter from #3829 reserving space on both sides of the scroll content, message rows drop their own px so user rows and turn cards share one edge.

Known gap

SessionView's generic "Copy" item — selection, or the GitHub link under the cursor — is still shadowed on messages; right-clicking a bubble always offers "Copy message". ⌘C still copies a selection. Happy to make "Copy message" selection-aware, or add a separate item, if that's worth it.

Testing

Full UI suite (2169 tests) and workspace typecheck pass. Driven in the running app over CDP: 11 turn buttons and 11 message buttons render, clicking each puts the right text on the clipboard (a message's own text; 2075 chars of prose for a turn), the icon ticks, and the "Copied!" toast renders anchored directly above the button (button at y=748, toast at y=714). Right-click on a message gives ["Copy message", "Show raw logs"]; the toggle opens the raw-logs view, and the outer menu there returns to the conversation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BYPVPkN4Lm3gygchrpcoqr

adamleithp and others added 3 commits July 27, 2026 07:17
The floating "jump to your message" pill only ever offered the single
anchored turn, and only once it had scrolled off. Replace it with a
small rail hugging the scroll container's top-right: one tick per user
message, width scaled by message length, current turn highlighted.

Hovering or keyboard-focusing the rail opens the full list, anchored to
the rail's own corner so it expands in place under the pointer. Rows
ticker-scroll their text on hover rather than clipping it, and picking
one scrolls the thread and stays open for the next hop. Built on
quill's DropdownMenu (Base UI), so roving focus, typeahead and Escape
come with it.

Both bodies render it: the windowed body passes its own jump and anchor
(the engine's reach only mounted rows). ThreadStickyHeader and the now
unused `offscreen` anchor flag are deleted.

The thread reserves CHAT_CONTENT_GUTTER on both sides of its scroll
content so rows stay clear of the rail at any panel width, and turn
rows drop their extra px now that the gutter provides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYPVPkN4Lm3gygchrpcoqr
Hover delay 120ms -> 50ms, close delay 80ms -> 30ms. The list opens
over the rail, so there is no pointer travel a long close delay needs
to protect; both linger read as lag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYPVPkN4Lm3gygchrpcoqr
Every message row carried a hover copy button plus 36px of reserved
right padding for it. Drop both. In its place:

- Each completed turn's hover footer gains a "Copy turn" button beside
  its timestamp, copying that turn as plain text (prompt + agent prose;
  tools and thoughts left out). The windowed body carries the same text
  on the row that already carries the turn timestamp.
- User messages get the same button in their own footer.
- Right-clicking any user or agent message offers "Copy message".

Both buttons are quill Button + Tooltip, muted-foreground in every
state, and confirm with an anchored quill toast ("Copied!") fired off
the copied flag -- so a rejected clipboard write never claims success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYPVPkN4Lm3gygchrpcoqr
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 6878871.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx:630
**Turn copies omit prompts**

When a user copies a completed turn, `groupIntoTurns` has already emitted the user message as a standalone row, so passing only `item.items` to `buildTurnCopyText` copies the agent prose without the associated prompt.

Reviews (1): Last reviewed commit: "feat(chat-thread): copy a turn from its ..." | Re-trigger Greptile

<RowTimestamp timestamp={completedTurnTimestamp(item)} />
<TurnFooter
timestamp={completedTurnTimestamp(item)}
copyText={buildTurnCopyText(item.items) ?? undefined}

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.

P1 Turn copies omit prompts

When a user copies a completed turn, groupIntoTurns has already emitted the user message as a standalone row, so passing only item.items to buildTurnCopyText copies the agent prose without the associated prompt.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/sessions/components/chat-thread/ChatThread.tsx
Line: 630

Comment:
**Turn copies omit prompts**

When a user copies a completed turn, `groupIntoTurns` has already emitted the user message as a standalone row, so passing only `item.items` to `buildTurnCopyText` copies the agent prose without the associated prompt.

How can I resolve this? If you propose a fix, please make it concise.

adamleithp and others added 2 commits July 27, 2026 13:47
The per-message context menu sits inside SessionView's own menu and
wins the event, so right-clicking a message was the one spot in the
session where "Show raw logs" went missing. Carry the toggle (reading
the same sessionViewStore state) alongside "Copy message".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYPVPkN4Lm3gygchrpcoqr
<ContextMenu>
<ContextMenuTrigger render={children} />
<ContextMenuContent>
<ContextMenuItem onClick={() => copy(value)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low: Context-menu copy can preserve stale clipboard data

useCopy writes immediately and silently ignores rejection, but clipboard writes from a context-menu selection can fail while Electron is restoring document focus. An attacker-controlled message can ask the user to copy and paste its text; if this write fails, the paste instead exposes whatever sensitive value was previously on the clipboard. Use the existing deferred copyFromContextMenu helper here and surface its error callback.

@veria-ai

veria-ai Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR overview

This pull request adds controls for copying a chat turn from its footer and copying an individual message from its context menu.

One clipboard-handling issue remains open in the new message context-menu flow. In Electron, a clipboard write may fail during focus restoration, causing a subsequent paste to expose stale clipboard contents instead of the selected message; exploitation requires user interaction and timing-dependent failure conditions. No reported issues have yet been addressed.

Open issues (1)

Fixed/addressed: 0 · PR risk: 4/10

Base automatically changed from claude/chat-message-minimap to main July 27, 2026 13:46
@trunk-io

trunk-io Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant