Title: Add a copy-as-Markdown action to assistant/agent responses by DragonnZhang · Pull Request #71 · modelstudioai/openwork · GitHub
Open Graph Title: Add a copy-as-Markdown action to assistant/agent responses by DragonnZhang · Pull Request #71 · modelstudioai/openwork
X Title: Add a copy-as-Markdown action to assistant/agent responses by DragonnZhang · Pull Request #71 · modelstudioai/openwork
Description: Closes #70 What & why OpenWork already lets you copy user messages (hover → copy) and code blocks (each CodeBlock has 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 a toast.copyFailed on error — mirroring the existing user-message copy and CodeBlock copy 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 an AssistantMessage component so it can own the copy button's transient copied state (a hook can't live in MessageBubble's role switch — the same reason ErrorMessage was 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 carries data-testid="assistant-copy" and a data-copied attribute for e2e; the aria-label/tooltip flips between common.copy and common.copied. No i18n changes — reuses the existing common.copy, common.copied, and toast.copyFailed keys (1544 keys/locale unchanged). e2e/app.ts + e2e/runner.ts — add a backend-independent seed(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 embedded SessionManager loads 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 vs main is zero: the only errors are the 11 pre-existing ones in apps/electron (auto-update.ts owner/repo, a settings-default-thinking test tuple, two test files importing vitest) — none in the files this PR touches. bun test — the failing set is byte-for-byte identical to main, verified by diffing the sorted unique failure lists from a clean-main run and this branch in the same environment: 56 pre-existing failures on both, diff reports IDENTICAL. This change adds zero new failures. bun run lint:i18n:parity — OK (6 locales, 1544 keys each; no keys added). eslint on ChatDisplay.tsx — 0 errors (6 pre-existing exhaustive-deps warnings, none from the new component). Renderer build (bun run electron:build:renderer, where this change lives) — ✅ builds cleanly. CDP assertion transpiles + loads (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.jsonl under the isolated profile's default-workspace root, which the embedded SessionManager loads on boot): Seed a 1-user + 1-assistant session on disk, then open it from the sidebar (the row selects on mousedown). The assistant bubble's copy button renders and starts in the data-copied="false" state (proof the seeded response loaded, not the empty/draft state). Stub navigator.clipboard.writeText in-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. Assert the button enters its data-copied="true" state — proving it's a real, wired action. ⚠️ Local CDP run was blocked by this environment's egress policy, not by the feature. bun install's Electron postinstall (and thus the app build/launch) fails with 403 Forbidden from the egress proxy when fetching the Electron binary — the org policy allowlists npm/PyPI/crates but not the Electron/GitHub release host. xvfb is present; only the binary fetch is denied (do-not-retry policy denial). The assertion and harness changes transpile and load, and are included so CI / a reviewer can run bun run e2e in an environment with normal network access. Everything that doesn't require launching Electron (typecheck, unit tests, i18n parity, eslint, renderer build) passes with zero delta vs main. Part of the autonomous desktop-feature loop (loop-bot). 🤖 Generated with Claude Code Generated by Claude Code
Open Graph Description: Closes #70 What & why OpenWork already lets you copy user messages (hover → copy) and code blocks (each CodeBlock has a copy button), but a full assistant/agent response had no copy affordance ...
X Description: Closes #70 What & why OpenWork already lets you copy user messages (hover → copy) and code blocks (each CodeBlock has a copy button), but a full assistant/agent response had no copy afforda...
Opengraph URL: https://github.com/modelstudioai/openwork/pull/71
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:02bcaf78-9dd8-4466-dbef-65941076d305 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | C9FC:2BB762:34A113D:4A15C9F:6A54DA24 |
| html-safe-nonce | a76c27d972f9c9cd06b3c3cacfe3472e3ee16bbddf07815d648452ffa8d1ef12 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDOUZDOjJCQjc2MjozNEExMTNEOjRBMTVDOUY6NkE1NERBMjQiLCJ2aXNpdG9yX2lkIjoiMzc0MDA1MzUzNzQxMTA5NTA3NiIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 62f0489499a0707afbdbe3c82a8c16467582402500c624fe60c778ebb8aecff4 |
| hovercard-subject-tag | pull_request:4014922724 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelstudioai/openwork/pull/71/files |
| twitter:image | https://avatars.githubusercontent.com/u/52599892?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/52599892?s=400&v=4 |
| og:image:alt | Closes #70 What & why OpenWork already lets you copy user messages (hover → copy) and code blocks (each CodeBlock has a copy button), but a full assistant/agent response had no copy affordance ... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | a556215b071af6609619e2bbe6f00bdfbf0812ad723b1ae6c301858a7a829f54 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelstudioai/openwork git https://github.com/modelstudioai/openwork.git |
| octolytics-dimension-user_id | 283556804 |
| octolytics-dimension-user_login | modelstudioai |
| octolytics-dimension-repository_id | 1256697754 |
| octolytics-dimension-repository_nwo | modelstudioai/openwork |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1256697754 |
| octolytics-dimension-repository_network_root_nwo | modelstudioai/openwork |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 00f21e38abd06cd6752db5c227083570f72fdb6c |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width