agentgui 1.0.1006 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1006",
3
+ "version": "1.0.1007",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -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' });
@@ -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
- h('div', { key: 'sc-head', class: 'ds-alert-head' },
606
- h('span', { key: 'sc-title', class: 'ds-alert-title' }, 'Keyboard shortcuts'),
607
- h('button', { key: 'sc-close', type: 'button', class: 'ds-btn', 'aria-label': 'Close keyboard shortcuts', ref: (el) => { if (el) requestAnimationFrame(() => el.focus()); }, onClick: () => { state.showShortcuts = false; render(); announce('shortcuts closed'); } }, 'close')),
608
- h('div', { key: 'sc-body', class: 'ds-alert-body' }, ShortcutList({ shortcuts: SHORTCUTS })))
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
- actions: expanded ? [{
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
- }] : undefined,
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
- : undefined,
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);