agentgui 1.0.149 → 1.0.151
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/claude-runner.js +36 -26
- package/package.json +1 -1
- package/static/index.html +29 -12
- package/static/js/streaming-renderer.js +55 -18
package/lib/claude-runner.js
CHANGED
|
@@ -139,7 +139,24 @@ class AgentRunner {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
async runACP(prompt, cwd, config = {}) {
|
|
142
|
+
async runACP(prompt, cwd, config = {}, _retryCount = 0) {
|
|
143
|
+
const maxRetries = config.maxRetries ?? 1;
|
|
144
|
+
try {
|
|
145
|
+
return await this._runACPOnce(prompt, cwd, config);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const isEmptyExit = err.message && err.message.includes('ACP exited with code');
|
|
148
|
+
const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
|
|
149
|
+
if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
|
|
150
|
+
const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
|
|
151
|
+
console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
152
|
+
await new Promise(r => setTimeout(r, delay));
|
|
153
|
+
return this.runACP(prompt, cwd, config, _retryCount + 1);
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _runACPOnce(prompt, cwd, config = {}) {
|
|
143
160
|
return new Promise((resolve, reject) => {
|
|
144
161
|
const {
|
|
145
162
|
timeout = 300000,
|
|
@@ -147,11 +164,10 @@ class AgentRunner {
|
|
|
147
164
|
onError = null
|
|
148
165
|
} = config;
|
|
149
166
|
|
|
150
|
-
// Use adapter if required (e.g., for Claude Code via Zed adapter)
|
|
151
167
|
const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
|
|
152
168
|
const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : ['acp'];
|
|
153
169
|
const args = [...baseArgs];
|
|
154
|
-
|
|
170
|
+
|
|
155
171
|
const proc = spawn(cmd, args, { cwd });
|
|
156
172
|
|
|
157
173
|
if (config.onPid) {
|
|
@@ -163,6 +179,7 @@ class AgentRunner {
|
|
|
163
179
|
let sessionId = null;
|
|
164
180
|
let requestId = 0;
|
|
165
181
|
let initialized = false;
|
|
182
|
+
let stderrText = '';
|
|
166
183
|
|
|
167
184
|
const timeoutHandle = setTimeout(() => {
|
|
168
185
|
timedOut = true;
|
|
@@ -170,12 +187,9 @@ class AgentRunner {
|
|
|
170
187
|
reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
|
|
171
188
|
}, timeout);
|
|
172
189
|
|
|
173
|
-
// ACP protocol handler
|
|
174
190
|
const handleMessage = (message) => {
|
|
175
|
-
// Normalize ACP message to common format
|
|
176
191
|
const normalized = this.protocolHandler(message, { sessionId, initialized });
|
|
177
192
|
if (!normalized) {
|
|
178
|
-
// Check for initialization response
|
|
179
193
|
if (message.id === 1 && message.result) {
|
|
180
194
|
initialized = true;
|
|
181
195
|
}
|
|
@@ -217,13 +231,13 @@ class AgentRunner {
|
|
|
217
231
|
|
|
218
232
|
proc.stderr.on('data', (chunk) => {
|
|
219
233
|
const errorText = chunk.toString();
|
|
234
|
+
stderrText += errorText;
|
|
220
235
|
console.error(`[${this.id}] stderr:`, errorText);
|
|
221
236
|
if (onError) {
|
|
222
237
|
try { onError(errorText); } catch (e) {}
|
|
223
238
|
}
|
|
224
239
|
});
|
|
225
240
|
|
|
226
|
-
// Send ACP initialize request (protocolVersion must be an integer per ACP spec)
|
|
227
241
|
const initRequest = {
|
|
228
242
|
jsonrpc: '2.0',
|
|
229
243
|
id: ++requestId,
|
|
@@ -245,12 +259,10 @@ class AgentRunner {
|
|
|
245
259
|
|
|
246
260
|
let sessionCreated = false;
|
|
247
261
|
|
|
248
|
-
// Wait for initialization then create session and send prompt
|
|
249
262
|
const checkInitAndSend = () => {
|
|
250
263
|
if (initialized && !sessionCreated) {
|
|
251
264
|
sessionCreated = true;
|
|
252
|
-
|
|
253
|
-
// Step 1: Create a new session
|
|
265
|
+
|
|
254
266
|
const sessionRequest = {
|
|
255
267
|
jsonrpc: '2.0',
|
|
256
268
|
id: ++requestId,
|
|
@@ -269,14 +281,11 @@ class AgentRunner {
|
|
|
269
281
|
let promptId = null;
|
|
270
282
|
let completed = false;
|
|
271
283
|
|
|
272
|
-
// Handle session creation response and send prompt
|
|
273
284
|
const originalHandler = handleMessage;
|
|
274
285
|
const enhancedHandler = (message) => {
|
|
275
|
-
// Check for session/new response
|
|
276
286
|
if (message.id && message.result && message.result.sessionId) {
|
|
277
287
|
sessionId = message.result.sessionId;
|
|
278
|
-
|
|
279
|
-
// Step 2: Send the prompt
|
|
288
|
+
|
|
280
289
|
promptId = ++requestId;
|
|
281
290
|
const promptRequest = {
|
|
282
291
|
jsonrpc: '2.0',
|
|
@@ -290,8 +299,7 @@ class AgentRunner {
|
|
|
290
299
|
proc.stdin.write(JSON.stringify(promptRequest) + '\n');
|
|
291
300
|
return;
|
|
292
301
|
}
|
|
293
|
-
|
|
294
|
-
// Check for prompt response (end of turn)
|
|
302
|
+
|
|
295
303
|
if (message.id === promptId && message.result && message.result.stopReason) {
|
|
296
304
|
completed = true;
|
|
297
305
|
clearTimeout(timeoutHandle);
|
|
@@ -299,11 +307,10 @@ class AgentRunner {
|
|
|
299
307
|
resolve({ outputs, sessionId });
|
|
300
308
|
return;
|
|
301
309
|
}
|
|
302
|
-
|
|
310
|
+
|
|
303
311
|
originalHandler(message);
|
|
304
312
|
};
|
|
305
313
|
|
|
306
|
-
// Override the message handler
|
|
307
314
|
buffer = '';
|
|
308
315
|
proc.stdout.removeAllListeners('data');
|
|
309
316
|
proc.stdout.on('data', (chunk) => {
|
|
@@ -317,12 +324,11 @@ class AgentRunner {
|
|
|
317
324
|
if (line.trim()) {
|
|
318
325
|
try {
|
|
319
326
|
const message = JSON.parse(line);
|
|
320
|
-
|
|
321
|
-
// Check for initialization response
|
|
327
|
+
|
|
322
328
|
if (message.id === 1 && message.result) {
|
|
323
329
|
initialized = true;
|
|
324
330
|
}
|
|
325
|
-
|
|
331
|
+
|
|
326
332
|
enhancedHandler(message);
|
|
327
333
|
} catch (e) {
|
|
328
334
|
console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
|
|
@@ -340,7 +346,8 @@ class AgentRunner {
|
|
|
340
346
|
if (code === 0 || outputs.length > 0) {
|
|
341
347
|
resolve({ outputs, sessionId });
|
|
342
348
|
} else {
|
|
343
|
-
|
|
349
|
+
const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
|
|
350
|
+
reject(new Error(`${this.name} ACP exited with code ${code}${detail}`));
|
|
344
351
|
}
|
|
345
352
|
});
|
|
346
353
|
|
|
@@ -386,12 +393,15 @@ class AgentRegistry {
|
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
listACPAvailable() {
|
|
389
|
-
|
|
390
|
-
const { execSync } = require('child_process');
|
|
396
|
+
const { spawnSync } = require('child_process');
|
|
391
397
|
return this.list().filter(agent => {
|
|
392
398
|
try {
|
|
393
|
-
|
|
394
|
-
return
|
|
399
|
+
const which = spawnSync('which', [agent.command], { encoding: 'utf-8', timeout: 3000 });
|
|
400
|
+
if (which.status !== 0) return false;
|
|
401
|
+
const binPath = (which.stdout || '').trim();
|
|
402
|
+
if (!binPath) return false;
|
|
403
|
+
const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000 });
|
|
404
|
+
return check.status === 0 && (check.stdout || '').trim().length > 0;
|
|
395
405
|
} catch {
|
|
396
406
|
return false;
|
|
397
407
|
}
|
package/package.json
CHANGED
package/static/index.html
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
<title>AgentGUI</title>
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%233b82f6'/%3E%3Ctext x='50' y='68' font-size='50' font-family='sans-serif' font-weight='bold' fill='white' text-anchor='middle'%3EG%3C/text%3E%3C/svg%3E">
|
|
9
9
|
|
|
10
|
+
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
11
|
+
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
|
10
12
|
<link href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" rel="stylesheet">
|
|
11
|
-
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-dark.css" rel=
|
|
12
|
-
<link href="https://
|
|
13
|
-
<
|
|
13
|
+
<link rel="preload" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-dark.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
|
14
|
+
<noscript><link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-dark.css" rel="stylesheet"></noscript>
|
|
15
|
+
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
|
16
|
+
<noscript><link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" rel="stylesheet"></noscript>
|
|
17
|
+
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
|
14
18
|
|
|
15
19
|
<style>
|
|
16
20
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -1158,6 +1162,19 @@
|
|
|
1158
1162
|
font-size: 0.9rem;
|
|
1159
1163
|
}
|
|
1160
1164
|
|
|
1165
|
+
.block-text + .block-text {
|
|
1166
|
+
margin-top: -0.75rem;
|
|
1167
|
+
padding-top: 0;
|
|
1168
|
+
border-top-left-radius: 0;
|
|
1169
|
+
border-top-right-radius: 0;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.block-text:has(+ .block-text) {
|
|
1173
|
+
margin-bottom: 0;
|
|
1174
|
+
border-bottom-left-radius: 0;
|
|
1175
|
+
border-bottom-right-radius: 0;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1161
1178
|
.block-code {
|
|
1162
1179
|
margin-bottom: 0.75rem;
|
|
1163
1180
|
border-radius: 0.5rem;
|
|
@@ -2060,16 +2077,16 @@
|
|
|
2060
2077
|
</div>
|
|
2061
2078
|
</div>
|
|
2062
2079
|
|
|
2063
|
-
<script src="/gm/js/event-processor.js"></script>
|
|
2064
|
-
<script src="/gm/js/streaming-renderer.js"></script>
|
|
2065
|
-
<script src="/gm/js/websocket-manager.js"></script>
|
|
2066
|
-
<script src="/gm/js/event-filter.js"></script>
|
|
2067
|
-
<script src="/gm/js/syntax-highlighter.js"></script>
|
|
2068
|
-
<script src="/gm/js/ui-components.js"></script>
|
|
2069
|
-
<script src="/gm/js/conversations.js"></script>
|
|
2070
|
-
<script src="/gm/js/client.js"></script>
|
|
2080
|
+
<script defer src="/gm/js/event-processor.js"></script>
|
|
2081
|
+
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
2082
|
+
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
2083
|
+
<script defer src="/gm/js/event-filter.js"></script>
|
|
2084
|
+
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
2085
|
+
<script defer src="/gm/js/ui-components.js"></script>
|
|
2086
|
+
<script defer src="/gm/js/conversations.js"></script>
|
|
2087
|
+
<script defer src="/gm/js/client.js"></script>
|
|
2071
2088
|
<script type="module" src="/gm/js/voice.js"></script>
|
|
2072
|
-
<script src="/gm/js/features.js"></script>
|
|
2089
|
+
<script defer src="/gm/js/features.js"></script>
|
|
2073
2090
|
|
|
2074
2091
|
<script>
|
|
2075
2092
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
@@ -135,12 +135,17 @@ class StreamingRenderer {
|
|
|
135
135
|
const lastTime = this.dedupMap.get(key);
|
|
136
136
|
const now = Date.now();
|
|
137
137
|
|
|
138
|
-
// Deduplicate within 100ms window
|
|
139
138
|
if (lastTime && (now - lastTime) < 100) {
|
|
140
139
|
return true;
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
this.dedupMap.set(key, now);
|
|
143
|
+
if (this.dedupMap.size > 5000) {
|
|
144
|
+
const cutoff = now - 1000;
|
|
145
|
+
for (const [k, t] of this.dedupMap) {
|
|
146
|
+
if (t < cutoff) this.dedupMap.delete(k);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
144
149
|
return false;
|
|
145
150
|
}
|
|
146
151
|
|
|
@@ -358,17 +363,25 @@ class StreamingRenderer {
|
|
|
358
363
|
* Render text block with semantic HTML
|
|
359
364
|
*/
|
|
360
365
|
renderBlockText(block, context) {
|
|
361
|
-
const div = document.createElement('div');
|
|
362
|
-
div.className = 'block-text';
|
|
363
|
-
|
|
364
366
|
const text = block.text || '';
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
const isHtml = this.containsHtmlTags(text);
|
|
368
|
+
const cached = this.renderCache.get(text);
|
|
369
|
+
const html = cached || (isHtml ? this.sanitizeHtml(text) : this.parseAndRenderMarkdown(text));
|
|
370
|
+
|
|
371
|
+
if (!cached && this.renderCache.size < 2000) {
|
|
372
|
+
this.renderCache.set(text, html);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const lastChild = this.outputContainer && this.outputContainer.lastElementChild;
|
|
376
|
+
if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
|
|
377
|
+
lastChild.innerHTML += html;
|
|
378
|
+
return null;
|
|
370
379
|
}
|
|
371
380
|
|
|
381
|
+
const div = document.createElement('div');
|
|
382
|
+
div.className = 'block-text';
|
|
383
|
+
if (isHtml) div.classList.add('html-content');
|
|
384
|
+
div.innerHTML = html;
|
|
372
385
|
return div;
|
|
373
386
|
}
|
|
374
387
|
|
|
@@ -447,11 +460,9 @@ class StreamingRenderer {
|
|
|
447
460
|
|
|
448
461
|
const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;border-top:none;margin:0";
|
|
449
462
|
const codeContainer = document.createElement('div');
|
|
463
|
+
codeContainer.innerHTML = `<pre style="${preStyle}"><code>${this.escapeHtml(code)}</code></pre>`;
|
|
450
464
|
if (typeof hljs !== 'undefined') {
|
|
451
|
-
|
|
452
|
-
codeContainer.innerHTML = `<pre style="${preStyle}"><code class="hljs">${result.value}</code></pre>`;
|
|
453
|
-
} else {
|
|
454
|
-
codeContainer.innerHTML = `<pre style="${preStyle}"><code>${this.escapeHtml(code)}</code></pre>`;
|
|
465
|
+
this.lazyHighlight(codeContainer, code);
|
|
455
466
|
}
|
|
456
467
|
|
|
457
468
|
details.appendChild(summary);
|
|
@@ -1785,13 +1796,39 @@ class StreamingRenderer {
|
|
|
1785
1796
|
* Auto-scroll to bottom of container
|
|
1786
1797
|
*/
|
|
1787
1798
|
autoScroll() {
|
|
1788
|
-
if (this.
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1799
|
+
if (this._scrollRafPending) return;
|
|
1800
|
+
this._scrollRafPending = true;
|
|
1801
|
+
requestAnimationFrame(() => {
|
|
1802
|
+
this._scrollRafPending = false;
|
|
1803
|
+
if (this.scrollContainer) {
|
|
1804
|
+
try { this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; } catch (_) {}
|
|
1793
1805
|
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
lazyHighlight(container, code) {
|
|
1810
|
+
if (!this._hlObserver) {
|
|
1811
|
+
this._hlObserver = new IntersectionObserver((entries) => {
|
|
1812
|
+
for (const entry of entries) {
|
|
1813
|
+
if (!entry.isIntersecting) continue;
|
|
1814
|
+
const el = entry.target;
|
|
1815
|
+
const raw = el._rawCode;
|
|
1816
|
+
if (!raw) continue;
|
|
1817
|
+
this._hlObserver.unobserve(el);
|
|
1818
|
+
try {
|
|
1819
|
+
const codeEl = el.querySelector('code');
|
|
1820
|
+
if (codeEl && typeof hljs !== 'undefined') {
|
|
1821
|
+
const result = hljs.highlightAuto(raw);
|
|
1822
|
+
codeEl.classList.add('hljs');
|
|
1823
|
+
codeEl.innerHTML = result.value;
|
|
1824
|
+
}
|
|
1825
|
+
} catch (_) {}
|
|
1826
|
+
delete el._rawCode;
|
|
1827
|
+
}
|
|
1828
|
+
}, { rootMargin: '200px' });
|
|
1794
1829
|
}
|
|
1830
|
+
container._rawCode = code;
|
|
1831
|
+
this._hlObserver.observe(container);
|
|
1795
1832
|
}
|
|
1796
1833
|
|
|
1797
1834
|
updateVirtualScroll() {
|