Problem or Motivation
OpenWork already lets you copy user messages (hover → copy) and code blocks (each CodeBlock has a copy button), but a full assistant/agent response cannot be copied. The only hover action on an assistant bubble is "open in new window" (pop-out). To grab a response today you have to manually select the rendered text, which loses the Markdown and is awkward for long, multi-block answers.
Every comparable AI desktop surface offers a per-message copy:
- Codex desktop — "Copy as Markdown" is one of its most-requested features (openai/codex #2880, #17241).
- Claude desktop / ChatGPT desktop — every assistant message has a copy button.
This is a clear, frontend-only parity gap.
Proposed Solution
Add a Copy button to the assistant message bubble's hover actions (next to the existing pop-out control) that copies the response's raw Markdown (message.content) to the clipboard.
- Renderer-only:
navigator.clipboard.writeText(message.content), a 2-second "copied" check state, and a toast.copyFailed on error — mirroring the existing user-message copy and CodeBlock copy patterns.
- Extract an
AssistantMessage component in ChatDisplay.tsx so it can own the copied state (the same reason ErrorMessage was already extracted — a hook can't live in MessageBubble's role switch).
- Only shown on a settled (non-streaming) response.
- Zero new i18n keys — reuses
common.copy, common.copied, toast.copyFailed.
Feasibility: frontend-only. No qwen-code backend change — it reads text already in the rendered message and calls the Web Clipboard API.
Alternatives Considered
- A "Copy as Markdown" entry in a per-message context menu — heavier and less discoverable than a hover button; OpenWork's assistant bubble already has an inline hover-action slot (pop-out), so a sibling button fits the existing pattern.
- Copying the entire conversation — larger surface; per-message copy is the atomic, most-requested unit and can be composed later.
Additional Context
Acceptance / CDP assertion (e2e/assertions/copy-message.assert.ts): seed a user+assistant session on disk (via a new backend-independent seed hook on the e2e harness — the embedded SessionManager loads it on boot), open it, and drive the copy button. Assert that clicking it (a) writes the exact response Markdown to the clipboard and (b) flips the button into its data-copied="true" state — proving it copies the right content and is a real wired action, not just an icon.
Part of the autonomous desktop-feature loop (loop-bot).
Problem or Motivation
OpenWork already lets you copy user messages (hover → copy) and code blocks (each
CodeBlockhas a copy button), but a full assistant/agent response cannot be copied. The only hover action on an assistant bubble is "open in new window" (pop-out). To grab a response today you have to manually select the rendered text, which loses the Markdown and is awkward for long, multi-block answers.Every comparable AI desktop surface offers a per-message copy:
This is a clear, frontend-only parity gap.
Proposed Solution
Add a Copy button to the assistant message bubble's hover actions (next to the existing pop-out control) that copies the response's raw Markdown (
message.content) to the clipboard.navigator.clipboard.writeText(message.content), a 2-second "copied" check state, and atoast.copyFailedon error — mirroring the existing user-message copy andCodeBlockcopy patterns.AssistantMessagecomponent inChatDisplay.tsxso it can own thecopiedstate (the same reasonErrorMessagewas already extracted — a hook can't live inMessageBubble's role switch).common.copy,common.copied,toast.copyFailed.Feasibility: frontend-only. No qwen-code backend change — it reads text already in the rendered message and calls the Web Clipboard API.
Alternatives Considered
Additional Context
Acceptance / CDP assertion (
e2e/assertions/copy-message.assert.ts): seed a user+assistant session on disk (via a new backend-independentseedhook on the e2e harness — the embeddedSessionManagerloads it on boot), open it, and drive the copy button. Assert that clicking it (a) writes the exact response Markdown to the clipboard and (b) flips the button into itsdata-copied="true"state — proving it copies the right content and is a real wired action, not just an icon.Part of the autonomous desktop-feature loop (
loop-bot).