Add a copy-as-Markdown action to assistant/agent responses#71
Open
DragonnZhang wants to merge 1 commit into
Open
Add a copy-as-Markdown action to assistant/agent responses#71DragonnZhang wants to merge 1 commit into
DragonnZhang wants to merge 1 commit into
Conversation
Assistant/agent responses had only a pop-out hover action and no way to copy them, even though user messages and code blocks were already copyable. Comparable desktop chat surfaces (Codex's much-requested Copy as Markdown, Claude/ChatGPT desktop) all offer a per-message copy. Extract an AssistantMessage component in ChatDisplay so it can own the copy button's transient copied state (mirroring the existing ErrorMessage extraction). The button sits next to the pop-out control, copies the response's raw Markdown via the Clipboard API, shows a 2s check state, and toasts on failure. Only shown on a settled (non-streaming) response. Reuses existing i18n keys (common.copy / common.copied / toast.copyFailed) — no new keys. Also add a backend-independent seed hook to the e2e harness so an assertion can pre-seed an on-disk session that the embedded SessionManager loads on boot, and a CDP assertion that seeds a user+assistant session, clicks the copy button, and asserts the exact response markdown reaches the clipboard and the button enters its copied state. Closes #70
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #70
What & why
OpenWork already lets you copy user messages (hover → copy) and code blocks (each
CodeBlockhas a copy button), but a full assistant/agent response had no copy affordance — the only hover action on an assistant bubble was "open in new window" (pop-out). Grabbing a response meant manually selecting the rendered text, losing the Markdown.Every comparable AI desktop surface offers a per-message copy: Codex desktop ("Copy as Markdown" — openai/codex #2880, #17241, one of its most-requested features) and Claude / ChatGPT desktop. This closes that frontend-only parity gap.
This adds a Copy button to the assistant bubble's hover actions (next to the pop-out control) that copies the response's raw Markdown (
message.content) to the clipboard, with a 2-second "copied" check state and atoast.copyFailedon error — mirroring the existing user-message copy andCodeBlockcopy patterns.Frontend-only. No qwen-code backend change: it reads text already in the rendered message and calls the Web Clipboard API.
Changes
components/app-shell/ChatDisplay.tsx— extract anAssistantMessagecomponent so it can own the copy button's transientcopiedstate (a hook can't live inMessageBubble's role switch — the same reasonErrorMessagewas already extracted). Hover actions are now a small top-right cluster: Copy (new) + the existing pop-out, shown only on a settled (non-streaming) response. The copy button carriesdata-testid="assistant-copy"and adata-copiedattribute for e2e; the aria-label/tooltip flips betweencommon.copyandcommon.copied.common.copy,common.copied, andtoast.copyFailedkeys (1544 keys/locale unchanged).e2e/app.ts+e2e/runner.ts— add a backend-independentseed(profileDirs)hook that runs after the isolated profile is created and before Electron launches, so an assertion can pre-seed on-disk session state that the embeddedSessionManagerloads on boot (no qwen-code needed to render a historical transcript).e2e/assertions/copy-message.assert.ts— new CDP assertion (below).Verification (DoD)
bun run typecheck:all— zero new errors. Delta vsmainis zero: the only errors are the 11 pre-existing ones inapps/electron(auto-update.tsowner/repo, asettings-default-thinkingtest tuple, two test files importingvitest) — none in the files this PR touches.bun test— the failing set is byte-for-byte identical tomain, verified by diffing the sorted unique failure lists from a clean-mainrun and this branch in the same environment: 56 pre-existing failures on both,diffreports IDENTICAL. This change adds zero new failures.bun run lint:i18n:parity— OK (6 locales, 1544 keys each; no keys added).eslintonChatDisplay.tsx— 0 errors (6 pre-existingexhaustive-depswarnings, none from the new component).bun run electron:build:renderer, where this change lives) — ✅ builds cleanly.bun build --target=bun+ dynamic import) — ✅.CDP e2e assertion (
copy-message.assert.ts)Drives the real built app over CDP through the full path (no backend required — the session is pre-seeded as a plain
session.jsonlunder the isolated profile's default-workspace root, which the embeddedSessionManagerloads on boot):mousedown).data-copied="false"state (proof the seeded response loaded, not the empty/draft state).navigator.clipboard.writeTextin-page (so the check is deterministic on a headless host), click the copy button, and assert the exact response Markdown was written to the clipboard — proving it copies the real response text, not merely toggles an icon.data-copied="true"state — proving it's a real, wired action.Part of the autonomous desktop-feature loop (
loop-bot).🤖 Generated with Claude Code
Generated by Claude Code