agentgui 1.0.984 → 1.0.986

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.
@@ -13,7 +13,17 @@ function authToken() {
13
13
  try { return (typeof window !== 'undefined' && window.__WS_TOKEN) || ''; } catch { return ''; }
14
14
  }
15
15
 
16
- function authedFetch(url, opts = {}) {
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
- return fetch(withToken(url), { credentials: 'same-origin', ...opts });
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) { lsSet(KEY, fromQs); return 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('ws closed'));
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
- return wsCall(base, 'chat.cancel', { sessionId });
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) yield { type: 'error', error: 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