fix(chat): copy reply excludes thinking and tool calls - #1672
Conversation
Copy-reply actions previously included thinking content, tool calls, and the user message in the clipboard. Replace the single copy action in both active entry points (round footer button and context menu) with two actions that only collect assistant text items: - Copy All Reply: all text items across rounds - Copy Final Summary: only the last text item Extract the text-collection into a shared pure helper (collectAssistantText) to avoid duplication and enable unit testing. DOM fallback left unchanged per minimal-fix scope (known limitation for unhydrated historical turns).
limityan
left a comment
There was a problem hiding this comment.
Review Summary
Verdict: Approve — core logic is correct, well-tested, and all CI checks pass (5/5). The findings below are minor polish items, not blockers.
What's good
collectAssistantText.tscorrectly filters onlytype === 'text'items, excluding thinking / tool / user-steering — exactly the fix for #1657.- Shared pure helper avoids duplication between the two active entry points (footer button + context menu).
- 6 unit tests cover all/final modes, exclusion of non-text items, empty-text filtering, no-text → empty, and default mode.
modeparameter flows correctly:FlowChatMenuProvider→globalEventBus.emit→useFlowChatCopyDialoglistener →collectAssistantText.- Dead code (
useCopyDialog.ts,CopyOutputButton.tsx) correctly left untouched. - i18n keys added to all 3 locales (en-US, zh-CN, zh-TW).
Minor items (non-blocking, can be addressed in follow-up)
-
Second copy button missing
ref—copyButtonRefis only on the first button ("Copy All Reply"). Themousedownclick-outside handler (~L252 inModelRoundItem.tsx) checkscopyButtonRef.current.contains(event.target). Whencopiedis set and you click the second button ("Copy Final Summary"),mousedownfires first and resetscopiedtonull(second button is outsidecopyButtonRef), thenclickfireshandleCopy('final')which re-sets it. Final state is correct but there's a brief visual flicker (Check icon → icon → Check icon). Fix: wrap both buttons in a container ref, or add a second ref and check both. -
Indentation regression in
FlowChatMenuProvider.ts— the closing brace ofif (dialogTurn && items.length > 0)changed from 4-space to 3-space (}instead of}). -
Unnecessary
exportonextractAssistantTextinuseFlowChatCopyDialog.ts— the function is exported but never imported elsewhere.ModelRoundItem.tsxuses its own local callback. Consider removingexport. -
anytyping incollectAssistantText.ts—{ modelRounds?: { items?: any[] }[] }could use the existingFlowItemtype union for better type safety, though this is consistent with the pre-existing code style. -
Dangling i18n keys — old
copyDialogkeys are retained (intentional per PR description) but no longer referenced by active code. Clean up when dead code is removed. -
Join separator changed from
'\n\n---\n\n'(round-separated) to'\n\n'(all text items joined). Intentional per PR description, but users who relied on the---separator will see a different format.
Known limitation (accepted by PR)
DOM fallback getElementText in useFlowChatCopyDialog is unchanged. When the store can't resolve a turn (unhydrated historical session), the fallback still reads the full dialog DOM (including thinking/tools). Only affects an edge case.
Overall a clean, well-scoped fix. Ship it. 🚀
Problem
#1657
Copying an agent's reply copied more than the answer: the clipboard included
the model's thinking blocks, tool call inputs/results, and even the
user's own message. Users expected "copy reply" to yield only the
assistant's actual text.
Root cause: all four copy-extract paths iterated
modelRounds[].itemsandkept every item type (
text/thinking/tool/user-steering), plusprefixed the user message. Two of them were active:
ModelRoundItem.tsx(extractDialogTurnContent)FlowChatMenuProvider→useFlowChatCopyDialog.ts(The other two,
CopyOutputButton.tsxandhooks/useCopyDialog.ts, are deadcode and left untouched per scope.)
Solution
Per the agreed minimal-filter + two-button approach: replace the single
copy action in each active entry point with two actions that only collect
type === 'text'items, excluding thinking / tool / user-steering /user-message:
Changes
ModelRoundItem.tsx— footer copy button split into two (Copyicon =all reply,
FileTexticon = final summary);copiedstate promoted to'all' | 'final' | null. Dropped now-dead imports(
projectEffectiveToolItem,formatSessionViewPreviewText).FlowChatMenuProvider.ts— single "复制整个对话" menu item replacedwith two items emitting
flowchat:copy-dialogwithmode: 'all' | 'final'.useFlowChatCopyDialog.ts— listener readsmode; extraction delegatesto the shared helper. DOM fallback (
getElementText) left unchanged perminimal scope.
collectAssistantText.ts(new) — shared pure helper, used by bothactive paths (avoids duplication; makes the logic unit-testable without
dragging in the component module's side effects).
copyAllReply/copyFinalSummaryto thecontextMenuand
modelRoundsections ofen-US,zh-CN,zh-TW. OldcopyDialogkeys retained to avoid dangling references.
Testing
collectAssistantText.test.ts— 6 cases, all passing:all/finalmodes, exclusion of thinking/tool/user-steering, empty-text filtering,
no-text → empty, default mode.
pnpm run type-check:web✅pnpm run i18n:audit✅ (0 warnings)pnpm --dir src/web-ui run test:run src/flow_chat/utils/collectAssistantText.test.ts✅Known limitation (accepted)
Per the minimal-fix scope, the
getElementTextDOM fallback inuseFlowChatCopyDialogis unchanged. In the rare case the store cannotresolve a turn (unhydrated historical session), the fallback still reads the
full dialog DOM (including thinking/tools). This only affects an edge case;
rendered turns come from the store, so normal usage is correct. Can be
hardened later via a DOM-selector fallback if desired.
Checklist