agentgui 1.0.783 → 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 +11 -11
- package/static/js/client.js +78 -31
- package/static/js/streaming-renderer.js +0 -17
- 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
|
@@ -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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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';
|
package/static/js/client.js
CHANGED
|
@@ -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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
-
|
|
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
|
-
}
|