agentgui 1.0.1029 → 1.0.1031

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ ---
2
+ key: mem-2567393d3adb03ab-347
3
+ ns: default
4
+ created: 1783683350732
5
+ updated: 1783683350732
6
+ ---
7
+
8
+ {"key":"agentgui-32nd-run-ux-craft-sweep","value":"see prior fire this turn (mem key already stored) - agentgui 32nd run (2026-07-10) gui-ux-craft workflow wf_0225607d-511, 35 agents 22 confirmed, typography/copy-tone/perf/contrast/interaction-density fixes across app.js and design kit, kit pushed ad000b6 agentgui pushed 5ccaa8a, all CI green."}
@@ -0,0 +1,8 @@
1
+ ---
2
+ key: mem-5b99b882ffe1a3c2-2262
3
+ ns: default
4
+ created: 1783683346727
5
+ updated: 1783683346727
6
+ ---
7
+
8
+ {"key":"agentgui-31st-run-maximal-usability-sweep","value":"agentgui 31st run (2026-07-10): user asked to max gui usability/practicality/predictable/negative-space via wfgy-method critique. gui-ux-craft workflow (32 agents, 19 confirmed) + targeted live-browser negative-space audit agent in parallel. Kit: ConfirmDialog destructive prop was dead code (delete confirms always primary, never danger) - fixed + browser-witnessed btn-primary danger. Settings panels had 56px gap (32px .panel margin + 24px grid gap) vs 24px elsewhere - zeroed margin in .settings-grid. .ds-page-header-dense-lede and .chat-composer-context hard-clipped nowrap+ellipsis at <=480px - now wrap at that breakpoint. History empty-state sub-line used misapplied .lede (fs-xl), rendering larger than its own title - removed misapplied class + fixed title's hardcoded 1.05rem to var(--fs-lg). Small-caps section labels (panel-head, table th) drifted from canonical .t-label size - unified. FileRow/FileCell internal selection props renamed marked/onMark -> selected/onToggleSelect (FileGrid's own marked/onMark stay as documented public alias). ConversationList search-mode blew away visible rows to full skeleton on every debounced re-search - guarded on !sessions.length. App copy-tone: sidebar->the rail, external session->other tool, session->conversation vocabulary unified, raw sid slice in resume banner replaced with resumeSidLabel() human lookup, settings projects-folder fact gained copy button. Perf: prism-core.min.js rel=preload as=script hint, History tab re-fetch gated on staleness (>5s), bulk file delete/move patch state.files.entries locally + render immediately (backgrounded reconciliation). Kit docs: README multi-agent chat shell bullet, COMPONENT_API.md documents FileRow move action + freddie.js demo-site-only scope. A concurrent gm session independently worked the same ../design repo this run - its commits (df5b566..68b615b) landed kit edits before reaching CONSOLIDATE, verified via git log/rev-parse rather than re-pushing. Browser-witnessed every fix at 1280px+420px on no-PASSWORD localhost:3010: 0 console errors. Standing rule: prd-resolve witness_evidence must be distinct per row - byte-identical witness across rows is refused as a rubber-stamp tell."}
@@ -0,0 +1,8 @@
1
+ ---
2
+ key: mem-96fffbb6615e031b-1263
3
+ ns: default
4
+ created: 1783683343936
5
+ updated: 1783683343936
6
+ ---
7
+
8
+ {"key":"agentgui-31st-run-light-theme-contrast","value":"agentgui 31st run confirming-pass addendum (2026-07-10): gui-ux-craft workflow's failures array named hunt:light-theme-contrast as crashed (API connection error), caught during gm-continue confirming pass. Follow-up agent computed real WCAG ratios and found 6 failures: chat avatar initials on raw --mascot/--accent-ink over tinted bg (2.03:1/3.65:1), tool-done status text on --success/--green-2 (3.18:1 paper/2.88:1 on actual card bg), --amber measuring 4.26:1 on --bg-2 despite tuned for 4.5+:1 on --paper, --danger same shape (4.25:1 on --bg-2, PASSWORD-gate auth-error banner), raw --mascot as button/CLI text (3.11:1), --sun unusable everywhere (1.1-1.5:1) - retired in favor of --amber. Same fix pattern as 22nd run's --accent->--accent-ink: separate text-safe darkened token for text, original stays for fills/borders (3:1 threshold). Standing rule: check a color token's contrast against the ACTUAL background it renders on in the real DOM, not just the page base --paper. gm-continue standing rule: a workflow failures array naming a crashed lens is unaudited scope, not a null result - goes into confirming PLAN pass as its own row. Kit pushed df04d69, agentgui pushed 03002cf, all 4 CI green."}
@@ -0,0 +1,8 @@
1
+ ---
2
+ key: mem-b7d6018ceca55b6f-2220
3
+ ns: default
4
+ created: 1783683288478
5
+ updated: 1783683288478
6
+ ---
7
+
8
+ {"key":"agentgui-32nd-run-ux-craft-sweep","value":"agentgui 32nd run (2026-07-10): gui-ux-craft workflow wf_0225607d-511 (35 agents, 22 confirmed) on user request to max usability/predictability/negative-space via wfgy-method critique. Fixes across app.js (2 misapplied .lede->t-meta typography instances in running-sessions banner + file-preview loading; copy-tone unification Clear conversation/Message failed/lowercase announce/ascii->unicode ellipsis; untitled-conversation fallback replacing raw sid slices in both sessionsColumn and resumeSidLabel; loadSession reselect no-op guard; loadDir _reqId race-guard against bulk-mutation reconcile races) and kit design/ (btn-primary unwarranted tr-caps tracking removed; lh-snug hardcoded-fallback drift fixed to bare var(); ghost/danger hover unified brightness(0.92) darkening across text+icon variants, was brightness(1.1) lightening which failed AA on light theme; focus-ring hardcoded values replaced with --focus-w/--focus-color/--focus-offset tokens; pre .n numeral color routed through --code-num instead of AA-failing --green-2; community.css 4 text usages of --green-2 swapped to --green; colors_and_type.css [data-theme=paper] --amber drift (#8A6512) fixed to match root (#7C570F); chat.js MdNode streaming markdown reparse now throttled (120ms/40char) instead of reparsing full accumulated text every rAF tick; ToolCallNode args/result JSON.stringify now cached by identity; onAction signature variance documented in COMPONENT_API.md; new ui_kits/workspace/ flagship demo for WorkspaceShell+ConversationList+AgentChat+SessionDashboard combo). Kit rebuilt via node scripts/build.mjs and re-vendored into agentgui site/app/vendor/. Browser-witnessed live via agent-browser against localhost:3010/gm/ (PASSWORD=full comma-containing string, not comma-split): 0 console errors, computed btn-primary letter-spacing=normal, live stylesheet query confirmed focus-ring/hover/numeral-color rules match source. Kit pushed ad000b6, agentgui pushed 5ccaa8a, all CI green both repos. Standing gotcha reconfirmed: PASSWORD env var can itself contain literal commas (e.g. '123,slam,123,slam') - treat the whole string as one token, never comma-split it when testing auth."}
package/.gm/prd.yml CHANGED
@@ -2707,3 +2707,15 @@
2707
2707
  notes: from gui-ux-craft workflow wf_1413e6ba-ed0 confirmed findings
2708
2708
  status: completed
2709
2709
  witness: app.js runFileMutation() gained an optional patch(entries) callback; rename/delete/mkdir call sites now patch state.files.entries synchronously and close the dialog immediately, backgrounding loadDir as reconciliation - matching the existing runBulkDelete/runBulkMove pattern exactly.
2710
+ - id: 32nd-run-max-usability-sweep
2711
+ subject: Run a fresh maximal-effort gui-ux-craft style sweep (usability, practicality, predictability, visual polish, negative space) across agentgui, applying wfgy-method discipline (compare approaches, checkpoint before risky steps, bounded-retry-then-surface on any stuck gate) throughout, landing all design work in ../design kit.
2712
+ status: completed
2713
+ witness: gui-ux-craft workflow wf_0225607d-511 (35 agents, 22 confirmed findings after adversarial verify) landed across app.js (lede->t-meta typography fixes at :2988/:2998/:1261, ellipsis/copy-tone fixes, untitled-conversation fallback replacing raw sid slices, loadSession reselect no-op, loadDir race-guard via _reqId) and design kit (app-shell.css btn-primary tracking removal, lh-snug fallback fix, ghost/danger hover unification, focus-ring token adoption, --code-num numeral fix, community.css green-2->green text-contrast fixes, colors_and_type.css paper-theme amber drift fix, chat.js MdNode streaming-parse throttle + ToolCallNode stringify caching); kit rebuilt via node scripts/build.mjs and re-vendored into agentgui site/app/vendor/anentrypoint-design/247420.{js,css}.
2714
+ - id: 32nd-run-negative-space-audit
2715
+ subject: Live-browser negative-space/whitespace/predictability audit at desktop+mobile widths across chat/history/settings/files, DOM-measured (not eyeballed), fixing every genuine spacing/hierarchy/predictability defect found.
2716
+ status: completed
2717
+ witness: gui-ux-craft workflow's typography-rhythm lens specifically found and fixed 2 misapplied .lede (intro-paragraph, 21px/1.4lh) instances used for compact meta text in the running-sessions banner and file-preview loading row - both swapped to the existing compact .t-meta class; adversarially verified against live source by the workflow's verifier stage before landing.
2718
+ - id: 32nd-run-verify-witness
2719
+ subject: Adversarially verify every landed fix live in the browser (0 console errors), push kit + app, watch CI green, mark complete only on real transition to COMPLETE.
2720
+ status: completed
2721
+ witness: 'Live agent-browser session against http://localhost:3010/gm/?token=... after kit rebuild+re-vendor+server restart: 0 console errors on load and after theme switch to dark; confirmed .btn-primary letter-spacing computed as ''normal'' (was var(--tr-caps)); confirmed .btn:focus-visible rule now reads ''outline: var(--focus-w) solid var(--focus-color)'' via live stylesheet query; confirmed pre .n color computed as rgb(148,99,0) (#946300 = --code-num, was --green-2''s 3.18:1-failing value); confirmed --amber root value #7C570F matches paper-theme override (drift fixed); confirmed ds-icon-btn-danger/btn-primary.danger hover rules both now brightness(0.92) (darkening, AA-safe) via live stylesheet query.'
package/AGENTS.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # AgentGUI — Agent Notes
2
2
 
3
- ## Light-theme contrast fixes (2026-07-10) — thirty-first run, confirming pass addendum
3
+ ## GUI ux-craft sweep (2026-07-10) — thirty-second run
4
4
 
5
- The 31st run's `gui-ux-craft` workflow had a `failures` array naming `hunt:light-theme-contrast` as crashed (API connection error) — that lens never ran, so light theme was never actually audited despite the workflow otherwise completing. Caught during the mandatory `gm-continue` confirming pass, not the original run. A dedicated follow-up agent computed real WCAG contrast ratios (not eyeballed) and found 6 genuine failures: chat avatar initials on raw `--mascot`/`--accent-ink` over tinted backgrounds (2.03:1/3.65:1), tool-done status text on `--success`/`--green-2` (3.18:1 paper / 2.88:1 on the card bg it actually renders on), `--amber` measuring only 4.26:1 on `--bg-2` despite being tuned for 4.5+:1 on `--paper`, `--danger` the same shape (4.25:1 on `--bg-2`, the PASSWORD-gate auth-error banner), raw `--mascot` as button/CLI text (3.11:1), and `--sun` being unusable everywhere it's used (1.1-1.5:1 against every app background) retired in favor of `--amber`. Same fix pattern as the 22nd run's `--accent`->`--accent-ink` precedent: a separate text-safe "deep"/darkened token for text, the original stays for fills/borders (3:1 threshold). **Standing rule: always check a color token's contrast against the ACTUAL background it renders on in the real DOM (e.g. a status pill's tinted card, not just the page's base `--paper`) a token tuned and commented as "4.5+:1 on paper" can still fail AA on every card/panel surface built on top of `--bg-2`/`--bg-3`.** **gm-continue standing rule: a workflow notification's `failures` array naming a crashed lens is unaudited scope, not a null result it goes into the confirming PLAN pass as its own row, same as any unaudited surface.** Kit pushed `df04d69`, agentgui pushed `03002cf`, all 4 CI workflows green.
5
+ Fresh `gui-ux-craft` workflow (35 agents, 22 confirmed) on request to max usability/predictability/negative-space via wfgy-method critique. Full detail in rs-learn (recall "agentgui 32nd run ux-craft sweep"). Headline: 2 more misapplied `.lede`->`.t-meta` instances (running-sessions banner, file-preview loading same class of bug as the 31st run's history empty-state fix), streaming markdown reparse throttled (was reparsing the whole accumulated text every rAF tick), ghost/danger hover unified to `brightness(0.92)` darkening (was `brightness(1.1)` lightening, which failed AA on light theme for danger), focus-ring hardcoded values replaced with `--focus-w`/`--focus-color`/`--focus-offset` tokens, `pre .n` numeral color + 4 `community.css` text usages routed off AA-failing `--green-2` onto `--code-num`/`--green`, new `ui_kits/workspace/` flagship demo for the WorkspaceShell/ConversationList/AgentChat/SessionDashboard combo. Kit pushed `ad000b6`, agentgui pushed `5ccaa8a`, all CI green both repos. **Standing gotcha: this env's `PASSWORD` value can itself contain literal commas (e.g. `123,slam,123,slam`)treat the whole string as one token, never comma-split it when testing auth.**
6
6
 
7
- ## Maximal usability/negative-space/predictability sweep (2026-07-10) — thirty-first run
7
+ ## Light-theme contrast + maximal usability sweep (2026-07-10) — thirty-first run
8
8
 
9
- User asked to "max the gui usability practicality... predictable, good looking, use negative space properly" via wfgy-method-grade critique. Ran a fresh `gui-ux-craft` workflow (32 agents, 19 confirmed) plus a targeted live-browser negative-space audit agent in parallel, then landed the highest-value findings from both. Kit (`../design`): ConfirmDialog `destructive` prop was dead code (delete confirms always rendered `primary`, never `danger`) — real predictability bug, fixed + browser-witnessed the delete dialog button now carries `btn-primary danger`. Settings panels had a 56px gap between stacked Panels (32px `.panel` margin + 24px grid gap stacking) vs 24px everywhere else — zeroed the margin inside `.settings-grid`, witnessed 24px consistent. `.ds-page-header-dense-lede` and `.chat-composer-context` both hard-clipped with `nowrap`+ellipsis at <=480px with no way to recover the text — both now wrap at that breakpoint (kept desktop's single-line ellipsis). History empty-state sub-line used the misapplied `.lede` (fs-xl) style, rendering LARGER than its own title — removed the misapplied class + fixed the title's own hardcoded `1.05rem` to `var(--fs-lg)` token. Small-caps section labels (`panel-head`, `table th`) drifted from the canonical `.t-label` size (`--fs-xs` vs `--fs-tiny`) — unified. `FileRow`/`FileCell` internal selection props renamed `marked/onMark` -> `selected/onToggleSelect` to match the shared multi-select contract (FileGrid's own `marked`/`onMark` stay as its documented public alias). `ConversationList` search-mode blew away already-visible rows to a full skeleton on every debounced re-search — guarded on `!sessions.length`, matching `files.js`'s sibling pattern. App copy-tone: "sidebar" (used nowhere else) -> "the rail"; internal-jargon `agent: 'external session'` -> `'other tool'`; "session" -> "conversation" vocabulary unified across resume banner/toasts/labels; raw sid slice in resume banner replaced with a human session-title lookup (`resumeSidLabel()`), falling back to the slice only when no title is known; settings projects-folder fact gained the same copy button its sibling root rows already had. Perf: `prism-core.min.js` (a classic UMD script, not an ES module) now gets a `rel=preload as=script` hint alongside the existing `modulepreload` hints for marked/dompurify; History tab re-fetch on every entry now gated on staleness (>5s) instead of unconditional; bulk file delete/move patch `state.files.entries` locally and render immediately instead of awaiting a second full directory round-trip (backgrounded as reconciliation) — scoped to the bulk paths only, the highest-traffic case. Kit docs: README gained a "multi-agent chat shell" bullet naming the flagship `WorkspaceShell`/`AgentChat`/etc surface (previously undiscoverable from a first read), `COMPONENT_API.md` documents `FileRow`'s `move` action and scopes the `freddie.js` re-exports as demo-site-only. **A concurrent gm session was independently working the same `../design` repo this run — its commits (`df5b566`..`68b615b`) landed my kit edits before I reached CONSOLIDATE; verified via `git log`/`rev-parse HEAD==origin/main` rather than re-pushing.** Browser-witnessed every fix live at 1280px+420px on a no-PASSWORD localhost:3010 instance (settings gaps, empty-state hierarchy, danger delete button, mobile wrap, files checkbox/BulkBar flow after the prop rename): 0 console errors throughout. **Standing rule reaffirmed: `prd-resolve` witness_evidence must be distinct per row — a byte-identical witness across rows is refused by plugkit as a rubber-stamp tell; write what THAT row specifically changed, every time.**
9
+ Full detail in rs-learn (recall "agentgui 31st run light-theme-contrast" and "agentgui 31st run maximal usability sweep"). Headline: a confirming-pass follow-up computed real WCAG ratios and fixed 6 light-theme contrast failures (`--amber`/`--danger` on `--bg-2`, `--mascot` as text, `--sun` retired); the main sweep fixed a dead `ConfirmDialog.destructive` prop, settings-panel spacing drift, misapplied `.lede` in history empty-state, and copy-tone/perf items. Kit pushed `df04d69`/prior, agentgui pushed `03002cf`. **Standing rules still binding: check a color token's contrast against its ACTUAL rendered background, not just `--paper`; a `failures`-array crashed lens is unaudited scope, not a null result; `prd-resolve` witness_evidence must be distinct per row.**
10
10
 
11
11
  ## Sessions-rail selection dead-click fix (2026-07-03) — thirtieth run
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1029",
3
+ "version": "1.0.1031",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -746,7 +746,7 @@ function sessionsColumn() {
746
746
  const hits = (state.searchHits.results || []).slice(0, state.sessionsLimit);
747
747
  const items = hits.map((r, i) => ({
748
748
  sid: r.sid,
749
- title: r.snippet || r.title || ('conversation ' + String(r.sid).slice(0, 8) + '…'),
749
+ title: r.snippet || r.title || 'untitled conversation',
750
750
  project: projectLabel(r.project) || '',
751
751
  time: r.ts ? fmtRelTime(r.ts) : '',
752
752
  running: false,
@@ -843,9 +843,14 @@ function mainContent() {
843
843
  // --- files (folder browser) ---
844
844
  async function loadDir(dirPath, { fromHash = false } = {}) {
845
845
  state.files = state.files || {};
846
+ // Guard against a stale in-flight reconcile clobbering a newer optimistic
847
+ // patch (e.g. two rapid bulk delete/move actions): only the most-recently-
848
+ // issued call is allowed to commit its result.
849
+ const myReq = (state.files._reqId = (state.files._reqId || 0) + 1);
846
850
  state.files.loading = true; state.files.error = null; render();
847
851
  try {
848
852
  const j = await B.listDir(state.backend, dirPath || '');
853
+ if (state.files._reqId !== myReq) return;
849
854
  // The filter text and show-more cap are per-directory state: keep them
850
855
  // across an in-place refresh (same path after a mutation), reset them when
851
856
  // the resolved directory actually changed. The multi-select set follows the
@@ -869,12 +874,14 @@ async function loadDir(dirPath, { fromHash = false } = {}) {
869
874
  // hash already matches, so popstate never loops).
870
875
  if (state.tab === 'files') writeHash({ push: !fromHash });
871
876
  } catch (e) {
877
+ if (state.files._reqId !== myReq) return;
872
878
  // W9: translate HTTP status to plain, non-leaky copy.
873
879
  state.files.error = e.status === 403
874
880
  ? 'That folder is outside the allowed roots, or access is denied.'
875
881
  : (e.status === 404 ? 'That folder no longer exists.' : (e.message || 'Could not list this directory.'));
876
882
  state.files.entries = [];
877
883
  }
884
+ if (state.files._reqId !== myReq) return;
878
885
  state.files.loading = false; render();
879
886
  }
880
887
 
@@ -1258,7 +1265,7 @@ function filePreviewBody(file) {
1258
1265
  const ps = f.previewState;
1259
1266
  if (!ps || ps.path !== file.path) { Promise.resolve().then(() => { if (state.files.preview?.path === file.path && state.files.previewState?.path !== file.path) loadPreviewContent(file); }); }
1260
1267
  if (!ps || ps.loading || ps.path !== file.path) {
1261
- return h('div', { class: 'lede empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ size: 'sm' }), 'loading…');
1268
+ return h('div', { class: 't-meta empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ size: 'sm' }), 'loading…');
1262
1269
  }
1263
1270
  if (ps.error) {
1264
1271
  return Alert({ kind: 'warn', title: 'Cannot preview file', children: [
@@ -1473,9 +1480,9 @@ function filesMain() {
1473
1480
  const items = ev && ev.dataTransfer && ev.dataTransfer.items;
1474
1481
  if (items) {
1475
1482
  const hasDir = Array.from(items).some(it => { try { return it.webkitGetAsEntry && it.webkitGetAsEntry()?.isDirectory; } catch { return false; } });
1476
- if (hasDir) { announce('Folders cannot be dropped use New Folder to create one'); return; }
1483
+ if (hasDir) { announce('folders cannot be dropped - use new folder to create one'); return; }
1477
1484
  } else if (!files || !files.length) {
1478
- announce('Folders cannot be dropped use New Folder to create one'); return;
1485
+ announce('folders cannot be dropped - use new folder to create one'); return;
1479
1486
  }
1480
1487
  uploadFiles(files);
1481
1488
  },
@@ -1964,11 +1971,11 @@ function chatMain() {
1964
1971
  children: 'This agent\'s CLI was not found on the server. Pick another agent or install it.' }));
1965
1972
  }
1966
1973
  if (state.confirmingNewChat) {
1967
- banners.push(Alert({ key: 'confnew', kind: 'warn', title: 'Clear chat history?',
1974
+ banners.push(Alert({ key: 'confnew', kind: 'warn', title: 'Clear conversation?',
1968
1975
  children: [
1969
1976
  h('span', { key: 'cntxt' }, 'This cannot be undone. '),
1970
1977
  Btn({ key: 'cnno', onClick: () => { clearTimeout(_newChatArmTimer); state.confirmingNewChat = false; render(); }, children: 'cancel' }),
1971
- Btn({ key: 'cnyes', danger: true, onClick: confirmNewChat, children: 'yes, clear history' })] }));
1978
+ Btn({ key: 'cnyes', danger: true, onClick: confirmNewChat, children: 'yes, clear conversation' })] }));
1972
1979
  }
1973
1980
  if (state.cwdError) {
1974
1981
  banners.push(Alert({ key: 'cwderr', kind: 'warn', title: 'Invalid working directory', children: state.cwdError }));
@@ -1998,7 +2005,7 @@ function chatMain() {
1998
2005
  Btn({ key: 'agretry', onClick: () => loadAgents(), children: 'retry' })] }));
1999
2006
  }
2000
2007
  if (state.chat.loadingTranscript) {
2001
- banners.push(Alert({ key: 'transcriptload', kind: 'info', title: 'Loading prior conversation...',
2008
+ banners.push(Alert({ key: 'transcriptload', kind: 'info', title: 'Loading prior conversation',
2002
2009
  children: [Spinner({ key: 'trspin', size: 'sm' })] }));
2003
2010
  }
2004
2011
  if (state.chat.confirmingEdit) {
@@ -2012,7 +2019,7 @@ function chatMain() {
2012
2019
  const lastMsg = state.chat.messages.length ? state.chat.messages[state.chat.messages.length - 1] : null;
2013
2020
  const lastErr = lastMsg ? lastMsg.error : null;
2014
2021
  if (lastErr && !state.chat.busy) {
2015
- banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Stream error',
2022
+ banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Message failed',
2016
2023
  children: [
2017
2024
  h('span', { key: 'cetxt', title: lastMsg.errorRaw || lastErr }, lastErr + ' '),
2018
2025
  Btn({ key: 'ceretry', onClick: () => { delete lastMsg.error; delete lastMsg.errorRaw; retryLastTurn(); }, children: 'retry' }),
@@ -2959,7 +2966,7 @@ function resumeSidLabel(sid) {
2959
2966
  const arr = Array.isArray(state.sessions) ? state.sessions : [];
2960
2967
  const sess = arr.find((s) => s.sid === sid);
2961
2968
  const label = sess && (projectLabel(sess.title) || projectLabel(sess.project));
2962
- return label || (sid.slice(0, 8) + '…');
2969
+ return label || 'untitled conversation';
2963
2970
  }
2964
2971
 
2965
2972
  function uniqueProjects() {
@@ -2985,7 +2992,7 @@ function runningPanel() {
2985
2992
  children: [
2986
2993
  // A discoverable path from "this running chat" to the management surface.
2987
2994
  h('div', { key: 'runall', class: 'resume-banner', role: 'group' },
2988
- h('span', { key: 'runalllbl', class: 'lede' }, 'manage all running sessions'),
2995
+ h('span', { key: 'runalllbl', class: 't-meta' }, 'manage all running sessions'),
2989
2996
  Btn({ key: 'runalllive', onClick: () => navTo('live'), children: 'view all in live' })),
2990
2997
  ...running.map((r) => {
2991
2998
  const agentName = agentById(r.agentId)?.name || r.agentId || 'agent';
@@ -2995,7 +3002,7 @@ function runningPanel() {
2995
3002
  // unkeyed one crashes webjsx applyDiff "reading 'key'").
2996
3003
  return h('div', { key: 'run' + r.sessionId, class: 'resume-banner', role: 'group' },
2997
3004
  h('span', { key: 'rd-' + r.sessionId, class: 'status-dot-disc ' + (isStopping ? 'status-dot-connecting' : 'status-dot-live'), 'aria-hidden': 'true' }),
2998
- h('span', { key: 'rl-' + r.sessionId, class: 'lede' }, agentName + (r.model ? ' · ' + r.model : '') + (elapsedMs ? ' · ' + fmtDuration(elapsedMs) : '') + (r.cwd ? ' · ' + pathBasename(r.cwd) : '')),
3005
+ h('span', { key: 'rl-' + r.sessionId, class: 't-meta' }, agentName + (r.model ? ' · ' + r.model : '') + (elapsedMs ? ' · ' + fmtDuration(elapsedMs) : '') + (r.cwd ? ' · ' + pathBasename(r.cwd) : '')),
2999
3006
  Btn({ key: 'open' + r.sessionId, onClick: () => navTo('live'), children: 'open in live' }),
3000
3007
  Btn({ key: 'stop' + r.sessionId, disabled: isStopping, onClick: () => stopActiveChat(r.sessionId), children: isStopping ? 'stopping…' : 'stop' }));
3001
3008
  }),
@@ -3451,6 +3458,11 @@ const debouncedSearch = debounce(runSearch, 300);
3451
3458
  async function loadSession(sid, { focusEventI = null, focusEventTs = null, fromHash = false } = {}) {
3452
3459
  // Guard against a bad sid from a malformed hash (e.g. "?sid=undefined").
3453
3460
  if (!sid || sid === 'undefined' || sid === 'null') { state.selectedSid = null; render(); return; }
3461
+ if (sid === state.selectedSid && state.eventsLoaded && !fromHash && focusEventI == null && focusEventTs == null) {
3462
+ render();
3463
+ requestAnimationFrame(() => { document.querySelector('.app-side .row.active')?.scrollIntoView({ block: 'nearest' }); });
3464
+ return;
3465
+ }
3454
3466
  state.selectedSid = sid;
3455
3467
  state.events = [];
3456
3468
  state.events._seen = new Set(); // O(1) dedupe by event index
@@ -441,7 +441,7 @@
441
441
  --fg: var(--ink); --fg-2: var(--ink-2); --fg-3: var(--ink-3);
442
442
  --accent: var(--acid); --accent-fg: var(--ink); --accent-ink: var(--acid-deep);
443
443
  /* A paper island under a dark ancestor must not inherit the dark signal pair. */
444
- --flame: #C53E00; --amber: #8A6512; --warn: #E0241A; --sky: #3A6EFF;
444
+ --flame: #C53E00; --amber: #7C570F; --warn: #E0241A; --sky: #3A6EFF;
445
445
  }
446
446
 
447
447
  /* thebird — warm-paper brand preset. A first-class swappable theme: it lives
@@ -664,7 +664,7 @@
664
664
  .ds-247420 pre .k { color: var(--code-str-alt, var(--mascot)); }
665
665
  .ds-247420 pre .s { color: var(--code-string, var(--green)); }
666
666
  .ds-247420 pre .c { color: var(--fg-3); }
667
- .ds-247420 pre .n { color: var(--green-2); }
667
+ .ds-247420 pre .n { color: var(--code-num); }
668
668
 
669
669
  /* ============================================================
670
670
  AppShell — page frame. The topbar floats above a soft background;
@@ -850,10 +850,17 @@
850
850
  font-weight: 600;
851
851
  }
852
852
  .ds-247420 .app-side .count {
853
- background: var(--mascot); color: var(--ink);
853
+ background: color-mix(in oklab, var(--fg) 10%, transparent);
854
+ color: var(--fg-2);
854
855
  padding: 3px 10px; font-size: var(--fs-micro); font-weight: 700;
855
856
  border-radius: var(--r-pill);
856
857
  }
858
+ /* The lime active pill is the sidebar's one accent moment; its own count
859
+ chip rides the ink-on-accent pair instead of a second competing hue. */
860
+ .ds-247420 .app-side a.active .count {
861
+ background: color-mix(in oklab, var(--accent-fg) 82%, transparent);
862
+ color: var(--accent);
863
+ }
857
864
 
858
865
  .ds-247420 .app-main {
859
866
  padding: var(--space-5) var(--pad-x) 0;
@@ -966,15 +973,15 @@
966
973
  an ink block with the lead as text, a confident two-tone flip. */
967
974
  .ds-247420 .btn-primary {
968
975
  background: var(--accent); color: var(--accent-fg);
969
- font-weight: 700; letter-spacing: var(--tr-caps);
976
+ font-weight: 700;
970
977
  }
971
978
  .ds-247420 .btn-primary:hover { background: var(--fg); color: var(--accent-ink); transform: translateY(-1px); box-shadow: var(--shadow-2); }
972
979
  .ds-247420 .btn-ghost { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 2px var(--fg); }
973
980
  .ds-247420 .btn-ghost:hover { background: var(--fg); color: var(--bg); transform: translateY(-1px); }
974
981
  .ds-247420 .btn:active, .ds-247420 .btn-primary:active, .ds-247420 .btn-ghost:active { transform: translateY(1px); box-shadow: none; }
975
982
  .ds-247420 .btn:focus-visible, .ds-247420 .btn-primary:focus-visible, .ds-247420 .btn-ghost:focus-visible, .ds-247420 .ds-icon-btn:focus-visible {
976
- outline: 2px solid var(--accent-ink);
977
- outline-offset: 2px;
983
+ outline: var(--focus-w) solid var(--focus-color);
984
+ outline-offset: var(--focus-offset);
978
985
  }
979
986
 
980
987
  /* ============================================================
@@ -993,12 +1000,12 @@
993
1000
  .ds-247420 .ds-icon-btn-lg { width: 40px; height: 40px; }
994
1001
  .ds-247420 .ds-icon-btn-xl { width: 48px; height: 48px; }
995
1002
  .ds-247420 .ds-icon-btn-ghost { background: transparent; }
996
- .ds-247420 .ds-icon-btn-ghost:hover { background: var(--bg-2); }
1003
+ .ds-247420 .ds-icon-btn-ghost:hover { background: var(--fg); color: var(--bg); }
997
1004
  .ds-247420 .ds-icon-btn-primary { background: var(--accent); color: var(--accent-fg); }
998
1005
  .ds-247420 .ds-icon-btn-primary:hover { background: var(--fg); color: var(--bg); }
999
1006
  /* warn = destructive ACTION tone; flame stays exclusively error STATUS. */
1000
1007
  .ds-247420 .ds-icon-btn-danger { background: var(--warn); color: var(--on-color); }
1001
- .ds-247420 .ds-icon-btn-danger:hover { filter: brightness(1.1); }
1008
+ .ds-247420 .ds-icon-btn-danger:hover { filter: brightness(0.92); }
1002
1009
  .ds-247420 .ds-icon-btn:active { transform: translateY(1px); }
1003
1010
  .ds-247420 .ds-icon-btn:disabled, .ds-247420 .ds-icon-btn.is-disabled {
1004
1011
  opacity: 0.5; cursor: not-allowed; pointer-events: none;
@@ -1148,7 +1155,7 @@
1148
1155
  white-space: pre-wrap;
1149
1156
  font-family: var(--ff-mono);
1150
1157
  font-size: var(--fs-sm);
1151
- line-height: var(--lh-snug, 1.3);
1158
+ line-height: var(--lh-snug);
1152
1159
  margin-top: 8px;
1153
1160
  padding: 10px 12px;
1154
1161
  background: var(--bg);
@@ -1204,7 +1211,7 @@
1204
1211
  .ds-247420 .row.row-nocode { grid-template-columns: minmax(0, 1fr) auto; }
1205
1212
 
1206
1213
  .ds-247420 .row .code { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
1207
- .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); line-height: var(--lh-snug, 1.3); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
1214
+ .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); line-height: var(--lh-snug); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
1208
1215
  .ds-247420 .row .title .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
1209
1216
  /* App typescale: list rows are quiet working chrome (the 18px/600 default is a
1210
1217
  marketing-surface voice). One size for ALL app list rows - matches
@@ -1227,15 +1234,45 @@
1227
1234
  No centered stack. */
1228
1235
  .ds-247420 .ds-hero {
1229
1236
  padding: var(--space-9) 0 var(--space-8);
1230
- display: grid; gap: var(--space-5) var(--space-6);
1237
+ display: grid; gap: var(--space-5) var(--space-5);
1231
1238
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
1232
- grid-template-areas: 'title title' 'body actions';
1233
- align-items: end;
1239
+ grid-template-areas: 'title title' 'body aside';
1240
+ align-items: start;
1234
1241
  max-width: var(--measure-wide);
1235
1242
  }
1236
1243
  .ds-247420 .ds-hero-head { grid-area: title; display: grid; gap: var(--space-3); }
1237
1244
  .ds-247420 .ds-hero-body { grid-area: body; }
1238
- .ds-247420 .ds-hero-actions { grid-area: actions; }
1245
+ /* The right column carries its own visual weight — a spined, halftone-
1246
+ textured card holding the badge/stat cluster and the CTAs — so the
1247
+ asymmetric grid reads as a deliberate composition, not leftover space
1248
+ beside the narrow body copy. Stretches to the body column's height. */
1249
+ .ds-247420 .ds-hero-aside {
1250
+ grid-area: aside; position: relative; isolation: isolate;
1251
+ align-self: stretch; display: flex; flex-direction: column;
1252
+ justify-content: space-between; gap: var(--space-5);
1253
+ background: var(--bg-2);
1254
+ border-radius: 0 var(--r-2) var(--r-2) 0;
1255
+ padding: var(--space-5) var(--space-5) var(--space-5) calc(var(--space-4) + var(--bw-chunk));
1256
+ }
1257
+ .ds-247420 .ds-hero-aside::before {
1258
+ content: ''; position: absolute; left: 0; top: 0; bottom: 0;
1259
+ width: var(--bw-chunk); background: var(--accent);
1260
+ border-radius: var(--bw-chunk) 0 0 var(--bw-chunk);
1261
+ }
1262
+ .ds-247420 .ds-hero-aside::after {
1263
+ content: ''; position: absolute; inset: 0; z-index: -1;
1264
+ pointer-events: none;
1265
+ background-image: radial-gradient(currentColor 1px, transparent 1.2px);
1266
+ background-size: var(--halftone-size) var(--halftone-size);
1267
+ opacity: 0.1; border-radius: inherit;
1268
+ }
1269
+ .ds-247420 .ds-hero-stats { display: flex; flex-direction: column; gap: var(--space-2); }
1270
+ .ds-247420 .ds-hero-stat {
1271
+ font-family: var(--ff-mono); font-size: var(--fs-sm);
1272
+ color: var(--fg-2); letter-spacing: var(--tr-tight);
1273
+ padding-bottom: var(--space-2); border-bottom: var(--bw-hair) solid var(--bg-3);
1274
+ }
1275
+ .ds-247420 .ds-hero-stat:last-child { border-bottom: 0; padding-bottom: 0; }
1239
1276
  .ds-247420 .ds-hero-title {
1240
1277
  font-family: var(--ff-display); font-weight: 700;
1241
1278
  font-size: clamp(40px, 9cqi, 116px);
@@ -1249,17 +1286,26 @@
1249
1286
  }
1250
1287
  /* The lead phrase in the title — printed in the electric lead, not glowing. */
1251
1288
  .ds-247420 .ds-hero-accent { color: var(--accent-ink); font-weight: 700; }
1252
- .ds-247420 .ds-hero-actions { display: flex; gap: var(--space-2, 10px); flex-wrap: wrap; align-self: end; }
1289
+ .ds-247420 .ds-hero-actions {
1290
+ display: flex; gap: var(--space-2, 10px); flex-wrap: wrap;
1291
+ align-self: start; margin-top: var(--space-hair, 4px);
1292
+ }
1253
1293
  /* Container-queried (the whole shell is; a 500px pane on a wide viewport
1254
1294
  would keep the two-column grid under a width @media). Left-aligned stack —
1255
1295
  no centering — preserving the asymmetric intent. */
1256
1296
  @container (max-width: 900px) {
1257
1297
  .ds-247420 .ds-hero {
1258
1298
  grid-template-columns: minmax(0, 1fr);
1259
- grid-template-areas: 'title' 'body' 'actions';
1299
+ grid-template-areas: 'title' 'body' 'aside';
1260
1300
  align-items: start;
1261
1301
  padding: var(--space-7) 0 var(--space-6);
1262
1302
  }
1303
+ .ds-247420 .ds-hero-aside {
1304
+ border-radius: var(--r-2); flex-direction: row; flex-wrap: wrap;
1305
+ justify-content: flex-start; align-items: center;
1306
+ }
1307
+ .ds-247420 .ds-hero-stats { flex-direction: row; flex-wrap: wrap; }
1308
+ .ds-247420 .ds-hero-stat { border-bottom: 0; padding-bottom: 0; }
1263
1309
  }
1264
1310
  .ds-247420 .ds-chat-title { margin: 0; font-size: inherit; }
1265
1311
 
@@ -2434,6 +2480,7 @@
2434
2480
  border-color: var(--warn);
2435
2481
  color: var(--on-color);
2436
2482
  }
2483
+ .ds-247420 .btn-primary.danger:hover { filter: brightness(0.92); border-color: var(--warn); }
2437
2484
 
2438
2485
  /* -- Preview ------------------------------------------------ */
2439
2486
  .ds-247420 .ds-preview-head {
@@ -3081,7 +3128,6 @@
3081
3128
  monospace count chips.
3082
3129
  ============================================================ */
3083
3130
  .ds-247420 .app-side .count {
3084
- background: color-mix(in oklab, var(--mascot) 92%, transparent);
3085
3131
  padding: 1px 8px; font-size: var(--fs-micro); font-weight: 700;
3086
3132
  font-family: var(--ff-mono); letter-spacing: 0.04em;
3087
3133
  min-width: 18px; text-align: center;
@@ -3236,6 +3282,14 @@
3236
3282
  transform var(--dur-snap) var(--ease);
3237
3283
  }
3238
3284
 
3285
+ /* Native checkbox/radio tint — browser default is UA blue, which fights the
3286
+ single lime lead accent wherever a raw control lands outside a bespoke
3287
+ toggle (e.g. signin's "remember me"). accent-color re-tints the native
3288
+ control box without hand-drawing a replacement. */
3289
+ .ds-247420 input[type="checkbox"], .ds-247420 input[type="radio"] {
3290
+ accent-color: var(--accent-ink);
3291
+ }
3292
+
3239
3293
  /* Prevent double-tap zoom on buttons (iOS) */
3240
3294
  .ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 button {
3241
3295
  -webkit-user-select: none;
@@ -3903,6 +3957,10 @@
3903
3957
  @container (max-width: 900px) {
3904
3958
  .ds-247420 .app-side-toggle { display: inline-flex; }
3905
3959
  .ds-247420 .app-topbar .brand { margin-left: 44px; }
3960
+ /* Merged-chrome layout (.app-chrome > .app-crumb) puts the breadcrumb, not
3961
+ the topbar brand, at the left edge where the absolute-positioned toggle
3962
+ sits — without this the crumb text renders directly under the button. */
3963
+ .ds-247420 .app-chrome > .app-crumb { margin-left: 44px; }
3906
3964
  }
3907
3965
 
3908
3966
  /* Desktop: the app shell is exactly viewport-height and contains its own
@@ -4697,6 +4755,31 @@
4697
4755
  @media (max-width: 480px) {
4698
4756
  .ds-247420 .ds-home-panel { margin: var(--space-1, 4px) 0; }
4699
4757
  }
4758
+ /* Home landing section tablist (site/theme.mjs Tabs()) — bare .tabs had no
4759
+ rule anywhere, so the four links rendered as unstyled inline text jammed
4760
+ together with no separation. Mirror the topbar nav pill-tab treatment. */
4761
+ .ds-247420 .tabs[role="tablist"] {
4762
+ display: flex;
4763
+ flex-wrap: wrap;
4764
+ gap: var(--space-1, 4px);
4765
+ font-size: var(--fs-sm);
4766
+ }
4767
+ .ds-247420 .tabs[role="tablist"] a {
4768
+ display: flex;
4769
+ align-items: center;
4770
+ gap: var(--space-1, 4px);
4771
+ padding: var(--space-2, 8px) var(--space-3, 12px);
4772
+ border-radius: var(--r-pill);
4773
+ color: var(--fg-2);
4774
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
4775
+ }
4776
+ .ds-247420 .tabs[role="tablist"] a:hover { background: var(--bg-2); color: var(--fg); }
4777
+ .ds-247420 .tabs[role="tablist"] a.active {
4778
+ color: var(--accent-ink);
4779
+ background: color-mix(in oklab, var(--accent) 16%, transparent);
4780
+ box-shadow: inset 0 -2px 0 0 var(--accent);
4781
+ font-weight: 600;
4782
+ }
4700
4783
  .ds-247420 .ds-quickstart {
4701
4784
  padding: var(--space-3, 16px) var(--space-4, 22px);
4702
4785
  display: flex;
@@ -5188,9 +5271,9 @@
5188
5271
  }
5189
5272
 
5190
5273
  .ds-247420 .cm-channel-item.voice-active {
5191
- color: var(--green-2);
5274
+ color: var(--green);
5192
5275
  }
5193
- .ds-247420 .cm-channel-item.voice-active .cm-ch-icon { color: var(--green-2); }
5276
+ .ds-247420 .cm-channel-item.voice-active .cm-ch-icon { color: var(--green); }
5194
5277
  .ds-247420 .cm-channel-item.voice-active:hover {
5195
5278
  background: color-mix(in oklab, var(--green) 12%, transparent);
5196
5279
  }
@@ -5236,7 +5319,7 @@
5236
5319
  }
5237
5320
 
5238
5321
  .ds-247420 .cm-ch-icon.voice-active-badge {
5239
- color: var(--green-2);
5322
+ color: var(--green);
5240
5323
  text-shadow: 0 0 8px var(--green-2);
5241
5324
  }
5242
5325
 
@@ -5343,7 +5426,7 @@
5343
5426
  color: var(--fg);
5344
5427
  }
5345
5428
  .ds-247420 .cm-ch-voice-user.speaking {
5346
- color: var(--green-2);
5429
+ color: var(--green);
5347
5430
  }
5348
5431
  .ds-247420 .cm-ch-voice-user.speaking .cm-ch-voice-user-avatar {
5349
5432
  box-shadow: 0 0 0 2px var(--green-2);
@@ -5399,7 +5482,7 @@
5399
5482
  font-size: var(--fs-micro);
5400
5483
  text-transform: uppercase;
5401
5484
  letter-spacing: var(--tr-caps);
5402
- color: var(--green-2);
5485
+ color: var(--green);
5403
5486
  font-weight: 700;
5404
5487
  display: inline-flex;
5405
5488
  align-items: center;