Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

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

Merged
trunk-io[bot] merged 4 commits into
mainfrom
claude/chat-copy-buttons
Jul 29, 2026
Merged

feat(chat-thread): copy a turn from its footer, a message from its menu#3831
trunk-io[bot] merged 4 commits into
mainfrom
claude/chat-copy-buttons

Conversation

@adamleithp

@adamleithp adamleithp commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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:

2026-07-27 13 59 09
  • "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. The context-menu copy goes through the deferred copyFromContextMenu (a synchronous write from a closing menu rejects while focus is being restored) and reports both outcomes as toasts.

Row padding: the thread gutter that landed with the minimap (#3829) reserves space on both sides of the scroll content, so message rows drop their own px and 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

Rebased onto main now that the minimap (#3829) has merged, so the diff here is only the copy work. Full UI suite (2333 tests) and workspace typecheck pass after the rebase. 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

@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 4563f4a.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4563f4a. groupIntoTurns now stamps the user-initiated row that opened each turn on the AgentTurn (prompt), and both bodies pass it ahead of the turn's items to buildTurnCopyText, so a copied turn leads with the prompt it answers. Covered by a new flattenTurnRows test.


Generated by Claude Code

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4563f4a. The context-menu item now copies through the deferred copyFromContextMenu helper (same as SessionView's outer menu) and surfaces both outcomes as toasts — "Copied" on success, "Couldn't copy" on rejection — so a failed write can no longer silently leave the clipboard's previous contents in place.


Generated by Claude Code

@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

😎 Merged successfully - details.

@adamleithp
adamleithp force-pushed the claude/chat-copy-buttons branch from 6878871 to f1eac23 Compare July 29, 2026 09:48
adamleithp and others added 3 commits July 29, 2026 09:49
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
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
@adamleithp
adamleithp force-pushed the claude/chat-copy-buttons branch from f1eac23 to fce7bd7 Compare July 29, 2026 09:53
@adamleithp adamleithp added the Stamphog This will request an autostamp by stamphog on small changes label Jul 29, 2026
@stamphog

stamphog Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

🤖 stamphog reviewed fce7bd7d226cf546ae27634724c4b95b7ffd99a6 — verdict: REFUSED

Both unresolved inline review comments hold up against the diff: buildTurnCopyText(item.items) is fed only the agent-turn buffer, which groupIntoTurns never includes the preceding user message in — so "Copy turn" silently omits the prompt, contradicting the PR's own description — and the new context-menu copy calls copy(value) directly instead of the existing deferred copyFromContextMenu helper the codebase already uses elsewhere for this exact stale-clipboard race.

  • 👍 on the PR from greptile-apps[bot].
  • Unresolved @greptile-apps comment (ChatThread.tsx): 'Copy turn' excludes the user prompt because groupIntoTurns emits user messages as standalone rows, never into the agent_turn item buffer passed to buildTurnCopyText — confirmed by reading groupIntoTurns.
  • Unresolved @veria-ai comment (ChatThread.tsx MessageContextMenu): uses useCopy()'s immediate copy() instead of the project's existing copyFromContextMenu deferred helper (already used in SessionView.tsx for the same reason), so a focus-restoration clipboard-write failure can silently leave stale/sensitive clipboard contents.
  • Neither concern is marked resolved/outdated and both are still present in the current diff.
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 382L, 3F substantive, 488L/5F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1d-complex (488L, 5F, single-area, feat)
stamphog 2.0.0b3 .stamphog/policy.yml @ afb8fd5 · reviewed head fce7bd7

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jul 29, 2026
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Both concerns are confirmed by reading the code.

Issue 1 — "Copy turn" silently omits the user prompt

buildTurnCopyText handles user_message items, but its caller never passes them. AgentTurn.items is built by grouping consecutive non-user items — user messages are emitted as standalone TurnRows, not folded into the following agent turn's buffer. So the user_message branch in buildTurnCopyText is dead code in this call path; the function is handed only agent-side items. The PR description says "Copy turn" copies "the prompt and the agent's prose", but it can't — the preceding user message is never in item.items.

Fix: thread the user message forward when building the agent turn (either in groupIntoTurns or as a parallel lookup at the call sites in ThreadRow and flattenTurnRows) and pass it as a prefix to buildTurnCopyText.

Issue 2 — context-menu copy uses copy() directly

copyContextTarget.ts exists precisely for this and is well-commented:

When a Radix ContextMenu.Item is selected, the menu's focus scope is being torn down and the document is momentarily not focused — calling navigator.clipboard.writeText synchronously there rejects with "Document is not focused" in Electron/Chromium, so the clipboard is left unchanged.

MessageContextMenu calls useCopy()'s copy(value) synchronously in the onClick, which hits that same race. The fix is to use copyFromContextMenu from copyContextTarget.ts instead — it defers via setTimeout(0) and accepts success/error callbacks you can use to drive a copied state flag for the toast.

Both issues hold. The copyFromContextMenu one is a latent silent failure on every right-click copy in Electron.

…u copy

Two review fixes:

groupIntoTurns emits the user message as a standalone row, so the turn
footer's copy carried the agent's prose without the prompt it answers.
Each AgentTurn now records the user-initiated row that opened it, and
both bodies pass it ahead of the turn's items to buildTurnCopyText.

The context-menu "Copy message" wrote to the clipboard synchronously
from the closing menu, which rejects in Electron while focus is being
restored — silently leaving the clipboard's previous contents. It now
goes through the deferred copyFromContextMenu helper (same as
SessionView's outer menu) and surfaces both outcomes as toasts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q2H7r8hPC6pFCDrnYkMSRD
@adamleithp
adamleithp enabled auto-merge (squash) July 29, 2026 11:58
@trunk-io
trunk-io Bot merged commit f0e2ed4 into main Jul 29, 2026
33 checks passed
@trunk-io
trunk-io Bot deleted the claude/chat-copy-buttons branch July 29, 2026 12:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants