agentgui 1.0.829 → 1.0.831

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/CHANGELOG.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ## 2026-04-12
2
+ - refactor: split client.js (3212L) into 22 files ≤200L each; all 122 methods preserved including 2 extracted helpers (_setupUIButtonEvents, _setupUIWindowEvents, _loadConvRender); index.html updated with correct load order
2
3
  - refactor: split streaming-renderer.js (2193L) into 15 files ≤200L each; all 75 prototype methods and 8 static methods preserved; index.html updated with correct load order
3
4
 
4
5
  ## 2026-04-11
package/CLAUDE.md CHANGED
@@ -74,7 +74,28 @@ static/js/app-shortcuts.js Keyboard shortcuts overlay
74
74
  static/theme.js Theme switching
75
75
  static/css/main.css All application styles (extracted from index.html)
76
76
  static/css/tools-popup.css Tool popup styles
77
- static/js/client.js Main client logic
77
+ static/js/client.js AgentGUIClient class (constructor + _dbg + init); instantiation at bottom
78
+ static/js/client-ws.js WebSocket listeners, _convIsStreaming, _setConvStreaming, setupRendererListeners, restoreStateFromUrl, isValidId (prototype extension)
79
+ static/js/client-url.js URL/scroll helpers: updateUrlForConversation, saveScrollPosition, restoreScrollPosition, setupScrollTracking (prototype extension)
80
+ static/js/client-ui.js setupUI (modified, calls _setupUIButtonEvents/_setupUIWindowEvents) + setupChatMicButton (prototype extension)
81
+ static/js/client-ui-controls.js _setupUIButtonEvents + _setupUIWindowEvents extracted helpers (prototype extension)
82
+ static/js/client-ws-msg.js connectWebSocket, handleWebSocketMessage, queueEvent (prototype extension)
83
+ static/js/client-streaming.js handleStreamingStart (prototype extension)
84
+ static/js/client-streaming2.js handleStreamingResumed, handleStreamingProgress, _handleStreamingProgressInner (prototype extension)
85
+ static/js/client-streaming3.js renderBlockContent, scrollToBottom, _showNewContentPill, _removeNewContentPill, handleStreamingError (prototype extension)
86
+ static/js/client-streaming4.js handleStreamingComplete, _promptPushIfWeOwnRemote, handleConversationCreated, handleMessageCreated, queue handlers (prototype extension)
87
+ static/js/client-events.js fetchAndRenderQueue, handleRateLimitHit/Clear, handleAllConversationsDeleted, isHtmlContent, sanitizeHtml, parseMarkdownCodeBlocks (prototype extension)
88
+ static/js/client-render.js renderCodeBlock, renderMessageContent (prototype extension)
89
+ static/js/client-exec.js startExecution, optimistic message helpers, _subscribeToConversationUpdates, _flushBgCache (prototype extension)
90
+ static/js/client-helpers.js _recoverMissedChunks, cache/placeholder/height/countdown/debug helpers, showLoadingSpinner/hideLoadingSpinner (prototype extension)
91
+ static/js/client-ui2.js _showWelcomeScreen, _showSkeletonLoading, streamToConversation, _hydrateSessionBlocks (prototype extension)
92
+ static/js/client-conv.js _getLazyObserver, _renderConversationContent, renderChunk, _renderChunkInner, loadAgents, loadSubAgentsForCli (prototype extension)
93
+ static/js/client-agents.js checkSpeechStatus, loadModelsForAgent, _populateModelSelector, lock/unlockAgentAndModel, applyAgentAndModelSelection, loadConversations, updateConnectionStatus (prototype extension)
94
+ static/js/client-status.js _updateConnectionIndicator, _handleModelDownloadProgress, _handleTTSSetupProgress, _toggleConnectionTooltip, updateMetrics, controls, toggleTheme, createNewConversation (prototype extension)
95
+ static/js/client-cache.js cacheCurrentConversation, invalidateCache, loadConversationMessages (prototype extension)
96
+ static/js/client-load.js _makeLoadRequest, _verifyRequestId, _completeLoadRequest, _loadConvRender (prototype extension)
97
+ static/js/client-scroll.js syncPromptState, updateBusyPromptArea, removeScrollUpDetection, setupScrollUpDetection (prototype extension)
98
+ static/js/client-utils.js renderMessagesFragment, renderMessages, escapeHtml, showError, on, emit, agent/model getters, draft/prompt helpers, destroy (prototype extension)
78
99
  static/js/conversations.js Conversation management (class definition)
79
100
  static/js/conv-list-renderer.js Conversation list render, CRUD, WS listener (prototype extension)
80
101
  static/js/conv-sidebar-actions.js Sidebar delegated listeners, folder browser (prototype extension)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.829",
3
+ "version": "1.0.831",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/static/index.html CHANGED
@@ -332,6 +332,27 @@
332
332
  <script defer src="/gm/js/pm2-monitor.js"></script>
333
333
  <script defer src="/gm/js/event-filter-config.js"></script>
334
334
  <script defer src="/gm/js/client.js"></script>
335
+ <script defer src="/gm/js/client-ws.js"></script>
336
+ <script defer src="/gm/js/client-url.js"></script>
337
+ <script defer src="/gm/js/client-ui.js"></script>
338
+ <script defer src="/gm/js/client-ui-controls.js"></script>
339
+ <script defer src="/gm/js/client-ws-msg.js"></script>
340
+ <script defer src="/gm/js/client-streaming.js"></script>
341
+ <script defer src="/gm/js/client-streaming2.js"></script>
342
+ <script defer src="/gm/js/client-streaming3.js"></script>
343
+ <script defer src="/gm/js/client-streaming4.js"></script>
344
+ <script defer src="/gm/js/client-events.js"></script>
345
+ <script defer src="/gm/js/client-render.js"></script>
346
+ <script defer src="/gm/js/client-exec.js"></script>
347
+ <script defer src="/gm/js/client-helpers.js"></script>
348
+ <script defer src="/gm/js/client-ui2.js"></script>
349
+ <script defer src="/gm/js/client-conv.js"></script>
350
+ <script defer src="/gm/js/client-agents.js"></script>
351
+ <script defer src="/gm/js/client-status.js"></script>
352
+ <script defer src="/gm/js/client-cache.js"></script>
353
+ <script defer src="/gm/js/client-load.js"></script>
354
+ <script defer src="/gm/js/client-scroll.js"></script>
355
+ <script defer src="/gm/js/client-utils.js"></script>
335
356
  <script defer src="/gm/js/features.js"></script>
336
357
  <script defer src="/gm/js/agent-auth.js"></script>
337
358
  <script defer src="/gm/js/agent-auth-oauth.js"></script>
@@ -0,0 +1,155 @@
1
+ Object.assign(AgentGUIClient.prototype, {
2
+ async checkSpeechStatus() {
3
+ try {
4
+ const status = await window.wsClient.rpc('speech.status');
5
+ if (status.modelsComplete) {
6
+ this._modelDownloadProgress = { done: true, complete: true };
7
+ this._modelDownloadInProgress = false;
8
+ } else if (status.modelsDownloading) {
9
+ this._modelDownloadProgress = status.modelsProgress || { downloading: true };
10
+ this._modelDownloadInProgress = true;
11
+ } else {
12
+ this._modelDownloadProgress = { done: false };
13
+ this._modelDownloadInProgress = false;
14
+ }
15
+ } catch (error) {
16
+ console.error('Failed to check speech status:', error);
17
+ this._modelDownloadProgress = { done: false };
18
+ this._modelDownloadInProgress = false;
19
+ }
20
+ },
21
+
22
+
23
+ async loadModelsForAgent(agentId) {
24
+ if (!agentId || !this.ui.modelSelector) return;
25
+ const cached = this._modelCache.get(agentId);
26
+ if (cached) {
27
+ this._populateModelSelector(cached);
28
+ return;
29
+ }
30
+ try {
31
+ const { models } = await window.wsClient.rpc('agent.models', { id: agentId });
32
+ this._modelCache.set(agentId, models);
33
+ this._populateModelSelector(models);
34
+ } catch (error) {
35
+ console.error('Failed to load models:', error);
36
+ this._populateModelSelector([]);
37
+ }
38
+ },
39
+
40
+
41
+ _populateModelSelector(models) {
42
+ if (!this.ui.modelSelector) return;
43
+ if (!models || models.length === 0) {
44
+ this.ui.modelSelector.innerHTML = '';
45
+ this.ui.modelSelector.setAttribute('data-empty', 'true');
46
+ return;
47
+ }
48
+ this.ui.modelSelector.removeAttribute('data-empty');
49
+ this.ui.modelSelector.innerHTML = models
50
+ .map(m => `<option value="${m.id}">${this.escapeHtml(m.label)}</option>`)
51
+ .join('');
52
+ },
53
+
54
+
55
+ lockAgentAndModel(agentId, model) {
56
+ this._agentLocked = true;
57
+ if (this.ui.cliSelector) {
58
+ this.ui.cliSelector.disabled = true;
59
+ }
60
+
61
+ this.loadModelsForAgent(agentId).then(() => {
62
+ if (this.ui.modelSelector && model) {
63
+ this.ui.modelSelector.value = model;
64
+ }
65
+ });
66
+ },
67
+
68
+
69
+ unlockAgentAndModel() {
70
+ this._agentLocked = false;
71
+ if (this.ui.cliSelector) {
72
+ this.ui.cliSelector.disabled = false;
73
+ if (this.ui.cliSelector.options.length > 0) {
74
+ this.ui.cliSelector.style.display = 'inline-block';
75
+ }
76
+ }
77
+ if (this.ui.modelSelector) {
78
+ this.ui.modelSelector.disabled = false;
79
+ }
80
+ },
81
+
82
+
83
+ applyAgentAndModelSelection(conversation, hasActivity) {
84
+ const agentId = conversation.agentId || conversation.agentType || null;
85
+ const model = conversation.model || null;
86
+ const subAgent = conversation.subAgent || null;
87
+
88
+ if (hasActivity) {
89
+ this._setCliSelectorToAgent(agentId);
90
+ this.lockAgentAndModel(agentId, model);
91
+ } else {
92
+ this.unlockAgentAndModel();
93
+ this._setCliSelectorToAgent(agentId);
94
+
95
+ const effectiveAgentId = agentId || this.getEffectiveAgentId();
96
+ this.loadSubAgentsForCli(effectiveAgentId).then(() => {
97
+ if (subAgent && this.ui.agentSelector) {
98
+ this.ui.agentSelector.value = subAgent;
99
+ }
100
+ });
101
+ this.loadModelsForAgent(effectiveAgentId).then(() => {
102
+ if (model && this.ui.modelSelector) {
103
+ this.ui.modelSelector.value = model;
104
+ }
105
+ });
106
+ }
107
+ },
108
+
109
+
110
+ _setCliSelectorToAgent(agentId) {
111
+ if (this.ui.cliSelector) {
112
+ this.ui.cliSelector.value = agentId;
113
+ if (!this.ui.cliSelector.value) {
114
+ this.ui.cliSelector.selectedIndex = 0;
115
+ }
116
+ }
117
+ },
118
+
119
+
120
+ async loadConversations() {
121
+ const now = Date.now();
122
+ if (this.conversationListCache.data.length > 0 &&
123
+ (now - this.conversationListCache.timestamp) < this.conversationListCache.ttl) {
124
+ this.state.conversations = this.conversationListCache.data;
125
+ return this.conversationListCache.data;
126
+ }
127
+
128
+ return this._dedupedFetch('loadConversations', async () => {
129
+ try {
130
+ const { conversations } = await window.wsClient.rpc('conv.ls');
131
+ this.state.conversations = conversations;
132
+ this.conversationListCache.data = conversations;
133
+ this.conversationListCache.timestamp = Date.now();
134
+ return conversations;
135
+ } catch (error) {
136
+ console.error('Failed to load conversations:', error);
137
+ return [];
138
+ }
139
+ });
140
+ },
141
+
142
+
143
+ updateConnectionStatus(status) {
144
+ if (this.ui.statusIndicator) {
145
+ this.ui.statusIndicator.dataset.status = status;
146
+ this.ui.statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
147
+ }
148
+ if (status === 'disconnected' || status === 'reconnecting') {
149
+ this._updateConnectionIndicator(status);
150
+ } else if (status === 'connected') {
151
+ this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
152
+ }
153
+ }
154
+
155
+ });
@@ -0,0 +1,172 @@
1
+ Object.assign(AgentGUIClient.prototype, {
2
+ cacheCurrentConversation() {
3
+ const convId = this.state.currentConversation?.id;
4
+ if (!convId) return;
5
+ const outputEl = document.getElementById('output');
6
+ if (!outputEl || !outputEl.firstChild) return;
7
+ if (this._convIsStreaming(convId)) return;
8
+
9
+ this.saveScrollPosition(convId);
10
+ const clone = outputEl.cloneNode(true);
11
+ this.conversationCache.set(convId, {
12
+ dom: clone,
13
+ conversation: this.state.currentConversation,
14
+ timestamp: Date.now()
15
+ });
16
+
17
+ if (this.conversationCache.size > this.MAX_CACHE_SIZE) {
18
+ const oldest = this.conversationCache.keys().next().value;
19
+ this.conversationCache.delete(oldest);
20
+ }
21
+ },
22
+
23
+
24
+ invalidateCache(conversationId) {
25
+ this.conversationCache.delete(conversationId);
26
+ },
27
+
28
+
29
+ async loadConversationMessages(conversationId) {
30
+ performance.mark(`conv-load-start:${conversationId}`);
31
+ try {
32
+ if (this._previousConvAbort) {
33
+ this._previousConvAbort.abort();
34
+ }
35
+ this._previousConvAbort = new AbortController();
36
+ const convSignal = this._previousConvAbort.signal;
37
+
38
+ const prevConversationId = this.state.currentConversation?.id;
39
+ const availableFallback = this.state.conversations?.find(c => c.id !== conversationId) || null;
40
+ this.cacheCurrentConversation();
41
+
42
+ this.removeScrollUpDetection();
43
+ if (this.renderer.resetScrollState) this.renderer.resetScrollState();
44
+ this._userScrolledUp = false;
45
+ this._removeNewContentPill();
46
+
47
+ if (this.ui.messageInput) {
48
+ this.ui.messageInput.value = '';
49
+ this.ui.messageInput.style.height = 'auto';
50
+ }
51
+
52
+ if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
53
+ if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
54
+ if (this.ui.queueButton) this.ui.queueButton.classList.remove('visible');
55
+ if (this.ui.sendButton) this.ui.sendButton.style.display = '';
56
+
57
+ var prevId = this.state.currentConversation?.id;
58
+ if (prevId && prevId !== conversationId) {
59
+ if (this.wsManager.isConnected && !this._convIsStreaming(prevId)) {
60
+ this.wsManager.sendMessage({ type: 'unsubscribe', conversationId: prevId });
61
+ }
62
+ this.state.currentSession = null;
63
+ }
64
+
65
+ const cachedConv = this.state.conversations.find(c => c.id === conversationId);
66
+ if (cachedConv && this.state.currentConversation?.id !== conversationId) {
67
+ window.ConversationState?.selectConversation(conversationId, 'cache_load', 1);
68
+ this.state.currentConversation = cachedConv;
69
+ }
70
+
71
+ this.updateUrlForConversation(conversationId);
72
+ if (this.wsManager.isConnected) {
73
+ this.wsManager.sendMessage({ type: 'subscribe', conversationId });
74
+ }
75
+
76
+ const cached = this.conversationCache.get(conversationId);
77
+ if (cached) { this.conversationCache.delete(conversationId); this.conversationCache.set(conversationId, cached); }
78
+ if (cached && (Date.now() - cached.timestamp) < 600000) {
79
+ const outputEl = document.getElementById('output');
80
+ if (outputEl) {
81
+ const children = [];
82
+ while (cached.dom.firstChild) children.push(cached.dom.firstChild);
83
+ outputEl.replaceChildren(...children);
84
+ window.ConversationState?.selectConversation(conversationId, 'dom_cache_load', 1);
85
+ this.state.currentConversation = cached.conversation;
86
+ window.dispatchEvent(new CustomEvent('conversation-changed', { detail: { conversationId, conversation: cached.conversation } }));
87
+ const cachedHasActivity = cached.conversation.messageCount > 0 || this._convIsStreaming(conversationId);
88
+ this.applyAgentAndModelSelection(cached.conversation, cachedHasActivity);
89
+ this.conversationCache.delete(conversationId);
90
+ if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
91
+ this._flushBgCache && this._flushBgCache(conversationId);
92
+ this.setupScrollUpDetection && this.setupScrollUpDetection();
93
+ this.syncPromptState(conversationId);
94
+ this.restoreScrollPosition(conversationId);
95
+ return;
96
+ }
97
+ }
98
+
99
+ this.conversationCache.delete(conversationId);
100
+
101
+ if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
102
+ this._showSkeletonLoading(conversationId);
103
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
104
+
105
+ let fullData;
106
+ try {
107
+ fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
108
+ performance.mark(`conv-data-received:${conversationId}`);
109
+ if (convSignal.aborted) return;
110
+ } catch (wsErr) {
111
+ if (wsErr.code === 404) {
112
+ console.warn('Conversation no longer exists:', conversationId);
113
+ window.ConversationState?.clear('conversation_not_found');
114
+ this.state.currentConversation = null;
115
+ if (window.conversationManager) window.conversationManager.loadConversations();
116
+ const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
117
+ if (fallbackConv && fallbackConv !== conversationId) {
118
+ this._dbg('Resuming from fallback conversation:', fallbackConv);
119
+ this.showError('Conversation not found. Resuming previous conversation.');
120
+ await this.loadConversationMessages(fallbackConv);
121
+ } else {
122
+ const outputEl = document.getElementById('output');
123
+ if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
124
+ this.enableControls();
125
+ }
126
+ return;
127
+ }
128
+ try {
129
+ const base = window.__BASE_URL || '';
130
+ const [convRes, chunksRes, msgsRes] = await Promise.all([
131
+ fetch(`${base}/api/conversations/${conversationId}`),
132
+ fetch(`${base}/api/conversations/${conversationId}/chunks`),
133
+ fetch(`${base}/api/conversations/${conversationId}/messages?limit=500`)
134
+ ]);
135
+ const convData = await convRes.json();
136
+ const chunksData = await chunksRes.json();
137
+ const msgsData = await msgsRes.json();
138
+ fullData = {
139
+ conversation: convData.conversation,
140
+ isActivelyStreaming: false,
141
+ latestSession: null,
142
+ chunks: chunksData.chunks || [],
143
+ totalChunks: chunksData.totalChunks || (chunksData.chunks || []).length,
144
+ messages: msgsData.messages || []
145
+ };
146
+ if (convSignal.aborted) return;
147
+ } catch (restErr) {
148
+ throw wsErr;
149
+ }
150
+ }
151
+ if (convSignal.aborted) return;
152
+ await this._loadConvRender(conversationId, convSignal, prevConversationId, availableFallback, fullData);
153
+ } catch (error) {
154
+ if (error.name === 'AbortError') return;
155
+ console.error('Failed to load conversation messages:', error);
156
+ const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
157
+ if (fallbackConv && fallbackConv !== conversationId) {
158
+ this._dbg('Resuming from fallback conversation due to error:', fallbackConv);
159
+ this.showError('Failed to load conversation. Resuming previous conversation.');
160
+ try {
161
+ await this.loadConversationMessages(fallbackConv);
162
+ } catch (fallbackError) {
163
+ console.error('Failed to resume fallback conversation:', fallbackError);
164
+ this.showError('Failed to load conversation: ' + error.message);
165
+ }
166
+ } else {
167
+ this.showError('Failed to load conversation: ' + error.message);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ });
@@ -0,0 +1,198 @@
1
+ Object.assign(AgentGUIClient.prototype, {
2
+ _getLazyObserver() {
3
+ if (this._lazyObserver) return this._lazyObserver;
4
+ if (typeof IntersectionObserver === 'undefined') return null;
5
+ this._lazyObserver = new IntersectionObserver((entries) => {
6
+ for (const entry of entries) {
7
+ if (!entry.isIntersecting) continue;
8
+ const msgDiv = entry.target;
9
+ const pendingChunks = msgDiv._lazyChunks;
10
+ if (!pendingChunks) continue;
11
+ delete msgDiv._lazyChunks;
12
+ this._lazyObserver.unobserve(msgDiv);
13
+ const blocksEl = msgDiv.querySelector('.message-blocks');
14
+ if (blocksEl) this._hydrateSessionBlocks(blocksEl, pendingChunks);
15
+ }
16
+ }, { rootMargin: '400px 0px' });
17
+ return this._lazyObserver;
18
+ },
19
+
20
+
21
+ _renderConversationContent(messagesContainer, chunks, userMessages, activeSessionId) {
22
+ if (!chunks || chunks.length === 0) return;
23
+ const sessionMap = new Map();
24
+ for (const chunk of chunks) {
25
+ if (!sessionMap.has(chunk.sessionId)) sessionMap.set(chunk.sessionId, []);
26
+ sessionMap.get(chunk.sessionId).push(chunk);
27
+ }
28
+
29
+ const sessionIds = [...sessionMap.keys()];
30
+ const EAGER_TAIL = 8;
31
+ const eagerSet = new Set(sessionIds.slice(-EAGER_TAIL));
32
+ if (activeSessionId) eagerSet.add(activeSessionId);
33
+ const observer = sessionIds.length > EAGER_TAIL ? this._getLazyObserver() : null;
34
+
35
+ const frag = document.createDocumentFragment();
36
+ let ui = 0;
37
+ for (const [sid, list] of sessionMap) {
38
+ const sessionStart = list[0].created_at;
39
+ while (ui < userMessages.length && userMessages[ui].created_at <= sessionStart) {
40
+ const m = userMessages[ui++];
41
+ const uDiv = document.createElement('div');
42
+ uDiv.className = 'message message-user';
43
+ uDiv.setAttribute('data-msg-id', m.id);
44
+ uDiv.innerHTML = `<div class="message-role">User<button class="msg-edit-btn" data-edit-msg="${this.escapeHtml(m.id)}" title="Edit and re-run">&#9998;</button></div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
45
+ frag.appendChild(uDiv);
46
+ }
47
+ const isActive = sid === activeSessionId;
48
+ const msgDiv = document.createElement('div');
49
+ msgDiv.className = `message message-assistant${isActive ? ' streaming-message' : ''}`;
50
+ msgDiv.id = isActive ? `streaming-${sid}` : `message-${sid}`;
51
+ msgDiv.setAttribute('data-session-id', sid);
52
+ msgDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
53
+ const blocksEl = msgDiv.querySelector('.message-blocks');
54
+
55
+ if (observer && !eagerSet.has(sid)) {
56
+ msgDiv._lazyChunks = list;
57
+ observer.observe(msgDiv);
58
+ } else {
59
+ this._hydrateSessionBlocks(blocksEl, list);
60
+ }
61
+
62
+ if (isActive) {
63
+ const ind = document.createElement('div');
64
+ ind.className = 'streaming-indicator';
65
+ ind.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
66
+ ind.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">Processing...</span>';
67
+ msgDiv.appendChild(ind);
68
+ } else {
69
+ const ts = document.createElement('div');
70
+ ts.className = 'message-timestamp';
71
+ ts.textContent = new Date(list[list.length - 1].created_at).toLocaleString();
72
+ msgDiv.appendChild(ts);
73
+ }
74
+ frag.appendChild(msgDiv);
75
+ }
76
+ while (ui < userMessages.length) {
77
+ const m = userMessages[ui++];
78
+ const uDiv = document.createElement('div');
79
+ uDiv.className = 'message message-user';
80
+ uDiv.setAttribute('data-msg-id', m.id);
81
+ uDiv.innerHTML = `<div class="message-role">User<button class="msg-edit-btn" data-edit-msg="${this.escapeHtml(m.id)}" title="Edit and re-run">&#9998;</button></div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
82
+ frag.appendChild(uDiv);
83
+ }
84
+ messagesContainer.appendChild(frag);
85
+ },
86
+
87
+
88
+ renderChunk(chunk) {
89
+ try { return this._renderChunkInner(chunk); } catch (e) { console.error('[render-error] chunk:', e); }
90
+ },
91
+
92
+ _renderChunkInner(chunk) {
93
+ if (!chunk || !chunk.block) return;
94
+ const seq = chunk.sequence;
95
+ if (seq !== undefined) {
96
+ const seen = (this._renderedSeqs = this._renderedSeqs || {})[chunk.sessionId] || (this._renderedSeqs[chunk.sessionId] = new Set());
97
+ if (seen.has(seq)) return;
98
+ seen.add(seq);
99
+ }
100
+ const streamingEl = document.getElementById(`streaming-${chunk.sessionId}`);
101
+ if (!streamingEl) return;
102
+ const blocksEl = streamingEl.querySelector('.streaming-blocks');
103
+ if (!blocksEl) return;
104
+ if (chunk.block.type === 'tool_result') {
105
+ const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
106
+ const lastEl = blocksEl.lastElementChild;
107
+ const toolUseEl = matchById || (lastEl?.classList?.contains('block-tool-use') ? lastEl : null);
108
+ if (toolUseEl) {
109
+ this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
110
+ this.scrollToBottom();
111
+ return;
112
+ }
113
+ }
114
+ if (chunk.block.type === 'tool_status' || chunk.block.type === 'hook_progress') return;
115
+ const element = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
116
+ if (!element) { this.scrollToBottom(); return; }
117
+ blocksEl.appendChild(element);
118
+ this.scrollToBottom();
119
+ },
120
+
121
+
122
+ async loadAgents() {
123
+ return this._dedupedFetch('loadAgents', async () => {
124
+ try {
125
+ const { agents } = await window.wsClient.rpc('agent.ls');
126
+ this.state.agents = agents;
127
+
128
+ const displayAgents = agents;
129
+
130
+ if (this.ui.cliSelector) {
131
+ if (displayAgents.length > 0) {
132
+ this.ui.cliSelector.innerHTML = displayAgents
133
+ .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
134
+ .join('');
135
+ this.ui.cliSelector.style.display = 'inline-block';
136
+ } else {
137
+ this.ui.cliSelector.innerHTML = '';
138
+ this.ui.cliSelector.style.display = 'none';
139
+ }
140
+ }
141
+
142
+ window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
143
+
144
+ if (displayAgents.length > 0 && !this._agentLocked) {
145
+ const firstId = displayAgents[0].id;
146
+ await this.loadSubAgentsForCli(firstId);
147
+ this.loadModelsForAgent(this.getEffectiveAgentId());
148
+ }
149
+ return agents;
150
+ } catch (error) {
151
+ console.error('Failed to load agents:', error);
152
+ return [];
153
+ }
154
+ });
155
+ },
156
+
157
+
158
+ async loadSubAgentsForCli(cliAgentId) {
159
+ if (this.ui.agentSelector) {
160
+ this.ui.agentSelector.innerHTML = '';
161
+ this.ui.agentSelector.style.display = 'none';
162
+ }
163
+ try {
164
+ const { subAgents } = await window.wsClient.rpc('agent.subagents', { id: cliAgentId });
165
+ if (subAgents && subAgents.length > 0 && this.ui.agentSelector) {
166
+ this.ui.agentSelector.innerHTML = subAgents
167
+ .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
168
+ .join('');
169
+ this.ui.agentSelector.style.display = 'inline-block';
170
+ this._dbg(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
171
+ const firstSubAgentId = subAgents[0].id;
172
+ this.ui.agentSelector.value = firstSubAgentId;
173
+ this.loadModelsForAgent(cliAgentId);
174
+ } else {
175
+ this._dbg(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
176
+ const cliToAcpMap = {
177
+ 'cli-opencode': 'opencode',
178
+ 'cli-gemini': 'gemini',
179
+ 'cli-kilo': 'kilo',
180
+ 'cli-codex': 'codex'
181
+ };
182
+ const acpAgentId = cliToAcpMap[cliAgentId] || cliAgentId;
183
+ this.loadModelsForAgent(acpAgentId);
184
+ }
185
+ } catch (err) {
186
+ console.warn(`[Agent Selector] Failed to load sub-agents for ${cliAgentId}:`, err.message);
187
+ const cliToAcpMap = {
188
+ 'cli-opencode': 'opencode',
189
+ 'cli-gemini': 'gemini',
190
+ 'cli-kilo': 'kilo',
191
+ 'cli-codex': 'codex'
192
+ };
193
+ const acpAgentId = cliToAcpMap[cliAgentId] || cliAgentId;
194
+ this.loadModelsForAgent(acpAgentId);
195
+ }
196
+ }
197
+
198
+ });