agentgui 1.0.963 → 1.0.965

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ export const meta = {
2
+ name: 'gui-ux-craft',
3
+ description: 'Hunt UX-craft gaps the 11 prior sweeps never lensed: typography rhythm, copy tone, perceived performance, kit DX, light-theme contrast, interaction density; adversarially verify each against live source',
4
+ whenToUse: 'Run for the 12th maximum-effort agentgui run. Prior sweeps covered jank/redesign/polish/cohesion/completion/logic/parity/files; this one hunts the remaining craft dimensions. Kit lives at /config/workspace/design on this env.',
5
+ phases: [
6
+ { title: 'Hunt', detail: 'one hunter per craft lens against real source in both repos' },
7
+ { title: 'Verify', detail: 'adversarial verifier per finding (kept-typography guard)' },
8
+ ],
9
+ }
10
+
11
+ const KIT = '/config/workspace/design'
12
+ const APP = '/config/workspace/agentgui'
13
+
14
+ const COMMON = `
15
+ You are auditing the agentgui GUI (a Claude-Code-web-class multi-agent chat product) and its design kit.
16
+ Repos: app at ${APP} (client: site/app/js/app.js, site/app/js/backend.js, site/app/index.html; server: lib/http-handler.js, server.js), kit at ${KIT} (src/components/*.js, src/styles/ or *.css at root, scripts/build.mjs; the kit is the published anentrypoint-design library and OWNS all visual CSS - the app only wires state/callbacks).
17
+ Reference bar: claude.ai/code (calm, content-first, quiet chrome, generous thread width).
18
+ HARD GUARDS:
19
+ - The middot separator, the ellipsis char, and em/en dashes in prose are KEPT product typography - never flag them.
20
+ - Status is words + CSS-drawn .status-dot-disc - never propose decorative glyphs.
21
+ - GUI fixes land in the KIT (CSS/components); app gets wiring only; index.html gets NO new !important.
22
+ - Already-filed findings you must NOT duplicate: chat column crush at moderate widths (pane collapse priority), loud PageHeader+lede on every tab, permanent DropZone band, double breadcrumb on files, stadium toolbar pills, duplicate New-chat buttons, cumulative flame rail on history rows, title==project dedupe, ccsniff/same-origin jargon purge, footer status-bar wrap, live empty-state CTA, theme-toggle 'auto (ink)' label, 420px rail sliver regression, composer dangling middot, select truncation, live running-count contradiction, live toolbar stacking, context-pane ungrouped action / zero stats.
23
+ Read the REAL source before claiming anything; every finding needs file:line evidence. Findings must be IMPLEMENTABLE this session (no rewrites).
24
+ `
25
+
26
+ const FINDINGS_SCHEMA = {
27
+ type: 'object',
28
+ required: ['findings'],
29
+ properties: {
30
+ findings: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'object',
34
+ required: ['id', 'title', 'evidence', 'fixLocation', 'fix', 'severity'],
35
+ properties: {
36
+ id: { type: 'string', description: 'kebab-case slug' },
37
+ title: { type: 'string' },
38
+ evidence: { type: 'string', description: 'file:line + what the source shows' },
39
+ fixLocation: { type: 'string', enum: ['kit', 'app', 'server', 'kit+app'] },
40
+ fix: { type: 'string', description: 'concrete implementable change' },
41
+ severity: { type: 'string', enum: ['high', 'medium', 'low'] },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ }
47
+
48
+ const VERDICT_SCHEMA = {
49
+ type: 'object',
50
+ required: ['isReal', 'reason'],
51
+ properties: {
52
+ isReal: { type: 'boolean' },
53
+ reason: { type: 'string' },
54
+ refinedFix: { type: 'string' },
55
+ },
56
+ }
57
+
58
+ const LENSES = [
59
+ {
60
+ key: 'typography-rhythm',
61
+ prompt: `Lens: TYPOGRAPHY + VERTICAL RHYTHM. Audit the kit's type scale, weights, line-heights, and spacing rhythm across app-shell.css and component CSS vs a calm claude.ai/code feel: heading sizes vs body (is the H1 too loud relative to content?), panel/section title treatment (the ALL-CAPS small-caps labels), monospace leakage into non-code UI (footer status bar, stats, crumb), inconsistent font-size tokens between sibling surfaces (rail labels vs list rows vs toolbar), margin/padding rhythm breaking between stacked blocks (header->roots->crumb->toolbar->content gaps on files), and any place two adjacent surfaces use different spacing for the same relationship. 4-8 highest-leverage findings.`,
62
+ },
63
+ {
64
+ key: 'copy-tone',
65
+ prompt: `Lens: COPY TONE + MICROCOPY. Sweep EVERY user-facing string in ${APP}/site/app/js/app.js and kit component defaults (empty states, placeholders, dialog titles/labels, alerts, announce() calls, button labels, settings panel text): inconsistent case (Title vs lower), dev jargon beyond the already-filed ccsniff/same-origin items (sid, cwd shown raw, 'ws clients', 'EACCES', protocol names), unclear or scary error copy without recovery guidance, inconsistent vocabulary for the same concept across tabs (session vs conversation vs chat), placeholder text that states the obvious, dangling or doubled separators in composed strings. 4-8 findings with exact strings.`,
66
+ },
67
+ {
68
+ key: 'perceived-performance',
69
+ prompt: `Lens: PERCEIVED PERFORMANCE. Audit loading/transition feel: which surfaces show NOTHING during their first load (files dir load? agents list? history events? settings health?), skeleton coverage vs spinner vs blank (ConversationList has skeletons - who else needs them), the first-paint sequence of the SPA (what does the user see between index.html and first render - blank? flash of unstyled?), optimistic updates on mutations (rename/delete/mkdir wait for server round-trip - do rows flash?), the markdown stack fetching from jsdelivr on FIRST chat render (visible stall? preload opportunity), tab-switch latency (does navTo re-fetch things it already has?), and streaming-render smoothness. 4-8 findings.`,
70
+ },
71
+ {
72
+ key: 'kit-dx',
73
+ prompt: `Lens: KIT DX - the design SDK itself (${KIT}) as a published library a developer consumes. Audit: prop-naming consistency across components (onMark vs onSelect vs onAction vs onNav - is there a coherent convention?), components exporting inconsistent shapes (some take {children}, some positional), dead/unused exports in src/components.js, undocumented load-bearing props (the README/demo coverage of AgentChat, FileGrid, SessionDashboard, WorkspaceShell - does the demo show the flagship surfaces?), the null-children webjsx crash class (are there remaining unwrapped conditional children arrays in kit components - grep for '? h(' patterns NOT wrapped in filter(Boolean)), CSS class naming drift (.ds-* vs .ws-* vs .chat-* vs .agentchat-* - is the prefixing rule coherent?), and build/lint gaps (lint-tokens/lint-glyphs exist - what obvious lint is missing, e.g. the null-children pattern). 4-8 findings.`,
74
+ },
75
+ {
76
+ key: 'light-theme-contrast',
77
+ prompt: `Lens: LIGHT (paper) THEME FIDELITY. The app has dark (ink) and light (paper) themes; prior runs witnessed almost exclusively dark. Audit the kit token definitions and component CSS for paper-theme issues: status tones (green/purple/flame rails, status-dot-disc variants) - do they meet WCAG 3:1 against the paper bg, code-block / pre backgrounds in light (is there a distinct token or does it reuse dark values), .chat-msg-flat assistant-turn tint visibility in light, scrim/overlay opacity in light, hover/active states that rely on lightening (invisible on light bg), selection tint, hairline visibility (--rule against rgb(246,245,241)), green accent button text contrast, and any hardcoded dark-only color in component CSS that bypasses tokens (grep hex literals in kit CSS). 4-8 findings.`,
78
+ },
79
+ {
80
+ key: 'interaction-density',
81
+ prompt: `Lens: INTERACTION DENSITY + CONTROL CRAFT. Beyond the already-filed toolbar items, audit control sizing/shape coherence kit-wide: how many distinct button treatments exist (stadium pill, r-1 rect, ghost, icon, danger) and whether their usage follows a rule (primary vs secondary vs toolbar vs destructive); Select/SearchInput/filter input heights vs button heights on the same row (do they align?); checkbox/radio custom treatments vs native; dialog button order and destructive placement consistency (ConfirmDialog vs inline Alert confirms - same order?); list-row action affordances (reveal-on-hover vs always-visible - coherent rule incl. coarse pointer?); chip/tag/badge treatments (errors-only chip, permissions tag, status pill, count badge - coherent family?); focus ring consistency across all of these. 4-8 findings.`,
82
+ },
83
+ ]
84
+
85
+ phase('Hunt')
86
+ const results = await pipeline(
87
+ LENSES,
88
+ l => agent(COMMON + '\n' + l.prompt + '\n\nReturn findings via the structured output schema; ids prefixed ' + l.key + '-.', { label: 'hunt:' + l.key, phase: 'Hunt', schema: FINDINGS_SCHEMA }),
89
+ (res, l) => {
90
+ if (!res || !res.findings || !res.findings.length) return []
91
+ return parallel(res.findings.map(f => () =>
92
+ agent(
93
+ COMMON + `\nYou are an ADVERSARIAL VERIFIER. A hunter claims:\nTITLE: ${f.title}\nEVIDENCE: ${f.evidence}\nPROPOSED FIX: ${f.fix}\nfixLocation: ${f.fixLocation}\n\nRead the cited source yourself. Refute it if: the evidence is wrong or stale, the behavior is intentional kept design (especially kept typography: middot/ellipsis/dash), it duplicates an already-filed finding from the guard list, the fix is not implementable this session, or the severity is inflated. Default isReal=false when uncertain. If real, refine the fix to the most concrete minimal change.`,
94
+ { label: 'verify:' + f.id, phase: 'Verify', schema: VERDICT_SCHEMA }
95
+ ).then(v => ({ ...f, verdict: v }))
96
+ ))
97
+ }
98
+ )
99
+ const confirmed = results.flat().filter(Boolean).filter(f => f.verdict && f.verdict.isReal)
100
+ log(`confirmed ${confirmed.length} findings`)
101
+ return { confirmed: confirmed.map(f => ({ id: f.id, title: f.title, severity: f.severity, fixLocation: f.fixLocation, evidence: f.evidence, fix: f.verdict.refinedFix || f.fix, verifierReason: f.verdict.reason })) }
package/AGENTS.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # AgentGUI — Agent Notes
2
2
 
3
+ ## UX-craft sweep + empirical-witness-first (2026-06-11) — twelfth run
4
+
5
+ Two-track run: EMPIRICAL baseline witnessing of the live page found 18 shipped janks BEFORE theorizing, then a fresh hunt workflow `.claude/workflows/gui-ux-craft.js` (6 lenses the 11 prior sweeps never used: typography-rhythm, copy-tone, perceived-performance, kit-DX, light-theme-contrast, interaction-density; 46 agents -> 40 confirmed, `PUNCHLIST-UX.md`). ALL 58 implemented. Server on this env runs `PORT=3009` (3000 owned by another app).
6
+
7
+ **Load-bearing shipped-bug discoveries (the live page as debugger).**
8
+ (1) **The live tab's "1 running over No-live-sessions" contradiction was a webjsx applyDiff CRASH** (reading 'key'), reproduced via `window.__agentgui.render()` in page.evaluate: SessionDashboard swapped its UNKEYED empty-state body for keyed group children, so the diff died mid-apply leaving the header updated and the body stale. Fix: ONE stable keyed `.ds-dash-groups` body across empty/grouped/flat (the ConversationList stable-keyed-body rule) + `Btn` now passes `key` through (it silently DROPPED it before) + no stop control when empty + kit `emptyAction` CTA prop. (2) **The mobile drawers were dead in production**: base `.ws-drawer-toggle/.ws-scrim { display:none }` sat AFTER the media blocks - same specificity, later source order wins regardless of media. CSS-order class of bug; base-hidden rules must PRECEDE their media reveals. (3) **The composer cwd bit was an invisible dead button**: app passes `{label}`, kit read only `.text` - the prop-name-contract failure mode AGENTS.md already warned about; ChatComposer now normalizes text||label and drops empty bits BEFORE separators (kills the dangling middot too). (4) **The 11th run's multi-select checkbox was hover-hidden/clipped in the shipped bundle**: a stale duplicate `.ds-file-check` absolute-overlay block later in app-shell.css clobbered the real in-flow rules. (5) `lint-null-children.mjs` (new build gate) immediately caught **14 latent crash sites in freddie.js**.
9
+
10
+ **Layout architecture: staged column yield.** `.ws-shell` now yields columns to the CONTENT in stages: pane track -> right overlay drawer at <=1480px, sessions track -> left drawer at <=1100px, rail -> fixed 60px icon strip below 900px (labels hidden UNCONDITIONALLY - a 60px rail showing clipped 'age'/'Ne' text was the witnessed 420px regression). The chat thread at 1304px went from ~245px (crushed by a 380px context pane) to 776px. Drawer width is a literal (not --ws-pane-w which is 0 when collapsed); `.ws-pane-open.ws-pane-collapsed` un-hides pane children via `display: revert`.
11
+
12
+ **Content-first surfaces.** `PageHeader` gained `dense` (one row: small h1 + one-line ellipsized lede; 24px instead of ~180px) - all five tabs use it. DropZone with children is a passive wrapper (dashed overlay ONLY during drag; `ds-dropzone--wrap`). Files: ONE breadcrumb (top crumb says just 'files'; in-page BreadcrumbPath navigates), compact ghost toolbar (`.ds-file-toolbar .btn` 32px/r-1), compact right-aligned filter, `.ds-files-stack` 16px rhythm wrapper in app. FileGrid skeleton only on COLD load; refresh dims in place (`is-refreshing`). History rail flame derives from error DENSITY (`sessionErrorDense`: errors>=3 AND >15% of events - tool errors are routine, every real session had 1-2%) and title==project rows drop the duplicate sub.
13
+
14
+ **Theme/token work.** `--flame`/`--amber` are now theme-tuned pairs (light #C53E00/#8A6512 pass AA on paper; dark keeps #FF5A1F/#D9A93A - they were dark-tuned only, 2.9:1/2.0:1 on paper); `--amber` was referenced 8x but NEVER DEFINED (hardcoded fallbacks). Five `--code-*` syntax-token aliases (light AA, dark brights) feed both Prism palettes + static pre. Destructive ACTIONS are warn; flame is exclusively error STATUS. `ThemeToggle` compact says 'theme: auto|light|dark' (ink/paper codenames only in title) + CSS half-disc for icon-only strips.
15
+
16
+ **Control craft.** One 32px/r-1 metric for agentchat-controls, dash-header, file toolbar, BulkBar (quiet warn-outline destructive; arm state goes loud); two compact-button tiers consolidated at chat.css END (later-file wins by order - chat.css loads after app-shell.css in the bundle); focus rings unified on inset accent; `.ds-select` ellipsizes + `:focus-visible`; cancel-before-destructive in app banners (matches ConfirmDialog); history subagent toggle is the kit Checkbox.
17
+
18
+ **Perceived perf.** index.html: 3x modulepreload + jsdelivr preconnect + static boot-splash in #app. AgentChat warms the markdown stack on mount; MdNode paints raw text synchronously while the loader is in flight (streamed tokens were invisible on cold start). EventList gained a loading skeleton (history events pane); agents panel has a loading/error/empty tri-state (`state.agentsLoading`).
19
+
20
+ **Server.** `POST /api/move` (dual realpath confinement, root/self-nesting refusal, 409-unless-overwrite, never overwrites a dir, EXDEV mapped) + `backend.js moveEntry` + BulkBar 'move selected' -> bulk-move PromptDialog -> `runBulkMove` (upfront dest stat, allSettled, failures stay marked). `validate-mutations.mjs` now 26 checks (9 move cases) - run it after touching the mutation surface.
21
+
22
+ **Kit DX.** COMPONENT_API.md gained a 'Workspace Surfaces' section documenting every flagship contract (AgentChat incl. the onPasteFiles-not-onPasteImage trap, WorkspaceShell stableFrame/mainFlush, SessionDashboard, FileGrid selection/density, BulkBar, DropZone wrapper semantics) + an honest CSS-taxonomy section. FileGrid accepts canonical `selected`/`onToggleSelect` (marked/onMark stay as aliases). `Icon`/`iconMarkup` accept the props-object form. 22 deprecated `Btn primary:true` call sites -> `variant:'primary'`; AgentChat `who` -> `role`. Build now runs FOUR lints: tokens, glyphs, null-children, classes (class-prefix taxonomy with a frozen bare-name allowlist).
23
+
24
+ **Witness caveats (this env).** The spool out/ dir keeps STALE responses from prior sessions - a Read racing the watcher's write returns old bytes; re-read when content doesn't match your probe. plugkit browser sessions expire between dispatches (a fresh session needs a full goto). The first git_status after heavy file churn can report stat-cache false positives (empty git_diff resolves them).
25
+
3
26
  ## Files command surface + PASSWORD-boot fix (2026-06-11) — eleventh run
4
27
 
5
28
  Lands the 10th run's deferred batch + the janks a live witness surfaced on this env. **Kit path on this (linux) env is `../design` = `/config/workspace/design`** (same repo as `c:\dev\anentrypoint-design` on the windows env); update flow unchanged: edit kit -> `node scripts/build.mjs` -> copy `dist/247420.{js,css}` into `site/app/vendor/anentrypoint-design/` -> witness -> push both.
@@ -94,17 +117,7 @@ Dependencies:
94
117
 
95
118
  ## GUI audit pass (2026-06-04) — structured chat, predictable presentation
96
119
 
97
- A maximum-effort GUI sweep landed these, GUI changes in the kit `c:\dev\anentrypoint-design` (built+vendored locally), correctness/security in agentgui:
98
-
99
- - **Structured tool cards in chat.** `app.js sendChat` now emits structured parts — `toolPart()` builds `{kind:'tool',name,label,args,status,_id}` and `applyToolResult()` matches a `tool_result` back to its `tool_use` by id (sets `result`+done/error status). The kit `AgentChat` passes structured `{kind,...}` parts straight through to `ChatMessage.renderPart` (only bare strings are wrapped as text), so the kit's collapsible `ToolCallNode` (args/result sections, status icons) renders instead of the old flattened `'-> '+text`. Full args/result live in the card (no 120/160-char truncation).
100
- - **AgentChat kit additions** (`src/components/agent-chat.js`): empty state (`.agentchat-empty*`, title/sub/`suggestions`+`onSuggestionClick`), composer stop button while streaming (`busy`+`onCancel:onStop` into `ChatComposer`), a `working…` tail (`.agentchat-working`) when a content-bearing turn is still streaming (silent tool runs no longer read as frozen). CSS in `chat.css`.
101
- - **Streaming render is rAF-coalesced** via `scheduleStreamRender()` (was render+scroll per token).
102
- - **Multi-turn resume wired end-to-end.** Server `chat.sendMessage` is one-shot (new ephemeral `chat-` id per call); claude's real session id is `parsed.session_id` in the stream. `ws-handlers-util.js onEvent` now broadcasts `streaming_session{claudeSessionId}` once; `backend.js` yields `{type:'session'}`; `app.js` captures it into `state.chat.resumeSid` (claude-code only) + persists, so the next turn `--resume`s.
103
- - **Skip link works.** Kit `AppShell` `<main id=app-main>` now has `tabindex=-1` so the skip-link moves focus, not just scrolls.
104
- - **History:** `state.eventsLimit` (default 300, reset per session) + `load N older` control (no more silently-unreachable older events); expand-all/collapse-all; per-event rail tone (green=normal, purple=tool_use, flame=error) matching the GUI-wide rail semantics; search hits carry `{focusEventI,focusEventTs}` so a click scrolls to + flashes (`.event-flash`) the matched event; mobile drawer auto-closes on session select (`loadSession` removes `.side-open` since DS only auto-closes on `<a>` clicks).
105
- - **Security** (`lib/http-handler.js`): `/api/image` confined to an allowlist root (`CLAUDE_PROJECTS_DIR`, home, `IMAGE_ROOTS`) — was arbitrary-file-read (the `..` check was a no-op after `path.normalize`); CORS no longer advertises `Access-Control-Allow-Origin:*` when `PASSWORD` is set (reflects `CORS_ORIGIN` if set, else same-origin only). Markdown XSS verified inert (DOMPurify strips script/onerror/`javascript:`).
106
- - **Consistency:** unified ellipsis to `…` (kept set); settings health chip uses connection vocab (`connected/connecting/offline`) matching the crumb dot; running-chats panel now shows on chat tab too (extracted `runningPanel()`); aborted-empty turns pruned from persisted/restored chat; restored transcript reopens under its own agent (`restoredAgent`).
107
- - **Workflow:** `.claude/workflows/gui-audit.js` — a reusable multi-agent GUI-audit workflow (per-surface reviewers -> adversarial verify -> synthesize, kit-vs-app fixLocation).
120
+ Landed structured tool cards (toolPart/applyToolResult by id), multi-turn resume wiring (streaming_session -> state.chat.resumeSid), rAF-coalesced streaming, /api/image confinement, history eventsLimit + search-to-event flash. Largely superseded by runs 5-12; full detail in rs-learn (recall "agentgui GUI audit pass 2026-06-04 structured chat").
108
121
 
109
122
  ## Orchestration agents
110
123
 
@@ -152,18 +165,7 @@ Pattern: all imports in lib/plugins/* must be either built-in (crypto, fs, path,
152
165
 
153
166
  ## GM Plugin Autonomy Blocker
154
167
 
155
- **gm plugin's pre-tool-use-hook.js enforces "must invoke gm:gm first" gate, blocking multi-tool autonomy. Hook content is NOT sourced from gm-starter/hooks/ files it is templated from somewhere else.**
156
-
157
- The gm plugin enforces a gate via `.gm/needs-gm` marker that requires invoking `gm:gm` before any other tool use, which fragments multi-tool autonomous sessions in agentgui. A bypass patch was committed to c:/dev/gm (commit e300acf7, origin/main) in gm-starter/hooks/{pre-tool-use,prompt-submit}-hook.js to skip the gate when `.gm/prd.yml` exists, but it did NOT propagate after `/plugin update gm` (cache hash changed 495e36843d77 -> 075e64d58498 but hook content unchanged).
158
-
159
- The actual hook content is generated/templated from c:/dev/gm, likely from `lib/cli-adapter.js` or `platforms/cli-config-shared.js` or `lib/template-builder.js`, not from gm-starter/hooks/. Next session must:
160
- 1. Locate the real hook generator in the gm codebase
161
- 2. Patch it to add `if (autonomous) { try { fs.unlinkSync(needsGmPath); } catch {} }` when `.gm/prd.yml` exists
162
- 3. Rebuild via `node c:/dev/gm/cli.js c:/dev/gm/gm-starter c:/dev/gm/build`
163
- 4. Push changes to c:/dev/gm origin/main
164
- 5. Run `/plugin update gm` in agentgui
165
-
166
- Only after the real generator is patched will agentgui sessions run autonomously without per-tool ceremony.
168
+ gm plugin's pre-tool-use hook gates multi-tool autonomy via a `.gm/needs-gm` marker; the hook content is TEMPLATED from the gm codebase, not gm-starter/hooks/ - patching those files does not propagate. Full diagnosis + fix path in rs-learn (recall "agentgui GM plugin autonomy blocker").
167
169
 
168
170
  ## History Integration via ccsniff (2026-05-21 — reverted from acptoapi)
169
171
 
@@ -0,0 +1,222 @@
1
+ # PUNCHLIST-UX — 12th run: UX-craft sweep (.claude/workflows/gui-ux-craft.js, 46 agents -> 40 adversarially-confirmed findings)
2
+
3
+ Lenses: typography-rhythm, copy-tone, perceived-performance, kit-dx, light-theme-contrast, interaction-density. Every fix below was refined by an adversarial verifier against live source (file:line).
4
+
5
+ ## HIGH
6
+
7
+ ### typography-rhythm-panel-head-body-indent-mismatch [kit]
8
+ Panel head and body sit on different left axes (48px vs 32px) with doubled padding
9
+ - evidence: /config/workspace/design/app-shell.css:504-526 — .panel has its own padding: var(--space-3) (16px), but .panel-head adds padding: var(--space-4) var(--space-5) var(--space-3) (24/32/16) and .panel-body adds padding: var(--space-2) var(--space-3) var(--space-3) (8/16/16). Inside a padded panel the ALL-CAPS head label is inset 16+32=48px from the panel edge while body rows are inset 16+16=32px — the label and its content never share a left edge, and the panel opens with 40px of dead chrome above the label. Agentgui renders Panel({title}) ~10 times (app.js:2373-2898: events, settings, agents), so the misalignment is on every tab. The head paddings were authored for the pre-padding .panel-wide variant (app-shell.css:513,526).
10
+ - fix: In /config/workspace/design/app-shell.css: (1) line 523, change .panel-head padding to `padding: var(--space-2) var(--space-3);` (8px vertical, 16px sides — shares the .panel-body 32px left axis, trims top dead space 40px -> 24px; keep the existing fs-xs/600/caps declarations untouched). (2) line 1041 inside the `@media (min-width: 481px) and (max-width: 1024px)` block, change `.panel-head { padding: var(--space-4) var(--space-4); }` to `.panel-head { padding: var(--space-2) var(--space-3); }` (or delete the line — the base rule now matches the body axis) so the alignment holds at laptop widths; the <=480px block (line 880) is already self-consistent and stays. (3) add `.panel.panel-wide .panel-head { padding-left: 0; padding-right: 0; }` next to the existing `.panel.panel-wide .panel-body { padding: 0; }` (line 533) — defensive for the published kit; no current panel-wide consumer passes a title. Then `node scripts/build.mjs`, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, browser-witness panel head/body left edges align at 32px on settings/history, and push both repos.
11
+
12
+ ### typography-rhythm-row-title-scale-fork [kit]
13
+ List-row primary text renders at three different sizes/weights across sibling surfaces (.row 18px/600 vs files/session rows 15px/500)
14
+ - evidence: /config/workspace/design/app-shell.css:586 — `.row .title { font-weight: 600; font-size: var(--fs-lg) }` (18px) and .row padding 16px 22px (line 537); vs /config/workspace/design/app-shell.css:1177-1178 — `.ds-file-row .title { font-weight: 500; font-size: var(--fs-sm) }` (15px, padding 11px 16px); vs /config/workspace/design/chat.css:347 — `.ds-session-title { font-size: var(--fs-sm) }`; vs app-shell.css:3001 — `.ws-rail-item ... font-size: var(--fs-sm)`. The kit itself already patches the fork locally for one surface: chat.css:388 `.ds-context .row { font-size: var(--fs-sm) }`. History (EventList renders Row, content.js:382) and settings rows therefore shout at 18px semibold — louder than the app H2 floor (--fs-h2-app min 19px, colors_and_type.css:120) — while every other list row is 15px. claude.ai/code keeps all list rows at one quiet body-ish size.
15
+ - fix: Kit, /config/workspace/design/app-shell.css. Add immediately after the .row .title block (after line ~595, keeping the rule next to what it overrides): `[data-typescale="app"] .row { padding: 12px 16px; }` and `[data-typescale="app"] .row .title { font-size: var(--fs-sm); font-weight: 500; }` — the padding/size values are copied verbatim from the kit's own mobile block (app-shell.css:884-890), and font-weight 500 matches .ds-file-row .title exactly (the nested .title .sub stays distinct via 400/--fg-3). Specificity (0,2,1) beats .row .title (0,2,0) so no !important; marketing surfaces without data-typescale="app" keep fs-lg/600. The now-redundant local fork at chat.css:413 (`.ds-context .row { font-size: var(--fs-sm); }`) can be deleted in the same commit. Then node scripts/build.mjs, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, and witness history+settings rows at 15px/500. No app.js or index.html change.
16
+
17
+ ### typography-rhythm-files-stack-zero-gap [kit+app]
18
+ Files tab command stack has no vertical rhythm: 24px after the header, then 0px between roots, toolbar, bulkbar, uploads and grid
19
+ - evidence: /config/workspace/agentgui/site/app/js/app.js:1252-1276 — filesMain stacks PageHeader, then unstyled wrapper divs (keys froots/ftb/fbulk/fup/fbody) directly into .ws-main, which is a flex column with padding only, no gap (/config/workspace/design/app-shell.css:3039). PageHeader's .ds-section-compact contributes margin-bottom: var(--space-4) (app-shell.css:624) but the kit bands carry no stack margin: .ds-file-toolbar (app-shell.css:1355-1358), .ds-roots-picker (chat.css:504), .ds-bulkbar (app-shell.css:1247-1251), .ds-upload-progress (1405) all margin-less. fsbrowse gets its rhythm from .ds-file-stage { gap: var(--space-3) } (app-shell.css:1329-1333) but agentgui's files tab renders no stage, so adjacent control bands touch while the header gap is 24px — the exact stacked-block rhythm break.
20
+ - fix: Kit (/config/workspace/design/app-shell.css, files section near .ds-file-stage ~1338): add one rule `.ds-files-stack { display: flex; flex-direction: column; gap: var(--space-3); min-height: 0; }` - reuses the stage's space-3 beat without its 920px cap/auto-margins (agentgui's files tab is full-width). App (agentgui/site/app/js/app.js filesMain return, lines 1272-1297, wiring only): keep offlineBanner + PageHeader + fileDialog/preview-modal at the top level, and wrap ONLY the five bands in `h('div', { key: 'fstack', class: 'ds-files-stack' }, [rootsRow ? h('div',{key:'froots'},rootsRow) : null, h('div',{key:'ftb'},toolbar), ...fbulk, ...fup, h('div',{key:'fbody'},droppableBody)].filter(Boolean))` - the .filter(Boolean) is load-bearing per the documented webjsx applyDiff null-sibling crash. The 16px after the dense header (ds-page-header-dense margin) then matches the 16px in-stack beat, giving one consistent rhythm; no change to .ws-main (a global gap there would double-space settings/history sections). Then rebuild the kit (node scripts/build.mjs) and re-vendor dist/247420.css into site/app/vendor/anentrypoint-design/.
21
+
22
+ ### perceived-performance-filegrid-skeleton-replaces-loaded-rows [kit]
23
+ Every files refresh (rename/delete/upload/bulk-delete) flashes the populated grid to skeleton rows
24
+ - evidence: Kit /config/workspace/design/src/components/files.js:185 — `if (loading) return FileSkeleton({});` is unconditional: a populated grid (including the sort header, filter input, density radiogroup, and select-all checkbox) is fully replaced by 8 shimmer rows whenever loading=true. App /config/workspace/agentgui/site/app/js/app.js:741 sets `state.files.loading = true; render()` synchronously in loadDir, and every mutation re-runs it in place: app.js:917 (`await loadDir(state.files.path, ...)` after rename/delete/mkdir), :900 (bulk delete), :954 (upload), :971 (overwrite retry). The user renames one file and watches the whole directory blink to placeholders and back — rows flash on every mutation round-trip, and dir-to-dir navigation also drops the toolbar/filter mid-keystroke.
25
+ - fix: Kit-only, three edits. (1) /config/workspace/design/src/components/files.js:185 - change to `if (loading && !files.length) return FileSkeleton({});`. (2) Same function: when `loading && files.length`, add `is-refreshing` to the `.ds-file-grid` root class and set `'aria-busy': 'true'` on it (mirror the existing busy/aria-busy pattern at files.js:130). (3) /config/workspace/design/app-shell.css next to the skeleton rules (~line 1651): `.ds-file-grid.is-refreshing { opacity: .6; }` and `@media (prefers-reduced-motion: no-preference) { .ds-file-grid { transition: opacity .15s var(--ease); } }`. Keep pointer-events (stale-row hazards are already handled by the app pruning marked entries on refresh, app.js:774-777). No app change - app.js:1184 already passes loading. Then rebuild kit (node scripts/build.mjs), copy dist/247420.{js,css} into site/app/vendor/anentrypoint-design/, and browser-witness a rename round-trip showing the grid dims and settles instead of flashing to skeleton.
26
+
27
+ ### perceived-performance-agentchat-markdown-cold-start [kit+app]
28
+ AgentChat never warms the markdown/Prism stack, so the jsdelivr fetch starts mid-first-response
29
+ - evidence: Kit chat.js warms the caches on mount for Chat (/config/workspace/design/src/components/chat.js:531 `ensureCachesInit()`) and AICat (:574), but AgentChat — the component agentgui actually uses — never calls it (/config/workspace/design/src/components/agent-chat.js:142ff has no ensureCachesInit / markdown-cache import; it imports only ChatComposer/ChatMessage/makeThreadAutoScroll, line 14). markdown.js:13-14 loads marked+dompurify from cdn.jsdelivr.net lazily on first renderMarkdown call. Net effect: on an empty chat the CDN round-trip begins only when the FIRST assistant `md` part hits MdNode (chat.js:167) — i.e., exactly while the user is watching their first response stream. index.html (/config/workspace/agentgui/site/app/index.html:15-18) also carries no preconnect to cdn.jsdelivr.net, so the stall includes a cold DNS+TLS handshake.
30
+ - fix: Kit (agent-chat.js): ensureCachesInit is module-local to chat.js, so either (a) export it from chat.js and add it to the agent-chat.js:14 import, or — more minimal — (b) in agent-chat.js add `import { initializeCachesEagerly } from '../markdown-cache.js';` alongside the existing imports, and at the top of AgentChat (agent-chat.js:142, right after the props destructure) call `initializeCachesEagerly().catch((err) => console.warn('[247420] cache init error:', err));` — it is self-idempotent via its internal _initPromise (markdown-cache.js:38-40), so per-render calls are safe and no _cacheInitialized guard is needed. Then `node scripts/build.mjs` and re-vendor dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/ per the standing flow. App (index.html): add `<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>` in the head before the vendored stylesheet link (~line 16) — pure resource hint, no !important, so the first-render module fetch skips DNS+TLS setup.
31
+
32
+ ### perceived-performance-mdnode-blank-bubble-while-loader-inflight [kit]
33
+ MdNode paints an empty bubble until the async markdown stack resolves — streamed text is invisible on cold start
34
+ - evidence: /config/workspace/design/src/components/chat.js:158-169 — MdNode returns `h('div', { class: 'chat-bubble chat-md', ref: refSink })` and only fills content via `renderMarkdownCached(p.text).then((html) => { el.innerHTML = html; ... })`. While the CDN import is in flight (markdown.js:22-41 — the escaped fallback applies only AFTER a load failure, and a failed load backs off 30s, markdown.js:11), every assistant `md` part (agentgui maps all assistant text to kind 'md', app.js:1606-1609) renders as a blank div. On first response over a slow network the user sees tokens arriving as... nothing: an empty bubble with a caret, then a sudden full-message pop-in.
35
+ - fix: Kit, /config/workspace/design/src/components/chat.js MdNode refSink (lines 158-169). After `el.dataset.mdSrc = srcKey;` and before the renderMarkdownCached call, add a synchronous plain-text paint gated on the stack being unavailable (isMarkdownDegraded is already imported and used at line 164):
36
+
37
+ if (el.dataset.mdSrc === srcKey) return;
38
+ el.dataset.mdSrc = srcKey;
39
+ // Markdown stack still loading (or down): paint the raw text synchronously
40
+ // so streamed tokens are visible the same frame they arrive; the resolved
41
+ // render swaps in sanitized markdown in place.
42
+ if (isMarkdownDegraded()) el.textContent = p.text || '';
43
+ renderMarkdownCached(p.text || '').then((html) => { el.innerHTML = html; injectCodeCopy(el); });
44
+
45
+ Why this exact gate: (1) it repaints EVERY streaming delta while the CDN import is in flight (a first-attach-only gate freezes the bubble at the first delta); (2) once the stack loads, isMarkdownDegraded() is false, so settled renders never flash plain text before the cached innerHTML lands; (3) textContent is XSS-safe by construction; (4) the existing '~degraded~' srcKey versioning at line 164 already forces the real-markdown re-render once the loader recovers, so no new invalidation logic is needed. Then rebuild and re-vendor: node scripts/build.mjs in /config/workspace/design, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, browser-witness a cold-start stream.
46
+
47
+ ### kit-dx-flagship-surfaces-undocumented [kit]
48
+ COMPONENT_API.md and preview/ cover zero of the flagship surfaces (AgentChat, WorkspaceShell, SessionDashboard, ConversationList, FileGrid's new props, ContextPane, BulkBar)
49
+ - evidence: /config/workspace/design/COMPONENT_API.md is an 819-line 'Component API Reference' whose section list (Shell/Content/Chat/Editor/Community/Utility, lines 26-690) contains no entry for AgentChat, WorkspaceShell, WorkspaceRail, SessionDashboard, SessionCard, ConversationList, ContextPane, FileGrid, FileToolbar, BulkBar, or FilePreviewPane — grep for those names across COMPONENT_API.md returns nothing. preview/ has file-grid.html but it references none of the load-bearing new props (grep for selectable|density|thumb|marked in preview/file-grid.html: zero hits), and there is no preview page at all for AgentChat/WorkspaceShell/SessionDashboard (grep across preview/ and site/: zero hits). Load-bearing contracts like FileGrid {selectable, marked:Set, onMark(f,{range}), onSelectAll, density, thumbUrl} (src/components/files.js:180-184), WorkspaceShell {stableFrame, mainFlush} (src/components/shell.js:371-374), and AgentChat {shownMessages, onShowEarlier, onPasteFiles, installHint} (src/components/agent-chat.js:142-159) exist only as source comments; the consumer's AGENTS.md records a real integration break this caused (app passed onPasteImage vs kit onPasteFiles, 9th run).
50
+ - fix: fixLocation: kit. Priority 1 (load-bearing, prevents the recorded break class): extend /config/workspace/design/COMPONENT_API.md with a new 'Workspace surfaces' section group mirroring the existing Brand/Btn prop-table format, lifting prose already written as source comments: (a) AgentChat — full destructure at src/components/agent-chat.js:142-159 incl. onPasteFiles/onDropFiles, shownMessages/onShowEarlier, installHint, exportActions, confirmEdit/onArmEdit, followups; (b) WorkspaceShell + WorkspaceRail — comment block shell.js:355-370 plus stableFrame/mainFlush; (c) SessionDashboard + SessionCard; (d) ConversationList; (e) ContextPane; (f) FileGrid new props (selectable, marked:Set keyed by path, onMark(f,{range}), onSelectAll/onClearSelection, density:'list'|'compact'|'thumb', onDensity, thumbUrl) + BulkBar ({count,noun,nounPlural,actions,onClear,busy}) + FilePreviewPane. Also update the section index at the top of the doc. Priority 2 (smaller): extend the EXISTING preview/file-grid.html with three static states — selectable+marked Set, density='compact', density='thumb' with thumbUrl. Priority 3 (only if budget allows): minimal static preview/agent-chat.html, preview/workspace-shell.html, preview/session-dashboard.html pages with canned props; keep them dependency-free like the existing preview pages.
51
+
52
+ ### kit-dx-null-children-lint-missing [kit]
53
+ The webjsx null-children crash class has no lint - the filter(Boolean) discipline is hand-maintained by comments despite two recorded live crashes
54
+ - evidence: scripts/build.mjs:12-13,23-28 wires exactly two guards (lintTokensOrThrow, lintGlyphsOrThrow); package.json scripts (lines 68-71) expose only lint:tokens and build. The crash class is real and recurrent: src/components/sessions.js:180 carries the hand-written rule 'EVERY children array is filter(Boolean)'d: webjsx applyDiff crashes', src/components/shell.js:33-36 (Btn) documents the same crash mode, and the consumer's AGENTS.md records two separate live-witness crashes ("reading 'key'", 4th and 10th runs) from unwrapped conditional children. Compliance today is purely manual: a scan for array literals containing '? h(' ternaries without .filter(Boolean) shows the kit currently clean only because each call site was individually fixed after a crash (e.g. files-modals.js:136-140, chat.js:252-253) - nothing stops the next regression.
55
+ - fix: Add /config/workspace/design/scripts/lint-null-children.mjs, same skeleton as lint-glyphs.mjs (SCAN_DIRS=['src'], .js/.mjs only, per-file ALLOW map, line-numbered failures, exported lintNullChildrenOrThrow + standalone main). Detection rule, kept narrow to avoid false-positives on attr-value ternaries inside {} props objects: bracket-match every top-level array literal `[ ... ]` in the file; flag it iff (a) at least one element-level member is a vnode call (`h(` or an Uppercase `Component(` token at element start), AND (b) at least one element-level member token is `: null` or `? null` (a conditional child), AND (c) the matching `]` is not immediately followed (whitespace-tolerant) by `.filter(`. Element-level means bracket-depth of the array itself, so `{ tabindex: disabled ? '-1' : null }` props never match. Wire it as the third unconditional gate in build.mjs directly after lintGlyphsOrThrow() (import at top alongside the other two, call at ~line 29 with a matching comment: webjsx applyDiff crashes (reading 'key') on a bare null among VElement siblings), and add `"lint:null-children": "node scripts/lint-null-children.mjs"` to package.json scripts. Seed ALLOW with any current intentional hits the first run surfaces (expected: zero, since the tree is clean today). No app/index.html changes; rebuild + re-vendor not required since the lint is build-time only.
56
+
57
+ ### light-theme-contrast-flame-error-tone [kit]
58
+ --flame #FF5A1F is 2.9:1 on paper and is never retuned for light, so every error state (text, disc, rail) fails WCAG in the paper theme
59
+ - evidence: /config/workspace/design/colors_and_type.css:38 defines `--flame: #FF5A1F` once; the [data-theme="ink"/"dark"] block (lines 227-249) and the auto block (255-276) retune --danger but NOT --flame, so light and dark share the same value. #FF5A1F vs --paper #F6F5F1 = 2.86:1 (fails 4.5:1 text AND 3:1 graphics; vs ink it is 5.9:1, i.e. it was tuned on dark — prior runs witnessed almost exclusively dark per AGENTS.md). It is used as TEXT in chat.css:365 (.ds-session-state-error), 393 (.ds-dash-status.is-error), 479/673 (.ds-dash-stream.is-lost/.is-offline), 654 (.agentchat-cwd-hint.is-error), 731 (.chat-tool-pre.is-error), 771 (.ds-dash-breakdown .seg.is-error), and chat.css:717 (.chat-tool.tool-error .chat-tool-status, flame text on a 12% flame tint — even lower contrast); and as GRAPHIC in chat.css:292 (.status-dot-disc.status-dot-error), chat.css:357 + app-shell.css:589 (rail-flame bars), chat.css:400-ish (.ds-dash-card.is-error border).
60
+ - fix: In /config/workspace/design/colors_and_type.css only (zero component-CSS changes — every use already goes through var(--flame)): (1) line 38, change the :root value to a concrete deepened hex, `--flame: #C53E00;` (computed 4.74:1 on --paper, >=4.5 text AA and >=3 graphics; still reads as flame/orange); (2) add `--flame: #FF5A1F;` to the [data-theme="ink"],[data-theme="dark"] block right after the `--danger` line (248); (3) add the same `--flame: #FF5A1F;` line to the @media (prefers-color-scheme: dark) [data-theme="auto"] block (255-276) — the NOTE at 251-254 says the two blocks are intentionally kept separate, so add to both rather than consolidating. paper/thebird (color-scheme: light) correctly inherit the new root value; --cat-flame (line 78) tracks automatically. Do NOT add --flame to the .ds-247420 .ds-247420 inherit block — --danger is not there either, keeping the pattern identical. Then `node scripts/build.mjs`, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, and witness in the paper theme: session-state-error text, dash status/breakdown error segs, tool-error status pill (flame-on-tint), and the error status disc/rail.
61
+
62
+ ### light-theme-contrast-amber-token-undefined [kit]
63
+ --amber is referenced 8 times but never defined; every use falls back to hardcoded #d9a93a which is 2.0:1 on paper (stale/connecting/idle/stopping states near-invisible in light)
64
+ - evidence: grep across colors_and_type.css / app-shell.css / chat.css finds no `--amber:` definition anywhere in the kit — all uses are `var(--amber, #d9a93a)` literal fallbacks that bypass the theme system: chat.css:293-294 (.status-dot-disc.status-dot-connecting/.status-dot-stale disc bg), chat.css:462 (.ds-dash-status.is-stale TEXT), chat.css:479-area .ds-dash-stream.is-connecting TEXT, chat.css:506 (.ds-dash-card.is-stale inset 2px amber bar — the 10th run's 'stale amber inset bar'), chat.css:663 (.ds-dash-status.is-stopping TEXT), chat.css:772 (.ds-dash-breakdown .seg.is-idle TEXT — the header 'M idle' count). #d9a93a vs paper #F6F5F1 = 1.99:1 — fails 3:1 for the 8px disc/inset bar and badly fails 4.5:1 for the status text; the idle/stale/stopping vocabulary is effectively unreadable in the paper theme.
65
+ - fix: In kit colors_and_type.css: (1) add `--amber: #8A6512;` to the base :root status-palette cluster (next to --flame at ~line 38); (2) add `--amber: #D9A93A;` inside the [data-theme="ink"],[data-theme="dark"] block (line 227+) AND inside the @media(prefers-color-scheme:dark) [data-theme="auto"] block (line 256+) so dark keeps the currently-witnessed appearance pixel-for-pixel; (3) per the kit's nested-theme contract (comment at lines 318-328), also add `--amber: #8A6512;` to the [data-theme="paper"] block (line 324) so a paper island under a dark ancestor doesn't inherit the dark amber. Leave all 7 chat.css call-sites unchanged (the #d9a93a fallback becomes dead but harmless). Then `node scripts/build.mjs`, copy dist/247420.{js,css} into agentgui site/app/vendor/anentrypoint-design/, and witness the live dashboard in BOTH themes with a stale session and a connecting stream (confirm the is-idle breakdown count and is-stale inset bar are readable on paper, unchanged on ink).
66
+
67
+ ### interaction-density-stale-file-check-override [kit]
68
+ Stale duplicate .ds-file-check block clobbers the shipped multi-select checkbox (hover-hidden, absolutely positioned, clipped brackets)
69
+ - evidence: Kit app-shell.css has TWO competing .ds-file-check definitions. The live one at /config/workspace/design/app-shell.css:1235-1246 (in-flow 28px mono-bracket button, .is-marked accent) matches what the component emits — files.js only ever sets 'ds-file-check' + 'is-marked' (/config/workspace/design/src/components/files.js:94, 321). A later stale block at app-shell.css:1676-1700 ('Multi-select — per-row checkbox overlay + selected state + bulk bar') re-declares .ds-file-check as position:absolute; left:4px; width/height 18px; font-size:var(--fs-micro); color:var(--accent-fg); opacity:0 revealed only on .ds-file-row:hover/.ds-file-check.on (1679-1690), plus .ds-file-row.selected rules (1697-1700). Grep shows NO component emits '.on' or '.selected' — same specificity, later block wins, so the real checkbox renders as an invisible-until-hover 18px box overlapping the row's left padding, with the [x]/[-] bracket text clipped by fs-micro/18px, and the .is-marked accent text invisible against accent-fg color.
70
+ - fix: In kit /config/workspace/design/app-shell.css: (1) delete the stale absolute-overlay .ds-file-check rule block (lines ~1676-1684) and the .ds-file-row:hover/.ds-file-check.on reveal rule (~1685-1687); (2) delete .ds-file-check.on (~1695) and the .ds-file-row.selected + .ds-file-row.selected:hover rules (~1696-1700); (3) also delete the dead .ds-bulk-bar and .ds-bulk-count rules (~1702-1711) — the component emits .ds-bulkbar/.ds-bulkbar-count which are styled at :1256-1261; (4) KEEP the @media (hover: none), (pointer: coarse) block (~1691-1694) but drop its now-meaningless .ds-file-check { opacity: 1; } line, retaining .ds-file-actions { opacity: 1; } (it is the only coarse-pointer visibility rule for row actions; line 911's lookalike is inside @media (max-width:480px)); (5) keep .ds-file-row { position: relative; } (~1675) — harmless and cheap insurance; keep the drag-to-move .ds-drop-target/draggable rules above (~1665-1672). Then node scripts/build.mjs, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, and browser-witness the files tab: checkbox visible without hover, in-flow 28px, brackets unclipped, marked row tinted via .ds-file-row.is-marked.
71
+
72
+ ## MEDIUM
73
+
74
+ ### typography-rhythm-mono-chrome-leakage [kit]
75
+ Monospace leaks into non-code chrome: the footer status bar and the composer context line set --ff-mono on whole sentences
76
+ - evidence: /config/workspace/design/app-shell.css:384-392 — `.app-status { font-family: var(--ff-mono); font-size: var(--fs-xs); }` renders the whole footer ('same-origin · connected · agent: Claude Code · press ? for shortcuts', app.js:554-557) in JetBrains Mono — prose words, not code. /config/workspace/design/chat.css:423-428 — `.chat-composer-context { font-family: var(--ff-mono); font-size: var(--fs-tiny); }` sets the agent/model/cwd line above the composer fully mono, though only the cwd path is path-like. claude.ai/code reserves mono strictly for code/paths; its status and context chrome are quiet sans. (Distinct from the already-filed footer WRAP finding — this is the typeface.)
77
+ - fix: Kit-only, two CSS edits plus an optional 3-line hook. (1) /config/workspace/design/app-shell.css:388 — change `font-family: var(--ff-mono);` to `font-family: var(--ff-body);` in `.app-status` (keep --fs-xs and color: var(--fg-3) so the nowrap/priority-drop layout at :399-401 is untouched). (2) /config/workspace/design/chat.css:451 — change `font-family: var(--ff-mono);` to `font-family: var(--ff-body);` in `.chat-composer-context`. Since the cwd bit renders a basename word (not a path), no mono carve-out is strictly needed; if path-mono is wanted for future full-path bits, do NOT use the nonexistent data-kind selector — instead in /config/workspace/design/src/components/chat.js bit normalization (~line 458) carry an optional `mono: b.mono` flag and append ' is-mono' to the bit class at lines 471/475, plus chat.css `.chat-composer-context .is-mono { font-family: var(--ff-mono); }`; the app then passes `mono:true` on its cwd bit (app.js:1803-1807, wiring only). Rebuild kit (node scripts/build.mjs), re-vendor dist/247420.{js,css} into site/app/vendor/anentrypoint-design/, browser-witness the footer and composer context line.
78
+
79
+ ### typography-rhythm-caps-label-spec-forks [kit]
80
+ Five different ALL-CAPS micro-label specs for the same 'section kicker' relationship (12-14px, 600-700 weight, .03-.10em tracking)
81
+ - evidence: Same semantic role, five treatments: /config/workspace/design/app-shell.css:517-519 .panel-head (fs-xs 14px / 600 / var(--tr-caps) .06em); /config/workspace/design/chat.css:328 .ds-session-group-label (fs-tiny 13px / normal weight / .04em); chat.css:732 .ds-dash-group-label (fs-micro 12px / 700 / .05em); chat.css:652 .chat-role (fs-tiny / 700 / .05em); chat.css:693 .chat-tool-section-label (fs-micro / 700 / .04em); plus app-shell.css:703 table th (fs-xs / 600 / tr-caps). Adjacent surfaces (live dashboard group label vs sessions rail group label, both visible at once) use different size AND weight for the identical relationship. The 700-weight variants read shouty vs claude.ai/code's single quiet label voice.
82
+ - fix: Kit, chat.css only - converge the four chat-surface kicker labels on ONE spec while leaving .panel-head and table th alone (they are already mutually consistent at fs-xs/600/var(--tr-caps) and serve a larger container-heading role). Set on .ds-session-group-label, .ds-dash-group-label, .chat-role, and .chat-tool-section-label: `font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--fg-3);` keeping each rule's layout props (session-group-label keeps sticky/top/z-index/background/padding; chat-role keeps margin-bottom and the `.chat-msg-flat.you .chat-role { color: var(--accent) }` override; dash-group-label keeps its padding). Net edits: add `font-weight:600` and change .04em->var(--tr-caps) + fs unchanged on .ds-session-group-label; 700->600, fs-micro->fs-tiny, .05em->var(--tr-caps) on .ds-dash-group-label; 700->600, .05em->var(--tr-caps) on .chat-role; 700->600, fs-micro->fs-tiny, .04em->var(--tr-caps) on .chat-tool-section-label. Then `node scripts/build.mjs` and re-vendor dist/247420.{js,css} into site/app/vendor/anentrypoint-design/ per the standard flow.
83
+
84
+ ### copy-tone-sid-jargon [app]
85
+ Raw 'sid' jargon exposed in history detail buttons, meta strip, and copy toast
86
+ - evidence: site/app/js/app.js:2356 Btn 'copy sid'; app.js:2396 SessionMeta `{ label: 'sid', value: state.selectedSid.slice(0, 8) + '…', ... copyText(state.selectedSid, 'sid copied') }`; app.js:1672 banner 'next message continues session ' + resumeSid.slice(0,8) and app.js:1786 composer bit 'continues session ' + sid8. 'sid' is internal shorthand (ccsniff's key name) shown verbatim to users; every other meta label in the same strip ('events', 'turns', 'tools', 'errors') is a real word.
87
+ - fix: In /config/workspace/agentgui/site/app/js/app.js: (1) line 2378 change Btn children fallback 'copy sid' -> 'copy session id'; (2) line 2418 change SessionMeta item label 'sid' -> 'session id' and toast 'sid copied' -> 'session id copied'; (3) leave the 'continues session <sid8>' phrasing in both the banner (1692) and composer bit (1806) as-is per the ninth-run kept wording, but unify the truncation rendering by ADDING the ellipsis to the composer bit at 1806 ('continues session ' + state.chat.resumeSid.slice(0, 8) + '…') rather than removing it from the banner - the meta strip at 2418 also renders sid8 + ellipsis, so 1806 is the lone outlier, and adding (not deleting) the ellipsis is the kept-typography-guard-safe direction. fixLocation: app.
88
+
89
+ ### copy-tone-settings-protocol-jargon [app]
90
+ Settings panels leak transport jargon: 'ws clients', 'projects dir', raw protocol ids, 'port N', 'N restarts'
91
+ - evidence: site/app/js/app.js:2854 `'ws clients: ' + (wsClients != null ? wsClients : 'unknown')`; app.js:2855 `'projects dir: ' + ...`; app.js:2949 agents-row sub starts with `const bits = [a.protocol || 'agent']` so rows read 'acp · running healthy · port 18101' - 'acp' and 'ws' are protocol names a user never chose, and 'port 18101' is server plumbing.
92
+ - fix: All in /config/workspace/agentgui/site/app/js/app.js, no kit change. (1) serverPanel: line 2877 'ws clients: ' -> 'connected clients: '; line 2878 'projects dir: ' -> 'projects folder: '. (2) agentsPanel line 2972: add `const PROTOCOL_WORDS = { acp: 'managed server', cli: 'local CLI' };` (module-level near PRIMARY_AGENTS) and seed `const bits = [PROTOCOL_WORDS[a.protocol] || 'agent'];`. (3) Line 2975: delete the `bits.push('port ' + acp.port)` line — the port is fixed server plumbing (18100-18102) and the kit Row exposes no hover-detail surface to relocate it to. (4) Line 2976: gate and rephrase: `if (acp && acp.restartCount > 1) bits.push('restarted ' + acp.restartCount + ' times');` (a single on-demand restart is routine per the acpnote at line 2966). Keep the middot join at line 2981 unchanged.
93
+
94
+ ### copy-tone-session-vs-conversation-split [app]
95
+ Same list is 'conversations' in the rail but 'sessions' in the adjacent history pane
96
+ - evidence: site/app/js/app.js:681 and :718 placeholder 'Search conversations', :726 emptyText 'No conversations yet', :711 caption 'Resume a conversation in chat' - while the history main pane one column over says 'No sessions yet' / 'Select a session to view its events' / 'Start a chat or run a local coding agent - its session will appear here live.' (app.js:2334-2338) and the header lede 'Pick a session from the sidebar' (app.js:2330). Identical objects, two names on one screen; 'session' should be reserved for the Live tab's running processes (app.js:1501 'Live sessions').
97
+ - fix: App-only string edits in /config/workspace/agentgui/site/app/js/app.js, reserving 'session' for Live-tab running processes: (1) :2355 'Select a session to view its events' -> 'Select a conversation to view its events' and 'No sessions yet' -> 'No conversations yet'; (2) :2358 count + ' session' + (count===1?'':'s') + ' available...' -> count + ' conversation' + (count===1?'':'s') + ' available...'; (3) :2359 '...its session will appear here live.' -> '...its conversation will appear here live.'; (4) :731 history-tab caption 'Browse a session\'s events' -> 'Browse a conversation\'s events'. Leave the lede at :2351 (already says 'conversation'), leave all Live-tab strings (:1521,:1528,:1561, stop announcements :1355-1357), and leave the composer 'continues session <sid8>' context (deliberate resume semantics per AGENTS.md 9th run).
98
+
99
+ ### copy-tone-singular-plural-grammar [app]
100
+ Composed counters read '1 turns', '1 events', '1 restarts' in singular case
101
+ - evidence: site/app/js/app.js:1784 composer context `userTurnCount > 0 ? userTurnCount + ' turns' : null` shows '1 turns' after the first message of every conversation; app.js:2344 history lede `(sess.events || 0) + ' events · ' + (sess.userTurns || 0) + ' turns'` shows '1 events · 1 turns'; app.js:2953 `acp.restartCount + ' restarts'` shows '1 restarts'. The codebase already pluralizes correctly elsewhere (app.js:890 'entry'/'entries', :1335 'session'/'sessions'), so these are inconsistencies, not policy.
102
+ - fix: In site/app/js/app.js, add near the fmt helpers at the top (after line 12 fmtBytes): const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's'); Then apply at exactly four sites: (1) line 1804: userTurnCount > 0 ? plural(userTurnCount, 'turn') : null; (2) line 2365: ... + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ...; (3) line 2461 panel title: plural(total, 'event') + (ef !== 'all' ? ...); (4) line 2976: bits.push(plural(acp.restartCount, 'restart')). Do NOT touch line 2358 (sessions-available) - it already pluralizes. Optionally refactor 910/917/1038/1355/2931 onto the helper, but that is cleanup, not the fix. fixLocation: app.
103
+
104
+ ### perceived-performance-boot-blank-page-no-modulepreload [app]
105
+ SPA boot shows a blank dark page through a serial module waterfall (no boot placeholder, no modulepreload)
106
+ - evidence: /config/workspace/agentgui/site/app/index.html:258-259 — `<div id="app"></div>` is empty and the only script is `<script type="module" src="./js/app.js">`; the head (lines 15-18) has the CSS link and an importmap but NO `<link rel="modulepreload">` for the kit bundle. The browser must fetch+parse app.js (3408 lines) before it discovers the `anentrypoint-design` import and starts fetching the 314,846-byte vendor module (/config/workspace/agentgui/site/app/vendor/anentrypoint-design/247420.js), and first render happens only at `render = mount(...)` (app.js:3254). Between HTML paint and that mount the user stares at a featureless #16161a rectangle with zero indication the app is loading — worst on the PASSWORD-gated cold path where subresources also pay auth.
107
+ - fix: In /config/workspace/agentgui/site/app/index.html only: (1) after the importmap (line 18) add three preload links covering the whole module graph, not just the kit: <link rel="modulepreload" href="./vendor/anentrypoint-design/247420.js">, <link rel="modulepreload" href="./js/backend.js">, <link rel="modulepreload" href="./js/codec.js"> (paths are relative to /gm/ same as the script src, so the PASSWORD cookie set by the document response covers them). (2) Replace line 258 with <div id="app"><div class="boot-splash" role="status">loading agentgui…</div></div> and add to the existing head <style> block (no !important): .boot-splash { height: 100%; display: flex; align-items: center; justify-content: center; color: var(--agentgui-muted); font-size: .9rem; } — static text only (no animation, so no prefers-reduced-motion concern). The kit mount's applyDiff replaces #app's children at first render, so the splash self-removes with no app.js change. Re-witness: load /gm/ with devtools network throttling and confirm the splash paints before mount and the kit fetch starts in parallel with app.js.
108
+
109
+ ### perceived-performance-events-pane-spinner-not-skeleton [kit+app]
110
+ History events — the slowest fetch in the product — blanks the pane and shows a lone spinner while the rail got skeletons
111
+ - evidence: App /config/workspace/agentgui/site/app/js/app.js:3058-3059 — loadSession synchronously clears `state.events = []` on every selection, then app.js:2371-2372 renders only `Spinner({size:'sm'}) 'loading events…'` (a single centered row) for the whole main column; the first events fetch can sit behind ccsniff's 30-90s JSONL walk (the 5s eventsSlow swap at :3073 proves the wait is expected). Meanwhile the kit's ConversationList got shape-matched skeleton rows for exactly this cold walk (/config/workspace/design/src/components/sessions.js:71-76), and EventList itself has no loading affordance at all (/config/workspace/design/src/components/content.js:395 — no `loading` prop). Selecting a session collapses a full event log into near-empty space, then pops a wall of rows in.
112
+ - fix: Kit (/config/workspace/design/src/components/content.js): extend EventList's signature to ({ items, events, emptyText = 'no events', rankPad = 3, loading = false, loadingText = 'loading events…' }); when loading, return the same container with (a) one h('div',{key:'st',role:'status','aria-live':'polite',class:'ds-event-state'},loadingText) and (b) 7 keyed h('div',{key:'sk'+i,class:'ds-event-row-skeleton','aria-hidden':'true'},...) rows each containing a rank stub (span.ds-skel.ds-skel-rank) plus title/meta bars (span.ds-skel.ds-skel-title, span.ds-skel.ds-skel-meta) — mirror ConversationList's keying discipline (single keyed wrapper, all-keyed siblings, sessions.js:65-78) to avoid the webjsx applyDiff key crash. CSS: in the stylesheet that owns EventList rows, add .ds-event-row-skeleton (flex row, gap, padding matching .row) and .ds-skel-rank width ~2.5ch, reusing the existing .ds-skel base + ds-skel-shimmer keyframes (app-shell.css:1133-1139) under the existing prefers-reduced-motion guard pattern (chat.css:783-786). Rebuild via node scripts/build.mjs and re-vendor dist/247420.{js,css} into site/app/vendor/anentrypoint-design/. App (/config/workspace/agentgui/site/app/js/app.js:2407-2413): in the events-empty branch, replace ONLY the !state.eventsLoaded arm (the Spinner row at :2411-2412) with EventList({ loading: true, loadingText: state.eventsSlow ? 'Indexing your Claude history — the first load can take a minute…' : 'loading events…' }) as the Panel children, keeping the eventsLoaded-true "no events in this session" branch and the surrounding head/actions untouched.
113
+
114
+ ### kit-dx-selection-prop-split [kit]
115
+ Identical multi-select contract has two prop vocabularies: FileGrid marked/onMark vs SessionDashboard selected/onToggleSelect
116
+ - evidence: src/components/files.js:53-54 (FileRow) and 183 (FileGrid) name the row-selection contract 'selectable, marked, onMark({range})', while src/components/sessions.js:162 (SessionCard) and 245 (SessionDashboard) name the structurally identical contract 'selectable, selected, onToggleSelect'. Both components then SHARE the other two names (onSelectAll/onClearSelection: files.js:183 and sessions.js:245), so a consumer wiring both surfaces (exactly what agentgui does) must remember which half of the convention each component uses. COMPONENT_API.md's own Prop Naming Standards section (lines 9-24) promises standardized callback naming but never resolves this pair.
117
+ - fix: Kit-only, back-compat. (1) files.js FileGrid (line 183): destructure the canonical names first and alias the legacy ones to them — `selected, onToggleSelect, marked = selected, onMark = onToggleSelect` (later default params may reference earlier ones). Internal FileRow/FileCell plumbing stays on marked/onMark; only FileGrid's public surface gains the canonical pair. The shift-range opts survive unchanged because the per-row callback already passes the entity first: onMark(f, { range }) is signature-compatible with onToggleSelect(item, opts). (2) COMPONENT_API.md Prop Naming Standards section: add a 'Multi-select contract' entry naming the canonical set — selectable (bool), selected (Set), onToggleSelect(item, { range }), onSelectAll(), onClearSelection() — and note marked/onMark as accepted FileGrid aliases. No SessionDashboard change; no app change (existing onMark wiring keeps working).
118
+
119
+ ### kit-dx-css-prefix-rule-incoherent [kit]
120
+ Class-prefix taxonomy has five families plus bare generic leaf classes, and contradicts the documented naming rule
121
+ - evidence: COMPONENT_API.md:786-797 (CSS Classes Reference) claims 'All components follow the pattern: <component>-<element> or ds-<scope>-<element>', but the shipped class families are .ds-* (171 selectors in chat.css alone), .chat-* (105), .agentchat-* (66+), .ws-* (app-shell.css, shell.js:385-447), .app-* (shell.js:181-290), .cm-* (community), plus BARE single-word classes emitted by components: class:'name' (chat.js:194,275,283; files-modals.js:253), 'size' (chat.js:276,284), 'icon' (shell.js:184), 'count' (shell.js:236), 'sep'/'leaf' (shell.js:209-211), 'cap'/'thumb'/'desc'/'tick' (chat.js:270,288,292,354). Because consumer markup lives INSIDE the .ds-247420 scope root (src/index.js:39-43 auto-adds the scope class to the mount root), a consumer's own .name/.size/.icon/.count rules collide with kit internals with no namespace protecting either side.
122
+ - fix: Two minimal kit changes. (1) Rewrite the CSS Classes Reference section in /config/workspace/design/COMPONENT_API.md (the block ending 'All components follow the pattern...') to document the REAL taxonomy: ds-* = scoped primitives, app-* = AppShell chrome, ws-* = WorkspaceShell, chat-/agentchat-/aicat-* = chat surfaces, cm-* = community; plus an explicit 'public utility classes' list (.btn/.btn-primary/.btn-ghost/.row/.panel/.seg — intentional, stable API) and a 'legacy bare internals' note (name,size,icon,count,sep,leaf,cap,thumb,desc,tick — always styled under a prefixed parent, frozen, do not add new ones; consumers should avoid global rules on these words inside the mount root). (2) Add /config/workspace/design/scripts/lint-classes.mjs mirroring lint-glyphs.mjs: scan src/components/*.js for class string literals (class: '...' and className patterns, splitting on whitespace), fail the build on any token that neither starts with an approved prefix (ds-|app-|ws-|chat-|agentchat-|aicat-|cm-|btn|row|panel|seg|crumb) nor appears in a frozen allowlist seeded from the current bare names (name,size,icon,count,sep,leaf,cap,thumb,desc,tick,active,show,sub,meta,spread,token,select,is-*,rail-* and the other existing literals enumerated at seed time via the script's own first run). Wire lintClassesOrThrow() into scripts/build.mjs next to lintTokensOrThrow()/lintGlyphsOrThrow() (lines 23/28). Zero shipped-CSS/JS behavior change; drift stops growing at the next build.
123
+
124
+ ### light-theme-contrast-syntax-token-palette [kit]
125
+ Code syntax token colors (preview Prism palette, chat-code Prism palette, static pre .k/.s) reuse dark-tuned brights: strings 3.3:1, keywords 4.0:1, functions 2.9:1, sun-strings 1.5:1 on paper
126
+ - evidence: chat.css:794 `.ds-preview-code .token.string...` uses var(--green-2) #3A9A34 = 3.29:1 on paper; chat.css:795 keyword/atrule uses var(--sky) #3A6EFF = 3.96:1; chat.css:796 function/class-name uses var(--flame) #FF5A1F = 2.86:1 (all code-size text needing 4.5:1). app-shell.css:1943-1944 `.chat-bubble.chat-code .token.string,.token.attr-value { color: var(--mascot) }` #E84B8A = 3.32:1. app-shell.css:146-147 static highlight `pre .k { color: var(--mascot) }` / `pre .s { color: var(--sun) }` — sun #F5C344 = 1.51:1, effectively invisible string text on the light pre background (pre bg at app-shell.css:135-145 is var(--panel-bg) = paper in light). All four palettes pass on ink (4.5-6:1) — tuned dark-only.
127
+ - fix: In /config/workspace/design/colors_and_type.css add five code-token aliases (self-contained, no dependency on the separate flame-retune finding): in :root (light, near line 37): --code-string: var(--green); --code-keyword: #2453C9; --code-fn: #B84300; --code-str-alt: var(--mascot-deep); --code-num: #946300; (verified on paper: green 5.4:1, #2453C9 6.1:1, #B84300 5.0:1, mascot-deep 5.6:1, #946300 4.8:1). In BOTH dark blocks ([data-theme="ink"],[data-theme="dark"] at ~line 227 AND the @media prefers-dark [data-theme="auto"] block at ~line 255): --code-string: var(--green-2); --code-keyword: var(--sky); --code-fn: var(--flame); --code-str-alt: var(--mascot); --code-num: var(--sun);. Add all five as `inherit` lines in the .ds-247420 .ds-247420 nested-scope block (~line 284) like the existing tokens. Then repoint consumers: chat.css:794 -> var(--code-string), :795 -> var(--code-keyword), :796 -> var(--code-fn); app-shell.css:1940-1941 (token.string/attr-value) -> var(--code-str-alt); app-shell.css:1943-1944 (token.number/boolean, currently var(--sun)) -> var(--code-num); app-shell.css:146 pre .k -> var(--code-str-alt); app-shell.css:147 pre .s -> var(--code-string). Leave purple-2 (7.5:1) and fg-based token rules untouched. Rebuild (node scripts/build.mjs), copy dist/247420.{js,css} into site/app/vendor/anentrypoint-design/, witness a code block in light theme.
128
+
129
+ ### light-theme-contrast-sun-file-type-cues [kit]
130
+ Document file-type icon and 3px left-edge cue use --sun #F5C344 at 1.5:1 on paper - the file-type color coding disappears for documents in light theme
131
+ - evidence: app-shell.css:1174 `.ds-file-row[data-file-type="document"] { border-left-width: 3px; border-left-color: var(--sun); }` and app-shell.css:1205 `.ds-file-row[data-file-type="document"] .ds-file-icon { color: var(--sun); }`. --sun #F5C344 (colors_and_type.css:37) vs paper #F6F5F1 = 1.51:1 — far below the 3:1 graphical-object minimum; the icon and edge are practically invisible, so in the paper theme documents read as the only un-typed rows in the FileGrid while image (mascot 3.3:1) and other types stay visible. --sun has no light override anywhere; it cannot simply be darkened globally because .ds-badge.tone-sun/.chip.tone-sun (app-shell.css:463/483) use it as a chip BACKGROUND under ink text where the light value is correct.
132
+ - fix: Kit, two files. (1) colors_and_type.css: define the theme-aware amber token the kit already consumes by fallback - add `--amber: #8A6512;` (4.9:1 on paper) to the base :root token block (near --sun, line ~37), and add `--amber: #D9A93A;` to BOTH dark blocks: the [data-theme="ink"],[data-theme="dark"] block (line ~227) and the @media (prefers-color-scheme: dark) [data-theme="auto"] block (line ~256). (2) app-shell.css: line 1181 change `border-left-color: var(--sun)` to `border-left-color: var(--amber, var(--sun))`; line 1212 change `color: var(--sun)` to `color: var(--amber, var(--sun))`. Dark appearance is perceptually unchanged (#D9A93A vs #F5C344, adjacent ochres on ink); tone-sun chips/badges, toast.warning, alert-warn, and code tokens keep --sun untouched. Bonus: the seven existing chat.css var(--amber, #d9a93a) consumers (status-dot-connecting/stale, dash is-stale/is-connecting/is-stopping, breakdown is-idle) automatically gain the readable light-theme value, since #d9a93a on paper is only ~2.0:1 today. Then node scripts/build.mjs, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, witness the files tab in paper theme with a .md or .pdf row (edge + icon visible) and in ink theme (unchanged).
133
+
134
+ ### interaction-density-agentchat-controls-row-misaligned [kit]
135
+ AgentChat controls row mixes three control heights and three corner radii (41px r-2 Select, 46px r-pill Btn, 32px r-1 export acts)
136
+ - evidence: AgentControls renders Select + Btn('stop'/'new') + .agentchat-export-act buttons on one flex row (/config/workspace/design/src/components/agent-chat.js:74-105). Base .ds-select is padding:10px 14px; border-radius:var(--r-2) (~41px tall, app-shell.css:2185-2201); base .btn is padding:13px 22px; border-radius:var(--r-pill) (~46px stadium, app-shell.css:400-411); .agentchat-export-act is min-height:32px; border-radius:var(--r-1) (chat.css:228-233). chat.css:14-22 (.agentchat-controls) only sets min/max-width on the selects — no height/shape normalization, unlike the dashboard which explicitly aligns its toolbar on a shared 32px height (chat.css:465-477 '.ds-dash-toolbar .ds-select... min-height:32px; border-radius:var(--r-1)'). The most-seen chrome row in the product has no shared control metric.
137
+ - fix: Kit-only, in /config/workspace/design/chat.css immediately after line 22 (the existing .agentchat-controls .ds-select width rule), mirroring the .ds-dash-toolbar precedent at chat.css:474-477: add `.agentchat-controls .ds-select { min-height: 32px; padding: 4px 28px 4px 10px; font-size: var(--fs-sm); border-radius: var(--r-1); }` (keep the existing min/max-width rules; right padding stays 28px so the chevron at calc(100% - 18px) stays clear) and `.agentchat-controls .btn { padding: 5px 12px; min-height: 32px; border-radius: var(--r-1); font-weight: 500; }`. Then add the kit-standard touch floor (this row has none today): `@media (pointer: coarse) { .agentchat-controls .ds-select, .agentchat-controls .btn { min-height: 44px; } }` placed AFTER the base rules (same specificity, order decides - same convention as chat.css:331). .agentchat-export-act already matches at 32px/r-1 and needs no change. Then the standard ship flow: node scripts/build.mjs in /config/workspace/design, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, browser-witness the chat tab controls row (all controls 32px tall, r-1), push both repos.
138
+
139
+ ### interaction-density-bulkbar-stadium-cta-pills [kit]
140
+ BulkBar renders full-size stadium pills (and a solid red danger CTA) inside a slim toolbar strip next to 32px rect toolbar buttons
141
+ - evidence: BulkBar maps actions straight onto raw Btn (/config/workspace/design/src/components/files.js:350-365, danger action -> class 'btn-primary danger'), and .ds-bulkbar CSS (app-shell.css:1256-1261) defines only the strip + count — no button scoping, so the delete action renders as the 13px/22px r-pill solid var(--warn) CTA (~46px) inside a padding:var(--space-1) chrome strip. This sits directly adjacent to .ds-file-toolbar whose buttons are explicitly compacted to 'padding:5px 12px; min-height:32px; border-radius:var(--r-1)' with the kit's own stated rule 'a toolbar is chrome, not a CTA row' (app-shell.css:1380-1391). BulkBar even declares role=toolbar (files.js:365) yet is the one toolbar without the compact treatment.
142
+ - fix: In /config/workspace/design/app-shell.css, immediately after the .ds-bulkbar-count rule (line 1261), add the toolbar-scoped compaction mirroring .ds-file-toolbar and .ds-dash-header: `.ds-bulkbar .btn, .ds-bulkbar .btn-primary { padding: 5px 12px; min-height: 32px; border-radius: var(--r-1); font-weight: 500; }` `.ds-bulkbar .btn { background: transparent; border: var(--bw-hair) solid var(--rule); color: var(--fg-2); }` `.ds-bulkbar .btn:hover { background: var(--bg-2); color: var(--fg); }` and demote the danger action to the kit's established quiet-destructive form (the app already arm-confirms bulk delete via ConfirmDialog): `.ds-bulkbar .btn-primary.danger { background: transparent; color: var(--flame); border: var(--bw-hair) solid var(--flame); }` `.ds-bulkbar .btn-primary.danger:hover { background: color-mix(in oklab, var(--flame) 12%, transparent); color: var(--flame); }` plus `@media (pointer: coarse) { .ds-bulkbar .btn, .ds-bulkbar .btn-primary { min-height: 44px; } }`. No component change needed. Then `node scripts/build.mjs` in /config/workspace/design and copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/ per the standing update flow.
143
+
144
+ ### interaction-density-confirm-button-order-flipped [app]
145
+ Inline Alert confirms put the destructive action FIRST while ConfirmDialog puts it LAST — opposite orders for the same arm-confirm pattern
146
+ - evidence: Kit ConfirmDialog/PromptDialog order actions [cancel, confirm/destructive] — destructive on the right (/config/workspace/design/src/components/files-modals.js:160-164, 191-194). The app's inline banner confirms reverse it: new-chat 'yes, clear chat' (danger) then 'cancel' (/config/workspace/agentgui/site/app/js/app.js:1694-1695), edit-and-resend 'continue' (danger) then 'cancel' (app.js:1728-1729), settings clear-local-data 'clear' (danger) then 'cancel' (app.js:2930-2931). A user trained by the file-delete dialog to find the destructive action on the right finds it on the left in every banner confirm.
147
+ - fix: In /config/workspace/agentgui/site/app/js/app.js, swap the two Btn entries (keep the leading explanatory span first) in three children arrays so cancel comes before the danger button, matching ConfirmDialog's [cancel, destructive] order: (1) confirmingNewChat banner at lines 1762-1763 — order becomes Btn cnno 'cancel' then Btn cnyes danger 'yes, clear chat'; (2) confirmingEdit banner at lines 1796-1797 — Btn ceno 'cancel' then Btn ceyes danger 'continue'; (3) confirmingClearData alert at lines 3003-3004 — Btn cldno 'cancel' then Btn cldyes danger 'clear'. App-only wiring change; no kit or CSS edits.
148
+
149
+ ### interaction-density-destructive-tone-split [kit]
150
+ Destructive controls split between two hues: warn red (#E0241A) for danger buttons, flame orange (#FF5A1F) for armed stops and danger icon buttons
151
+ - evidence: Tokens are distinct: --warn:#E0241A, --flame:#FF5A1F (/config/workspace/design/colors_and_type.css:38-40). .btn-primary.danger { background: var(--warn) } (app-shell.css:1552-1556) — so ConfirmDialog's destructive confirm and BulkBar delete are warn red. But .ds-icon-btn-danger { background: var(--flame) } (app-shell.css:442) and the armed stop in the dash header '.ds-dash-header .btn-primary.danger.is-armed { background: var(--flame) }' (chat.css:386) are flame orange, while .ds-file-act-warn:hover tints with warn again (app-shell.css:1229). The SAME action class (stop/delete, all destructive buttons) renders in two different reds depending on surface; flame elsewhere means error STATUS (rail semantics), muddying both.
152
+ - fix: Kit-only, minimal: do NOT add a new token (--danger already exists at colors_and_type.css:41 with a different oklch value serving editor-primitives; leave it alone). Point the two flame-toned destructive-ACTION rules at warn: (1) /config/workspace/design/chat.css:379-386 — in .ds-dash-header .btn-primary.danger change color/border-color var(--flame)->var(--warn), in its :hover change the color-mix base var(--flame)->var(--warn), and in .is-armed change background/border-color var(--flame)->var(--warn); (2) /config/workspace/design/app-shell.css:449 — .ds-icon-btn-danger background var(--flame)->var(--warn) (no live call sites, but keeps the IconButton danger variant API consistent). Leave .ds-file-act-warn:hover (app-shell.css:1229) and .btn-primary.danger (app-shell.css:1559) unchanged — they already use warn. Flame stays exclusively error-STATUS (rails, .ds-dash-status.is-error, .ds-session-state-error, .ds-modal-error). Then node scripts/build.mjs in /config/workspace/design and copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/ per the standard update flow.
153
+
154
+ ### interaction-density-compact-button-zoo [kit]
155
+ Eight ad-hoc compact-quiet button clones with divergent metrics (24/28/32px heights, fs-micro/tiny/sm, border bg-3 vs rule, bg none/bg/bg-2) — no kit small-button primitive
156
+ - evidence: Each surface re-implements the same 'quiet compact rect button' with different numbers: .row-act 28px/4-10px/fs-tiny/bg-2+border bg-3 (app-shell.css:3246-3252); .ds-file-toolbar .btn 32px/5-12px/fw-500/transparent+border rule (app-shell.css:1382-1389); .ds-dash-errors-toggle 32px/4-10px/fs-tiny/bg-2+border bg-3 (chat.css:482-486); .agentchat-export-act 32px/4-10px/fs-tiny/none+border bg-3 (chat.css:228-233); .agentchat-install-copy 24px/2-8px/fs-tiny (chat.css:217-222); .ds-upload-act 24px/2-10px/fs-micro/bg+border rule (app-shell.css:1484-1488); .ds-session-meta-copy 24px/2-8px/fs-micro/bg-2+border bg-3 (chat.css:573-577); .ds-file-more-btn 32px/4-12px/fs-tiny/bg-2 (chat.css:530-534). Adjacent surfaces show visibly different secondary-button heights and border tones with no rule deciding which clone applies.
157
+ - fix: Kit CSS-only consolidation, two tiers, no JS change needed. (1) Add one canonical grouped rule (in app-shell.css near .row-act, since both css files ship in the same bundle): tier A (toolbar-grade) `.row-act, .ds-dash-errors-toggle, .agentchat-export-act, .ds-file-more-btn { min-height:32px; padding:5px 12px; border-radius:var(--r-1); font-family:var(--ff-body); font-size:var(--fs-tiny); font-weight:500; background:transparent; border:var(--bw-hair) solid var(--rule); color:var(--fg-2); cursor:pointer; }` — this matches the already-canonical .ds-file-toolbar .btn metrics (app-shell.css ~1389), which stays as-is and becomes the reference; tier B (inline-row-grade) `.agentchat-install-copy, .ds-upload-act, .ds-session-meta-copy { min-height:24px; padding:2px 8px; border-radius:var(--r-1); font-family:var(--ff-body); font-size:var(--fs-tiny); background:transparent; border:var(--bw-hair) solid var(--rule); color:var(--fg-3); cursor:pointer; }`. (2) Delete the now-duplicated metric/tone declarations from the eight per-clone rules (they would otherwise win the cascade), keeping only their non-metric bits: .ds-dash-errors-toggle.active, all :hover (normalize hover to background:var(--bg-2); color:var(--fg)) and :focus-visible rules, the flex/layout props (.agentchat-install-copy flex:0 0 auto etc.), and the existing coarse-pointer 44px media bumps. Net effect: 28px .row-act and the fs-micro strays normalize within their tier, border tone unifies on var(--rule), background unifies on transparent — adjacent surfaces stop showing three different secondary-button treatments. Optional follow-up (defer if time-boxed): a Btn size:'sm' prop in shell.js mapping to a .btn-sm class aliasing tier A.
158
+
159
+ ## LOW
160
+
161
+ ### typography-rhythm-off-scale-em-sizes [kit]
162
+ AgentChat chrome uses ad-hoc em font-sizes (.8/.85/.9em) that land off the token scale and drift from sibling token-sized controls
163
+ - evidence: /config/workspace/design/chat.css:45 (.agentchat-status font-size:.85em = 13.6px), :71 (.agentchat-cwd .85em), :112 (.agentchat-sub .85em), :128 (.agentchat-jump .8em = 12.8px), :172 (.agentchat-empty-sub .9em), :183 (.agentchat-empty-suggestion .85em), :244 (.agentchat-working .85em). Every sibling control on the same surfaces uses tokens (.ds-dash-stream var(--fs-tiny) 13px chat.css:442; .chat-msg-action var(--fs-tiny) chat.css:153), so the status line, cwd bar and working tail sit at 13.6px next to 13px peers — a sub-pixel mismatch that blurs the scale and ignores [data-typescale] retuning.
164
+ - fix: In /config/workspace/design/chat.css, replace eight em font-sizes in the .agentchat-* block: (a) .85em -> var(--fs-tiny) at lines 45 (.agentchat-status), 71 (.agentchat-cwd), 112 (.agentchat-sub), 183 (.agentchat-empty-suggestion), 244 (.agentchat-working); (b) .8em -> var(--fs-tiny) at line 128 (.agentchat-jump) — 12.8px is nearer fs-tiny 13px than fs-micro 12px, and matches its peer .chat-msg-action (chat.css:153) which is already fs-tiny; (c) .9em -> var(--fs-sm) at lines 172 (.agentchat-empty-sub) and 203 (.agentchat-install-text) — 14.4px is nearer fs-sm 15px and both are small prose, not meta text. Leave 1em/1.05em titles (lines 111, 171) and em-based padding/gap alone. Then node scripts/build.mjs in the kit, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, and browser-witness the chat tab (status line, cwd bar + hint now both 13px, jump pill, empty state). Note for the punch-list text: drop the '[data-typescale] retuning' justification — typescale only retunes --fs-body/lg/xl, so tokens are typescale-inert; the win is scale unification, e.g. the cwd bar no longer mixes 13.6px text with a 13px hint in the same row.
165
+
166
+ ### typography-rhythm-composer-textarea-line-height-conflict [kit]
167
+ Composer textarea declares two conflicting line-heights (1.45 then 1.5) in the same sheet; the autogrow min/max-height math follows the losing value
168
+ - evidence: /config/workspace/design/app-shell.css:2071-2078 — `.chat-composer textarea { ... line-height: 1.45; min-height: 28px; max-height: 200px; }` is silently overridden by the later duplicate block at app-shell.css:2162-2166 — `.chat-composer textarea { resize: none; ... line-height: 1.5; }` (equal specificity, later source order wins). The single-line height the first block sized for (15px * 1.45 ~ 21.75 + 16px padding) no longer matches the rendered 1.5, and neither uses the rhythm tokens (--lh-tall 1.4 / --lh-base 1.55, colors_and_type.css:124-127,182).
169
+ - fix: In /config/workspace/design/app-shell.css, merge the two `.chat-composer textarea` blocks: (1) in the primary block at lines 2122-2131, change `line-height: 1.45;` to `line-height: 1.5;` so the sizing block states the value that actually renders; (2) in the polish block at lines 2219-2224, delete the redundant `resize: none;`, `overflow-y: auto;`, and `line-height: 1.5;` declarations, keeping only the net-new `transition: height var(--dur-snap) var(--ease);` (the adjacent `.chat-composer textarea:disabled` rule at 2225-2228 stays). Do NOT change min-height: 28px — it is inert under either line-height and the autogrow JS sizes from scrollHeight, so no min-height recalculation is needed. Then rebuild the kit (`node scripts/build.mjs`) and copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/ per the standard vendoring flow.
170
+
171
+ ### copy-tone-dangling-separator-before-button [app]
172
+ Empty-state strings end in a dangling ' - ' separator hanging before a button
173
+ - evidence: site/app/js/app.js:2369 `h('span', ..., 'no events in this session - ')` immediately followed by a 'reload' Btn, and app.js:2410 `'0 events match this filter - '` followed by 'clear filter' Btn. The visible text terminates in a floating hyphen-space joining prose to a control; if the button wraps to the next line the dash dangles alone. (This flags the dangling composed-string construction, not dash typography in prose.)
174
+ - fix: In site/app/js/app.js only (fixLocation: app): (1) line 2391 change the span text to 'no events in this session' (drop the trailing ' - '); (2) line 2432 change to 'no events match this filter' (drops the separator AND the digit-leading '0 events' phrasing). Do NOT capitalize or add terminal periods as the hunter proposed — app empty-state house style is lowercase without periods ('no sessions yet' line 2714, 'no matches for "…"' line 2693, 'type at least 2 characters to search' line 2696). The .empty-state flex gap:8px (index.html:99) already provides the text-to-button separation, so no markup or CSS change is needed.
175
+
176
+ ### copy-tone-stop-success-reads-partial [app]
177
+ Fully successful bulk stop announces 'stopped 2 of 2 sessions', which reads as a partial result
178
+ - evidence: site/app/js/app.js:1337 `announce('stopped ' + okSids.length + ' of ' + sids.length + ' sessions')` fires unconditionally, so a clean bulk stop is announced in failure-report phrasing; the 'N of M' frame is only meaningful when failed > 0 (the bulkStopError at app.js:1334-1336 already covers that case).
179
+ - fix: In site/app/js/app.js stopAllActive, replace line 1357 (`announce('stopped ' + okSids.length + ' of ' + sids.length + ' sessions');`) with a branch on the already-computed `failed` (line 1353): `announce(failed ? 'stopped ' + okSids.length + ' of ' + sids.length + ' sessions' : 'stopped ' + okSids.length + ' session' + (okSids.length === 1 ? '' : 's'));`. Full success announces 'stopped 2 sessions' / 'stopped 1 session'; the 'N of M sessions' framing fires only when failed > 0, matching the bulkStopError banner that already covers that case.
180
+
181
+ ### copy-tone-announce-case-inconsistent [app]
182
+ One announce() is capitalized ('Now on ...') while every other live-region message is lowercase
183
+ - evidence: site/app/js/app.js:287 `announce('Now on ' + tab + ' tab')` vs the established lowercase announce voice everywhere else: :869 'selection cleared', :953 'upload finished', :2230 'generation stopped', :3179 'reconnected', :3348 'composer focused', :3359 'shortcuts closed'. One concept (status announcements) in two cases.
184
+ - fix: In /config/workspace/agentgui/site/app/js/app.js line 287, change `announce('Now on ' + tab + ' tab');` to `announce('now on ' + tab + ' tab');`. fixLocation: app (announce strings are app copy, not kit). No other call sites need changes; leave fileMutationCopy's sentence-case error prose as-is (it is dialog body copy, not status-snippet voice).
185
+
186
+ ### copy-tone-kit-dialog-default-titles [kit]
187
+ Kit dialog default titles 'confirm' and 'name' are lowercase fragments that clash with app Title-case dialog titles
188
+ - evidence: design/src/components/files-modals.js:154 `ConfirmDialog({ title = 'confirm', ... confirmLabel = 'confirm' ...})` and :168 `PromptDialog({ title = 'name', ... confirmLabel = 'ok' ...})`. Every app-supplied title is sentence-case ('Rename X', 'Delete X', 'New folder', 'Delete N selected entries' - app.js:990/1004/1018/1030), so any consumer that omits title gets a bare lowercase 'confirm'/'name' heading; 'name' as a dialog title also states nothing about the action.
189
+ - fix: In /config/workspace/design/src/components/files-modals.js change only the two heading defaults: line 154 `title = 'confirm'` -> `title = 'Are you sure?'`; line 168 `title = 'name'` -> `title = 'Enter a name'`. Leave confirmLabel/cancelLabel defaults ('confirm'/'ok'/'cancel') lowercase — they match the kit-wide lowercase button voice. No app change (all app call sites already pass titles, so agentgui's rendered UI is unchanged); rebuild the kit (node scripts/build.mjs) and re-vendor dist/247420.{js,css} into site/app/vendor/anentrypoint-design/ as part of the session's normal kit-update batch.
190
+
191
+ ### perceived-performance-agents-panel-loading-reads-as-empty [app]
192
+ Settings agents panel reads 'agents · 0/0 installed · no agents loaded' while the boot fetch/retry is still in flight
193
+ - evidence: /config/workspace/agentgui/site/app/js/app.js:2937 builds the panel title from `state.agents.length` ('agents · 0/0 installed' before load) and :2970 falls through to `'no agents loaded'` whenever the array is empty — there is no agents-loading flag anywhere (state at :25 has only `agentsError`). loadAgents runs async at init (app.js:3195) and on failure retryLoadAgents backs off 2s/5s/10s (:3154-3159), so a user opening Settings in the first seconds (or during server warmup) sees what looks like a broken/empty agent registry rather than a load in progress.
194
+ - fix: App wiring only, in /config/workspace/agentgui/site/app/js/app.js: (1) add `agentsLoading: false,` beside `agentsError` at :25. (2) In loadAgents (:3174) set `state.agentsLoading = true;` after clearing agentsError, and set `state.agentsLoading = false;` immediately before each render() in both the success (:3185) and catch (:3190) paths. (3) In agentsPanel, compute `const pending = !state.agents.length && (state.agentsLoading || state.agentsError);` — when `state.agentsLoading && !state.agents.length`, title becomes 'agents · loading…' and the :3013 fallthrough becomes the file's existing pattern `h('div',{key:'agload',class:'lede empty-state',role:'status','aria-live':'polite'},Spinner({key:'spin',size:'sm'}),'loading agents…')` (mirrors :2448; keep the key — webjsx keyed-children rule). When `state.agentsError && !state.agents.length` (the backoff gap between retries, where agentsLoading is false again), render the failed copy with a retry instead: `h('p',{key:'agfail',class:'lede'},'the agent list failed to load')` + `Btn({key:'agretry',onClick:()=>loadAgents(),children:'retry'})` (mirrors :1746-1750), so the panel never flaps back to 'no agents loaded' mid-retry. Keep 'no agents loaded' only for the genuinely settled empty case.
195
+
196
+ ### kit-dx-icon-positional-outlier [kit]
197
+ Icon(name, {size}) is the barrel's only positional-signature component, among four divergent icon entry points
198
+ - evidence: src/components/shell.js:168 defines `export function Icon(name, { size = 16 } = {})` - the only component-shaped export in the 179-export barrel (src/components.js) that takes positional args; every sibling takes a single props object: FileIcon({type}) (files.js:45), IconButton({icon,...}) (shell.js:55), Glyph({children,...}) (shell.js:71), Badge({children,...}) (shell.js:67). There are four icon-ish entry points (Glyph, Icon, iconMarkup, FileIcon) with three different shapes; a consumer destructuring `const { Icon } = C` and calling Icon({name:'folder'}) (the shape everything else trains them to use) silently renders the empty-glyph fallback (shell.js:170 returns the blank span when ICON_PATHS[props-object] misses) rather than throwing.
199
+ - fix: Kit, /config/workspace/design/src/components/shell.js. (1) In Icon (line 170), add as the first statement: `if (name && typeof name === 'object') ({ name, size = 16 } = name);` — parenthesized destructuring assignment to the existing `name`/`size` bindings, making Icon({name:'folder', size:16}) equivalent to Icon('folder', {size:16}) while keeping every existing positional call site (agent-chat.js:392, chat.js, community.js, files.js:46,79, etc.) untouched. (2) Mirror the same one-line guard at the top of iconMarkup (line 163): `if (name && typeof name === 'object') ({ name, size = 16 } = name);`. (3) Add an `### Icon` section to /config/workspace/design/COMPONENT_API.md (near IconButton/Glyph): document both accepted forms `Icon(name, { size = 16 })` and `Icon({ name, size = 16 })`, note the blank-span fallback for unknown names, and point to the ICON_PATHS table in shell.js as the canonical name list. Rebuild (`node scripts/build.mjs`), re-vendor dist/247420.{js,css} into agentgui site/app/vendor/anentrypoint-design/, witness, push both per the standing update flow.
200
+
201
+ ### kit-dx-deprecated-api-dogfooding [kit]
202
+ Kit components still use the Btn/ChatMessage APIs its own migration guide deprecates, and the promised deprecation notices don't exist
203
+ - evidence: COMPONENT_API.md:716-737 declares Btn primary/ghost booleans the 'Old API' superseded by variant, and claims 'Old primary and ghost props ... trigger deprecation notices in development' - but shell.js:18-21 resolves the booleans silently (no console.warn anywhere in Btn), and the kit's own components call the deprecated form 22 times (e.g. sessions.js:202 `Btn({ key:'open', primary: true, ...})`, files-modals.js:163 `Btn({ primary: true, danger: !!destructive, ...})`, agent-chat.js:122 `Btn({ key:'save', primary: true, ...})`). Likewise COMPONENT_API.md:756-768 deprecates ChatMessage who:'you'/'them' for role:'user'/'assistant', yet AgentChat itself passes `who: isAssistant ? 'them' : 'you'` (agent-chat.js:254). A consumer reading kit source as the reference implementation learns the deprecated API every time.
204
+ - fix: Kit-only mechanical sweep, three parts. (1) Replace the 22 legacy Btn call sites in src/components/{agent-chat,sessions,content,files-modals,freddie}.js: primary:true -> variant:'primary', danger:true -> variant:'danger', per the resolution order in shell.js:20 (variant first), so rendered classes are byte-identical. EXCEPTION at files-modals.js:163: 'primary: true, danger: !!destructive' currently resolves to plain 'btn-primary' because primary precedes danger in the legacy chain (the danger prop is dead) - the zero-visual-change replacement is variant:'primary' (drop the dead danger:!!destructive); if the destructive-red styling was the intent, change it to variant: destructive?'danger':'primary' as a deliberate, separately-witnessed fix, not part of the mechanical sweep. (2) src/components/agent-chat.js:254: replace who: isAssistant?'them':'you' with role: isAssistant?'assistant':'user' (ChatMessage chat.js:312-317 maps role first; identical resolvedWho). (3) Make the doc true the cheap way: delete the 'trigger deprecation notices in development' sentence from COMPONENT_API.md's Btn migration section (no such warn exists anywhere in src/); do NOT add a console.warn - the legacy path must stay silent for downstream consumers and adding dev-mode plumbing is out of scope. Then node scripts/build.mjs, copy dist/247420.{js,css} into agentgui site/app/vendor/anentrypoint-design/, and witness chat/files/live for unchanged button classes (btn-primary, btn-primary danger, btn-ghost) and 0 console errors.
205
+
206
+ ### light-theme-contrast-composer-hairline-boundary [kit]
207
+ Composer shell and tool-card boundaries use --rule (14% currentColor) which lands ~1.3:1 on paper - the bg-on-bg composer input field has a near-invisible boundary in light
208
+ - evidence: colors_and_type.css:158 `--rule: color-mix(in oklab, currentColor 14%, transparent)`. app-shell.css:2109-2117 `.chat-composer { background: var(--bg); border: var(--bw-hair) solid var(--rule); }` — the composer sits on the thread which is also --bg, so the 14%-ink hairline (blended ~#D7D6D3 on paper, ~1.3:1) is the ONLY resting boundary of the primary text input; in dark the same 14%-paper mix over ink reads relatively stronger (~1.5:1) and dark was the witnessed theme. Same hairline frames the .chat-tool card (chat.css:704) and .chat-msg-notice (chat.css:624). The reference bar (claude.ai/code) gives its composer a clearly visible border plus a soft shadow at rest.
209
+ - fix: Kit, minimal: in /config/workspace/design/app-shell.css line 2113 change `border: var(--bw-hair) solid var(--rule);` to `border: var(--bw-hair) solid var(--rule-strong);` on `.chat-composer` (reuse the existing 28% token at colors_and_type.css:159 - do NOT add a new --rule-input token). Leave .chat-tool and .chat-msg-notice on --rule (the tool card is a passive disclosure, and the notice already has a --bg-2 tonal fill). Skip the rest box-shadow to stay consistent with the 11th-run F3 elevation unification (in-flow shadows were deliberately dropped; :focus-within replaces box-shadow anyway). The existing :focus-within accent border+ring (app-shell.css:2118-2121) is untouched. Then `node scripts/build.mjs` in /config/workspace/design, copy dist/247420.{js,css} into /config/workspace/agentgui/site/app/vendor/anentrypoint-design/, and witness the composer at rest in the paper theme (border should read ~#B6B5B2, clearly visible) and in dark.
210
+
211
+ ### interaction-density-input-focus-ring-split [kit]
212
+ Text inputs split between two focus treatments: inset accent box-shadow (ds-field/ds-search-input) vs 2px outline (file filter, session search) — and .ds-select rings on plain :focus
213
+ - evidence: Canonical input focus is 'outline:none; box-shadow: inset 0 0 0 2px var(--accent)' for .ds-field input/textarea/.ds-select/.ds-search-input (/config/workspace/design/app-shell.css:2523-2530). But .ds-file-filter-input:focus-visible uses 'outline: 2px solid var(--accent); outline-offset:1px' (app-shell.css:1114) and .ds-session-search:focus-visible the same (chat.css:329) — two visually different rings within the same control family (three search/filter inputs, two looks). Additionally .ds-select:focus { box-shadow: inset 0 0 0 2px var(--accent) } (app-shell.css:2202) fires on every mouse click, not :focus-visible, unlike every other control in the bundle (50+ :focus-visible rules).
214
+ - fix: Kit CSS only, four rule edits: (1) app-shell.css:1121 change '.ds-file-filter-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }' to '{ outline: none; box-shadow: inset 0 0 0 2px var(--accent); }'; (2) chat.css:329 change '.ds-session-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }' to '{ outline: none; box-shadow: inset 0 0 0 2px var(--accent); }'; (3) app-shell.css:1647 change '.ds-filter-input:focus { outline: 2px solid var(--accent); outline-offset: 2px; }' to '.ds-filter-input:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }' (this third filter input was missed by the hunter); (4) app-shell.css:2215 change '.ds-select:focus' to '.ds-select:focus-visible' (keeping the existing inset box-shadow), matching the .ds-field .ds-select:focus-visible rule at 2538. All three inputs have opaque backgrounds (--bg/--bg-2) so the inset ring remains visible; note .ds-session-search keeps its --bg-3 hairline border, which the 2px inset ring paints over on focus exactly as .ds-field inputs do over their --rule hairline. Rebuild kit (node scripts/build.mjs), re-vendor dist/247420.css into site/app/vendor/anentrypoint-design/, witness keyboard-tab focus on files filter + session search + a settings select.
215
+
216
+ ### interaction-density-native-checkbox-bypasses-kit [app]
217
+ History 'show subagents' toggle is a raw unstyled native checkbox while the kit ships a styled Checkbox primitive
218
+ - evidence: App renders h('input', { type:'checkbox', ... }) with no class inside a bare label (/config/workspace/agentgui/site/app/js/app.js:2708-2710) — UA-default checkbox, no .ds-check styling, no kit focus ring. The kit exports a Checkbox primitive (/config/workspace/design/src/components/form-primitives.js:16-34, exported via src/components.js:64) with .ds-check/.ds-check-row styling and :focus-visible rules in editor-primitives.css:591-616. So the product shows three checkbox treatments: kit .ds-check, the files/dash mono-bracket role=checkbox (deliberate grid affordance), and this one unstyled UA control.
219
+ - fix: App-only, no kit rebuild needed (vendored 247420.js already exports Checkbox and 247420.css has .ds-check rules). (1) Add Checkbox to the C destructure at /config/workspace/agentgui/site/app/js/app.js:6. (2) Replace the label+input at app.js:2766-2768 with: Checkbox({ key: 'subtog', checked: state.showSubagents, label: 'show subagents (' + (state.showSubagents ? subagentCount + ' shown' : subagentCount + ' hidden') + ')', onChange: (v) => { state.showSubagents = v; render(); } }) — note kit onChange passes (checked, event), so v is the boolean. (3) Delete the now-dead .subagent-toggle rules at site/app/index.html:118-119 (removes an app-side style shim; adds no !important). Re-witness the history tab: styled .ds-check renders, toggling shows/hides subagent rows, :focus-visible ring present.
220
+
221
+ ## Landed (2026-06-11, same run)
222
+ All 40 confirmed findings above were implemented in this run: kit CSS/components + theme tokens + two new build lints (lint-null-children caught 14 latent webjsx crash sites in freddie.js), app wiring (copy tone, confirm order, kit Checkbox, boot splash + modulepreload, agents tri-state, EventList skeleton wiring, files stack), docs (COMPONENT_API.md Workspace Surfaces + honest CSS taxonomy). Witnessed live: browser-19 (metric assertions) + browser-20 (0 errors, light tokens, glyphs 0, no h-scroll, 420 icon rail).