agentgui 1.0.197 → 1.0.199

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.197",
3
+ "version": "1.0.199",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -410,6 +410,46 @@ const server = http.createServer(async (req, res) => {
410
410
  return;
411
411
  }
412
412
 
413
+ const queueMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue$/);
414
+ if (queueMatch && req.method === 'GET') {
415
+ const conversationId = queueMatch[1];
416
+ const conv = queries.getConversation(conversationId);
417
+ if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
418
+ const queue = messageQueues.get(conversationId) || [];
419
+ sendJSON(req, res, 200, { queue });
420
+ return;
421
+ }
422
+
423
+ const queueItemMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue\/([^/]+)$/);
424
+ if (queueItemMatch && req.method === 'DELETE') {
425
+ const conversationId = queueItemMatch[1];
426
+ const messageId = queueItemMatch[2];
427
+ const queue = messageQueues.get(conversationId);
428
+ if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
429
+ const index = queue.findIndex(q => q.messageId === messageId);
430
+ if (index === -1) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
431
+ queue.splice(index, 1);
432
+ if (queue.length === 0) messageQueues.delete(conversationId);
433
+ broadcastSync({ type: 'queue_status', conversationId, queueLength: queue?.length || 0, timestamp: Date.now() });
434
+ sendJSON(req, res, 200, { deleted: true });
435
+ return;
436
+ }
437
+
438
+ if (queueItemMatch && req.method === 'PATCH') {
439
+ const conversationId = queueItemMatch[1];
440
+ const messageId = queueItemMatch[2];
441
+ const body = await parseBody(req);
442
+ const queue = messageQueues.get(conversationId);
443
+ if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
444
+ const item = queue.find(q => q.messageId === messageId);
445
+ if (!item) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
446
+ if (body.content !== undefined) item.content = body.content;
447
+ if (body.agentId !== undefined) item.agentId = body.agentId;
448
+ broadcastSync({ type: 'queue_updated', conversationId, messageId, content: item.content, agentId: item.agentId, timestamp: Date.now() });
449
+ sendJSON(req, res, 200, { updated: true, item });
450
+ return;
451
+ }
452
+
413
453
  const messageMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
414
454
  if (messageMatch && req.method === 'GET') {
415
455
  const msg = queries.getMessage(messageMatch[2]);
@@ -573,7 +613,11 @@ const server = http.createServer(async (req, res) => {
573
613
  if (!pkg.scripts || !pkg.scripts[script]) { sendJSON(req, res, 400, { error: `Script "${script}" not found` }); return; }
574
614
  } catch { sendJSON(req, res, 400, { error: 'No package.json' }); return; }
575
615
 
576
- const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: { ...process.env, FORCE_COLOR: '1' } });
616
+ const childEnv = { ...process.env, FORCE_COLOR: '1' };
617
+ delete childEnv.PORT;
618
+ delete childEnv.BASE_URL;
619
+ delete childEnv.HOT_RELOAD;
620
+ const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv });
577
621
  activeScripts.set(conversationId, { process: child, script, startTime: Date.now() });
578
622
  broadcastSync({ type: 'script_started', conversationId, script, timestamp: Date.now() });
579
623
 
@@ -616,6 +660,100 @@ const server = http.createServer(async (req, res) => {
616
660
  return;
617
661
  }
618
662
 
663
+ if (pathOnly === '/api/agents/auth-status' && req.method === 'GET') {
664
+ const statuses = discoveredAgents.map(agent => {
665
+ const status = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
666
+ try {
667
+ if (agent.id === 'claude-code') {
668
+ const credFile = path.join(os.homedir(), '.claude', '.credentials.json');
669
+ if (fs.existsSync(credFile)) {
670
+ const creds = JSON.parse(fs.readFileSync(credFile, 'utf-8'));
671
+ if (creds.claudeAiOauth && creds.claudeAiOauth.expiresAt > Date.now()) {
672
+ status.authenticated = true;
673
+ status.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
674
+ } else {
675
+ status.detail = 'expired';
676
+ }
677
+ } else {
678
+ status.detail = 'no credentials';
679
+ }
680
+ } else if (agent.id === 'gemini') {
681
+ const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
682
+ if (fs.existsSync(acctFile)) {
683
+ const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
684
+ if (accts.active) {
685
+ status.authenticated = true;
686
+ status.detail = accts.active;
687
+ } else {
688
+ status.detail = 'logged out';
689
+ }
690
+ } else {
691
+ status.detail = 'no credentials';
692
+ }
693
+ } else if (agent.id === 'opencode') {
694
+ const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
695
+ const countMatch = out.match(/(\d+)\s+credentials?/);
696
+ if (countMatch && parseInt(countMatch[1], 10) > 0) {
697
+ status.authenticated = true;
698
+ status.detail = countMatch[1] + ' credential(s)';
699
+ } else {
700
+ status.detail = 'no credentials';
701
+ }
702
+ } else {
703
+ status.detail = 'unknown';
704
+ }
705
+ } catch (e) {
706
+ status.detail = 'check failed';
707
+ }
708
+ return status;
709
+ });
710
+ sendJSON(req, res, 200, { agents: statuses });
711
+ return;
712
+ }
713
+
714
+ const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
715
+ if (agentAuthMatch && req.method === 'POST') {
716
+ const agentId = agentAuthMatch[1];
717
+ const agent = discoveredAgents.find(a => a.id === agentId);
718
+ if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
719
+
720
+ const authCommands = {
721
+ 'claude-code': { cmd: 'claude', args: ['setup-token'] },
722
+ 'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
723
+ 'gemini': { cmd: 'gemini', args: [] }
724
+ };
725
+ const authCmd = authCommands[agentId];
726
+ if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
727
+
728
+ const conversationId = '__agent_auth__';
729
+ if (activeScripts.has(conversationId)) {
730
+ sendJSON(req, res, 409, { error: 'Auth process already running' });
731
+ return;
732
+ }
733
+
734
+ const child = spawn(authCmd.cmd, authCmd.args, {
735
+ stdio: ['pipe', 'pipe', 'pipe'],
736
+ env: { ...process.env, FORCE_COLOR: '1' }
737
+ });
738
+ activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
739
+ broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
740
+
741
+ const onData = (stream) => (chunk) => {
742
+ broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
743
+ };
744
+ child.stdout.on('data', onData('stdout'));
745
+ child.stderr.on('data', onData('stderr'));
746
+ child.on('error', (err) => {
747
+ activeScripts.delete(conversationId);
748
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
749
+ });
750
+ child.on('close', (code) => {
751
+ activeScripts.delete(conversationId);
752
+ broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
753
+ });
754
+ sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
755
+ return;
756
+ }
619
757
 
620
758
  if (pathOnly === '/api/import/claude-code' && req.method === 'GET') {
621
759
  const result = queries.importClaudeCodeConversations();
@@ -1378,7 +1516,7 @@ wss.on('connection', (ws, req) => {
1378
1516
 
1379
1517
  const BROADCAST_TYPES = new Set([
1380
1518
  'message_created', 'conversation_created', 'conversation_updated',
1381
- 'conversations_updated', 'conversation_deleted', 'queue_status',
1519
+ 'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
1382
1520
  'streaming_start', 'streaming_complete', 'streaming_error',
1383
1521
  'rate_limit_hit', 'rate_limit_clear',
1384
1522
  'script_started', 'script_stopped', 'script_output'
package/static/index.html CHANGED
@@ -447,12 +447,12 @@
447
447
  .script-buttons { display: flex; gap: 0.25rem; align-items: center; }
448
448
  .header-icon-btn {
449
449
  display: flex; align-items: center; justify-content: center;
450
- width: 32px; height: 32px; background: none; border: none;
450
+ width: 36px; height: 36px; background: none; border: none;
451
451
  border-radius: 0.375rem; cursor: pointer; color: var(--color-text-secondary);
452
452
  transition: background-color 0.15s, color 0.15s;
453
453
  }
454
454
  .header-icon-btn:hover { background-color: var(--color-bg-primary); color: var(--color-text-primary); }
455
- .header-icon-btn svg { width: 16px; height: 16px; }
455
+ .header-icon-btn svg { width: 18px; height: 18px; }
456
456
  #scriptStartBtn { color: var(--color-success); }
457
457
  #scriptStartBtn:hover { background-color: rgba(16,185,129,0.1); color: var(--color-success); }
458
458
  .script-dev-btn { color: var(--color-info); }
@@ -460,6 +460,32 @@
460
460
  .script-stop-btn { color: var(--color-error); }
461
461
  .script-stop-btn:hover { background-color: rgba(239,68,68,0.1); color: var(--color-error); }
462
462
 
463
+ .agent-auth-btn { color: var(--color-text-secondary); position: relative; }
464
+ .agent-auth-btn.auth-ok { color: var(--color-success); }
465
+ .agent-auth-btn.auth-warn { color: var(--color-warning); }
466
+ .agent-auth-btn:hover { background-color: var(--color-bg-primary); }
467
+ .agent-auth-dropdown {
468
+ position: absolute; top: 100%; right: 0; z-index: 100;
469
+ min-width: 200px; padding: 0.25rem 0;
470
+ background: var(--color-bg-secondary); border: 1px solid var(--color-border);
471
+ border-radius: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
472
+ display: none;
473
+ }
474
+ .agent-auth-dropdown.open { display: block; }
475
+ .agent-auth-item {
476
+ display: flex; align-items: center; gap: 0.5rem;
477
+ padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.8125rem;
478
+ color: var(--color-text-primary); border: none; background: none; width: 100%;
479
+ text-align: left;
480
+ }
481
+ .agent-auth-item:hover { background: var(--color-bg-primary); }
482
+ .agent-auth-dot {
483
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
484
+ }
485
+ .agent-auth-dot.ok { background: var(--color-success); }
486
+ .agent-auth-dot.missing { background: var(--color-warning); }
487
+ .agent-auth-dot.unknown { background: var(--color-text-secondary); }
488
+
463
489
  .terminal-container {
464
490
  flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e;
465
491
  }
@@ -2234,6 +2260,10 @@
2234
2260
  <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="1"></rect></svg>
2235
2261
  </button>
2236
2262
  </div>
2263
+ <button class="header-icon-btn agent-auth-btn" id="agentAuthBtn" title="Agent authentication" aria-label="Agent authentication" style="display:none;">
2264
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
2265
+ <div class="agent-auth-dropdown" id="agentAuthDropdown"></div>
2266
+ </button>
2237
2267
  <div class="status-badge">
2238
2268
  <div class="status-indicator" data-status="disconnected"></div>
2239
2269
  <span id="connectionStatus" data-status-indicator>Disconnected</span>
@@ -2378,6 +2408,7 @@
2378
2408
  <script type="module" src="/gm/js/voice.js"></script>
2379
2409
  <script defer src="/gm/js/features.js"></script>
2380
2410
  <script defer src="/gm/js/script-runner.js"></script>
2411
+ <script defer src="/gm/js/agent-auth.js"></script>
2381
2412
 
2382
2413
  <script>
2383
2414
  const savedTheme = localStorage.getItem('theme') || 'light';
@@ -0,0 +1,144 @@
1
+ (function() {
2
+ var BASE = window.__BASE_URL || '';
3
+ var btn = document.getElementById('agentAuthBtn');
4
+ var dropdown = document.getElementById('agentAuthDropdown');
5
+ var agents = [];
6
+ var authRunning = false;
7
+ var AUTH_CONV_ID = '__agent_auth__';
8
+
9
+ function init() {
10
+ if (!btn || !dropdown) return;
11
+ btn.addEventListener('click', toggleDropdown);
12
+ document.addEventListener('click', function(e) {
13
+ if (!btn.contains(e.target)) closeDropdown();
14
+ });
15
+ window.addEventListener('conversation-selected', function() { fetchAuthStatus(); });
16
+ window.addEventListener('ws-message', onWsMessage);
17
+ fetchAuthStatus();
18
+ }
19
+
20
+ function fetchAuthStatus() {
21
+ fetch(BASE + '/api/agents/auth-status')
22
+ .then(function(r) { return r.json(); })
23
+ .then(function(data) {
24
+ agents = data.agents || [];
25
+ updateButton();
26
+ renderDropdown();
27
+ })
28
+ .catch(function() {});
29
+ }
30
+
31
+ function updateButton() {
32
+ if (agents.length === 0) { btn.style.display = 'none'; return; }
33
+ btn.style.display = 'flex';
34
+ var allOk = agents.every(function(a) { return a.authenticated; });
35
+ var anyMissing = agents.some(function(a) { return !a.authenticated; });
36
+ btn.classList.toggle('auth-ok', allOk);
37
+ btn.classList.toggle('auth-warn', anyMissing);
38
+ }
39
+
40
+ function renderDropdown() {
41
+ dropdown.innerHTML = '';
42
+ agents.forEach(function(agent) {
43
+ var item = document.createElement('button');
44
+ item.className = 'agent-auth-item';
45
+ var dotClass = agent.authenticated ? 'ok' : (agent.detail === 'unknown' ? 'unknown' : 'missing');
46
+ item.innerHTML = '<span class="agent-auth-dot ' + dotClass + '"></span>' +
47
+ '<span>' + escapeHtml(agent.name) + '</span>' +
48
+ '<span style="margin-left:auto;font-size:0.7rem;color:var(--color-text-secondary)">' + escapeHtml(agent.detail) + '</span>';
49
+ item.addEventListener('click', function(e) {
50
+ e.stopPropagation();
51
+ closeDropdown();
52
+ triggerAuth(agent.id);
53
+ });
54
+ dropdown.appendChild(item);
55
+ });
56
+ }
57
+
58
+ function toggleDropdown(e) {
59
+ e.stopPropagation();
60
+ dropdown.classList.toggle('open');
61
+ }
62
+
63
+ function closeDropdown() {
64
+ dropdown.classList.remove('open');
65
+ }
66
+
67
+ function triggerAuth(agentId) {
68
+ if (authRunning) return;
69
+ fetch(BASE + '/api/agents/' + agentId + '/auth', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: '{}'
73
+ })
74
+ .then(function(r) { return r.json(); })
75
+ .then(function(data) {
76
+ if (data.ok) {
77
+ authRunning = true;
78
+ showTerminalTab();
79
+ switchToTerminalView();
80
+ var term = getTerminal();
81
+ if (term) {
82
+ term.clear();
83
+ term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n');
84
+ }
85
+ }
86
+ })
87
+ .catch(function() {});
88
+ }
89
+
90
+ function onWsMessage(e) {
91
+ var data = e.detail;
92
+ if (!data || data.conversationId !== AUTH_CONV_ID) return;
93
+ if (data.type === 'script_started') {
94
+ authRunning = true;
95
+ showTerminalTab();
96
+ switchToTerminalView();
97
+ var term = getTerminal();
98
+ if (term) {
99
+ term.clear();
100
+ term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n');
101
+ }
102
+ } else if (data.type === 'script_output') {
103
+ showTerminalTab();
104
+ var term = getTerminal();
105
+ if (term) term.write(data.data);
106
+ } else if (data.type === 'script_stopped') {
107
+ authRunning = false;
108
+ var term = getTerminal();
109
+ var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
110
+ if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
111
+ setTimeout(fetchAuthStatus, 1000);
112
+ }
113
+ }
114
+
115
+ function showTerminalTab() {
116
+ var tabBtn = document.getElementById('terminalTabBtn');
117
+ if (tabBtn) tabBtn.style.display = '';
118
+ }
119
+
120
+ function switchToTerminalView() {
121
+ var bar = document.getElementById('viewToggleBar');
122
+ if (!bar) return;
123
+ var termBtn = bar.querySelector('[data-view="terminal"]');
124
+ if (termBtn) termBtn.click();
125
+ }
126
+
127
+ function getTerminal() {
128
+ return window.scriptRunner ? window.scriptRunner.getTerminal() : null;
129
+ }
130
+
131
+ function escapeHtml(s) {
132
+ var d = document.createElement('div');
133
+ d.textContent = s;
134
+ return d.innerHTML;
135
+ }
136
+
137
+ if (document.readyState === 'loading') {
138
+ document.addEventListener('DOMContentLoaded', init);
139
+ } else {
140
+ init();
141
+ }
142
+
143
+ window.agentAuth = { refresh: fetchAuthStatus };
144
+ })();
@@ -372,6 +372,9 @@ class AgentGUIClient {
372
372
  case 'queue_status':
373
373
  this.handleQueueStatus(data);
374
374
  break;
375
+ case 'queue_updated':
376
+ this.handleQueueUpdated(data);
377
+ break;
375
378
  case 'rate_limit_hit':
376
379
  this.handleRateLimitHit(data);
377
380
  break;
@@ -478,7 +481,7 @@ class AgentGUIClient {
478
481
  const bFrag = document.createDocumentFragment();
479
482
  sList.forEach(chunk => {
480
483
  if (!chunk.block?.type) return;
481
- const el = this.renderer.renderBlock(chunk.block, chunk);
484
+ const el = this.renderer.renderBlock(chunk.block, chunk, bFrag);
482
485
  if (!el) return;
483
486
  if (chunk.block.type === 'tool_result') {
484
487
  const lastInFrag = bFrag.lastElementChild;
@@ -699,21 +702,68 @@ class AgentGUIClient {
699
702
 
700
703
  handleQueueStatus(data) {
701
704
  if (data.conversationId !== this.state.currentConversation?.id) return;
705
+ this.fetchAndRenderQueue(data.conversationId);
706
+ }
707
+
708
+ handleQueueUpdated(data) {
709
+ if (data.conversationId !== this.state.currentConversation?.id) return;
710
+ this.fetchAndRenderQueue(data.conversationId);
711
+ }
702
712
 
713
+ async fetchAndRenderQueue(conversationId) {
703
714
  const outputEl = document.querySelector('.conversation-messages');
704
715
  if (!outputEl) return;
705
716
 
706
- let queueEl = outputEl.querySelector('.queue-indicator');
707
- if (data.queueLength > 0) {
717
+ try {
718
+ const response = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue`);
719
+ const { queue } = await response.json();
720
+
721
+ let queueEl = outputEl.querySelector('.queue-indicator');
722
+ if (!queue || queue.length === 0) {
723
+ if (queueEl) queueEl.remove();
724
+ return;
725
+ }
726
+
708
727
  if (!queueEl) {
709
728
  queueEl = document.createElement('div');
710
729
  queueEl.className = 'queue-indicator';
711
- queueEl.style.cssText = 'padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;text-align:center;';
712
730
  outputEl.appendChild(queueEl);
713
731
  }
714
- queueEl.textContent = `${data.queueLength} message${data.queueLength > 1 ? 's' : ''} queued`;
715
- } else if (queueEl) {
716
- queueEl.remove();
732
+
733
+ queueEl.innerHTML = queue.map((q, i) => `
734
+ <div class="queue-item" data-message-id="${q.messageId}" style="padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;display:flex;align-items:center;gap:0.5rem;">
735
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${i + 1}. ${this.escapeHtml(q.content)}</span>
736
+ <button class="queue-edit-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Edit</button>
737
+ <button class="queue-delete-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Delete</button>
738
+ </div>
739
+ `).join('');
740
+
741
+ queueEl.querySelectorAll('.queue-delete-btn').forEach(btn => {
742
+ btn.addEventListener('click', async (e) => {
743
+ const index = parseInt(e.target.dataset.index);
744
+ const msgId = queue[index].messageId;
745
+ if (confirm('Delete this queued message?')) {
746
+ await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${msgId}`, { method: 'DELETE' });
747
+ }
748
+ });
749
+ });
750
+
751
+ queueEl.querySelectorAll('.queue-edit-btn').forEach(btn => {
752
+ btn.addEventListener('click', (e) => {
753
+ const index = parseInt(e.target.dataset.index);
754
+ const q = queue[index];
755
+ const newContent = prompt('Edit message:', q.content);
756
+ if (newContent !== null && newContent !== q.content) {
757
+ fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${q.messageId}`, {
758
+ method: 'PATCH',
759
+ headers: { 'Content-Type': 'application/json' },
760
+ body: JSON.stringify({ content: newContent })
761
+ });
762
+ }
763
+ });
764
+ });
765
+ } catch (err) {
766
+ console.error('Failed to fetch queue:', err);
717
767
  }
718
768
  }
719
769
 
@@ -1160,8 +1210,8 @@ class AgentGUIClient {
1160
1210
  if (!streamingEl) return;
1161
1211
  const blocksEl = streamingEl.querySelector('.streaming-blocks');
1162
1212
  if (!blocksEl) return;
1163
- const element = this.renderer.renderBlock(chunk.block, chunk);
1164
- if (!element) return;
1213
+ const element = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
1214
+ if (!element) { this.scrollToBottom(); return; }
1165
1215
  if (chunk.block.type === 'tool_result') {
1166
1216
  const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
1167
1217
  const lastEl = blocksEl.lastElementChild;
@@ -1187,8 +1237,8 @@ class AgentGUIClient {
1187
1237
  const blocksEl = streamingEl.querySelector('.streaming-blocks');
1188
1238
  if (!blocksEl) continue;
1189
1239
  for (const chunk of groups[sid]) {
1190
- const el = this.renderer.renderBlock(chunk.block, chunk);
1191
- if (!el) continue;
1240
+ const el = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
1241
+ if (!el) { appended = true; continue; }
1192
1242
  if (chunk.block.type === 'tool_result') {
1193
1243
  const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
1194
1244
  const lastEl = blocksEl.lastElementChild;
@@ -1271,7 +1321,6 @@ class AgentGUIClient {
1271
1321
  */
1272
1322
  disableControls() {
1273
1323
  if (this.ui.sendButton) this.ui.sendButton.disabled = true;
1274
- if (this.ui.agentSelector) this.ui.agentSelector.disabled = true;
1275
1324
  }
1276
1325
 
1277
1326
  /**
@@ -1279,7 +1328,6 @@ class AgentGUIClient {
1279
1328
  */
1280
1329
  enableControls() {
1281
1330
  if (this.ui.sendButton) this.ui.sendButton.disabled = false;
1282
- if (this.ui.agentSelector) this.ui.agentSelector.disabled = false;
1283
1331
  }
1284
1332
 
1285
1333
  /**
@@ -1501,7 +1549,7 @@ class AgentGUIClient {
1501
1549
  const blockFrag = document.createDocumentFragment();
1502
1550
  sessionChunkList.forEach(chunk => {
1503
1551
  if (!chunk.block?.type) return;
1504
- const element = this.renderer.renderBlock(chunk.block, chunk);
1552
+ const element = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
1505
1553
  if (!element) return;
1506
1554
  if (chunk.block.type === 'tool_result') {
1507
1555
  const lastInFrag = blockFrag.lastElementChild;
@@ -328,13 +328,13 @@ class StreamingRenderer {
328
328
  /**
329
329
  * Render Claude message blocks with beautiful styling
330
330
  */
331
- renderBlock(block, context = {}) {
331
+ renderBlock(block, context = {}, targetContainer = null) {
332
332
  if (!block || !block.type) return null;
333
333
 
334
334
  try {
335
335
  switch (block.type) {
336
336
  case 'text':
337
- return this.renderBlockText(block, context);
337
+ return this.renderBlockText(block, context, targetContainer);
338
338
  case 'code':
339
339
  return this.renderBlockCode(block, context);
340
340
  case 'thinking':
@@ -363,7 +363,7 @@ class StreamingRenderer {
363
363
  /**
364
364
  * Render text block with semantic HTML
365
365
  */
366
- renderBlockText(block, context) {
366
+ renderBlockText(block, context, targetContainer = null) {
367
367
  const text = block.text || '';
368
368
  const isHtml = this.containsHtmlTags(text);
369
369
  const cached = this.renderCache.get(text);
@@ -373,7 +373,8 @@ class StreamingRenderer {
373
373
  this.renderCache.set(text, html);
374
374
  }
375
375
 
376
- const lastChild = this.outputContainer && this.outputContainer.lastElementChild;
376
+ const container = targetContainer || this.outputContainer;
377
+ const lastChild = container && container.lastElementChild;
377
378
  if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
378
379
  lastChild.innerHTML += html;
379
380
  return null;