fix(web-ui): correct composer inline reference pills and the '@' picker - #3188
Merged
Merged
Conversation
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.
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.
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_chatcomposer (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 markedtext/htmlflavor 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 whensetDatais 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, andtsc --noEmitall 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:
64834b5a5keep composer capsules across a clipboard round trip,af4c6577doptically center the composer reference pill label,cfad8720flet 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