feat(desktop): one polished copy answer: the glyph crossfades to a check, a menu's copy raises a toast
Dean didn't like the "Copied" feedback: it didn't feel polished, and it stayed up too long. In the toolbar, the word Copied stood until the plan next changed. Elsewhere, the label swapped to "Copied" for as long as the pointer stayed on the button. This PR replaces all of it with one shared pattern, the way Linear, GitHub, and Vercel do it.
What changes
Copy buttons (Copy lines on a Code card, Copy on a transcript or agent message)
- The copy glyph crossfades to a check in the success colour (
--state-complete) for 1.2s, then back. - The label and accessible name don't change ("Copy lines" stays "Copy lines"), so nothing shifts width.
- The Tooltip reads "Copied" while the check shows, and a visually hidden status line announces it.
- The crossfade is a short opacity and scale change on the motion tokens, so reduced motion and capture runs hold it still.
- Pressing again restarts the timer from that press. Only the last press's answer is shown, even if the clipboard answers out of order; menu copies work the same way.
Copies from a menu or chord (Copy plan from the ⋯ menu or ⌘⇧C, Copy branch name, Copy checkout command)
- These raise a toast at the bottom centre for 1.5s: "Plan copied", "Branch name copied", or "Checkout command copied". There is no action button.
- The toast rises in on the spring and fades out over
--duration-exit, staying mounted while it fades. It holds still during a capture run, as the tooltip does. - It is the app's existing toast (the feedback dialog's "Thanks — sent", #1928), now in one shared module,
toast.tsx, which the app mounts once. "Thanks — sent" uses it too.
Failure
- On a button, the glyph turns to a quiet alert icon (
--error-text) for the same 1.2s, and the hint reads "Could not copy". - From a menu, the toast reads "Could not copy to the clipboard. Try again." with the alert icon.
Note that Copy plan now lives in the ⋯ menu and the ⌘⇧C chord (moved there in #1877), so there's no button left to morph. It uses the toast, and the toolbar's lingering Copied word is gone.
API (shared with the "Tab context menu + pinned tabs" PR)
useCopyFeedback()returns{ state, copy(words) }. Draw the glyph with<CopyGlyph state={state} />and the hint withcopyHint(state, label).useCopyToast()returnscopyWithToast(words, label). Call it from a menu item'sonSelect.
Both go through the existing ACT_KIND.WINDOW_COPY_TEXT act. Removed as dead code:
- the per-site check marks and "Copied" labels
- the pointer and focus "left" bookkeeping
COPY_SHOWN,CopyOutcome,copyShown,copyPlanDocument, and the plan'sCOPY_FAILED_NOTE- the
.desktop-toolbar-wordstyle - the
copyTextprop threaded throughside-panel→code-pane,plan-transcript, andagent-tab
Timers are rendererRuntime.fn atoms waiting on Effect.sleep, which is the same pattern as the feedback dialog and board save. Tests point the runtime atom at the test's own context, so TestClock.adjust drives them (#testing/copy-feedback).
Tests (through public components, on it.effect + TestClock)
- Code card Copy lines: the clipboard gets the lines, the check shows, and the name stays "Copy lines". Pressed again at 1.0s, the check is still there at 2.0s and gone at 2.2s.
- Refused copy: shows the alert, then reverts after 1.2s.
- Transcript and agent turn copy: the check shows under the button's own name.
- Copy plan through
usePlansTab: the clipboard gets the plan's Markdown and the toast reads "Plan copied". It is still there at 1,499ms and gone after the exit. A refused copy shows the failure toast. - Agent PR menu: each copy names its toast's words.
- I checked that the button tests fail when the hold is changed.
Frames (headless Chrome, the real CodePane / CopyMessageAction / ToastHost over the built styles.css)
Top card at rest, middle card copied (check), bottom card refused (alert), transcript turn copied, and the menu toast.
| Dark | Light |
|---|---|
![]() | ![]() |
![]() | ![]() |
Mac verification
CI could not verify the Mac UI: a PR builds nothing for the Mac. ./scripts/verify.sh was not run, because this was built on a Linux cloud VM. LUKE-159 is the manual Mac pass. ./scripts/check.sh exits 0.
🤖 Generated with Claude Code



