agentgui 1.0.937 → 1.0.939
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/skills/gm-skill/SKILL.md +25 -0
- package/.agents/skills/gm-skill/index.js +21 -0
- package/UX_OPTIMIZATION_SUMMARY.md +238 -0
- package/package.json +1 -1
- package/site/app/index.html +158 -4
- package/site/app/js/app.js +140 -67
- package/site/app/vendor/anentrypoint-design/247420.css +5216 -0
- package/site/app/vendor/anentrypoint-design/247420.js +247 -0
- package/skills-lock.json +11 -0
package/site/app/js/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList, Spinner, Alert } = C;
|
|
7
7
|
|
|
8
8
|
const state = {
|
|
9
9
|
backend: B.getBackend(),
|
|
@@ -50,6 +50,36 @@ function scheduleRender() {
|
|
|
50
50
|
requestAnimationFrame(() => { renderScheduled = false; render(); });
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function isNarrow() { return typeof window !== 'undefined' && window.innerWidth < 768; }
|
|
54
|
+
function truncate(str, mobileLen, desktopLen) {
|
|
55
|
+
const s = String(str ?? '');
|
|
56
|
+
const max = isNarrow() ? mobileLen : desktopLen;
|
|
57
|
+
return s.length > max ? s.slice(0, max) + '…' : s;
|
|
58
|
+
}
|
|
59
|
+
function debounce(fn, ms) {
|
|
60
|
+
let t;
|
|
61
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pillButton(key, label, active, title, onClick) {
|
|
65
|
+
return h('button', {
|
|
66
|
+
key,
|
|
67
|
+
type: 'button',
|
|
68
|
+
class: 'pill lede' + (active ? ' pill-active' : ''),
|
|
69
|
+
title,
|
|
70
|
+
'aria-pressed': active ? 'true' : 'false',
|
|
71
|
+
onClick,
|
|
72
|
+
}, label);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scrollChatToBottom() {
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
const el = document.querySelector('#agentgui-main') || document.querySelector('[role="main"]') || document.getElementById('app');
|
|
78
|
+
const scroller = el?.querySelector('[data-chat-scroll]') || el;
|
|
79
|
+
if (scroller) scroller.scrollTop = scroller.scrollHeight;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
function timeNow() {
|
|
54
84
|
const d = new Date();
|
|
55
85
|
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
@@ -130,7 +160,8 @@ function view() {
|
|
|
130
160
|
? '◌ ' + state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
|
|
131
161
|
: (liveActive ? '● live · ' + state.live.eventCount : (state.live.connected ? '● live' : '◌ connecting…')))
|
|
132
162
|
: (ok ? (state.health.ws === 'reconnecting' ? '◌ ws reconnecting' : '● connected') : '○ offline');
|
|
133
|
-
const
|
|
163
|
+
const dotLive = state.tab === 'history' ? (liveActive || state.live.connected) : ok;
|
|
164
|
+
const dot = h('span', { key: 'dot', class: 'status-dot' + (dotLive ? ' status-dot-live' : ''), role: 'status', 'aria-live': 'polite' }, dotText);
|
|
134
165
|
|
|
135
166
|
const topbar = Topbar({
|
|
136
167
|
brand: 'agentgui',
|
|
@@ -141,50 +172,45 @@ function view() {
|
|
|
141
172
|
});
|
|
142
173
|
|
|
143
174
|
const crumbRight = state.tab === 'chat'
|
|
144
|
-
? [
|
|
175
|
+
? [h('div', { key: 'cc', class: 'chat-controls' },
|
|
145
176
|
Select({
|
|
146
177
|
key: 'modelsel',
|
|
147
178
|
value: state.selectedModel,
|
|
148
179
|
placeholder: '— model —',
|
|
180
|
+
title: 'Select AI model',
|
|
149
181
|
options: state.models.map(m => ({ value: m.id, label: m.id })),
|
|
150
182
|
onChange: (v) => { state.selectedModel = v; render(); },
|
|
151
183
|
}),
|
|
152
184
|
state.chat.busy
|
|
153
|
-
? Btn({ key: 'stop', onClick: cancelChat, children: '◼ stop' })
|
|
154
|
-
: Btn({ key: 'new', onClick: newChat, children: '+ new' }),
|
|
185
|
+
? Btn({ key: 'stop', onClick: cancelChat, children: '◼ stop', title: 'Stop streaming' })
|
|
186
|
+
: Btn({ key: 'new', onClick: newChat, children: '+ new', title: 'Start new chat (clears history)' }),
|
|
155
187
|
dot,
|
|
156
|
-
]
|
|
188
|
+
)]
|
|
157
189
|
: [dot];
|
|
158
190
|
|
|
191
|
+
// Topbar already shows "agentgui / <tab>"; the crumb is reserved for contextual
|
|
192
|
+
// controls (model picker, new/stop, live status) so it doesn't duplicate the path.
|
|
159
193
|
const crumb = Crumb({
|
|
160
|
-
trail: [
|
|
161
|
-
leaf:
|
|
194
|
+
trail: [],
|
|
195
|
+
leaf: '',
|
|
162
196
|
right: crumbRight,
|
|
163
197
|
});
|
|
164
198
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
group: 'navigate',
|
|
169
|
-
items: [
|
|
170
|
-
{ glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
|
|
171
|
-
onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
172
|
-
{ glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
|
|
173
|
-
onClick: (e) => { e.preventDefault(); navTo('history'); } },
|
|
174
|
-
{ glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
|
|
175
|
-
onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
176
|
-
],
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
});
|
|
180
|
-
const side = state.tab === 'history' ? historySide() : navSide;
|
|
199
|
+
// Sidebar is contextual: history shows the session list; chat/settings have no
|
|
200
|
+
// sidebar (the topbar already provides primary nav) so main content gets full width.
|
|
201
|
+
const side = state.tab === 'history' ? historySide() : null;
|
|
181
202
|
|
|
182
203
|
const status = Status({
|
|
183
|
-
left: [state.backend, ok ? '● live' : '○ offline'],
|
|
204
|
+
left: [state.backend || 'same-origin', ok ? '● live' : '○ offline'],
|
|
184
205
|
right: [state.selectedModel ? '⌘ ' + state.selectedModel : '○ no model'],
|
|
185
206
|
});
|
|
186
207
|
|
|
187
|
-
|
|
208
|
+
const mainStyle = state.tab === 'chat'
|
|
209
|
+
? 'min-height:0;height:100%;display:flex;flex-direction:column'
|
|
210
|
+
: 'min-height:0;height:100%;overflow:auto';
|
|
211
|
+
const main = h('div', { id: 'agentgui-main', role: 'main', 'data-chat-scroll': '', class: 'agentgui-main agentgui-main-' + state.tab, style: mainStyle }, mainContent());
|
|
212
|
+
// settings reads better centered in a measure; chat + history use full width.
|
|
213
|
+
return AppShell({ topbar, crumb, side, main, status, narrow: state.tab === 'settings' });
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
function mainContent() {
|
|
@@ -201,7 +227,7 @@ function chatMain() {
|
|
|
201
227
|
const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
|
|
202
228
|
const isEmptyStreaming = isStreaming && !m.content;
|
|
203
229
|
return {
|
|
204
|
-
key: String(i),
|
|
230
|
+
key: m.id || String(i),
|
|
205
231
|
who: isAssistant ? 'them' : 'you',
|
|
206
232
|
name: isAssistant ? (state.selectedModel || 'agent') : 'you',
|
|
207
233
|
time: m.time || '',
|
|
@@ -221,7 +247,7 @@ function chatMain() {
|
|
|
221
247
|
});
|
|
222
248
|
|
|
223
249
|
const resumeBanner = state.chat.resumeSid
|
|
224
|
-
? h('div', { key: 'rb',
|
|
250
|
+
? h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
|
|
225
251
|
h('span', { class: 'lede' }, '▶ resuming session ' + state.chat.resumeSid.slice(0, 8) + '… via claude --resume'),
|
|
226
252
|
Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; render(); }, children: '× clear' }))
|
|
227
253
|
: null;
|
|
@@ -229,7 +255,7 @@ function chatMain() {
|
|
|
229
255
|
resumeBanner,
|
|
230
256
|
Chat({
|
|
231
257
|
title: (state.selectedModel || 'agent') + (state.chat.resumeSid ? ' · resume' : ''),
|
|
232
|
-
sub: state.chat.busy ? 'streaming…' :
|
|
258
|
+
sub: state.chat.busy ? 'streaming…' : undefined,
|
|
233
259
|
messages: msgs,
|
|
234
260
|
composer,
|
|
235
261
|
}),
|
|
@@ -237,6 +263,7 @@ function chatMain() {
|
|
|
237
263
|
}
|
|
238
264
|
|
|
239
265
|
function newChat() {
|
|
266
|
+
if (!confirm('Clear chat history? This cannot be undone.')) return;
|
|
240
267
|
state.chat.abort?.abort();
|
|
241
268
|
state.chat = { messages: [], busy: false, abort: null, draft: '', resumeSid: null };
|
|
242
269
|
render();
|
|
@@ -248,13 +275,15 @@ async function sendChat() {
|
|
|
248
275
|
const text = (state.chat.draft || '').trim();
|
|
249
276
|
if (!text || !state.selectedModel || state.chat.busy) return;
|
|
250
277
|
const t = timeNow();
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
const userMsg = { id: 'u' + Date.now(), role: 'user', content: text, time: t };
|
|
279
|
+
const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: t };
|
|
280
|
+
state.chat.messages = [...state.chat.messages, userMsg, curMsg];
|
|
253
281
|
state.chat.draft = '';
|
|
254
282
|
state.chat.busy = true;
|
|
255
283
|
const ctrl = new AbortController();
|
|
256
284
|
state.chat.abort = ctrl;
|
|
257
285
|
render();
|
|
286
|
+
scrollChatToBottom();
|
|
258
287
|
const cur = state.chat.messages[state.chat.messages.length - 1];
|
|
259
288
|
try {
|
|
260
289
|
for await (const ev of B.streamChat(state.backend, {
|
|
@@ -263,7 +292,7 @@ async function sendChat() {
|
|
|
263
292
|
signal: ctrl.signal,
|
|
264
293
|
resumeSid: state.chat.resumeSid || undefined,
|
|
265
294
|
})) {
|
|
266
|
-
if (ev.type === 'text') { cur.content += ev.text; render(); }
|
|
295
|
+
if (ev.type === 'text') { cur.content += ev.text; render(); scrollChatToBottom(); }
|
|
267
296
|
if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
268
297
|
}
|
|
269
298
|
} catch (e) {
|
|
@@ -272,6 +301,7 @@ async function sendChat() {
|
|
|
272
301
|
state.chat.busy = false;
|
|
273
302
|
state.chat.abort = null;
|
|
274
303
|
render();
|
|
304
|
+
scrollChatToBottom();
|
|
275
305
|
}
|
|
276
306
|
}
|
|
277
307
|
|
|
@@ -290,17 +320,26 @@ function historyMain() {
|
|
|
290
320
|
: state.selectedSid;
|
|
291
321
|
|
|
292
322
|
const head = PageHeader({
|
|
293
|
-
title: '§ ' + (sess?.title || state.selectedSid
|
|
323
|
+
title: '§ ' + truncate(sess?.title || state.selectedSid, 40, 80),
|
|
294
324
|
lede,
|
|
295
325
|
});
|
|
296
326
|
|
|
297
|
-
|
|
327
|
+
if (!state.selectedSid) {
|
|
328
|
+
return [head, state.live.error ? Alert({
|
|
329
|
+
key: 'err',
|
|
330
|
+
kind: 'error',
|
|
331
|
+
title: 'Connection lost',
|
|
332
|
+
children: [state.live.error, ' — ', Btn({ onClick: openLiveStream, children: 'reconnect', title: 'Reconnect to history stream' })],
|
|
333
|
+
}) : null];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const actions = h('div', { key: 'acts', class: 'history-actions' },
|
|
298
337
|
Btn({ key: 'resume', primary: true, onClick: () => resumeInChat(sess || { sid: state.selectedSid }), children: '▶ open in chat' }),
|
|
299
338
|
Btn({ key: 'copy', onClick: () => { try { navigator.clipboard.writeText(state.selectedSid); } catch {} }, children: '⎘ copy sid' }),
|
|
300
339
|
);
|
|
301
340
|
|
|
302
341
|
if (state.events.length === 0) {
|
|
303
|
-
return [head, actions, Panel({ title: 'events', children: h('
|
|
342
|
+
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…') })];
|
|
304
343
|
}
|
|
305
344
|
|
|
306
345
|
return [
|
|
@@ -390,44 +429,38 @@ function historySide() {
|
|
|
390
429
|
const projects = uniqueProjects();
|
|
391
430
|
|
|
392
431
|
return [
|
|
393
|
-
Side({
|
|
394
|
-
sections: [
|
|
395
|
-
{
|
|
396
|
-
group: 'navigate',
|
|
397
|
-
items: [
|
|
398
|
-
{ glyph: '▣', label: 'chat', key: 'chat', onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
399
|
-
{ glyph: '§', label: 'history', key: 'history', active: true },
|
|
400
|
-
{ glyph: '⌘', label: 'settings', key: 'settings', onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
401
|
-
],
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
}),
|
|
405
432
|
Panel({
|
|
406
|
-
title: searching
|
|
433
|
+
title: searching
|
|
434
|
+
? 'matches · ' + (state.searchHits.results?.length || 0)
|
|
435
|
+
: ('sessions · ' + sessionsView.length + (subagentCount && !state.showSubagents ? ' (+'+subagentCount+' sub)' : '')),
|
|
407
436
|
children: [
|
|
408
437
|
SearchInput({
|
|
409
438
|
key: 'searchInput',
|
|
410
439
|
placeholder: 'search sessions…',
|
|
440
|
+
'aria-label': 'Search sessions by text or project',
|
|
411
441
|
value: state.searchQ,
|
|
412
|
-
onInput: (v) => { state.searchQ = v;
|
|
442
|
+
onInput: (v) => { state.searchQ = v; debouncedSearch(); },
|
|
413
443
|
}),
|
|
444
|
+
searching && state.searchHits.error
|
|
445
|
+
? Alert({ key: 'searcherr', kind: 'error', title: 'Search failed', children: state.searchHits.error })
|
|
446
|
+
: null,
|
|
414
447
|
state.searchQ && searching
|
|
415
448
|
? Btn({ key: 'clearq', onClick: () => { state.searchQ = ''; state.searchHits = null; render(); }, children: '× clear search' })
|
|
416
449
|
: null,
|
|
417
450
|
!searching && projects.length > 1
|
|
418
|
-
? h('div', { key: 'projfilter',
|
|
419
|
-
|
|
451
|
+
? h('div', { key: 'projfilter', class: 'pill-row', role: 'group', 'aria-label': 'Filter sessions by project' },
|
|
452
|
+
pillButton('allp', 'all', !state.projectFilter, 'Show all projects', () => { state.projectFilter = ''; render(); }),
|
|
420
453
|
...projects.slice(0, 8).map(([name, count]) =>
|
|
421
|
-
|
|
454
|
+
pillButton('p'+name, truncate(name, 14, 20) + ' (' + count + ')', state.projectFilter === name, name, () => { state.projectFilter = state.projectFilter === name ? '' : name; render(); })))
|
|
422
455
|
: null,
|
|
423
456
|
!searching && subagentCount
|
|
424
|
-
? h('label', { key: 'subtog', class: 'lede
|
|
457
|
+
? h('label', { key: 'subtog', class: 'lede subagent-toggle' },
|
|
425
458
|
h('input', { type: 'checkbox', checked: state.showSubagents, onChange: (e) => { state.showSubagents = e.target.checked; render(); } }),
|
|
426
459
|
'show subagents (' + subagentCount + ')')
|
|
427
460
|
: null,
|
|
428
461
|
state.historyError
|
|
429
|
-
? h('p', { key: 'err', class: 'lede' }, '⚠ ' + state.historyError)
|
|
430
|
-
: (rows.length ? h('div', { key: 'rows' }, ...rows) : h('p', { key: 'empty', class: 'lede' }, 'no sessions yet')),
|
|
462
|
+
? h('p', { key: 'err', class: 'lede field-error', role: 'alert' }, '⚠ ' + state.historyError)
|
|
463
|
+
: (rows.length ? h('div', { key: 'rows' }, ...rows) : h('p', { key: 'empty', class: 'lede empty-state' }, 'no sessions yet')),
|
|
431
464
|
!searching && truncatedBy > 0
|
|
432
465
|
? Btn({ key: 'more', onClick: () => { state.sessionsLimit += 60; render(); }, children: '↓ show '+Math.min(60, truncatedBy)+' more ('+truncatedBy+' hidden)' })
|
|
433
466
|
: null,
|
|
@@ -437,8 +470,39 @@ function historySide() {
|
|
|
437
470
|
}
|
|
438
471
|
|
|
439
472
|
// ── settings ───────────────────────────────────────────────────────────────
|
|
473
|
+
function isValidUrl(s) {
|
|
474
|
+
if (!s) return true; // blank = same-origin is valid
|
|
475
|
+
try { new URL(s.startsWith('http') ? s : 'http://' + s); return true; }
|
|
476
|
+
catch { return false; }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function saveBackend() {
|
|
480
|
+
if (!isValidUrl(state.backendDraft)) return;
|
|
481
|
+
if (!confirm('Reconnect to new backend? Current session will be lost.')) return;
|
|
482
|
+
B.setBackend(state.backendDraft);
|
|
483
|
+
state.backend = state.backendDraft;
|
|
484
|
+
state.health = { status: 'unknown' };
|
|
485
|
+
render();
|
|
486
|
+
init();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function healthSummary() {
|
|
490
|
+
const hh = state.health || {};
|
|
491
|
+
const ok = hh.status === 'ok';
|
|
492
|
+
const dot = ok ? '●' : (hh.status === 'unknown' ? '◌' : '○');
|
|
493
|
+
const bits = [];
|
|
494
|
+
bits.push(dot + ' ' + (hh.status || 'unknown'));
|
|
495
|
+
if (hh.version) bits.push('v' + hh.version);
|
|
496
|
+
if (typeof hh.agents === 'number') bits.push(hh.agents + ' agents');
|
|
497
|
+
if (typeof hh.activeExecutions === 'number') bits.push(hh.activeExecutions + ' active');
|
|
498
|
+
if (hh.db) bits.push('db ' + (hh.db.ok ? 'ok' : 'down'));
|
|
499
|
+
return h('div', { key: 'hp', class: 'health-summary' + (ok ? ' health-ok' : '') },
|
|
500
|
+
...bits.map((b, i) => h('span', { key: 'hb' + i, class: 'health-chip' }, b)));
|
|
501
|
+
}
|
|
502
|
+
|
|
440
503
|
function settingsMain() {
|
|
441
504
|
const ok = state.health.status === 'ok';
|
|
505
|
+
const isValid = isValidUrl(state.backendDraft);
|
|
442
506
|
return [
|
|
443
507
|
PageHeader({
|
|
444
508
|
title: '⌘ settings',
|
|
@@ -446,29 +510,32 @@ function settingsMain() {
|
|
|
446
510
|
}),
|
|
447
511
|
Panel({
|
|
448
512
|
title: 'backend',
|
|
449
|
-
children:
|
|
513
|
+
children: h('form', {
|
|
514
|
+
key: 'backendForm',
|
|
515
|
+
onSubmit: (e) => { e.preventDefault(); saveBackend(); },
|
|
516
|
+
}, [
|
|
450
517
|
TextField({
|
|
451
518
|
key: 'backendField',
|
|
452
519
|
label: 'backend url',
|
|
453
520
|
value: state.backendDraft,
|
|
454
521
|
placeholder: '(blank = same origin)',
|
|
522
|
+
'aria-describedby': !isValid ? 'backend-url-error' : undefined,
|
|
523
|
+
'aria-invalid': !isValid ? 'true' : 'false',
|
|
524
|
+
title: isValid ? 'Enter a valid URL or leave blank for same-origin' : 'Invalid URL format',
|
|
455
525
|
onInput: (v) => { state.backendDraft = v; render(); },
|
|
456
526
|
}),
|
|
457
|
-
h('p', { key: '
|
|
527
|
+
!isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, '⚠ Invalid URL format') : null,
|
|
528
|
+
healthSummary(),
|
|
458
529
|
Btn({
|
|
459
530
|
key: 'savebtn',
|
|
531
|
+
type: 'submit',
|
|
460
532
|
primary: true,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
B.setBackend(state.backendDraft);
|
|
464
|
-
state.backend = state.backendDraft;
|
|
465
|
-
state.health = { status: 'unknown' };
|
|
466
|
-
render();
|
|
467
|
-
init();
|
|
468
|
-
},
|
|
533
|
+
disabled: !isValid,
|
|
534
|
+
onClick: (e) => { e.preventDefault(); saveBackend(); },
|
|
469
535
|
children: 'save + reconnect',
|
|
536
|
+
title: isValid ? 'Save backend URL and reconnect' : 'Fix URL format first',
|
|
470
537
|
}),
|
|
471
|
-
],
|
|
538
|
+
]),
|
|
472
539
|
}),
|
|
473
540
|
Panel({
|
|
474
541
|
title: 'models',
|
|
@@ -511,6 +578,7 @@ async function runSearch() {
|
|
|
511
578
|
render();
|
|
512
579
|
}
|
|
513
580
|
}
|
|
581
|
+
const debouncedSearch = debounce(runSearch, 300);
|
|
514
582
|
|
|
515
583
|
async function loadSession(sid) {
|
|
516
584
|
state.selectedSid = sid;
|
|
@@ -519,7 +587,12 @@ async function loadSession(sid) {
|
|
|
519
587
|
render();
|
|
520
588
|
try { state.events = await B.getSessionEvents(state.backend, sid); render(); }
|
|
521
589
|
catch (e) {
|
|
522
|
-
state.events = [{
|
|
590
|
+
state.events = [{
|
|
591
|
+
ts: Date.now(),
|
|
592
|
+
role: 'error',
|
|
593
|
+
type: 'fetch',
|
|
594
|
+
text: 'Failed to load session: ' + e.message + ' — retry via sidebar',
|
|
595
|
+
}];
|
|
523
596
|
render();
|
|
524
597
|
}
|
|
525
598
|
}
|