agentgui 1.0.620 → 1.0.622

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/README.md CHANGED
@@ -26,7 +26,7 @@ Multi-agent GUI client for AI coding agents with real-time streaming, WebSocket
26
26
  | Kiro CLI | ACP | - |
27
27
  | fast-agent | ACP | - |
28
28
 
29
- ![Main Interface](docs/screenshot-main.png)
29
+ ![Main Interface](docs/screenshot-dark.png)
30
30
 
31
31
  ## Why AgentGUI?
32
32
 
@@ -50,17 +50,13 @@ Modern AI coding requires juggling multiple agents, each in their own terminal.
50
50
 
51
51
  ### Screenshots
52
52
 
53
- | Main Interface | Chat View |
54
- |----------------|-----------|
55
- | ![Main](docs/screenshot-main.png) | ![Chat](docs/screenshot-chat.png) |
53
+ | Light Mode | Dark Mode |
54
+ |------------|-----------|
55
+ | ![Light](docs/screenshot-light.png) | ![Dark](docs/screenshot-dark.png) |
56
56
 
57
- | Files Browser | Terminal View |
58
- |---------------|---------------|
59
- | ![Files](docs/screenshot-files.png) | ![Terminal](docs/screenshot-terminal.png) |
60
-
61
- | Tools Management | Conversation |
62
- |------------------|--------------|
63
- | ![Tools](docs/screenshot-tools-popup.png) | ![Conversation](docs/screenshot-conversation.png) |
57
+ | Active Conversation |
58
+ |---------------------|
59
+ | ![Conversation](docs/screenshot-dark-conversation.png) |
64
60
 
65
61
  ## Quick Start
66
62
 
package/docs/index.html CHANGED
@@ -193,21 +193,27 @@
193
193
  /* Screenshots */
194
194
  .screenshot-gallery {
195
195
  display: grid;
196
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
197
- gap: 2rem;
196
+ grid-template-columns: repeat(3, 1fr);
197
+ gap: 1.5rem;
198
198
  margin-top: 3rem;
199
199
  }
200
200
 
201
+ .screenshot-gallery.two-col {
202
+ grid-template-columns: repeat(2, 1fr);
203
+ margin-top: 1.5rem;
204
+ }
205
+
201
206
  .screenshot-item {
202
207
  border-radius: 12px;
203
208
  overflow: hidden;
204
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
209
+ box-shadow: 0 4px 24px rgba(0,0,0,0.12);
205
210
  transition: all 0.3s ease;
211
+ background: #1f2937;
206
212
  }
207
213
 
208
214
  .screenshot-item:hover {
209
- transform: scale(1.03);
210
- box-shadow: 0 8px 30px rgba(0,0,0,0.15);
215
+ transform: translateY(-4px);
216
+ box-shadow: 0 12px 40px rgba(0,0,0,0.2);
211
217
  }
212
218
 
213
219
  .screenshot-item img {
@@ -217,11 +223,13 @@
217
223
  }
218
224
 
219
225
  .screenshot-caption {
220
- padding: 1rem;
221
- background: #f9fafb;
226
+ padding: 0.875rem 1rem;
227
+ background: #1f2937;
222
228
  text-align: center;
223
229
  font-weight: 600;
224
- color: #374151;
230
+ color: #e5e7eb;
231
+ font-size: 0.9rem;
232
+ letter-spacing: 0.02em;
225
233
  }
226
234
 
227
235
  /* Code Block */
@@ -321,7 +329,8 @@
321
329
  grid-template-columns: 1fr;
322
330
  }
323
331
 
324
- .screenshot-gallery {
332
+ .screenshot-gallery,
333
+ .screenshot-gallery.two-col {
325
334
  grid-template-columns: 1fr;
326
335
  }
327
336
  }
@@ -332,7 +341,7 @@
332
341
  <section class="hero">
333
342
  <div class="hero-content">
334
343
  <h1>AgentGUI</h1>
335
- <p>Multi-agent GUI for AI coding assistants. One interface for all your AI agents.</p>
344
+ <p>Multi-agent GUI for Claude Code, Gemini CLI, OpenCode &amp; more. Real-time streaming, dark mode, voice, and SQLite persistence.</p>
336
345
 
337
346
  <div class="badges">
338
347
  <img src="https://img.shields.io/npm/v/agentgui?color=brightgreen" alt="npm version">
@@ -443,33 +452,26 @@
443
452
 
444
453
  <div class="screenshot-gallery">
445
454
  <div class="screenshot-item">
446
- <img src="screenshot-main.png" alt="Main Interface">
447
- <div class="screenshot-caption">Main Interface</div>
455
+ <img src="screenshot-light.png" alt="Welcome Screen - Light Mode">
456
+ <div class="screenshot-caption">Welcome Screen · Light Mode</div>
448
457
  </div>
449
-
450
458
  <div class="screenshot-item">
451
- <img src="screenshot-chat.png" alt="Chat View">
452
- <div class="screenshot-caption">Chat & Conversation</div>
459
+ <img src="screenshot-dark.png" alt="Welcome Screen - Dark Mode">
460
+ <div class="screenshot-caption">Welcome Screen · Dark Mode</div>
453
461
  </div>
454
-
455
- <div class="screenshot-item">
456
- <img src="screenshot-files.png" alt="File Browser">
457
- <div class="screenshot-caption">File Browser</div>
458
- </div>
459
-
460
462
  <div class="screenshot-item">
461
- <img src="screenshot-terminal.png" alt="Terminal View">
462
- <div class="screenshot-caption">Terminal Execution</div>
463
+ <img src="screenshot-dark-conversation.png" alt="Live Streaming">
464
+ <div class="screenshot-caption">Live Agent Streaming</div>
463
465
  </div>
464
-
466
+ </div>
467
+ <div class="screenshot-gallery two-col">
465
468
  <div class="screenshot-item">
466
- <img src="screenshot-tools-popup.png" alt="Tools Management">
467
- <div class="screenshot-caption">Tools Management</div>
469
+ <img src="screenshot-conversation.png" alt="Tool Use & Code Output">
470
+ <div class="screenshot-caption">Tool Use &amp; Code Output</div>
468
471
  </div>
469
-
470
472
  <div class="screenshot-item">
471
- <img src="screenshot-conversation.png" alt="Conversation View">
472
- <div class="screenshot-caption">Conversation History</div>
473
+ <img src="screenshot-tools.png" alt="Tools & Extensions">
474
+ <div class="screenshot-caption">Tools &amp; Extensions Manager</div>
473
475
  </div>
474
476
  </div>
475
477
  </div>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -115,9 +115,10 @@ class AgentRunner {
115
115
  }, timeout);
116
116
 
117
117
  // Write to stdin if supported
118
+ // Don't close stdin - keep it open for steering/injection during execution
118
119
  if (this.supportsStdin) {
119
120
  proc.stdin.write(prompt);
120
- proc.stdin.end();
121
+ // Don't call stdin.end() - agents need open stdin for session/prompt steering
121
122
  }
122
123
 
123
124
  proc.stdout.on('error', () => {});
@@ -1189,6 +1190,33 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
1189
1190
  if (!enhancedConfig.systemPrompt) {
1190
1191
  enhancedConfig.systemPrompt = '';
1191
1192
  }
1193
+
1194
+ // Append communication guidelines for all agents
1195
+ const communicationGuidelines = `
1196
+ COMMUNICATION STYLE: Minimize output. Only inform the user about:
1197
+ - Critical errors that block work
1198
+ - User needs to know info (e.g., "port in use", "authentication failed", "file not found")
1199
+ - Action required from user
1200
+ - Important decisions that affect their work
1201
+
1202
+ DO NOT output:
1203
+ - Progress updates ("doing X now", "completed Y", "searching for...")
1204
+ - Verbose summaries of what was done
1205
+ - Status checks or verification messages
1206
+ - Detailed explanations unless asked
1207
+ - "Working on...", "Looking for...", step-by-step progress
1208
+
1209
+ INSTEAD:
1210
+ - Run tools silently
1211
+ - Show results only when relevant
1212
+ - Be conversational and direct
1213
+ - Let code/output speak for itself
1214
+ `;
1215
+
1216
+ if (!enhancedConfig.systemPrompt.includes('COMMUNICATION STYLE')) {
1217
+ enhancedConfig.systemPrompt = communicationGuidelines + enhancedConfig.systemPrompt;
1218
+ }
1219
+
1192
1220
  if (agentId && agentId !== 'claude-code') {
1193
1221
  const displayAgentId = agentId.split('-·-')[0];
1194
1222
  const agentPrefix = `use ${displayAgentId} subagent to. `;
@@ -107,55 +107,8 @@ export function register(router, deps) {
107
107
  protocol: a.protocol || 'unknown', description: a.description || '', status: 'available'
108
108
  }))
109
109
  }));
110
-
111
- router.handle('agent.subagents', async (p) => {
112
- const agent = discoveredAgents.find(x => x.id === p.id);
113
- if (p.id === 'claude-code') {
114
- // Directly enumerate skills from ~/.claude/skills directory
115
- // Don't try to run 'claude agents list' as it gets intercepted by the running Claude Code instance
116
- const skillsDir = path.join(os.homedir(), '.claude', 'skills');
117
- try {
118
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
119
- const skills = entries
120
- .filter(e => e.isDirectory() || e.isSymbolicLink())
121
- .map(e => {
122
- // Try to read SKILL.md to get the actual name
123
- const md = path.join(skillsDir, e.name, 'SKILL.md');
124
- let name = e.name;
125
- try {
126
- const content = fs.readFileSync(md, 'utf-8');
127
- const m = content.match(/^name:\s*(.+)$/m);
128
- if (m) name = m[1].trim();
129
- } catch (_) {
130
- // Fallback: format the directory name as the skill name
131
- name = e.name
132
- .split(/[\-_]/)
133
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
134
- .join(' ');
135
- }
136
- return { id: e.name, name };
137
- })
138
- .sort((a, b) => a.name.localeCompare(b.name));
139
-
140
- if (skills.length > 0) {
141
- console.log(`[agent.subagents] Found ${skills.length} Claude Code skills:`, skills.map(s => s.name).join(', '));
142
- return { subAgents: skills };
143
- }
144
- return { subAgents: [] };
145
- } catch (err) {
146
- console.error(`[agent.subagents] Error reading skills directory:`, err.message);
147
- return { subAgents: [] };
148
- }
149
- }
150
- if (!agent || agent.protocol !== 'acp') return { subAgents: [] };
151
- const port = await ensureRunning(p.id);
152
- if (!port) return { subAgents: [] };
153
- try {
154
- const data = await acpFetch(port, '/agent');
155
- const list = Array.isArray(data) ? data : (data?.agents || []);
156
- return { subAgents: list.map(a => ({ id: a.name || a.agent_id || a.id, name: a.name || a.agent_id || a.id })) };
157
- } catch (_) { return { subAgents: [] }; }
158
- });
110
+ // Note: agent.subagents is handled by ws-handlers-util.js which is registered after this file
111
+ // Keeping this note to avoid duplicate handler registration
159
112
 
160
113
  router.handle('agent.get', (p) => {
161
114
  const a = discoveredAgents.find(x => x.id === p.id);
@@ -173,7 +126,7 @@ export function register(router, deps) {
173
126
  const cached = modelCache.get(p.id);
174
127
  if (cached && (Date.now() - cached.ts) < 300000) return { models: cached.models };
175
128
  let models = [];
176
- if (p.id === 'claude-code') {
129
+ if (p.id === 'claude-code' || p.id === 'cli-claude') {
177
130
  models = [
178
131
  { id: 'haiku', label: 'Haiku' },
179
132
  { id: 'sonnet', label: 'Sonnet' },
@@ -274,9 +274,45 @@ export function register(router, deps) {
274
274
  const { id } = p;
275
275
  if (!id) err(400, 'Missing agent id');
276
276
 
277
+ // For Claude Code, read skills from ~/.claude/skills directory
278
+ if (id === 'cli-claude') {
279
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills');
280
+ try {
281
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
282
+ const skills = entries
283
+ .filter(e => e.isDirectory() || e.isSymbolicLink())
284
+ .map(e => {
285
+ // Try to read SKILL.md to get the actual name
286
+ const md = path.join(skillsDir, e.name, 'SKILL.md');
287
+ let name = e.name;
288
+ try {
289
+ const content = fs.readFileSync(md, 'utf-8');
290
+ const m = content.match(/^name:\s*(.+)$/m);
291
+ if (m) name = m[1].trim();
292
+ } catch (_) {
293
+ // Fallback: format the directory name as the skill name
294
+ name = e.name
295
+ .split(/[\-_]/)
296
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
297
+ .join(' ');
298
+ }
299
+ return { id: e.name, name };
300
+ })
301
+ .sort((a, b) => a.name.localeCompare(b.name));
302
+
303
+ if (skills.length > 0) {
304
+ console.log(`[agent.subagents] Found ${skills.length} Claude Code skills:`, skills.map(s => s.name).join(', '));
305
+ return { subAgents: skills };
306
+ }
307
+ return { subAgents: [] };
308
+ } catch (err) {
309
+ console.error(`[agent.subagents] Error reading skills directory:`, err.message);
310
+ return { subAgents: [] };
311
+ }
312
+ }
313
+
277
314
  // Map CLI agent IDs to their corresponding plugin sub-agents
278
315
  const subAgentMap = {
279
- 'cli-claude': [{ id: 'gm-cc', name: 'GM Claude' }],
280
316
  'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
281
317
  'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
282
318
  'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.620",
3
+ "version": "1.0.622",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -1771,10 +1771,8 @@
1771
1771
  margin: 0;
1772
1772
  border-radius: 0.375rem;
1773
1773
  overflow: hidden;
1774
- background: #f3f4f6;
1775
1774
  }
1776
1775
  html.dark .folded-tool {
1777
- background: #262626;
1778
1776
  }
1779
1777
  .folded-tool-bar {
1780
1778
  display: flex;
@@ -283,11 +283,17 @@ class AgentGUIClient {
283
283
  const latestConv = this.state.conversations[0];
284
284
  console.log('Loading latest conversation instead:', latestConv.id);
285
285
  return this.loadConversationMessages(latestConv.id);
286
+ } else {
287
+ // No conversations available - show welcome screen
288
+ this._showWelcomeScreen();
286
289
  }
287
290
  }).finally(() => {
288
291
  this._isLoadingConversation = false;
289
292
  });
290
293
  }
294
+ } else {
295
+ // No conversation in URL - show welcome screen
296
+ this._showWelcomeScreen();
291
297
  }
292
298
  }
293
299
 
@@ -599,8 +605,7 @@ class AgentGUIClient {
599
605
  this.updateUrlForConversation(null);
600
606
  this.stopChunkPolling();
601
607
  this.enableControls();
602
- const outputEl = document.getElementById('output');
603
- if (outputEl) outputEl.innerHTML = '';
608
+ this._showWelcomeScreen();
604
609
  if (this.ui.messageInput) {
605
610
  this.ui.messageInput.value = '';
606
611
  this.ui.messageInput.style.height = 'auto';
@@ -904,18 +909,30 @@ class AgentGUIClient {
904
909
  console.warn('Failed to load prior messages for streaming view:', e);
905
910
  }
906
911
  }
907
- const streamingDiv = document.createElement('div');
908
- streamingDiv.className = 'message message-assistant streaming-message';
909
- streamingDiv.id = `streaming-${data.sessionId}`;
910
- streamingDiv.innerHTML = `
911
- <div class="message-role">Assistant</div>
912
- <div class="message-blocks streaming-blocks"></div>
913
- <div class="streaming-indicator" style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;">
914
- <span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
915
- <span class="streaming-indicator-label">Thinking...</span>
916
- </div>
917
- `;
918
- messagesEl.appendChild(streamingDiv);
912
+ let streamingDiv = document.getElementById(`streaming-${data.sessionId}`);
913
+ if (!streamingDiv) {
914
+ streamingDiv = document.createElement('div');
915
+ streamingDiv.className = 'message message-assistant streaming-message';
916
+ streamingDiv.id = `streaming-${data.sessionId}`;
917
+ streamingDiv.innerHTML = `
918
+ <div class="message-role">Assistant</div>
919
+ <div class="message-blocks streaming-blocks"></div>
920
+ <div class="streaming-indicator" style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;">
921
+ <span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
922
+ <span class="streaming-indicator-label">Thinking...</span>
923
+ </div>
924
+ `;
925
+ messagesEl.appendChild(streamingDiv);
926
+ } else {
927
+ // Reuse existing div - ensure streaming class and single indicator
928
+ streamingDiv.classList.add('streaming-message');
929
+ streamingDiv.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
930
+ const indDiv = document.createElement('div');
931
+ indDiv.className = 'streaming-indicator';
932
+ indDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
933
+ indDiv.innerHTML = `<span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span><span class="streaming-indicator-label">Thinking...</span>`;
934
+ streamingDiv.appendChild(indDiv);
935
+ }
919
936
  this.scrollToBottom(true);
920
937
  }
921
938
 
@@ -1144,10 +1161,13 @@ class AgentGUIClient {
1144
1161
  if (queueEl) queueEl.remove();
1145
1162
 
1146
1163
  const sessionId = data.sessionId || this.state.currentSession?.id;
1164
+ // Remove ALL streaming indicators from the entire messages container
1165
+ const outputEl2 = document.getElementById('output');
1166
+ if (outputEl2) {
1167
+ outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
1168
+ }
1147
1169
  const streamingEl = document.getElementById(`streaming-${sessionId}`);
1148
1170
  if (streamingEl) {
1149
- const indicator = streamingEl.querySelector('.streaming-indicator');
1150
- if (indicator) indicator.remove();
1151
1171
  streamingEl.classList.remove('streaming-message');
1152
1172
  const prevTextEl = streamingEl.querySelector('.streaming-text-current');
1153
1173
  if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
@@ -1860,6 +1880,54 @@ class AgentGUIClient {
1860
1880
  });
1861
1881
  }
1862
1882
 
1883
+ /**
1884
+ * Show native loading spinner on document element
1885
+ */
1886
+ showLoadingSpinner() {
1887
+ document.documentElement.style.pointerEvents = 'auto';
1888
+ // Show native CSS loading indicator (not removing, just visual cue)
1889
+ const indicator = document.querySelector('[data-model-dl-indicator]');
1890
+ if (indicator && !indicator.classList.contains('visible')) {
1891
+ indicator.classList.add('visible');
1892
+ }
1893
+ }
1894
+
1895
+ /**
1896
+ * Hide native loading spinner
1897
+ */
1898
+ hideLoadingSpinner() {
1899
+ const indicator = document.querySelector('[data-model-dl-indicator]');
1900
+ if (indicator && indicator.classList.contains('visible')) {
1901
+ indicator.classList.remove('visible');
1902
+ }
1903
+ }
1904
+
1905
+ /**
1906
+ * Show welcome screen when no conversation is selected
1907
+ */
1908
+ _showWelcomeScreen() {
1909
+ const outputEl = document.getElementById('output');
1910
+ if (!outputEl) return;
1911
+ outputEl.innerHTML = `
1912
+ <div style="display:flex;align-items:center;justify-content:center;height:100%;flex-direction:column;gap:2rem;padding:2rem;">
1913
+ <div style="text-align:center;">
1914
+ <h1 style="margin:0;font-size:2.5rem;color:var(--color-text-primary);">Welcome to AgentGUI</h1>
1915
+ <p style="margin:1rem 0 0 0;font-size:1.1rem;color:var(--color-text-secondary);">Start a new conversation or select one from the sidebar</p>
1916
+ </div>
1917
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1rem;max-width:600px;">
1918
+ <div style="padding:1.5rem;border-radius:0.5rem;background:var(--color-bg-secondary);border:1px solid var(--color-border);">
1919
+ <h3 style="margin:0 0 0.5rem 0;color:var(--color-primary);">New Conversation</h3>
1920
+ <p style="margin:0;font-size:0.9rem;color:var(--color-text-secondary);">Create a new chat with any AI agent</p>
1921
+ </div>
1922
+ <div style="padding:1.5rem;border-radius:0.5rem;background:var(--color-bg-secondary);border:1px solid var(--color-border);">
1923
+ <h3 style="margin:0 0 0.5rem 0;color:var(--color-primary);">Available Agents</h3>
1924
+ <p style="margin:0;font-size:0.9rem;color:var(--color-text-secondary);">Claude Code, Gemini, OpenCode, and more</p>
1925
+ </div>
1926
+ </div>
1927
+ </div>
1928
+ `;
1929
+ }
1930
+
1863
1931
  _showSkeletonLoading(conversationId) {
1864
1932
  const outputEl = document.getElementById('output');
1865
1933
  if (!outputEl) return;
@@ -1883,6 +1951,8 @@ class AgentGUIClient {
1883
1951
  </div>
1884
1952
  </div>
1885
1953
  `;
1954
+ // Keep loading spinner visible during hydration
1955
+ this.showLoadingSpinner();
1886
1956
  }
1887
1957
 
1888
1958
  async streamToConversation(conversationId, prompt, agentId, model, subAgent) {
@@ -2248,10 +2318,14 @@ class AgentGUIClient {
2248
2318
  .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
2249
2319
  .join('');
2250
2320
  this.ui.agentSelector.style.display = 'inline-block';
2321
+ console.log(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
2251
2322
  this.loadModelsForAgent(cliAgentId);
2323
+ } else {
2324
+ console.log(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
2252
2325
  }
2253
- } catch (_) {
2326
+ } catch (err) {
2254
2327
  // No sub-agents available for this CLI tool — keep hidden
2328
+ console.warn(`[Agent Selector] Failed to load sub-agents for ${cliAgentId}:`, err.message);
2255
2329
  }
2256
2330
  }
2257
2331
 
@@ -2722,6 +2796,8 @@ class AgentGUIClient {
2722
2796
  this.conversationCache.delete(conversationId);
2723
2797
  this.syncPromptState(conversationId);
2724
2798
  this.restoreScrollPosition(conversationId);
2799
+ // Hydration complete - hide loading spinner
2800
+ this.hideLoadingSpinner();
2725
2801
  // Prompt state is immutable: computed from shouldResumeStreaming via syncPromptState
2726
2802
  // Do not call enableControls/disableControls here - prompt state is determined by streaming status
2727
2803
  return;
@@ -2734,7 +2810,9 @@ class AgentGUIClient {
2734
2810
 
2735
2811
  let fullData;
2736
2812
  try {
2737
- fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
2813
+ // Load only recent chunks initially (lazy load older ones)
2814
+ // Use chunkLimit of 50 to make page load faster
2815
+ fullData = await window.wsClient.rpc('conv.full', { id: conversationId, chunkLimit: 50 });
2738
2816
  } catch (e) {
2739
2817
  if (e.code === 404) {
2740
2818
  console.warn('Conversation no longer exists:', conversationId);
@@ -2967,9 +3045,12 @@ class AgentGUIClient {
2967
3045
 
2968
3046
  this.restoreScrollPosition(conversationId);
2969
3047
  this.setupScrollUpDetection(conversationId);
3048
+ // Hydration complete - hide loading spinner
3049
+ this.hideLoadingSpinner();
2970
3050
  }
2971
3051
  } catch (error) {
2972
3052
  if (error.name === 'AbortError') return;
3053
+ this.hideLoadingSpinner();
2973
3054
  console.error('Failed to load conversation messages:', error);
2974
3055
  // Resume from last successful conversation if available, or fall back to any available conversation
2975
3056
  const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
Binary file
Binary file