agentgui 1.0.1005 → 1.0.1007
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 +5 -1
- package/package.json +1 -1
- package/scripts/validate-mutations.mjs +12 -0
- package/site/app/js/app.js +33 -12
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# AgentGUI — Agent Notes
|
|
2
2
|
|
|
3
|
+
## GUI perceived-performance sweep (2026-07-02) — twenty-third run
|
|
4
|
+
|
|
5
|
+
Small, targeted run landing the 22nd-run's deferred perceived-perf findings. `index.html` now `modulepreload`s the marked/dompurify ESM CDN modules (was preconnect-only) so the fetch starts in parallel with the kit/app module graph instead of waiting for `markdown.js`'s dynamic import on the first chat response's escaped-text-to-rendered-markdown swap. Settings server panel shows "checking…" instead of "unknown" for version/uptime/clients/projects-folder during the very first health probe (new `state.healthChecking` flag around `recheckHealth`'s await — "unknown" reads as a permanent broken-server fact, not in-flight loading). Settings agents panel title shows "agents · loading…" instead of the literal "agents · 0/0 installed" while the initial fetch is in flight. Still deferred (its own PRD row, re-scoped not dropped): first-render network-gating — the shell paints nothing between the boot-splash and health-probe completion, needing a render-architecture change (skeleton/optimistic shell) bigger than this run's slice. No kit changes this run. agentgui pushed `6a045ba`, 28/28 tests, 0 console errors.
|
|
6
|
+
|
|
3
7
|
## GUI ux-craft sweep (2026-07-02) — twenty-second run
|
|
4
8
|
|
|
5
9
|
`gui-ux-craft` workflow wf_48cd6bae-6ae (35 agents, 20 confirmed). Kit (`../design`): **accent-on-paper contrast fix** — `var(--accent)` (lime, fill-only per its own token comment) was used directly as TEXT color in 7 sites and as a non-text FILL in 3 more (the live status dot, green session-rail fill), rendering ~1.07:1 contrast on the paper theme (WCAG needs 4.5:1 text / 3:1 non-text). Swapped all 10 to `--accent-ink`; borders/outlines/box-shadows on the same rules stayed on `--accent` (already non-text-passing). Four typography-rhythm fixes: `.ds-dash-title` now matches `.ds-session-title` size (was one step larger despite a comment claiming parity), `ContextPane` Row meta/sub now match the kit-wide Row default size (were one step smaller, `.sub` also dropped an unwanted forced monospace), `.ds-session-group-label` now matches `.panel-head`/`table th` sizing, `.ws-crumb` height now derives from `--app-crumb-h` instead of an unlinked magic number. Session rail's new-chat button and search input now share a 36px desktop height. `.ds-file-act` gained an explicit `:focus-visible` ring (was the only icon/toolbar-button family without one).
|
|
@@ -109,7 +113,7 @@ Shipped the three-column **WorkspaceShell** + **WorkspaceRail** (kit `shell.js`,
|
|
|
109
113
|
|
|
110
114
|
## GUI-predictability sweep closure (2026-06-05) — third maximum-effort run
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
Override-elimination pass (index.html `!important` -> kit defaults) + 19 other PRD rows. Full detail in rs-learn (recall "agentgui 3rd run GUI-predictability sweep closure").
|
|
113
117
|
|
|
114
118
|
## GUI-audit closure (2026-06-04 fan-out) — second maximum-effort sweep
|
|
115
119
|
|
package/package.json
CHANGED
|
@@ -39,6 +39,18 @@ await check('mkdir-ads-colon', 400, await post('/api/mkdir', { dir: ROOT, name:
|
|
|
39
39
|
// one). On a PASSWORD-gated server the auth wall rejects it first (401);
|
|
40
40
|
// open servers hit the CSRF guard (403). Either way the mutation is refused.
|
|
41
41
|
await check('csrf-cross-site-form', process.env.PASSWORD ? 401 : 403, await post('/api/delete', 'path=x', { 'Content-Type': 'application/x-www-form-urlencoded', 'Sec-Fetch-Site': 'cross-site' }));
|
|
42
|
+
// The upload PUT is octet-stream, not a form-postable content-type, but the
|
|
43
|
+
// CSRF guard's stated reasoning (rides the same-origin SPA fetch) only holds
|
|
44
|
+
// if a cross-site PUT is actually rejected the same way POST/DELETE are.
|
|
45
|
+
// Deliberately omits AUTH here (unlike every other case in this file): the
|
|
46
|
+
// guard's `authed` bypass is legitimate for a Bearer token an attacker page
|
|
47
|
+
// cannot read, but including it here would test the wrong thing (a stolen-
|
|
48
|
+
// token scenario, not CSRF) - this asserts the cookie/no-credential path a
|
|
49
|
+
// real cross-site attacker is actually limited to.
|
|
50
|
+
await check('csrf-cross-site-upload', process.env.PASSWORD ? 401 : 403,
|
|
51
|
+
await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(ROOT) + '&name=csrf-probe.txt', {
|
|
52
|
+
method: 'PUT', headers: { 'Sec-Fetch-Site': 'cross-site' }, body: 'x',
|
|
53
|
+
}).then(async r => ({ status: r.status, body: await r.text() })));
|
|
42
54
|
// round trip
|
|
43
55
|
await check('mkdir-ok', 200, await post('/api/mkdir', { dir: ROOT, name: '_wtest' }));
|
|
44
56
|
const up = await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(join(ROOT, '_wtest')) + '&name=up.txt', { method: 'PUT', headers: AUTH, body: 'hello upload' });
|
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, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, Icon, EventList, Spinner, Alert, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker, BreadcrumbPath, EmptyState, FileViewer, FilePreviewPane, FilePreviewCode, FilePreviewText, FilePreviewMedia, ThemeToggle, ContextPane, PromptDialog, ConfirmDialog, DropZone, UploadProgress, FilterPills, SessionMeta, BulkBar, Checkbox, ShortcutList } = C;
|
|
6
|
+
const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, Icon, EventList, Spinner, Alert, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker, BreadcrumbPath, EmptyState, FileViewer, FilePreviewPane, FilePreviewCode, FilePreviewText, FilePreviewMedia, ThemeToggle, ContextPane, PromptDialog, ConfirmDialog, DropZone, UploadProgress, FilterPills, SessionMeta, BulkBar, Checkbox, ShortcutList, FocusTrap } = C;
|
|
7
7
|
|
|
8
8
|
// One duration/bytes vocabulary across every surface: prefer the kit's shared
|
|
9
9
|
// formatters (exported alongside the components), fall back to the local
|
|
@@ -51,7 +51,7 @@ const ARM_RESET_MS = 4000;
|
|
|
51
51
|
|
|
52
52
|
// Full routable param set. Every view-defining piece of state round-trips
|
|
53
53
|
// through the hash so reload and Back/forward restore the exact view.
|
|
54
|
-
const HASH_KEYS = ['tab', 'sid', 'dir', 'file', 'q', 'project', 'section', 'filter'];
|
|
54
|
+
const HASH_KEYS = ['tab', 'sid', 'dir', 'file', 'q', 'project', 'section', 'filter', 'lsort', 'lfilter', 'lerr'];
|
|
55
55
|
function readHash() {
|
|
56
56
|
const hash = location.hash || '';
|
|
57
57
|
const out = {};
|
|
@@ -82,6 +82,12 @@ function buildHash() {
|
|
|
82
82
|
if (state.projectFilter) parts.push('project=' + encodeURIComponent(state.projectFilter));
|
|
83
83
|
}
|
|
84
84
|
if (tab === 'settings' && state.settingsSection) parts.push('section=' + encodeURIComponent(state.settingsSection));
|
|
85
|
+
if (tab === 'live') {
|
|
86
|
+
const lv = state.live || {};
|
|
87
|
+
if (lv.sort && lv.sort !== 'status') parts.push('lsort=' + encodeURIComponent(lv.sort));
|
|
88
|
+
if (lv.filter) parts.push('lfilter=' + encodeURIComponent(lv.filter));
|
|
89
|
+
if (lv.errorsOnly) parts.push('lerr=1');
|
|
90
|
+
}
|
|
85
91
|
return parts.length ? '#' + parts.join('&') : '';
|
|
86
92
|
}
|
|
87
93
|
function writeHash({ push = false } = {}) {
|
|
@@ -602,10 +608,12 @@ function view() {
|
|
|
602
608
|
|
|
603
609
|
const shortcutsHint = state.showShortcuts
|
|
604
610
|
? h('div', { key: 'sc', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Keyboard shortcuts', class: 'ds-alert ds-alert--info shortcuts-dialog' },
|
|
605
|
-
|
|
606
|
-
h('
|
|
607
|
-
|
|
608
|
-
|
|
611
|
+
FocusTrap({ children: [
|
|
612
|
+
h('div', { key: 'sc-head', class: 'ds-alert-head' },
|
|
613
|
+
h('span', { key: 'sc-title', class: 'ds-alert-title' }, 'Keyboard shortcuts'),
|
|
614
|
+
h('button', { key: 'sc-close', type: 'button', class: 'ds-btn', 'aria-label': 'Close keyboard shortcuts', onClick: () => { state.showShortcuts = false; render(); announce('shortcuts closed'); } }, 'close')),
|
|
615
|
+
h('div', { key: 'sc-body', class: 'ds-alert-body' }, ShortcutList({ shortcuts: SHORTCUTS })),
|
|
616
|
+
] }))
|
|
609
617
|
: null;
|
|
610
618
|
const main = h('div', { id: 'agentgui-main', role: 'region', 'aria-label': 'main content', 'data-chat-scroll': '', class: 'agentgui-main agentgui-main-' + state.tab }, [shortcutsHint, ...mainContent()].filter(Boolean));
|
|
611
619
|
|
|
@@ -1467,7 +1475,8 @@ function filesMain() {
|
|
|
1467
1475
|
const LIVE_PREFS_KEY = 'agentgui.live';
|
|
1468
1476
|
const FILES_PREFS_KEY = 'agentgui.files';
|
|
1469
1477
|
function persistLivePrefs() {
|
|
1470
|
-
lsSet(LIVE_PREFS_KEY, JSON.stringify({ sort: state.live.sort || 'status', errorsOnly: !!state.live.errorsOnly }));
|
|
1478
|
+
lsSet(LIVE_PREFS_KEY, JSON.stringify({ sort: state.live.sort || 'status', errorsOnly: !!state.live.errorsOnly, filter: state.live.filter || '' }));
|
|
1479
|
+
if (state.tab === 'live') writeHash();
|
|
1471
1480
|
}
|
|
1472
1481
|
function persistFilesPrefs() {
|
|
1473
1482
|
lsSet(FILES_PREFS_KEY, JSON.stringify({ sort: state.files.sort || 'name', sortDir: state.files.sortDir || 'asc', density: state.files.density || 'list' }));
|
|
@@ -1475,7 +1484,7 @@ function persistFilesPrefs() {
|
|
|
1475
1484
|
function hydratePrefs() {
|
|
1476
1485
|
try {
|
|
1477
1486
|
const lv = JSON.parse(lsGet(LIVE_PREFS_KEY) || 'null');
|
|
1478
|
-
if (lv) { if (lv.sort) state.live.sort = lv.sort; state.live.errorsOnly = !!lv.errorsOnly; }
|
|
1487
|
+
if (lv) { if (lv.sort) state.live.sort = lv.sort; state.live.errorsOnly = !!lv.errorsOnly; if (lv.filter) state.live.filter = lv.filter; }
|
|
1479
1488
|
} catch {}
|
|
1480
1489
|
try {
|
|
1481
1490
|
const fp = JSON.parse(lsGet(FILES_PREFS_KEY) || 'null');
|
|
@@ -1700,7 +1709,7 @@ function liveMain() {
|
|
|
1700
1709
|
streamState,
|
|
1701
1710
|
activeSid: activeCard ? activeCard.sid : state.chat.resumeSid, // W13
|
|
1702
1711
|
sort: { value: sortKey, onChange: (v) => { state.live.sort = v; persistLivePrefs(); render(); } },
|
|
1703
|
-
filter: { value: lv.filter || '', placeholder: 'Filter sessions', onInput: (v) => { state.live.filter = v; render(); } },
|
|
1712
|
+
filter: { value: lv.filter || '', placeholder: 'Filter sessions', onInput: (v) => { state.live.filter = v; persistLivePrefs(); render(); } },
|
|
1704
1713
|
errorsOnly: !!lv.errorsOnly,
|
|
1705
1714
|
onErrorsOnly: (on) => { state.live.errorsOnly = on; persistLivePrefs(); render(); },
|
|
1706
1715
|
selectable: true,
|
|
@@ -2730,10 +2739,13 @@ function historyMain() {
|
|
|
2730
2739
|
rail,
|
|
2731
2740
|
expanded, // disclosure state -> kit Row sets aria-expanded
|
|
2732
2741
|
highlight: q || undefined,
|
|
2733
|
-
|
|
2742
|
+
// Copy is available whether or not the row is expanded - a user
|
|
2743
|
+
// scanning collapsed rows for a specific payload shouldn't have
|
|
2744
|
+
// to expand every row first just to copy one.
|
|
2745
|
+
actions: [{
|
|
2734
2746
|
label: 'copy', title: 'copy event',
|
|
2735
2747
|
onClick: () => copyText(full || raw || ('(' + type + ')'), 'event copied'),
|
|
2736
|
-
}]
|
|
2748
|
+
}],
|
|
2737
2749
|
title: expanded ? (typePrefix + (text || '(' + type + ')')) : (collapsedTitle || typePrefix + '(' + type + ')'),
|
|
2738
2750
|
detail: expanded && e.toolInput ? JSON.stringify(e.toolInput, null, 2) : undefined,
|
|
2739
2751
|
// Guard ts: a missing/zero timestamp renders "Invalid Date" otherwise.
|
|
@@ -3211,7 +3223,11 @@ function agentsPanel() {
|
|
|
3211
3223
|
onClick: usable ? () => { navTo('chat'); selectAgent(a.id); } : undefined,
|
|
3212
3224
|
right: (acp && !acp.healthy)
|
|
3213
3225
|
? [Btn({ key: 'acprestart', onClick: (e) => { e.stopPropagation(); B.restartAcpAgent(state.backend, a.id).then(() => loadAgents()); }, children: 'restart' })]
|
|
3214
|
-
|
|
3226
|
+
// Direct-runner agents (claude-code/agy) have no restart, but a
|
|
3227
|
+
// 'not installed' verdict can go stale if the user installs the
|
|
3228
|
+
// CLI mid-session - give it the same recheck affordance ACP rows
|
|
3229
|
+
// get via 'restart', instead of only static prose.
|
|
3230
|
+
: (!avail ? [Btn({ key: 'agrecheck', onClick: (e) => { e.stopPropagation(); loadAgents(); }, children: 're-check' })] : undefined),
|
|
3215
3231
|
});
|
|
3216
3232
|
})
|
|
3217
3233
|
// The empty array means one of three things; never let an in-flight load
|
|
@@ -3501,6 +3517,11 @@ async function init() {
|
|
|
3501
3517
|
}
|
|
3502
3518
|
if (bootTab === 'history' && hp.q) state.searchQ = hp.q;
|
|
3503
3519
|
if (bootTab === 'history' && hp.project) state.projectFilter = hp.project;
|
|
3520
|
+
if (bootTab === 'live') {
|
|
3521
|
+
if (hp.lsort) state.live.sort = hp.lsort;
|
|
3522
|
+
if (hp.lfilter) state.live.filter = hp.lfilter;
|
|
3523
|
+
if (hp.lerr) state.live.errorsOnly = true;
|
|
3524
|
+
}
|
|
3504
3525
|
navTo(bootTab, { push: false });
|
|
3505
3526
|
if (bootTab === 'history' && state.searchQ.trim().length >= 2) runSearch();
|
|
3506
3527
|
if (bootTab === 'settings' && hp.section) focusSettingsSection(hp.section);
|