agentgui 1.0.947 → 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 +14 -10
- package/docs/demo.html +27 -27
- package/lib/codec.js +1 -1
- package/lib/http-handler.js +2 -2
- package/lib/process-message.js +1 -1
- package/lib/stream-event-handler.js +3 -3
- package/lib/terminal.js +1 -1
- package/lib/ws-setup.js +1 -1
- package/package.json +1 -1
- package/scripts/build-rippleui.mjs +5 -5
- package/scripts/capture-screenshots.mjs +3 -3
- package/scripts/copy-vendor.js +1 -1
- package/scripts/harvest-fixtures.mjs +7 -7
- package/scripts/seed-large-conversation.js +2 -2
- package/scripts/smoke-ws-chat.mjs +2 -2
- package/server.js +1 -1
- package/site/app/index.html +8 -6
- package/site/app/js/app.js +46 -42
- package/site/app/js/backend.js +8 -8
- package/site/theme.mjs +1 -1
- package/test.js +27 -11
package/AGENTS.md
CHANGED
|
@@ -60,13 +60,7 @@ 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`
|
|
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
|
|
|
@@ -97,9 +91,7 @@ The function previously returned "Model: X." when agentId was 'claude-code' and
|
|
|
97
91
|
|
|
98
92
|
## WebSocket Sync Endpoint Testing
|
|
99
93
|
|
|
100
|
-
|
|
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.
|
|
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").
|
|
103
95
|
|
|
104
96
|
## better-sqlite3 & Node v24 Startup
|
|
105
97
|
|
|
@@ -154,3 +146,15 @@ GUI source keeps typographic product characters — the middot separator `·`, t
|
|
|
154
146
|
## DS CSS cascade — overriding component styles (2026-05-28)
|
|
155
147
|
|
|
156
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
|
|
7
|
-
<meta name="description" content="See what AgentGUI looks like on your machine
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
<!--
|
|
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
|
-
<!--
|
|
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
|
-
<!--
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
<!--
|
|
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 & steer</div>
|
|
787
|
-
<div class="feat-desc">Queue messages while the agent is running. Steer mid-session with a single click
|
|
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
|
-
<!--
|
|
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/
|
|
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
|
|
808
|
+
<span>- agentgui</span>
|
|
809
809
|
<span class="spread"></span>
|
|
810
810
|
<a href="https://github.com/AnEntrypoint/agentgui">github</a>
|
|
811
|
-
<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
|
|
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.
|
package/lib/http-handler.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/lib/process-message.js
CHANGED
|
@@ -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
|
|
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
|
|
8
|
-
* 2. Detect inline rate-limit messages
|
|
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
|
|
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
|
|
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
|
|
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,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
|
|
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)
|
|
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
|
|
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()}
|
|
55
|
-
` * Generated by scripts/build-rippleui.mjs
|
|
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
|
|
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}
|
|
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
|
|
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({
|
package/scripts/copy-vendor.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
91
|
-
`and CORS
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, '
|
|
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
|
|
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);
|
package/site/app/index.html
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; }
|
package/site/app/js/app.js
CHANGED
|
@@ -11,10 +11,10 @@ const state = {
|
|
|
11
11
|
health: { status: 'unknown' },
|
|
12
12
|
tab: 'chat',
|
|
13
13
|
agents: [],
|
|
14
|
-
selectedAgent:
|
|
14
|
+
selectedAgent: lsGet('agentgui.agent') || '',
|
|
15
15
|
agentModels: [],
|
|
16
|
-
selectedModel:
|
|
17
|
-
chatCwd:
|
|
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) + '
|
|
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
|
|
136
|
+
// current model selection - no-op early.
|
|
136
137
|
if (id === state.selectedAgent && state.agentModels.length) return;
|
|
137
138
|
state.selectedAgent = id;
|
|
138
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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) + '
|
|
440
|
-
+ (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
|
|
486
|
-
: (state.modelsLoading ? 'loading models
|
|
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
|
|
513
|
-
// Windows drive X
|
|
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
|
-
|
|
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) {
|
|
564
|
-
|
|
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 =
|
|
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
|
|
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 + '
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
961
|
-
state.confirmingBackend ? h('p', { key: 'bcw', class: 'lede field-error', role: 'alert' }, 'changing backend discards this browser\'s chat transcript
|
|
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
|
|
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
|
|
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 + '
|
|
1103
|
+
text: 'Failed to load session: ' + e.message + ' - retry via sidebar',
|
|
1100
1104
|
}];
|
|
1101
1105
|
state.eventsLoaded = true;
|
|
1102
1106
|
render();
|
package/site/app/js/backend.js
CHANGED
|
@@ -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
|
|
92
|
-
const _sessionListeners = new Map(); // sessionId
|
|
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
|
|
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: '...' }
|
|
235
|
-
// { type: 'tool', block: {...} }
|
|
236
|
-
// { type: '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
|
|
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 ? '
|
|
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
|
|
28
|
-
(err) => { console.error(`FAIL
|
|
29
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
|
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();
|