agentgui 1.0.938 → 1.0.940
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 +10 -0
- package/lib/claude-runner-agents.js +25 -0
- package/lib/ws-handlers-util.js +27 -1
- package/package.json +1 -1
- package/site/app/index.html +63 -4
- package/site/app/js/app.js +322 -103
- package/site/app/js/backend.js +24 -22
- package/site/app/vendor/anentrypoint-design/247420.css +5216 -0
- package/site/app/vendor/anentrypoint-design/247420.js +247 -0
- package/site/app/vendor/cdn/dompurify.js +9 -0
- package/site/app/vendor/cdn/fonts/1291de6d401a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/1ba89a87e0b8.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/3644d51c507b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/4b91d2650dc2.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/530d036ba64a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/570a2bdd8f8b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/5dd6d880fee9.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/62de9143afe3.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/64884efa2f11.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/68cd7063be2e.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/6c252abcf99b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/71e69e06516a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/9ea68c62083f.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/c010f9b7d6b2.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/d69723fc74be.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/fonts.css +459 -0
- package/site/app/vendor/cdn/marked.js +8 -0
- package/site/app/vendor/cdn/prismjs/components/prism-bash.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-clike.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-core.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-css.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-diff.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-go.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-javascript.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-json.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-jsx.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-markdown.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-markup.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-python.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-rust.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-sql.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-toml.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-tsx.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-typescript.min.js +1 -0
- package/site/app/vendor/cdn/prismjs/components/prism-yaml.min.js +1 -0
package/site/app/js/app.js
CHANGED
|
@@ -10,8 +10,11 @@ const state = {
|
|
|
10
10
|
backendDraft: B.getBackend(),
|
|
11
11
|
health: { status: 'unknown' },
|
|
12
12
|
tab: 'chat',
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
agents: [],
|
|
14
|
+
selectedAgent: localStorage.getItem('agentgui.agent') || '',
|
|
15
|
+
agentModels: [],
|
|
16
|
+
selectedModel: localStorage.getItem('agentgui.model') || '',
|
|
17
|
+
chatCwd: localStorage.getItem('agentgui.cwd') || '',
|
|
15
18
|
chat: { messages: [], busy: false, abort: null, draft: '', resumeSid: null },
|
|
16
19
|
sessions: [],
|
|
17
20
|
selectedSid: null,
|
|
@@ -23,6 +26,8 @@ const state = {
|
|
|
23
26
|
sessionsLimit: 60,
|
|
24
27
|
projectFilter: '',
|
|
25
28
|
live: { es: null, connected: false, lastEventTs: 0, error: null, eventCount: 0, reconnects: 0 },
|
|
29
|
+
active: [],
|
|
30
|
+
activeTimer: null,
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
function readHash() {
|
|
@@ -85,18 +90,60 @@ function timeNow() {
|
|
|
85
90
|
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
async function selectAgent(id) {
|
|
94
|
+
state.selectedAgent = id;
|
|
95
|
+
localStorage.setItem('agentgui.agent', id);
|
|
96
|
+
state.agentModels = [];
|
|
97
|
+
state.selectedModel = '';
|
|
98
|
+
render();
|
|
99
|
+
const models = await B.listAgentModels(state.backend, id);
|
|
100
|
+
if (state.selectedAgent !== id) return; // changed while loading
|
|
101
|
+
state.agentModels = models;
|
|
102
|
+
const saved = localStorage.getItem('agentgui.model');
|
|
103
|
+
state.selectedModel = (saved && models.some(m => m.id === saved)) ? saved : (models[0]?.id || '');
|
|
104
|
+
render();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function selectModel(id) {
|
|
108
|
+
state.selectedModel = id;
|
|
109
|
+
localStorage.setItem('agentgui.model', id);
|
|
110
|
+
render();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function agentById(id) { return state.agents.find(a => a.id === id); }
|
|
114
|
+
function agentAvailable(id) { const a = agentById(id); return !a || a.available !== false; }
|
|
115
|
+
|
|
88
116
|
function navTo(tab) {
|
|
89
117
|
const prev = state.tab;
|
|
90
118
|
state.tab = tab;
|
|
91
119
|
if (tab === 'history') {
|
|
92
120
|
refreshHistory();
|
|
93
121
|
openLiveStream();
|
|
122
|
+
startActivePolling();
|
|
94
123
|
} else if (prev === 'history') {
|
|
95
124
|
closeLiveStream();
|
|
125
|
+
stopActivePolling();
|
|
96
126
|
}
|
|
97
127
|
render();
|
|
98
128
|
}
|
|
99
129
|
|
|
130
|
+
async function refreshActive() {
|
|
131
|
+
state.active = await B.listActiveChats(state.backend);
|
|
132
|
+
render();
|
|
133
|
+
}
|
|
134
|
+
function startActivePolling() {
|
|
135
|
+
if (state.activeTimer) return;
|
|
136
|
+
refreshActive();
|
|
137
|
+
state.activeTimer = setInterval(refreshActive, 3000);
|
|
138
|
+
}
|
|
139
|
+
function stopActivePolling() {
|
|
140
|
+
if (state.activeTimer) { clearInterval(state.activeTimer); state.activeTimer = null; }
|
|
141
|
+
}
|
|
142
|
+
async function stopActiveChat(sid) {
|
|
143
|
+
try { await B.cancelChat(state.backend, sid); } catch {}
|
|
144
|
+
refreshActive();
|
|
145
|
+
}
|
|
146
|
+
|
|
100
147
|
function openLiveStream() {
|
|
101
148
|
if (state.live.es) return;
|
|
102
149
|
state.live.error = null;
|
|
@@ -171,53 +218,70 @@ function view() {
|
|
|
171
218
|
onNav: (label) => navTo(label),
|
|
172
219
|
});
|
|
173
220
|
|
|
221
|
+
const agentOptions = state.agents.map(a => ({
|
|
222
|
+
value: a.id,
|
|
223
|
+
label: a.name + (a.available === false ? (a.npxInstallable ? ' (via npx)' : ' (not installed)') : ''),
|
|
224
|
+
disabled: a.available === false && !a.npxInstallable,
|
|
225
|
+
}));
|
|
226
|
+
const showModelPicker = state.tab === 'chat' && state.agentModels.length > 0;
|
|
227
|
+
|
|
174
228
|
const crumbRight = state.tab === 'chat'
|
|
175
|
-
? [
|
|
229
|
+
? [h('div', { key: 'cc', class: 'chat-controls' },
|
|
176
230
|
Select({
|
|
177
|
-
key: '
|
|
178
|
-
value: state.
|
|
179
|
-
placeholder: '—
|
|
180
|
-
title: 'Select
|
|
181
|
-
options:
|
|
182
|
-
onChange: (v) => {
|
|
231
|
+
key: 'agentsel',
|
|
232
|
+
value: state.selectedAgent,
|
|
233
|
+
placeholder: '— agent —',
|
|
234
|
+
title: 'Select coding agent',
|
|
235
|
+
options: agentOptions,
|
|
236
|
+
onChange: (v) => { selectAgent(v); },
|
|
183
237
|
}),
|
|
238
|
+
showModelPicker
|
|
239
|
+
? Select({
|
|
240
|
+
key: 'modelsel',
|
|
241
|
+
value: state.selectedModel,
|
|
242
|
+
placeholder: '— model —',
|
|
243
|
+
title: 'Select model for this agent',
|
|
244
|
+
options: state.agentModels.map(m => ({ value: m.id, label: m.name || m.id })),
|
|
245
|
+
onChange: (v) => { selectModel(v); },
|
|
246
|
+
})
|
|
247
|
+
: null,
|
|
184
248
|
state.chat.busy
|
|
185
249
|
? Btn({ key: 'stop', onClick: cancelChat, children: '◼ stop', title: 'Stop streaming' })
|
|
186
250
|
: Btn({ key: 'new', onClick: newChat, children: '+ new', title: 'Start new chat (clears history)' }),
|
|
187
251
|
dot,
|
|
188
|
-
]
|
|
252
|
+
)]
|
|
189
253
|
: [dot];
|
|
190
254
|
|
|
255
|
+
// Topbar already shows "agentgui / <tab>"; the crumb is reserved for contextual
|
|
256
|
+
// controls (model picker, new/stop, live status) so it doesn't duplicate the path.
|
|
191
257
|
const crumb = Crumb({
|
|
192
|
-
trail: [
|
|
193
|
-
leaf:
|
|
258
|
+
trail: [],
|
|
259
|
+
leaf: '',
|
|
194
260
|
right: crumbRight,
|
|
195
261
|
});
|
|
196
262
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
group: 'navigate',
|
|
201
|
-
items: [
|
|
202
|
-
{ glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
|
|
203
|
-
onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
204
|
-
{ glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
|
|
205
|
-
onClick: (e) => { e.preventDefault(); navTo('history'); } },
|
|
206
|
-
{ glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
|
|
207
|
-
onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
208
|
-
],
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
});
|
|
212
|
-
const side = state.tab === 'history' ? historySide() : navSide;
|
|
263
|
+
// Sidebar is contextual: history shows the session list; chat/settings have no
|
|
264
|
+
// sidebar (the topbar already provides primary nav) so main content gets full width.
|
|
265
|
+
const side = state.tab === 'history' ? historySide() : null;
|
|
213
266
|
|
|
267
|
+
const agentLabel = state.selectedAgent
|
|
268
|
+
? '⌘ ' + (agentById(state.selectedAgent)?.name || state.selectedAgent) + (state.selectedModel ? ' · ' + state.selectedModel : '')
|
|
269
|
+
: '○ no agent';
|
|
214
270
|
const status = Status({
|
|
215
|
-
left: [state.backend, ok ? '● live' : '○ offline'],
|
|
216
|
-
right: [
|
|
271
|
+
left: [state.backend || 'same-origin', ok ? '● live' : '○ offline'],
|
|
272
|
+
right: [agentLabel],
|
|
217
273
|
});
|
|
218
274
|
|
|
219
|
-
const
|
|
220
|
-
|
|
275
|
+
const mainStyle = state.tab === 'chat'
|
|
276
|
+
? 'min-height:0;height:100%;display:flex;flex-direction:column'
|
|
277
|
+
: 'min-height:0;height:100%;overflow:auto';
|
|
278
|
+
const shortcutsHint = state.showShortcuts
|
|
279
|
+
? Alert({ key: 'sc', kind: 'info', title: 'Keyboard shortcuts',
|
|
280
|
+
children: 'g then c/h/s — chat/history/settings · n — new chat · / — focus search/composer · ? — toggle this · Esc — blur field' })
|
|
281
|
+
: null;
|
|
282
|
+
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));
|
|
283
|
+
// settings reads better centered in a measure; chat + history use full width.
|
|
284
|
+
return AppShell({ topbar, crumb, side, main, status, narrow: state.tab === 'settings' });
|
|
221
285
|
}
|
|
222
286
|
|
|
223
287
|
function mainContent() {
|
|
@@ -227,42 +291,78 @@ function mainContent() {
|
|
|
227
291
|
}
|
|
228
292
|
|
|
229
293
|
// ── chat ───────────────────────────────────────────────────────────────────
|
|
294
|
+
function canSend() {
|
|
295
|
+
return !!state.selectedAgent && agentAvailable(state.selectedAgent) && !state.chat.busy;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function toolSummary(block) {
|
|
299
|
+
const name = block.name || block.kind || 'tool';
|
|
300
|
+
let arg = '';
|
|
301
|
+
const inp = block.input || block.rawInput;
|
|
302
|
+
if (inp && typeof inp === 'object') {
|
|
303
|
+
arg = inp.command || inp.file_path || inp.path || inp.pattern || inp.query || inp.url || '';
|
|
304
|
+
if (!arg) { try { arg = JSON.stringify(inp).slice(0, 120); } catch {} }
|
|
305
|
+
}
|
|
306
|
+
return '⌘ ' + name + (arg ? ' · ' + String(arg).slice(0, 120) : '');
|
|
307
|
+
}
|
|
308
|
+
|
|
230
309
|
function chatMain() {
|
|
231
310
|
const lastIdx = state.chat.messages.length - 1;
|
|
311
|
+
const agentName = agentById(state.selectedAgent)?.name || state.selectedAgent || 'agent';
|
|
232
312
|
const msgs = state.chat.messages.map((m, i) => {
|
|
233
313
|
const isAssistant = m.role === 'assistant';
|
|
234
314
|
const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
|
|
235
|
-
const
|
|
315
|
+
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
316
|
+
const isEmptyStreaming = isStreaming && !m.content && !hasParts;
|
|
317
|
+
const parts = [];
|
|
318
|
+
if (m.content) parts.push({ kind: isAssistant ? 'md' : 'text', text: m.content });
|
|
319
|
+
if (hasParts) for (const p of m.parts) parts.push({ kind: 'text', text: p });
|
|
236
320
|
return {
|
|
237
321
|
key: m.id || String(i),
|
|
238
322
|
who: isAssistant ? 'them' : 'you',
|
|
239
|
-
name: isAssistant ?
|
|
323
|
+
name: isAssistant ? agentName : 'you',
|
|
240
324
|
time: m.time || '',
|
|
241
325
|
typing: isEmptyStreaming,
|
|
242
|
-
parts: isEmptyStreaming
|
|
243
|
-
? undefined
|
|
244
|
-
: [{ kind: isAssistant ? 'md' : 'text', text: m.content || '' }],
|
|
326
|
+
parts: isEmptyStreaming ? undefined : (parts.length ? parts : [{ kind: 'text', text: '' }]),
|
|
245
327
|
};
|
|
246
328
|
});
|
|
247
329
|
|
|
330
|
+
const placeholder = !state.selectedAgent
|
|
331
|
+
? 'choose an agent first'
|
|
332
|
+
: (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message…');
|
|
248
333
|
const composer = ChatComposer({
|
|
249
334
|
value: state.chat.draft,
|
|
250
|
-
disabled: state.chat.busy,
|
|
251
|
-
placeholder
|
|
335
|
+
disabled: state.chat.busy || !canSend(),
|
|
336
|
+
placeholder,
|
|
252
337
|
onInput: (v) => { state.chat.draft = v; render(); },
|
|
253
338
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
254
339
|
});
|
|
255
340
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
341
|
+
const banners = [];
|
|
342
|
+
if (state.chat.resumeSid) {
|
|
343
|
+
banners.push(h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
|
|
344
|
+
h('span', { class: 'lede' }, '▶ resuming session ' + state.chat.resumeSid.slice(0, 8) + '… via --resume'),
|
|
345
|
+
Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; render(); }, children: '× clear' })));
|
|
346
|
+
}
|
|
347
|
+
banners.push(h('div', { key: 'cwdb', class: 'resume-banner', role: 'group', 'aria-label': 'Working directory' },
|
|
348
|
+
h('span', { class: 'lede' }, state.chatCwd ? '▣ cwd: ' + state.chatCwd : '▣ cwd: server default'),
|
|
349
|
+
Btn({ key: 'cwdset', onClick: () => {
|
|
350
|
+
const v = prompt('Working directory for new chats (absolute path; blank = server default):', state.chatCwd || '');
|
|
351
|
+
if (v === null) return;
|
|
352
|
+
state.chatCwd = v.trim();
|
|
353
|
+
if (state.chatCwd) localStorage.setItem('agentgui.cwd', state.chatCwd); else localStorage.removeItem('agentgui.cwd');
|
|
354
|
+
render();
|
|
355
|
+
}, children: state.chatCwd ? 'change' : 'set' }),
|
|
356
|
+
state.chatCwd ? Btn({ key: 'cwdclr', onClick: () => { state.chatCwd = ''; localStorage.removeItem('agentgui.cwd'); render(); }, children: '× default' }) : null));
|
|
357
|
+
if (state.selectedAgent && !agentAvailable(state.selectedAgent)) {
|
|
358
|
+
banners.push(Alert({ key: 'unavail', kind: 'warn', title: agentName + ' is not installed',
|
|
359
|
+
children: 'This agent\'s CLI was not found on the server. Pick another agent or install it.' }));
|
|
360
|
+
}
|
|
261
361
|
return [
|
|
262
|
-
|
|
362
|
+
...banners,
|
|
263
363
|
Chat({
|
|
264
|
-
title: (state.selectedModel
|
|
265
|
-
sub: state.chat.busy ? 'streaming…' :
|
|
364
|
+
title: agentName + (state.selectedModel ? ' · ' + state.selectedModel : '') + (state.chat.resumeSid ? ' · resume' : ''),
|
|
365
|
+
sub: state.chat.busy ? 'streaming…' : undefined,
|
|
266
366
|
messages: msgs,
|
|
267
367
|
composer,
|
|
268
368
|
}),
|
|
@@ -270,43 +370,69 @@ function chatMain() {
|
|
|
270
370
|
}
|
|
271
371
|
|
|
272
372
|
function newChat() {
|
|
273
|
-
if (!confirm('Clear chat history? This cannot be undone.')) return;
|
|
373
|
+
if (state.chat.messages.length && !confirm('Clear chat history? This cannot be undone.')) return;
|
|
274
374
|
state.chat.abort?.abort();
|
|
275
375
|
state.chat = { messages: [], busy: false, abort: null, draft: '', resumeSid: null };
|
|
376
|
+
try { localStorage.removeItem(CHAT_KEY); } catch {}
|
|
276
377
|
render();
|
|
277
378
|
}
|
|
278
379
|
|
|
279
380
|
function cancelChat() { state.chat.abort?.abort(); }
|
|
280
381
|
|
|
382
|
+
const CHAT_KEY = 'agentgui.chat';
|
|
383
|
+
function persistChat() {
|
|
384
|
+
try {
|
|
385
|
+
const msgs = state.chat.messages.map(m => ({ id: m.id, role: m.role, content: m.content, time: m.time, parts: m.parts }));
|
|
386
|
+
if (!msgs.length) { localStorage.removeItem(CHAT_KEY); return; }
|
|
387
|
+
localStorage.setItem(CHAT_KEY, JSON.stringify({ messages: msgs, resumeSid: state.chat.resumeSid, agent: state.selectedAgent, model: state.selectedModel }));
|
|
388
|
+
} catch {}
|
|
389
|
+
}
|
|
390
|
+
function restoreChat() {
|
|
391
|
+
try {
|
|
392
|
+
const raw = localStorage.getItem(CHAT_KEY);
|
|
393
|
+
if (!raw) return;
|
|
394
|
+
const saved = JSON.parse(raw);
|
|
395
|
+
if (Array.isArray(saved.messages) && saved.messages.length) {
|
|
396
|
+
state.chat.messages = saved.messages.map(m => ({ ...m, parts: Array.isArray(m.parts) ? m.parts : [] }));
|
|
397
|
+
state.chat.resumeSid = saved.resumeSid || null;
|
|
398
|
+
}
|
|
399
|
+
} catch {}
|
|
400
|
+
}
|
|
401
|
+
|
|
281
402
|
async function sendChat() {
|
|
282
403
|
const text = (state.chat.draft || '').trim();
|
|
283
|
-
if (!text || !
|
|
404
|
+
if (!text || !canSend()) return;
|
|
284
405
|
const t = timeNow();
|
|
285
406
|
const userMsg = { id: 'u' + Date.now(), role: 'user', content: text, time: t };
|
|
286
|
-
const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: t };
|
|
407
|
+
const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: t, parts: [] };
|
|
287
408
|
state.chat.messages = [...state.chat.messages, userMsg, curMsg];
|
|
288
409
|
state.chat.draft = '';
|
|
289
410
|
state.chat.busy = true;
|
|
290
411
|
const ctrl = new AbortController();
|
|
291
412
|
state.chat.abort = ctrl;
|
|
413
|
+
persistChat();
|
|
292
414
|
render();
|
|
293
415
|
scrollChatToBottom();
|
|
294
416
|
const cur = state.chat.messages[state.chat.messages.length - 1];
|
|
295
417
|
try {
|
|
296
418
|
for await (const ev of B.streamChat(state.backend, {
|
|
297
|
-
|
|
419
|
+
agentId: state.selectedAgent,
|
|
420
|
+
model: state.selectedModel || undefined,
|
|
421
|
+
cwd: state.chatCwd || undefined,
|
|
298
422
|
messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
|
|
299
423
|
signal: ctrl.signal,
|
|
300
424
|
resumeSid: state.chat.resumeSid || undefined,
|
|
301
425
|
})) {
|
|
302
426
|
if (ev.type === 'text') { cur.content += ev.text; render(); scrollChatToBottom(); }
|
|
303
|
-
if (ev.type === '
|
|
427
|
+
else if (ev.type === 'tool') { cur.parts.push(toolSummary(ev.block)); render(); scrollChatToBottom(); }
|
|
428
|
+
else if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
304
429
|
}
|
|
305
430
|
} catch (e) {
|
|
306
431
|
if (e.name !== 'AbortError') cur.content += '\n[error] ' + e.message;
|
|
307
432
|
} finally {
|
|
308
433
|
state.chat.busy = false;
|
|
309
434
|
state.chat.abort = null;
|
|
435
|
+
persistChat();
|
|
310
436
|
render();
|
|
311
437
|
scrollChatToBottom();
|
|
312
438
|
}
|
|
@@ -349,6 +475,7 @@ function historyMain() {
|
|
|
349
475
|
return [head, actions, Panel({ title: 'events', children: h('div', { key: 'loading', class: 'lede empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ key: 'spin', size: 'sm' }), 'loading events…') })];
|
|
350
476
|
}
|
|
351
477
|
|
|
478
|
+
if (!state.expandedEvents) state.expandedEvents = new Set();
|
|
352
479
|
return [
|
|
353
480
|
head,
|
|
354
481
|
actions,
|
|
@@ -356,16 +483,21 @@ function historyMain() {
|
|
|
356
483
|
title: state.events.length + ' events',
|
|
357
484
|
children: EventList({
|
|
358
485
|
items: state.events.slice(-300).map((e, i) => {
|
|
486
|
+
const idx = e.i ?? i;
|
|
359
487
|
const role = e.role || '?';
|
|
360
488
|
const type = e.type || '?';
|
|
361
489
|
const tool = e.tool ? ' · ⌘ ' + e.tool : '';
|
|
362
490
|
const errMark = e.isError ? ' · ⚠' : '';
|
|
363
|
-
const
|
|
491
|
+
const raw = e.text || '';
|
|
492
|
+
const text = raw.replace(/\s+/g, ' ').trim();
|
|
493
|
+
const expanded = state.expandedEvents.has(idx);
|
|
494
|
+
const full = e.toolInput ? (text + '\n\n' + JSON.stringify(e.toolInput, null, 2)) : raw;
|
|
364
495
|
return {
|
|
365
|
-
key: 'ev' +
|
|
366
|
-
code: String(
|
|
367
|
-
title: text.slice(0, 220) || '(' + type + ')',
|
|
368
|
-
sub: new Date(e.ts).toLocaleString() + ' · ' + role + ' · ' + type + tool + errMark,
|
|
496
|
+
key: 'ev' + idx,
|
|
497
|
+
code: String(idx + 1).padStart(4, '0'),
|
|
498
|
+
title: expanded ? (full || '(' + type + ')') : (text.slice(0, 220) || '(' + type + ')'),
|
|
499
|
+
sub: new Date(e.ts).toLocaleString() + ' · ' + role + ' · ' + type + tool + errMark + (raw.length > 220 ? ' · ' + (expanded ? 'click to collapse' : 'click to expand') : ''),
|
|
500
|
+
onClick: () => { expanded ? state.expandedEvents.delete(idx) : state.expandedEvents.add(idx); render(); },
|
|
369
501
|
};
|
|
370
502
|
}),
|
|
371
503
|
}),
|
|
@@ -379,8 +511,8 @@ function resumeInChat(sess) {
|
|
|
379
511
|
state.chat.resumeSid = sess?.sid || state.selectedSid;
|
|
380
512
|
state.chat.messages = [];
|
|
381
513
|
state.chat.draft = '';
|
|
382
|
-
//
|
|
383
|
-
if (
|
|
514
|
+
// Only claude-code supports --resume by sid here.
|
|
515
|
+
if (state.selectedAgent !== 'claude-code') selectAgent('claude-code');
|
|
384
516
|
render();
|
|
385
517
|
}
|
|
386
518
|
|
|
@@ -434,20 +566,22 @@ function historySide() {
|
|
|
434
566
|
);
|
|
435
567
|
const subagentCount = (Array.isArray(state.sessions) ? state.sessions : []).filter(s => s.isSubagent).length;
|
|
436
568
|
const projects = uniqueProjects();
|
|
569
|
+
const running = Array.isArray(state.active) ? state.active : [];
|
|
437
570
|
|
|
438
571
|
return [
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
572
|
+
running.length
|
|
573
|
+
? Panel({
|
|
574
|
+
key: 'runningPanel',
|
|
575
|
+
title: '▶ running · ' + running.length,
|
|
576
|
+
children: running.map((r, i) => {
|
|
577
|
+
const agentName = agentById(r.agentId)?.name || r.agentId || 'agent';
|
|
578
|
+
const elapsed = r.startedAt ? Math.round((Date.now() - r.startedAt) / 1000) : 0;
|
|
579
|
+
return h('div', { key: 'run' + r.sessionId, class: 'resume-banner', role: 'group' },
|
|
580
|
+
h('span', { class: 'lede' }, '● ' + agentName + (r.model ? ' · ' + r.model : '') + ' · ' + elapsed + 's' + (r.cwd ? ' · ' + r.cwd.split('/').slice(-1)[0] : '')),
|
|
581
|
+
Btn({ key: 'stop' + r.sessionId, onClick: () => stopActiveChat(r.sessionId), children: '◼ stop' }));
|
|
582
|
+
}),
|
|
583
|
+
})
|
|
584
|
+
: null,
|
|
451
585
|
Panel({
|
|
452
586
|
title: searching
|
|
453
587
|
? 'matches · ' + (state.searchHits.results?.length || 0)
|
|
@@ -460,11 +594,17 @@ function historySide() {
|
|
|
460
594
|
value: state.searchQ,
|
|
461
595
|
onInput: (v) => { state.searchQ = v; debouncedSearch(); },
|
|
462
596
|
}),
|
|
597
|
+
state.searchBusy
|
|
598
|
+
? h('div', { key: 'searchbusy', class: 'lede empty-state', role: 'status' }, Spinner({ key: 'ss', size: 'sm' }), 'searching…')
|
|
599
|
+
: null,
|
|
463
600
|
searching && state.searchHits.error
|
|
464
601
|
? Alert({ key: 'searcherr', kind: 'error', title: 'Search failed', children: state.searchHits.error })
|
|
465
602
|
: null,
|
|
466
|
-
state.
|
|
467
|
-
?
|
|
603
|
+
searching && !state.searchBusy && !state.searchHits.error && (state.searchHits.results || []).length === 0
|
|
604
|
+
? h('p', { key: 'nomatch', class: 'lede empty-state' }, 'no matches for "' + state.searchQ + '"')
|
|
605
|
+
: null,
|
|
606
|
+
state.searchQ && (searching || state.searchBusy)
|
|
607
|
+
? Btn({ key: 'clearq', onClick: () => { state.searchQ = ''; state.searchHits = null; state.searchBusy = false; render(); }, children: '× clear search' })
|
|
468
608
|
: null,
|
|
469
609
|
!searching && projects.length > 1
|
|
470
610
|
? h('div', { key: 'projfilter', class: 'pill-row', role: 'group', 'aria-label': 'Filter sessions by project' },
|
|
@@ -485,7 +625,7 @@ function historySide() {
|
|
|
485
625
|
: null,
|
|
486
626
|
],
|
|
487
627
|
}),
|
|
488
|
-
];
|
|
628
|
+
].filter(Boolean);
|
|
489
629
|
}
|
|
490
630
|
|
|
491
631
|
// ── settings ───────────────────────────────────────────────────────────────
|
|
@@ -495,14 +635,30 @@ function isValidUrl(s) {
|
|
|
495
635
|
catch { return false; }
|
|
496
636
|
}
|
|
497
637
|
|
|
498
|
-
function saveBackend() {
|
|
499
|
-
if (!isValidUrl(state.backendDraft)) return;
|
|
500
|
-
if (!confirm('Reconnect to new backend? Current session will be lost.')) return;
|
|
638
|
+
async function saveBackend() {
|
|
639
|
+
if (!isValidUrl(state.backendDraft) || state.backendDraft === state.backend) return;
|
|
501
640
|
B.setBackend(state.backendDraft);
|
|
502
641
|
state.backend = state.backendDraft;
|
|
503
642
|
state.health = { status: 'unknown' };
|
|
643
|
+
state.backendStatus = 'connecting';
|
|
504
644
|
render();
|
|
505
|
-
init();
|
|
645
|
+
await init();
|
|
646
|
+
state.backendStatus = state.health.status === 'ok' ? 'ok' : 'failed';
|
|
647
|
+
render();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function healthSummary() {
|
|
651
|
+
const hh = state.health || {};
|
|
652
|
+
const ok = hh.status === 'ok';
|
|
653
|
+
const dot = ok ? '●' : (hh.status === 'unknown' ? '◌' : '○');
|
|
654
|
+
const bits = [];
|
|
655
|
+
bits.push(dot + ' ' + (hh.status || 'unknown'));
|
|
656
|
+
if (hh.version) bits.push('v' + hh.version);
|
|
657
|
+
if (typeof hh.agents === 'number') bits.push(hh.agents + ' agents');
|
|
658
|
+
if (typeof hh.activeExecutions === 'number') bits.push(hh.activeExecutions + ' active');
|
|
659
|
+
if (hh.db) bits.push('db ' + (hh.db.ok ? 'ok' : 'down'));
|
|
660
|
+
return h('div', { key: 'hp', class: 'health-summary' + (ok ? ' health-ok' : '') },
|
|
661
|
+
...bits.map((b, i) => h('span', { key: 'hb' + i, class: 'health-chip' }, b)));
|
|
506
662
|
}
|
|
507
663
|
|
|
508
664
|
function settingsMain() {
|
|
@@ -530,36 +686,57 @@ function settingsMain() {
|
|
|
530
686
|
onInput: (v) => { state.backendDraft = v; render(); },
|
|
531
687
|
}),
|
|
532
688
|
!isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, '⚠ Invalid URL format') : null,
|
|
533
|
-
h('p', { key: '
|
|
689
|
+
state.backendStatus === 'connecting' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, '◌ connecting…') : null,
|
|
690
|
+
state.backendStatus === 'ok' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, '● connected') : null,
|
|
691
|
+
state.backendStatus === 'failed' ? h('p', { key: 'bst', class: 'lede field-error', role: 'alert' }, '○ connection failed — check the URL') : null,
|
|
692
|
+
healthSummary(),
|
|
534
693
|
Btn({
|
|
535
694
|
key: 'savebtn',
|
|
536
695
|
type: 'submit',
|
|
537
696
|
primary: true,
|
|
538
|
-
disabled: !isValid,
|
|
697
|
+
disabled: !isValid || state.backendDraft === state.backend || state.backendStatus === 'connecting',
|
|
539
698
|
onClick: (e) => { e.preventDefault(); saveBackend(); },
|
|
540
|
-
children: 'save + reconnect',
|
|
699
|
+
children: state.backendStatus === 'connecting' ? 'connecting…' : 'save + reconnect',
|
|
541
700
|
title: isValid ? 'Save backend URL and reconnect' : 'Fix URL format first',
|
|
542
701
|
}),
|
|
543
702
|
]),
|
|
544
703
|
}),
|
|
545
|
-
|
|
546
|
-
title: 'models',
|
|
547
|
-
children: state.models.length
|
|
548
|
-
? state.models.slice(0, 40).map((m, i) =>
|
|
549
|
-
Row({
|
|
550
|
-
key: 'm' + i,
|
|
551
|
-
rank: String(i + 1).padStart(3, '0'),
|
|
552
|
-
title: m.id,
|
|
553
|
-
sub: m.name ? (m.name + ' · ' + (m.protocol || 'agent')) : (m.protocol || 'agent'),
|
|
554
|
-
rail: m.id === state.selectedModel ? 'green' : 'purple',
|
|
555
|
-
onClick: () => { state.selectedModel = m.id; render(); },
|
|
556
|
-
})
|
|
557
|
-
)
|
|
558
|
-
: h('p', { key: 'none', class: 'lede' }, 'no models loaded'),
|
|
559
|
-
}),
|
|
704
|
+
agentsPanel(),
|
|
560
705
|
];
|
|
561
706
|
}
|
|
562
707
|
|
|
708
|
+
function acpStatusFor(agentId) {
|
|
709
|
+
const acp = Array.isArray(state.health.acp) ? state.health.acp : [];
|
|
710
|
+
return acp.find(a => a.id === agentId) || null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function agentsPanel() {
|
|
714
|
+
const installed = state.agents.filter(a => a.available !== false);
|
|
715
|
+
return Panel({
|
|
716
|
+
title: 'agents · ' + installed.length + '/' + state.agents.length + ' installed',
|
|
717
|
+
children: state.agents.length
|
|
718
|
+
? state.agents.map((a, i) => {
|
|
719
|
+
const acp = acpStatusFor(a.id);
|
|
720
|
+
const avail = a.available !== false;
|
|
721
|
+
const bits = [a.protocol || 'agent'];
|
|
722
|
+
if (!avail) bits.push(a.npxInstallable ? 'runs via npx' : 'not installed');
|
|
723
|
+
if (acp) bits.push(acp.healthy ? 'running·healthy' : (acp.running ? 'running' : 'stopped'));
|
|
724
|
+
if (acp && acp.port) bits.push('port ' + acp.port);
|
|
725
|
+
if (acp && acp.restartCount) bits.push(acp.restartCount + ' restarts');
|
|
726
|
+
return Row({
|
|
727
|
+
key: 'ag' + a.id,
|
|
728
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
729
|
+
title: a.name + (avail ? '' : ' ·'),
|
|
730
|
+
sub: bits.join(' · '),
|
|
731
|
+
rail: a.id === state.selectedAgent ? 'green' : (avail ? 'purple' : 'flame'),
|
|
732
|
+
active: a.id === state.selectedAgent,
|
|
733
|
+
onClick: () => { if (avail || a.npxInstallable) { navTo('chat'); selectAgent(a.id); } },
|
|
734
|
+
});
|
|
735
|
+
})
|
|
736
|
+
: h('p', { key: 'none', class: 'lede' }, 'no agents loaded'),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
563
740
|
// ── data ──────────────────────────────────────────────────────────────────
|
|
564
741
|
async function refreshHistory() {
|
|
565
742
|
try {
|
|
@@ -574,12 +751,17 @@ async function refreshHistory() {
|
|
|
574
751
|
}
|
|
575
752
|
|
|
576
753
|
async function runSearch() {
|
|
577
|
-
|
|
754
|
+
const q = state.searchQ.trim();
|
|
755
|
+
if (!q) { state.searchHits = null; state.searchBusy = false; render(); return; }
|
|
756
|
+
if (q.length < 2) { state.searchHits = null; state.searchBusy = false; render(); return; }
|
|
757
|
+
state.searchBusy = true;
|
|
758
|
+
render();
|
|
578
759
|
try {
|
|
579
|
-
state.searchHits = await B.searchHistory(state.backend,
|
|
580
|
-
render();
|
|
760
|
+
state.searchHits = await B.searchHistory(state.backend, q, 50);
|
|
581
761
|
} catch (e) {
|
|
582
|
-
state.searchHits = { query:
|
|
762
|
+
state.searchHits = { query: q, results: [], error: e.message };
|
|
763
|
+
} finally {
|
|
764
|
+
state.searchBusy = false;
|
|
583
765
|
render();
|
|
584
766
|
}
|
|
585
767
|
}
|
|
@@ -611,10 +793,13 @@ async function init() {
|
|
|
611
793
|
}
|
|
612
794
|
render();
|
|
613
795
|
try {
|
|
614
|
-
state.
|
|
615
|
-
if
|
|
796
|
+
state.agents = await B.listAgents(state.backend);
|
|
797
|
+
// Restore the saved agent if still present; else first available, else first.
|
|
798
|
+
let target = state.agents.find(a => a.id === state.selectedAgent);
|
|
799
|
+
if (!target) target = state.agents.find(a => a.available !== false) || state.agents[0];
|
|
800
|
+
if (target) await selectAgent(target.id);
|
|
616
801
|
render();
|
|
617
|
-
} catch (e) { console.warn('
|
|
802
|
+
} catch (e) { console.warn('agents fetch failed:', e.message); }
|
|
618
803
|
|
|
619
804
|
const initialSid = readHash();
|
|
620
805
|
if (initialSid) {
|
|
@@ -632,6 +817,40 @@ async function init() {
|
|
|
632
817
|
});
|
|
633
818
|
}
|
|
634
819
|
|
|
820
|
+
restoreChat();
|
|
635
821
|
render = mount(document.getElementById('app'), view);
|
|
636
822
|
window.__agentgui = { state, render };
|
|
823
|
+
|
|
824
|
+
// Keyboard shortcuts. 'g' then c/h/s switches tabs; 'n' new chat; '/' focuses
|
|
825
|
+
// search (history) or composer (chat). Ignored while typing in a field.
|
|
826
|
+
let gPending = false;
|
|
827
|
+
function focusComposer() {
|
|
828
|
+
const el = document.querySelector('#agentgui-main textarea, #agentgui-main [contenteditable="true"], #agentgui-main input[type="text"]');
|
|
829
|
+
el?.focus();
|
|
830
|
+
}
|
|
831
|
+
function focusSearch() {
|
|
832
|
+
const el = document.querySelector('#app input[type="search"]');
|
|
833
|
+
el?.focus();
|
|
834
|
+
}
|
|
835
|
+
window.addEventListener('keydown', (e) => {
|
|
836
|
+
const t = e.target;
|
|
837
|
+
const typing = t && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable);
|
|
838
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
839
|
+
if (typing) {
|
|
840
|
+
if (e.key === 'Escape') t.blur();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (gPending) {
|
|
844
|
+
gPending = false;
|
|
845
|
+
if (e.key === 'c') { navTo('chat'); return; }
|
|
846
|
+
if (e.key === 'h') { navTo('history'); return; }
|
|
847
|
+
if (e.key === 's') { navTo('settings'); return; }
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (e.key === 'g') { gPending = true; setTimeout(() => { gPending = false; }, 1000); return; }
|
|
851
|
+
if (e.key === 'n' && state.tab === 'chat') { e.preventDefault(); newChat(); return; }
|
|
852
|
+
if (e.key === '/') { e.preventDefault(); state.tab === 'history' ? focusSearch() : focusComposer(); return; }
|
|
853
|
+
if (e.key === '?') { state.showShortcuts = !state.showShortcuts; render(); return; }
|
|
854
|
+
});
|
|
855
|
+
|
|
637
856
|
init();
|