agentgui 1.0.1007 → 1.0.1009

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # AgentGUI — Agent Notes
2
2
 
3
+ ## GUI completion sweep (2026-07-02) — twenty-fourth run
4
+
5
+ `gui-completion` workflow wf_4a731841-832 (33 agents, 17 confirmed). Landed 5 of the smaller-scope findings: Live-tab dashboard state (sort/filter/errors-only) now round-trips through the URL hash (`lsort=`/`lfilter=`/`lerr=1`) and localStorage — previously `lv.filter` wasn't persisted anywhere and the whole dashboard view reverted to defaults on reload. Shortcuts overlay (`?`) now wraps its content in the kit's existing `FocusTrap` component instead of a bare `role=dialog` div — Tab could previously escape into the page behind the overlay. History event rows expose copy even when collapsed (was expanded-only). Settings agents panel gives a not-installed direct-runner agent (claude-code/agy) a "re-check" button, matching the ACP rows' "restart" affordance. **Caught and fixed a real CSRF-guard test gap**: adding the regression test for the upload-PUT route (`scripts/validate-mutations.mjs`), the first draft accidentally included the `Authorization` header — matching every other case in the file but testing the wrong scenario (a stolen-token request, not real cross-site CSRF) — and passed trivially at 200. Corrected to omit auth (the path a real cross-site attacker is actually limited to); the server's existing guard correctly rejects it. **Rule for any CSRF/auth regression test: match the real attacker's capability set exactly — copying a nearby test's header pattern can silently test the wrong threat model.** 12 findings deferred to their own re-scoped PRD row (mid-thread retry/fork, file-management move UX with folder-tree picker, event-level permalinks, rolling context-size indicator — each needs a larger architecture change than a targeted fix). No kit changes this run. Pushed `d3e2d1e`. 28/28 tests, 17/17 validate-mutations, 0 console errors.
6
+
3
7
  ## GUI perceived-performance sweep (2026-07-02) — twenty-third run
4
8
 
5
9
  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.
@@ -75,19 +79,7 @@ Fluid-column + drag-resize density layer (workflow `gui-screen-realestate.js`, 5
75
79
  Visual/layout/motion parity vs claude.ai/code (workflow `gui-claude-code-parity.js`, 66 agents -> 37 gaps). Flat full-width chat turns (`.chat-msg-flat` at `--measure`), single bordered composer rounded-rect, `.chat-tool` status-toned cards, `WorkspaceShell mainFlush`, command-center `SessionDashboard`, reduced-motion-guarded transitions. **Load-bearing webjsx rule: never pass a conditional `x?h():null` positionally — build the array and `.filter(Boolean)` it; ANY null among VElement siblings crashes `applyDiff` (reading 'key'); `Btn` spreads array children.** Full detail in rs-learn (recall "agentgui 10th run parity").
76
80
  ## GUI logic+predictability sweep (2026-06-10) — ninth maximum-effort run
77
81
 
78
- Audits whether everything OPERATES logically and predictably (not coverage, not cohesion). New workflow `.claude/workflows/gui-logic-predictability.js` (6 lenses: interaction-lifecycle, cross-surface-consistency, information-architecture, realtime-truth, input-ergonomics, scale-robustness; hunt -> adversarial verify (kept-typography guard) -> plan). 78 agents -> 67 confirmed findings (`PUNCHLIST-LOGIC.md`). ALL implemented across kit/app/server.
79
-
80
- **Interaction lifecycles.** Kit `ConfirmDialog`/`PromptDialog` gain `error` (inside `.ds-modal-body`, role=alert) + `busy`/`busyLabel` (disables actions AND Escape/backdrop close via `data-busy`); Backdrop's Escape listener is **document-level** (focus can sit outside the dialog) and focus-restore fires only on genuine close (queueMicrotask remount guard) - the app's global Escape ladder ALSO closes `state.files.dialog` as belt-and-braces (a witness caught Escape dead with focus on H1). Per-session stop: `state.live.stopping` Set + kit `session.stopping` ('stopping...', disabled); bulk stops await `Promise.allSettled`, announce 'stopped N of M', partial failures keep their selection. Upload: one drain loop, concurrent drops append, 409 rows get `replace` (overwrite=1) + dismiss (kit `UploadProgress {actions,onDismiss}`). new-chat/backend-change/stop confirms are arm-only with bound values + 4s reset; chips never clobber a typed draft.
81
-
82
- **Realtime truth.** Dashboard joins on `realSid = claudeSessionId || sessionId` - server `chat.active` rows now carry `claudeSessionId`; `streaming_cancelled` is broadcast (an aborted turn never also completes; backend maps it to `{type:'cancelled'}`, app marks the turn 'stopped'); a 60s per-session terminal-frame buffer replays on re-subscribe (`replayedTerminal`) so a turn finishing during a ws drop cannot hang busy; `chat_active_changed` hints (started/ended/cancelled) trigger immediate refreshActive. Error status derives from recency (not cumulative count - a session is not 'error' forever); stale derivation honors any session's running tool (per-sid `toolRunning` off SSE); SSE counter increments dedup by `ev.i` maxI; counters take max(index,tally) and never move backwards; clock-skew clamps to 'just now'; external (non-owned) sessions render as read-only dashboard cards; sorts compare real timestamps with sid tiebreaker; tally pruned 24h/200-LRU. persistChat: 500ms debounce, 100-msg cap, >4KB tool payloads trimmed (`truncatedForStorage`), quota failure banners once, cross-tab `storage` listener with ts guard ('updated in another tab' banner instead of clobber).
83
-
84
- **Input ergonomics.** IME guards (composer, PromptDialog, SearchInput - `isComposing || keyCode===229` never sends). Escape ladder: shortcuts overlay > file dialog > confirmingEdit > new-chat arm > stop-arms > stop generation (and Esc in composer cancels while busy). Window-level dragover/drop preventDefault outside `.ds-dropzone` (a dropped file never navigates the page). Composer: `onPasteFiles`/`onDropFiles` kit props (image paste with no handler -> polite transient note), `.chat-composer-hint` 'Enter to send · Shift+Enter for a new line', context `bits` may carry per-bit `onClick` (cwd is its own target; agent/model inert). `Mod+Shift+L` focuses composer from anywhere (the one chord BEFORE the modifier early-return). cwd validates on blur (debounced `/api/stat` -> kit `cwdError`/`cwdChecking`). Streaming re-render pauses while a selection is held inside the thread (kit `hasSelectionInside` + autoscroll pause).
85
-
86
- **Scale.** `AgentChat {shownMessages,onShowEarlier}` windowed thread (`MESSAGE_CAP=100`, keyed 'show N earlier turns' row); >20k streaming part renders a 4k tail window ('streaming · N KB so far'); history events sliced to last 5000 in-app (server adds a `?limit=N[&before=]` slicing shim in server.js - ccsniff itself has no limit support, do NOT edit node_modules); markdown loader failure no longer latches degraded (30s retry, cache never stores escaped fallback).
87
-
88
- **Consistency/IA.** ONE `fmtDuration` (kit sessions.js) + ONE `fmtBytes` (= fmtFileSize, `0 B` not em-dash) kit-wide with app fallbacks; SessionCard `title` shares the rails' title expression; `session.external` + `session.elapsedMs`; STREAM_WORD `offline`; coarse-pointer always-visible file row actions; idle status never reads 'resuming...' (plain 'continues session <sid8>' in composer context); `/` focuses the files/live filter; running panel links 'open in live'; settings reordered backend -> server -> agents -> appearance -> keyboard -> data; crumb says 'settings' and Files crumb carries the parent trail.
89
-
90
- **Witness.** localhost:3000/gm/ 0 console/page errors across chat/files/live/history/settings; composer hint renders; rename dialog opens + Escape closes + focus restores; 64-row files grid + crumb trail; 420px chat+live no h-scroll; decorative-glyph count 0 (kept middot/ellipsis/dash). Joint risk to re-check after parallel agent runs: prop-name contracts (this run caught app `onPasteImage` vs kit `onPasteFiles`, and backend missing the new `streaming_cancelled` type).
82
+ 78-agent workflow, 67 confirmed findings, all implemented (interaction lifecycles, realtime truth, input ergonomics, scale, consistency). **Escape ladder order: shortcuts overlay > file dialog > confirmingEdit > new-chat arm > stop-arms > stop generation** (still binding - later runs extend the ladder, never reorder it). Full detail in rs-learn (recall "agentgui 9th run logic predictability sweep").
91
83
 
92
84
  ## GUI completion sweep (2026-06-10) — eighth maximum-effort run
93
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1007",
3
+ "version": "1.0.1009",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -1945,11 +1945,12 @@ function chatMain() {
1945
1945
  children: [Spinner({ key: 'trspin', size: 'sm' })] }));
1946
1946
  }
1947
1947
  if (state.chat.confirmingEdit) {
1948
- banners.push(Alert({ key: 'confedit', kind: 'warn', title: 'Edit this message?',
1948
+ const isRetry = state.chat.confirmingEdit.kind === 'retry';
1949
+ banners.push(Alert({ key: 'confedit', kind: 'warn', title: isRetry ? 'Retry this turn?' : 'Edit this message?',
1949
1950
  children: [
1950
- h('span', { key: 'cetext' }, 'Editing will remove the later turns - continue? '),
1951
+ h('span', { key: 'cetext' }, (isRetry ? 'Retrying' : 'Editing') + ' will remove the later turns - continue? '),
1951
1952
  Btn({ key: 'ceno', onClick: cancelEditAndResend, children: 'cancel' }),
1952
- Btn({ key: 'ceyes', danger: true, onClick: confirmEditAndResend, children: 'continue' })] }));
1953
+ Btn({ key: 'ceyes', danger: true, onClick: confirmEditAndResend, children: isRetry ? 'retry' : 'continue' })] }));
1953
1954
  }
1954
1955
  const lastMsg = state.chat.messages.length ? state.chat.messages[state.chat.messages.length - 1] : null;
1955
1956
  const lastErr = lastMsg ? lastMsg.error : null;
@@ -2041,7 +2042,10 @@ function chatMain() {
2041
2042
  // edit-and-resend a user message (two-step: arm a confirm banner first,
2042
2043
  // since the truncation destroys the later turns).
2043
2044
  onCopyMessage: (m) => copyMessageText(m),
2044
- onRetryMessage: () => retryLastTurn(),
2045
+ // Mid-thread retry (gui-completion #7): the kit now offers retry on
2046
+ // EVERY assistant turn, not just the last - pass the specific message
2047
+ // through so retryTurn truncates from the right position.
2048
+ onRetryMessage: (m) => retryTurn(m),
2045
2049
  confirmEdit: true,
2046
2050
  onArmEdit: (m) => armEditAndResend(m),
2047
2051
  onEditMessage: (m) => armEditAndResend(m),
@@ -2334,28 +2338,56 @@ function chatFollowups() {
2334
2338
  return result;
2335
2339
  }
2336
2340
  // Retry the last assistant turn: drop it and re-send the preceding user message.
2337
- function retryLastTurn() {
2338
- if (!canSend() && !state.chat.busy) { /* allow when idle */ }
2341
+ // Retry a turn: truncates the transcript back to (and resends) the user
2342
+ // message that preceded the given assistant message, discarding it and
2343
+ // everything after - the same truncate+resend mechanism edit-and-resend
2344
+ // uses. `m` is optional (falls back to the trailing assistant turn) so this
2345
+ // also serves as "retry last turn" for onRetryMessage on a dangling user
2346
+ // message with no reply yet. Mid-thread retry (any assistant turn, not just
2347
+ // the last) was a gui-completion #7 finding - the kit's per-message retry
2348
+ // action is no longer gated to the last message index for assistant turns.
2349
+ function retryTurn(m) {
2339
2350
  if (state.chat.busy) return;
2340
- // The armed edit confirm refers to indices this retry is about to shift.
2341
- state.chat.confirmingEdit = null;
2342
2351
  const msgs = state.chat.messages;
2343
- // Find the trailing assistant turn and the user message before it.
2344
- let ai = msgs.length - 1;
2345
- while (ai >= 0 && msgs[ai].role !== 'assistant') ai--;
2352
+ let ai = m ? msgs.indexOf(m) : -1;
2353
+ if (ai < 0) {
2354
+ // Fall back to the trailing assistant turn (no specific message given,
2355
+ // or the given message somehow isn't in the current transcript).
2356
+ ai = msgs.length - 1;
2357
+ while (ai >= 0 && msgs[ai].role !== 'assistant') ai--;
2358
+ }
2346
2359
  if (ai < 0) return;
2347
2360
  let ui = ai - 1;
2348
2361
  while (ui >= 0 && msgs[ui].role !== 'user') ui--;
2349
2362
  if (ui < 0) return;
2363
+ // Retrying anything before the trailing turn silently discards every LATER
2364
+ // turn too - the same destructive-truncation risk edit-and-resend already
2365
+ // confirms for, so mid-thread retry gets the identical arm-then-confirm
2366
+ // banner instead of firing immediately.
2367
+ const discardsLaterTurns = ai < msgs.length - 1;
2368
+ if (discardsLaterTurns) {
2369
+ state.chat.confirmingEdit = { idx: ui, text: msgs[ui].content || '', kind: 'retry' };
2370
+ render();
2371
+ return;
2372
+ }
2373
+ state.chat.confirmingEdit = null;
2374
+ executeTruncateAndResend(ui);
2375
+ }
2376
+ function executeTruncateAndResend(ui) {
2377
+ const msgs = state.chat.messages;
2350
2378
  const userText = msgs[ui].content || '';
2351
- // Drop the user+assistant pair (and anything after) and resend the user text.
2352
2379
  state.chat.messages = msgs.slice(0, ui);
2353
2380
  state.chat.totalCost = computeTotalCost(); // discarded turns leave the spend
2381
+ // Never --resume a session whose tail diverged from what the server saw -
2382
+ // a mid-thread retry/edit truncates history the resumed session still has.
2383
+ state.chat.resumeSid = null;
2384
+ state.chat.resumeNote = null;
2354
2385
  state.chat.draft = userText;
2355
2386
  persistChat();
2356
2387
  if (canSend()) sendChat();
2357
2388
  else render();
2358
2389
  }
2390
+ function retryLastTurn() { retryTurn(); }
2359
2391
  // Edit-and-resend a user message: two-step. The edit click ARMS a confirmation
2360
2392
  // (truncating destroys the later turns), the banner's continue executes it.
2361
2393
  function armEditAndResend(m) {
@@ -2369,6 +2401,9 @@ function confirmEditAndResend() {
2369
2401
  const ce = state.chat.confirmingEdit;
2370
2402
  if (!ce || state.chat.busy) return;
2371
2403
  state.chat.confirmingEdit = null;
2404
+ // Retry resends the SAME text immediately (no edit step); edit-and-resend
2405
+ // populates the draft so the user can change it before sending.
2406
+ if (ce.kind === 'retry') { executeTruncateAndResend(ce.idx); return; }
2372
2407
  state.chat.messages = state.chat.messages.slice(0, ce.idx);
2373
2408
  state.chat.totalCost = computeTotalCost(); // discarded turns leave the spend
2374
2409
  // Never --resume a session whose tail diverged from what the server saw.