agentgui 1.0.783 → 1.0.785

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/lib/db-queries.js CHANGED
@@ -1038,32 +1038,28 @@ export function createQueries(db, prep, generateId) {
1038
1038
  },
1039
1039
 
1040
1040
  getChunksBefore(conversationId, beforeTimestamp, limit = 500) {
1041
- const countStmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1042
- const total = countStmt.get(conversationId).count;
1041
+ const total = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ? AND created_at < ?')
1042
+ .get(conversationId, beforeTimestamp).count;
1043
1043
 
1044
- const stmt = prep(`
1044
+ const rows = prep(`
1045
1045
  SELECT id, sessionId, conversationId, sequence, type, data, created_at
1046
1046
  FROM chunks
1047
1047
  WHERE conversationId = ? AND created_at < ?
1048
1048
  ORDER BY created_at DESC LIMIT ?
1049
- `);
1050
- const rows = stmt.all(conversationId, beforeTimestamp, limit);
1049
+ `).all(conversationId, beforeTimestamp, limit);
1051
1050
  rows.reverse();
1052
1051
 
1053
1052
  return {
1054
1053
  chunks: rows.map(row => {
1055
1054
  try {
1056
- return {
1057
- ...row,
1058
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1059
- };
1055
+ return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data };
1060
1056
  } catch (e) {
1061
1057
  return row;
1062
1058
  }
1063
1059
  }),
1064
1060
  total,
1065
1061
  limit,
1066
- hasMore: total > (limit + 1)
1062
+ hasMore: rows.length === limit
1067
1063
  };
1068
1064
  },
1069
1065
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.783",
3
+ "version": "1.0.785",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+ // Seed a large conversation for profiling browser rendering performance.
3
+ // Usage: bun scripts/seed-large-conversation.js [--turns N] [--chunks-per-turn N]
4
+ // Output: conversation ID on stdout, progress on stderr.
5
+
6
+ import { Database } from 'bun:sqlite';
7
+ import { randomUUID } from 'crypto';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import fs from 'fs';
11
+
12
+ const args = process.argv.slice(2);
13
+ const getArg = (flag, def) => {
14
+ const i = args.indexOf(flag);
15
+ return i !== -1 ? parseInt(args[i + 1]) : def;
16
+ };
17
+ const turns = getArg('--turns', 6000);
18
+ const chunksPerTurn = getArg('--chunks-per-turn', 5);
19
+
20
+ const dataDir = process.env.PORTABLE_DATA_DIR || path.join(os.homedir(), '.gmgui');
21
+ const dbDir = dataDir;
22
+ fs.mkdirSync(dbDir, { recursive: true });
23
+ const dbPath = path.join(dbDir, 'data.db');
24
+
25
+ console.error(`[seed] opening ${dbPath}`);
26
+ const db = new Database(dbPath);
27
+ db.run('PRAGMA journal_mode = WAL');
28
+ db.run('PRAGMA synchronous = NORMAL');
29
+
30
+ db.run(`CREATE TABLE IF NOT EXISTS conversations (
31
+ id TEXT PRIMARY KEY, agentId TEXT NOT NULL, title TEXT,
32
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status TEXT DEFAULT 'active',
33
+ agentType TEXT, workingDirectory TEXT, model TEXT, isStreaming INTEGER DEFAULT 0,
34
+ claudeSessionId TEXT, subAgent TEXT, tags TEXT, pinned INTEGER DEFAULT 0,
35
+ sortOrder INTEGER DEFAULT 0, source TEXT DEFAULT 'gui'
36
+ )`);
37
+ db.run(`CREATE TABLE IF NOT EXISTS messages (
38
+ id TEXT PRIMARY KEY, conversationId TEXT NOT NULL, role TEXT NOT NULL,
39
+ content TEXT NOT NULL, created_at INTEGER NOT NULL
40
+ )`);
41
+ db.run(`CREATE TABLE IF NOT EXISTS sessions (
42
+ id TEXT PRIMARY KEY, conversationId TEXT NOT NULL, status TEXT NOT NULL,
43
+ started_at INTEGER NOT NULL, completed_at INTEGER, response TEXT, error TEXT,
44
+ run_id TEXT, input TEXT, config TEXT, interrupt TEXT, claudeSessionId TEXT
45
+ )`);
46
+ db.run(`CREATE TABLE IF NOT EXISTS chunks (
47
+ id TEXT PRIMARY KEY, sessionId TEXT NOT NULL, conversationId TEXT NOT NULL,
48
+ sequence INTEGER NOT NULL, type TEXT NOT NULL, data BLOB NOT NULL, created_at INTEGER NOT NULL
49
+ )`);
50
+ db.run(`CREATE INDEX IF NOT EXISTS idx_chunks_conv_created ON chunks(conversationId, created_at)`);
51
+ db.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(sessionId, sequence)`);
52
+ try { db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_unique ON chunks(sessionId, sequence)`); } catch(_) {}
53
+
54
+ const convId = randomUUID();
55
+ const now = Date.now();
56
+ const TURN_INTERVAL_MS = 30000;
57
+ const startTime = now - turns * TURN_INTERVAL_MS;
58
+
59
+ console.error(`[seed] inserting conversation ${convId} with ${turns} turns, ${chunksPerTurn} chunks/turn`);
60
+
61
+ db.run(
62
+ `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, agentType, workingDirectory, model)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
64
+ [convId, 'cli-claude', `Profiling Seed — ${turns} turns`, startTime, now, 'active', 'claude', '/home/user', 'claude-opus-4-6']
65
+ );
66
+
67
+ const insertMsg = db.prepare(
68
+ `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
69
+ );
70
+ const insertSession = db.prepare(
71
+ `INSERT INTO sessions (id, conversationId, status, started_at, completed_at) VALUES (?, ?, ?, ?, ?)`
72
+ );
73
+ const insertChunk = db.prepare(
74
+ `INSERT INTO chunks (id, sessionId, conversationId, sequence, type, data, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
75
+ );
76
+
77
+ const prompts = [
78
+ 'Read the file src/index.ts and explain what it does.',
79
+ 'Search for all uses of useEffect in the codebase.',
80
+ 'Write a function to validate email addresses.',
81
+ 'Fix the TypeScript error in lib/auth.ts line 42.',
82
+ 'Add unit tests for the formatDate utility.',
83
+ 'Refactor the database connection pool to use async/await.',
84
+ 'Find all TODO comments in the project.',
85
+ 'Implement pagination for the user list endpoint.',
86
+ 'Add error handling to the upload route.',
87
+ 'Create a migration to add the created_at column.',
88
+ ];
89
+ const toolNames = ['Read', 'Bash', 'Glob', 'Grep', 'Write', 'Edit'];
90
+ const filePaths = ['src/index.ts', 'lib/auth.ts', 'lib/db.ts', 'src/components/App.tsx', 'tests/auth.test.ts'];
91
+
92
+ let totalChunks = 0;
93
+
94
+ const runBatch = db.transaction((batchTurns) => {
95
+ for (const { turn, t } of batchTurns) {
96
+ const prompt = prompts[turn % prompts.length];
97
+ insertMsg.run(randomUUID(), convId, 'user', prompt, t);
98
+
99
+ const sessId = randomUUID();
100
+ const sessStart = t + 1000;
101
+ const sessEnd = sessStart + 8000 + (turn % 5) * 2000;
102
+ insertSession.run(sessId, convId, 'completed', sessStart, sessEnd);
103
+
104
+ let seq = 0;
105
+ insertChunk.run(
106
+ randomUUID(), sessId, convId, seq++, 'block',
107
+ JSON.stringify({ type: 'text', text: `I'll help with that. Let me analyze ${prompt.slice(0, 40)}...` }),
108
+ sessStart + 500
109
+ );
110
+
111
+ const pairs = Math.max(1, Math.floor((chunksPerTurn - 2) / 2));
112
+ for (let p = 0; p < pairs; p++) {
113
+ const tool = toolNames[(turn + p) % toolNames.length];
114
+ const file = filePaths[(turn + p) % filePaths.length];
115
+ const toolUseId = `tu_${turn}_${p}`;
116
+ const input = tool === 'Bash' ? { command: `cat ${file} | head -20` }
117
+ : tool === 'Glob' ? { pattern: '**/*.ts' }
118
+ : tool === 'Grep' ? { pattern: 'useEffect', path: '.' }
119
+ : { file_path: file };
120
+ insertChunk.run(
121
+ randomUUID(), sessId, convId, seq++, 'block',
122
+ JSON.stringify({ type: 'tool_use', id: toolUseId, name: tool, input }),
123
+ sessStart + 1000 + p * 800
124
+ );
125
+ const resultContent = tool === 'Read' ? `// ${file}\nexport function main() {\n return 42;\n}\n`
126
+ : tool === 'Bash' ? `stdout: Line 1\nLine 2\nLine 3\n`
127
+ : tool === 'Glob' ? `src/index.ts\nsrc/app.ts\n`
128
+ : `src/App.tsx:12: useEffect(() => {\n`;
129
+ insertChunk.run(
130
+ randomUUID(), sessId, convId, seq++, 'block',
131
+ JSON.stringify({ type: 'tool_result', tool_use_id: toolUseId, content: resultContent }),
132
+ sessStart + 1400 + p * 800
133
+ );
134
+ }
135
+
136
+ insertChunk.run(
137
+ randomUUID(), sessId, convId, seq++, 'block',
138
+ JSON.stringify({ type: 'text', text: `Done. The file looks correct. I've completed turn ${turn + 1}.` }),
139
+ sessEnd - 200
140
+ );
141
+
142
+ totalChunks += seq;
143
+ }
144
+ });
145
+
146
+ const BATCH = 500;
147
+ for (let i = 0; i < turns; i += BATCH) {
148
+ const batch = [];
149
+ for (let j = i; j < Math.min(i + BATCH, turns); j++) {
150
+ batch.push({ turn: j, t: startTime + j * TURN_INTERVAL_MS });
151
+ }
152
+ runBatch(batch);
153
+ process.stderr.write(`\r[seed] ${Math.min(i + BATCH, turns)}/${turns} turns`);
154
+ }
155
+
156
+ db.close();
157
+ process.stderr.write('\n');
158
+ console.error(`[seed] complete — ${totalChunks} total chunks for conv ${convId}`);
159
+ console.log(convId);
package/static/index.html CHANGED
@@ -294,20 +294,20 @@
294
294
  <script defer src="/gm/lib/msgpackr.min.js"></script>
295
295
  <script defer src="/gm/js/websocket-manager.js"></script>
296
296
  <script defer src="/gm/js/ws-client.js"></script>
297
- <script defer src="/gm/js/syntax-highlighter.js"></script>
297
+ <script defer src="/gm/js/syntax-highlighter.js"></script>
298
298
  <script defer src="/gm/js/dialogs.js"></script>
299
299
  <script defer src="/gm/js/ui-components.js"></script>
300
300
  <script defer src="/gm/js/state-barrier.js"></script>
301
- <script defer src="/gm/js/terminal.js"></script>
302
- <script defer src="/gm/js/script-runner.js"></script>
303
- <script defer src="/gm/js/tools-manager-ui.js"></script>
304
- <script defer src="/gm/js/tools-manager.js"></script>
305
- <script defer src="/gm/js/stt-handler.js"></script>
306
- <script defer src="/gm/js/voice.js"></script>
307
- <script defer src="/gm/js/pm2-monitor.js"></script>
308
- <script defer src="/gm/js/client.js"></script>
309
- <script defer src="/gm/js/features.js"></script>
310
- <script defer src="/gm/js/agent-auth.js"></script>
301
+ <script defer src="/gm/js/terminal.js"></script>
302
+ <script defer src="/gm/js/script-runner.js"></script>
303
+ <script defer src="/gm/js/tools-manager-ui.js"></script>
304
+ <script defer src="/gm/js/tools-manager.js"></script>
305
+ <script defer src="/gm/js/stt-handler.js"></script>
306
+ <script defer src="/gm/js/voice.js"></script>
307
+ <script defer src="/gm/js/pm2-monitor.js"></script>
308
+ <script defer src="/gm/js/client.js"></script>
309
+ <script defer src="/gm/js/features.js"></script>
310
+ <script defer src="/gm/js/agent-auth.js"></script>
311
311
 
312
312
  <script>
313
313
  const savedTheme = localStorage.getItem('theme') || 'light';
@@ -2156,6 +2156,56 @@ class AgentGUIClient {
2156
2156
  }
2157
2157
  }
2158
2158
 
2159
+ _hydrateSessionBlocks(blocksEl, list) {
2160
+ const blockFrag = document.createDocumentFragment();
2161
+ const deferred = [];
2162
+ for (const chunk of list) {
2163
+ if (!chunk.block?.type) continue;
2164
+ const bt = chunk.block.type;
2165
+ if (bt === 'tool_result' || bt === 'tool_status' || bt === 'hook_progress') { deferred.push(chunk); continue; }
2166
+ const el = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
2167
+ if (!el) continue;
2168
+ el.classList.add('block-loaded');
2169
+ blockFrag.appendChild(el);
2170
+ }
2171
+ blocksEl.appendChild(blockFrag);
2172
+ for (const chunk of deferred) {
2173
+ const b = chunk.block;
2174
+ if (b.type === 'tool_result') {
2175
+ const tid = b.tool_use_id;
2176
+ const toolUseEl = (tid ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`) : null)
2177
+ || (blocksEl.lastElementChild?.classList.contains('block-tool-use') ? blocksEl.lastElementChild : null);
2178
+ if (toolUseEl) this.renderer.mergeResultIntoToolUse(toolUseEl, b);
2179
+ } else if (b.type === 'tool_status') {
2180
+ const tid = b.tool_use_id;
2181
+ const toolUseEl = tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`);
2182
+ if (toolUseEl) {
2183
+ const isError = b.status === 'failed';
2184
+ const isDone = b.status === 'completed';
2185
+ if (isDone || isError) toolUseEl.classList.add(isError ? 'tool-result-error' : 'tool-result-success');
2186
+ }
2187
+ }
2188
+ }
2189
+ }
2190
+
2191
+ _getLazyObserver() {
2192
+ if (this._lazyObserver) return this._lazyObserver;
2193
+ if (typeof IntersectionObserver === 'undefined') return null;
2194
+ this._lazyObserver = new IntersectionObserver((entries) => {
2195
+ for (const entry of entries) {
2196
+ if (!entry.isIntersecting) continue;
2197
+ const msgDiv = entry.target;
2198
+ const pendingChunks = msgDiv._lazyChunks;
2199
+ if (!pendingChunks) continue;
2200
+ delete msgDiv._lazyChunks;
2201
+ this._lazyObserver.unobserve(msgDiv);
2202
+ const blocksEl = msgDiv.querySelector('.message-blocks');
2203
+ if (blocksEl) this._hydrateSessionBlocks(blocksEl, pendingChunks);
2204
+ }
2205
+ }, { rootMargin: '400px 0px' });
2206
+ return this._lazyObserver;
2207
+ }
2208
+
2159
2209
  _renderConversationContent(messagesContainer, chunks, userMessages, activeSessionId) {
2160
2210
  if (!chunks || chunks.length === 0) return;
2161
2211
  const sessionMap = new Map();
@@ -2163,6 +2213,13 @@ class AgentGUIClient {
2163
2213
  if (!sessionMap.has(chunk.sessionId)) sessionMap.set(chunk.sessionId, []);
2164
2214
  sessionMap.get(chunk.sessionId).push(chunk);
2165
2215
  }
2216
+
2217
+ const sessionIds = [...sessionMap.keys()];
2218
+ const EAGER_TAIL = 8;
2219
+ const eagerSet = new Set(sessionIds.slice(-EAGER_TAIL));
2220
+ if (activeSessionId) eagerSet.add(activeSessionId);
2221
+ const observer = sessionIds.length > EAGER_TAIL ? this._getLazyObserver() : null;
2222
+
2166
2223
  const frag = document.createDocumentFragment();
2167
2224
  let ui = 0;
2168
2225
  for (const [sid, list] of sessionMap) {
@@ -2179,39 +2236,17 @@ class AgentGUIClient {
2179
2236
  const msgDiv = document.createElement('div');
2180
2237
  msgDiv.className = `message message-assistant${isActive ? ' streaming-message' : ''}`;
2181
2238
  msgDiv.id = isActive ? `streaming-${sid}` : `message-${sid}`;
2239
+ msgDiv.setAttribute('data-session-id', sid);
2182
2240
  msgDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
2183
2241
  const blocksEl = msgDiv.querySelector('.message-blocks');
2184
- const blockFrag = document.createDocumentFragment();
2185
- const deferred = [];
2186
- for (const chunk of list) {
2187
- if (!chunk.block?.type) continue;
2188
- const bt = chunk.block.type;
2189
- if (bt === 'tool_result' || bt === 'tool_status' || bt === 'hook_progress') { deferred.push(chunk); continue; }
2190
- const el = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
2191
- if (!el) continue;
2192
- el.classList.add('block-loaded');
2193
- blockFrag.appendChild(el);
2194
- }
2195
- blocksEl.appendChild(blockFrag);
2196
- for (const chunk of deferred) {
2197
- const b = chunk.block;
2198
- if (b.type === 'tool_result') {
2199
- const tid = b.tool_use_id;
2200
- const toolUseEl = (tid ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`) : null)
2201
- || (blocksEl.lastElementChild?.classList.contains('block-tool-use') ? blocksEl.lastElementChild : null);
2202
- if (toolUseEl) this.renderer.mergeResultIntoToolUse(toolUseEl, b);
2203
- } else if (b.type === 'tool_status') {
2204
- const tid = b.tool_use_id;
2205
- const toolUseEl = tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`);
2206
- if (toolUseEl) {
2207
- const isError = b.status === 'failed';
2208
- const isDone = b.status === 'completed';
2209
- if (isDone || isError) {
2210
- toolUseEl.classList.add(isError ? 'tool-result-error' : 'tool-result-success');
2211
- }
2212
- }
2213
- }
2242
+
2243
+ if (observer && !eagerSet.has(sid)) {
2244
+ msgDiv._lazyChunks = list;
2245
+ observer.observe(msgDiv);
2246
+ } else {
2247
+ this._hydrateSessionBlocks(blocksEl, list);
2214
2248
  }
2249
+
2215
2250
  if (isActive) {
2216
2251
  const ind = document.createElement('div');
2217
2252
  ind.className = 'streaming-indicator';
@@ -2753,6 +2788,7 @@ class AgentGUIClient {
2753
2788
  }
2754
2789
 
2755
2790
  async loadConversationMessages(conversationId) {
2791
+ performance.mark(`conv-load-start:${conversationId}`);
2756
2792
  try {
2757
2793
  if (this._previousConvAbort) {
2758
2794
  this._previousConvAbort.abort();
@@ -2822,11 +2858,13 @@ class AgentGUIClient {
2822
2858
 
2823
2859
  this.conversationCache.delete(conversationId);
2824
2860
 
2861
+ if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
2825
2862
  this._showSkeletonLoading(conversationId);
2826
2863
 
2827
2864
  let fullData;
2828
2865
  try {
2829
2866
  fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
2867
+ performance.mark(`conv-data-received:${conversationId}`);
2830
2868
  if (convSignal.aborted) return;
2831
2869
  } catch (wsErr) {
2832
2870
  if (wsErr.code === 404) {
@@ -2954,7 +2992,11 @@ class AgentGUIClient {
2954
2992
 
2955
2993
  if (chunks.length > 0) {
2956
2994
  const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
2995
+ performance.mark(`conv-render-start:${conversationId}`);
2957
2996
  if (!convSignal.aborted) this._renderConversationContent(messagesEl, chunks, userMessages, activeSessionId);
2997
+ performance.mark(`conv-render-complete:${conversationId}`);
2998
+ performance.measure(`conv-render:${conversationId}`, `conv-render-start:${conversationId}`, `conv-render-complete:${conversationId}`);
2999
+ performance.measure(`conv-data-fetch:${conversationId}`, `conv-load-start:${conversationId}`, `conv-data-received:${conversationId}`);
2958
3000
  } else {
2959
3001
  if (!convSignal.aborted) messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
2960
3002
  }
@@ -3404,6 +3446,11 @@ class AgentGUIClient {
3404
3446
  }
3405
3447
  }
3406
3448
 
3449
+ window.__convPerfMetrics = () => {
3450
+ const entries = performance.getEntriesByType('measure').filter(e => e.name.startsWith('conv-'));
3451
+ return entries.map(e => ({ name: e.name, ms: Math.round(e.duration) }));
3452
+ };
3453
+
3407
3454
  // Global instance
3408
3455
  let agentGUIClient = null;
3409
3456
 
@@ -3413,7 +3460,7 @@ document.addEventListener('DOMContentLoaded', async () => {
3413
3460
  agentGUIClient = new AgentGUIClient();
3414
3461
  window.agentGuiClient = agentGUIClient;
3415
3462
  await agentGUIClient.init();
3416
- this._dbg('AgentGUI ready');
3463
+ agentGUIClient._dbg('AgentGUI ready');
3417
3464
  } catch (error) {
3418
3465
  console.error('Failed to initialize AgentGUI:', error);
3419
3466
  }
@@ -66,25 +66,11 @@ class StreamingRenderer {
66
66
  throw new Error(`Output container not found: ${outputContainerId}`);
67
67
  }
68
68
 
69
- this.setupDOMObserver();
70
- this.setupResizeObserver();
71
69
  this.setupScrollOptimization();
72
70
  StreamingRenderer._setupGlobalLazyHL();
73
71
  return this;
74
72
  }
75
73
 
76
- /**
77
- * Setup DOM mutation observer for external changes
78
- */
79
- setupDOMObserver() {
80
- }
81
-
82
- /**
83
- * Setup resize observer for viewport changes
84
- */
85
- setupResizeObserver() {
86
- }
87
-
88
74
  /**
89
75
  * Setup scroll optimization and auto-scroll
90
76
  */
@@ -2273,9 +2259,6 @@ class StreamingRenderer {
2273
2259
  this._userScrolledUp = false;
2274
2260
  }
2275
2261
 
2276
- updateVirtualScroll() {
2277
- }
2278
-
2279
2262
  /**
2280
2263
  * Update DOM node count for monitoring
2281
2264
  */
package/lib/acp-runner.js DELETED
@@ -1,136 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { spawnSync } from 'child_process';
3
-
4
- const isWindows = process.platform === 'win32';
5
-
6
- function getSpawnOptions(cwd) {
7
- const options = { cwd, windowsHide: true };
8
- if (isWindows) options.shell = true;
9
- options.env = { ...process.env };
10
- delete options.env.CLAUDECODE;
11
- return options;
12
- }
13
-
14
- function resolveCommand(command, npxPackage) {
15
- const whichCmd = isWindows ? 'where' : 'which';
16
- const check = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
17
- if (check.status === 0 && (check.stdout || '').trim()) return { cmd: command, prefixArgs: [] };
18
- if (npxPackage) {
19
- if (spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 }).status === 0) return { cmd: 'npx', prefixArgs: ['--yes', npxPackage] };
20
- if (spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 }).status === 0) return { cmd: 'bun', prefixArgs: ['x', npxPackage] };
21
- }
22
- return { cmd: command, prefixArgs: [] };
23
- }
24
-
25
- export function runACPOnce(agent, prompt, cwd, config = {}) {
26
- return new Promise((resolve, reject) => {
27
- const { timeout = 300000, onEvent = null, onError = null } = config;
28
- let cmd, args;
29
- if (agent.requiresAdapter && agent.adapterCommand) { cmd = agent.adapterCommand; args = [...agent.adapterArgs]; }
30
- else { const resolved = resolveCommand(agent.command, agent.npxPackage); cmd = resolved.cmd; args = [...resolved.prefixArgs, ...agent.buildArgs(prompt, config)]; }
31
- const spawnOpts = getSpawnOptions(cwd);
32
- if (Object.keys(agent.spawnEnv).length > 0) spawnOpts.env = { ...spawnOpts.env, ...agent.spawnEnv };
33
- const proc = spawn(cmd, args, spawnOpts);
34
- if (config.onPid) { try { config.onPid(proc.pid); } catch (_) {} }
35
- if (config.onProcess) { try { config.onProcess(proc); } catch (_) {} }
36
- const outputs = [];
37
- let timedOut = false, sessionId = null, requestId = 0, initialized = false, stderrText = '';
38
- const timeoutHandle = setTimeout(() => { timedOut = true; proc.kill(); reject(new Error(`${agent.name} ACP timeout after ${timeout}ms`)); }, timeout);
39
-
40
- const handleMessage = (message) => {
41
- const normalized = agent.protocolHandler(message, { sessionId, initialized });
42
- if (!normalized) { if (message.id === 1 && message.result) initialized = true; return; }
43
- outputs.push(normalized);
44
- if (normalized.session_id) sessionId = normalized.session_id;
45
- if (onEvent) { try { onEvent(normalized); } catch (e) { console.error(`[${agent.id}] onEvent error: ${e.message}`); } }
46
- };
47
-
48
- proc.stdout.on('error', () => {});
49
- proc.stderr.on('error', () => {});
50
- let buffer = '';
51
- proc.stdout.on('data', (chunk) => {
52
- if (timedOut) return;
53
- buffer += chunk.toString();
54
- const lines = buffer.split('\n'); buffer = lines.pop();
55
- for (const line of lines) { if (line.trim()) { try { handleMessage(JSON.parse(line)); } catch (e) { console.error(`[${agent.id}] JSON parse error:`, line.substring(0, 100)); } } }
56
- });
57
- proc.stderr.on('data', (chunk) => { const t = chunk.toString(); stderrText += t; console.error(`[${agent.id}] stderr:`, t); if (onError) { try { onError(t); } catch (_) {} } });
58
-
59
- proc.stdin.on('error', () => {});
60
- proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestId, method: 'initialize', params: { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true }, clientInfo: { name: 'agentgui', title: 'AgentGUI', version: '1.0.0' } } }) + '\n');
61
-
62
- let sessionCreated = false;
63
- const checkInitAndSend = () => {
64
- if (initialized && !sessionCreated) {
65
- sessionCreated = true;
66
- const sp = { cwd, mcpServers: [] };
67
- if (config.model) sp.model = config.model;
68
- if (config.subAgent) sp.agent = config.subAgent;
69
- if (config.systemPrompt) sp.systemPrompt = config.systemPrompt;
70
- proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestId, method: 'session/new', params: sp }) + '\n');
71
- } else if (!initialized) { setTimeout(checkInitAndSend, 100); }
72
- };
73
-
74
- let promptId = null, completed = false, draining = false;
75
- const enhancedHandler = (message) => {
76
- if (message.id && message.result && message.result.sessionId) {
77
- sessionId = message.result.sessionId;
78
- promptId = ++requestId;
79
- proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: promptId, method: 'session/prompt', params: { sessionId, prompt: [{ type: 'text', text: prompt }] } }) + '\n');
80
- return;
81
- }
82
- if (message.id === promptId && message.result && message.result.stopReason) {
83
- completed = true; draining = true; clearTimeout(timeoutHandle);
84
- setTimeout(() => { draining = false; try { proc.kill(); } catch (_) {} resolve({ outputs, sessionId }); }, 1000);
85
- return;
86
- }
87
- if (message.id === promptId && message.error) {
88
- completed = true; draining = true; clearTimeout(timeoutHandle); handleMessage(message);
89
- setTimeout(() => { draining = false; try { proc.kill(); } catch (_) {} reject(new Error(message.error.message || 'ACP prompt error')); }, 1000);
90
- return;
91
- }
92
- handleMessage(message);
93
- };
94
-
95
- buffer = '';
96
- proc.stdout.removeAllListeners('data');
97
- proc.stdout.on('data', (chunk) => {
98
- if (timedOut || (completed && !draining)) return;
99
- buffer += chunk.toString();
100
- const lines = buffer.split('\n'); buffer = lines.pop();
101
- for (const line of lines) {
102
- if (line.trim()) {
103
- try { const m = JSON.parse(line); if (m.id === 1 && m.result) initialized = true; enhancedHandler(m); }
104
- catch (e) { console.error(`[${agent.id}] JSON parse error:`, line.substring(0, 100)); }
105
- }
106
- }
107
- });
108
- setTimeout(checkInitAndSend, 200);
109
-
110
- proc.on('close', (code) => {
111
- clearTimeout(timeoutHandle);
112
- if (timedOut || completed) return;
113
- if (buffer.trim()) { try { const m = JSON.parse(buffer.trim()); if (m.id === 1 && m.result) initialized = true; enhancedHandler(m); } catch (_) {} }
114
- if (code === 0 || outputs.length > 0) resolve({ outputs, sessionId });
115
- else { const err = new Error(`${agent.name} ACP exited with code ${code}${stderrText ? `: ${stderrText.substring(0, 200)}` : ''}`); err.isPrematureEnd = true; err.exitCode = code; err.stderrText = stderrText; reject(err); }
116
- });
117
- proc.on('error', (err) => { clearTimeout(timeoutHandle); reject(err); });
118
- });
119
- }
120
-
121
- export async function runACPWithRetry(agent, prompt, cwd, config = {}, _retryCount = 0) {
122
- const maxRetries = config.maxRetries ?? 1;
123
- try { return await runACPOnce(agent, prompt, cwd, config); }
124
- catch (err) {
125
- const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
126
- const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
127
- if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
128
- const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
129
- console.error(`[${agent.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
130
- await new Promise(r => setTimeout(r, delay));
131
- return runACPWithRetry(agent, prompt, cwd, config, _retryCount + 1);
132
- }
133
- if (err.isPrematureEnd) { const premErr = new Error(err.message); premErr.isPrematureEnd = true; premErr.exitCode = err.exitCode; premErr.stderrText = err.stderrText; throw premErr; }
134
- throw err;
135
- }
136
- }