agentgui 1.0.987 → 1.0.988

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
+ ## CRITICAL — ACP is managed ONLY by `lib/acp-sdk-manager.js` (2026-06-20) — twentieth run
4
+
5
+ A destructive quality run found a second, redundant ACP path: `lib/plugins/acp-plugin.js` eager-spawned all four ACP tools at boot on **conflicting/mismatched ports** (it mapped gemini=18101 / kilo=18102 / codex=18103, but the canonical `acp-sdk-manager.js` uses opencode=18100 / kilo=18101 / codex=18102), implemented no restart/health logic despite a `/api/acp/status` route that promised `restarts`, and **crashed server boot** with `[FATAL] Uncaught exception: Executable not found in $PATH: "codex"` whenever an ACP binary was missing. Its route was consumed by nobody (the client `acpStatusFor` reads `state.health.acp` from `getACPStatus()` → `acp-sdk-manager.js`); no plugin declared an `acp` dependency. **Deleted the file.** Rule: ACP lifecycle (on-demand start, health via `/provider`, restart-backoff, status) lives in `lib/acp-sdk-manager.js` alone — never add a second eager-spawn ACP manager. Also: `spawn()` for a missing binary surfaces ENOENT as an **async `'error'` event** under Bun (it escapes a synchronous try/catch), so every `spawn()` callsite needs a `proc.on('error', …)` handler. `acp-sdk-manager.js startProcess` was the only remaining callsite missing it (every other — terminal.js, claude-runner-direct.js, gm-agent-configs.js, claude-runner-acp.js — already had one); added `proc.on('error')` routing into the CRASHED + `scheduleRestart` backoff path. Witnessed: clean boot (0 FATAL), `[PLUGINS] Loaded extensions` no longer lists `acp`, `/api/acp/status` → 404, tests 46/46, browser localhost:3009/gm/ 0 console errors. Pushed this run.
6
+
3
7
  ## GUI quality sweep (2026-06-19) — nineteenth run
4
8
 
5
9
  132-agent workflow wf_8183560b-e3d (12 lenses, 93 confirmed). Kit: thinking settled state, retry on non-last message, AT aria fixes (followup chips live guard, cwd focus restore, skip-link target, sessions toggle chevron, shell print styles, a11y-01 index.html), composer disabled visual state, files-modals focus trap re-query + stable aria-labelledby, sessions.js listbox+aria-selected. App: resume-transcript-load (loadResumeTranscript historical messages + spinner), ACP force-restart (unhealthy agent restart btn + WS handler), history tool_use rail=purple, shortcuts overlay role=dialog, streaming-active badge on chat rail tab, sortedAgents memoized, files filter 150ms debounce, _seen Set capped 5000, humanizeMs->fmtDuration, dead historySide() removed, pathBasename util, ARM_RESET_MS constant, refreshHistory guard, perf-002/006/007/008 memoization+cleanup, live-tokens accumulated, backend-change-mid-chat guard, transcript loading state, session-expiry onSessionExpired hook, server-500 stream error sanitized. Server: IPv4-mapped IPv6 normalization in rate-limit, image route streaming (no sync read), isWindows module-level, getAvailableAgents export removed, files-plugin+workflow-plugin confinement, acp-plugin shell injection fix, plugin-routes CSRF fix, asset-server JS injection fix. Full detail in rs-learn (recall "agentgui 19th run").
@@ -50,28 +54,11 @@ Lands the 10th run's deferred batch + the janks a live witness surfaced on this
50
54
 
51
55
  ## GUI screen-real-estate layer (2026-06-12) — fluid columns + drag-resize handles
52
56
 
53
- An additive viewport-density layer on top of the concurrent 11th/12th-run work (the user verdict: maximize screen real-estate, the SDK must read as a desktop application). New workflow `.claude/workflows/gui-screen-realestate.js` (6 density lenses: space-efficiency, viewport-utilization, information-density, adaptive-layout, application-chrome, finish-pending). 50 agents -> 17 confirmed gaps (`PUNCHLIST-DENSITY.md`). Kit pushed `anentrypoint-design` 97dc646.
54
-
55
- **The shell reclaims the viewport (genuinely-new, complementary to the 12th run's breakpoint column-yield).** Fixed `--ws-rail-w 232 / --ws-sessions-w 296 / --ws-pane-w 320` (≈848px chrome) became fluid clamps (`clamp(200px,16vw,260px)` etc) so chrome scales with the viewport. Kit `shell.js` renders keyboard+pointer `.ws-resizer` separators (`role=separator`, ArrowLeft/Right 16px, pointer-drag) between each track that write a clamped inline `--ws-<col>-w` (inline overrides the clamp base, pinning the width) persisted to `localStorage ds.ws.w.<col>` and seeded on mount via a `ref` (`seedWsWidths`, no app wiring). Sessions gained a desktop collapse toggle (`.ws-sessions-toggle` in the crumb; `.ws-desktop-toggle` CSS = inverse of the mobile drawer-toggle; `toggleWs` extended with `'sessions' -> ws-sessions-collapsed`). `.btn*` moved off `--r-pill` to `--r-1` (8/14 pad, 32 min-h, 44 coarse floor); `.app-status` is a thin 26px ellipsized strip (`--app-status-h:26px`); `.chat-msg-flat` centers at `--measure` (`max-width:var(--measure);margin-inline:auto`) + density-scaled spacing via the `--density` token + 84ch ultrawide tier; `.ds-dash-grid` densified (minmax 240, 220 wide tier).
56
-
57
- **Concurrent-writer reconciliation (load-bearing — both repos have active concurrent writers).** While this run was in flight, concurrent writers shipped the ENTIRE Files multi-select + density-thumbnail + move feature on BOTH `anentrypoint-design/main` (FileGrid `selected/onToggleSelect/onSelectAll/onClearSelection/density/onDensity/thumbUrl` + `BulkBar` export) AND `agentgui/main` (the 11th/12th runs: `onMark`/`density` wiring + `B.moveEntry` -> confined `POST /api/move` + bulk move). So this run's parallel multi-select/move/F3 work was DROPPED and theirs adopted; the only thing landed on agentgui is the re-vendored kit dist (the screen-real-estate layer above) + this workflow + docs. **Rule: when origin advances mid-run with the same feature, drop yours and adopt theirs, then re-apply only your genuinely-distinct work onto their current files — never force a parallel implementation; the agentgui `/api/move-confined` endpoint built this run was redundant with their `/api/move` and discarded.** The kit ships 4 build lints (`lint-tokens/glyphs/null-children/classes`) - all must pass. Witness localhost:3009/gm/ (3000 owned by another app): the vendored bundle is served fresh only after a SERVER restart (it pre-warms statics in memory) AND a fresh browser context (the persistent witness context caches the ES module - clear it or new-session), else you read a stale bundle. Witnessed 0 console errors chat/files, btn 10px, status 26px, 3 resizers, sessions toggle, fluid rail 256@1600, no h-scroll.
57
+ Fluid-column + drag-resize density layer (workflow `gui-screen-realestate.js`, 50 agents -> 17 gaps). Fixed `--ws-*-w` chrome became fluid `clamp()`; kit `shell.js` `.ws-resizer` separators write persisted inline `--ws-<col>-w`; `.ws-sessions-toggle` desktop collapse; `.btn*` -> `--r-1`; `.app-status` 26px. **Concurrent-writer rule (load-bearing): when origin advances mid-run with the same feature, drop yours and adopt theirs, then re-apply only genuinely-distinct work never a parallel impl.** Full detail in rs-learn (recall "agentgui screen-real-estate layer").
58
58
 
59
59
  ## GUI Claude-Code-web parity sweep (2026-06-11) — tenth maximum-effort run
60
60
 
61
- Hunts the VISUAL + LAYOUT + MOTION design-language parity gap vs claude.ai/code (the user verdict: too primitive, layout jank). New workflow `.claude/workflows/gui-claude-code-parity.js` (6 design-language lenses: visual-language, layout-composition, chat-thread-craft, file-browser-fidelity, live-session-command, motion-microinteraction; hunt -> adversarial verify (kept-typography guard) -> plan). 66 agents -> 37 confirmed gaps (`PUNCHLIST-PARITY.md`). Pushed: kit `anentrypoint-design` 28387c3, agentgui 0a93fc6.
62
-
63
- **Chat reads like Claude Code, not a messenger.** AgentChat passes `flat:true` + `aicat:false` to `ChatMessage`; flat turns are FULL-WIDTH, avatar-less, capped at `--measure`, led by a `.chat-role` label (You / agent name), the assistant turn carrying a faint `color-mix(--fg 3%)` background (`.chat-msg-flat` in chat.css). The bubble/avatar layout is kept ONLY for the messenger demo. Composer is ONE bordered rounded-rect shell (`.chat-composer` border `var(--rule)` + `--r-2`, `:focus-within` ring) with a borderless textarea and an inline `--r-1` send (was a `--r-pill` stadium + 50% round button); `flex-wrap` + `.chat-composer-hint{order:5}` lay context/input/hint as rows. Tool calls render as a bordered status-toned card (`.chat-tool` de-frames the inherited `.chat-bubble`, head=icon·name·label·status-pill, running->accent / error->flame); the bundle shipped ZERO `.chat-tool` CSS before. `injectCodeCopy` reads the `<code>` `language-*` class -> a `.chat-code-lang` header tab on every rendered block. `::selection` is accent-tint, not mascot pink.
64
-
65
- **Layout has a real content gutter.** Kit `WorkspaceShell` gains `mainFlush`; `.ws-main` now has `padding: var(--space-4) var(--space-5)` (files/live/history/settings) and the chat tab opts out via `.ws-main--flush` (app sets `mainFlush: state.tab==='chat'` since the thread self-gutters at `--measure`). `.ws-crumb` gets `min-height:48px` + a matching left gutter so the top edge aligns with the rail head.
66
-
67
- **Live dashboard is a command center.** `SessionDashboard`: status-bucketed grid (Errored/Running/Idle/External `role=group` sections when `sort==='status'`, flat otherwise); header status breakdown `N running · M idle · K errors` (toned `.ds-dash-breakdown .seg.is-*`); a `.ds-dash-stream-disc` heartbeat (pulsing `status-dot-live` when connected); tri-state `role=checkbox` select-all + clear (`onSelectAll`/`onClearSelection`, app toggles `state.live.selected`); `is-armed` one-shot pulse on the armed stop button. `SessionCard`: cost/token stat (`session.cost`/`tokens`, app feeds the in-page chat's `totalCost` onto its own card), `is-new` arrival cue (`session.isNew`, app sets it for <3s-old sids), icon-led actions (open=primary), `is-stale` amber inset bar (was opacity .92). Files: `TYPE_ICON.dir='folder'` + new `file-image`/`link`/`folder-open` icons (shell.js); ConversationList loading skeleton rows. File preview: Prism highlight hook (`highlightAllUnder` ref) + `.ds-preview-gutter` line numbers + image fit/actual toggle + checkerboard.
68
-
69
- **Motion (all `prefers-reduced-motion: no-preference` guarded).** `.ws-shell` eases `grid-template-columns` (rail/pane collapse no longer snaps; track count stays stable); `.agentchat-thread` `scroll-behavior:smooth`; `.ds-session-row` hover transition; `.ws-scrim` opacity fade (was display toggle); `.ds-alert` enter; copy->copied color flip; `.ds-dash-card.is-new` fade-in.
70
-
71
- **Load-bearing webjsx fix (caught by the live witness).** SessionCard passed raw `null`s positionally in its head/meta/actions/top-level children arrays; webjsx `applyDiff` crashes (`reading 'key'`) on ANY null among VElement siblings (not just keyed ones). This was a PRE-EXISTING latent bug (the unmodified kit crashed identically on this env's external sessions) that this env's data now triggered. Fix: `.filter(Boolean)` EVERY children array in SessionCard + SessionDashboard header/body, and `Btn` now spreads array `children`. The kit-wide rule: never pass a conditional `x ? h() : null` positionally — build the array and `.filter(Boolean)` it (the same pattern ConversationList already uses). Witness localhost:3009/gm/ (port 3000 was owned by another app): 0 console/page errors across chat/files/live; composer 14px radius; ws-main flush=0 on chat, 32px on files; folder icon distinct; no h-scroll. First `/gm/` request triggers the 30-90s ccsniff walk -> poll until 200 before witnessing.
72
-
73
- **Deferred (documented in `PUNCHLIST-PARITY.md`, rows pending):** Files multi-select + BulkBar + density-thumbnail grid (kit props + heavy app/server wiring), the `/api/move-confined` drag-to-move endpoint, a toolbar up-button + file-grid keys in the SHORTCUTS array, and F3 panel/card elevation unification. These are lower-leverage than the shipped visual/layout/motion/command-center batch.
74
-
61
+ 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").
75
62
  ## GUI logic+predictability sweep (2026-06-10) — ninth maximum-effort run
76
63
 
77
64
  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.
@@ -68,6 +68,22 @@ function startProcess(tool) {
68
68
 
69
69
  log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
70
70
 
71
+ // A missing/unspawnable binary surfaces as an async 'error' event, NOT the
72
+ // synchronous throw the try/catch above guards (witnessed under Bun: an
73
+ // uninstalled ACP CLI emits ENOENT asynchronously and, without this listener,
74
+ // escapes as an uncaught exception that kills the whole server). Funnel it
75
+ // into the same CRASHED + backoff path as a normal exit so an uninstalled
76
+ // on-demand agent degrades to "not healthy", never a process crash.
77
+ proc.on('error', (err) => {
78
+ processes.delete(tool.id);
79
+ if (shuttingDown) return;
80
+ log(tool.id + ' spawn error: ' + err.message);
81
+ acpMachine.send(tool.id, { type: 'CRASHED' });
82
+ const snap = acpMachine.snapshot(tool.id);
83
+ if (snap?.value === 'stopped') { log(tool.id + ' max restarts reached'); return; }
84
+ scheduleRestart(tool);
85
+ });
86
+
71
87
  proc.on('close', (code) => {
72
88
  processes.delete(tool.id);
73
89
  if (shuttingDown) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.987",
3
+ "version": "1.0.988",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -1,110 +0,0 @@
1
- // ACP plugin - OpenCode, Gemini, Kilo, Codex startup and health checks
2
-
3
- import { spawn, execFileSync } from 'child_process';
4
- import path from 'path';
5
- import fs from 'fs';
6
-
7
- // Resolve a binary name to an absolute path (mirrors lib/claude-runner-direct.js pattern).
8
- // Returns the resolved path, or null if not found.
9
- function resolveBinaryPath(name) {
10
- try {
11
- const which = process.platform === 'win32' ? 'where' : 'which';
12
- const result = execFileSync(which, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
13
- const first = result.trim().split(/\r?\n/)[0];
14
- return first || null;
15
- } catch {
16
- return null;
17
- }
18
- }
19
-
20
- export default {
21
- name: 'acp',
22
- version: '1.0.0',
23
- dependencies: [],
24
-
25
- async init(config, plugins) {
26
- const toolProcesses = new Map();
27
- const healthCheckIntervals = new Map();
28
- const restartCounts = new Map();
29
- const acpPorts = new Map();
30
-
31
- // Each spec uses argv (binary + args) rather than a shell command string so
32
- // spawn runs with shell:false — no shell injection risk even if port/name
33
- // values are later sourced from config. Mirrors lib/claude-runner-direct.js.
34
- const toolSpecs = [
35
- { name: 'opencode', port: 18100, argv: ['opencode', 'acp', '--port', '18100'] },
36
- { name: 'gemini', port: 18101, argv: ['gemini', 'acp', '--port', '18101'] },
37
- { name: 'kilo', port: 18102, argv: ['kilo', 'acp', '--port', '18102'] },
38
- { name: 'codex', port: 18103, argv: ['codex', 'acp', '--port', '18103'] },
39
- ];
40
-
41
- const startTool = async (spec) => {
42
- try {
43
- const [bin, ...args] = spec.argv;
44
- // Resolve to an absolute path so spawn never relies on PATH expansion
45
- // inside a shell; shell:false is the default for spawn() when called
46
- // this way, matching the project-wide rule in AGENTS.md.
47
- const resolvedBin = resolveBinaryPath(bin) || bin;
48
- const proc = spawn(resolvedBin, args, { shell: false });
49
- toolProcesses.set(spec.name, proc);
50
- acpPorts.set(spec.name, spec.port);
51
- restartCounts.set(spec.name, 0);
52
-
53
- // Health check every 30s
54
- const interval = setInterval(() => {
55
- if (proc.killed) {
56
- clearInterval(interval);
57
- healthCheckIntervals.delete(spec.name);
58
- }
59
- }, 30000);
60
- healthCheckIntervals.set(spec.name, interval);
61
- } catch (e) {
62
- console.error(`[ACP] Failed to start ${spec.name}:`, e.message);
63
- }
64
- };
65
-
66
- // Start all ACP tools
67
- for (const spec of toolSpecs) {
68
- await startTool(spec);
69
- }
70
-
71
- return {
72
- routes: [
73
- {
74
- method: 'GET',
75
- path: '/api/acp/status',
76
- handler: (req, res) => {
77
- const status = {};
78
- for (const [name, proc] of toolProcesses) {
79
- status[name] = {
80
- running: !proc.killed,
81
- port: acpPorts.get(name),
82
- pid: proc.pid,
83
- restarts: restartCounts.get(name) || 0,
84
- };
85
- }
86
- res.json({ tools: status });
87
- },
88
- },
89
- ],
90
- wsHandlers: {},
91
- api: {
92
- getStatus: () => Object.fromEntries(acpPorts),
93
- },
94
- stop: async () => {
95
- for (const [name, interval] of healthCheckIntervals) {
96
- clearInterval(interval);
97
- }
98
- for (const [name, proc] of toolProcesses) {
99
- if (proc && !proc.killed) proc.kill();
100
- }
101
- },
102
- };
103
- },
104
-
105
- async reload(state) {
106
- return state;
107
- },
108
-
109
- async stop() {},
110
- };