agentgui 1.0.1008 → 1.0.1010

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
+ ## Mid-thread retry (2026-07-02) — twenty-fifth run
4
+
5
+ Landed the highest-severity finding deferred from the 24th run (gui-completion #7): retry was hard-gated to the LAST message only — a user unhappy with an earlier assistant reply had no way to redo it. Kit `AgentChat` now offers the retry action on every settled assistant turn, not just `i === lastIdx`; `onRetryMessage(m)` passes the specific message. App `retryTurn(m)` truncates+resends from `m`'s preceding user turn (falls back to the trailing assistant turn when called with no argument, preserving the old "retry last" behavior). **Retrying anything before the trailing turn discards every later turn too** — it now arms the same confirm banner edit-and-resend already uses (generalized with a `kind: 'retry'` tag: "Retry this turn? Retrying will remove the later turns") instead of firing immediately; retrying the actual last turn (no data loss) still executes immediately. Caught and fixed a real bug while generalizing: the old `retryLastTurn()` never cleared `state.chat.resumeSid`/`resumeNote` on truncation, unlike `confirmEditAndResend()` which already did per the "never `--resume` a diverged tail" rule — both paths now share `executeTruncateAndResend()` so this can't drift apart again. Kit pushed `834c81d`, agentgui pushed `18cd971`. Browser-witnessed end-to-end (mid-thread retry click -> confirm banner armed with the correct index -> confirm -> truncated to exactly the retried turn -> resent -> resumeSid cleared), 0 console errors, 28/28 tests. **Note:** this run's Deploy GH Pages CI step flaked twice ("Timeout reached" / "Deployment cancelled") before succeeding on a 3rd rerun — an infra flake in the docs-site deploy environment, not a code regression (`npm publish`, the actual consumer artifact, was green from the first attempt every time).
6
+
3
7
  ## GUI completion sweep (2026-07-02) — twenty-fourth run
4
8
 
5
9
  `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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1008",
3
+ "version": "1.0.1010",
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.