agentgui 1.0.962 → 1.0.964
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.
- package/.claude/workflows/gui-ux-craft.js +101 -0
- package/AGENTS.md +40 -38
- package/PUNCHLIST-PARITY.md +6 -0
- package/PUNCHLIST-UX.md +222 -0
- package/lib/http-handler.js +62 -1
- package/package.json +1 -1
- package/scripts/validate-mutations.mjs +39 -13
- package/site/app/index.html +14 -4
- package/site/app/js/app.js +296 -72
- package/site/app/js/backend.js +1 -0
- package/site/app/vendor/anentrypoint-design/247420.css +490 -123
- package/site/app/vendor/anentrypoint-design/247420.js +14 -14
package/PUNCHLIST-UX.md
ADDED
|
@@ -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).
|
package/lib/http-handler.js
CHANGED
|
@@ -156,10 +156,29 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
156
156
|
_ok = _checkToken(_auth.slice(7));
|
|
157
157
|
}
|
|
158
158
|
// EventSource and same-origin links can't set headers - accept ?token= as fallback.
|
|
159
|
+
let _viaQuery = false;
|
|
159
160
|
if (!_ok) {
|
|
160
161
|
try {
|
|
161
162
|
const _qsTok = new URL(req.url, 'http://localhost').searchParams.get('token');
|
|
162
|
-
if (_qsTok) _ok = _checkToken(_qsTok);
|
|
163
|
+
if (_qsTok) { _ok = _checkToken(_qsTok); _viaQuery = _ok; }
|
|
164
|
+
} catch (_) {}
|
|
165
|
+
}
|
|
166
|
+
// Static subresources (js/css/vendor) are requested by the BROWSER, which
|
|
167
|
+
// can attach neither a header nor a ?token= - without a cookie the SPA
|
|
168
|
+
// can never boot behind PASSWORD (index.html 200s, app.js 401s, blank
|
|
169
|
+
// page). On any successful auth, set an HttpOnly SameSite=Lax cookie the
|
|
170
|
+
// gate accepts for subsequent requests. Lax + the CSRF guard below keep
|
|
171
|
+
// cross-site mutations rejected; the value is URL-encoded (a password
|
|
172
|
+
// may carry cookie-hostile characters like commas).
|
|
173
|
+
if (!_ok) {
|
|
174
|
+
try {
|
|
175
|
+
const _ck = req.headers['cookie'] || '';
|
|
176
|
+
const _m = /(?:^|;\s*)agentgui_token=([^;]+)/.exec(_ck);
|
|
177
|
+
if (_m) _ok = _checkToken(decodeURIComponent(_m[1]));
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
} else if (_viaQuery || _auth) {
|
|
180
|
+
try {
|
|
181
|
+
res.setHeader('Set-Cookie', 'agentgui_token=' + encodeURIComponent(_pwd) + '; Path=/; HttpOnly; SameSite=Lax');
|
|
163
182
|
} catch (_) {}
|
|
164
183
|
}
|
|
165
184
|
if (!_ok) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' }); res.end('Unauthorized'); return; }
|
|
@@ -452,6 +471,48 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
452
471
|
return;
|
|
453
472
|
}
|
|
454
473
|
|
|
474
|
+
// POST /api/move {path, destDir, overwrite?} -> {ok, path}. Moves an
|
|
475
|
+
// entry into another directory; BOTH endpoints re-confine via realpath.
|
|
476
|
+
// Refuses: a root as the source, a directory moved into itself or its
|
|
477
|
+
// own subtree, and an existing target unless overwrite:true (and never
|
|
478
|
+
// overwrites a directory).
|
|
479
|
+
if (routePath.split('?')[0] === '/api/move' && req.method === 'POST') {
|
|
480
|
+
let body;
|
|
481
|
+
try { body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}'); }
|
|
482
|
+
catch (e) { sendJSON(req, res, e.code === 'TOO_LARGE' ? 413 : 400, { error: 'bad request body' }); return; }
|
|
483
|
+
const allowRoots = fsAllowRoots();
|
|
484
|
+
const conf = confineToRoots(String(body.path || ''), allowRoots);
|
|
485
|
+
if (!conf.ok) { sendJSON(req, res, conf.reason === 'not found' ? 404 : 403, { error: 'forbidden: ' + conf.reason }); return; }
|
|
486
|
+
if (isAllowRoot(conf.realPath, allowRoots)) { sendJSON(req, res, 403, { error: 'forbidden: cannot move an allowed root' }); return; }
|
|
487
|
+
const dConf = confineToRoots(String(body.destDir || ''), allowRoots);
|
|
488
|
+
if (!dConf.ok) { sendJSON(req, res, dConf.reason === 'not found' ? 404 : 403, { error: 'forbidden: ' + dConf.reason }); return; }
|
|
489
|
+
let destIsDir = false;
|
|
490
|
+
try { destIsDir = fs.statSync(dConf.realPath).isDirectory(); } catch {}
|
|
491
|
+
if (!destIsDir) { sendJSON(req, res, 400, { error: 'destination is not a directory' }); return; }
|
|
492
|
+
const name = sanitizeEntryName(path.basename(conf.realPath));
|
|
493
|
+
if (!name) { sendJSON(req, res, 400, { error: 'invalid name' }); return; }
|
|
494
|
+
// A directory must never move into itself or its own subtree.
|
|
495
|
+
const srcPrefix = conf.realPath + path.sep;
|
|
496
|
+
if (dConf.realPath === conf.realPath || dConf.realPath.startsWith(srcPrefix)) {
|
|
497
|
+
sendJSON(req, res, 400, { error: 'cannot move a folder into itself' }); return;
|
|
498
|
+
}
|
|
499
|
+
const target = path.join(dConf.realPath, name);
|
|
500
|
+
if (target === conf.realPath) { sendJSON(req, res, 200, { ok: true, path: target }); return; }
|
|
501
|
+
if (fs.existsSync(target)) {
|
|
502
|
+
let targetIsDir = false;
|
|
503
|
+
try { targetIsDir = fs.lstatSync(target).isDirectory(); } catch {}
|
|
504
|
+
if (targetIsDir || body.overwrite !== true) {
|
|
505
|
+
sendJSON(req, res, 409, { error: 'a file with that name already exists' }); return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
try { fs.renameSync(conf.realPath, target); sendJSON(req, res, 200, { ok: true, path: target }); }
|
|
509
|
+
catch (err) {
|
|
510
|
+
const code = err.code === 'EACCES' || err.code === 'EPERM' ? 403 : 400;
|
|
511
|
+
sendJSON(req, res, code, { error: err.code === 'EXDEV' ? 'cannot move across drives' : err.message });
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
455
516
|
// POST /api/delete {path, recursive?} -> {ok}. Deleting a non-empty dir
|
|
456
517
|
// requires recursive:true (the client confirms first).
|
|
457
518
|
if (routePath.split('?')[0] === '/api/delete' && req.method === 'POST') {
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// Live security validation of the confined file-mutation endpoints.
|
|
2
2
|
// Run with the server up: node scripts/validate-mutations.mjs
|
|
3
|
+
// Portable: ROOT defaults to this repo (an allowed root on both platforms);
|
|
4
|
+
// override with VALIDATE_ROOT. A PASSWORD-gated server is handled via the
|
|
5
|
+
// PASSWORD env var (Bearer), matching lib/http-handler.js.
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
3
8
|
const BASE = process.env.BASE || 'http://localhost:3000';
|
|
4
|
-
const
|
|
9
|
+
const ROOT = process.env.VALIDATE_ROOT || path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const SEP = ROOT.includes('\\') ? '\\' : '/';
|
|
11
|
+
const WIN = SEP === '\\';
|
|
12
|
+
const AUTH = process.env.PASSWORD ? { Authorization: 'Bearer ' + process.env.PASSWORD } : {};
|
|
13
|
+
const J = { 'Content-Type': 'application/json', ...AUTH };
|
|
5
14
|
const results = [];
|
|
6
15
|
async function post(route, body, headers = J) {
|
|
7
16
|
const r = await fetch(BASE + route, { method: 'POST', headers, body: typeof body === 'string' ? body : JSON.stringify(body) });
|
|
@@ -11,29 +20,46 @@ async function check(name, expected, actual) {
|
|
|
11
20
|
const ok = actual.status === expected;
|
|
12
21
|
results.push({ name, expected, got: actual.status, ok, body: actual.body.slice(0, 80) });
|
|
13
22
|
}
|
|
14
|
-
const
|
|
23
|
+
const join = (...parts) => parts.join(SEP);
|
|
24
|
+
const OUTSIDE = WIN ? 'C:\\Windows\\Temp\\x.txt' : '/etc/hostname';
|
|
25
|
+
const OUTSIDE_DIR = WIN ? 'C:\\Windows' : '/etc';
|
|
26
|
+
const TRAVERSAL = join(ROOT, '..', '..', ...(WIN ? ['Windows', 'win.ini'] : ['etc', 'passwd']));
|
|
15
27
|
// security cases
|
|
16
|
-
await check('rename-traversal', 403, await post('/api/rename', { path:
|
|
17
|
-
await check('delete-out-of-roots', 403, await post('/api/delete', { path:
|
|
28
|
+
await check('rename-traversal', 403, await post('/api/rename', { path: TRAVERSAL, newName: 'x.txt' }));
|
|
29
|
+
await check('delete-out-of-roots', 403, await post('/api/delete', { path: OUTSIDE }));
|
|
18
30
|
await check('delete-root-refused', 403, await post('/api/delete', { path: ROOT }));
|
|
19
31
|
await check('mkdir-reserved-name', 400, await post('/api/mkdir', { dir: ROOT, name: 'CON' }));
|
|
20
32
|
await check('mkdir-name-with-separator', 400, await post('/api/mkdir', { dir: ROOT, name: 'a/b' }));
|
|
21
33
|
await check('mkdir-name-trailing-dot', 400, await post('/api/mkdir', { dir: ROOT, name: 'evil.' }));
|
|
22
34
|
await check('mkdir-ads-colon', 400, await post('/api/mkdir', { dir: ROOT, name: 'a:b' }));
|
|
23
|
-
|
|
35
|
+
// A real cross-site form carries no Authorization header (forms cannot set
|
|
36
|
+
// one). On a PASSWORD-gated server the auth wall rejects it first (401);
|
|
37
|
+
// open servers hit the CSRF guard (403). Either way the mutation is refused.
|
|
38
|
+
await check('csrf-cross-site-form', process.env.PASSWORD ? 401 : 403, await post('/api/delete', 'path=x', { 'Content-Type': 'application/x-www-form-urlencoded', 'Sec-Fetch-Site': 'cross-site' }));
|
|
24
39
|
// round trip
|
|
25
40
|
await check('mkdir-ok', 200, await post('/api/mkdir', { dir: ROOT, name: '_wtest' }));
|
|
26
|
-
const up = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(ROOT
|
|
41
|
+
const up = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(join(ROOT, '_wtest')) + '&name=up.txt', { method: 'PUT', headers: AUTH, body: 'hello upload' });
|
|
27
42
|
results.push({ name: 'upload-ok', expected: 200, got: up.status, ok: up.status === 200 });
|
|
28
|
-
const up2 = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(ROOT
|
|
43
|
+
const up2 = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(join(ROOT, '_wtest')) + '&name=up.txt', { method: 'PUT', headers: AUTH, body: 'x' });
|
|
29
44
|
results.push({ name: 'upload-conflict-409', expected: 409, got: up2.status, ok: up2.status === 409 });
|
|
30
|
-
await check('rename-ok', 200, await post('/api/rename', { path: ROOT
|
|
31
|
-
await check('rename-conflict-409', 409, await post('/api/rename', { path: ROOT
|
|
32
|
-
|
|
33
|
-
await check('
|
|
34
|
-
|
|
45
|
+
await check('rename-ok', 200, await post('/api/rename', { path: join(ROOT, '_wtest', 'up.txt'), newName: 'up2.txt' }));
|
|
46
|
+
await check('rename-conflict-409', 409, await post('/api/rename', { path: join(ROOT, '_wtest', 'up2.txt'), newName: 'up2.txt' }));
|
|
47
|
+
// move: security cases + round trip (subdir -> back), then self-nesting refusal
|
|
48
|
+
await check('move-traversal-src-403', 403, await post('/api/move', { path: TRAVERSAL, destDir: ROOT }));
|
|
49
|
+
await check('move-outside-dest-403', 403, await post('/api/move', { path: join(ROOT, '_wtest', 'up2.txt'), destDir: OUTSIDE_DIR }));
|
|
50
|
+
await check('move-root-refused-403', 403, await post('/api/move', { path: ROOT, destDir: join(ROOT, '_wtest') }));
|
|
51
|
+
await check('mkdir-move-sub-ok', 200, await post('/api/mkdir', { dir: join(ROOT, '_wtest'), name: 'sub' }));
|
|
52
|
+
await check('move-ok', 200, await post('/api/move', { path: join(ROOT, '_wtest', 'up2.txt'), destDir: join(ROOT, '_wtest', 'sub') }));
|
|
53
|
+
const up3 = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(join(ROOT, '_wtest')) + '&name=up2.txt', { method: 'PUT', headers: AUTH, body: 'collide' });
|
|
54
|
+
results.push({ name: 'upload-for-move-conflict', expected: 200, got: up3.status, ok: up3.status === 200 });
|
|
55
|
+
await check('move-conflict-409', 409, await post('/api/move', { path: join(ROOT, '_wtest', 'sub', 'up2.txt'), destDir: join(ROOT, '_wtest') }));
|
|
56
|
+
await check('move-overwrite-ok', 200, await post('/api/move', { path: join(ROOT, '_wtest', 'sub', 'up2.txt'), destDir: join(ROOT, '_wtest'), overwrite: true }));
|
|
57
|
+
await check('move-dir-into-itself-400', 400, await post('/api/move', { path: join(ROOT, '_wtest', 'sub'), destDir: join(ROOT, '_wtest', 'sub') }));
|
|
58
|
+
await check('delete-nonempty-no-recursive-409', 409, await post('/api/delete', { path: join(ROOT, '_wtest') }));
|
|
59
|
+
await check('delete-recursive-ok', 200, await post('/api/delete', { path: join(ROOT, '_wtest'), recursive: true }));
|
|
60
|
+
const st = await fetch(BASE + '/api/stat/' + encodeURIComponent(ROOT), { headers: AUTH });
|
|
35
61
|
results.push({ name: 'stat-ok', expected: 200, got: st.status, ok: st.status === 200 });
|
|
36
|
-
const st2 = await fetch(BASE + '/api/stat/' + encodeURIComponent(
|
|
62
|
+
const st2 = await fetch(BASE + '/api/stat/' + encodeURIComponent(OUTSIDE_DIR), { headers: AUTH });
|
|
37
63
|
results.push({ name: 'stat-outside-403', expected: 403, got: st2.status, ok: st2.status === 403 });
|
|
38
64
|
let fail = 0;
|
|
39
65
|
for (const r of results) { if (!r.ok) fail++; console.log((r.ok ? 'PASS' : 'FAIL') + ' ' + r.name + ' expected=' + r.expected + ' got=' + r.got + (r.ok ? '' : ' body=' + (r.body || ''))); }
|
package/site/app/index.html
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
<script type="importmap">
|
|
17
17
|
{ "imports": { "anentrypoint-design": "./vendor/anentrypoint-design/247420.js" } }
|
|
18
18
|
</script>
|
|
19
|
+
<!-- Preload the whole module graph so the 300KB kit fetch starts in parallel
|
|
20
|
+
with app.js instead of after its parse; preconnect warms the jsdelivr
|
|
21
|
+
DNS+TLS the markdown stack needs on first chat render. -->
|
|
22
|
+
<link rel="modulepreload" href="./vendor/anentrypoint-design/247420.js">
|
|
23
|
+
<link rel="modulepreload" href="./js/backend.js">
|
|
24
|
+
<link rel="modulepreload" href="./js/codec.js">
|
|
25
|
+
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
19
26
|
<style>
|
|
20
27
|
:root {
|
|
21
28
|
--agentgui-bg: #16161a;
|
|
@@ -114,12 +121,15 @@
|
|
|
114
121
|
}
|
|
115
122
|
.pill:focus-visible { outline: 2px solid var(--accent, var(--agentgui-accent)); outline-offset: 2px; }
|
|
116
123
|
|
|
117
|
-
/* subagent toggle */
|
|
118
|
-
.subagent-toggle {
|
|
119
|
-
.subagent-toggle input { width: 16px; height: 16px; cursor: pointer; }
|
|
124
|
+
/* subagent toggle (the control itself is the kit Checkbox) */
|
|
125
|
+
.subagent-toggle { padding: .4em 0; min-height: 32px; }
|
|
120
126
|
|
|
121
127
|
.field-error { color: var(--warn, var(--agentgui-warn)); }
|
|
122
128
|
|
|
129
|
+
/* Boot splash: static text painted with the HTML, replaced by the first
|
|
130
|
+
mount render - the cold module waterfall no longer reads as a blank page. */
|
|
131
|
+
.boot-splash { height: 100%; min-height: 60vh; display: flex; align-items: center; justify-content: center; color: var(--agentgui-muted); font-size: .9rem; }
|
|
132
|
+
|
|
123
133
|
/* chat control cluster in the crumb bar: model picker, +new, status.
|
|
124
134
|
Allowed to wrap so it never overflows the crumb on tablet widths. */
|
|
125
135
|
.chat-controls { display: flex; align-items: center; gap: .6em; flex-wrap: wrap; }
|
|
@@ -255,7 +265,7 @@
|
|
|
255
265
|
</style>
|
|
256
266
|
</head>
|
|
257
267
|
<body>
|
|
258
|
-
<div id="app"></div>
|
|
268
|
+
<div id="app"><div class="boot-splash" role="status">loading agentgui…</div></div>
|
|
259
269
|
<script type="module" src="./js/app.js"></script>
|
|
260
270
|
</body>
|
|
261
271
|
</html>
|