agentgui 1.0.1000 → 1.0.1001
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/lib/http-handler.js
CHANGED
|
@@ -362,6 +362,10 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
362
362
|
status: 'ok', version: PKG_VERSION, uptime: process.uptime(), agents: discoveredAgents.length,
|
|
363
363
|
activeExecutions: activeExecutions.size, wsClients: getWss()?.clients?.size ?? 0,
|
|
364
364
|
acp: getACPStatus(), db: dbStatus, queueSizes,
|
|
365
|
+
// Surfaces the console-only CORS_ORIGIN=*+no-PASSWORD warning (above)
|
|
366
|
+
// as a fact the GUI settings panel can render a persistent banner
|
|
367
|
+
// from - an operator not tailing logs otherwise never sees the risk.
|
|
368
|
+
corsWildcardOpen: process.env.CORS_ORIGIN === '*' && !process.env.PASSWORD,
|
|
365
369
|
};
|
|
366
370
|
// Host-internal facts (memory, filesystem paths) are exposed only to an
|
|
367
371
|
// authenticated caller, never on the unauthenticated health-probe bypass.
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -345,6 +345,10 @@ function activeSig(list) {
|
|
|
345
345
|
return (Array.isArray(list) ? list : []).map(a => a.sessionId + ':' + (a.claudeSessionId || '') + ':' + (a.model || '') + ':' + (a.startedAt || '')).sort().join('|');
|
|
346
346
|
}
|
|
347
347
|
async function refreshActive() {
|
|
348
|
+
// A backgrounded tab still holds its interval timer, but polling every ~3.3s
|
|
349
|
+
// while hidden is needless server load across many left-open tabs - skip the
|
|
350
|
+
// network call (and let the next visible tick catch up).
|
|
351
|
+
if (typeof document !== 'undefined' && document.hidden) return;
|
|
348
352
|
let next;
|
|
349
353
|
try { next = await B.listActiveChats(state.backend); } catch { return; }
|
|
350
354
|
const changed = activeSig(next) !== activeSig(state.active);
|
|
@@ -366,6 +370,10 @@ function startActivePolling() {
|
|
|
366
370
|
refreshActive();
|
|
367
371
|
// Small jitter so many tabs don't hit the server in lockstep.
|
|
368
372
|
state.activeTimer = setInterval(refreshActive, 3000 + Math.floor(Math.random() * 600));
|
|
373
|
+
if (!state._visActiveBound && typeof document !== 'undefined') {
|
|
374
|
+
state._visActiveBound = true;
|
|
375
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refreshActive(); });
|
|
376
|
+
}
|
|
369
377
|
}
|
|
370
378
|
function stopActivePolling() {
|
|
371
379
|
if (state.activeTimer) { clearInterval(state.activeTimer); state.activeTimer = null; }
|
|
@@ -606,7 +614,7 @@ function view() {
|
|
|
606
614
|
// main content, and an optional right context pane. The rail replaces the old
|
|
607
615
|
// Topbar tabs; it collapses to icon-only and, on mobile, behind its own toggle.
|
|
608
616
|
const rail = workspaceRail();
|
|
609
|
-
const sessions = (state.tab === 'chat' || state.tab === 'history') ? sessionsColumn() : null;
|
|
617
|
+
const sessions = (state.tab === 'chat' || state.tab === 'history' || state.tab === 'live') ? sessionsColumn() : null;
|
|
610
618
|
// Right context pane. On chat it carries agent/model/cwd + the live running-tool
|
|
611
619
|
// count + last-turn usage. On files it hosts the inline file preview (split view).
|
|
612
620
|
// On other tabs it is null but the shell keeps the column TRACK (stableFrame) so
|
|
@@ -623,6 +631,13 @@ function view() {
|
|
|
623
631
|
turns: state.chat.messages.filter(m => m.role === 'user').length,
|
|
624
632
|
cost: state.chat.totalCost || null,
|
|
625
633
|
},
|
|
634
|
+
recentFiles: recentSessionFiles(),
|
|
635
|
+
onOpenFile: (path) => {
|
|
636
|
+
const base = pathBasename(path);
|
|
637
|
+
const dir = base === path ? '' : path.slice(0, path.length - base.length - 1);
|
|
638
|
+
navTo('files');
|
|
639
|
+
loadDir(dir);
|
|
640
|
+
},
|
|
626
641
|
// One affordance per action: the cwd row opens the SAME inline editor as
|
|
627
642
|
// the composer context line (it validates via /api/stat) instead of
|
|
628
643
|
// navigating away to the Files tab.
|
|
@@ -638,7 +653,11 @@ function view() {
|
|
|
638
653
|
|
|
639
654
|
// The left workspace rail: brand, New chat action, and the primary view nav.
|
|
640
655
|
function workspaceRail() {
|
|
641
|
-
|
|
656
|
+
// Same owned+external session set the Live dashboard renders, so the nav
|
|
657
|
+
// badge count and error tone never diverge from what the dashboard shows.
|
|
658
|
+
const liveSessions = computeLiveSessions();
|
|
659
|
+
const liveCount = liveSessions.length;
|
|
660
|
+
const liveHasError = liveSessions.some((s) => s.status === 'error');
|
|
642
661
|
// Show a live pulse on the chat rail item when a stream is in progress but the
|
|
643
662
|
// user has navigated to a different tab - so the background stream stays visible.
|
|
644
663
|
const chatStreaming = state.chat.busy && state.tab !== 'chat';
|
|
@@ -646,7 +665,7 @@ function workspaceRail() {
|
|
|
646
665
|
{ key: 'chat', label: 'Chat', icon: 'forum', active: state.tab === 'chat', count: chatStreaming ? 1 : null, onClick: () => navTo('chat') },
|
|
647
666
|
{ key: 'history', label: 'History', icon: 'thread', active: state.tab === 'history', onClick: () => navTo('history') },
|
|
648
667
|
{ key: 'files', label: 'Files', icon: 'folder', active: state.tab === 'files', onClick: () => navTo('files') },
|
|
649
|
-
{ key: 'live', label: 'Live', icon: 'activity', active: state.tab === 'live', count: liveCount || null, onClick: () => navTo('live') },
|
|
668
|
+
{ key: 'live', label: 'Live', icon: 'activity', active: state.tab === 'live', count: liveCount || null, rail: liveHasError ? 'flame' : undefined, onClick: () => navTo('live') },
|
|
650
669
|
{ key: 'settings', label: 'Settings', icon: 'settings', active: state.tab === 'settings', onClick: () => navTo('settings') },
|
|
651
670
|
];
|
|
652
671
|
return WorkspaceRail({
|
|
@@ -748,6 +767,10 @@ function sessionsColumn() {
|
|
|
748
767
|
const sessionsView = visibleSessions();
|
|
749
768
|
const sliced = sessionsView.slice(0, state.sessionsLimit);
|
|
750
769
|
const runningSids = new Set((Array.isArray(state.active) ? state.active : []).map(a => a.claudeSessionId || a.sessionId));
|
|
770
|
+
// Same status computation the Live tab uses (running/stale/error), keyed by
|
|
771
|
+
// realSid, so a session pinned to the rail's "Running" group shows whether it
|
|
772
|
+
// is actually stuck vs busy instead of just a plain live dot.
|
|
773
|
+
const liveStatusBySid = new Map(computeLiveSessions().map((s) => [s.realSid, s.status]));
|
|
751
774
|
const items = sliced.map((s) => {
|
|
752
775
|
const title = projectLabel(s.title) || projectLabel(s.project) || s.sid;
|
|
753
776
|
const project = projectLabel(s.project) || '';
|
|
@@ -760,6 +783,7 @@ function sessionsColumn() {
|
|
|
760
783
|
time: fmtRelTime(s.last),
|
|
761
784
|
agent: undefined,
|
|
762
785
|
running: runningSids.has(s.sid),
|
|
786
|
+
status: runningSids.has(s.sid) ? liveStatusBySid.get(s.sid) : undefined,
|
|
763
787
|
unread: false,
|
|
764
788
|
// Tool errors are NORMAL in agent sessions (a failed Bash call counts) -
|
|
765
789
|
// flame-on-any-error painted every real session red. Flame only when
|
|
@@ -1507,17 +1531,15 @@ const STALE_AFTER_MS = 45000;
|
|
|
1507
1531
|
const STATUS_RANK = { error: 0, stale: 1, running: 2 };
|
|
1508
1532
|
|
|
1509
1533
|
// --- live (multi-session dashboard) ---
|
|
1510
|
-
|
|
1511
|
-
|
|
1534
|
+
// Shared source of truth for "every live session" (owned chat.active rows plus
|
|
1535
|
+
// external SSE-observed sessions) - used by BOTH the Live tab dashboard and the
|
|
1536
|
+
// WorkspaceRail nav badge/tone, so the count and error signal never diverge
|
|
1537
|
+
// between the two surfaces (a prior cohesion gap: the rail counted owned-only).
|
|
1538
|
+
function computeLiveSessions() {
|
|
1512
1539
|
const bySid = state.sessionsBySid || new Map();
|
|
1513
1540
|
const tally = state.live.tally || new Map();
|
|
1514
1541
|
const now = Date.now();
|
|
1515
|
-
// Live-stream health: connected (recent event), connecting (opened, no event
|
|
1516
|
-
// yet), or offline (errored - one connection vocabulary across the GUI).
|
|
1517
|
-
const streamState = state.live.error ? 'offline' : (state.live.connected ? 'connected' : 'connecting');
|
|
1518
1542
|
const stoppingSet = state.live.stopping || new Set();
|
|
1519
|
-
// Clock-skew-corrected "now" in server-timestamp terms, so pure skew never
|
|
1520
|
-
// derives a session stale.
|
|
1521
1543
|
const nowS = now - (state.live.clockSkew || 0);
|
|
1522
1544
|
let sessions = (Array.isArray(state.active) ? state.active : []).map((r) => {
|
|
1523
1545
|
// The history index + SSE tally are keyed by claude's REAL session id, not
|
|
@@ -1627,6 +1649,16 @@ function liveMain() {
|
|
|
1627
1649
|
stopping: false,
|
|
1628
1650
|
});
|
|
1629
1651
|
}
|
|
1652
|
+
return sessions;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function liveMain() {
|
|
1656
|
+
const offline = state.health.status !== 'ok' && state.health.status !== 'unknown';
|
|
1657
|
+
// Live-stream health: connected (recent event), connecting (opened, no event
|
|
1658
|
+
// yet), or offline (errored - one connection vocabulary across the GUI).
|
|
1659
|
+
const streamState = state.live.error ? 'offline' : (state.live.connected ? 'connected' : 'connecting');
|
|
1660
|
+
const stoppingSet = state.live.stopping || new Set();
|
|
1661
|
+
let sessions = computeLiveSessions();
|
|
1630
1662
|
// W12: in-dir filter + errors-only toggle.
|
|
1631
1663
|
const lv = state.live;
|
|
1632
1664
|
if (lv.errorsOnly) sessions = sessions.filter(s => s.status === 'error');
|
|
@@ -1751,6 +1783,29 @@ function runningToolCount() {
|
|
|
1751
1783
|
return last.parts.filter(p => p && p.kind === 'tool' && p.status === 'running').length;
|
|
1752
1784
|
}
|
|
1753
1785
|
|
|
1786
|
+
// Files touched by Edit/Write/MultiEdit/Read tool calls in the current chat
|
|
1787
|
+
// session, most-recent first, deduped by path - feeds ContextPane's "recent
|
|
1788
|
+
// files" panel (Claude Desktop surfaces recently-touched files; this GUI had
|
|
1789
|
+
// no path to that fact at all before).
|
|
1790
|
+
const FILE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'Read', 'NotebookEdit']);
|
|
1791
|
+
function recentSessionFiles() {
|
|
1792
|
+
const msgs = Array.isArray(state.chat.messages) ? state.chat.messages : [];
|
|
1793
|
+
const seen = new Set();
|
|
1794
|
+
const out = [];
|
|
1795
|
+
for (let i = msgs.length - 1; i >= 0 && out.length < 5; i--) {
|
|
1796
|
+
const parts = Array.isArray(msgs[i].parts) ? msgs[i].parts : [];
|
|
1797
|
+
for (let j = parts.length - 1; j >= 0 && out.length < 5; j--) {
|
|
1798
|
+
const p = parts[j];
|
|
1799
|
+
if (!p || p.kind !== 'tool' || !FILE_TOOLS.has(p.name)) continue;
|
|
1800
|
+
const path = p.args && (p.args.file_path || p.args.path || p.args.notebook_path);
|
|
1801
|
+
if (!path || seen.has(path)) continue;
|
|
1802
|
+
seen.add(path);
|
|
1803
|
+
out.push({ path });
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return out;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1754
1809
|
function toolPart(block) {
|
|
1755
1810
|
const name = block.name || block.kind || 'tool';
|
|
1756
1811
|
const input = block.input || block.rawInput || {};
|
|
@@ -2262,7 +2317,7 @@ function chatFollowups() {
|
|
|
2262
2317
|
const fm = text.match(/([A-Za-z]:\\\S+|\/[\w./-]+\.\w+)/);
|
|
2263
2318
|
if (fm) {
|
|
2264
2319
|
const base = fm[1].split(/[/\\]/).filter(Boolean).pop();
|
|
2265
|
-
if (base) out.push('
|
|
2320
|
+
if (base) out.push('Explain ' + base.replace(/[^\w.\-]/g, ''));
|
|
2266
2321
|
}
|
|
2267
2322
|
const result = out.length ? out.slice(0, 3) : FOLLOWUP_SEEDS;
|
|
2268
2323
|
state._followupsMsgs = msgs;
|
|
@@ -2457,7 +2512,7 @@ function reconnectAlert() {
|
|
|
2457
2512
|
return Alert({
|
|
2458
2513
|
key: 'liveerr',
|
|
2459
2514
|
kind: 'error',
|
|
2460
|
-
title: 'Live stream
|
|
2515
|
+
title: 'Live stream offline',
|
|
2461
2516
|
children: [h('span', { key: 'lemsg' }, state.live.error + ' - '), Btn({ key: 'reco', onClick: openLiveStream, children: 'reconnect', title: 'Reconnect to history stream' })],
|
|
2462
2517
|
});
|
|
2463
2518
|
}
|
|
@@ -2946,6 +3001,10 @@ function settingsMain() {
|
|
|
2946
3001
|
// backend panel where it applies.
|
|
2947
3002
|
lede: 'Connection, agents, appearance, keyboard, and local data.',
|
|
2948
3003
|
}),
|
|
3004
|
+
state.health.corsWildcardOpen
|
|
3005
|
+
? Alert({ key: 'cors-open-warn', kind: 'error', title: 'Open CORS with no password',
|
|
3006
|
+
children: 'This server allows requests from any origin (CORS_ORIGIN=*) with no PASSWORD set - any page a user visits in their browser can read and modify this filesystem. Set PASSWORD or restrict CORS_ORIGIN to a specific origin.' })
|
|
3007
|
+
: null,
|
|
2949
3008
|
h('div', { key: 'settings-grid', class: 'settings-grid' }, [
|
|
2950
3009
|
Panel({
|
|
2951
3010
|
id: 'backend',
|
|
@@ -3121,7 +3180,7 @@ function agentsPanel() {
|
|
|
3121
3180
|
const usable = avail || a.npxInstallable; // selectable from this row
|
|
3122
3181
|
const bits = [PROTOCOL_WORDS[a.protocol] || 'agent'];
|
|
3123
3182
|
if (!avail) bits.push(a.npxInstallable ? 'runs via npx' : 'not installed');
|
|
3124
|
-
if (acp) bits.push(acp.healthy ? 'connected' : (acp.running ? 'connecting' : '
|
|
3183
|
+
if (acp) bits.push(acp.healthy ? 'connected' : (acp.running ? 'connecting' : 'offline'));
|
|
3125
3184
|
if (acp && acp.restartCount >= 1) bits.push('restarted ' + acp.restartCount + (acp.restartCount === 1 ? ' time' : ' times'));
|
|
3126
3185
|
if (acp && !acp.healthy && acp.providerInfo?.error) bits.push(acp.providerInfo.error);
|
|
3127
3186
|
if (acp && acp.idleMs > 3_600_000) bits.push(fmtDuration(acp.idleMs) + ' idle');
|