agentgui 1.0.1057 → 1.0.1059

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 logic+predictability sweep (2026-07-12) — forty-first run
4
+
5
+ `gui-logic-predictability` workflow wf_c645ba95-c6a (23 agents, 192 tool calls, 0 errors) — 10 confirmed findings, all implemented, plus one real kit bug found and fixed mid-verification (not from the workflow's own hunt). App (`site/app/js/app.js`): `runningPanel()`'s title read `'running · N'` with no scoping cue even though the panel always lists every in-flight session app-wide, not just ones related to the current chat — retitled `'all running sessions · N'`, and it now appends `'(refreshing…)'` while a backgrounded tab's poll is catching up after refocus (`refreshActive()` sets a `state._activeStale` flag while `document.hidden` skips the network call, cleared on the next real response) instead of silently painting stale data as fresh. `runningRowTitle()` fell straight to `UNTITLED_CONVERSATION` for a brand-new session with no index row yet, while the Live dashboard's `computeLiveSessions()` already had a 3s "indexing…" carve-out for the identical case — the same session read two contradictory identities in the same 0-3s window depending only on which panel you looked at; gave `runningRowTitle` the matching carve-out. Live dashboard cards sorted by `elapsed`/`activity` re-sort on every tick (both are continuously-changing values), which could reorder a card out from under a mid-click pointer (selecting a checkbox, clicking stop) — added a module-level `_livePointerDown` flag (set/cleared on document `pointerdown`/`pointerup`/`pointercancel`) that freezes the sort to the last-computed sid order while a pointer is held anywhere in the dashboard. agy's real `--continue` resume path (wired last run) had no composer disclosure the way claude-code's resume banner does — added a `'continues most recent conversation'` composerContext bit for agy past its first turn. History's `ConversationList` sessions mapper hardcoded `agent: undefined` while the Running-panel/Live-dashboard rows for the identical finished-vs-in-flight session both show an agent/model badge — traced to ccsniff's `Store.sessions()` never folding per-event `model` into the session record even though `flattenEvent` captures it; fixed upstream (see below) and wired `agent: agentById('claude-code').name + ' · ' + s.model` into the mapper (ccsniff only reads Claude Code's own JSONL, so the agent is always constant). Kit (`design/src/components/{chat,content}.js`, `design/chat.css`, `design/src/components/sessions.js`): per-message chat copy action (`ChatMessage`'s generic `actionRow`) had zero visible click feedback for sighted/mouse users — only an `aria-live` `announce()` — unlike the three sibling copy patterns already in the same file (code-block copy, `CodeNode`/`ToolCallNode` copy) which all self-manage a label/icon flip to "copied" for 1.6s; added the matching flip keyed off `a.label === 'copy'`. Follow-up chips (contextual, derived from the last turn) and empty-state seed chips (generic starter ideas) shared identical DOM shape/class with zero visual signal distinguishing them — `.agentchat-followup` gained a dashed border (same pill shape/size, no new color/shape language). `flashComposerNote`'s single shared DOM node silently overwrote an unread note if a second call landed before the first note's 2.6s timeout — now queues notes and drains them in order instead of dropping the first message. `SessionDashboard`'s filter `SearchInput` never forwarded `resultCount` to its `aria-live` region (the Live dashboard's own filtered session count was available but unused) — wired it through. **That last change surfaced a real, previously-latent kit bug independent of this run's own hunt: `SearchInput` (`design/src/components/content.js`) conditionally returned either a bare `<input>` VElement or a wrapping `<span>` VElement depending on whether `resultCount`/a clear button were present that render — since `resultCount` now toggles from `undefined` to a string as the Live filter's value changes, webjsx's `applyDiff` tried to morph one element type into another at the same keyed toolbar slot, producing a corrupted merged DOM node (a real `<select>` picked up `type=search`/`name=q`/`placeholder=...` attributes from the `SearchInput` render, and the actual `<input>` vanished entirely) — live-witnessed on buildesk before the fix (`hasInput:false` on the toolbar). Fixed by always returning the `.ds-search-input-wrap` shape (confirmed `display:contents` in CSS, so it's layout-transparent and safe for every existing call site).** ccsniff (separate repo, `github:AnEntrypoint/ccsniff#main`, cloned fresh to `/config/workspace/ccsniff-work`): `Store.sessions()` (`src/store.js`) folded in the most-recent-by-timestamp `model` per session (events aren't guaranteed in ts order since JSONL is read file-by-file); `index.js`'s assistant-event ingestion never actually populated `model` on pushed content blocks (only `system`/init events carried `e.model`) — real Claude Code JSONL carries the actually-used model at `message.model` on every assistant turn, so that's now inherited onto every block from that message. `node test.js`: ALL TESTS PASS both commits. Pushed `11f6c3b` then (after a remote-advanced rebase) `26bbfcc`/`beff5e7`, ccsniff CI (Publish/Auto-Declaudeify/Deploy GH Pages) green both pushes. Kit built+tested twice (once per round of edits — findings, then the SearchInput fix), re-vendored both times. Reinstalling the ccsniff dependency required removing `bun.lock`'s pinned commit hash entirely (`bun install ccsniff@github:...#main --force` alone kept resolving to the stale pinned sha even after a `rm -rf node_modules/ccsniff`) — deleting `bun.lock` and re-running `bun install` re-resolved `#main` to the actual latest commit. The local buildesk server process needed an explicit restart (`kill -TERM` on the `bun server.js` PID) for the new `node_modules/ccsniff` to load — Node/Bun caches modules in-process, so an npm/git dependency swap under a running process is invisible until restart; the env's custom Node supervisor (`/opt/gmweb-startup`, not a standard supervisord) auto-respawned it within ~20s of the health-check failing, re-launching via `bunx agentgui@latest` which correctly resolves to the local workspace's own `bin/gmgui.cjs` in this workspace-linked setup (confirmed by the "Workspace mode: skipping npm version checker" startup log line), not the published npm package. Browser-witnessed every finding live on buildesk: `.ds-dash-toolbar` filter input/select intact before AND after typing with the corruption fixed, `aria-live` region reading `'0 results'`; `.chat-msg-action.is-copied`/`.agentchat-followup` CSS rules present in the live CSSOM; History rail rows reading `'agentgui · 3s ago · Claude Code · claude-sonnet-5'` (18/18 sessions carrying a real model after the ccsniff fix + server restart); synthetic `pointerdown` dispatch confirmed no page error from the new listener. 0 pageErrors throughout.
6
+
3
7
  ## GUI logic+predictability sweep (2026-07-12) — fortieth run
4
8
 
5
9
  `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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1057",
3
+ "version": "1.0.1059",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -358,10 +358,13 @@ function activeSig(list) {
358
358
  async function refreshActive() {
359
359
  // A backgrounded tab still holds its interval timer, but polling every ~3.3s
360
360
  // while hidden is needless server load across many left-open tabs - skip the
361
- // network call (and let the next visible tick catch up).
362
- if (typeof document !== 'undefined' && document.hidden) return;
361
+ // network call (and let the next visible tick catch up). Mark the currently
362
+ // held state.active as stale so a refocus render doesn't silently paint
363
+ // minutes-old data as fresh while the re-poll is still in flight.
364
+ if (typeof document !== 'undefined' && document.hidden) { state._activeStale = true; return; }
363
365
  let next;
364
366
  try { next = await B.listActiveChats(state.backend); } catch { return; }
367
+ state._activeStale = false;
365
368
  const changed = activeSig(next) !== activeSig(state.active);
366
369
  state.active = next;
367
370
  if (changed) state._sessionGroupsCache = null;
@@ -845,7 +848,13 @@ function sessionsColumn() {
845
848
  // carries only the time (and agent when present).
846
849
  project: project === title ? '' : project,
847
850
  time: fmtRelTime(s.last),
848
- agent: undefined,
851
+ // ccsniff only reads Claude Code's own JSONL (see AGENTS.md), so the
852
+ // agent is always claude-code; ccsniff's sessions() now also folds in
853
+ // the session's most-recent model - surfacing both here matches the
854
+ // badge the running-panel/live-dashboard rows already show for the
855
+ // identical session while it's in-flight, instead of going blank the
856
+ // moment it finishes and drops into History.
857
+ agent: (agentById('claude-code')?.name || 'Claude Code') + (s.model ? ' · ' + s.model : ''),
849
858
  running: runningSids.has(s.sid),
850
859
  status: runningSids.has(s.sid) ? liveStatusBySid.get(s.sid) : undefined,
851
860
  unread: false,
@@ -1724,6 +1733,14 @@ async function stopAllActive(sessions) {
1724
1733
  // before it is treated as STALE - alive but not making progress, so a stuck
1725
1734
  // agent reads differently from a busy one.
1726
1735
  const STALE_AFTER_MS = 45000;
1736
+ // Tracks whether a pointer is currently held down anywhere in the document,
1737
+ // used to freeze Live-dashboard card sort order mid-click (see liveMain()).
1738
+ let _livePointerDown = false;
1739
+ if (typeof document !== 'undefined') {
1740
+ document.addEventListener('pointerdown', () => { _livePointerDown = true; });
1741
+ document.addEventListener('pointerup', () => { _livePointerDown = false; });
1742
+ document.addEventListener('pointercancel', () => { _livePointerDown = false; });
1743
+ }
1727
1744
  // Rank for the error-then-stale-first ordering (W3).
1728
1745
  const STATUS_RANK = { error: 0, stale: 1, running: 2 };
1729
1746
 
@@ -1879,15 +1896,26 @@ function liveMain() {
1879
1896
  // external, and a deterministic sid tiebreaker so equal-rank cards never
1880
1897
  // reshuffle with the server's return order.
1881
1898
  const sortKey = lv.sort || 'status';
1882
- sessions.sort((a, b) => {
1883
- let d = (a.external ? 1 : 0) - (b.external ? 1 : 0);
1884
- if (d) return d;
1885
- if (sortKey === 'elapsed') d = (b.elapsedMs || 0) - (a.elapsedMs || 0);
1886
- else if (sortKey === 'activity') d = (b.lastTs || 0) - (a.lastTs || 0);
1887
- else if (sortKey === 'errors') d = (b.errors || 0) - (a.errors || 0);
1888
- else d = (STATUS_RANK[a.status] ?? 9) - (STATUS_RANK[b.status] ?? 9);
1889
- return d || String(a.sid).localeCompare(String(b.sid));
1890
- });
1899
+ // 'elapsed'/'activity' sort keys are continuously-ticking values (they change
1900
+ // every second), so re-sorting on every render can reorder a card out from
1901
+ // under a mid-click pointer (selecting a checkbox, clicking stop). While a
1902
+ // pointer is held down anywhere in the dashboard, freeze the sort to the
1903
+ // last-computed sid order instead of live-recomputing it.
1904
+ if (_livePointerDown && state._liveSortedOrder && state._liveSortedOrder.length) {
1905
+ const rank = new Map(state._liveSortedOrder.map((sid, i) => [sid, i]));
1906
+ sessions.sort((a, b) => (rank.has(a.sid) ? rank.get(a.sid) : 1e9) - (rank.has(b.sid) ? rank.get(b.sid) : 1e9));
1907
+ } else {
1908
+ sessions.sort((a, b) => {
1909
+ let d = (a.external ? 1 : 0) - (b.external ? 1 : 0);
1910
+ if (d) return d;
1911
+ if (sortKey === 'elapsed') d = (b.elapsedMs || 0) - (a.elapsedMs || 0);
1912
+ else if (sortKey === 'activity') d = (b.lastTs || 0) - (a.lastTs || 0);
1913
+ else if (sortKey === 'errors') d = (b.errors || 0) - (a.errors || 0);
1914
+ else d = (STATUS_RANK[a.status] ?? 9) - (STATUS_RANK[b.status] ?? 9);
1915
+ return d || String(a.sid).localeCompare(String(b.sid));
1916
+ });
1917
+ state._liveSortedOrder = sessions.map(s => s.sid);
1918
+ }
1891
1919
  const selected = lv.selected instanceof Set ? lv.selected : new Set();
1892
1920
  // The in-page chat's card accent matches on the real sid, but the kit
1893
1921
  // compares activeSid against card.sid (the ephemeral id for owned cards).
@@ -2230,7 +2258,9 @@ function chatMain() {
2230
2258
  userTurnCount > 0 ? plural(userTurnCount, 'turn') : null,
2231
2259
  (state.chat.resumeSid && state.selectedAgent === 'claude-code')
2232
2260
  ? 'continues conversation (' + resumeSidLabel(state.chat.resumeSid) + ')'
2233
- : null,
2261
+ : (state.selectedAgent === 'agy' && userTurnCount > 0)
2262
+ ? 'continues most recent conversation'
2263
+ : null,
2234
2264
  ].filter(Boolean),
2235
2265
  } : undefined,
2236
2266
  // W15: contextual follow-up chips derived from the settled last turn
@@ -3164,6 +3194,13 @@ function resumeSidLabel(sid) {
3164
3194
  // everywhere else.
3165
3195
  function runningRowTitle(r) {
3166
3196
  const realSid = r.claudeSessionId || r.sessionId;
3197
+ const arr = Array.isArray(state.sessions) ? state.sessions : [];
3198
+ const sess = arr.find((s) => s.sid === realSid);
3199
+ // A brand-new session (started <3s ago) with no index row yet is still
3200
+ // BEING indexed, not permanently untitled - same carve-out and threshold
3201
+ // computeLiveSessions() uses, so the identical session doesn't read
3202
+ // "Untitled conversation" here and "indexing…" on the Live dashboard.
3203
+ if (!sess && r.startedAt && (Date.now() - r.startedAt < 3000)) return 'indexing…';
3167
3204
  return resumeSidLabel(realSid);
3168
3205
  }
3169
3206
 
@@ -3200,7 +3237,7 @@ function runningPanel() {
3200
3237
  const stopping = state.live.stopping || new Set();
3201
3238
  return Panel({
3202
3239
  key: 'runningPanel',
3203
- title: 'running · ' + running.length,
3240
+ title: 'all running sessions · ' + running.length + (state._activeStale ? ' (refreshing…)' : ''),
3204
3241
  children: [
3205
3242
  // A discoverable path from "this running chat" to the management surface.
3206
3243
  h('div', { key: 'runall', class: 'resume-banner', role: 'group' },
@@ -7130,6 +7130,7 @@
7130
7130
  .ds-247420 .chat-msg-action-label { font-size: var(--fs-tiny); }
7131
7131
  .ds-247420 .chat-msg-action:hover { background: var(--bg-2); color: var(--fg); }
7132
7132
  .ds-247420 .chat-msg-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; opacity: 1; }
7133
+ .ds-247420 .chat-msg-action.is-copied { color: var(--accent-ink); opacity: 1; }
7133
7134
 
7134
7135
  /* Empty state: a fresh thread is an invitation, not a void. */
7135
7136
  .ds-247420 .agentchat-empty {
@@ -7164,6 +7165,11 @@
7164
7165
  }
7165
7166
  .ds-247420 .agentchat-empty-suggestion:hover, .ds-247420 .chat-empty-suggestion:hover { background: color-mix(in srgb, var(--accent, var(--fg)) 12%, transparent); }
7166
7167
  .ds-247420 .agentchat-empty-suggestion:focus-visible, .ds-247420 .chat-empty-suggestion:focus-visible { outline: 2px solid var(--accent, var(--fg)); outline-offset: var(--focus-offset); }
7168
+ /* Follow-up chips (contextual, based on the last turn) vs empty-state seed
7169
+ chips (generic starter ideas) previously shared identical DOM shape with no
7170
+ visual signal distinguishing them - a dashed border reads as "derived from
7171
+ context" without introducing a new color/shape language. */
7172
+ .ds-247420 .agentchat-followup { border-style: dashed; }
7167
7173
 
7168
7174
  /* Guided install path in the empty state: copy line + monospaced command rows
7169
7175
  (each with its own copy button) + a recheck button. No animation. */