Skip to content

fix(web-ui): correct composer inline reference pills and the '@' picker - #3188

Merged
kev1n77 merged 3 commits into
GCWing:mainfrom
kev1n77:fmy/bugfix
Sep 22, 2026
Merged

kev1n77 merged 3 commits into
GCWing:mainfrom
kev1n77:fmy/bugfix

Conversation

@kev1n77

@kev1n77 kev1n77 commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix three composer input defects that all live in the chat input's inline reference path. A reference pill now survives a clipboard round trip instead of degrading to plain text such as [Skill: pdf], the pill's badge icon, label, and dismiss glyph now sit on one optical row instead of leaving the label visibly low, and the @ reference picker now keeps its navigation and acceptance keys while it is open instead of losing ArrowDown to history navigation and Enter to send.

Type and Areas

Type:

Bug fix / regression fix / UI, UX

Areas:

Web UI — src/web-ui/src/flow_chat composer (ChatInput.tsx, RichTextInput.tsx, RichTextInput.scss) and its key-ownership unit tests. No Rust, mobile web, server, relay, adapter, or installer surface is touched.

Motivation / Impact

All three defects are user-visible in the chat composer, which is the primary input surface.

Copy and paste: copying a message or a composer selection only carried the readable text value, so pasting it back left the capsule source text (for example [Skill: pdf]) behind and the pill was gone. The clipboard now also carries a marked text/html flavor holding the canonical editor tokens, and an in-app paste rebuilds skill, widget, MCP, and additional-mode capsules from it. Foreign HTML is never inserted, a plain-text-only clipboard still degrades to readable text, and copy falls back to the browser default when setData is unavailable.

Pill alignment: the pill centers its children as boxes, so the badge icon and the dismiss glyph already sat on the pill centre, while centering the 12px label line box left the lowercase text body about 1px below that centre because the font ascent and descent are asymmetric. The label is now lifted 1px so the three read as one row. The offset is paint-only, so the pill height, its padding, the label hit box, and the ellipsis behavior are unchanged.

@ picker keys: the picker is an overlay layer, so the overlay coordinator routes its keyboard from the document after the React handlers run. The composer key handler therefore consumed ArrowDown as history navigation and Enter as send before the picker could move or accept a row, which left the list reachable by ArrowUp only and made Enter submit the draft instead of the highlighted reference. The composer now releases the keys the picker owns (ArrowUp, ArrowDown, Enter, Tab) while the picker is active, and keeps its own handling for every other key.

No new dependency, no user-facing string or locale change, no persisted shape, wire protocol, or remote-surface contract change. The clipboard change is additive: it adds a flavor and never inserts foreign markup.

Verification

Automated:

  • pnpm --dir src/web-ui run test:run src/flow_chat/components/RichTextInput.test.tsx → 36 passed.
  • pnpm --dir src/web-ui run test:run src/flow_chat/utils/chatInputKeyOwnership.test.ts src/flow_chat/components/chatInputRegistration.test.ts src/flow_chat/components/ChatInputDraftRecovery.test.ts src/flow_chat/components/ChatInputImageIntake.test.ts src/flow_chat/utils/chatInputQuickSkills.test.ts → 5 files, 20 passed.
  • pnpm run check:web → Canvas SDK check, Appearance contract audit, and tsc --noEmit all pass.
  • pnpm run motion:audit → inventory only, no new entry (the pill nudge is a static transform with no transition).

Geometry evidence for the pill fix, measured in headless Chromium against the real design tokens and the real RichTextInput.scss, in CSS px, pill height 21.39: pill centre 10.70; badge icon ink centre 10.69; dismiss glyph ink centre 10.69; label ink centre 11.70 before the fix and 10.70 after it. The same harness produced before/after captures for a lowercase skill name (cinematography), a file name (RichTextInput.tsx), and a capital-anchored name (Remotion), plus a pixel measurement of the reporter's screenshot.

Manual: the reporter reproduced the pill defects in the running app, including the original reporter screenshot used to locate the label offset. Not covered by automation: the overlay coordinator's document-level event order in the running app. The focused unit tests pin the ownership contract that the composer relies on, but the end-to-end @ picker interaction (ArrowDown, ArrowUp, Enter, Tab) was exercised in the running app by the reporter and is the check to repeat when reviewing.

Reviewer Notes

The branch carries three commits and they can be dropped individually: 64834b5a5 keep composer capsules across a clipboard round trip, af4c6577d optically center the composer reference pill label, cfad8720f let the '@' reference picker own its keys while open.

The 1px pill offset is a deliberate optical nudge, not a geometry fix: the box model was already correct and the icon and dismiss glyph were already centered. The one documented trade-off is that a capital-anchored label without a descender (for example Remotion) now rides about 1px high instead of being perfectly centered, because a lowercase label with descenders and a capital-anchored label need different offsets and one CSS value cannot satisfy both. 0.5px is the softer compromise if maintainers prefer it, at the cost of half-pixel text smoothing on 1x displays.

The picker fix routes key ownership through a small pure helper (chatInputKeyOwnership.ts) instead of relying on listener order, so the ownership contract is testable without mounting the composer. Tab is included because the picker accepts a row with it.

Compatibility and rollback: no persisted data, protocol, or capability change, so no migration or version negotiation is needed; the transform is DPR-independent; revert any commit independently.

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • No user-facing string was added or changed, so no locale update was needed.

Copying a message or a composer selection only carried the readable value, so pasting it back left the capsule source text like [Skill: pdf] behind. The clipboard now also carries a marked text/html flavor holding the canonical editor tokens, and an in-app paste rebuilds skill, widget, MCP, and additional-mode capsules from it. Foreign HTML is never inserted, plain-text-only clipboards keep degrading to readable text, and copy falls back to the browser default when setData is unavailable.
The pill centers its children as boxes, so the badge icon and the dismiss
glyph already sat on the pill centre while the label did not. Centering a
12px label line box leaves the lowercase text body about 1px below that
centre, because the font ascent and descent are asymmetric, which read as a
pill whose text had slipped down.

Lift the label by 1px so the icon, the label ink, and the dismiss glyph
share one optical row. The offset is paint-only, so the pill height, its
padding, and the label hit box stay untouched.
The picker is an overlay layer, so the overlay coordinator routes its
keyboard from the document after the React handlers run. The composer key
handler therefore consumed ArrowDown as history navigation and Enter as
send before the picker could move or accept a row, which left the list
reachable by ArrowUp only and made Enter submit the draft.

Release the keys the picker owns while it is open instead of relying on
event order, and keep the composer path for every other key.
@kev1n77
kev1n77 merged commit 034988a into GCWing:main Sep 22, 2026
9 checks passed
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