agentgui 1.0.782 → 1.0.784
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 +6 -10
- package/package.json +1 -1
- package/scripts/seed-large-conversation.js +159 -0
- package/static/index.html +31 -15
- package/static/js/client.js +84 -43
- package/static/js/streaming-renderer.js +0 -17
- package/static/js/tools-manager.js +2 -2
- package/lib/acp-runner.js +0 -136
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
|
|
1042
|
-
|
|
1041
|
+
const total = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ? AND created_at < ?')
|
|
1042
|
+
.get(conversationId, beforeTimestamp).count;
|
|
1043
1043
|
|
|
1044
|
-
const
|
|
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:
|
|
1062
|
+
hasMore: rows.length === limit
|
|
1067
1063
|
};
|
|
1068
1064
|
},
|
|
1069
1065
|
|
package/package.json
CHANGED
|
@@ -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
|
@@ -6,23 +6,39 @@
|
|
|
6
6
|
<meta name="description" content="AgentGUI - Real-time Claude Code Execution Visualization">
|
|
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
|
+
<link rel="preload" href="/gm/css/main.css" as="style">
|
|
10
|
+
<link rel="preload" href="/gm/lib/xstate.umd.min.js" as="script">
|
|
11
|
+
<link rel="preload" href="/gm/lib/msgpackr.min.js" as="script">
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
<script>
|
|
13
16
|
(function(){
|
|
14
17
|
var b=(window.__BASE_URL||'');
|
|
15
|
-
|
|
18
|
+
// Critical CSS only - rippleui needed for layout
|
|
19
|
+
['vendor/rippleui.css'].forEach(function(h){
|
|
16
20
|
var l=document.createElement('link');l.rel='stylesheet';l.href=b+'/'+h;document.head.appendChild(l);
|
|
17
21
|
});
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// Non-critical CSS - load async via media trick
|
|
23
|
+
['vendor/prism-dark.css','vendor/highlight-js.css','vendor/xterm.css'].forEach(function(h){
|
|
24
|
+
var l=document.createElement('link');l.rel='stylesheet';l.href=b+'/'+h;l.media='print';l.onload=function(){l.media='all';};document.head.appendChild(l);
|
|
20
25
|
});
|
|
26
|
+
// Vendor JS - lazy load on idle or first use
|
|
27
|
+
window._vendorLoaded = false;
|
|
28
|
+
window._loadVendorJS = function() {
|
|
29
|
+
if (window._vendorLoaded) return;
|
|
30
|
+
window._vendorLoaded = true;
|
|
31
|
+
['vendor/highlight.min.js','vendor/xterm.min.js','vendor/xterm-addon-fit.min.js'].forEach(function(s){
|
|
32
|
+
var e=document.createElement('script');e.defer=true;e.src=b+'/'+s;document.head.appendChild(e);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
if (typeof requestIdleCallback !== 'undefined') requestIdleCallback(window._loadVendorJS, { timeout: 3000 });
|
|
36
|
+
else setTimeout(window._loadVendorJS, 1500);
|
|
21
37
|
})();
|
|
22
38
|
</script>
|
|
23
39
|
|
|
24
40
|
<link rel="stylesheet" href="/gm/css/main.css">
|
|
25
|
-
<link rel="stylesheet" href="/gm/css/tools-popup.css">
|
|
41
|
+
<link rel="stylesheet" href="/gm/css/tools-popup.css" media="print" onload="this.media='all'">
|
|
26
42
|
</head>
|
|
27
43
|
<body>
|
|
28
44
|
<!-- Sidebar overlay (mobile) -->
|
|
@@ -278,20 +294,20 @@
|
|
|
278
294
|
<script defer src="/gm/lib/msgpackr.min.js"></script>
|
|
279
295
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
280
296
|
<script defer src="/gm/js/ws-client.js"></script>
|
|
281
|
-
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
297
|
+
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
282
298
|
<script defer src="/gm/js/dialogs.js"></script>
|
|
283
299
|
<script defer src="/gm/js/ui-components.js"></script>
|
|
284
300
|
<script defer src="/gm/js/state-barrier.js"></script>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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>
|
|
295
311
|
|
|
296
312
|
<script>
|
|
297
313
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
package/static/js/client.js
CHANGED
|
@@ -103,28 +103,22 @@ class AgentGUIClient {
|
|
|
103
103
|
try {
|
|
104
104
|
this._dbg('Initializing AgentGUI client');
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// Start WebSocket connection immediately (don't wait for UI setup)
|
|
107
|
+
const wsReady = this.config.autoConnect ? this.connectWebSocket() : Promise.resolve();
|
|
108
|
+
|
|
109
|
+
// Initialize renderer and UI in parallel with WS connection
|
|
107
110
|
this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
|
|
108
111
|
|
|
109
|
-
// Initialize image loader
|
|
110
112
|
if (typeof ImageLoader !== 'undefined') {
|
|
111
113
|
window.imageLoader = new ImageLoader();
|
|
112
|
-
this._dbg('Image loader initialized');
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
// Setup event listeners
|
|
116
116
|
this.setupWebSocketListeners();
|
|
117
117
|
this.setupRendererListeners();
|
|
118
|
-
|
|
119
|
-
// Setup UI elements (must happen before loading data so DOM refs exist)
|
|
120
118
|
this.setupUI();
|
|
121
119
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
await this.connectWebSocket();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Load initial data in parallel - none of these depend on each other
|
|
120
|
+
// Wait for WS, then load data in parallel
|
|
121
|
+
await wsReady;
|
|
128
122
|
await Promise.all([
|
|
129
123
|
this.loadAgents(),
|
|
130
124
|
this.loadConversations(),
|
|
@@ -2162,6 +2156,56 @@ class AgentGUIClient {
|
|
|
2162
2156
|
}
|
|
2163
2157
|
}
|
|
2164
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
|
+
|
|
2165
2209
|
_renderConversationContent(messagesContainer, chunks, userMessages, activeSessionId) {
|
|
2166
2210
|
if (!chunks || chunks.length === 0) return;
|
|
2167
2211
|
const sessionMap = new Map();
|
|
@@ -2169,6 +2213,13 @@ class AgentGUIClient {
|
|
|
2169
2213
|
if (!sessionMap.has(chunk.sessionId)) sessionMap.set(chunk.sessionId, []);
|
|
2170
2214
|
sessionMap.get(chunk.sessionId).push(chunk);
|
|
2171
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
|
+
|
|
2172
2223
|
const frag = document.createDocumentFragment();
|
|
2173
2224
|
let ui = 0;
|
|
2174
2225
|
for (const [sid, list] of sessionMap) {
|
|
@@ -2185,39 +2236,17 @@ class AgentGUIClient {
|
|
|
2185
2236
|
const msgDiv = document.createElement('div');
|
|
2186
2237
|
msgDiv.className = `message message-assistant${isActive ? ' streaming-message' : ''}`;
|
|
2187
2238
|
msgDiv.id = isActive ? `streaming-${sid}` : `message-${sid}`;
|
|
2239
|
+
msgDiv.setAttribute('data-session-id', sid);
|
|
2188
2240
|
msgDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
2189
2241
|
const blocksEl = msgDiv.querySelector('.message-blocks');
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
const el = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
|
|
2197
|
-
if (!el) continue;
|
|
2198
|
-
el.classList.add('block-loaded');
|
|
2199
|
-
blockFrag.appendChild(el);
|
|
2200
|
-
}
|
|
2201
|
-
blocksEl.appendChild(blockFrag);
|
|
2202
|
-
for (const chunk of deferred) {
|
|
2203
|
-
const b = chunk.block;
|
|
2204
|
-
if (b.type === 'tool_result') {
|
|
2205
|
-
const tid = b.tool_use_id;
|
|
2206
|
-
const toolUseEl = (tid ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`) : null)
|
|
2207
|
-
|| (blocksEl.lastElementChild?.classList.contains('block-tool-use') ? blocksEl.lastElementChild : null);
|
|
2208
|
-
if (toolUseEl) this.renderer.mergeResultIntoToolUse(toolUseEl, b);
|
|
2209
|
-
} else if (b.type === 'tool_status') {
|
|
2210
|
-
const tid = b.tool_use_id;
|
|
2211
|
-
const toolUseEl = tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`);
|
|
2212
|
-
if (toolUseEl) {
|
|
2213
|
-
const isError = b.status === 'failed';
|
|
2214
|
-
const isDone = b.status === 'completed';
|
|
2215
|
-
if (isDone || isError) {
|
|
2216
|
-
toolUseEl.classList.add(isError ? 'tool-result-error' : 'tool-result-success');
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2242
|
+
|
|
2243
|
+
if (observer && !eagerSet.has(sid)) {
|
|
2244
|
+
msgDiv._lazyChunks = list;
|
|
2245
|
+
observer.observe(msgDiv);
|
|
2246
|
+
} else {
|
|
2247
|
+
this._hydrateSessionBlocks(blocksEl, list);
|
|
2220
2248
|
}
|
|
2249
|
+
|
|
2221
2250
|
if (isActive) {
|
|
2222
2251
|
const ind = document.createElement('div');
|
|
2223
2252
|
ind.className = 'streaming-indicator';
|
|
@@ -2759,6 +2788,7 @@ class AgentGUIClient {
|
|
|
2759
2788
|
}
|
|
2760
2789
|
|
|
2761
2790
|
async loadConversationMessages(conversationId) {
|
|
2791
|
+
performance.mark(`conv-load-start:${conversationId}`);
|
|
2762
2792
|
try {
|
|
2763
2793
|
if (this._previousConvAbort) {
|
|
2764
2794
|
this._previousConvAbort.abort();
|
|
@@ -2828,11 +2858,13 @@ class AgentGUIClient {
|
|
|
2828
2858
|
|
|
2829
2859
|
this.conversationCache.delete(conversationId);
|
|
2830
2860
|
|
|
2861
|
+
if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
|
|
2831
2862
|
this._showSkeletonLoading(conversationId);
|
|
2832
2863
|
|
|
2833
2864
|
let fullData;
|
|
2834
2865
|
try {
|
|
2835
2866
|
fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
|
|
2867
|
+
performance.mark(`conv-data-received:${conversationId}`);
|
|
2836
2868
|
if (convSignal.aborted) return;
|
|
2837
2869
|
} catch (wsErr) {
|
|
2838
2870
|
if (wsErr.code === 404) {
|
|
@@ -2960,7 +2992,11 @@ class AgentGUIClient {
|
|
|
2960
2992
|
|
|
2961
2993
|
if (chunks.length > 0) {
|
|
2962
2994
|
const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
|
|
2995
|
+
performance.mark(`conv-render-start:${conversationId}`);
|
|
2963
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}`);
|
|
2964
3000
|
} else {
|
|
2965
3001
|
if (!convSignal.aborted) messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
|
|
2966
3002
|
}
|
|
@@ -3410,6 +3446,11 @@ class AgentGUIClient {
|
|
|
3410
3446
|
}
|
|
3411
3447
|
}
|
|
3412
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
|
+
|
|
3413
3454
|
// Global instance
|
|
3414
3455
|
let agentGUIClient = null;
|
|
3415
3456
|
|
|
@@ -3419,7 +3460,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
3419
3460
|
agentGUIClient = new AgentGUIClient();
|
|
3420
3461
|
window.agentGuiClient = agentGUIClient;
|
|
3421
3462
|
await agentGUIClient.init();
|
|
3422
|
-
|
|
3463
|
+
agentGUIClient._dbg('AgentGUI ready');
|
|
3423
3464
|
} catch (error) {
|
|
3424
3465
|
console.error('Failed to initialize AgentGUI:', error);
|
|
3425
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
|
*/
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
var tools = [];
|
|
5
5
|
var isRefreshing = false;
|
|
6
6
|
|
|
7
|
+
var hasRefreshed = false;
|
|
7
8
|
function init() {
|
|
8
9
|
if (!btn || !popup) return;
|
|
9
10
|
btn.style.display = 'flex';
|
|
10
|
-
btn.addEventListener('click', togglePopup);
|
|
11
|
+
btn.addEventListener('click', function() { if (!hasRefreshed) { hasRefreshed = true; refresh(); } togglePopup(); });
|
|
11
12
|
document.addEventListener('click', function(e) {
|
|
12
13
|
if (!btn.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
13
14
|
});
|
|
14
15
|
window.addEventListener('ws-message', onWsMessage);
|
|
15
16
|
initVoiceControls();
|
|
16
|
-
refresh();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function initVoiceControls() {
|
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
|
-
}
|