agentgui 1.0.946 → 1.0.948

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/AGENTS.md CHANGED
@@ -10,7 +10,7 @@ When `PASSWORD` env var is set, every HTTP route is gated by `lib/http-handler.j
10
10
  - `site/app/js/backend.js` — same-origin WS/HTTP client (`DEFAULT_BACKEND = ''`); the transport glue that wires the kit; `?backend=` query override for cross-origin debugging
11
11
  - `site/app/js/app.js` — webjsx view + state; renders the `AgentChat` kit (from `anentrypoint-design`) for the chat surface and wires agentgui's WS/ccsniff state as kit callbacks; history/settings remain agentgui-local. Exposes `window.__agentgui`
12
12
 
13
- The chat GUI lives in the design kit, not in agentgui. The reusable multi-agent chat surface is the `AgentChat` component in `anentrypoint-design` (`src/components/agent-chat.js`); agentgui keeps only the transport glue (WS `backend.js`, ccsniff history wiring, agent orchestration) and passes state + callbacks into the kit. To change chat UI, edit the kit and push it (CI publishes to npm unpkg `@latest`), not agentgui.
13
+ The chat GUI lives in the design kit, not in agentgui. The reusable multi-agent chat surface is the `AgentChat` component in `anentrypoint-design` (`src/components/agent-chat.js`); agentgui keeps only the transport glue (WS `backend.js`, ccsniff history wiring, agent orchestration) and passes state + callbacks into the kit. To change chat UI, edit the kit and push it (CI publishes to npm -> unpkg `@latest`), not agentgui.
14
14
  - `server.js` — boots ACP/agents/websocket plugins, mounts `createHistoryRouter()` from `ccsniff` at `/`, serves `site/app/` as static root
15
15
  - Plugins kept (lib/plugins/): acp, agents, database, files, stream, websocket, workflow
16
16
 
@@ -60,19 +60,13 @@ Pattern: all imports in lib/plugins/* must be either built-in (crypto, fs, path,
60
60
 
61
61
  ## Plugin Tool Provisioning on Windows
62
62
 
63
- **`lib/tool-spawner.js` must iterate bun npx fallback and match cross-platform command-not-found errors.**
64
-
65
- On Windows hosts without bun installed, the auto-provisioner on startup and 6h periodic update checker failed silently when spawnBunxProc tried `bun.cmd` directly. The missing command error came via process stderr+close, not the 'error' event, so simple ENOENT detection was insufficient. Additionally, the error message format differs by OS: Windows shows "'bun.cmd' is not recognized as an internal or external command", Linux/Mac show "command not found" or "cannot find".
66
-
67
- Fix: `BUNX_RUNNERS` array iterates `['bun', 'npx']` and tries each in sequence. Error detection regex `isMissingCmdError` matches `/not recognized|ENOENT|command not found|cannot find/i` on both `error.message` and captured stdout+stderr. Only falls through to next runner when the `missing` flag is set.
68
-
69
- Pattern: When a binary might not exist on all platforms, use a runner fallback strategy. Always capture and check both error.message and process output streams. Cross-platform error detection requires regex alternation on common message patterns.
63
+ **`lib/tool-spawner.js` iterates `BUNX_RUNNERS=['bun','npx']` and detects missing-command via regex on both error.message and stdout+stderr.** Full detail in rs-learn (recall "agentgui windows tool-spawner bunx fallback").
70
64
 
71
65
  ## GM Plugin Autonomy Blocker
72
66
 
73
67
  **gm plugin's pre-tool-use-hook.js enforces "must invoke gm:gm first" gate, blocking multi-tool autonomy. Hook content is NOT sourced from gm-starter/hooks/ files — it is templated from somewhere else.**
74
68
 
75
- The gm plugin enforces a gate via `.gm/needs-gm` marker that requires invoking `gm:gm` before any other tool use, which fragments multi-tool autonomous sessions in agentgui. A bypass patch was committed to c:/dev/gm (commit e300acf7, origin/main) in gm-starter/hooks/{pre-tool-use,prompt-submit}-hook.js to skip the gate when `.gm/prd.yml` exists, but it did NOT propagate after `/plugin update gm` (cache hash changed 495e36843d77 075e64d58498 but hook content unchanged).
69
+ The gm plugin enforces a gate via `.gm/needs-gm` marker that requires invoking `gm:gm` before any other tool use, which fragments multi-tool autonomous sessions in agentgui. A bypass patch was committed to c:/dev/gm (commit e300acf7, origin/main) in gm-starter/hooks/{pre-tool-use,prompt-submit}-hook.js to skip the gate when `.gm/prd.yml` exists, but it did NOT propagate after `/plugin update gm` (cache hash changed 495e36843d77 -> 075e64d58498 but hook content unchanged).
76
70
 
77
71
  The actual hook content is generated/templated from c:/dev/gm, likely from `lib/cli-adapter.js` or `platforms/cli-config-shared.js` or `lib/template-builder.js`, not from gm-starter/hooks/. Next session must:
78
72
  1. Locate the real hook generator in the gm codebase
@@ -97,35 +91,19 @@ The function previously returned "Model: X." when agentId was 'claude-code' and
97
91
 
98
92
  ## WebSocket Sync Endpoint Testing
99
93
 
100
- **WebSocket `/sync` endpoint message ordering requires registering handler BEFORE sending.**
101
-
102
- Server sends `sync_connected` with `clientId` on connect. Legacy handler (`lib/ws-legacy-handlers.js`) handles `ping→pong`, `subscribe→subscription_confirmed`, `get_subscriptions→subscriptions`, `unsubscribe`, `latency_report`. All responses use codec encode/decode (`lib/codec.js`). Pattern: queue outbound messages and use a waiters array + sequential promises to avoid race between send and handler registration. Test structure: `const queued = []; let waiting; ws.on('message', ...); queued.forEach(msg => ws.send(msg)); waiting.resolve(...)` ensures the handler is live before messages flow.
103
-
104
- ## better-sqlite3 & Node v24 Startup (2026-05-03)
94
+ **`/sync` sends `sync_connected`+clientId on connect; legacy handler in `lib/ws-legacy-handlers.js` (ping/subscribe/get_subscriptions/unsubscribe/latency_report) via codec; tests must register the message handler before sending.** Full detail in rs-learn (recall "agentgui websocket /sync testing handler ordering").
105
95
 
106
- **Node v24 has no prebuilt binary for better-sqlite3 (module version 137); npm rebuild silently fails.**
96
+ ## better-sqlite3 & Node v24 Startup
107
97
 
108
- When npm install runs on Node v24, the postinstall hook for better-sqlite3 silently fails because no prebuilt `.node` binary exists for that module version. The server then fails to require better-sqlite3 and crashes. npm rebuild also silently fails.
109
-
110
- Fix: compile from source in postinstall:
111
- ```json
112
- "postinstall": "node scripts/patch-fsbrowse.js && node scripts/copy-vendor.js && (cd node_modules/better-sqlite3 && node-gyp rebuild 2>/dev/null) || true"
113
- ```
114
-
115
- Paired changes:
116
- - `package.json` `start` script: `"bun server.js || node server.js"` (prefer bun, fall back to node)
117
- - `bin/gmgui.cjs` runtime detection: `spawnSync('bun', ['--version'], { shell: true })` to auto-detect bun availability, fallback to node (lines 46-47)
118
- - better-sqlite3 bumped ^12.6.2 → ^12.9.0
119
-
120
- bun start was already working (bun has native sqlite support via `bun:sqlite`). The node path was broken due to the missing native binding. Only compile from source fixes the node path.
98
+ Node v24 lacks a prebuilt better-sqlite3 binary; the node path needs a from-source `postinstall` compile, and `start` is `bun server.js || node server.js`. `database.js` tries `bun:sqlite` first. Detail (postinstall command, paired package.json/bin changes, CI must-run-under-bun) in rs-learn (recall "agentgui startup better-sqlite3 node v24 bun").
121
99
 
122
100
  ## webjsx applyDiff Array Keying (2026-05-04)
123
101
 
124
102
  **Children arrays that mix keyed VElements with raw text/numbers crash in `vendor/webjsx/applyDiff.js`.**
125
103
 
126
- `webjsx` requires all siblings in an array prop (children, right, left, breadcrumbs, etc.) to either ALL be VElements with keys or ALL be non-keyed. Passing `Crumb({ right: [h('span', { key: 'a' }, 'A'), h('span', { key: 'b' }, 'B'), 'connected'] })` crashes on render with `TypeError: Cannot read properties of undefined (reading 'key')` at line 247420.js when the render loop tries to read `.key` on the bare string.
104
+ `webjsx` requires all siblings in an array prop (children, right, left, breadcrumbs, etc.) to either ALL be VElements with keys or ALL be non-keyed. Passing `Crumb({ right: [h('span', { key: 'a' }, 'A'), h('span', { key: 'b' }, 'B'), 'connected'] })` crashes on render with `TypeError: Cannot read properties of undefined (reading 'key')` at line 247420.js when the render loop tries to read `.key` on the bare string.
127
105
 
128
- Fix: wrap any bare strings as keyed VElements: `h('span', { key: 'dot' }, 'connected')`. Rule: if ANY sibling in an array has a key, ALL siblings must be VElements (no raw strings or numbers).
106
+ Fix: wrap any bare strings as keyed VElements: `h('span', { key: 'dot' }, 'connected')`. Rule: if ANY sibling in an array has a key, ALL siblings must be VElements (no raw strings or numbers).
129
107
 
130
108
  Surfaced 2026-05-04 while validating the chat surface in `site/app/`. Fix applied to crumbRight construction in `site/app/js/app.js` view().
131
109
 
@@ -143,7 +121,7 @@ Event dispatch loop:
143
121
 
144
122
  Throttle renders via `requestAnimationFrame` to avoid event storm during burst loads.
145
123
 
146
- **First request to `/v1/history/*` triggers loadOnce() that walks all JSONL files under ~/.claude/projects** (env default; override with `CLAUDE_PROJECTS_DIR`). In our test env: 299 files, 80MB, 69k events 30-90s startup latency. Health check timeouts during this window are normal and expected. Subsequent requests are fast (cached index).
124
+ **First request to `/v1/history/*` triggers loadOnce() that walks all JSONL files under ~/.claude/projects** (env default; override with `CLAUDE_PROJECTS_DIR`). In our test env: 299 files, 80MB, 69k events -> 30-90s startup latency. Health check timeouts during this window are normal and expected. Subsequent requests are fast (cached index).
147
125
 
148
126
  The endpoints are served by ccsniff's Express router mounted in-process from `server.js`. No external proxy.
149
127
 
@@ -153,10 +131,30 @@ The GUI loads `anentrypoint-design` from `https://unpkg.com/anentrypoint-design@
153
131
 
154
132
  ## Agent/model/session management (2026-05-28)
155
133
 
156
- - `agents.list` (WS) returns `available` + `npxInstallable` per agent; `agents.models` returns model choices (claude-code sonnet/opus/haiku). The chat picker is **agent-then-model**, not a flat model list. Unavailable agents are disabled/gated.
134
+ - `agents.list` (WS) returns `available` + `npxInstallable` per agent; `agents.models` returns model choices (claude-code -> sonnet/opus/haiku). The chat picker is **agent-then-model**, not a flat model list. Unavailable agents are disabled/gated.
157
135
  - `chat.sendMessage` accepts `cwd` (defaults to STARTUP_CWD) and `model`/`agentId` separately. `chat.active` (WS) lists in-flight chats with agentId/model/cwd/startedAt/pid; the history tab polls it (3s) and shows a running panel with per-session stop.
158
136
  - Client (`app.js`): chat transcript persists to `localStorage[agentgui.chat]` and restores on load; tool_use/result events render as chat parts; keyboard shortcuts (g+c/h/s, n, /, ?); settings has an agents-status panel from `health.acp[]`.
159
137
 
138
+ ## Kit Row rail + rank contract
139
+
140
+ The design-kit `Row` (`anentrypoint-design` `src/components/content.js`) renders a leading status **rail** from a `rail` prop (`green` | `purple` | `flame`, via a `.row.rail-<tone>::before` bar) and accepts `rank` as an alias for the leading `code` index. A `state: 'disabled'` Row is inert — no `onClick`/`role=button`/tab-stop, `aria-disabled=true`. agentgui's history sessions, search results, and agents panel pass `rail` + `rank` and rely on this; the rendering reaches the live GUI only after the kit publishes to npm -> unpkg `@latest`. agentgui keeps the rail semantics consistent everywhere: **green = selected/ok, purple = subagent, flame = error/unavailable**.
141
+
142
+ ## GUI glyph policy
143
+
144
+ GUI source keeps typographic product characters — the middot separator `·`, the ellipsis `…`, and em/en dashes in prose — as established design (AGENTS.md itself uses `·`). It carries **no** decorative tells: arrows, box-drawing, bullets, status dots, checks. Status is words + the CSS-drawn `.status-dot-disc`. Convert any arrow/box-drawing/bullet glyph to ASCII on sight (`->`, `// --- x ---`, `-`/`*`).
145
+
160
146
  ## DS CSS cascade — overriding component styles (2026-05-28)
161
147
 
162
148
  **`installStyles()` injects DS CSS into a runtime `<style>` after the head `<style>`, so local overrides need `!important` or higher specificity than the DS's `.ds-247420`-prefixed rules.** Full detail (font vars `--ff-display`/`--ff-mono`, the `.chat-head .sub` "00 msgs" quirk, EventList `.row[role=button]`, `[data-prog-focus]` focus suppression, `projectLabel()` for cwd slugs) is in rs-learn (recall "agentgui GUI styling DS cascade installStyles").
149
+
150
+ ## No decorative glyphs — ASCII-only GUI (2026-06-04)
151
+
152
+ **The GUI carries no decorative tell-tale glyphs; remaining non-ASCII is the deliberate typographic separator set.** The de-jank sweep removed every tell-tale (status dots `[CHAR]`, arrows, box-drawing dividers, checks) in favor of ASCII words + the CSS-drawn status disc. The middot separator `·` is kept as established product design (it reads as a real meta-separator, used consistently across rows/subs); ellipsis `…` and em/en dashes were converted to `...` / `-` on sight. Verified live on buildesk: `document.body.innerText` matches no decorative-glyph regex (excluding the kept middot) across chat/history/settings. The DS Alert dismiss control renders its own close glyph only on a dismissible alert (intentional DS icon affordance, exempt).
153
+
154
+ ## DS SearchInput accessible name comes from `label`, not `aria-label` (2026-06-04)
155
+
156
+ **`anentrypoint-design`'s `SearchInput` (internal `De`) sets `aria-label = label || placeholder`; it ignores any `aria-label` prop you pass.** To give the search box a real accessible name, pass `label:` (and a matching `placeholder:` for the visible hint). Passing only `aria-label` leaves AT announcing the placeholder. A post-render `setAttribute` race-loses against the DS re-render, so the prop is the only durable fix.
157
+
158
+ ## DS load moved to unpkg @latest — offline regression, re-vendor follow-up (2026-06-04)
159
+
160
+ **`index.html` currently loads the DS kit (`247420.css` + `247420.js`) from `https://unpkg.com/anentrypoint-design@latest` at runtime, NOT the local `vendor/` copy.** Commit `947086e` made this deliberate trade ("always-latest" over offline) because the GUI's chat surface now renders via the kit's **load-bearing `AgentChat` component**, which exists in `@latest` (0.0.186) but NOT in the older locally-vendored bundle (`vendor/anentrypoint-design/247420.js`). Consequences: the GUI requires network, the shipped UI shifts when the upstream package publishes, and the local vendoring (gzip-Ha CSS blob, font/CDN-const localization) is bypassed. The separate jsdelivr imports inside the vendored bundle (`si`=marked@15, `ri`=dompurify@3, `_s`=prismjs@1.30.0/components) are therefore moot while on unpkg. **Follow-up to restore offline + predictability:** re-vendor 0.0.186+ locally (it has AgentChat), re-apply the offline localization (recall "DS bundle gzip Ha vendoring") AND rewire its `si`/`ri`/`_s` markdown-stack consts to the local `vendor/cdn/{marked.js,dompurify.js,prismjs/components/}` copies, then point `index.html` back at `./vendor/`. Witnessed live (browser `page.on('request')`): on `@latest` the page fetches `unpkg.com/anentrypoint-design@latest/dist/247420.{css,js}` at load.
package/docs/demo.html CHANGED
@@ -3,13 +3,13 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AgentGUI Live Demo Preview</title>
7
- <meta name="description" content="See what AgentGUI looks like on your machine a non-interactive preview of the Gmail-inspired multi-agent interface.">
6
+ <title>AgentGUI - Live Demo Preview</title>
7
+ <meta name="description" content="See what AgentGUI looks like on your machine - a non-interactive preview of the Gmail-inspired multi-agent interface.">
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;600&family=Google+Sans:wght@300;400;500;700&display=swap">
11
11
  <style>
12
- /* ── Page chrome ─────────────────────────────────────── */
12
+ /* ---- Page chrome ---- */
13
13
  :root {
14
14
  --paper: #EFE9DD; --ink: #0B0B09; --green: #247420; --green-2: #92CEAC;
15
15
  --panel-0: var(--paper); --panel-1: #f6f1e7; --panel-2: #ece5d8; --panel-3: #ddd4c3;
@@ -37,7 +37,7 @@
37
37
  -webkit-font-smoothing: antialiased;
38
38
  }
39
39
 
40
- /* ── Page layout ─────────────────────────────────────── */
40
+ /* ---- Page layout ---- */
41
41
  .page { display: flex; flex-direction: column; min-height: 100vh; }
42
42
 
43
43
  .topbar {
@@ -66,7 +66,7 @@
66
66
  .hero p { color: var(--panel-text-2); font-size: 13px; margin: 0 0 8px; }
67
67
  .hero .sub { font-size: 12px; color: var(--panel-text-3); }
68
68
 
69
- /* ── Browser mockup ──────────────────────────────────── */
69
+ /* ---- Browser mockup ---- */
70
70
  .mockup-wrap {
71
71
  margin: 0 auto;
72
72
  max-width: 1200px;
@@ -112,7 +112,7 @@
112
112
  .browser-viewport { background: #202124; }
113
113
  }
114
114
 
115
- /* ── Gmail-skin app shell (inside viewport) ──────────── */
115
+ /* ---- Gmail-skin app shell (inside viewport) ---- */
116
116
  .app-shell {
117
117
  display: grid;
118
118
  grid-template-columns: 72px 280px 1fr;
@@ -452,7 +452,7 @@
452
452
  }
453
453
  .send-btn svg { width: 16px; height: 16px; }
454
454
 
455
- /* ── Below-fold sections ─────────────────────────────── */
455
+ /* ---- Below-fold sections ---- */
456
456
  .features { margin-top: 64px; }
457
457
  .features h2 {
458
458
  font-family: var(--ff-display); font-size: 24px; text-transform: lowercase;
@@ -484,7 +484,7 @@
484
484
  }
485
485
  .install-note { font-size: 12px; color: var(--panel-text-3); margin-top: 8px; }
486
486
 
487
- /* ── Footer ──────────────────────────────────────────── */
487
+ /* ---- Footer ---- */
488
488
  .footer {
489
489
  height: 24px; background: var(--panel-accent); color: var(--panel-accent-fg);
490
490
  display: flex; align-items: center; padding: 0 14px; gap: 14px;
@@ -503,7 +503,7 @@
503
503
  <body>
504
504
  <div class="page">
505
505
 
506
- <!-- ── Top bar ── -->
506
+ <!-- ---- Top bar ---- -->
507
507
  <header class="topbar">
508
508
  <span class="brand">247420<span class="slash">/</span>agentgui</span>
509
509
  <nav>
@@ -514,7 +514,7 @@
514
514
  </nav>
515
515
  </header>
516
516
 
517
- <!-- ── Main content ── -->
517
+ <!-- ---- Main content ---- -->
518
518
  <main class="content">
519
519
 
520
520
  <section class="hero">
@@ -523,7 +523,7 @@
523
523
  <p class="sub">Gmail-inspired layout · Claude Code · real project · real output</p>
524
524
  </section>
525
525
 
526
- <!-- ── Browser mockup ── -->
526
+ <!-- ---- Browser mockup ---- -->
527
527
  <div class="mockup-wrap">
528
528
 
529
529
  <!-- Browser chrome -->
@@ -661,7 +661,7 @@
661
661
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
662
662
  <span class="tool-name">Read</span>
663
663
  <span style="flex:1;font-size:11px;color:#9aa0a6">static/css/main.css</span>
664
- <span class="tool-ok">✓ ok</span>
664
+ <span class="tool-ok">[ok] ok</span>
665
665
  </div>
666
666
  <div class="tool-body">read 620 lines · sidebar · main-panel · conversation-item · input-card</div>
667
667
  </div>
@@ -671,7 +671,7 @@
671
671
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
672
672
  <span class="tool-name">Read</span>
673
673
  <span style="flex:1;font-size:11px;color:#9aa0a6">static/css/brand-bible.css</span>
674
- <span class="tool-ok">✓ ok</span>
674
+ <span class="tool-ok">[ok] ok</span>
675
675
  </div>
676
676
  <div class="tool-body">read 449 lines · design tokens · panel vars · typography</div>
677
677
  </div>
@@ -681,7 +681,7 @@
681
681
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
682
682
  <span class="tool-name">Write</span>
683
683
  <span style="flex:1;font-size:11px;color:#9aa0a6">static/css/gmail-skin.css</span>
684
- <span class="tool-ok">✓ written</span>
684
+ <span class="tool-ok">[ok] written</span>
685
685
  </div>
686
686
  <div class="tool-body">363 lines · grid layout · nav-rail · gm-* tokens · pill rows · search bar</div>
687
687
  </div>
@@ -691,7 +691,7 @@
691
691
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
692
692
  <span class="tool-name">Edit</span>
693
693
  <span style="flex:1;font-size:11px;color:#9aa0a6">static/index.html</span>
694
- <span class="tool-ok">✓ updated</span>
694
+ <span class="tool-ok">[ok] updated</span>
695
695
  </div>
696
696
  <div class="tool-body">added .nav-rail · .header-search-wrap · loaded gmail-skin.css</div>
697
697
  </div>
@@ -703,12 +703,12 @@
703
703
  </div>
704
704
  <div class="msg-body">
705
705
  Done. Here's what changed:<br><br>
706
- <strong>Three-column grid</strong> icon nav rail (72px) conversation list (280px) main panel<br>
707
- <strong>Nav rail</strong> with Compose FAB + Chats / Archived / Tools icons, pill hover/active states<br>
708
- <strong>Header search bar</strong> prominent pill input spanning the header, syncs to sidebar search<br>
709
- <strong>Conversation rows</strong> bold title, muted meta + timestamp, pill hover highlight in Google blue<br>
710
- <strong>Input card</strong> rounded corners, elevation shadow, Google-blue send pill<br>
711
- <strong>Soft borders</strong> <code>#e0e0e0</code> hairlines + drop shadows instead of brand-bible outlines
706
+ - <strong>Three-column grid</strong> - icon nav rail (72px) -> conversation list (280px) -> main panel<br>
707
+ - <strong>Nav rail</strong> with Compose FAB + Chats / Archived / Tools icons, pill hover/active states<br>
708
+ - <strong>Header search bar</strong> - prominent pill input spanning the header, syncs to sidebar search<br>
709
+ - <strong>Conversation rows</strong> - bold title, muted meta + timestamp, pill hover highlight in Google blue<br>
710
+ - <strong>Input card</strong> - rounded corners, elevation shadow, Google-blue send pill<br>
711
+ - <strong>Soft borders</strong> - <code>#e0e0e0</code> hairlines + drop shadows instead of brand-bible outlines
712
712
  </div>
713
713
  <div class="msg-ts">10:43 AM</div>
714
714
  </div>
@@ -757,7 +757,7 @@
757
757
  </div><!-- /viewport -->
758
758
  </div><!-- /mockup-wrap -->
759
759
 
760
- <!-- ── Features section ── -->
760
+ <!-- ---- Features section ---- -->
761
761
  <section class="features">
762
762
  <h2>what you get</h2>
763
763
  <div class="feat-grid">
@@ -784,7 +784,7 @@
784
784
  <div class="feat-card">
785
785
  <div class="feat-icon">🔄</div>
786
786
  <div class="feat-title">Queue &amp; steer</div>
787
- <div class="feat-desc">Queue messages while the agent is running. Steer mid-session with a single click no need to wait.</div>
787
+ <div class="feat-desc">Queue messages while the agent is running. Steer mid-session with a single click - no need to wait.</div>
788
788
  </div>
789
789
  <div class="feat-card">
790
790
  <div class="feat-icon">🧩</div>
@@ -794,21 +794,21 @@
794
794
  </div>
795
795
  </section>
796
796
 
797
- <!-- ── Install section ── -->
797
+ <!-- ---- Install section ---- -->
798
798
  <section class="install-section">
799
799
  <h2>get started</h2>
800
800
  <div class="install-cmd">$ npx agentgui</div>
801
- <p class="install-note">Requires Node.js 18+. Opens on http://localhost:3000/gm/ no account, no cloud, runs entirely local.</p>
801
+ <p class="install-note">Requires Node.js 18+. Opens on http://localhost:3000/gm/ - no account, no cloud, runs entirely local.</p>
802
802
  </section>
803
803
 
804
804
  </main>
805
805
 
806
806
  <footer class="footer">
807
807
  <span>main</span>
808
- <span>• agentgui</span>
808
+ <span>- agentgui</span>
809
809
  <span class="spread"></span>
810
810
  <a href="https://github.com/AnEntrypoint/agentgui">github</a>
811
- <span>• 247420 / mmxxvi</span>
811
+ <span>- 247420 / mmxxvi</span>
812
812
  </footer>
813
813
 
814
814
  </div>
package/lib/codec.js CHANGED
@@ -1,4 +1,4 @@
1
- // agentgui WS wire codec plain JSON (UTF-8 text frames).
1
+ // agentgui WS wire codec - plain JSON (UTF-8 text frames).
2
2
  // Browser-compatible (no msgpackr). encode() returns a string the ws library
3
3
  // sends as a text frame; decode() handles both Buffer/Uint8Array (Node) and
4
4
  // string (browser) inputs.
@@ -40,7 +40,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
40
40
  } else if (_auth.startsWith('Bearer ')) {
41
41
  _ok = _checkToken(_auth.slice(7));
42
42
  }
43
- // EventSource and same-origin links can't set headers accept ?token= as fallback.
43
+ // EventSource and same-origin links can't set headers - accept ?token= as fallback.
44
44
  if (!_ok) {
45
45
  try {
46
46
  const _qsTok = new URL(req.url, 'http://localhost').searchParams.get('token');
@@ -86,7 +86,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
86
86
  return;
87
87
  }
88
88
 
89
- // Terminal sessions gated by the Basic-auth check at the top of this handler.
89
+ // Terminal sessions - gated by the Basic-auth check at the top of this handler.
90
90
  // Never expose these routes without PASSWORD set.
91
91
  if (pathOnly === '/api/terminal/sessions' && req.method === 'GET') {
92
92
  sendJSON(req, res, 200, term.listSessions()); return;
@@ -29,7 +29,7 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
29
29
  queries.updateSession(sessionId, { status: 'active' });
30
30
  const cwd = conv?.workingDirectory || STARTUP_CWD;
31
31
  // Resolve agent before building stateRef so isJsonlBacked can be set correctly.
32
- // claude-code (protocol: direct) writes JSONL JsonlParser owns event broadcasting.
32
+ // claude-code (protocol: direct) writes JSONL -> JsonlParser owns event broadcasting.
33
33
  // All other agents (protocol: acp) rely solely on onEvent for streaming.
34
34
  let resolvedAgentId = agentId || 'claude-code';
35
35
  const wrapperAgent = discoveredAgents.find(a => a.id === resolvedAgentId && a.protocol === 'cli-wrapper' && a.acpId);
@@ -4,8 +4,8 @@
4
4
  * For claude-code (isJsonlBacked=true):
5
5
  * JsonlParser owns all event broadcasting via the JSONL file watcher.
6
6
  * This handler only needs to:
7
- * 1. Extract session_id setClaudeSessionId + registerSession
8
- * 2. Detect inline rate-limit messages scheduleRetry
7
+ * 1. Extract session_id -> setClaudeSessionId + registerSession
8
+ * 2. Detect inline rate-limit messages -> scheduleRetry
9
9
  *
10
10
  * For ACP agents (isJsonlBacked=false):
11
11
  * No JSONL file is written. This handler broadcasts streaming_start,
@@ -18,7 +18,7 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
18
18
  if (entry) entry.lastActivity = Date.now();
19
19
 
20
20
  // Register session with file watcher as soon as we see the session_id.
21
- // This pre-maps claudeSessionId (convId, dbSessionId) in JsonlParser before
21
+ // This pre-maps claudeSessionId -> (convId, dbSessionId) in JsonlParser before
22
22
  // the 16ms file-watcher debounce fires, preventing duplicate conversation creation.
23
23
  if (parsed.session_id) {
24
24
  if (!batcherRef.resumeSessionId || batcherRef.resumeSessionId !== parsed.session_id) {
package/lib/terminal.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // WebSocket terminal sessions. Ported from acptoapi 2026-05-21.
2
2
  // Auth: callers MUST gate /api/terminal/* + the WS upgrade behind agentgui's
3
- // PASSWORD Basic-auth check. This module does no auth on its own never
3
+ // PASSWORD Basic-auth check. This module does no auth on its own - never
4
4
  // expose without the gate.
5
5
 
6
6
  import os from 'os';
package/lib/ws-setup.js CHANGED
@@ -24,7 +24,7 @@ export function createWsSetup(server, { BASE_URL, watch, staticDir, _assetCache,
24
24
  hotReloadClients.push(ws);
25
25
  ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
26
26
  } else if (wsRoute.startsWith('/api/terminal/sessions/')) {
27
- // Terminal session WS auth was already enforced by wss-level PASSWORD
27
+ // Terminal session WS - auth was already enforced by wss-level PASSWORD
28
28
  // check at the top of this connection callback. attach to existing session.
29
29
  const m = wsRoute.match(/^\/api\/terminal\/sessions\/([0-9a-f]+)$/);
30
30
  if (!m) { ws.close(4400, 'bad-terminal-path'); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.946",
3
+ "version": "1.0.948",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // One-time build: produces static/vendor/rippleui.css from the rippleui npm package.
3
- // Buildless at runtime commit the output so `npm install` is enough to serve.
3
+ // Buildless at runtime - commit the output so `npm install` is enough to serve.
4
4
  //
5
5
  // Source files (from node_modules/rippleui/dist/css/):
6
6
  // base.css - Tailwind preflight/reset (~2KB)
7
7
  // components.css - RippleUI components (.btn etc.) (~143KB)
8
8
  // utilities.css - RippleUI extra utilities (~0.05KB)
9
- // styles.css - full TW + RippleUI monolith (~4.7MB) we only mine token defs
9
+ // styles.css - full TW + RippleUI monolith (~4.7MB) - we only mine token defs
10
10
  //
11
11
  // Output: static/vendor/rippleui.css containing
12
12
  // 1. :root / html.dark token blocks extracted from styles.css (so components.css works)
@@ -47,12 +47,12 @@ while ((m = blockRe.exec(styles))) {
47
47
  }
48
48
 
49
49
  if (tokenBlocks.length === 0) {
50
- console.error('Could not find token blocks in styles.css rippleui shape changed?');
50
+ console.error('Could not find token blocks in styles.css - rippleui shape changed?');
51
51
  process.exit(1);
52
52
  }
53
53
 
54
- const header = `/* RippleUI v${readPkgVersion()} localized for AgentGUI.\n` +
55
- ` * Generated by scripts/build-rippleui.mjs do not edit by hand.\n` +
54
+ const header = `/* RippleUI v${readPkgVersion()} - localized for AgentGUI.\n` +
55
+ ` * Generated by scripts/build-rippleui.mjs - do not edit by hand.\n` +
56
56
  ` * Regenerate: bun run scripts/build-rippleui.mjs\n` +
57
57
  ` */\n`;
58
58
 
@@ -33,7 +33,7 @@ function pickChrome() {
33
33
  // macOS
34
34
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
35
35
  '/Applications/Chromium.app/Contents/MacOS/Chromium',
36
- // Windows program files variants
36
+ // Windows - program files variants
37
37
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
38
38
  'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
39
39
  path.join(home, 'AppData/Local/Google/Chrome/Application/chrome.exe'),
@@ -72,7 +72,7 @@ async function waitForHealthy(url, timeoutMs = 20000) {
72
72
  async function main() {
73
73
  fs.mkdirSync(OUT, { recursive: true });
74
74
  if (!fs.existsSync(FIXTURES)) {
75
- console.warn(`[capture] fixture dir does not exist: ${FIXTURES} using empty data dir`);
75
+ console.warn(`[capture] fixture dir does not exist: ${FIXTURES} - using empty data dir`);
76
76
  fs.mkdirSync(FIXTURES, { recursive: true });
77
77
  }
78
78
 
@@ -112,7 +112,7 @@ async function main() {
112
112
  try {
113
113
  await waitForHealthy(`${BASE}/api/health`);
114
114
  if (serverExitCode !== null) throw new Error(`Server exited with code ${serverExitCode} before becoming healthy`);
115
- console.log('[capture] server healthy launching browser');
115
+ console.log('[capture] server healthy - launching browser');
116
116
 
117
117
  const { default: puppeteer } = await import('puppeteer-core');
118
118
  const browser = await puppeteer.launch({
@@ -46,5 +46,5 @@ if (fs.existsSync(webjsxDist)) {
46
46
  fs.writeFileSync(dest, iife);
47
47
  console.log('[copy-vendor] built webjsx IIFE ->', 'static/lib/webjsx.js', `(${iife.split('\n').length} lines)`);
48
48
  } else {
49
- console.warn('[copy-vendor] webjsx not found in node_modules run npm install');
49
+ console.warn('[copy-vendor] webjsx not found in node_modules - run npm install');
50
50
  }
@@ -66,7 +66,7 @@ function det(prefix, ...seeds) {
66
66
  return `${prefix}-${h.slice(0, 16)}`;
67
67
  }
68
68
 
69
- // Anonymise returns cleaned text
69
+ // Anonymise - returns cleaned text
70
70
  function scrub(text) {
71
71
  if (typeof text !== 'string') return text;
72
72
  return text
@@ -87,8 +87,8 @@ function synthAssistant(title) {
87
87
  `\`lib/db-queries.js\` to use the new column and patch the associated tests.\n\n` +
88
88
  `The fix is a one-line change to the INSERT statement. All 19 tests pass now.`,
89
89
  'Refactor auth middleware':
90
- `The current auth middleware has three responsibilities rate limiting, basic-auth check, ` +
91
- `and CORS packed into a single 80-line function. I'll split them into three small ` +
90
+ `The current auth middleware has three responsibilities - rate limiting, basic-auth check, ` +
91
+ `and CORS - packed into a single 80-line function. I'll split them into three small ` +
92
92
  `middlewares in \`lib/http-middlewares/\` and compose them in \`http-handler.js\`. ` +
93
93
  `No behaviour change, just readability.`,
94
94
  'Add dark mode toggle':
@@ -109,13 +109,13 @@ function synthAssistant(title) {
109
109
  `markdown doc grouped by module. Output goes to \`docs/api.md\`. I'll add a JSDoc-style comment ` +
110
110
  `parser so handler descriptions can live inline with the route.`,
111
111
  };
112
- return lookup[title] || 'Done see the diff for details.';
112
+ return lookup[title] || 'Done - see the diff for details.';
113
113
  }
114
114
 
115
115
  function synthUser(title) {
116
116
  const lookup = {
117
117
  'Fix failing tests in db-queries':
118
- 'Hey the test suite has been flaky since yesterday. Can you figure out which specs are failing and fix them? Start with `bun test` and work from there.',
118
+ 'Hey - the test suite has been flaky since yesterday. Can you figure out which specs are failing and fix them? Start with `bun test` and work from there.',
119
119
  'Refactor auth middleware':
120
120
  `The auth middleware in lib/http-handler.js has grown into a monster. Please split it into single-responsibility pieces.`,
121
121
  'Add dark mode toggle':
@@ -123,7 +123,7 @@ function synthUser(title) {
123
123
  'Write migration for user schema':
124
124
  `Add last_login_at and a preferences JSON column to the users table. Include a backfill for existing rows.`,
125
125
  'Debug WebSocket reconnect loop':
126
- `Clients are getting stuck in a reconnect loop the network tab shows two sockets opening before one closes. Can you trace it in ws-machine and fix?`,
126
+ `Clients are getting stuck in a reconnect loop - the network tab shows two sockets opening before one closes. Can you trace it in ws-machine and fix?`,
127
127
  'Generate API docs from handlers':
128
128
  `We need API docs. Generate them from the route handler declarations and dump to docs/api.md.`,
129
129
  };
@@ -171,7 +171,7 @@ function build() {
171
171
 
172
172
  const db = openDb(OUT_DB);
173
173
 
174
- // Use the real schema pipeline same as database.js so fixture DB always matches production.
174
+ // Use the real schema pipeline - same as database.js - so fixture DB always matches production.
175
175
  initSchema(db);
176
176
  migrateFromJson(db, path.join(OUT_DIR, 'nonexistent.json'));
177
177
  migrateToACP(db);
@@ -61,7 +61,7 @@ console.error(`[seed] inserting conversation ${convId} with ${turns} turns, ${ch
61
61
  db.run(
62
62
  `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, agentType, workingDirectory, model)
63
63
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
64
- [convId, 'cli-claude', `Profiling Seed ${turns} turns`, startTime, now, 'active', 'claude', '/home/user', 'claude-opus-4-6']
64
+ [convId, 'cli-claude', `Profiling Seed - ${turns} turns`, startTime, now, 'active', 'claude', '/home/user', 'claude-opus-4-6']
65
65
  );
66
66
 
67
67
  const insertMsg = db.prepare(
@@ -155,5 +155,5 @@ for (let i = 0; i < turns; i += BATCH) {
155
155
 
156
156
  db.close();
157
157
  process.stderr.write('\n');
158
- console.error(`[seed] complete ${totalChunks} total chunks for conv ${convId}`);
158
+ console.error(`[seed] complete - ${totalChunks} total chunks for conv ${convId}`);
159
159
  console.log(convId);
@@ -1,5 +1,5 @@
1
1
  // Smoke-test the WS chat protocol added in this session.
2
- // Boots no server assumes one is already running at ws://localhost:$PORT/sync.
2
+ // Boots no server - assumes one is already running at ws://localhost:$PORT/sync.
3
3
  //
4
4
  // Usage: PORT=3990 node scripts/smoke-ws-chat.mjs
5
5
  //
@@ -43,7 +43,7 @@ ws.on('message', async (data) => {
43
43
  console.log('sync_connected, clientId =', m.clientId);
44
44
  try {
45
45
  const r1 = await call('agents.list');
46
- console.log('agents.list OK, count =', r1.agents.length, ' first =', r1.agents[0]?.id);
46
+ console.log('agents.list OK, count =', r1.agents.length, '- first =', r1.agents[0]?.id);
47
47
  const r2 = await call('conversation.subscribe', { sessionId: 'smoke-test-sid' });
48
48
  console.log('conversation.subscribe OK =', r2);
49
49
  console.log('PASS');
package/server.js CHANGED
@@ -179,7 +179,7 @@ server.on('error', (err) => {
179
179
  if (err.code === 'EADDRINUSE') {
180
180
  _portRetries++;
181
181
  if (_portRetries > MAX_PORT_RETRIES) {
182
- // Bail instead of retrying forever an unbounded retry loop leaks a
182
+ // Bail instead of retrying forever - an unbounded retry loop leaks a
183
183
  // live process that holds no useful port and accumulates on each launch.
184
184
  console.error(`Port ${PORT} still in use after ${MAX_PORT_RETRIES} retries; exiting. Free the port or set PORT to a different value.`);
185
185
  process.exit(1);
@@ -4,11 +4,13 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>agentgui</title>
7
- <meta name="description" content="agentgui multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
7
+ <meta name="description" content="agentgui - multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
8
8
  <!-- The GUI lives in the anentrypoint-design kit and is loaded from unpkg at
9
9
  its latest published version, so agentgui always tracks the kit without a
10
10
  hand-maintained vendored copy. (This trades the prior offline guarantee for
11
- always-latest, per project decision; a network connection is required.) -->
11
+ always-latest, per project decision; a network connection is required.)
12
+ NOTE: re-vendoring 0.0.186+ locally (it adds the load-bearing AgentChat
13
+ component) would restore offline + predictability -- tracked in AGENTS.md. -->
12
14
  <link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
13
15
  <script type="importmap">
14
16
  { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
@@ -44,7 +46,7 @@
44
46
  #app { height: 100vh; height: 100dvh; }
45
47
  #app > * { height: 100%; }
46
48
 
47
- /* Themed thin scrollbars the native chrome scrollbar is chunky/light and
49
+ /* Themed thin scrollbars - the native chrome scrollbar is chunky/light and
48
50
  clashes with the dark theme on the history sidebar and settings column. */
49
51
  * {
50
52
  scrollbar-width: thin;
@@ -79,7 +81,7 @@
79
81
  }
80
82
  .skip-link:focus { left: 8px; top: 8px; outline: 2px solid #fff; outline-offset: 2px; }
81
83
 
82
- /* status connection dot a CSS-drawn disc (real UI affordance, not a text
84
+ /* status connection dot - a CSS-drawn disc (real UI affordance, not a text
83
85
  glyph). The .status-dot-disc child is always present; the parent's state
84
86
  class colours it, and only is-live pulses. */
85
87
  .status-dot { display: inline-flex; align-items: center; gap: .4em; white-space: nowrap; }
@@ -180,7 +182,7 @@
180
182
 
181
183
  /* Topbar nav: the DS renders the active tab as a large filled green pill that
182
184
  sits taller than the inactive text links and reads as misaligned. Make all
183
- tabs consistent equal padding, the active one a subtle tinted underline-pill
185
+ tabs consistent - equal padding, the active one a subtle tinted underline-pill
184
186
  rather than an oversized oval. */
185
187
  /* Prefix with .ds-247420 (the <html> class) to match the DS selector's
186
188
  specificity; source order then lets these win. */
@@ -249,7 +251,7 @@
249
251
  .agentgui-main-settings .settings-grid { margin-top: 0; }
250
252
 
251
253
  /* The DS Chat head computes its own zero-padded count ("00 msgs") and ignores
252
- our sub prop; it reads as a bug. Hide the DS sub streaming state shows via
254
+ our sub prop; it reads as a bug. Hide the DS sub - streaming state shows via
253
255
  the title and the busy banner. Also hide the DS's empty decorative head dot. */
254
256
  .chat-head .sub { display: none; }
255
257
  .chat-head .dot { display: none; }
@@ -11,10 +11,10 @@ const state = {
11
11
  health: { status: 'unknown' },
12
12
  tab: 'chat',
13
13
  agents: [],
14
- selectedAgent: localStorage.getItem('agentgui.agent') || '',
14
+ selectedAgent: lsGet('agentgui.agent') || '',
15
15
  agentModels: [],
16
- selectedModel: localStorage.getItem('agentgui.model') || '',
17
- chatCwd: localStorage.getItem('agentgui.cwd') || '',
16
+ selectedModel: lsGet('agentgui.model') || '',
17
+ chatCwd: lsGet('agentgui.cwd') || '',
18
18
  chat: { messages: [], busy: false, abort: null, draft: '', resumeSid: null },
19
19
  sessions: [],
20
20
  selectedSid: null,
@@ -75,13 +75,14 @@ function isNarrow() { return typeof window !== 'undefined' && window.innerWidth
75
75
  function truncate(str, mobileLen, desktopLen) {
76
76
  const s = String(str ?? '');
77
77
  const max = isNarrow() ? mobileLen : desktopLen;
78
- return s.length > max ? s.slice(0, max) + '' : s;
78
+ return s.length > max ? s.slice(0, max) + '...' : s;
79
79
  }
80
80
  function debounce(fn, ms) {
81
81
  let t;
82
82
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
83
83
  }
84
84
 
85
+ function lsGet(k) { try { return localStorage.getItem(k); } catch { return null; } }
85
86
  function lsSet(k, v) { try { localStorage.setItem(k, v); } catch {} }
86
87
  function lsRemove(k) { try { localStorage.removeItem(k); } catch {} }
87
88
 
@@ -132,10 +133,10 @@ function timeNow() {
132
133
 
133
134
  async function selectAgent(id) {
134
135
  // Re-selecting the same agent would needlessly refetch models and reset the
135
- // current model selection no-op early.
136
+ // current model selection - no-op early.
136
137
  if (id === state.selectedAgent && state.agentModels.length) return;
137
138
  state.selectedAgent = id;
138
- localStorage.setItem('agentgui.agent', id);
139
+ lsSet('agentgui.agent', id);
139
140
  state.agentModels = [];
140
141
  state.selectedModel = '';
141
142
  state.modelsLoading = true;
@@ -144,7 +145,7 @@ async function selectAgent(id) {
144
145
  if (state.selectedAgent !== id) return; // changed while loading
145
146
  state.modelsLoading = false;
146
147
  state.agentModels = models;
147
- const saved = localStorage.getItem('agentgui.model');
148
+ const saved = lsGet('agentgui.model');
148
149
  // Only restore a saved model that the NEW agent actually offers; otherwise
149
150
  // fall back to its first model (never carry a stale model from a prior agent).
150
151
  state.selectedModel = (saved && models.some(m => m.id === saved)) ? saved : (models[0]?.id || '');
@@ -153,7 +154,7 @@ async function selectAgent(id) {
153
154
 
154
155
  function selectModel(id) {
155
156
  state.selectedModel = id;
156
- localStorage.setItem('agentgui.model', id);
157
+ lsSet('agentgui.model', id);
157
158
  render();
158
159
  }
159
160
 
@@ -161,7 +162,7 @@ function agentById(id) { return state.agents.find(a => a.id === id); }
161
162
  function agentAvailable(id) { const a = agentById(id); return !a || a.available !== false; }
162
163
 
163
164
  // The four flagship orchestration targets surface first, then other available
164
- // agents, then npx-installable, then not-installed so the agents the GUI
165
+ // agents, then npx-installable, then not-installed - so the agents the GUI
165
166
  // exists to drive are reachable without scanning a flat 17-item list. This
166
167
  // ordering is agentgui's orchestration priority, so it stays in the host and is
167
168
  // passed pre-sorted to the (app-agnostic) AgentChat kit.
@@ -206,7 +207,7 @@ function navTo(tab) {
206
207
  const heading = region.querySelector('h1, h2');
207
208
  const target = heading || region;
208
209
  if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
209
- // Mark as programmatically focused so CSS can suppress the focus ring we
210
+ // Mark as programmatically focused so CSS can suppress the focus ring - we
210
211
  // move focus here for AT, but a visible green outline box around the heading
211
212
  // reads as an accidental border to sighted users.
212
213
  target.setAttribute('data-prog-focus', '');
@@ -277,7 +278,7 @@ function openLiveStream() {
277
278
  if (data.isError) sess.errors = (sess.errors || 0) + 1;
278
279
  } else {
279
280
  // Unknown session: a burst of events for a new session would trigger
280
- // a full session-list refetch per event debounce it into one.
281
+ // a full session-list refetch per event - debounce it into one.
281
282
  debouncedRefreshHistory();
282
283
  return;
283
284
  }
@@ -316,10 +317,10 @@ function view() {
316
317
  const dotLabel = state.tab === 'history'
317
318
  ? (state.live.error
318
319
  ? state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
319
- : (liveActive ? 'live · ' + state.live.eventCount : (state.live.connected ? 'live' : 'connecting')))
320
+ : (liveActive ? 'live · ' + state.live.eventCount : (state.live.connected ? 'live' : 'connecting...')))
320
321
  : (ok ? (state.health.ws === 'reconnecting' ? 'ws reconnecting' : 'connected') : 'offline');
321
322
  const dotLive = state.tab === 'history' ? (liveActive || state.live.connected) : ok;
322
- // The status dot is drawn entirely by CSS (.status-dot::before) a small
323
+ // The status dot is drawn entirely by CSS (.status-dot::before) - a small
323
324
  // colored disc, real product design, not a text glyph. State drives its colour
324
325
  // via the modifier class; the label carries only words so AT reads "live", and
325
326
  // there are no literal status-glyph characters in the DOM.
@@ -379,7 +380,7 @@ function view() {
379
380
  : 'min-height:0';
380
381
  const shortcutsHint = state.showShortcuts
381
382
  ? Alert({ key: 'sc', kind: 'info', title: 'Keyboard shortcuts',
382
- children: 'g then c/h/s chat/history/settings · n new chat · / focus search/composer · ? toggle this · Esc blur field' })
383
+ children: 'g then c/h/s - chat/history/settings · n - new chat · / - focus search/composer · ? - toggle this · Esc - blur field' })
383
384
  : null;
384
385
  const main = h('div', { id: 'agentgui-main', role: 'main', 'data-chat-scroll': '', class: 'agentgui-main agentgui-main-' + state.tab, style: mainStyle }, [shortcutsHint, ...mainContent()].filter(Boolean));
385
386
  // narrow drives the DS main-column class; the history sidebar itself collapses
@@ -436,8 +437,8 @@ function chatMain() {
436
437
  // than as a separate stacked Alert, so resume context reads as one block.
437
438
  banners.push(h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
438
439
  h('span', { key: 'rbtxt', class: 'lede' },
439
- 'resuming session ' + state.chat.resumeSid.slice(0, 8) + ' via --resume'
440
- + (state.chat.resumeNote ? ' ' + state.chat.resumeNote : '')),
440
+ 'resuming session ' + state.chat.resumeSid.slice(0, 8) + '... via --resume'
441
+ + (state.chat.resumeNote ? ' - ' + state.chat.resumeNote : '')),
441
442
  Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; state.chat.resumeNote = null; render(); }, children: 'clear' })));
442
443
  }
443
444
  if (state.selectedAgent && !agentAvailable(state.selectedAgent)) {
@@ -465,7 +466,7 @@ function chatMain() {
465
466
 
466
467
  const placeholder = !state.selectedAgent
467
468
  ? 'choose an agent first'
468
- : (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message');
469
+ : (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message...');
469
470
 
470
471
  // The reusable AgentChat kit owns the agent/model picker, cwd bar, transcript
471
472
  // (with AICat-style auto-scroll + thinking), and the caret-stable composer.
@@ -482,8 +483,8 @@ function chatMain() {
482
483
  busy: state.chat.busy,
483
484
  draft: state.chat.draft,
484
485
  status: state.chat.busy
485
- ? (state.health.ws === 'reconnecting' ? 'reconnecting' : 'streaming')
486
- : (state.modelsLoading ? 'loading models' : (state.chat.resumeSid ? 'resume' : 'ready')),
486
+ ? (state.health.ws === 'reconnecting' ? 'reconnecting...' : 'streaming...')
487
+ : (state.modelsLoading ? 'loading models...' : (state.chat.resumeSid ? 'resume' : 'ready')),
487
488
  agentName,
488
489
  placeholder,
489
490
  canSend: canSend(),
@@ -509,8 +510,8 @@ function chatMain() {
509
510
  onCwdSave: () => {
510
511
  const path = (state.cwdDraft ?? '').trim();
511
512
  // A relative cwd would resolve against the server process dir, not what
512
- // the user means require an absolute path (POSIX /…, UNC \\…, or
513
- // Windows drive X:\…). Blank is valid (server default).
513
+ // the user means - require an absolute path (POSIX /..., UNC \\..., or
514
+ // Windows drive X:\...). Blank is valid (server default).
514
515
  if (path && !/^([/\\]|[A-Za-z]:[/\\])/.test(path)) {
515
516
  state.cwdError = 'enter an absolute path (e.g. /home/you/proj or C:\\proj) or leave blank';
516
517
  render();
@@ -545,7 +546,7 @@ function newChat() {
545
546
  state.confirmingNewChat = false;
546
547
  state.chat.abort?.abort();
547
548
  state.chat = { messages: [], busy: false, abort: null, draft: '', resumeSid: null };
548
- try { localStorage.removeItem(CHAT_KEY); } catch {}
549
+ lsRemove(CHAT_KEY);
549
550
  render();
550
551
  }
551
552
 
@@ -560,13 +561,13 @@ const CHAT_KEY = 'agentgui.chat';
560
561
  function persistChat() {
561
562
  try {
562
563
  const msgs = state.chat.messages.map(m => ({ id: m.id, role: m.role, content: m.content, time: m.time, parts: m.parts }));
563
- if (!msgs.length) { localStorage.removeItem(CHAT_KEY); return; }
564
- localStorage.setItem(CHAT_KEY, JSON.stringify({ messages: msgs, resumeSid: state.chat.resumeSid, agent: state.selectedAgent, model: state.selectedModel }));
564
+ if (!msgs.length) { lsRemove(CHAT_KEY); return; }
565
+ lsSet(CHAT_KEY, JSON.stringify({ messages: msgs, resumeSid: state.chat.resumeSid, agent: state.selectedAgent, model: state.selectedModel }));
565
566
  } catch {}
566
567
  }
567
568
  function restoreChat() {
568
569
  try {
569
- const raw = localStorage.getItem(CHAT_KEY);
570
+ const raw = lsGet(CHAT_KEY);
570
571
  if (!raw) return;
571
572
  const saved = JSON.parse(raw);
572
573
  if (Array.isArray(saved.messages) && saved.messages.length) {
@@ -603,7 +604,7 @@ async function sendChat() {
603
604
  if (ev.type === 'text') { cur.content += ev.text; render(); scrollChatToBottom(); }
604
605
  else if (ev.type === 'tool') { cur.parts.push(toolSummary(ev.block)); render(); scrollChatToBottom(); }
605
606
  else if (ev.type === 'tool_result') { cur.parts.push('-> ' + toolResultSummary(ev.block)); render(); scrollChatToBottom(); }
606
- else if (ev.type === 'result') { /* terminal usage/summary block already reflected via text */ }
607
+ else if (ev.type === 'result') { /* terminal usage/summary block - already reflected via text */ }
607
608
  else if (ev.type === 'error') { cur.error = errText(ev.error); render(); }
608
609
  }
609
610
  } catch (e) {
@@ -624,7 +625,7 @@ function reconnectAlert() {
624
625
  key: 'liveerr',
625
626
  kind: 'error',
626
627
  title: 'Live stream disconnected',
627
- children: [h('span', { key: 'lemsg' }, state.live.error + ' '), Btn({ key: 'reco', onClick: openLiveStream, children: 'reconnect', title: 'Reconnect to history stream' })],
628
+ children: [h('span', { key: 'lemsg' }, state.live.error + ' - '), Btn({ key: 'reco', onClick: openLiveStream, children: 'reconnect', title: 'Reconnect to history stream' })],
628
629
  });
629
630
  }
630
631
 
@@ -635,7 +636,7 @@ function historyMain() {
635
636
  reconnectAlert(),
636
637
  PageHeader({
637
638
  title: 'history',
638
- lede: 'pick a session from the sidebar events stream live from ccsniff /v1/history.',
639
+ lede: 'pick a session from the sidebar - events stream live from ccsniff /v1/history.',
639
640
  }),
640
641
  h('div', { key: 'histempty', class: 'history-empty', role: 'status' },
641
642
  h('p', { key: 'gt', class: 'history-empty-title' },
@@ -643,7 +644,7 @@ function historyMain() {
643
644
  h('p', { key: 'gs', class: 'lede history-empty-sub' },
644
645
  count
645
646
  ? count + ' session' + (count === 1 ? '' : 's') + ' available · use the search box or press / to filter'
646
- : 'Start a chat or run a local coding agent its session will appear here live.')),
647
+ : 'Start a chat or run a local coding agent - its session will appear here live.')),
647
648
  ].filter(Boolean);
648
649
  }
649
650
 
@@ -667,9 +668,9 @@ function historyMain() {
667
668
  // doesn't spin forever.
668
669
  const body = state.eventsLoaded
669
670
  ? h('div', { key: 'noev', class: 'lede empty-state', role: 'status' },
670
- h('span', { key: 'noevtxt' }, 'no events in this session '),
671
+ h('span', { key: 'noevtxt' }, 'no events in this session - '),
671
672
  Btn({ key: 'reload', onClick: () => loadSession(state.selectedSid), children: 'reload' }))
672
- : h('div', { key: 'loading', class: 'lede empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ key: 'spin', size: 'sm' }), 'loading events');
673
+ : h('div', { key: 'loading', class: 'lede empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ key: 'spin', size: 'sm' }), 'loading events...');
673
674
  return [reconnectAlert(), head, actions, Panel({ title: 'events', children: body })].filter(Boolean);
674
675
  }
675
676
 
@@ -746,7 +747,7 @@ function resumeInChat(sess) {
746
747
  // Only claude-code supports --resume by sid; warn if we have to switch the
747
748
  // user's selected agent rather than silently discarding it.
748
749
  if (state.selectedAgent && state.selectedAgent !== 'claude-code') {
749
- state.chat.resumeNote = 'Switched to Claude Code only it supports resuming a session by id.';
750
+ state.chat.resumeNote = 'Switched to Claude Code - only it supports resuming a session by id.';
750
751
  } else {
751
752
  state.chat.resumeNote = null;
752
753
  }
@@ -843,13 +844,16 @@ function historySide() {
843
844
  children: [
844
845
  SearchInput({
845
846
  key: 'searchInput',
846
- placeholder: 'search event text…',
847
+ // The DS SearchInput reads `label` (not aria-label) for the accessible
848
+ // name, falling back to placeholder; pass label so AT announces it.
849
+ placeholder: 'Search event text across sessions',
850
+ label: 'Search event text across sessions',
847
851
  'aria-label': 'Search event text across sessions',
848
852
  value: state.searchQ,
849
853
  onInput: (v) => { state.searchQ = v; debouncedSearch(); },
850
854
  }),
851
855
  state.searchBusy
852
- ? h('div', { key: 'searchbusy', class: 'lede empty-state', role: 'status' }, Spinner({ key: 'ss', size: 'sm' }), 'searching')
856
+ ? h('div', { key: 'searchbusy', class: 'lede empty-state', role: 'status' }, Spinner({ key: 'ss', size: 'sm' }), 'searching...')
853
857
  : null,
854
858
  searching && state.searchHits.error
855
859
  ? Alert({ key: 'searcherr', kind: 'error', title: 'Search failed', children: state.searchHits.error })
@@ -883,7 +887,7 @@ function historySide() {
883
887
  // Search is server-capped at 60 hits; there is no deeper page, so tell
884
888
  // the user the result set is truncated rather than silently hiding it.
885
889
  searching && truncatedBy > 0
886
- ? h('p', { key: 'searchmore', class: 'lede empty-state' }, 'showing first 60 matches (' + truncatedBy + ' more refine your search)')
890
+ ? h('p', { key: 'searchmore', class: 'lede empty-state' }, 'showing first 60 matches (' + truncatedBy + ' more - refine your search)')
887
891
  : null,
888
892
  ],
889
893
  }),
@@ -935,7 +939,7 @@ function settingsMain() {
935
939
  return [
936
940
  PageHeader({
937
941
  title: 'settings',
938
- lede: 'point agentgui at any backend. blank = same-origin (ccsniff in-process). ?backend=… or the field below persists via localStorage.',
942
+ lede: 'point agentgui at any backend. blank = same-origin (ccsniff in-process). ?backend=... or the field below persists via localStorage.',
939
943
  }),
940
944
  h('div', { key: 'settings-grid', class: 'settings-grid' }, [
941
945
  Panel({
@@ -955,10 +959,10 @@ function settingsMain() {
955
959
  onInput: (v) => { state.backendDraft = v; render(); },
956
960
  }),
957
961
  !isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, 'Invalid URL format') : null,
958
- state.backendStatus === 'connecting' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, 'connecting') : null,
962
+ state.backendStatus === 'connecting' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, 'connecting...') : null,
959
963
  state.backendStatus === 'ok' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, 'connected') : null,
960
- state.backendStatus === 'failed' ? h('p', { key: 'bst', class: 'lede field-error', role: 'alert' }, 'connection failed check the URL') : null,
961
- state.confirmingBackend ? h('p', { key: 'bcw', class: 'lede field-error', role: 'alert' }, 'changing backend discards this browser\'s chat transcript press save again to confirm') : null,
964
+ state.backendStatus === 'failed' ? h('p', { key: 'bst', class: 'lede field-error', role: 'alert' }, 'connection failed - check the URL') : null,
965
+ state.confirmingBackend ? h('p', { key: 'bcw', class: 'lede field-error', role: 'alert' }, 'changing backend discards this browser\'s chat transcript - press save again to confirm') : null,
962
966
  healthSummary(),
963
967
  Btn({
964
968
  key: 'savebtn',
@@ -966,7 +970,7 @@ function settingsMain() {
966
970
  primary: true,
967
971
  disabled: !isValid || state.backendDraft === state.backend || state.backendStatus === 'connecting',
968
972
  onClick: (e) => { e.preventDefault(); saveBackend(); },
969
- children: state.backendStatus === 'connecting' ? 'connecting' : 'save + reconnect',
973
+ children: state.backendStatus === 'connecting' ? 'connecting...' : 'save + reconnect',
970
974
  title: isValid ? 'Save backend URL and reconnect' : 'Fix URL format first',
971
975
  }),
972
976
  ]),
@@ -994,7 +998,7 @@ function preferencesPanel() {
994
998
  children: [
995
999
  h('div', { key: 'ver', class: 'lede' }, 'server ' + (hh.version ? 'v' + hh.version : 'version unknown') + (window.__SERVER_VERSION ? ' · build ' + window.__SERVER_VERSION : '')),
996
1000
  h('div', { key: 'sc', class: 'lede', style: 'margin:.5em 0' },
997
- 'keyboard: g then c/h/s switch tabs · n new chat · / focus search/composer · ? toggle hint · Esc blur field'),
1001
+ 'keyboard: g then c/h/s - switch tabs · n - new chat · / - focus search/composer · ? - toggle hint · Esc - blur field'),
998
1002
  state.confirmingClearData
999
1003
  ? Alert({ key: 'cld', kind: 'warn', title: 'Clear all local data?',
1000
1004
  children: [
@@ -1096,7 +1100,7 @@ async function loadSession(sid) {
1096
1100
  ts: Date.now(),
1097
1101
  role: 'error',
1098
1102
  type: 'fetch',
1099
- text: 'Failed to load session: ' + e.message + ' retry via sidebar',
1103
+ text: 'Failed to load session: ' + e.message + ' - retry via sidebar',
1100
1104
  }];
1101
1105
  state.eventsLoaded = true;
1102
1106
  render();
@@ -88,8 +88,8 @@ const SYNC_PATH = '/sync';
88
88
  let _ws = null;
89
89
  let _wsReady = null; // Promise that resolves when ws is OPEN
90
90
  let _nextReqId = 1;
91
- const _pending = new Map(); // requestId { resolve, reject }
92
- const _sessionListeners = new Map(); // sessionId Set<(event)=>void>
91
+ const _pending = new Map(); // requestId -> { resolve, reject }
92
+ const _sessionListeners = new Map(); // sessionId -> Set<(event)=>void>
93
93
  const _statusListeners = new Set(); // fn(state) where state in 'open'|'closed'|'error'|'reconnecting'
94
94
  let _reconnectAttempts = 0;
95
95
  let _reconnectTimer = null;
@@ -172,7 +172,7 @@ function ensureWs(base) {
172
172
  else p.resolve(msg.d);
173
173
  return;
174
174
  }
175
- // Unsolicited broadcast route by sessionId to subscribers.
175
+ // Unsolicited broadcast - route by sessionId to subscribers.
176
176
  // Server may send a single event or a batch (array) per ws-optimizer.
177
177
  const items = Array.isArray(msg) ? msg : [msg];
178
178
  for (const item of items) {
@@ -231,9 +231,9 @@ export async function listAgentModels(base, agentId) {
231
231
  // ---------- Streaming chat (WS) ----------
232
232
  //
233
233
  // Yields events of shape:
234
- // { type: 'text', text: '...' } assistant text deltas
235
- // { type: 'tool', block: {...} } tool_use blocks
236
- // { type: 'result', block: {...} } terminal result block
234
+ // { type: 'text', text: '...' } - assistant text deltas
235
+ // { type: 'tool', block: {...} } - tool_use blocks
236
+ // { type: 'result', block: {...} } - terminal result block
237
237
  // { type: 'error', error: '...' }
238
238
  //
239
239
  // Caller signature kept compatible with the previous HTTP/SSE impl.
@@ -282,12 +282,12 @@ export async function* streamChat(base, { model, messages, signal, agentId, resu
282
282
  }
283
283
  });
284
284
 
285
- // If the websocket drops mid-stream, streaming_complete will never arrive
285
+ // If the websocket drops mid-stream, streaming_complete will never arrive -
286
286
  // surface an error and end the iterator instead of hanging forever.
287
287
  const onWs = (s) => { if ((s === 'closed' || s === 'error') && !done) { errored = errored || 'connection lost during stream'; finish(); } };
288
288
  const unsubWs = onWsStatus ? onWsStatus(onWs) : null;
289
289
 
290
- // Wire AbortSignal to chat.cancel and end the iterator immediately so the
290
+ // Wire AbortSignal to chat.cancel - and end the iterator immediately so the
291
291
  // caller's busy state clears even if the server never emits a final event.
292
292
  const onAbort = () => { wsCall(base, 'chat.cancel', { sessionId }).catch(() => {}); finish(); };
293
293
  if (signal) {
package/site/theme.mjs CHANGED
@@ -137,7 +137,7 @@ const renderHtml = ({ site, nav, page, clientScript }) => `<!DOCTYPE html>
137
137
  <head>
138
138
  <meta charset="UTF-8" />
139
139
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
140
- <title>${escapeHtml(page.title || site.title)}${site.tagline ? ' ' + escapeHtml(site.tagline) : ''}</title>
140
+ <title>${escapeHtml(page.title || site.title)}${site.tagline ? ' - ' + escapeHtml(site.tagline) : ''}</title>
141
141
  <meta name="description" content="${escapeHtml(page.description || site.description || site.tagline || site.title)}" />
142
142
  <script type="importmap">{"imports":{"anentrypoint-design":"${SDK_URL}"}}</script>
143
143
  <style>html,body{margin:0;padding:0}body{background:var(--app-bg,#FBF6EB);color:var(--ink,#1F1B16);font-family:var(--ff-ui,'Nunito',system-ui,sans-serif)}</style>
package/test.js CHANGED
@@ -3,7 +3,6 @@ import { createRequire } from 'module';
3
3
  import { initSchema } from './database-schema.js';
4
4
  import { migrateConversationColumns } from './database-migrations.js';
5
5
  import { migrateACPSchema } from './database-migrations-acp.js';
6
- import { createQueries } from './lib/db-queries.js';
7
6
  import { encode, decode } from './lib/codec.js';
8
7
  import { WsRouter } from './lib/ws-protocol.js';
9
8
  import { WSOptimizer } from './lib/ws-optimizer.js';
@@ -13,8 +12,16 @@ import { maskKey, buildSystemPrompt } from './lib/provider-config.js';
13
12
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
14
13
  import { createACPProtocolHandler } from './lib/acp-protocol.js';
15
14
  import { sendJSON, compressAndSend, acceptsEncoding } from './lib/http-utils.js';
16
- import { JsonlParser } from './lib/jsonl-parser.js';
17
15
  const require = createRequire(import.meta.url);
16
+ // lib/jsonl-parser.js was also removed in the pivot (ccsniff owns JSONL parsing
17
+ // now). Load it dynamically so its test skips rather than crashing the suite.
18
+ let JsonlParser = null;
19
+ try { ({ JsonlParser } = await import('./lib/jsonl-parser.js')); } catch { JsonlParser = null; }
20
+ // lib/db-queries.js was removed in the 2026-05-19 single-surface pivot (history
21
+ // now flows through ccsniff, not the local query layer). Import it dynamically
22
+ // so its absence skips only the db-queries tests instead of crashing the suite.
23
+ let createQueries = null;
24
+ try { ({ createQueries } = await import('./lib/db-queries.js')); } catch { createQueries = null; }
18
25
  let Database, dbAvailable = false;
19
26
  try {
20
27
  try { Database = (await import('bun:sqlite')).default; }
@@ -22,11 +29,14 @@ try {
22
29
  new Database(':memory:');
23
30
  dbAvailable = true;
24
31
  } catch { dbAvailable = false; }
25
- let passed = 0, failed = 0;
32
+ let passed = 0, failed = 0, skipped = 0;
26
33
  const ok = (name, fn) => Promise.resolve().then(fn).then(
27
- () => { console.log(`ok ${name}`); passed++; },
28
- (err) => { console.error(`FAIL ${name}: ${err.message}`); failed++; });
29
- const okDb = (name, fn) => dbAvailable ? ok(name, fn) : (console.log(`skip (no sqlite) ${name}`), passed++, Promise.resolve());
34
+ () => { console.log(`ok - ${name}`); passed++; },
35
+ (err) => { console.error(`FAIL - ${name}: ${err.message}`); failed++; });
36
+ // Skip a test when sqlite is unavailable OR when it needs the removed db-queries layer.
37
+ const okDb = (name, fn) => (dbAvailable && createQueries)
38
+ ? ok(name, fn)
39
+ : (console.log(`skip (${dbAvailable ? 'no db-queries' : 'no sqlite'}) - ${name}`), skipped++, Promise.resolve());
30
40
  function inMemDb() {
31
41
  const db = new Database(':memory:');
32
42
  if (db.pragma) db.pragma('foreign_keys = ON'); else db.run('PRAGMA foreign_keys = ON');
@@ -43,11 +53,17 @@ function mockRes() {
43
53
  return r;
44
54
  }
45
55
  const run = async () => {
46
- await ok('codec: roundtrip + binary', () => {
56
+ await ok('codec: json roundtrip + wire-byte decode', () => {
57
+ // The codec is a plain-JSON text codec (encode = JSON.stringify); it does not
58
+ // preserve binary payloads (a Buffer is not JSON-native), so we assert the
59
+ // JSON-value contract plus decode()'s ability to read a Uint8Array wire frame.
47
60
  assert.deepEqual(decode(encode({ a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } })), { a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } });
48
- assert.deepEqual(Array.from(decode(encode({ bin: Buffer.from([1, 2, 3, 4]) })).bin), [1, 2, 3, 4]);
61
+ // decode() must accept an incoming Uint8Array/Buffer wire frame and parse it.
62
+ const wire = new TextEncoder().encode(encode({ ok: true, n: 5 }));
63
+ assert.deepEqual(decode(wire), { ok: true, n: 5 });
64
+ assert.deepEqual(decode(Buffer.from(encode({ z: 'y' }))), { z: 'y' });
49
65
  });
50
- await okDb('db: init schema creates conversations table', () => {
66
+ await (dbAvailable ? ok : (n) => (console.log(`skip (no sqlite) - ${n}`), skipped++, Promise.resolve()))('db: init schema creates conversations table', () => {
51
67
  assert.ok(inMemDb().db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'").get());
52
68
  });
53
69
  await okDb('db-queries: createConversation round-trip', () => {
@@ -157,7 +173,7 @@ await ok('http-utils: sendJSON + compressAndSend size threshold', () => {
157
173
  const big = mockRes(); compressAndSend(req, big, 200, 'text/plain', 'x'.repeat(2000)); assert.equal(big.headers['Content-Encoding'], 'gzip');
158
174
  const ng = mockRes(); compressAndSend({ headers: {} }, ng, 200, 'text/html', 'y'.repeat(2000)); assert.equal(ng.headers['Cache-Control'], 'no-store');
159
175
  });
160
- await ok('jsonl-parser: register + remove + clear', () => {
176
+ await (JsonlParser ? ok : (n) => (console.log(`skip (no jsonl-parser) - ${n}`), skipped++, Promise.resolve()))('jsonl-parser: register + remove + clear', () => {
161
177
  const p = new JsonlParser({ broadcastSync: () => {}, queries: { getConversationByClaudeSessionId: () => null } });
162
178
  p.registerSession('s1', 'c1', 'd1'); assert.equal(p._convMap.get('s1'), 'c1');
163
179
  p.removeSid('s1'); assert.equal(p._convMap.has('s1'), false);
@@ -195,6 +211,6 @@ await okDb('conv-routes+thread-routes+auth-config+util-routes', async () => {
195
211
  const vr = mr(); await uR['GET /api/version'](null, vr); assert.equal(vr.statusCode, 200);
196
212
  const fr = mr(); await uR['POST /api/folders']({ _b: { path: process.cwd() } }, fr); assert.equal(fr.statusCode, 200); assert.ok(Array.isArray(JSON.parse(fr.body).folders));
197
213
  });
198
- console.log(`\n${passed} passed, ${failed} failed`);
214
+ console.log(`\n${passed} passed, ${failed} failed, ${skipped} skipped`);
199
215
  process.exit(failed === 0 ? 0 : 1);
200
216
  }; run();