agentgui 1.0.985 → 1.0.987
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 +4 -0
- package/TEST-COVERAGE.md +393 -0
- package/database-schema.js +0 -2
- package/lib/asset-server.js +7 -1
- package/lib/claude-runner-run.js +0 -1
- package/lib/http-handler.js +126 -28
- package/lib/plugins/acp-plugin.js +27 -6
- package/lib/plugins/files-plugin.js +43 -12
- package/lib/plugins/workflow-plugin.js +20 -2
- package/lib/ws-handlers-util.js +7 -0
- package/package.json +2 -1
- package/server.js +0 -2
- package/site/app/index.html +0 -1
- package/site/app/js/app.js +174 -147
- package/site/app/js/backend.js +52 -6
- package/site/app/vendor/anentrypoint-design/247420.css +19 -0
- package/site/app/vendor/anentrypoint-design/247420.js +14 -14
- package/test-integration.js +491 -0
- package/test.js +218 -0
- package/acp-queries.js +0 -182
- package/lib/routes-agents.js +0 -108
- package/lib/routes-registry.js +0 -6
package/site/app/js/backend.js
CHANGED
|
@@ -13,7 +13,17 @@ function authToken() {
|
|
|
13
13
|
try { return (typeof window !== 'undefined' && window.__WS_TOKEN) || ''; } catch { return ''; }
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Set when a 401 is received mid-session so the app can show a reconnect banner.
|
|
17
|
+
let _sessionExpired = false;
|
|
18
|
+
const _sessionExpiredListeners = new Set();
|
|
19
|
+
export function onSessionExpired(fn) { _sessionExpiredListeners.add(fn); return () => _sessionExpiredListeners.delete(fn); }
|
|
20
|
+
function emitSessionExpired() {
|
|
21
|
+
if (_sessionExpired) return; // emit only once per session
|
|
22
|
+
_sessionExpired = true;
|
|
23
|
+
for (const fn of _sessionExpiredListeners) { try { fn(); } catch {} }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function authedFetch(url, opts = {}) {
|
|
17
27
|
// Thread the agentgui token via the ?token= query param (exactly like the WS,
|
|
18
28
|
// EventSource, and image/download URLs) rather than an `Authorization: Bearer`
|
|
19
29
|
// header. A Bearer header OVERWRITES the browser's cached HTTP Basic Auth
|
|
@@ -23,7 +33,9 @@ function authedFetch(url, opts = {}) {
|
|
|
23
33
|
// /health, /v1/history/*, and /api/* all 401. The query param coexists with
|
|
24
34
|
// Basic auth; agentgui accepts ?token= on every HTTP route. credentials are
|
|
25
35
|
// kept same-origin so the agentgui_token cookie also flows.
|
|
26
|
-
|
|
36
|
+
const r = await fetch(withToken(url), { credentials: 'same-origin', ...opts });
|
|
37
|
+
if (r.status === 401) emitSessionExpired();
|
|
38
|
+
return r;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
function withToken(url) {
|
|
@@ -38,7 +50,20 @@ function lsSet(k, v) { try { localStorage.setItem(k, v); } catch {} }
|
|
|
38
50
|
export function getBackend() {
|
|
39
51
|
const u = new URL(location.href);
|
|
40
52
|
const fromQs = u.searchParams.get('backend');
|
|
41
|
-
if (fromQs) {
|
|
53
|
+
if (fromQs) {
|
|
54
|
+
// Only accept same-origin backends from the query string. A cross-origin
|
|
55
|
+
// ?backend= would receive the ?token= auth credential on every request,
|
|
56
|
+
// which is a credential-theft vector. Reject silently and fall through to
|
|
57
|
+
// the stored/default value.
|
|
58
|
+
let accepted = false;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = new URL(fromQs, location.href);
|
|
61
|
+
const sameOrigin = parsed.origin === location.origin;
|
|
62
|
+
const isLocalDev = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
|
63
|
+
accepted = sameOrigin || isLocalDev;
|
|
64
|
+
} catch {}
|
|
65
|
+
if (accepted) { lsSet(KEY, fromQs); return fromQs; }
|
|
66
|
+
}
|
|
42
67
|
// Same-origin default: honour the server-injected router prefix so HTTP
|
|
43
68
|
// calls (/health, /v1/history/*, /api/*) stay under the proxied path
|
|
44
69
|
// (e.g. /gm) instead of hitting the site root, where a path-routing proxy
|
|
@@ -245,6 +270,8 @@ function ensureWs(base) {
|
|
|
245
270
|
for (const sid of _sessionListeners.keys()) {
|
|
246
271
|
try { _ws.send(encode({ m: 'conversation.subscribe', r: _nextReqId++, p: { sessionId: sid } })); } catch {}
|
|
247
272
|
}
|
|
273
|
+
// Reload the agent list after reconnect so the picker reflects reality.
|
|
274
|
+
emitStatus('reloadAgents');
|
|
248
275
|
resolve(_ws);
|
|
249
276
|
});
|
|
250
277
|
// The error Event has no .message; reject with a real Error so callers log
|
|
@@ -252,7 +279,7 @@ function ensureWs(base) {
|
|
|
252
279
|
_ws.addEventListener('error', () => { emitStatus('error'); reject(new Error('WebSocket connection failed')); });
|
|
253
280
|
_ws.addEventListener('close', () => {
|
|
254
281
|
emitStatus('closed');
|
|
255
|
-
for (const [, p] of _pending) p.reject(new Error('
|
|
282
|
+
for (const [, p] of _pending) p.reject(new Error('Connection lost - please reconnect'));
|
|
256
283
|
_pending.clear();
|
|
257
284
|
_ws = null;
|
|
258
285
|
_wsReady = null;
|
|
@@ -319,7 +346,20 @@ export async function listActiveChats(base) {
|
|
|
319
346
|
}
|
|
320
347
|
|
|
321
348
|
export async function cancelChat(base, sessionId) {
|
|
322
|
-
|
|
349
|
+
try {
|
|
350
|
+
return await wsCall(base, 'chat.cancel', { sessionId });
|
|
351
|
+
} catch (e) {
|
|
352
|
+
// Surface a distinct error type so the app can show a toast instead of
|
|
353
|
+
// silently swallowing the failure (user clicked stop but nothing happened).
|
|
354
|
+
const err = new Error(e.message || 'stop request not delivered');
|
|
355
|
+
err.cancelFailed = true;
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function restartAcpAgent(base, agentId) {
|
|
361
|
+
try { return await wsCall(base, 'acp.restart', { id: agentId }); }
|
|
362
|
+
catch { return { ok: false }; }
|
|
323
363
|
}
|
|
324
364
|
|
|
325
365
|
export async function listAgentModels(base, agentId) {
|
|
@@ -452,7 +492,13 @@ export async function* streamChat(base, { model, messages, signal, agentId, resu
|
|
|
452
492
|
}
|
|
453
493
|
yield queue.shift();
|
|
454
494
|
}
|
|
455
|
-
if (errored)
|
|
495
|
+
if (errored) {
|
|
496
|
+
// Sanitize raw server strings: strip internal stack traces / JSON blobs
|
|
497
|
+
// and map common cases to plain human copy.
|
|
498
|
+
let displayError = (typeof errored === 'string' ? errored : 'streaming error').trim();
|
|
499
|
+
if (/^\{/.test(displayError) || displayError.length > 300) displayError = 'An error occurred while streaming the response.';
|
|
500
|
+
yield { type: 'error', error: displayError };
|
|
501
|
+
}
|
|
456
502
|
} finally {
|
|
457
503
|
unsub();
|
|
458
504
|
if (graceTimer) { clearTimeout(graceTimer); graceTimer = null; }
|
|
@@ -3890,6 +3890,22 @@
|
|
|
3890
3890
|
.ds-247420 .ds-file-act:disabled { opacity: .45; cursor: default; }
|
|
3891
3891
|
.ds-247420 .ds-file-act:disabled:hover { background: transparent; color: var(--fg-3); }
|
|
3892
3892
|
|
|
3893
|
+
/* ============================================================
|
|
3894
|
+
Print — linearize WorkspaceShell so main content prints in full.
|
|
3895
|
+
The multi-column CSS grid with overflow:auto regions clips at
|
|
3896
|
+
the on-screen height in print/PDF; drop all chrome and let
|
|
3897
|
+
.ws-main expand to its natural document height.
|
|
3898
|
+
============================================================ */
|
|
3899
|
+
@media print {
|
|
3900
|
+
.ds-247420 .ws-shell { display: block; }
|
|
3901
|
+
.ds-247420 .ws-rail,
|
|
3902
|
+
.ds-247420 .ws-sessions,
|
|
3903
|
+
.ds-247420 .ws-pane,
|
|
3904
|
+
.ds-247420 .ws-crumb,
|
|
3905
|
+
.ds-247420 .app-status { display: none; }
|
|
3906
|
+
.ds-247420 .ws-main { overflow: visible; height: auto; }
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3893
3909
|
/* community.css */
|
|
3894
3910
|
/* ============================================================
|
|
3895
3911
|
247420 design system — community surface (Discord-style chat)
|
|
@@ -5982,6 +5998,7 @@
|
|
|
5982
5998
|
width: auto; border-radius: var(--r-1);
|
|
5983
5999
|
}
|
|
5984
6000
|
.ds-247420 .ds-dash-stream { font-size: var(--fs-tiny); color: var(--fg-3); }
|
|
6001
|
+
.ds-247420 .ds-dash-stream.is-connected { color: var(--accent); }
|
|
5985
6002
|
.ds-247420 .ds-dash-stream.is-lost { color: var(--flame); }
|
|
5986
6003
|
.ds-247420 .ds-dash-stream.is-connecting { color: var(--amber); }
|
|
5987
6004
|
.ds-247420 .ds-dash-header .spread { flex: 1; }
|
|
@@ -6010,6 +6027,8 @@
|
|
|
6010
6027
|
agent is flagged in a dense grid, not merely faded near-invisibly. The word
|
|
6011
6028
|
'idle' co-carries state, so this stays colour-blind safe. */
|
|
6012
6029
|
.ds-247420 .ds-dash-card.is-stale { box-shadow: inset 2px 0 0 var(--stale); }
|
|
6030
|
+
/* Active identity always wins over stale amber when both classes are present. */
|
|
6031
|
+
.ds-247420 .ds-dash-card.is-active.is-stale { box-shadow: inset 2px 0 0 var(--accent); }
|
|
6013
6032
|
|
|
6014
6033
|
/* --- H3: dashboard live disc pulses; stale/error do not (handled by H1). --- */
|
|
6015
6034
|
|