agentgui 1.0.936 → 1.0.937
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 +5 -1
- package/lib/ws-handlers-util.js +2 -1
- package/package.json +1 -1
- package/site/app/js/app.js +66 -16
- package/site/app/js/backend.js +36 -5
package/lib/http-handler.js
CHANGED
|
@@ -20,7 +20,11 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
20
20
|
if (hits > RATE_LIMIT_MAX) { res.writeHead(429, { 'Retry-After': '60' }); res.end('Too Many Requests'); return; }
|
|
21
21
|
|
|
22
22
|
const _pwd = process.env.PASSWORD;
|
|
23
|
-
|
|
23
|
+
// Optional: exempt /health from auth so container/k8s probes work
|
|
24
|
+
// without distributing the password to monitoring infra.
|
|
25
|
+
const _bareEarly = req.url.split('?')[0];
|
|
26
|
+
const _healthExempt = process.env.HEALTH_NO_AUTH === '1' && (_bareEarly === '/health' || _bareEarly === '/api/health' || _bareEarly === (BASE_URL + '/health') || _bareEarly === (BASE_URL + '/api/health'));
|
|
27
|
+
if (_pwd && !_healthExempt) {
|
|
24
28
|
const _auth = req.headers['authorization'] || '';
|
|
25
29
|
let _ok = false;
|
|
26
30
|
const _checkToken = (tok) => {
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -51,6 +51,7 @@ export function register(router, deps) {
|
|
|
51
51
|
const model = p?.model || undefined;
|
|
52
52
|
const subAgent = p?.subAgent || undefined;
|
|
53
53
|
const cwd = p?.cwd || STARTUP_CWD;
|
|
54
|
+
const resumeSessionId = p?.resumeSid || p?.resumeSessionId || undefined;
|
|
54
55
|
if (!registry.has(agentId)) err(404, `Unknown agentId: ${agentId}`);
|
|
55
56
|
|
|
56
57
|
const sessionId = 'chat-' + crypto.randomBytes(8).toString('hex');
|
|
@@ -88,7 +89,7 @@ export function register(router, deps) {
|
|
|
88
89
|
try {
|
|
89
90
|
const config = {
|
|
90
91
|
verbose: true, outputFormat: 'stream-json', timeout: 1800000, print: true,
|
|
91
|
-
model, subAgent, onEvent,
|
|
92
|
+
model, subAgent, onEvent, resumeSessionId,
|
|
92
93
|
onPid: () => {}, onProcess: (proc) => { ctrl.proc = proc; },
|
|
93
94
|
};
|
|
94
95
|
await runClaudeWithStreaming(content, cwd, agentId, config);
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -12,7 +12,7 @@ const state = {
|
|
|
12
12
|
tab: 'chat',
|
|
13
13
|
models: [],
|
|
14
14
|
selectedModel: '',
|
|
15
|
-
chat: { messages: [], busy: false, abort: null, draft: '' },
|
|
15
|
+
chat: { messages: [], busy: false, abort: null, draft: '', resumeSid: null },
|
|
16
16
|
sessions: [],
|
|
17
17
|
selectedSid: null,
|
|
18
18
|
events: [],
|
|
@@ -20,7 +20,9 @@ const state = {
|
|
|
20
20
|
searchHits: null,
|
|
21
21
|
historyError: null,
|
|
22
22
|
showSubagents: false,
|
|
23
|
-
|
|
23
|
+
sessionsLimit: 60,
|
|
24
|
+
projectFilter: '',
|
|
25
|
+
live: { es: null, connected: false, lastEventTs: 0, error: null, eventCount: 0, reconnects: 0 },
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
function readHash() {
|
|
@@ -74,12 +76,14 @@ function openLiveStream() {
|
|
|
74
76
|
state.live.lastEventTs = Date.now();
|
|
75
77
|
state.live.eventCount++;
|
|
76
78
|
if (kind === 'hello') {
|
|
77
|
-
state.live.connected = true;
|
|
79
|
+
if (!state.live.connected) state.live.connected = true;
|
|
80
|
+
if (state.live.error) { state.live.error = null; state.live.reconnects++; }
|
|
78
81
|
} else if (kind === 'event' && data) {
|
|
79
82
|
if (state.selectedSid && data.sid === state.selectedSid) {
|
|
80
83
|
state.events.push(data);
|
|
81
84
|
}
|
|
82
|
-
const
|
|
85
|
+
const arr = Array.isArray(state.sessions) ? state.sessions : [];
|
|
86
|
+
const sess = arr.find(s => s.sid === data.sid);
|
|
83
87
|
if (sess) {
|
|
84
88
|
sess.events = (sess.events || 0) + 1;
|
|
85
89
|
sess.last = data.ts || Date.now();
|
|
@@ -98,9 +102,12 @@ function openLiveStream() {
|
|
|
98
102
|
scheduleRender();
|
|
99
103
|
});
|
|
100
104
|
state.live.es.addEventListener('error', () => {
|
|
101
|
-
state
|
|
102
|
-
state.live.error
|
|
103
|
-
|
|
105
|
+
// EventSource auto-reconnects; only flap state once per disconnect.
|
|
106
|
+
if (!state.live.error) {
|
|
107
|
+
state.live.connected = false;
|
|
108
|
+
state.live.error = 'connection lost (auto-retry)';
|
|
109
|
+
scheduleRender();
|
|
110
|
+
}
|
|
104
111
|
});
|
|
105
112
|
} catch (e) {
|
|
106
113
|
state.live.error = e.message;
|
|
@@ -119,8 +126,10 @@ function view() {
|
|
|
119
126
|
const ok = state.health.status === 'ok';
|
|
120
127
|
const liveActive = state.tab === 'history' && state.live.connected && (Date.now() - state.live.lastEventTs < 30000);
|
|
121
128
|
const dotText = state.tab === 'history'
|
|
122
|
-
? (state.live.error
|
|
123
|
-
|
|
129
|
+
? (state.live.error
|
|
130
|
+
? '◌ ' + state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
|
|
131
|
+
: (liveActive ? '● live · ' + state.live.eventCount : (state.live.connected ? '● live' : '◌ connecting…')))
|
|
132
|
+
: (ok ? (state.health.ws === 'reconnecting' ? '◌ ws reconnecting' : '● connected') : '○ offline');
|
|
124
133
|
const dot = h('span', { key: 'dot' }, dotText);
|
|
125
134
|
|
|
126
135
|
const topbar = Topbar({
|
|
@@ -211,19 +220,25 @@ function chatMain() {
|
|
|
211
220
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
212
221
|
});
|
|
213
222
|
|
|
223
|
+
const resumeBanner = state.chat.resumeSid
|
|
224
|
+
? h('div', { key: 'rb', style: 'padding:.5em .75em;background:rgba(80,200,120,.1);border-radius:4px;display:flex;justify-content:space-between;align-items:center;margin-bottom:.5em' },
|
|
225
|
+
h('span', { class: 'lede' }, '▶ resuming session ' + state.chat.resumeSid.slice(0, 8) + '… via claude --resume'),
|
|
226
|
+
Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; render(); }, children: '× clear' }))
|
|
227
|
+
: null;
|
|
214
228
|
return [
|
|
229
|
+
resumeBanner,
|
|
215
230
|
Chat({
|
|
216
|
-
title: state.selectedModel || 'agent',
|
|
231
|
+
title: (state.selectedModel || 'agent') + (state.chat.resumeSid ? ' · resume' : ''),
|
|
217
232
|
sub: state.chat.busy ? 'streaming…' : (state.chat.messages.length + ' messages'),
|
|
218
233
|
messages: msgs,
|
|
219
234
|
composer,
|
|
220
235
|
}),
|
|
221
|
-
];
|
|
236
|
+
].filter(Boolean);
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
function newChat() {
|
|
225
240
|
state.chat.abort?.abort();
|
|
226
|
-
state.chat = { messages: [], busy: false, abort: null, draft: '' };
|
|
241
|
+
state.chat = { messages: [], busy: false, abort: null, draft: '', resumeSid: null };
|
|
227
242
|
render();
|
|
228
243
|
}
|
|
229
244
|
|
|
@@ -246,6 +261,7 @@ async function sendChat() {
|
|
|
246
261
|
model: state.selectedModel,
|
|
247
262
|
messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
|
|
248
263
|
signal: ctrl.signal,
|
|
264
|
+
resumeSid: state.chat.resumeSid || undefined,
|
|
249
265
|
})) {
|
|
250
266
|
if (ev.type === 'text') { cur.content += ev.text; render(); }
|
|
251
267
|
if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
@@ -314,21 +330,42 @@ function historyMain() {
|
|
|
314
330
|
function resumeInChat(sess) {
|
|
315
331
|
state.tab = 'chat';
|
|
316
332
|
closeLiveStream();
|
|
317
|
-
state.chat.
|
|
333
|
+
state.chat.resumeSid = sess?.sid || state.selectedSid;
|
|
334
|
+
state.chat.messages = [];
|
|
335
|
+
state.chat.draft = '';
|
|
336
|
+
// Default to claude-code if no model yet (only claude supports --resume by sid here).
|
|
337
|
+
if (!state.selectedModel || state.selectedModel !== 'claude-code') state.selectedModel = 'claude-code';
|
|
318
338
|
render();
|
|
319
339
|
}
|
|
320
340
|
|
|
321
341
|
function visibleSessions() {
|
|
322
342
|
const arr = Array.isArray(state.sessions) ? state.sessions : [];
|
|
323
|
-
|
|
343
|
+
let filtered = state.showSubagents ? arr : arr.filter(s => !s.isSubagent);
|
|
344
|
+
if (state.projectFilter) {
|
|
345
|
+
const pf = state.projectFilter.toLowerCase();
|
|
346
|
+
filtered = filtered.filter(s => (s.project || '').toLowerCase().includes(pf));
|
|
347
|
+
}
|
|
324
348
|
return filtered.slice().sort((a, b) => (b.last || 0) - (a.last || 0));
|
|
325
349
|
}
|
|
326
350
|
|
|
351
|
+
function uniqueProjects() {
|
|
352
|
+
const arr = Array.isArray(state.sessions) ? state.sessions : [];
|
|
353
|
+
const seen = new Map();
|
|
354
|
+
for (const s of arr) {
|
|
355
|
+
if (!s.project) continue;
|
|
356
|
+
seen.set(s.project, (seen.get(s.project) || 0) + 1);
|
|
357
|
+
}
|
|
358
|
+
return Array.from(seen.entries()).sort((a, b) => b[1] - a[1]);
|
|
359
|
+
}
|
|
360
|
+
|
|
327
361
|
function historySide() {
|
|
328
362
|
const searching = !!state.searchHits;
|
|
329
363
|
const sessionsView = visibleSessions();
|
|
364
|
+
const limit = state.sessionsLimit;
|
|
365
|
+
const visible = searching ? state.searchHits.results.slice(0, 60) : sessionsView.slice(0, limit);
|
|
366
|
+
const truncatedBy = searching ? Math.max(0, state.searchHits.results.length - 60) : Math.max(0, sessionsView.length - limit);
|
|
330
367
|
const rows = searching
|
|
331
|
-
?
|
|
368
|
+
? visible.map((r, i) =>
|
|
332
369
|
Row({
|
|
333
370
|
key: 'sr' + i,
|
|
334
371
|
rank: String(i + 1).padStart(3, '0'),
|
|
@@ -338,7 +375,7 @@ function historySide() {
|
|
|
338
375
|
onClick: () => loadSession(r.sid),
|
|
339
376
|
})
|
|
340
377
|
)
|
|
341
|
-
:
|
|
378
|
+
: visible.map((s, i) =>
|
|
342
379
|
Row({
|
|
343
380
|
key: 'sess' + s.sid,
|
|
344
381
|
rank: String(i + 1).padStart(3, '0'),
|
|
@@ -350,6 +387,7 @@ function historySide() {
|
|
|
350
387
|
})
|
|
351
388
|
);
|
|
352
389
|
const subagentCount = (Array.isArray(state.sessions) ? state.sessions : []).filter(s => s.isSubagent).length;
|
|
390
|
+
const projects = uniqueProjects();
|
|
353
391
|
|
|
354
392
|
return [
|
|
355
393
|
Side({
|
|
@@ -373,6 +411,15 @@ function historySide() {
|
|
|
373
411
|
value: state.searchQ,
|
|
374
412
|
onInput: (v) => { state.searchQ = v; runSearch(); },
|
|
375
413
|
}),
|
|
414
|
+
state.searchQ && searching
|
|
415
|
+
? Btn({ key: 'clearq', onClick: () => { state.searchQ = ''; state.searchHits = null; render(); }, children: '× clear search' })
|
|
416
|
+
: null,
|
|
417
|
+
!searching && projects.length > 1
|
|
418
|
+
? h('div', { key: 'projfilter', style: 'display:flex;flex-wrap:wrap;gap:.25em;padding:.25em 0' },
|
|
419
|
+
h('span', { key: 'allp', class: 'lede', style: 'cursor:pointer;padding:.15em .5em;border-radius:3px;' + (!state.projectFilter ? 'background:rgba(80,200,120,.15)' : ''), onClick: () => { state.projectFilter = ''; render(); } }, 'all'),
|
|
420
|
+
...projects.slice(0, 8).map(([name, count]) =>
|
|
421
|
+
h('span', { key: 'p'+name, class: 'lede', style: 'cursor:pointer;padding:.15em .5em;border-radius:3px;' + (state.projectFilter === name ? 'background:rgba(80,200,120,.15)' : ''), title: name, onClick: () => { state.projectFilter = state.projectFilter === name ? '' : name; render(); } }, (name.length > 20 ? name.slice(0, 20) + '…' : name) + ' (' + count + ')')))
|
|
422
|
+
: null,
|
|
376
423
|
!searching && subagentCount
|
|
377
424
|
? h('label', { key: 'subtog', class: 'lede', style: 'display:flex;gap:.5em;align-items:center;padding:.25em 0' },
|
|
378
425
|
h('input', { type: 'checkbox', checked: state.showSubagents, onChange: (e) => { state.showSubagents = e.target.checked; render(); } }),
|
|
@@ -381,6 +428,9 @@ function historySide() {
|
|
|
381
428
|
state.historyError
|
|
382
429
|
? h('p', { key: 'err', class: 'lede' }, '⚠ ' + state.historyError)
|
|
383
430
|
: (rows.length ? h('div', { key: 'rows' }, ...rows) : h('p', { key: 'empty', class: 'lede' }, 'no sessions yet')),
|
|
431
|
+
!searching && truncatedBy > 0
|
|
432
|
+
? Btn({ key: 'more', onClick: () => { state.sessionsLimit += 60; render(); }, children: '↓ show '+Math.min(60, truncatedBy)+' more ('+truncatedBy+' hidden)' })
|
|
433
|
+
: null,
|
|
384
434
|
],
|
|
385
435
|
}),
|
|
386
436
|
];
|
package/site/app/js/backend.js
CHANGED
|
@@ -87,11 +87,31 @@ let _wsReady = null; // Promise that resolves when ws is OPEN
|
|
|
87
87
|
let _nextReqId = 1;
|
|
88
88
|
const _pending = new Map(); // requestId → { resolve, reject }
|
|
89
89
|
const _sessionListeners = new Map(); // sessionId → Set<(event)=>void>
|
|
90
|
-
const _statusListeners = new Set(); // fn(state) where state in 'open'|'closed'|'error'
|
|
90
|
+
const _statusListeners = new Set(); // fn(state) where state in 'open'|'closed'|'error'|'reconnecting'
|
|
91
|
+
let _reconnectAttempts = 0;
|
|
92
|
+
let _reconnectTimer = null;
|
|
93
|
+
let _wsBaseHint = ''; // base remembered for reconnect
|
|
91
94
|
|
|
92
95
|
export function onWsStatus(fn) { _statusListeners.add(fn); return () => _statusListeners.delete(fn); }
|
|
93
96
|
function emitStatus(s) { for (const fn of _statusListeners) { try { fn(s); } catch {} } }
|
|
94
97
|
|
|
98
|
+
function scheduleReconnect() {
|
|
99
|
+
if (_reconnectTimer) return;
|
|
100
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
101
|
+
// Wait for online before retrying.
|
|
102
|
+
const onOnline = () => { window.removeEventListener('online', onOnline); _reconnectAttempts = 0; ensureWs(_wsBaseHint).catch(() => {}); };
|
|
103
|
+
window.addEventListener('online', onOnline);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const delay = Math.min(30000, 500 * Math.pow(2, _reconnectAttempts));
|
|
107
|
+
_reconnectAttempts++;
|
|
108
|
+
emitStatus('reconnecting');
|
|
109
|
+
_reconnectTimer = setTimeout(() => {
|
|
110
|
+
_reconnectTimer = null;
|
|
111
|
+
ensureWs(_wsBaseHint).catch(() => {});
|
|
112
|
+
}, delay);
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
function wsUrl(base) {
|
|
96
116
|
let proto, host;
|
|
97
117
|
if (base) {
|
|
@@ -110,11 +130,20 @@ function wsUrl(base) {
|
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
function ensureWs(base) {
|
|
133
|
+
_wsBaseHint = base || _wsBaseHint;
|
|
113
134
|
if (_ws && _ws.readyState === 1) return _wsReady;
|
|
114
135
|
if (_ws && _ws.readyState === 0) return _wsReady;
|
|
115
|
-
_ws = new WebSocket(wsUrl(
|
|
136
|
+
_ws = new WebSocket(wsUrl(_wsBaseHint));
|
|
116
137
|
_wsReady = new Promise((resolve, reject) => {
|
|
117
|
-
_ws.addEventListener('open', () => {
|
|
138
|
+
_ws.addEventListener('open', () => {
|
|
139
|
+
_reconnectAttempts = 0;
|
|
140
|
+
emitStatus('open');
|
|
141
|
+
// Re-subscribe any session listeners that survived the disconnect.
|
|
142
|
+
for (const sid of _sessionListeners.keys()) {
|
|
143
|
+
try { _ws.send(encode({ m: 'conversation.subscribe', r: _nextReqId++, p: { sessionId: sid } })); } catch {}
|
|
144
|
+
}
|
|
145
|
+
resolve(_ws);
|
|
146
|
+
});
|
|
118
147
|
_ws.addEventListener('error', (e) => { emitStatus('error'); reject(e); });
|
|
119
148
|
_ws.addEventListener('close', () => {
|
|
120
149
|
emitStatus('closed');
|
|
@@ -122,6 +151,8 @@ function ensureWs(base) {
|
|
|
122
151
|
_pending.clear();
|
|
123
152
|
_ws = null;
|
|
124
153
|
_wsReady = null;
|
|
154
|
+
// Auto-reconnect if there are listeners or callers will retry.
|
|
155
|
+
if (_sessionListeners.size > 0 || _statusListeners.size > 0) scheduleReconnect();
|
|
125
156
|
});
|
|
126
157
|
_ws.addEventListener('message', (ev) => {
|
|
127
158
|
let msg;
|
|
@@ -187,7 +218,7 @@ export async function listModels(base) {
|
|
|
187
218
|
// { type: 'error', error: '...' }
|
|
188
219
|
//
|
|
189
220
|
// Caller signature kept compatible with the previous HTTP/SSE impl.
|
|
190
|
-
export async function* streamChat(base, { model, messages, signal, agentId }) {
|
|
221
|
+
export async function* streamChat(base, { model, messages, signal, agentId, resumeSid }) {
|
|
191
222
|
// The last user message is the prompt; agentgui's claude-runner doesn't
|
|
192
223
|
// accept a full message list — it spawns the agent for a single prompt.
|
|
193
224
|
// For multi-turn, the agent's own session/resume handles continuity.
|
|
@@ -221,7 +252,7 @@ export async function* streamChat(base, { model, messages, signal, agentId }) {
|
|
|
221
252
|
// Kick off the chat on the server.
|
|
222
253
|
let started;
|
|
223
254
|
try {
|
|
224
|
-
started = await wsCall(base, 'chat.sendMessage', { content, agentId: resolvedAgentId, model: resolvedModel });
|
|
255
|
+
started = await wsCall(base, 'chat.sendMessage', { content, agentId: resolvedAgentId, model: resolvedModel, resumeSid });
|
|
225
256
|
} catch (e) {
|
|
226
257
|
yield { type: 'error', error: e.message };
|
|
227
258
|
return;
|