agentgui 1.0.941 → 1.0.942
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 +4 -0
- package/package.json +1 -1
- package/site/app/index.html +115 -1
- package/site/app/js/app.js +64 -12
package/AGENTS.md
CHANGED
|
@@ -144,3 +144,7 @@ The GUI runs fully offline. `site/app/vendor/cdn/` holds marked, dompurify, pris
|
|
|
144
144
|
- `agents.list` (WS) returns `available` + `npxInstallable` per agent; `agents.models` returns model choices (claude-code → sonnet/opus/haiku). The chat picker is **agent-then-model**, not a flat model list. Unavailable agents are disabled/gated.
|
|
145
145
|
- `chat.sendMessage` accepts `cwd` (defaults to STARTUP_CWD) and `model`/`agentId` separately. `chat.active` (WS) lists in-flight chats with agentId/model/cwd/startedAt/pid; the history tab polls it (3s) and shows a running panel with per-session stop.
|
|
146
146
|
- Client (`app.js`): chat transcript persists to `localStorage[agentgui.chat]` and restores on load; tool_use/result events render as chat parts; keyboard shortcuts (g+c/h/s, n, /, ?); settings has an agents-status panel from `health.acp[]`.
|
|
147
|
+
|
|
148
|
+
## DS CSS cascade — overriding component styles (2026-05-28)
|
|
149
|
+
|
|
150
|
+
**`installStyles()` injects DS CSS into a runtime `<style>` after the head `<style>`, so local overrides need `!important` or higher specificity than the DS's `.ds-247420`-prefixed rules.** Full detail (font vars `--ff-display`/`--ff-mono`, the `.chat-head .sub` "00 msgs" quirk, EventList `.row[role=button]`, `[data-prog-focus]` focus suppression, `projectLabel()` for cwd slugs) is in rs-learn (recall "agentgui GUI styling DS cascade installStyles").
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -21,11 +21,39 @@
|
|
|
21
21
|
height: 100%;
|
|
22
22
|
background: var(--bg, var(--agentgui-bg));
|
|
23
23
|
color: var(--fg, var(--agentgui-fg));
|
|
24
|
-
|
|
24
|
+
/* The DS exposes --ff-display / --ff-mono (there is no --font-sans), so the
|
|
25
|
+
old reference fell through to the system font. Use the DS display face. */
|
|
26
|
+
font-family: var(--ff-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);
|
|
25
27
|
}
|
|
26
28
|
#app { height: 100vh; height: 100dvh; }
|
|
27
29
|
#app > * { height: 100%; }
|
|
28
30
|
|
|
31
|
+
/* Themed thin scrollbars — the native chrome scrollbar is chunky/light and
|
|
32
|
+
clashes with the dark theme on the history sidebar and settings column. */
|
|
33
|
+
* {
|
|
34
|
+
scrollbar-width: thin;
|
|
35
|
+
scrollbar-color: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 22%, transparent) transparent;
|
|
36
|
+
}
|
|
37
|
+
*::-webkit-scrollbar { width: 9px; height: 9px; }
|
|
38
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
39
|
+
*::-webkit-scrollbar-thumb {
|
|
40
|
+
background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 20%, transparent);
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
border: 2px solid transparent;
|
|
43
|
+
background-clip: padding-box;
|
|
44
|
+
}
|
|
45
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
46
|
+
background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 34%, transparent);
|
|
47
|
+
background-clip: padding-box;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* We move focus to the page heading on tab change for AT users; suppress the
|
|
51
|
+
resulting green outline box on those programmatically-focused elements.
|
|
52
|
+
!important + .ds-247420 prefix to beat the runtime-injected DS focus rule. */
|
|
53
|
+
.ds-247420 [data-prog-focus]:focus,
|
|
54
|
+
.ds-247420 [data-prog-focus]:focus-visible,
|
|
55
|
+
[data-prog-focus]:focus, [data-prog-focus]:focus-visible { outline: none !important; box-shadow: none !important; }
|
|
56
|
+
|
|
29
57
|
/* skip link for keyboard/AT users */
|
|
30
58
|
.skip-link {
|
|
31
59
|
position: absolute; left: -9999px; top: 0; z-index: 1000;
|
|
@@ -127,6 +155,92 @@
|
|
|
127
155
|
outline: 2px solid var(--accent, var(--agentgui-accent)); outline-offset: 2px;
|
|
128
156
|
}
|
|
129
157
|
|
|
158
|
+
/* Topbar nav: the DS renders the active tab as a large filled green pill that
|
|
159
|
+
sits taller than the inactive text links and reads as misaligned. Make all
|
|
160
|
+
tabs consistent — equal padding, the active one a subtle tinted underline-pill
|
|
161
|
+
rather than an oversized oval. */
|
|
162
|
+
/* Prefix with .ds-247420 (the <html> class) to match the DS selector's
|
|
163
|
+
specificity; source order then lets these win. */
|
|
164
|
+
.ds-247420 .app-topbar nav { display: flex; align-items: center; gap: .15em; }
|
|
165
|
+
.ds-247420 .app-topbar nav a {
|
|
166
|
+
padding: .35em .75em; border-radius: 8px; line-height: 1.4;
|
|
167
|
+
color: var(--fg-2, color-mix(in srgb, var(--fg, var(--agentgui-fg)) 75%, transparent));
|
|
168
|
+
text-decoration: none; background: transparent;
|
|
169
|
+
transition: background-color .15s ease, color .15s ease;
|
|
170
|
+
}
|
|
171
|
+
.ds-247420 .app-topbar nav a:hover { background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 8%, transparent); color: var(--fg, var(--agentgui-fg)); }
|
|
172
|
+
/* installStyles() injects the DS CSS into a runtime <style> appended after
|
|
173
|
+
this block, so equal-specificity rules lose on source order; !important is
|
|
174
|
+
the targeted override for the few props we reshape on the active tab. */
|
|
175
|
+
.ds-247420 .app-topbar nav a.active {
|
|
176
|
+
background: color-mix(in srgb, var(--accent, var(--agentgui-accent)) 16%, transparent) !important;
|
|
177
|
+
color: var(--accent, var(--agentgui-accent)) !important;
|
|
178
|
+
box-shadow: inset 0 -2px 0 0 var(--accent, var(--agentgui-accent)) !important;
|
|
179
|
+
border-radius: 8px !important;
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Compact working-directory bar (replaces the full-width tall banner box). */
|
|
184
|
+
.cwd-bar {
|
|
185
|
+
display: flex; align-items: center; gap: .5em; flex-wrap: wrap;
|
|
186
|
+
padding: .15em 0 .5em; font-size: .85rem;
|
|
187
|
+
}
|
|
188
|
+
.cwd-bar-text {
|
|
189
|
+
font-family: var(--ff-mono, ui-monospace, monospace);
|
|
190
|
+
color: var(--fg-3, #999); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60vw;
|
|
191
|
+
}
|
|
192
|
+
.cwd-bar-btn {
|
|
193
|
+
cursor: pointer; font: inherit; line-height: 1.3;
|
|
194
|
+
padding: .15em .55em; border-radius: 6px;
|
|
195
|
+
background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 8%, transparent);
|
|
196
|
+
border: 1px solid color-mix(in srgb, var(--fg, var(--agentgui-fg)) 14%, transparent);
|
|
197
|
+
color: var(--fg-2, var(--agentgui-fg));
|
|
198
|
+
}
|
|
199
|
+
.cwd-bar-btn:hover { background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 14%, transparent); }
|
|
200
|
+
.cwd-bar-btn:focus-visible { outline: 2px solid var(--accent, var(--agentgui-accent)); outline-offset: 2px; }
|
|
201
|
+
|
|
202
|
+
/* History no-session empty state: fill the void with a centered prompt. */
|
|
203
|
+
.history-empty {
|
|
204
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
205
|
+
gap: .4em; text-align: center; min-height: 50vh; color: var(--fg-3, #888);
|
|
206
|
+
}
|
|
207
|
+
.history-empty-glyph { font-size: 3rem; opacity: .25; line-height: 1; }
|
|
208
|
+
.history-empty-title { margin: 0; font-size: 1.05rem; color: var(--fg-2, var(--agentgui-fg)); }
|
|
209
|
+
.history-empty-sub { margin: 0; max-width: 42ch; }
|
|
210
|
+
|
|
211
|
+
/* Settings: two-column on wide screens (backend + agents) so it uses the
|
|
212
|
+
full width instead of a cramped centered measure with empty margins. */
|
|
213
|
+
.settings-grid {
|
|
214
|
+
display: grid; gap: var(--space-4, 16px);
|
|
215
|
+
grid-template-columns: minmax(0, 1fr);
|
|
216
|
+
align-items: start;
|
|
217
|
+
}
|
|
218
|
+
@media (min-width: 900px) {
|
|
219
|
+
.settings-grid { grid-template-columns: minmax(0, 420px) minmax(0, 1fr); }
|
|
220
|
+
}
|
|
221
|
+
/* The DS PageHeader carries large vertical margins that leave a big empty
|
|
222
|
+
band above the heading and between the lede and the panels. Neutralize
|
|
223
|
+
them so settings/history read as normal top-aligned scrolling pages. */
|
|
224
|
+
.agentgui-main [class*="page-header"],
|
|
225
|
+
.agentgui-main .ds-page-header { margin-top: 0 !important; margin-bottom: var(--space-4, 16px) !important; }
|
|
226
|
+
.agentgui-main > :first-child { margin-top: 0 !important; }
|
|
227
|
+
.agentgui-main-settings .settings-grid { margin-top: 0; }
|
|
228
|
+
|
|
229
|
+
/* The DS Chat head computes its own zero-padded count ("00 msgs") and ignores
|
|
230
|
+
our sub prop; it reads as a bug. Hide the DS sub — streaming state shows via
|
|
231
|
+
the title and the busy banner. Also hide the DS's empty decorative head dot. */
|
|
232
|
+
.chat-head .sub { display: none; }
|
|
233
|
+
.chat-head .dot { display: none; }
|
|
234
|
+
|
|
235
|
+
/* EventList rows are role=button (click to expand) but the DS doesn't give
|
|
236
|
+
them a pointer cursor, so the affordance is invisible. */
|
|
237
|
+
.ds-event-list .row[role="button"] { cursor: pointer; }
|
|
238
|
+
.ds-event-list .row[role="button"]:hover { background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 5%, transparent); }
|
|
239
|
+
|
|
240
|
+
/* Chat composer: hide the idle scrollbar on the (empty/short) textarea. */
|
|
241
|
+
.chat-composer textarea { overflow-y: auto; scrollbar-width: thin; }
|
|
242
|
+
.chat-composer textarea:not(:focus) { overflow-y: hidden; }
|
|
243
|
+
|
|
130
244
|
/* touch targets on small screens */
|
|
131
245
|
@media (max-width: 640px) {
|
|
132
246
|
.pill { min-height: 36px; padding: .4em .8em; }
|
package/site/app/js/app.js
CHANGED
|
@@ -150,12 +150,31 @@ function navTo(tab) {
|
|
|
150
150
|
render();
|
|
151
151
|
// Move focus into the new region for keyboard/AT users.
|
|
152
152
|
requestAnimationFrame(() => {
|
|
153
|
+
syncAriaCurrent();
|
|
153
154
|
const region = document.querySelector('#agentgui-main');
|
|
154
155
|
if (!region) return;
|
|
155
156
|
const heading = region.querySelector('h1, h2');
|
|
156
157
|
const target = heading || region;
|
|
157
158
|
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
|
|
159
|
+
// Mark as programmatically focused so CSS can suppress the focus ring — we
|
|
160
|
+
// move focus here for AT, but a visible green outline box around the heading
|
|
161
|
+
// reads as an accidental border to sighted users.
|
|
162
|
+
target.setAttribute('data-prog-focus', '');
|
|
158
163
|
try { target.focus({ preventScroll: true }); } catch {}
|
|
164
|
+
const clear = () => { target.removeAttribute('data-prog-focus'); target.removeEventListener('blur', clear); };
|
|
165
|
+
target.addEventListener('blur', clear);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The DS Topbar derives aria-current from href↔location.hash matching, which
|
|
170
|
+
// drifts from our hash-based active tab (e.g. aria-current lands on "settings"
|
|
171
|
+
// while we're on "chat"). Re-assert aria-current on the actually-active tab.
|
|
172
|
+
function syncAriaCurrent() {
|
|
173
|
+
const links = document.querySelectorAll('.app-topbar nav a');
|
|
174
|
+
links.forEach((a) => {
|
|
175
|
+
const isActive = a.classList.contains('active');
|
|
176
|
+
if (isActive) a.setAttribute('aria-current', 'page');
|
|
177
|
+
else a.removeAttribute('aria-current');
|
|
159
178
|
});
|
|
160
179
|
}
|
|
161
180
|
|
|
@@ -255,8 +274,10 @@ function view() {
|
|
|
255
274
|
const glyphMatch = dotText.match(/^([●◌○])\s*(.*)$/);
|
|
256
275
|
const dotGlyph = glyphMatch ? glyphMatch[1] : '';
|
|
257
276
|
const dotLabel = glyphMatch ? glyphMatch[2] : dotText;
|
|
277
|
+
// When live, the CSS .status-dot-live::before draws the (pulsing) dot, so the
|
|
278
|
+
// literal glyph would render a second dot — only emit the glyph when NOT live.
|
|
258
279
|
const dot = h('span', { key: 'dot', class: 'status-dot' + (dotLive ? ' status-dot-live' : ''), role: 'status', 'aria-live': 'polite' },
|
|
259
|
-
dotGlyph ? h('span', { key: 'dg', 'aria-hidden': 'true' }, dotGlyph + ' ') : null,
|
|
280
|
+
(!dotLive && dotGlyph) ? h('span', { key: 'dg', 'aria-hidden': 'true' }, dotGlyph + ' ') : null,
|
|
260
281
|
h('span', { key: 'dl' }, dotLabel));
|
|
261
282
|
|
|
262
283
|
const topbar = Topbar({
|
|
@@ -331,8 +352,7 @@ function view() {
|
|
|
331
352
|
children: 'g then c/h/s — chat/history/settings · n — new chat · / — focus search/composer · ? — toggle this · Esc — blur field' })
|
|
332
353
|
: null;
|
|
333
354
|
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));
|
|
334
|
-
|
|
335
|
-
return AppShell({ topbar, crumb, side, main, status, narrow: state.tab === 'settings' });
|
|
355
|
+
return AppShell({ topbar, crumb, side, main, status, narrow: false });
|
|
336
356
|
}
|
|
337
357
|
|
|
338
358
|
function mainContent() {
|
|
@@ -434,7 +454,14 @@ function chatMain() {
|
|
|
434
454
|
...banners,
|
|
435
455
|
Chat({
|
|
436
456
|
title: agentName + (state.selectedModel ? ' · ' + state.selectedModel : '') + (state.chat.resumeSid ? ' · resume' : ''),
|
|
437
|
-
sub
|
|
457
|
+
// Explicit human-readable sub; the DS default ("NN msgs", zero-padded)
|
|
458
|
+
// leaks an event-list style into chat. Hide it when there are no messages
|
|
459
|
+
// (the empty-state already says "no messages yet").
|
|
460
|
+
sub: state.chat.busy
|
|
461
|
+
? 'streaming…'
|
|
462
|
+
: (state.chat.messages.length
|
|
463
|
+
? state.chat.messages.length + (state.chat.messages.length === 1 ? ' message' : ' messages')
|
|
464
|
+
: ''),
|
|
438
465
|
messages: msgs,
|
|
439
466
|
composer,
|
|
440
467
|
}),
|
|
@@ -459,10 +486,11 @@ function cwdBanner() {
|
|
|
459
486
|
}, children: 'save' }),
|
|
460
487
|
Btn({ key: 'cwdcancel', onClick: () => { state.cwdEditing = false; state.cwdDraft = undefined; render(); }, children: 'cancel' }));
|
|
461
488
|
}
|
|
462
|
-
return h('div', { key: 'cwdb', class: '
|
|
463
|
-
h('span', {
|
|
464
|
-
|
|
465
|
-
|
|
489
|
+
return h('div', { key: 'cwdb', class: 'cwd-bar', role: 'group', 'aria-label': 'Working directory' },
|
|
490
|
+
h('span', { key: 'cwdtxt', class: 'lede cwd-bar-text', title: state.chatCwd || 'server default working directory' },
|
|
491
|
+
state.chatCwd ? '▣ ' + truncate(state.chatCwd, 28, 60) : '▣ cwd: server default'),
|
|
492
|
+
h('button', { key: 'cwdset', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); } }, state.chatCwd ? 'change' : 'set'),
|
|
493
|
+
state.chatCwd ? h('button', { key: 'cwdclr', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); } }, '× default') : null);
|
|
466
494
|
}
|
|
467
495
|
|
|
468
496
|
function newChat() {
|
|
@@ -553,22 +581,31 @@ function reconnectAlert() {
|
|
|
553
581
|
|
|
554
582
|
function historyMain() {
|
|
555
583
|
if (!state.selectedSid) {
|
|
584
|
+
const count = (Array.isArray(state.sessions) ? state.sessions : []).length;
|
|
556
585
|
return [
|
|
557
586
|
reconnectAlert(),
|
|
558
587
|
PageHeader({
|
|
559
588
|
title: '§ history',
|
|
560
589
|
lede: 'pick a session from the sidebar — events stream live from ccsniff /v1/history.',
|
|
561
590
|
}),
|
|
591
|
+
h('div', { key: 'histempty', class: 'history-empty', role: 'status' },
|
|
592
|
+
h('div', { key: 'ge', class: 'history-empty-glyph', 'aria-hidden': 'true' }, '§'),
|
|
593
|
+
h('p', { key: 'gt', class: 'history-empty-title' },
|
|
594
|
+
count ? 'Select a session to view its events' : 'No sessions yet'),
|
|
595
|
+
h('p', { key: 'gs', class: 'lede history-empty-sub' },
|
|
596
|
+
count
|
|
597
|
+
? count + ' session' + (count === 1 ? '' : 's') + ' available · use the search box or press / to filter'
|
|
598
|
+
: 'Start a chat or run a local coding agent — its session will appear here live.')),
|
|
562
599
|
].filter(Boolean);
|
|
563
600
|
}
|
|
564
601
|
|
|
565
602
|
const sess = (Array.isArray(state.sessions) ? state.sessions : []).find(s => s.sid === state.selectedSid);
|
|
566
603
|
const lede = sess
|
|
567
|
-
? (sess.project || sess.cwd || '?') + ' · ' + (sess.events || 0) + ' events · ' + (sess.userTurns || 0) + ' turns · ' + fmtRelTime(sess.last)
|
|
604
|
+
? (projectLabel(sess.project) || sess.cwd || '?') + ' · ' + (sess.events || 0) + ' events · ' + (sess.userTurns || 0) + ' turns · ' + fmtRelTime(sess.last)
|
|
568
605
|
: state.selectedSid;
|
|
569
606
|
|
|
570
607
|
const head = PageHeader({
|
|
571
|
-
title: '§ ' + truncate(sess?.title || state.selectedSid, 40, 80),
|
|
608
|
+
title: '§ ' + truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || state.selectedSid, 40, 80),
|
|
572
609
|
lede,
|
|
573
610
|
});
|
|
574
611
|
|
|
@@ -668,6 +705,16 @@ function visibleSessions() {
|
|
|
668
705
|
return filtered.slice().sort((a, b) => (b.last || 0) - (a.last || 0));
|
|
669
706
|
}
|
|
670
707
|
|
|
708
|
+
// ccsniff derives `project` from the ~/.claude/projects dir name, which encodes
|
|
709
|
+
// the cwd as a dash-joined path (e.g. "-config-workspace-agentgui"). Show the
|
|
710
|
+
// last meaningful segment ("agentgui") rather than the raw slug.
|
|
711
|
+
function projectLabel(project) {
|
|
712
|
+
if (!project) return '';
|
|
713
|
+
if (/[/\\]/.test(project)) return project.split(/[/\\]/).filter(Boolean).pop() || project;
|
|
714
|
+
const segs = project.split('-').filter(Boolean);
|
|
715
|
+
return segs.length ? segs[segs.length - 1] : project;
|
|
716
|
+
}
|
|
717
|
+
|
|
671
718
|
function uniqueProjects() {
|
|
672
719
|
const arr = Array.isArray(state.sessions) ? state.sessions : [];
|
|
673
720
|
const seen = new Map();
|
|
@@ -699,7 +746,7 @@ function historySide() {
|
|
|
699
746
|
Row({
|
|
700
747
|
key: 'sess' + s.sid,
|
|
701
748
|
rank: String(i + 1).padStart(3, '0'),
|
|
702
|
-
title: (s.isSubagent ? '↳ ' : '') + (s.title || s.project || s.sid),
|
|
749
|
+
title: (s.isSubagent ? '↳ ' : '') + (projectLabel(s.title) || projectLabel(s.project) || s.sid),
|
|
703
750
|
sub: fmtRelTime(s.last) + ' · ' + (s.events || 0) + ' ev · ' + (s.tools || 0) + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
|
|
704
751
|
rail: s.errors ? 'flame' : (s.isSubagent ? 'purple' : 'green'),
|
|
705
752
|
active: s.sid === state.selectedSid,
|
|
@@ -755,7 +802,7 @@ function historySide() {
|
|
|
755
802
|
? h('div', { key: 'projfilter', class: 'pill-row', role: 'group', 'aria-label': 'Filter sessions by project' },
|
|
756
803
|
pillButton('allp', 'all', !state.projectFilter, 'Show all projects', () => { state.projectFilter = ''; render(); }),
|
|
757
804
|
...projects.slice(0, 8).map(([name, count]) =>
|
|
758
|
-
pillButton('p'+name, truncate(name, 14, 20) + ' (' + count + ')', state.projectFilter === name, name, () => { state.projectFilter = state.projectFilter === name ? '' : name; render(); })))
|
|
805
|
+
pillButton('p'+name, truncate(projectLabel(name), 14, 20) + ' (' + count + ')', state.projectFilter === name, name, () => { state.projectFilter = state.projectFilter === name ? '' : name; render(); })))
|
|
759
806
|
: null,
|
|
760
807
|
!searching && subagentCount
|
|
761
808
|
? h('label', { key: 'subtog', class: 'lede subagent-toggle' },
|
|
@@ -814,6 +861,7 @@ function settingsMain() {
|
|
|
814
861
|
title: '⌘ settings',
|
|
815
862
|
lede: 'point agentgui at any backend. blank = same-origin (ccsniff in-process). ?backend=… or the field below persists via localStorage.',
|
|
816
863
|
}),
|
|
864
|
+
h('div', { key: 'settings-grid', class: 'settings-grid' }, [
|
|
817
865
|
Panel({
|
|
818
866
|
title: 'backend',
|
|
819
867
|
children: h('form', {
|
|
@@ -847,6 +895,7 @@ function settingsMain() {
|
|
|
847
895
|
]),
|
|
848
896
|
}),
|
|
849
897
|
agentsPanel(),
|
|
898
|
+
]),
|
|
850
899
|
];
|
|
851
900
|
}
|
|
852
901
|
|
|
@@ -914,6 +963,8 @@ async function runSearch() {
|
|
|
914
963
|
const debouncedSearch = debounce(runSearch, 300);
|
|
915
964
|
|
|
916
965
|
async function loadSession(sid) {
|
|
966
|
+
// Guard against a bad sid from a malformed hash (e.g. "?sid=undefined").
|
|
967
|
+
if (!sid || sid === 'undefined' || sid === 'null') { state.selectedSid = null; render(); return; }
|
|
917
968
|
state.selectedSid = sid;
|
|
918
969
|
state.events = [];
|
|
919
970
|
state.eventsLoaded = false;
|
|
@@ -984,6 +1035,7 @@ function registerWsStatusOnce() {
|
|
|
984
1035
|
|
|
985
1036
|
restoreChat();
|
|
986
1037
|
render = mount(document.getElementById('app'), view);
|
|
1038
|
+
requestAnimationFrame(syncAriaCurrent);
|
|
987
1039
|
|
|
988
1040
|
// Re-render on resize so isNarrow()/truncate() reflect the current width
|
|
989
1041
|
// (they read window.innerWidth only at render time).
|