agentgui 1.0.1054 → 1.0.1055

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/.gm/prd.yml CHANGED
@@ -3076,3 +3076,6 @@
3076
3076
  - gm-plugkit/playwriter browser verb dom=/capture eval context defect - requires an upstream tool fix outside agentgui/design
3077
3077
  status: completed
3078
3078
  witness: 'External tool defect, not resolvable from this repo/session: the gm-plugkit/playwriter browser verb''s dom= and capture prefixes evaluate in a Node vm context with no page/document binding (ReferenceError on every attempt this session), unlike the screenshot= and url=<target> paths which do reach the real page. This is a defect in the upstream gm-plugkit/playwriter tool package itself, outside agentgui and design repo scope. Recorded per the false-completion rule as blockedBy:external rather than marked done - screenshots remain the working witness fallback and were used for all browser verification in this sweep.'
3079
+ - id: 40th-run-push-ci-green
3080
+ subject: Push kit and agentgui changes, verify CI green on both repos before COMPLETE
3081
+ status: pending
package/AGENTS.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # AgentGUI — Agent Notes
2
2
 
3
+ ## GUI logic+predictability sweep (2026-07-12) — fortieth run
4
+
5
+ `gui-logic-predictability` workflow wf_c0a8d954-547 (20 agents, 196 tool calls, 0 errors) — 11 confirmed findings, all implemented; the lens is still surfacing real gaps at run 40, contrary to the 39th run's thinning-yield note. App (`site/app/js/app.js`, `site/app/js/backend.js`): the global Escape ladder disarmed `confirmingEdit`/`confirmingNewChat`/`confirmingStopAll`/`confirmingStopSelected` but never `confirmingClearData` (settings' arm-then-confirm "clear all local data?" banner) — a keyboard user who armed it and pressed the ASCII-standard cancel gesture this app trains everywhere else got no response; added the matching disarm branch. History detail header title/lede fell back to the raw full `cwd` path instead of `pathBasename(cwd)` like every other headline surface (chat cwd label, files breadcrumb, resume banner) — wrapped both fallback sites; the unabbreviated path stays available via the labeled `SessionMeta` "directory" row's own `title=` tooltip. `wsCall` (the WS RPC helper every `backend.js` call goes through) had no timeout at all — a hung `chat.active`/any other response left its promise pending forever with zero indicator; added a 15s timeout that rejects and clears the pending map, `resolve`/`reject` wrapped to clear the timer on a real reply. agy (Antigravity) has a working `--continue` (most-recent-conversation) resume path server-side (`lib/claude-runner-agents.js`) that the client never triggered — `resumeSid` was hardcoded to only forward for claude-code, so agy always got the flattened-transcript preamble instead of true continuity, AND the "does not resume across turns" banner fired for agy exactly like the genuinely-stateless opencode/kilo/codex, misattributing a missing client wire as an agent limitation; now `resumeSid` forwards `true` for agy once the chat is past its first turn (agy's `--continue` ignores the sid value, boolean-truthy is sufficient) and the banner excludes agy. Pasted/dropped files into the composer only fired a screen-reader-only `announce()` during upload — zero visible feedback for sighted mouse users on progress or failure; now calls the kit's `flashComposerNote` (newly exported) on the real composer element for upload-start/failure/success, and the success announce text now discloses the undo-history caveat inline. Kit (`design/src/components/{chat,content}.js`, `design/app-shell.css`, `design/chat.css`, `design/src/components.js`): `Row`'s primary `.title` span had no `title=` tooltip fallback (only `.sub` got one in the 39th run) — the more commonly-truncated primary label was the bigger gap; fixed at the one shared render site so every `Row` consumer (settings agents panel, etc.) inherits it. `.ds-file-actions` (Files row rename/move/delete icons) were `opacity:0` by default on desktop mouse/trackpad pointers, fully invisible without an accidental hover — raised the resting baseline to `0.35` so the actions are discoverable without hover, full `1` still on hover/focus-within. The Enter-to-send composer hint was CSS-hidden except while focused or carrying a draft (and `aria-hidden`), so a brand-new user looking at an empty, unfocused composer had no in-UI way to learn Enter-vs-Shift+Enter — now visible at rest above 420px (still hidden under 420px to save rows) and no longer `aria-hidden` since it's real content, not decoration. `insertEmoji`'s direct `taEl.value=` assignment discards the native undo stack the same way draft-restore's `.value=` does, but only draft-restore surfaced an explanatory note — added a matching one-time (`sessionStorage`-gated) `flashComposerNote` call on first emoji insert per session. `flashComposerNote` exported from `chat.js` through the `components.js` barrel (33rd-run standing lesson: a kit function must clear the barrel to be consumable by the app). Kit built+tested (`bun test.js` all pass), rebuilt (`node scripts/build.mjs`, 0 lint errors), re-vendored into `site/app/vendor/anentrypoint-design/`. Browser-witnessed live on buildesk via the now-standard `page.context().setHTTPCredentials()` + single-dispatch (creds+goto+wait+evaluate, same session) pattern: `confirmingClearData` Escape-disarm before:true/after:false; composer hint `display:block` with `aria-hidden` absent; `.ds-file-actions` computed `opacity:"0.35"`; settings agents-panel `Row` `.title` elements 16/16 carry a `title=` attribute; deployed `backend.js`/`app.js`/`247420.js` all curl-confirmed to contain the shipped fix strings (`timeoutMs = 15000`, the agy banner-exclusion comment, `pathBasename(sess?.cwd)`, `undoNoteShown`, `ds-file-actions`); 0 pageErrors across every dispatch. **Reconfirmed standing tool defect, worse than previously documented: browser session ids silently rotate across SEPARATE dispatches even against the "same" nominal session, and a dispatch landing on a stale/rotated session reads an entirely blank `document.body` (0 children) despite `window.__agentgui` still resolving — the fix is to always combine credentials-set + goto + wait + evaluate into ONE single dispatch rather than splitting navigation and reading across dispatches.** Also reconfirmed the `capture` prefix's `page.evaluate` return value is NOT propagated back through the tool's `result` field (comes back `undefined` even though the evaluated expression itself runs correctly and its side effects land) — the reliable read pattern is `console.log("WITNESS:" + JSON.stringify(...))` inside the evaluated callback and grep the dispatch's `stdout`/`debug.console`, not the `result` field.
6
+
3
7
  ## GUI logic+predictability sweep (2026-07-12) — thirty-ninth run
4
8
 
5
9
  `gui-logic-predictability` workflow wf_88c431e5-931 (26 agents, 202 tool calls, 0 errors) — 6 confirmed findings after hunt+adversarial-verify, all implemented; a much thinner yield than the 37th/38th runs, consistent with the same lens converging after two straight passes. App (`site/app/js/app.js`): `runningPanel()`'s inline running-chat rows previously showed only agent+model+elapsed+cwd with no session title, reading nameless while the identical session shows a resolved title on the sessions rail/history rail/Live SessionCard — a new `runningRowTitle()` helper (wrapping the existing `resumeSidLabel()`) prepends the same resolved identity string; `stopActiveChat()` could wedge a stop button permanently disabled if the backend accepted the cancel but the underlying process outlived the immediate post-cancel `refreshActive()` call (the next 3s poll would silently re-add the sid to `state.active` while `stopping` never cleared) — a bounded 9s timeout now clears `stopping` and surfaces the same "conversation did not stop - try again" retry banner used on outright rejection; `computeLiveSessions()`'s per-sid SSE tally accumulated `tokens` but never `cost`, so every live/external session showed a blank cost field except the one open in-page in Chat — the tally now accumulates `total_cost_usd` from `result` events too, with the same tally-fallback pattern tokens already had; WorkspaceRail's footer gained a visible shortcuts-help `IconButton` (previously the `?`-overlay was 100% keyboard-only tribal knowledge with zero discovery path outside Settings). Kit (`design/src/components/{chat,content,shell}.js`): `CodeNode.refSink`'s syntax-highlight swap gained the same in-progress-text-selection guard `MdNode.refSink` already had (the two paths had drifted — code blocks still wiped an active selection mid-stream after the markdown path was fixed for the same bug); `Row.sub` gained a `title=` tooltip fallback for string values, matching every other long-text field in the app (agent-chat cwd, sessions rail title/sub/cwd/agent) — `Row` is used everywhere (history sessions rail, search results, context-pane cwd fact, agents panel) so this closes the gap at every call site at once; added a `help` icon path to `ICON_PATHS`/`Glyph` for the new rail affordance. Kit built+tested (`bun test.js` all pass), rebased cleanly on a concurrent `v0.0.316` release-bump commit (no source conflicts), dist rebuilt fresh post-rebase, pushed `00dae4c`. agentgui re-vendored, pushed `64f4466`, all 3 CI checks (Auto-Declaudeify/Publish and Release/Deploy GH Pages) green. Browser-witnessed live on buildesk via `page.context().setHTTPCredentials()` + `page.evaluate()`: help button found in `.ws-rail-foot`, clicking it opened the shortcuts overlay (`shortcutsTextPresent:true`), zero pageErrors; history rail `Row.sub` confirmed rendering `title="server default"` live. **New tool-defect note this run: this env's nginx Basic-Auth password contains a literal comma (`123,slam,123,slam` — see the 32nd-run standing gotcha), and embedding it directly in a `page.goto` URL (`https://user:pass@host/...`) throws `net::ERR_INVALID_AUTH_CREDENTIALS` at the Chromium URL-parser level even when percent-encoded (`%2C`) — this is a stricter failure than the previously-documented "breaks the app's own fetch()" caveat for comma-free creds. Working fix: `await page.context().setHTTPCredentials({username, password})` before `page.goto()` with a plain (no-embedded-creds) URL — this is the durable workaround for any future witness against this deploy, comma-password or not.** Also reconfirmed the 38th-run tool defect (`capture` prefix scripts run in a bare Node vm with no `document`/`page` binding unless the script itself calls `page.evaluate(() => {...})` — a raw top-level `document.querySelector(...)` throws `ReferenceError: document is not defined`; wrapping every DOM read/write in `page.evaluate()` inside the capture script is the reliable pattern, not a tool-side fix).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1054",
3
+ "version": "1.0.1055",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -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, TextField, Select, Btn, Icon, IconButton, 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, AgentListSkeleton } = C;
6
+ const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, TextField, Select, Btn, Icon, IconButton, 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, AgentListSkeleton, flashComposerNote } = 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
@@ -1214,25 +1214,34 @@ async function uploadFiles(fileList) {
1214
1214
  // the same confined endpoint the Files tab uses (into the chat's current cwd),
1215
1215
  // then insert the resulting relative path into the draft. Falls back to an
1216
1216
  // announce() if there's no cwd to upload into (chat hasn't picked one yet).
1217
+ function composerEl() {
1218
+ return document.querySelector('.chat-composer');
1219
+ }
1217
1220
  async function pastedFilesToChat(files) {
1218
1221
  const list = Array.from(files || []);
1219
1222
  if (!list.length) return;
1220
1223
  const dir = state.chatCwd;
1221
1224
  if (!dir) { announce('set a working directory before pasting files'); return; }
1225
+ const el = composerEl();
1226
+ if (flashComposerNote && el) flashComposerNote(el, 'uploading ' + plural(list.length, 'file') + '…');
1222
1227
  const inserted = [];
1223
1228
  for (const f of list) {
1224
1229
  try {
1225
1230
  await B.uploadFile(state.backend, dir, f);
1226
1231
  inserted.push(f.name);
1227
1232
  } catch (e) {
1228
- announce('paste upload failed: ' + fileMutationCopy(e));
1233
+ const msg = 'paste upload failed: ' + fileMutationCopy(e);
1234
+ announce(msg);
1235
+ if (flashComposerNote && el) flashComposerNote(el, msg);
1229
1236
  }
1230
1237
  }
1231
1238
  if (inserted.length) {
1232
1239
  const draft = state.chat.draft || '';
1233
1240
  state.chat.draft = draft + (draft && !draft.endsWith('\n') && !draft.endsWith(' ') ? ' ' : '') + inserted.join(' ');
1234
1241
  debouncedPersistDraft();
1235
- announce('pasted ' + plural(inserted.length, 'file') + ' into ' + pathBasename(dir));
1242
+ const msg = 'pasted ' + plural(inserted.length, 'file') + ' into ' + pathBasename(dir) + ' - undo history does not include this insert';
1243
+ announce(msg);
1244
+ if (flashComposerNote && el) flashComposerNote(el, 'pasted ' + plural(inserted.length, 'file'));
1236
1245
  render();
1237
1246
  }
1238
1247
  }
@@ -2079,10 +2088,13 @@ function chatMain() {
2079
2088
  // silently loses prior context. Warn once the conversation is past its first
2080
2089
  // turn so the user knows this agent is not carrying history forward.
2081
2090
  {
2082
- if (state.selectedAgent && state.selectedAgent !== 'claude-code' && userTurnCount > 1 && !state.chat.resumeSid) {
2091
+ if (state.selectedAgent && state.selectedAgent !== 'claude-code' && state.selectedAgent !== 'agy' && userTurnCount > 1 && !state.chat.resumeSid) {
2083
2092
  banners.push(Alert({ key: 'nocontinuity', kind: 'info', title: (agentById(state.selectedAgent)?.name || state.selectedAgent) + ' does not resume across turns',
2084
2093
  children: 'This agent starts fresh each message - it will not remember earlier turns in this chat. Use Claude Code for a continuous conversation.' }));
2085
2094
  }
2095
+ // agy has a real --continue (most-recent-conversation) path wired via
2096
+ // resumeSid once this chat is past its first turn - it is NOT stateless
2097
+ // like opencode/kilo/codex, so it gets no "does not resume" warning.
2086
2098
  }
2087
2099
  if (state.chat.resumeSid && state.selectedAgent === 'claude-code') {
2088
2100
  // The agent-switched note (if any) lives inside the resume banner rather
@@ -2690,9 +2702,13 @@ async function sendChat(textArg) {
2690
2702
  // readable transcript rather than raw API objects they cannot process.
2691
2703
  messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content || messageToText(m) })),
2692
2704
  signal: ctrl.signal,
2693
- // Only claude-code consumes a resume sid; never forward a stale one to
2694
- // another agent (it makes agy spuriously run --continue).
2695
- resumeSid: (state.selectedAgent === 'claude-code' && state.chat.resumeSid) || undefined,
2705
+ // claude-code consumes a literal --resume <sid>; agy's --continue is a
2706
+ // boolean 'resume most-recent conversation' flag (it ignores the sid
2707
+ // value entirely, see lib/claude-runner-agents.js), so any truthy value
2708
+ // is enough to opt agy into continuity once this chat has a prior turn.
2709
+ resumeSid: state.selectedAgent === 'claude-code'
2710
+ ? (state.chat.resumeSid || undefined)
2711
+ : (state.selectedAgent === 'agy' && state.chat.messages.length > 2 ? true : undefined),
2696
2712
  })) {
2697
2713
  // After a stop, the iterator drains buffered events for a turn the user
2698
2714
  // aborted - applying them would set resumeSid / accrue cost / write text
@@ -2859,13 +2875,13 @@ function historyMain() {
2859
2875
 
2860
2876
  const sess = (Array.isArray(state.sessions) ? state.sessions : []).find(s => s.sid === state.selectedSid);
2861
2877
  const lede = sess
2862
- ? (projectLabel(sess.project) || sess.cwd || 'unknown location') + ' · ' + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ' · ' + fmtRelTime(sess.last)
2878
+ ? (projectLabel(sess.project) || pathBasename(sess.cwd) || 'unknown location') + ' · ' + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ' · ' + fmtRelTime(sess.last)
2863
2879
  : UNTITLED_CONVERSATION;
2864
2880
 
2865
2881
  const head = PageHeader({
2866
2882
  compact: true,
2867
2883
  dense: true,
2868
- title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd || state.selectedSid || UNTITLED_CONVERSATION, 40, 80),
2884
+ title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || pathBasename(sess?.cwd) || state.selectedSid || UNTITLED_CONVERSATION, 40, 80),
2869
2885
  lede,
2870
2886
  });
2871
2887
 
@@ -3999,6 +4015,7 @@ window.addEventListener('keydown', (e) => {
3999
4015
  // backdrop listener covers in-dialog focus; this covers everything else).
4000
4016
  if (state.files.dialog) { if (!state.files.dialog.busy) closeFileDialog(); return; }
4001
4017
  if (state.chat.confirmingEdit) { state.chat.confirmingEdit = null; render(); announce('edit cancelled'); return; }
4018
+ if (state.confirmingClearData) { state.confirmingClearData = false; render(); announce('clear cancelled'); return; }
4002
4019
  if (state.confirmingNewChat) { clearTimeout(_newChatArmTimer); state.confirmingNewChat = false; render(); announce('new chat cancelled'); return; }
4003
4020
  if (state.live.confirmingStopAll || state.live.confirmingStopSelected) {
4004
4021
  state.live.confirmingStopAll = false; state.live.confirmingStopSelected = false;
@@ -351,10 +351,16 @@ function ensureWs(base) {
351
351
  return _wsReady;
352
352
  }
353
353
 
354
- function wsCall(base, method, params) {
354
+ function wsCall(base, method, params, timeoutMs = 15000) {
355
355
  return ensureWs(base).then(() => new Promise((resolve, reject) => {
356
356
  const r = _nextReqId++;
357
- _pending.set(r, { resolve, reject });
357
+ const timer = setTimeout(() => {
358
+ if (_pending.delete(r)) reject(new Error(method + ' timed out'));
359
+ }, timeoutMs);
360
+ _pending.set(r, {
361
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
362
+ reject: (e) => { clearTimeout(timer); reject(e); },
363
+ });
358
364
  _ws.send(encode({ m: method, r, p: params || {} }));
359
365
  }));
360
366
  }
@@ -2205,10 +2205,12 @@
2205
2205
  .ds-247420 .ds-file-row[data-file-type="archive"] .ds-file-icon { color: var(--flame); }
2206
2206
  .ds-247420 .ds-file-row[data-file-type="document"] .ds-file-icon { color: var(--amber, var(--sun)); }
2207
2207
 
2208
- /* Row actions — hidden until hover/focus, revealed without layout shift. */
2208
+ /* Row actions — a faint baseline keeps them discoverable at rest (not fully
2209
+ hidden, which left mouse/trackpad users unable to find rename/move/delete
2210
+ without accidentally hovering a row), full opacity on hover/focus. */
2209
2211
  .ds-247420 .ds-file-actions {
2210
2212
  display: inline-flex; gap: 2px; align-items: center;
2211
- opacity: 0; transition: opacity var(--dur-base) var(--ease);
2213
+ opacity: 0.35; transition: opacity var(--dur-base) var(--ease);
2212
2214
  }
2213
2215
  .ds-247420 .ds-file-row:hover .ds-file-actions,
2214
2216
  .ds-247420 .ds-file-row:focus-within .ds-file-actions { opacity: 1; }
@@ -7668,14 +7670,13 @@
7668
7670
  windowed thread, dashboard stopping/external states, cwd validation.
7669
7671
  ============================================================================ */
7670
7672
 
7671
- /* Enter-to-send hint: visible while the composer is focused or carries a
7672
- draft; hidden under 420px to save rows. */
7673
+ /* Enter-to-send hint: visible at rest so a new user with an empty composer
7674
+ can discover Enter-vs-Shift+Enter without having to focus it first;
7675
+ hidden under 420px to save rows. */
7673
7676
  .ds-247420 .chat-composer-hint {
7674
- display: none; width: 100%; order: 5; padding: 2px var(--space-1) 0;
7677
+ display: block; width: 100%; order: 5; padding: 2px var(--space-1) 0;
7675
7678
  font-size: var(--fs-micro); color: var(--fg-3);
7676
7679
  }
7677
- .ds-247420 .chat-composer:focus-within .chat-composer-hint,
7678
- .ds-247420 .chat-composer.has-draft .chat-composer-hint { display: block; }
7679
7680
  @media (max-width: 420px) {
7680
7681
  .ds-247420 .chat-composer .chat-composer-hint { display: none; }
7681
7682
  }