Skip to content

web-ui: port the Beautiful UI primitives into the chat surface - #992

Open
time-attack wants to merge 24 commits into
mainfrom
port-beautiful-ui-itinerary-to-qm
Open

web-ui: port the Beautiful UI primitives into the chat surface#992
time-attack wants to merge 24 commits into
mainfrom
port-beautiful-ui-itinerary-to-qm

Conversation

@time-attack

@time-attack time-attack commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports the Beautiful UI component library (https://www.beautifului.dev/, "crafted primitives for AI-native interfaces") into the web UI, everything except the reference's sidebar. The sidebar keeps its structure and only picks up the shared tokens.

Foundation. shell.css now defines the Beautiful UI palette (page / canvas / surface / inset / ink / line / field, blue / green / orange / red with tint and ink variants, tooltip colours), the 6 / 8 / 10 / 14 px radius scale, the strong ease, and the shared keyframes (pixel-on, shimmer-text, fade-up, fade-in, pop-in, spin). The existing shadcn tokens (--background, --card, --border, --muted-foreground, --sidebar, …) are mapped onto it, so every surface recolours consistently in light and dark. Inter and JetBrains Mono (latin subsets, OFL notice alongside) are hashed through Vite from src/fonts. Net-new component CSS lives in src/styles/*.css, each imported from shell.css and scanned by the same css-vars guardrail (no undefined vars, no new shadows). Imported custom themes emit the same tokens so nothing bleeds through.

Primitives mapped onto existing surfaces.

Reference Where it landed
01 Loading State pixelLoader() (3×3 pixel grid) replaces the wave loader on the chat surface; shimmer label; live elapsed time on the working status
02 Thinking workBlock / thinkingRow unified: sparkle + label + chevron, vertical guide line, grid-rows expansion
03 Streaming Text word fade on the streaming tail; .message-meta becomes the 24 px action row
04 Approval Card composer approval panel is the card with a ‹ 1/N › pager and ghost / primary pills; the transcript marker is the compact version
05 Tool Chips tool rows render label + mono detail chip with status tints; payload cards on inset
06 Task Rows settled work folds render as capsules with completed / stopped / failed / running glyph, tool count, status pill
07 Chat user bubble, assistant typography, tab-strip session header with 28 px icon buttons and count badges, headline empty state
08 Prompt Bar single-row composer: "+" attach, ghost pills for model / harness / effort / fast, circular send / stop; popover rows
09 Recommendation Card connector widget and the runtime-upgrade notice become the card (title, body with entity chip, state pill, primary / ghost pills)
10 Context Cards pasted-text attachments render as context cards ("N characters", 2-line snippet); files as typed source chips
12 Records Table markdown tables render as the records grid: sticky header, row numbers, hairline dividers, alignment honoured
13 Filter Table .resource-tabs are status chips with dots; every list page is a hairline table with tinted status pills
15 Search ⌘K palette restyled (rows, group headers, blue <mark>, kbd chips, shimmer loading)
18 Code Block header with </> glyph + language + Copy, line-number gutter, diff fences tinted, exec output card
19 Fine-tune Card settings page becomes inspector cards with segmented controls and the select row
20 Selection Actions new: select text in a message → floating toolbar (Quote / Explain / Ask / Copy) that inserts into the composer, never auto-submits

Not ported, and why. 11 Diff Table (no tool result carries row-level add/remove semantics), 14 Sidebar Nav (excluded by request), 16 Flowchart (no workflow-builder surface), 17 Insight Cards (no usage or KPI data reaches the web UI), 21 Agent Screen (no screen stream). Loader variants (Dots / Orbit / Surfer), the Pill composer variant, the Task Rows "List" variant, dictation, inline source chips and follow-ups have no counterpart or data in QM.

Review. Three independent lens reviews (control flow and integration; CSS, accessibility and responsive; security and simplification) plus the eight-angle /code-review pipeline ran on the merged diff. Everything confirmed was fixed in the last six commits: no auto-submit from the selection toolbar (it also survives Escape and auto-scroll and resolves read-only from state), the approval pager resets per conversation, the elapsed counter is aria-hidden, expanded folds no longer clip on narrow widths, the chat column fits a phone, phone and reduced-motion overrides moved after the rules they override, fonts honour WEB_UI_BASE, --ink-3 and the status pills meet AA, segmentStatus distinguishes stopped from completed, the code gutter is bounded per frame and refreshes stale diff marks, the shared sanitizer forbids background / bgcolor, and the new jsdom tests share one harness.

Test expectations changed

  • keychain-flow.test.ts: .kc-loading .spinner → asserts the pixel loader is rendered.
  • work-duration-reconciliation.test.ts: import pin widened for elapsedLabel / workStartedAt.
  • work-fold-source.test.ts: .work-divider.work-rows { border-left … } (horizontal divider replaced by the vertical guide).
  • pane-composer-source.test.ts: autosize floor pin updated; pane control size 34 → 28 px.
  • paste-text.test.ts: pasteChipLabel test removed with the function.
  • css-vars.test.ts now also scans src/styles/*.css.

New tests: pixel-loader, approval-pager, tool-chips, context-card-attachments, connector-widget, markdown-tables, session-topbar, search-palette-source, timeline (segment status), text-code (gutter / diff / streaming), settings-page (inspector cards), list-page (filter chips), selection-actions, shared test/dom-harness.ts.

Verification

  • plugins/web-ui: typecheck, full suite (986 tests), build with the default and a custom WEB_UI_BASE; root lint, lint:ox, lint:knip, format:check.
  • Live in a browser-only dev-instance with a seeded conversation covering a tool call, a markdown table, TypeScript and diff fences, a pending approval, the ⌘K palette, list pages and settings, in light and dark and at 390 px.

Screenshots

Before: the previous README hero (docs/screenshots/web-ui-hero.png on main).

After (rendered against the dev instance; full set on the demo/beautiful-ui-screenshots branch):

Chat, dark Chat, light
Approval card Empty state and prompt bar
Search palette Selection toolbar
Settings inspector cards Two sessions side by side
Phone: chat Phone: approval

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Defines the Beautiful UI palette (page/canvas/surface/ink/line/field, blue/green/
orange/red with tints, tooltip colours), radii, easing and shared keyframes in
shell.css and maps the existing shadcn tokens onto it so every surface recolours
consistently in light and dark. Self-hosts the Inter and JetBrains Mono latin
subsets. Splits net-new component CSS into src/styles/*.css, each imported from
shell.css and covered by the css-vars guardrail. Imported custom themes now emit
the same tokens so nothing bleeds through.
Markdown tables in chat and the shared transcript now render in the Beautiful UI Records Table shape: a rounded hairline container that scrolls in both axes with a sticky inset header, a CSS-counter row-number gutter in mono ink-3, 13px tabular-nums cells with a medium-weight first column, hairline row dividers and a hover wash. Column alignment comes from marked's align attribute, which DOMPurify keeps under both sanitizer configs; the new jsdom test renders a table through markdown() to pin that, and the shared-config sanitizer test now covers a table cell too.

The Diff Table is left out: nothing in the web UI or core tool results carries row-level add/remove semantics, so there is no data to tint.
Unify the three thinking renderings on one Thinking primitive: a sparkle
glyph, the label (shimmer while live, "Thought/Worked for Ns" settled) and
a chevron, with the trace body opening via a grid-template-rows 0fr→1fr
transition on ::details-content and every row sitting on a 1px guide line
under the glyph. Per-chunk thinking now renders through thinkingRow, so the
old .thinking rule and the horizontal .work-divider go away.

Work folds that contain tool calls become task capsules: a status glyph
(green check / red X / spinning ring with the call count while live), the
duration label, a right-hand tool count, a Completed/Failed pill and the
chevron. A fold is red when the turn failed or any tool in it failed; that
decision lives in timeline.segmentStatus so it can be unit tested. The
capsule marker rides a data-task attribute because the test suite pins the
literal <details class="work-fold">.

work-fold-source.test.ts: the .work-divider margin pin becomes a pin on the
.work-rows guide line, since the divider element no longer exists.
…fix phone and reduced-motion overrides

Fonts move under src/ so Vite hashes them into assets/ (base-aware, immutable
cache) instead of the no-cache public/ path; the OFL notice ships beside them.
Phone and reduced-motion overrides that lived in styles/*.css could never win
against shell.css because @imports load first, so they move after the rules they
override. The chat column no longer takes its width from the nowrap session
title on phones. Drops the dead caret-blink keyframe, folds mc-spin into spin,
reuses the thinking shimmer for the search palette's loading state, guards the
search timestamp against a missing createdAt, and removes the leftover
data-sheen attribute and cron-filter-empty class.
…theme derivations, harden the shared sanitizer
Tertiary text (--ink-3) moves to 55% / 66% lightness so 11-12.5px labels reach
4.5:1 on the surfaces they sit on. Status pills and diff markers get dedicated
--green-ink / --orange-ink / --red-ink text tokens (also emitted for imported
themes) instead of painting the fill colour as text on its own tint, and the
check / cross glyphs use the surface colour rather than hard-coded white so the
dark theme keeps 3:1. Also repairs the approval-pager test after the harness
dedupe and flattens a nested ternary in the code gutter.
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