agentgui 1.0.102 → 1.0.104
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/.prd +589 -0
- package/WAVE6_FINAL_REPORT.md +178 -0
- package/database.js +121 -0
- package/package.json +1 -1
- package/server.js +109 -59
- package/static/js/client.js +391 -98
package/static/js/client.js
CHANGED
|
@@ -71,6 +71,25 @@ class AgentGUIClient {
|
|
|
71
71
|
await this.connectWebSocket();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Initialize chunk polling state
|
|
75
|
+
this.chunkPollState = {
|
|
76
|
+
isPolling: false,
|
|
77
|
+
lastFetchTimestamp: 0,
|
|
78
|
+
pollTimer: null,
|
|
79
|
+
backoffDelay: 100,
|
|
80
|
+
maxBackoffDelay: 400,
|
|
81
|
+
abortController: null
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Initialize router state
|
|
85
|
+
this.routerState = {
|
|
86
|
+
currentConversationId: null,
|
|
87
|
+
currentSessionId: null
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Restore state from URL on page load
|
|
91
|
+
this.restoreStateFromUrl();
|
|
92
|
+
|
|
74
93
|
this.state.isInitialized = true;
|
|
75
94
|
this.emit('initialized');
|
|
76
95
|
|
|
@@ -134,6 +153,118 @@ class AgentGUIClient {
|
|
|
134
153
|
});
|
|
135
154
|
}
|
|
136
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Router state management: restore conversation from URL
|
|
158
|
+
* Format: ?conversation=<id>&session=<id>
|
|
159
|
+
*/
|
|
160
|
+
restoreStateFromUrl() {
|
|
161
|
+
const params = new URLSearchParams(window.location.search);
|
|
162
|
+
const conversationId = params.get('conversation');
|
|
163
|
+
const sessionId = params.get('session');
|
|
164
|
+
|
|
165
|
+
if (conversationId && this.isValidId(conversationId)) {
|
|
166
|
+
this.routerState.currentConversationId = conversationId;
|
|
167
|
+
if (sessionId && this.isValidId(sessionId)) {
|
|
168
|
+
this.routerState.currentSessionId = sessionId;
|
|
169
|
+
}
|
|
170
|
+
console.log('Restoring conversation from URL:', conversationId);
|
|
171
|
+
this.loadConversationMessages(conversationId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate ID format to prevent XSS
|
|
177
|
+
* Alphanumeric, dash, underscore only
|
|
178
|
+
*/
|
|
179
|
+
isValidId(id) {
|
|
180
|
+
if (!id || typeof id !== 'string') return false;
|
|
181
|
+
return /^[a-zA-Z0-9_-]+$/.test(id) && id.length < 256;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Update URL when conversation is selected
|
|
186
|
+
* Uses History API (pushState) for clean URLs
|
|
187
|
+
*/
|
|
188
|
+
updateUrlForConversation(conversationId, sessionId) {
|
|
189
|
+
if (!this.isValidId(conversationId)) return;
|
|
190
|
+
|
|
191
|
+
this.routerState.currentConversationId = conversationId;
|
|
192
|
+
if (sessionId && this.isValidId(sessionId)) {
|
|
193
|
+
this.routerState.currentSessionId = sessionId;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const params = new URLSearchParams();
|
|
197
|
+
params.set('conversation', conversationId);
|
|
198
|
+
if (sessionId && this.isValidId(sessionId)) {
|
|
199
|
+
params.set('session', sessionId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const url = `${window.location.pathname}?${params.toString()}`;
|
|
203
|
+
window.history.pushState({ conversationId, sessionId }, '', url);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Save scroll position to localStorage
|
|
208
|
+
* Key format: scroll_<conversationId>
|
|
209
|
+
*/
|
|
210
|
+
saveScrollPosition(conversationId) {
|
|
211
|
+
if (!this.isValidId(conversationId)) return;
|
|
212
|
+
|
|
213
|
+
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
214
|
+
if (scrollContainer) {
|
|
215
|
+
const position = scrollContainer.scrollTop;
|
|
216
|
+
try {
|
|
217
|
+
localStorage.setItem(`scroll_${conversationId}`, position.toString());
|
|
218
|
+
console.log(`Saved scroll position for ${conversationId}: ${position}`);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.warn('Failed to save scroll position:', e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Restore scroll position from localStorage
|
|
227
|
+
* Restores after conversation loads
|
|
228
|
+
*/
|
|
229
|
+
restoreScrollPosition(conversationId) {
|
|
230
|
+
if (!this.isValidId(conversationId)) return;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const position = localStorage.getItem(`scroll_${conversationId}`);
|
|
234
|
+
if (position !== null) {
|
|
235
|
+
const scrollTop = parseInt(position, 10);
|
|
236
|
+
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
237
|
+
if (scrollContainer && !isNaN(scrollTop)) {
|
|
238
|
+
requestAnimationFrame(() => {
|
|
239
|
+
scrollContainer.scrollTop = scrollTop;
|
|
240
|
+
console.log(`Restored scroll position for ${conversationId}: ${scrollTop}`);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.warn('Failed to restore scroll position:', e);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Setup scroll position tracking
|
|
251
|
+
* Debounced to avoid excessive localStorage writes
|
|
252
|
+
*/
|
|
253
|
+
setupScrollTracking() {
|
|
254
|
+
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
255
|
+
if (!scrollContainer) return;
|
|
256
|
+
|
|
257
|
+
let scrollTimer = null;
|
|
258
|
+
scrollContainer.addEventListener('scroll', () => {
|
|
259
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
260
|
+
scrollTimer = setTimeout(() => {
|
|
261
|
+
if (this.state.currentConversation?.id) {
|
|
262
|
+
this.saveScrollPosition(this.state.currentConversation.id);
|
|
263
|
+
}
|
|
264
|
+
}, 500); // Debounce 500ms
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
137
268
|
/**
|
|
138
269
|
* Setup UI elements
|
|
139
270
|
*/
|
|
@@ -174,6 +305,9 @@ class AgentGUIClient {
|
|
|
174
305
|
themeToggle.addEventListener('click', () => this.toggleTheme());
|
|
175
306
|
}
|
|
176
307
|
|
|
308
|
+
// Setup scroll position tracking for current conversation
|
|
309
|
+
this.setupScrollTracking();
|
|
310
|
+
|
|
177
311
|
window.addEventListener('create-new-conversation', (event) => {
|
|
178
312
|
const detail = event.detail || {};
|
|
179
313
|
this.createNewConversation(detail.workingDirectory, detail.title);
|
|
@@ -181,7 +315,9 @@ class AgentGUIClient {
|
|
|
181
315
|
|
|
182
316
|
// Listen for conversation selection
|
|
183
317
|
window.addEventListener('conversation-selected', (event) => {
|
|
184
|
-
|
|
318
|
+
const conversationId = event.detail.conversationId;
|
|
319
|
+
this.updateUrlForConversation(conversationId);
|
|
320
|
+
this.loadConversationMessages(conversationId);
|
|
185
321
|
});
|
|
186
322
|
}
|
|
187
323
|
|
|
@@ -255,6 +391,9 @@ class AgentGUIClient {
|
|
|
255
391
|
this.state.sessionEvents = [];
|
|
256
392
|
this.state.streamingBlocks = [];
|
|
257
393
|
|
|
394
|
+
// Update URL with session ID during streaming
|
|
395
|
+
this.updateUrlForConversation(data.conversationId, data.sessionId);
|
|
396
|
+
|
|
258
397
|
if (this.wsManager.isConnected) {
|
|
259
398
|
this.wsManager.subscribeToSession(data.sessionId);
|
|
260
399
|
}
|
|
@@ -281,97 +420,26 @@ class AgentGUIClient {
|
|
|
281
420
|
this.scrollToBottom();
|
|
282
421
|
}
|
|
283
422
|
|
|
423
|
+
// Start polling for chunks from database
|
|
424
|
+
this.startChunkPolling(data.conversationId);
|
|
425
|
+
|
|
284
426
|
this.disableControls();
|
|
285
427
|
this.emit('streaming:start', data);
|
|
286
428
|
}
|
|
287
429
|
|
|
288
430
|
handleStreamingProgress(data) {
|
|
431
|
+
// NOTE: With chunk-based architecture, blocks are rendered from polling
|
|
432
|
+
// This handler is kept for backward compatibility and to trigger polling updates
|
|
433
|
+
// But actual rendering happens in renderChunk() via polling
|
|
434
|
+
|
|
289
435
|
if (!data.block) return;
|
|
290
436
|
|
|
291
437
|
const block = data.block;
|
|
292
438
|
if (!this.state.streamingBlocks) this.state.streamingBlocks = [];
|
|
293
439
|
this.state.streamingBlocks.push(block);
|
|
294
440
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!streamingEl) return;
|
|
298
|
-
|
|
299
|
-
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
300
|
-
if (!blocksEl) return;
|
|
301
|
-
|
|
302
|
-
const indicator = streamingEl.querySelector('.streaming-indicator');
|
|
303
|
-
let indicatorText = 'Responding...';
|
|
304
|
-
|
|
305
|
-
if (block.type === 'system') {
|
|
306
|
-
const div = document.createElement('div');
|
|
307
|
-
div.className = 'streaming-block-system';
|
|
308
|
-
const toolCount = block.tools ? block.tools.length : 0;
|
|
309
|
-
div.innerHTML = `<span class="system-model-badge">${this.escapeHtml(block.model || 'unknown')}</span> <span class="system-info">${toolCount} tools available</span>`;
|
|
310
|
-
blocksEl.appendChild(div);
|
|
311
|
-
indicatorText = 'Initializing...';
|
|
312
|
-
} else if (block.type === 'text' && block.text) {
|
|
313
|
-
const existingTextEl = blocksEl.querySelector('.streaming-text-current');
|
|
314
|
-
if (existingTextEl && !data.isResult) {
|
|
315
|
-
existingTextEl.innerHTML = this.renderBlockContent(block);
|
|
316
|
-
} else {
|
|
317
|
-
const prevTextEl = blocksEl.querySelector('.streaming-text-current');
|
|
318
|
-
if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
|
|
319
|
-
const div = document.createElement('div');
|
|
320
|
-
div.className = 'message-text streaming-text-current';
|
|
321
|
-
div.innerHTML = this.renderBlockContent(block);
|
|
322
|
-
blocksEl.appendChild(div);
|
|
323
|
-
}
|
|
324
|
-
indicatorText = 'Responding...';
|
|
325
|
-
} else if (block.type === 'tool_use') {
|
|
326
|
-
const prevTextEl = blocksEl.querySelector('.streaming-text-current');
|
|
327
|
-
if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
|
|
328
|
-
|
|
329
|
-
const div = document.createElement('div');
|
|
330
|
-
div.className = 'streaming-block-tool-use';
|
|
331
|
-
div.dataset.toolUseId = block.id || '';
|
|
332
|
-
let inputHtml = '';
|
|
333
|
-
if (block.input && Object.keys(block.input).length > 0) {
|
|
334
|
-
const inputStr = JSON.stringify(block.input, null, 2);
|
|
335
|
-
inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
|
|
336
|
-
}
|
|
337
|
-
div.innerHTML = `<div class="tool-use-header"><span class="tool-use-icon">⚙</span> <span class="tool-use-name">${this.escapeHtml(block.name || 'unknown')}</span></div>${inputHtml}`;
|
|
338
|
-
blocksEl.appendChild(div);
|
|
339
|
-
indicatorText = `Using ${block.name || 'tool'}...`;
|
|
340
|
-
} else if (block.type === 'tool_result') {
|
|
341
|
-
const div = document.createElement('div');
|
|
342
|
-
div.className = 'streaming-block-tool-result' + (block.is_error ? ' tool-result-error' : '');
|
|
343
|
-
const content = block.content || '';
|
|
344
|
-
const displayContent = content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content;
|
|
345
|
-
div.innerHTML = `<div class="tool-result-header">${block.is_error ? '<span class="tool-result-error-badge">Error</span>' : '<span class="tool-result-ok-badge">Result</span>'}</div><pre class="tool-result-pre">${this.escapeHtml(displayContent)}</pre>`;
|
|
346
|
-
blocksEl.appendChild(div);
|
|
347
|
-
indicatorText = 'Processing result...';
|
|
348
|
-
} else if (block.type === 'result') {
|
|
349
|
-
const div = document.createElement('div');
|
|
350
|
-
div.className = 'streaming-block-result' + (block.is_error ? ' result-error' : '');
|
|
351
|
-
const duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
|
|
352
|
-
const cost = block.total_cost_usd ? '$' + block.total_cost_usd.toFixed(4) : '';
|
|
353
|
-
const turns = block.num_turns ? block.num_turns + ' turns' : '';
|
|
354
|
-
const parts = [duration, cost, turns].filter(Boolean);
|
|
355
|
-
div.innerHTML = `<span class="result-status">${block.is_error ? 'Failed' : 'Complete'}</span>${parts.length ? ' <span class="result-stats">' + parts.join(' / ') + '</span>' : ''}`;
|
|
356
|
-
blocksEl.appendChild(div);
|
|
357
|
-
indicatorText = 'Complete';
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (indicator) {
|
|
361
|
-
const labelEl = indicator.querySelector('.streaming-indicator-label');
|
|
362
|
-
if (labelEl) {
|
|
363
|
-
labelEl.textContent = indicatorText;
|
|
364
|
-
} else {
|
|
365
|
-
const existingLabel = indicator.querySelector('span:last-child');
|
|
366
|
-
if (existingLabel && !existingLabel.classList.contains('animate-spin')) existingLabel.remove();
|
|
367
|
-
const label = document.createElement('span');
|
|
368
|
-
label.className = 'streaming-indicator-label';
|
|
369
|
-
label.textContent = indicatorText;
|
|
370
|
-
indicator.appendChild(label);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
this.scrollToBottom();
|
|
441
|
+
// WebSocket is now just a notification trigger, not data source
|
|
442
|
+
// Actual blocks come from database polling in startChunkPolling()
|
|
375
443
|
}
|
|
376
444
|
|
|
377
445
|
renderBlockContent(block) {
|
|
@@ -426,6 +494,9 @@ class AgentGUIClient {
|
|
|
426
494
|
console.log('Streaming completed:', data);
|
|
427
495
|
this.state.isStreaming = false;
|
|
428
496
|
|
|
497
|
+
// Stop polling for chunks
|
|
498
|
+
this.stopChunkPolling();
|
|
499
|
+
|
|
429
500
|
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
430
501
|
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
431
502
|
if (streamingEl) {
|
|
@@ -441,6 +512,12 @@ class AgentGUIClient {
|
|
|
441
512
|
streamingEl.appendChild(ts);
|
|
442
513
|
}
|
|
443
514
|
|
|
515
|
+
// Save scroll position after streaming completes
|
|
516
|
+
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
517
|
+
if (conversationId) {
|
|
518
|
+
this.saveScrollPosition(conversationId);
|
|
519
|
+
}
|
|
520
|
+
|
|
444
521
|
this.enableControls();
|
|
445
522
|
this.emit('streaming:complete', data);
|
|
446
523
|
}
|
|
@@ -702,6 +779,148 @@ class AgentGUIClient {
|
|
|
702
779
|
}
|
|
703
780
|
}
|
|
704
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Fetch chunks from database for a conversation
|
|
784
|
+
* Supports incremental updates with since parameter
|
|
785
|
+
*/
|
|
786
|
+
async fetchChunks(conversationId, since = 0) {
|
|
787
|
+
if (!conversationId) return [];
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const params = new URLSearchParams();
|
|
791
|
+
if (since > 0) {
|
|
792
|
+
params.append('since', since.toString());
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const url = `${window.__BASE_URL}/api/conversations/${conversationId}/chunks?${params.toString()}`;
|
|
796
|
+
const response = await fetch(url);
|
|
797
|
+
|
|
798
|
+
if (!response.ok) {
|
|
799
|
+
throw new Error(`HTTP ${response.status}`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const data = await response.json();
|
|
803
|
+
if (!data.ok || !Array.isArray(data.chunks)) {
|
|
804
|
+
throw new Error('Invalid chunks response');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Parse JSON data field for each chunk
|
|
808
|
+
const chunks = data.chunks.map(chunk => ({
|
|
809
|
+
...chunk,
|
|
810
|
+
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
811
|
+
}));
|
|
812
|
+
|
|
813
|
+
return chunks;
|
|
814
|
+
} catch (error) {
|
|
815
|
+
console.error('Error fetching chunks:', error);
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Poll for new chunks at regular intervals
|
|
822
|
+
* Uses exponential backoff on errors
|
|
823
|
+
*/
|
|
824
|
+
async startChunkPolling(conversationId) {
|
|
825
|
+
if (!conversationId) return;
|
|
826
|
+
|
|
827
|
+
const pollState = this.chunkPollState;
|
|
828
|
+
if (pollState.isPolling) return; // Already polling
|
|
829
|
+
|
|
830
|
+
pollState.isPolling = true;
|
|
831
|
+
pollState.lastFetchTimestamp = Date.now();
|
|
832
|
+
pollState.backoffDelay = 100;
|
|
833
|
+
|
|
834
|
+
console.log('Starting chunk polling for conversation:', conversationId);
|
|
835
|
+
|
|
836
|
+
const pollOnce = async () => {
|
|
837
|
+
if (!pollState.isPolling) return;
|
|
838
|
+
|
|
839
|
+
try {
|
|
840
|
+
const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
|
|
841
|
+
|
|
842
|
+
if (chunks.length > 0) {
|
|
843
|
+
// Reset backoff on success
|
|
844
|
+
pollState.backoffDelay = 100;
|
|
845
|
+
|
|
846
|
+
// Update last fetch timestamp
|
|
847
|
+
const lastChunk = chunks[chunks.length - 1];
|
|
848
|
+
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
849
|
+
|
|
850
|
+
// Render new chunks
|
|
851
|
+
chunks.forEach(chunk => {
|
|
852
|
+
if (chunk.block && chunk.block.type) {
|
|
853
|
+
this.renderChunk(chunk);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Schedule next poll
|
|
859
|
+
if (pollState.isPolling) {
|
|
860
|
+
pollState.pollTimer = setTimeout(pollOnce, 100);
|
|
861
|
+
}
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.warn('Chunk poll error, applying backoff:', error.message);
|
|
864
|
+
|
|
865
|
+
// Apply exponential backoff
|
|
866
|
+
pollState.backoffDelay = Math.min(
|
|
867
|
+
pollState.backoffDelay * 2,
|
|
868
|
+
pollState.maxBackoffDelay
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
// Schedule next poll with backoff
|
|
872
|
+
if (pollState.isPolling) {
|
|
873
|
+
pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// Start polling loop
|
|
879
|
+
pollOnce();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Stop polling for chunks
|
|
884
|
+
*/
|
|
885
|
+
stopChunkPolling() {
|
|
886
|
+
const pollState = this.chunkPollState;
|
|
887
|
+
|
|
888
|
+
if (pollState.pollTimer) {
|
|
889
|
+
clearTimeout(pollState.pollTimer);
|
|
890
|
+
pollState.pollTimer = null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (pollState.abortController) {
|
|
894
|
+
pollState.abortController.abort();
|
|
895
|
+
pollState.abortController = null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
pollState.isPolling = false;
|
|
899
|
+
console.log('Stopped chunk polling');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Render a single chunk to the output
|
|
904
|
+
*/
|
|
905
|
+
renderChunk(chunk) {
|
|
906
|
+
if (!chunk || !chunk.block) return;
|
|
907
|
+
|
|
908
|
+
const sessionId = chunk.sessionId;
|
|
909
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
910
|
+
if (!streamingEl) return;
|
|
911
|
+
|
|
912
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
913
|
+
if (!blocksEl) return;
|
|
914
|
+
|
|
915
|
+
const block = chunk.block;
|
|
916
|
+
const element = this.renderer.renderBlock(block, chunk);
|
|
917
|
+
|
|
918
|
+
if (element) {
|
|
919
|
+
blocksEl.appendChild(element);
|
|
920
|
+
this.scrollToBottom();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
705
924
|
/**
|
|
706
925
|
* Load agents
|
|
707
926
|
*/
|
|
@@ -832,27 +1051,100 @@ class AgentGUIClient {
|
|
|
832
1051
|
const { conversation } = await convResponse.json();
|
|
833
1052
|
this.state.currentConversation = conversation;
|
|
834
1053
|
|
|
1054
|
+
// Update URL with conversation ID
|
|
1055
|
+
this.updateUrlForConversation(conversationId);
|
|
1056
|
+
|
|
835
1057
|
if (this.wsManager.isConnected) {
|
|
836
1058
|
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
837
1059
|
}
|
|
838
1060
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1061
|
+
// Try to fetch chunks first (Wave 3 architecture)
|
|
1062
|
+
try {
|
|
1063
|
+
const chunks = await this.fetchChunks(conversationId, 0);
|
|
1064
|
+
|
|
1065
|
+
const outputEl = document.getElementById('output');
|
|
1066
|
+
if (outputEl) {
|
|
1067
|
+
const wdInfo = conversation.workingDirectory ? ` - ${this.escapeHtml(conversation.workingDirectory)}` : '';
|
|
1068
|
+
outputEl.innerHTML = `
|
|
1069
|
+
<div class="conversation-header">
|
|
1070
|
+
<h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
|
|
1071
|
+
<p class="text-secondary">${conversation.agentType || 'unknown'} - ${new Date(conversation.created_at).toLocaleDateString()}${wdInfo}</p>
|
|
1072
|
+
</div>
|
|
1073
|
+
<div class="conversation-messages"></div>
|
|
1074
|
+
`;
|
|
1075
|
+
|
|
1076
|
+
// Render all chunks
|
|
1077
|
+
const messagesEl = outputEl.querySelector('.conversation-messages');
|
|
1078
|
+
if (chunks.length > 0) {
|
|
1079
|
+
// Group chunks by session
|
|
1080
|
+
const sessionChunks = {};
|
|
1081
|
+
chunks.forEach(chunk => {
|
|
1082
|
+
if (!sessionChunks[chunk.sessionId]) {
|
|
1083
|
+
sessionChunks[chunk.sessionId] = [];
|
|
1084
|
+
}
|
|
1085
|
+
sessionChunks[chunk.sessionId].push(chunk);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// Render each session's chunks
|
|
1089
|
+
Object.entries(sessionChunks).forEach(([sessionId, sessionChunkList]) => {
|
|
1090
|
+
const messageDiv = document.createElement('div');
|
|
1091
|
+
messageDiv.className = 'message message-assistant';
|
|
1092
|
+
messageDiv.id = `message-${sessionId}`;
|
|
1093
|
+
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks"></div>';
|
|
1094
|
+
|
|
1095
|
+
const blocksEl = messageDiv.querySelector('.message-blocks');
|
|
1096
|
+
sessionChunkList.forEach(chunk => {
|
|
1097
|
+
if (chunk.block && chunk.block.type) {
|
|
1098
|
+
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1099
|
+
if (element) {
|
|
1100
|
+
blocksEl.appendChild(element);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
const ts = document.createElement('div');
|
|
1106
|
+
ts.className = 'message-timestamp';
|
|
1107
|
+
ts.textContent = new Date(sessionChunkList[sessionChunkList.length - 1].created_at).toLocaleString();
|
|
1108
|
+
messageDiv.appendChild(ts);
|
|
1109
|
+
|
|
1110
|
+
messagesEl.appendChild(messageDiv);
|
|
1111
|
+
});
|
|
1112
|
+
} else {
|
|
1113
|
+
// Fall back to messages if no chunks
|
|
1114
|
+
const messagesResponse = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/messages`);
|
|
1115
|
+
if (messagesResponse.ok) {
|
|
1116
|
+
const messagesData = await messagesResponse.json();
|
|
1117
|
+
messagesEl.innerHTML = this.renderMessages(messagesData.messages || []);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Restore scroll position after rendering
|
|
1122
|
+
this.restoreScrollPosition(conversationId);
|
|
1123
|
+
}
|
|
1124
|
+
} catch (chunkError) {
|
|
1125
|
+
console.warn('Failed to fetch chunks, falling back to messages:', chunkError);
|
|
1126
|
+
|
|
1127
|
+
// Fallback: use messages
|
|
1128
|
+
const messagesResponse = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/messages`);
|
|
1129
|
+
if (!messagesResponse.ok) throw new Error(`Failed to fetch messages: ${messagesResponse.status}`);
|
|
1130
|
+
const messagesData = await messagesResponse.json();
|
|
1131
|
+
|
|
1132
|
+
const outputEl = document.getElementById('output');
|
|
1133
|
+
if (outputEl) {
|
|
1134
|
+
const wdInfo = conversation.workingDirectory ? ` - ${this.escapeHtml(conversation.workingDirectory)}` : '';
|
|
1135
|
+
outputEl.innerHTML = `
|
|
1136
|
+
<div class="conversation-header">
|
|
1137
|
+
<h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
|
|
1138
|
+
<p class="text-secondary">${conversation.agentType || 'unknown'} - ${new Date(conversation.created_at).toLocaleDateString()}${wdInfo}</p>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div class="conversation-messages">
|
|
1141
|
+
${this.renderMessages(messagesData.messages || [])}
|
|
1142
|
+
</div>
|
|
1143
|
+
`;
|
|
1144
|
+
|
|
1145
|
+
// Restore scroll position after rendering
|
|
1146
|
+
this.restoreScrollPosition(conversationId);
|
|
1147
|
+
}
|
|
856
1148
|
}
|
|
857
1149
|
} catch (error) {
|
|
858
1150
|
console.error('Failed to load conversation messages:', error);
|
|
@@ -1002,6 +1294,7 @@ class AgentGUIClient {
|
|
|
1002
1294
|
* Cleanup resources
|
|
1003
1295
|
*/
|
|
1004
1296
|
destroy() {
|
|
1297
|
+
this.stopChunkPolling();
|
|
1005
1298
|
this.renderer.destroy();
|
|
1006
1299
|
this.wsManager.destroy();
|
|
1007
1300
|
this.eventHandlers = {};
|