agentgui 1.0.829 → 1.0.830
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 +1 -0
- package/package.json +1 -1
- package/static/index.html +21 -0
- package/static/js/client-agents.js +155 -0
- package/static/js/client-cache.js +172 -0
- package/static/js/client-conv.js +198 -0
- package/static/js/client-events.js +164 -0
- package/static/js/client-exec.js +160 -0
- package/static/js/client-helpers.js +164 -0
- package/static/js/client-load.js +175 -0
- package/static/js/client-render.js +132 -0
- package/static/js/client-scroll.js +178 -0
- package/static/js/client-status.js +167 -0
- package/static/js/client-streaming.js +117 -0
- package/static/js/client-streaming2.js +116 -0
- package/static/js/client-streaming3.js +153 -0
- package/static/js/client-streaming4.js +194 -0
- package/static/js/client-ui-controls.js +170 -0
- package/static/js/client-ui.js +128 -0
- package/static/js/client-ui2.js +159 -0
- package/static/js/client-url.js +93 -0
- package/static/js/client-utils.js +175 -0
- package/static/js/client-ws-msg.js +88 -0
- package/static/js/client-ws.js +161 -0
- package/static/js/client.js +145 -3211
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/package.json
CHANGED
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">✎</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">✎</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
|
+
});
|