feat(chat-thread): copy a turn from its footer, a message from its menu - #3831
feat(chat-thread): copy a turn from its footer, a message from its menu#3831adamleithp wants to merge 5 commits into
Conversation
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
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix 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} |
There was a problem hiding this comment.
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.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)}> |
There was a problem hiding this comment.
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.
PR overviewThis 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 |
|
Merging to
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 |
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: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.MessageContextMenu).The message context menu also carries
SessionView's raw-logs toggle. That menu sits insideSessionView'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 samesessionViewStorestate, so it stays in sync with the outer menu.Styling
Both buttons are one shared
CopyButton: quillButton(variant="default" size="icon-xs") + quillTooltip,text-muted-foregroundin every state. No RadixIconButton, nocolor="green"—@radix-ui/themesis no longer imported byChatThread.tsxat all.Confirmation is an anchored quill toast ("Copied!") above the button, fired off the
copiedflag 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
pxso 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