agentgui 1.0.1117 → 1.0.1119

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.
@@ -0,0 +1 @@
1
+ b8b6f24106dd526a
@@ -0,0 +1 @@
1
+ b8b6f24106dd526a
@@ -0,0 +1 @@
1
+ b8b6f24106dd526a
@@ -0,0 +1 @@
1
+ b8b6f24106dd526a
package/.gm/prd.yml ADDED
@@ -0,0 +1,10 @@
1
+ - id: aaa-consolidate-acp-http-routing
2
+ subject: Route chat.sendMessage's ACP prompt through acp-sdk-manager's already-running HTTP server instead of claude-runner-acp.js spawning a fresh stdio subprocess per turn
3
+ witness: 'MAJOR PROGRESS this session, sha f5e57db253: root-caused opencode''s HTTP server appearing unreachable (same stdin-EOF disposal bug as acp-sdk-manager.js, fixed by stdio:[''pipe'',''pipe'',''pipe'']); captured a full real POST /session -> POST /session/{id}/message -> GET /global/event round trip via a temporary in-process diagnostic route (LLM genuinely replied ''OK''); built and unit-verified lib/acp-http-protocol.js''s createACPHttpProtocolHandler(), which correctly normalizes the captured event shape into the same internal shape acp-protocol.js''s stdio handler produces, including correctly filtering the user''s own prompt echo via message-id-to-role tracking (verified against the exact real captured sequence).'
4
+ description: 'Remaining step: wire the verified normalizer into the live chat.sendMessage path. Concretely: (1) in lib/claude-runner-acp.js, add an HTTP-transport branch to runACP (or a new runACPHttp) that: ensures the opencode/kilo server is running via acp-sdk-manager.ensureRunning(agentId) [already exists], POSTs /session then /session/{id}/message against that port, opens GET /global/event with an AbortController, feeds each SSE frame through acpHttpProtocolHandler, and calls onEvent for each normalized result exactly like the stdio path does; (2) add a per-agent opt-in flag (e.g. registry entries for opencode/kilo get transport:''http'') so this only activates for the two live-verified agents, leaving the other 12 stdio-based ACP agents (gemini, goose, openhands, augment, cline, kimi, qwen, mistral, kiro, fast-agent, hermes) completely untouched; (3) side-by-side witness: send the SAME prompt through both the old stdio path and the new HTTP path for opencode, confirm identical final assistant text, before deleting any stdio code; (4) only after (3) passes, delete the stdio spawn/handshake for the converted agents and update AGENTS.md''s ACP CRITICAL rule to be literally true. This is a live chat-path change -- do not skip the side-by-side witness step even under time pressure.'
5
+ status: pending
6
+ - id: aaa-split-app-js-god-file
7
+ subject: Extract site/app/js/app.js's mixed concerns into per-concern modules
8
+ witness: 'Slice 1/4 DONE (chat-persistence, sha d211f32cbd, live-witnessed). Slice 2 (keyboard shortcuts, app.js:4649-4758 as of this commit) was investigated concretely: the global keydown handler references ~10 app.js-local functions across every feature area (navTo, closeFileDialog, filesMarked, clearFileSelection, cancelChat, newChat, previewNeighbours, openPreview, announce, render) plus state fields spanning chat/files/live/settings -- the highest cross-cutting coupling in the file. This is the exact code AGENTS.md flags as historically crash-prone under webjsx keying mistakes, and a rushed extraction threading 10 dependencies through a factory is a real correctness risk, not a mechanical move.'
9
+ description: 'Slice 2 (shortcuts) needs a dedicated pass: enumerate the full dependency list precisely (grep every identifier referenced inside the keydown handler + SHORTCUTS array + focusComposer/focusSearch/focusFilter), decide whether those ~10 functions move WITH the shortcuts module (turning it into a larger cut than ''just shortcuts'') or stay in app.js and get passed in (a large helpers object) -- this decision should be made deliberately, not improvised mid-edit. Slices 3 (hash-routing) and 4 (settings/history) still queued after. Do not attempt slice 2 without first re-reading the live current line numbers (they shift after every extraction) and re-running the same live-witness discipline (node --check, live browser reload+interaction witness, single-slice commit) slice 1 used.'
10
+ status: pending
package/AGENTS.md CHANGED
@@ -21,7 +21,7 @@ Never add `puppeteer`/`puppeteer-core`/`playwright`/`playwright-core` as a depen
21
21
  - The "Untitled conversation" fallback is one shared `UNTITLED_CONVERSATION` constant — never a locally re-typed literal (casing/wording drifts across call sites otherwise).
22
22
  - Upload/mkdir/rename/list request bodies are scanned by `SECRET_RE` before being echoed into logs or responses.
23
23
  - **webjsx keying:** mixing keyed VElements with `null`/strings/numbers in any children array crashes `applyDiff` ("reading 'key'"). Never pass a conditional `x?h():null` positionally — build the array and `.filter(Boolean)` it. `Btn` spreads array children. A kit component prop with an established narrow contract can silently corrupt an unrelated call site when a caller passes a richer value than the prop was designed for (e.g. a VElement where only a plain string was ever read) — grep every other use site of a prop before extending what a caller passes through it.
24
- - **Escape ladder order (extend, never reorder):** shortcuts overlay > file dialog > confirmingEdit > new-chat arm > stop-arms > stop generation.
24
+ - **Escape ladder order (extend, never reorder):** shortcuts overlay > file dialog > confirmingEdit > cwd editor > confirmingClearData > new-chat arm > stop-arms > files multi-select clear > stop generation.
25
25
  - **Status-disc shape:** live = solid+pulse, error = solid+halo, connecting = hollow ring, stale = muted solid.
26
26
  - **Kit `Row` rail/rank contract:** the design-kit `Row` (`anentrypoint-design` `src/components/content.js`) renders a leading status rail from a `rail` prop (`green` | `purple` | `flame`, via `.row.rail-<tone>::before`) and accepts `rank` as an alias for the leading `code` index. `state: 'disabled'` is inert (no `onClick`/`role=button`/tab-stop, `aria-disabled=true`). Rail semantics are consistent everywhere: green = selected/ok, purple = subagent, flame = error/unavailable.
27
27
  - **No decorative glyphs.** GUI source and output use ASCII words and CSS-drawn discs (`.status-dot-disc`) for status, never `●/◌/○/⌘/§/▶` etc. The middot separator `·`, ellipsis `…`, and em/en dashes in prose are kept as deliberate typographic product design, not tells — convert any other decorative glyph (arrows, box-drawing, bullets, checks) to ASCII on sight (`->`, `// --- x ---`, `-`/`*`). A dismiss control's own close icon on a dismissible Alert is a real DS icon affordance, exempt.
@@ -0,0 +1,91 @@
1
+ // Normalizes opencode's REST+SSE event shape (POST /session, POST
2
+ // /session/{id}/message, GET /global/event) into the SAME internal event
3
+ // shape lib/acp-protocol.js's acpProtocolHandler produces for the stdio ACP
4
+ // JSON-RPC transport (assistant/tool_use/tool_result/result), so a caller can
5
+ // switch transports without caring which one produced the event.
6
+ //
7
+ // Live-captured event shapes this maps (opencode 1.2.15, acp --port, SSE
8
+ // frames from GET /global/event during a real session/message round trip):
9
+ // {payload:{type:'message.part.updated', properties:{part:{type:'text',text,...}}}}
10
+ // {payload:{type:'message.part.updated', properties:{part:{type:'reasoning',text,...}}}}
11
+ // {payload:{type:'message.updated', properties:{info:{role,finish,...}}}}
12
+ // {payload:{type:'session.status', properties:{status:{type:'busy'|'idle'}}}}
13
+ // {payload:{type:'session.idle', properties:{...}}}
14
+
15
+ function normalizePart(part, sessionId) {
16
+ if (!part || typeof part !== 'object') return null;
17
+ if (part.type === 'text' && typeof part.text === 'string') {
18
+ return { type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: part.text }] }, session_id: sessionId };
19
+ }
20
+ if (part.type === 'tool' || part.type === 'tool-invocation') {
21
+ return {
22
+ type: 'assistant',
23
+ message: { role: 'assistant', content: [{ type: 'tool_use', id: part.id, name: part.tool || part.name || 'tool', input: part.input || part.args || {} }] },
24
+ session_id: sessionId,
25
+ };
26
+ }
27
+ // reasoning/step-start/step-finish parts carry no user-facing content in
28
+ // agentgui's chat surface today - dropped (return null), same as the stdio
29
+ // path drops ACP updates it doesn't recognize.
30
+ return null;
31
+ }
32
+
33
+ // A `message.part.updated` frame's `part` carries NO role of its own - live
34
+ // capture confirmed the part's role lives on the earlier `message.updated`
35
+ // frame for the SAME messageID (the prompt echo arrives as a role:"user"
36
+ // message.updated immediately before its own message.part.updated). Each
37
+ // createACPHttpProtocolHandler() call keeps its own messageId->role map so a
38
+ // caller with one handler per session never leaks state across sessions.
39
+ export function createACPHttpProtocolHandler() {
40
+ const roleByMessageId = new Map();
41
+
42
+ return function (frame, context) {
43
+ if (!frame || typeof frame !== 'object') return null;
44
+ const payload = frame.payload;
45
+ if (!payload || typeof payload !== 'object') return null;
46
+ const sid = context?.sessionId;
47
+
48
+ if (payload.type === 'message.updated') {
49
+ const info = payload.properties?.info;
50
+ if (!info) return null;
51
+ if (info.id) roleByMessageId.set(info.id, info.role);
52
+ if (info.role === 'assistant' && info.finish) {
53
+ return { type: 'result', result: '', stopReason: info.finish, usage: info.tokens ? { used: info.tokens.total } : undefined, session_id: info.sessionID || sid };
54
+ }
55
+ return null;
56
+ }
57
+
58
+ if (payload.type === 'message.part.updated') {
59
+ const part = payload.properties?.part;
60
+ if (!part) return null;
61
+ // The user's own prompt echoes back as a part on their own message -
62
+ // only forward parts belonging to an assistant-role message.
63
+ const role = part.messageID ? roleByMessageId.get(part.messageID) : undefined;
64
+ if (role === 'user') return null;
65
+ return normalizePart(part, sid);
66
+ }
67
+
68
+ if (payload.type === 'session.status' && payload.properties?.status?.type === 'error') {
69
+ return { type: 'error', error: payload.properties.status };
70
+ }
71
+
72
+ return null;
73
+ };
74
+ }
75
+
76
+ export const acpHttpProtocolHandler = createACPHttpProtocolHandler();
77
+
78
+ // Parse one SSE "data: {...}\n\n" frame (or a raw chunk containing several)
79
+ // into an array of parsed JSON payloads. opencode's /global/event stream
80
+ // uses the standard `data: <json>\n\n` SSE framing (live-confirmed).
81
+ export function parseSSEChunk(chunk) {
82
+ const out = [];
83
+ for (const block of chunk.split('\n\n')) {
84
+ const line = block.trim();
85
+ if (!line.startsWith('data:')) continue;
86
+ const jsonText = line.slice(5).trim();
87
+ if (!jsonText) continue;
88
+ try { out.push(JSON.parse(jsonText)); } catch { /* partial frame, skip */ }
89
+ }
90
+ return out;
91
+ }
@@ -8,10 +8,18 @@ import * as acpMachine from './acp-server-machine.js';
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const projectRoot = path.resolve(__dirname, '..');
10
10
  const isWindows = os.platform() === 'win32';
11
+ // codex (@openai/codex >=0.147.0) has no `acp` subcommand - `codex acp` is
12
+ // parsed as the interactive TUI with prompt text "acp" ("Error: stdin is not
13
+ // a terminal" when non-interactive), live-confirmed via `codex --help`/`codex
14
+ // app-server --help`. Its ACP-equivalent surface is `app-server` over a
15
+ // WebSocket (`--remote ws://host:port`), not this manager's HTTP-polling
16
+ // `/provider` health-check model, so codex is intentionally excluded from
17
+ // ACP_TOOLS rather than spawning a command that can never succeed. Re-add it
18
+ // once a WebSocket app-server client exists (see PRD row
19
+ // aaa-codex-app-server-client).
11
20
  const ACP_TOOLS = [
12
21
  { id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
13
22
  { id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
14
- { id: 'codex', cmd: 'codex', args: ['acp'], port: 18102, npxPkg: '@openai/codex' },
15
23
  ];
16
24
  const HEALTH_INTERVAL_MS = 30000, STARTUP_GRACE_MS = 5000, IDLE_TIMEOUT_MS = 120000;
17
25
  const processes = new Map(), idleTimers = new Map();
@@ -51,8 +59,13 @@ function startProcess(tool) {
51
59
  const resolved = resolveCommand(tool);
52
60
  let proc;
53
61
  try {
62
+ // stdin MUST stay open (piped, never closed) - the ACP CLI's `acp`
63
+ // subcommand watches stdin and disposes itself on EOF even though it is
64
+ // actually serving its normal HTTP API on `tool.port` (confirmed live:
65
+ // spawning with stdio 'ignore' closes stdin immediately, which the
66
+ // process treats as EOF and exits). Never .end()/.destroy() proc.stdin.
54
67
  proc = spawn(resolved.bin, resolved.args, {
55
- stdio: ['ignore', 'pipe', 'pipe'],
68
+ stdio: ['pipe', 'pipe', 'pipe'],
56
69
  detached: false,
57
70
  });
58
71
  } catch (err) {
@@ -149,7 +149,14 @@ registry.register({ id: 'augment', name: 'Augment Code', command: 'augment', pro
149
149
  registry.register({ id: 'cline', name: 'Cline', command: 'cline', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
150
150
  registry.register({ id: 'kimi', name: 'Kimi CLI', command: 'kimi', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
151
151
  registry.register({ id: 'qwen', name: 'Qwen Code', command: 'qwen-code', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
152
- registry.register({ id: 'codex', name: 'Codex CLI', command: 'codex', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
152
+ // codex (@openai/codex >=0.147.0) has no `acp` subcommand - live-confirmed
153
+ // via `codex --help`/`codex app-server --help`: `codex acp` is parsed as the
154
+ // interactive TUI with prompt text "acp" and errors "stdin is not a
155
+ // terminal" non-interactively. Its ACP-equivalent is `app-server` over a
156
+ // WebSocket, a different transport this stdio-JSON-RPC runner cannot drive.
157
+ // Registering it with buildArgs:()=>['acp'] would spawn a command that can
158
+ // never succeed. Disabled until a WebSocket app-server client exists (see
159
+ // PRD row aaa-codex-app-server-client).
153
160
  registry.register({ id: 'mistral', name: 'Mistral Vibe', command: 'mistral-vibe', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
154
161
  registry.register({ id: 'kiro', name: 'Kiro CLI', command: 'kiro', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
155
162
  registry.register({ id: 'fast-agent', name: 'fast-agent', command: 'fast-agent', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });