Skip to content

feat(chat): copy the AI answer from the message action bar - #16

Merged
squarezw merged 1 commit into
mainfrom
feat/copy-answer
Aug 14, 2026
Merged

feat(chat): copy the AI answer from the message action bar#16
squarezw merged 1 commit into
mainfrom
feat/copy-answer

Conversation

@squarezw

Copy link
Copy Markdown
Owner

Adds a copy button next to the existing export / like / dislike actions. It copies the answer's markdown — the same content the PDF export uses. Clicking it swaps the icon to a check for two seconds and raises a toast.

The three i18n keys already existed in chat.json / common.json and had no callers: chat.copyAnswer, common.copied, common.copyFailed. This change just puts them to use — no new strings.

Why this isn't a one-line navigator.clipboard call

The async Clipboard API only exists in a secure context. Customer deployments are served over plain HTTP on an internal address (http://192.168.80.3:3001), where navigator.clipboard is undefined and calling it throws a TypeError.

That is the worst kind of failure: the button reacts, nothing is reported, and the clipboard still holds whatever was there before — so the user pastes stale content believing the copy worked.

lib/clipboard.ts therefore falls back to document.execCommand("copy") — deprecated, but still the only path available outside a secure context — and, crucially, returns false when it genuinely cannot copy, so the caller surfaces an error instead of pretending.

Two details that decide whether the fallback works at all:

  • The textarea is moved off-screen rather than display: none. A hidden element cannot be selected, so the copy fails silently.
  • Focus is restored afterwards, or the user loses their place in whatever input they were typing in.

The secure-context check also guards a subtler case: some browsers keep the clipboard object on HTTP but reject every call. Without the check, each copy would produce a rejected promise and a console error before falling back.

Verification

  • 268 tests pass (10 new)
  • 7 mutations, all red: no fallback / no secure-context check / fallback lies about success / textarea left in the DOM / focus not restored / display:none instead of off-screen / empty content still copied
  • One mutation initially survived — "no secure-context check". That test only asserted the end result, which is identical down either path. It now asserts writeText was not called.
  • tsc: non-TS5097 errors 112, unchanged from baseline. The single TS5097 is the .ts import extension that every other test file in this repo also carries.

Not verified

Rendering was not checked in a browser: the local dev server on :3000 runs the private repo, and the chat view requires an authenticated session.

🤖 Generated with Claude Code

Adds a copy button next to the existing export/like/dislike actions. It copies
the answer's markdown — the same content the PDF export uses.

The i18n keys already existed and were unused: chat.copyAnswer, common.copied,
common.copyFailed. No new strings.

## Why this isn't a one-line navigator.clipboard call

The async Clipboard API only exists in a **secure context**. Customer
deployments are served over plain HTTP on an internal address
(http://192.168.80.3:3001), where `navigator.clipboard` is `undefined` and a
direct call throws a TypeError.

That failure mode is the worst kind: the button reacts, nothing is reported,
and the clipboard still holds whatever was there before — so the user pastes
stale content believing the copy worked. `lib/clipboard.ts` therefore falls
back to `document.execCommand("copy")` (deprecated, but still the only path
available outside a secure context) and, crucially, **returns false when it
genuinely cannot copy** so the caller can say so.

Two details that make the fallback actually work:

- The textarea is moved off-screen rather than `display: none` — a hidden
  element cannot be selected, so the copy silently fails.
- Focus is restored afterwards, or the user loses their place in whatever
  input they were typing in.

The secure-context check also guards a subtler case: some browsers keep the
`clipboard` object on HTTP but reject every call. Without the check we'd
produce a rejected promise and a console error on every single copy before
falling back.

## Verification

- 268 tests pass (10 new)
- 7 mutations, all red: no fallback / no secure-context check / fallback lies
  about success / textarea left in the DOM / focus not restored /
  `display:none` instead of off-screen / empty content still copied
- One mutation initially survived — "no secure-context check". That test only
  asserted the end result, which is identical either way. It now asserts
  `writeText` was **not called**.
- tsc: non-TS5097 errors 112, unchanged from baseline. The one TS5097 is the
  `.ts` import extension every other test file in this repo also has.

## Not verified

Rendering was not checked in a browser. The local dev server on :3000 runs the
private repo, and the chat view needs an authenticated session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@squarezw
squarezw merged commit 0b7cd33 into main Aug 14, 2026
1 check passed
@squarezw
squarezw deleted the feat/copy-answer branch August 14, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant