Skip to content

Add a "Copy" (copy-as-Markdown) action to assistant/agent responses #70

Description

@DragonnZhang

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions