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 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.782",
3
+ "version": "1.0.784",
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
@@ -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
- ['vendor/rippleui.css','vendor/prism-dark.css','vendor/highlight-js.css','vendor/xterm.css'].forEach(function(h){
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
- ['vendor/highlight.min.js','vendor/xterm.min.js','vendor/xterm-addon-fit.min.js'].forEach(function(s){
19
- var e=document.createElement('script');e.defer=true;e.src=b+'/'+s;document.head.appendChild(e);
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
- <script defer src="/gm/js/terminal.js"></script>
286
- <script defer src="/gm/js/script-runner.js"></script>
287
- <script defer src="/gm/js/tools-manager-ui.js"></script>
288
- <script defer src="/gm/js/tools-manager.js"></script>
289
- <script defer src="/gm/js/stt-handler.js"></script>
290
- <script defer src="/gm/js/voice.js"></script>
291
- <script defer src="/gm/js/pm2-monitor.js"></script>
292
- <script defer src="/gm/js/client.js"></script>
293
- <script defer src="/gm/js/features.js"></script>
294
- <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>
295
311
 
296
312
  <script>
297
313
  const savedTheme = localStorage.getItem('theme') || 'light';
@@ -103,28 +103,22 @@ class AgentGUIClient {
103
103
  try {
104
104
  this._dbg('Initializing AgentGUI client');
105
105
 
106
- // Initialize renderer
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
- // Connect WebSocket before loading data (RPC requires connection)
123
- if (this.config.autoConnect) {
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
- const blockFrag = document.createDocumentFragment();
2191
- const deferred = [];
2192
- for (const chunk of list) {
2193
- if (!chunk.block?.type) continue;
2194
- const bt = chunk.block.type;
2195
- if (bt === 'tool_result' || bt === 'tool_status' || bt === 'hook_progress') { deferred.push(chunk); continue; }
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
- this._dbg('AgentGUI ready');
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
- }