agentgui 1.0.741 → 1.0.743

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.
@@ -117,6 +117,24 @@ export function register(router, deps) {
117
117
  return { ok: true, chunks: result.chunks, total: result.total, hasMore: result.hasMore, limit: result.limit };
118
118
  });
119
119
 
120
+ router.handle('conv.export', (p) => {
121
+ const conv = queries.getConversation(p.id);
122
+ if (!conv) notFound();
123
+ const msgs = queries.getConversationMessages(p.id);
124
+ const format = p.format || 'markdown';
125
+ if (format === 'json') return { conversation: conv, messages: msgs };
126
+ let md = `# ${conv.title || 'Conversation'}\n\n`;
127
+ md += `Agent: ${conv.agentType || 'unknown'} | Created: ${new Date(conv.created_at).toISOString()}\n\n---\n\n`;
128
+ for (const m of msgs) {
129
+ const role = m.role === 'user' ? 'User' : 'Assistant';
130
+ md += `## ${role}\n\n`;
131
+ let content = m.content;
132
+ try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { content = parsed.map(b => b.text || b.content || JSON.stringify(b)).join('\n'); } } catch {}
133
+ md += content + '\n\n---\n\n';
134
+ }
135
+ return { markdown: md, title: conv.title };
136
+ });
137
+
120
138
  router.handle('conv.cancel', (p) => {
121
139
  if (!execMachine.isActive(p.id)) notFound('No active execution to cancel');
122
140
  const ctx = execMachine.getContext(p.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.741",
3
+ "version": "1.0.743",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -3684,6 +3684,41 @@ const _assetCache = new LRUCache({ max: 200 });
3684
3684
  let _htmlCache = null;
3685
3685
  let _htmlCacheEtag = null;
3686
3686
 
3687
+ function warmAssetCache() {
3688
+ const dirs = ['js', 'css', 'lib', 'vendor'];
3689
+ let count = 0;
3690
+ for (const dir of dirs) {
3691
+ const full = path.join(staticDir, dir);
3692
+ if (!fs.existsSync(full)) continue;
3693
+ for (const file of fs.readdirSync(full)) {
3694
+ const filePath = path.join(full, file);
3695
+ try {
3696
+ const stats = fs.statSync(filePath);
3697
+ if (!stats.isFile()) continue;
3698
+ const etag = generateETag(stats);
3699
+ if (_assetCache.has(etag)) continue;
3700
+ const raw = fs.readFileSync(filePath);
3701
+ const entry = raw.length < 860 ? { raw, gz: null } : { raw, gz: zlib.gzipSync(raw, { level: 6 }) };
3702
+ _assetCache.set(etag, entry);
3703
+ count++;
3704
+ } catch (_) {}
3705
+ }
3706
+ }
3707
+ for (const file of ['app.js', 'theme.js']) {
3708
+ const filePath = path.join(staticDir, file);
3709
+ try {
3710
+ const stats = fs.statSync(filePath);
3711
+ const etag = generateETag(stats);
3712
+ if (!_assetCache.has(etag)) {
3713
+ const raw = fs.readFileSync(filePath);
3714
+ _assetCache.set(etag, raw.length < 860 ? { raw, gz: null } : { raw, gz: zlib.gzipSync(raw, { level: 6 }) });
3715
+ count++;
3716
+ }
3717
+ } catch (_) {}
3718
+ }
3719
+ if (count > 0) console.log(`[CACHE] Pre-warmed ${count} static assets`);
3720
+ }
3721
+
3687
3722
  function serveFile(filePath, res, req) {
3688
3723
  const ext = path.extname(filePath).toLowerCase();
3689
3724
  const contentType = MIME_TYPES[ext] || 'application/octet-stream';
@@ -5109,6 +5144,7 @@ function onServerReady() {
5109
5144
  }
5110
5145
 
5111
5146
  recoverStaleSessions();
5147
+ warmAssetCache();
5112
5148
 
5113
5149
  try {
5114
5150
  jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
@@ -5120,6 +5156,20 @@ function onServerReady() {
5120
5156
 
5121
5157
  resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
5122
5158
 
5159
+ setInterval(() => {
5160
+ try {
5161
+ const streaming = queries.getStreamingConversations();
5162
+ let cleared = 0;
5163
+ for (const c of streaming) {
5164
+ if (!activeExecutions.has(c.id)) {
5165
+ queries.setIsStreaming(c.id, false);
5166
+ cleared++;
5167
+ }
5168
+ }
5169
+ if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
5170
+ } catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
5171
+ }, 5 * 60 * 1000);
5172
+
5123
5173
  installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
5124
5174
 
5125
5175
  startACPTools().then(() => {
@@ -90,14 +90,18 @@ class AgentGUIClient {
90
90
  currentConversationId: null,
91
91
  currentSessionId: null
92
92
  };
93
+
94
+ this._debug = typeof localStorage !== 'undefined' && localStorage.getItem('debug') === '1';
93
95
  }
94
96
 
97
+ _dbg(...args) { if (this._debug) this._dbg(...args); }
98
+
95
99
  /**
96
100
  * Initialize the client
97
101
  */
98
102
  async init() {
99
103
  try {
100
- console.log('Initializing AgentGUI client');
104
+ this._dbg('Initializing AgentGUI client');
101
105
 
102
106
  // Initialize renderer
103
107
  this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
@@ -105,7 +109,7 @@ class AgentGUIClient {
105
109
  // Initialize image loader
106
110
  if (typeof ImageLoader !== 'undefined') {
107
111
  window.imageLoader = new ImageLoader();
108
- console.log('Image loader initialized');
112
+ this._dbg('Image loader initialized');
109
113
  }
110
114
 
111
115
  // Setup event listeners
@@ -137,7 +141,7 @@ class AgentGUIClient {
137
141
  this.emit('initialized');
138
142
  this._setupDebugHooks();
139
143
 
140
- console.log('AgentGUI client initialized');
144
+ this._dbg('AgentGUI client initialized');
141
145
  return this;
142
146
  } catch (error) {
143
147
  console.error('Client initialization error:', error);
@@ -151,7 +155,7 @@ class AgentGUIClient {
151
155
  */
152
156
  setupWebSocketListeners() {
153
157
  this.wsManager.on('connected', () => {
154
- console.log('WebSocket connected');
158
+ this._dbg('WebSocket connected');
155
159
  this.updateConnectionStatus('connected');
156
160
  this._subscribeToConversationUpdates();
157
161
  // On reconnect (not initial connect), invalidate current conversation's DOM
@@ -170,7 +174,7 @@ class AgentGUIClient {
170
174
  if (window.__SERVER_VERSION) {
171
175
  fetch((window.__BASE_URL || '') + '/api/version').then(r => r.json()).then(d => {
172
176
  if (d.version && d.version !== window.__SERVER_VERSION) {
173
- console.log(`Server updated ${window.__SERVER_VERSION} → ${d.version}, reloading`);
177
+ this._dbg(`Server updated ${window.__SERVER_VERSION} → ${d.version}, reloading`);
174
178
  window.location.reload();
175
179
  }
176
180
  }).catch(() => {});
@@ -178,7 +182,7 @@ class AgentGUIClient {
178
182
  });
179
183
 
180
184
  this.wsManager.on('disconnected', () => {
181
- console.log('WebSocket disconnected');
185
+ this._dbg('WebSocket disconnected');
182
186
  this.updateConnectionStatus('disconnected');
183
187
  this.updateSendButtonState();
184
188
  this.disablePromptArea();
@@ -186,7 +190,7 @@ class AgentGUIClient {
186
190
  });
187
191
 
188
192
  this.wsManager.on('reconnecting', (data) => {
189
- console.log('WebSocket reconnecting:', data);
193
+ this._dbg('WebSocket reconnecting:', data);
190
194
  this.updateConnectionStatus('reconnecting');
191
195
  });
192
196
 
@@ -266,7 +270,7 @@ class AgentGUIClient {
266
270
  */
267
271
  setupRendererListeners() {
268
272
  this.renderer.on('batch:complete', (data) => {
269
- console.log('Batch rendered:', data);
273
+ this._dbg('Batch rendered:', data);
270
274
  this.updateMetrics(data.metrics);
271
275
  });
272
276
 
@@ -293,7 +297,7 @@ class AgentGUIClient {
293
297
  if (sessionId && this.isValidId(sessionId)) {
294
298
  this.routerState.currentSessionId = sessionId;
295
299
  }
296
- console.log('Restoring conversation from URL:', conversationId);
300
+ this._dbg('Restoring conversation from URL:', conversationId);
297
301
  this._isLoadingConversation = true;
298
302
  if (window.conversationManager) {
299
303
  window.conversationManager.select(conversationId);
@@ -303,7 +307,7 @@ class AgentGUIClient {
303
307
  // If the URL conversation doesn't exist, try loading the most recent conversation
304
308
  if (this.state.conversations && this.state.conversations.length > 0) {
305
309
  const latestConv = this.state.conversations[0];
306
- console.log('Loading latest conversation instead:', latestConv.id);
310
+ this._dbg('Loading latest conversation instead:', latestConv.id);
307
311
  return this.loadConversationMessages(latestConv.id);
308
312
  } else {
309
313
  // No conversations available - show welcome screen
@@ -510,7 +514,7 @@ class AgentGUIClient {
510
514
  if (!this.state.currentConversation) return;
511
515
  try {
512
516
  const data = await window.wsClient.rpc('conv.cancel', { id: this.state.currentConversation.id });
513
- console.log('Stop response:', data);
517
+ this._dbg('Stop response:', data);
514
518
  } catch (err) {
515
519
  console.error('Failed to stop:', err);
516
520
  }
@@ -561,7 +565,7 @@ class AgentGUIClient {
561
565
  try {
562
566
  // Queue uses msg.send which will enqueue if streaming is active
563
567
  const data = await window.wsClient.rpc('msg.send', { id: this.state.currentConversation.id, content: message });
564
- console.log('Queue response:', data);
568
+ this._dbg('Queue response:', data);
565
569
  if (this.ui.messageInput) {
566
570
  this.ui.messageInput.value = '';
567
571
  this.ui.messageInput.style.height = 'auto';
@@ -587,6 +591,27 @@ class AgentGUIClient {
587
591
  });
588
592
  }
589
593
 
594
+ document.addEventListener('keydown', (e) => {
595
+ if (e.key === 'n' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
596
+ e.preventDefault();
597
+ const newBtn = document.querySelector('[data-new-conversation], #newConversationBtn, .new-conversation-btn');
598
+ if (newBtn) newBtn.click();
599
+ }
600
+ if (e.key === 'b' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
601
+ e.preventDefault();
602
+ const toggleBtn = document.querySelector('[data-sidebar-toggle]');
603
+ if (toggleBtn) toggleBtn.click();
604
+ }
605
+ if (e.key === 'Escape') {
606
+ const activeEl = document.activeElement;
607
+ if (activeEl && activeEl.tagName === 'TEXTAREA') { activeEl.blur(); return; }
608
+ if (this.state.isStreaming) {
609
+ const cancelBtn = document.querySelector('#cancelBtn, [data-cancel-btn]');
610
+ if (cancelBtn && cancelBtn.offsetParent !== null) cancelBtn.click();
611
+ }
612
+ }
613
+ });
614
+
590
615
  // Setup theme toggle
591
616
  const themeToggle = document.querySelector('[data-theme-toggle]');
592
617
  if (themeToggle) {
@@ -788,7 +813,7 @@ class AgentGUIClient {
788
813
  }
789
814
 
790
815
  async handleStreamingStart(data) {
791
- console.log('Streaming started:', data);
816
+ this._dbg('Streaming started:', data);
792
817
  if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'STREAMING', conversationId: data.conversationId });
793
818
  this._clearThinkingCountdown();
794
819
  if (this._lastSendTime > 0) {
@@ -812,9 +837,9 @@ class AgentGUIClient {
812
837
  // If this streaming event is for a different conversation than what we are viewing,
813
838
  // just track the state but do not modify the DOM or start polling
814
839
  if (this.state.currentConversation?.id !== data.conversationId) {
815
- console.log('Streaming started for non-active conversation:', data.conversationId);
840
+ this._dbg('Streaming started for non-active conversation:', data.conversationId);
816
841
  this._setConvStreaming(data.conversationId, true, data.sessionId, data.agentId);
817
- console.log('[SYNC] streaming_start - non-active conv:', { convId: data.conversationId, sessionId: data.sessionId, streamingCount: this.state.streamingConversations.size });
842
+ this._dbg('[SYNC] streaming_start - non-active conv:', { convId: data.conversationId, sessionId: data.sessionId, streamingCount: this.state.streamingConversations.size });
818
843
  this.updateBusyPromptArea(data.conversationId);
819
844
  this.emit('streaming:start', data);
820
845
 
@@ -900,7 +925,7 @@ class AgentGUIClient {
900
925
  }
901
926
 
902
927
  async handleStreamingResumed(data) {
903
- console.log('Streaming resumed:', data);
928
+ this._dbg('Streaming resumed:', data);
904
929
  const conv = this.state.currentConversation || { id: data.conversationId };
905
930
  await this.handleStreamingStart({
906
931
  type: 'streaming_start',
@@ -913,6 +938,9 @@ class AgentGUIClient {
913
938
  }
914
939
 
915
940
  handleStreamingProgress(data) {
941
+ try { return this._handleStreamingProgressInner(data); } catch (e) { console.error('[render-error] streaming progress:', e); }
942
+ }
943
+ _handleStreamingProgressInner(data) {
916
944
  if (!data.block || !data.sessionId) return;
917
945
 
918
946
  // Deduplicate by seq number to guarantee exactly-once rendering
@@ -1098,7 +1126,7 @@ class AgentGUIClient {
1098
1126
 
1099
1127
  // If this event is for a conversation we are NOT currently viewing, just track state
1100
1128
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
1101
- console.log('Streaming error for non-active conversation:', conversationId);
1129
+ this._dbg('Streaming error for non-active conversation:', conversationId);
1102
1130
  this._setConvStreaming(conversationId, false);
1103
1131
  this.updateBusyPromptArea(conversationId);
1104
1132
  this.emit('streaming:error', data);
@@ -1163,7 +1191,7 @@ class AgentGUIClient {
1163
1191
  }
1164
1192
 
1165
1193
  handleStreamingComplete(data) {
1166
- console.log('Streaming completed:', data);
1194
+ this._dbg('Streaming completed:', data);
1167
1195
  if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
1168
1196
  this._clearThinkingCountdown();
1169
1197
 
@@ -1171,16 +1199,16 @@ class AgentGUIClient {
1171
1199
  if (conversationId) this.invalidateCache(conversationId);
1172
1200
 
1173
1201
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
1174
- console.log('Streaming completed for non-active conversation:', conversationId);
1202
+ this._dbg('Streaming completed for non-active conversation:', conversationId);
1175
1203
  this._setConvStreaming(conversationId, false);
1176
- console.log('[SYNC] streaming_complete - non-active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size });
1204
+ this._dbg('[SYNC] streaming_complete - non-active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size });
1177
1205
  this.updateBusyPromptArea(conversationId);
1178
1206
  this.emit('streaming:complete', data);
1179
1207
  return;
1180
1208
  }
1181
1209
 
1182
1210
  this._setConvStreaming(conversationId, false);
1183
- console.log('[SYNC] streaming_complete - active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size, interrupted: data.interrupted });
1211
+ this._dbg('[SYNC] streaming_complete - active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size, interrupted: data.interrupted });
1184
1212
  this.updateBusyPromptArea(conversationId);
1185
1213
 
1186
1214
  const sessionId = data.sessionId || this.state.currentSession?.id;
@@ -1268,7 +1296,7 @@ class AgentGUIClient {
1268
1296
  return;
1269
1297
  }
1270
1298
 
1271
- console.log('[SYNC] message_created:', { msgId: data.message.id, role: data.message.role, convId: data.conversationId });
1299
+ this._dbg('[SYNC] message_created:', { msgId: data.message.id, role: data.message.role, convId: data.conversationId });
1272
1300
 
1273
1301
  // Update messageCount in current conversation state for user messages
1274
1302
  if (data.message.role === 'user' && this.state.currentConversation) {
@@ -2082,7 +2110,7 @@ class AgentGUIClient {
2082
2110
  }
2083
2111
 
2084
2112
  if (result.queued) {
2085
- console.log('Message queued, position:', result.queuePosition);
2113
+ this._dbg('Message queued, position:', result.queuePosition);
2086
2114
  this.enableControls();
2087
2115
  return;
2088
2116
  }
@@ -2182,6 +2210,9 @@ class AgentGUIClient {
2182
2210
  }
2183
2211
 
2184
2212
  renderChunk(chunk) {
2213
+ try { return this._renderChunkInner(chunk); } catch (e) { console.error('[render-error] chunk:', e); }
2214
+ }
2215
+ _renderChunkInner(chunk) {
2185
2216
  if (!chunk || !chunk.block) return;
2186
2217
  const seq = chunk.sequence;
2187
2218
  if (seq !== undefined) {
@@ -2260,13 +2291,13 @@ class AgentGUIClient {
2260
2291
  .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
2261
2292
  .join('');
2262
2293
  this.ui.agentSelector.style.display = 'inline-block';
2263
- console.log(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
2294
+ this._dbg(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
2264
2295
  // Auto-select first sub-agent and load its models
2265
2296
  const firstSubAgentId = subAgents[0].id;
2266
2297
  this.ui.agentSelector.value = firstSubAgentId;
2267
2298
  this.loadModelsForAgent(cliAgentId); // models keyed to parent agent
2268
2299
  } else {
2269
- console.log(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
2300
+ this._dbg(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
2270
2301
  // Load models for the CLI agent itself (fallback for agents without sub-agents)
2271
2302
  const cliToAcpMap = {
2272
2303
  'cli-opencode': 'opencode',
@@ -2508,7 +2539,7 @@ class AgentGUIClient {
2508
2539
  }
2509
2540
  if (progress.done || progress.status === 'completed') {
2510
2541
  this._modelDownloadInProgress = false;
2511
- console.log('[Models] Download complete');
2542
+ this._dbg('[Models] Download complete');
2512
2543
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2513
2544
  return;
2514
2545
  }
@@ -2520,7 +2551,7 @@ class AgentGUIClient {
2520
2551
 
2521
2552
  _handleTTSSetupProgress(data) {
2522
2553
  if (data.step && data.status) {
2523
- console.log('[TTS Setup]', data.step, ':', data.status, data.message || '');
2554
+ this._dbg('[TTS Setup]', data.step, ':', data.status, data.message || '');
2524
2555
  }
2525
2556
  }
2526
2557
 
@@ -2777,7 +2808,7 @@ class AgentGUIClient {
2777
2808
  if (window.conversationManager) window.conversationManager.loadConversations();
2778
2809
  const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
2779
2810
  if (fallbackConv && fallbackConv !== conversationId) {
2780
- console.log('Resuming from fallback conversation:', fallbackConv);
2811
+ this._dbg('Resuming from fallback conversation:', fallbackConv);
2781
2812
  this.showError('Conversation not found. Resuming previous conversation.');
2782
2813
  await this.loadConversationMessages(fallbackConv);
2783
2814
  } else {
@@ -2958,7 +2989,7 @@ class AgentGUIClient {
2958
2989
  // Resume from last successful conversation if available, or fall back to any available conversation
2959
2990
  const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
2960
2991
  if (fallbackConv && fallbackConv !== conversationId) {
2961
- console.log('Resuming from fallback conversation due to error:', fallbackConv);
2992
+ this._dbg('Resuming from fallback conversation due to error:', fallbackConv);
2962
2993
  this.showError('Failed to load conversation. Resuming previous conversation.');
2963
2994
  try {
2964
2995
  await this.loadConversationMessages(fallbackConv);
@@ -3354,7 +3385,7 @@ document.addEventListener('DOMContentLoaded', async () => {
3354
3385
  agentGUIClient = new AgentGUIClient();
3355
3386
  window.agentGuiClient = agentGUIClient;
3356
3387
  await agentGUIClient.init();
3357
- console.log('AgentGUI ready');
3388
+ this._dbg('AgentGUI ready');
3358
3389
  } catch (error) {
3359
3390
  console.error('Failed to initialize AgentGUI:', error);
3360
3391
  }
@@ -781,6 +781,7 @@ class StreamingRenderer {
781
781
  const details = document.createElement('details');
782
782
  details.className = 'block-tool-use folded-tool';
783
783
  if (block.id) details.dataset.toolUseId = block.id;
784
+ details.dataset.startedAt = Date.now();
784
785
  details.classList.add(this._getBlockTypeClass('tool_use'));
785
786
  details.classList.add(this._getToolColorClass(toolName));
786
787
  const normalizedForOpen = toolName.replace(/^mcp__.*?__/, '');
@@ -2228,6 +2229,16 @@ class StreamingRenderer {
2228
2229
  statusSpan.className = 'folded-tool-status';
2229
2230
  statusSpan.innerHTML = statusSvg;
2230
2231
  summary.appendChild(statusSpan);
2232
+ const startedAt = parseInt(toolUseEl.dataset.startedAt);
2233
+ if (startedAt > 0) {
2234
+ const ms = Date.now() - startedAt;
2235
+ const label = ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's';
2236
+ const dur = document.createElement('span');
2237
+ dur.className = 'folded-tool-duration';
2238
+ dur.style.cssText = 'margin-left:0.375rem;font-size:0.6rem;opacity:0.45;font-weight:400';
2239
+ dur.textContent = label;
2240
+ summary.appendChild(dur);
2241
+ }
2231
2242
  }
2232
2243
 
2233
2244
  const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);