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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
async fetchAndRenderQueue(conversationId) {
|
|
3
|
+
const outputEl = document.querySelector('.conversation-messages');
|
|
4
|
+
if (!outputEl) return;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const { queue } = await window.wsClient.rpc('q.ls', { id: conversationId });
|
|
8
|
+
|
|
9
|
+
let queueEl = outputEl.querySelector('.queue-indicator');
|
|
10
|
+
if (!queue || queue.length === 0) {
|
|
11
|
+
if (queueEl) queueEl.remove();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!queueEl) {
|
|
16
|
+
queueEl = document.createElement('div');
|
|
17
|
+
queueEl.className = 'queue-indicator';
|
|
18
|
+
outputEl.appendChild(queueEl);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
queueEl.innerHTML = queue.map((q, i) => `
|
|
22
|
+
<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;">
|
|
23
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${i + 1}. ${this.escapeHtml(q.content)}</span>
|
|
24
|
+
<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>
|
|
25
|
+
<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>
|
|
26
|
+
</div>
|
|
27
|
+
`).join('');
|
|
28
|
+
|
|
29
|
+
if (!queueEl._listenersAttached) {
|
|
30
|
+
queueEl._listenersAttached = true;
|
|
31
|
+
queueEl.addEventListener('click', async (e) => {
|
|
32
|
+
if (e.target.classList.contains('queue-delete-btn')) {
|
|
33
|
+
const index = parseInt(e.target.dataset.index);
|
|
34
|
+
const msgId = queue[index].messageId;
|
|
35
|
+
if (await window.UIDialog.confirm('Delete this queued message?', 'Delete Message')) {
|
|
36
|
+
await window.wsClient.rpc('q.del', { id: conversationId, messageId: msgId });
|
|
37
|
+
}
|
|
38
|
+
} else if (e.target.classList.contains('queue-edit-btn')) {
|
|
39
|
+
const index = parseInt(e.target.dataset.index);
|
|
40
|
+
const q = queue[index];
|
|
41
|
+
const newContent = await window.UIDialog.prompt('Edit message:', q.content, 'Edit Queued Message');
|
|
42
|
+
if (newContent !== null && newContent !== q.content) {
|
|
43
|
+
window.wsClient.rpc('q.upd', { id: conversationId, messageId: q.messageId, content: newContent });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Failed to fetch queue:', err);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
handleRateLimitHit(data) {
|
|
55
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
56
|
+
this._setConvStreaming(data.conversationId, false);
|
|
57
|
+
|
|
58
|
+
this.enableControls();
|
|
59
|
+
|
|
60
|
+
const cooldownMs = data.retryAfterMs || 60000;
|
|
61
|
+
this._rateLimitSafetyTimer = setTimeout(() => {
|
|
62
|
+
this.enableControls();
|
|
63
|
+
}, cooldownMs + 10000);
|
|
64
|
+
|
|
65
|
+
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
66
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
67
|
+
if (streamingEl) {
|
|
68
|
+
const indicator = streamingEl.querySelector('.streaming-indicator');
|
|
69
|
+
if (indicator) {
|
|
70
|
+
const retrySeconds = Math.ceil(cooldownMs / 1000);
|
|
71
|
+
indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${retrySeconds}s...</span>`;
|
|
72
|
+
let remaining = retrySeconds;
|
|
73
|
+
const countdownTimer = setInterval(() => {
|
|
74
|
+
remaining--;
|
|
75
|
+
if (remaining <= 0) {
|
|
76
|
+
clearInterval(countdownTimer);
|
|
77
|
+
indicator.innerHTML = '<span style="color:var(--color-info);">Restarting...</span>';
|
|
78
|
+
} else {
|
|
79
|
+
indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${remaining}s...</span>`;
|
|
80
|
+
}
|
|
81
|
+
}, 1000);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
handleRateLimitClear(data) {
|
|
88
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
89
|
+
if (this._rateLimitSafetyTimer) {
|
|
90
|
+
clearTimeout(this._rateLimitSafetyTimer);
|
|
91
|
+
this._rateLimitSafetyTimer = null;
|
|
92
|
+
}
|
|
93
|
+
this.enableControls();
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
handleAllConversationsDeleted(data) {
|
|
98
|
+
window.ConversationState?.clear('all_deleted');
|
|
99
|
+
this.state.currentConversation = null;
|
|
100
|
+
this.state.conversations = [];
|
|
101
|
+
this.state.sessionEvents = [];
|
|
102
|
+
this.conversationCache.clear();
|
|
103
|
+
this.conversationListCache = { data: [], timestamp: 0, ttl: 30000 };
|
|
104
|
+
this.draftPrompts.clear();
|
|
105
|
+
window.dispatchEvent(new CustomEvent('conversation-deselected'));
|
|
106
|
+
const outputEl = document.getElementById('output');
|
|
107
|
+
if (outputEl) outputEl.innerHTML = '';
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
isHtmlContent(text) {
|
|
112
|
+
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
113
|
+
return htmlPattern.test(text);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
sanitizeHtml(html) {
|
|
118
|
+
const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
|
|
119
|
+
let cleaned = html.replace(dangerous, '');
|
|
120
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
121
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
|
|
122
|
+
cleaned = cleaned.replace(/javascript\s*:/gi, '');
|
|
123
|
+
return cleaned;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
parseMarkdownCodeBlocks(text) {
|
|
128
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
129
|
+
const parts = [];
|
|
130
|
+
let lastIndex = 0;
|
|
131
|
+
let match;
|
|
132
|
+
|
|
133
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
134
|
+
if (match.index > lastIndex) {
|
|
135
|
+
const segment = text.substring(lastIndex, match.index);
|
|
136
|
+
parts.push({
|
|
137
|
+
type: this.isHtmlContent(segment) ? 'html' : 'text',
|
|
138
|
+
content: segment
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
parts.push({
|
|
142
|
+
type: 'code',
|
|
143
|
+
language: match[1] || 'plain',
|
|
144
|
+
code: match[2]
|
|
145
|
+
});
|
|
146
|
+
lastIndex = codeBlockRegex.lastIndex;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (lastIndex < text.length) {
|
|
150
|
+
const segment = text.substring(lastIndex);
|
|
151
|
+
parts.push({
|
|
152
|
+
type: this.isHtmlContent(segment) ? 'html' : 'text',
|
|
153
|
+
content: segment
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (parts.length === 0) {
|
|
158
|
+
return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return parts;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
async startExecution() {
|
|
3
|
+
const prompt = this.ui.messageInput?.value || '';
|
|
4
|
+
|
|
5
|
+
if (!prompt.trim()) {
|
|
6
|
+
this.showError('Please enter a prompt');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.disableControls();
|
|
11
|
+
const ttsActive = window.TTSHandler && window.TTSHandler.getAutoSpeak && window.TTSHandler.getAutoSpeak();
|
|
12
|
+
const savedPrompt = ttsActive ? prompt + '\n\n[Respond optimized for text-to-speech: use short sentences, simple words, and focus on clarity.]' : prompt;
|
|
13
|
+
if (this.ui.messageInput) {
|
|
14
|
+
this.ui.messageInput.value = '';
|
|
15
|
+
this.ui.messageInput.style.height = 'auto';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pendingId = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
|
|
19
|
+
|
|
20
|
+
const isStreaming = this._convIsStreaming(this.state.currentConversation?.id);
|
|
21
|
+
if (!isStreaming) {
|
|
22
|
+
this._showOptimisticMessage(pendingId, savedPrompt);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
let conv = this.state.currentConversation;
|
|
27
|
+
|
|
28
|
+
if (this._isLoadingConversation) {
|
|
29
|
+
this.showError('Conversation still loading. Please try again.');
|
|
30
|
+
this.enableControls();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (conv && typeof conv === 'string') {
|
|
35
|
+
this.showError('Conversation state invalid. Please reload.');
|
|
36
|
+
this.enableControls();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (conv?.id) {
|
|
41
|
+
const isNewConversation = !conv.messageCount && !this._convIsStreaming(conv.id);
|
|
42
|
+
const agentId = (isNewConversation ? this.getCurrentAgent() : null) || conv?.agentType || this.getCurrentAgent();
|
|
43
|
+
const subAgent = this.getEffectiveSubAgent() || conv?.subAgent || null;
|
|
44
|
+
const model = this.ui.modelSelector?.value || null;
|
|
45
|
+
|
|
46
|
+
this.lockAgentAndModel(agentId, model);
|
|
47
|
+
await this.streamToConversation(conv.id, savedPrompt, agentId, model, subAgent);
|
|
48
|
+
this.clearDraft(conv.id);
|
|
49
|
+
if (!isStreaming) {
|
|
50
|
+
this._confirmOptimisticMessage(pendingId);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
const agentId = this.getCurrentAgent();
|
|
54
|
+
const subAgent = this.getEffectiveSubAgent() || null;
|
|
55
|
+
const model = this.ui.modelSelector?.value || null;
|
|
56
|
+
|
|
57
|
+
const body = { agentId, title: savedPrompt.substring(0, 50) };
|
|
58
|
+
if (model) body.model = model;
|
|
59
|
+
if (subAgent) body.subAgent = subAgent;
|
|
60
|
+
const { conversation } = await window.wsClient.rpc('conv.new', body);
|
|
61
|
+
window.ConversationState?.selectConversation(conversation.id, 'conversation_created', 1);
|
|
62
|
+
this.state.currentConversation = conversation;
|
|
63
|
+
this.lockAgentAndModel(agentId, model);
|
|
64
|
+
|
|
65
|
+
if (window.conversationManager) {
|
|
66
|
+
window.conversationManager.loadConversations();
|
|
67
|
+
window.conversationManager.select(conversation.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.streamToConversation(conversation.id, savedPrompt, agentId, model, subAgent);
|
|
71
|
+
this.clearDraft(conversation.id);
|
|
72
|
+
this._confirmOptimisticMessage(pendingId);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Execution error:', error);
|
|
76
|
+
if (!isStreaming) {
|
|
77
|
+
this._failOptimisticMessage(pendingId, savedPrompt, error.message);
|
|
78
|
+
}
|
|
79
|
+
this.enableControls();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_showOptimisticMessage(pendingId, content) {
|
|
85
|
+
const messagesEl = document.querySelector('.conversation-messages');
|
|
86
|
+
if (!messagesEl) return;
|
|
87
|
+
messagesEl.querySelectorAll('p.text-secondary').forEach(p => p.remove());
|
|
88
|
+
const div = document.createElement('div');
|
|
89
|
+
div.className = 'message message-user message-sending';
|
|
90
|
+
div.id = pendingId;
|
|
91
|
+
div.innerHTML = `<div class="message-role">User</div><div class="message-text">${this.escapeHtml(content)}</div><div class="message-timestamp" style="opacity:0.5">Sending...</div>`;
|
|
92
|
+
messagesEl.appendChild(div);
|
|
93
|
+
this.scrollToBottom(true);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_confirmOptimisticMessage(pendingId) {
|
|
98
|
+
const el = document.getElementById(pendingId);
|
|
99
|
+
if (!el) return;
|
|
100
|
+
el.classList.remove('message-sending');
|
|
101
|
+
const ts = el.querySelector('.message-timestamp');
|
|
102
|
+
if (ts) {
|
|
103
|
+
ts.style.opacity = '1';
|
|
104
|
+
ts.textContent = new Date().toLocaleString();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_failOptimisticMessage(pendingId, content, errorMsg) {
|
|
110
|
+
const el = document.getElementById(pendingId);
|
|
111
|
+
if (!el) return;
|
|
112
|
+
el.classList.remove('message-sending');
|
|
113
|
+
el.classList.add('message-send-failed');
|
|
114
|
+
const ts = el.querySelector('.message-timestamp');
|
|
115
|
+
if (ts) {
|
|
116
|
+
ts.style.opacity = '1';
|
|
117
|
+
ts.innerHTML = `<span style="color:var(--color-error)">Failed: ${this.escapeHtml(errorMsg)}</span>`;
|
|
118
|
+
}
|
|
119
|
+
if (this.ui.messageInput) {
|
|
120
|
+
this.ui.messageInput.value = content;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_subscribeToConversationUpdates() {
|
|
126
|
+
if (!this.state.conversations || this.state.conversations.length === 0) return;
|
|
127
|
+
for (const conv of this.state.conversations) {
|
|
128
|
+
this.wsManager.subscribeToConversation(conv.id);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
_flushBgCache(conversationId, sessionId) {
|
|
134
|
+
const entry = this._bgCache.get(conversationId);
|
|
135
|
+
if (!entry || entry.items.length === 0) return;
|
|
136
|
+
if (entry.sessionId !== sessionId) { this._bgCache.delete(conversationId); return; }
|
|
137
|
+
|
|
138
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
139
|
+
if (!streamingEl) return;
|
|
140
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
141
|
+
if (!blocksEl) return;
|
|
142
|
+
|
|
143
|
+
const seenSeqs = this._renderedSeqs[sessionId] || (this._renderedSeqs[sessionId] = new Set());
|
|
144
|
+
for (const item of entry.items) {
|
|
145
|
+
if (item.seq !== undefined && seenSeqs.has(item.seq)) continue;
|
|
146
|
+
try {
|
|
147
|
+
const block = (typeof msgpackr !== 'undefined' && item.packed instanceof Uint8Array)
|
|
148
|
+
? msgpackr.unpack(item.packed) : item.packed;
|
|
149
|
+
const el = this.renderer.renderBlock(block, { sessionId }, blocksEl);
|
|
150
|
+
if (el) {
|
|
151
|
+
if (item.seq !== undefined) seenSeqs.add(item.seq);
|
|
152
|
+
blocksEl.appendChild(el);
|
|
153
|
+
}
|
|
154
|
+
} catch (_) {}
|
|
155
|
+
}
|
|
156
|
+
this._bgCache.delete(conversationId);
|
|
157
|
+
this.scrollToBottom();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
async _recoverMissedChunks() {
|
|
3
|
+
if (!this.state.currentSession?.id) return;
|
|
4
|
+
|
|
5
|
+
const sessionId = this.state.currentSession.id;
|
|
6
|
+
const lastSeq = this.wsManager.getLastSeq(sessionId);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const { chunks: rawChunks } = await window.wsClient.rpc('sess.chunks', { id: sessionId, sinceSeq: lastSeq });
|
|
10
|
+
if (!rawChunks || rawChunks.length === 0) return;
|
|
11
|
+
|
|
12
|
+
const chunks = rawChunks.map(c => ({
|
|
13
|
+
...c,
|
|
14
|
+
block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
|
|
15
|
+
})).filter(c => c.block && c.block.type);
|
|
16
|
+
|
|
17
|
+
const seenSeqs = (this._renderedSeqs || {})[sessionId];
|
|
18
|
+
const dedupedChunks = chunks.filter(c => !seenSeqs || !seenSeqs.has(c.sequence));
|
|
19
|
+
|
|
20
|
+
if (dedupedChunks.length > 0) {
|
|
21
|
+
for (const chunk of dedupedChunks) {
|
|
22
|
+
try { this.renderChunk(chunk); } catch (chunkErr) { console.warn('Skipping bad chunk:', chunkErr.message); }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.warn('Chunk recovery failed:', e.message);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_dedupedFetch(key, fetchFn) {
|
|
32
|
+
if (this._inflightRequests.has(key)) {
|
|
33
|
+
return this._inflightRequests.get(key);
|
|
34
|
+
}
|
|
35
|
+
const promise = fetchFn().finally(() => {
|
|
36
|
+
this._inflightRequests.delete(key);
|
|
37
|
+
});
|
|
38
|
+
this._inflightRequests.set(key, promise);
|
|
39
|
+
return promise;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_insertPlaceholder(sessionId) {
|
|
44
|
+
this._removePlaceholder();
|
|
45
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
46
|
+
if (!streamingEl) return;
|
|
47
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
48
|
+
if (!blocksEl) return;
|
|
49
|
+
const ph = document.createElement('div');
|
|
50
|
+
ph.className = 'chunk-placeholder';
|
|
51
|
+
ph.id = 'chunk-placeholder-active';
|
|
52
|
+
blocksEl.appendChild(ph);
|
|
53
|
+
this._placeholderAutoRemove = setTimeout(() => this._removePlaceholder(), 500);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_removePlaceholder() {
|
|
58
|
+
if (this._placeholderAutoRemove) { clearTimeout(this._placeholderAutoRemove); this._placeholderAutoRemove = null; }
|
|
59
|
+
const ph = document.getElementById('chunk-placeholder-active');
|
|
60
|
+
if (ph && ph.parentNode) ph.remove();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_trackBlockHeight(block, element) {
|
|
65
|
+
if (!element || !block?.type) return;
|
|
66
|
+
const h = element.offsetHeight;
|
|
67
|
+
if (h <= 0) return;
|
|
68
|
+
if (!this._blockHeightAvg) this._blockHeightAvg = {};
|
|
69
|
+
const t = block.type;
|
|
70
|
+
if (!this._blockHeightAvg[t]) this._blockHeightAvg[t] = { sum: 0, count: 0 };
|
|
71
|
+
this._blockHeightAvg[t].sum += h;
|
|
72
|
+
this._blockHeightAvg[t].count++;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_estimatedBlockHeight(type) {
|
|
77
|
+
const defaults = { text: 40, tool_use: 60, tool_result: 40 };
|
|
78
|
+
if (this._blockHeightAvg?.[type]?.count >= 3) {
|
|
79
|
+
return this._blockHeightAvg[type].sum / this._blockHeightAvg[type].count;
|
|
80
|
+
}
|
|
81
|
+
return defaults[type] || 40;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_startThinkingCountdown() {
|
|
86
|
+
this._clearThinkingCountdown();
|
|
87
|
+
if (!this._lastSendTime) return;
|
|
88
|
+
const predicted = this.wsManager?.latency?.predicted || 0;
|
|
89
|
+
const estimatedWait = predicted + this._serverProcessingEstimate;
|
|
90
|
+
if (estimatedWait < 1000) return;
|
|
91
|
+
let remaining = Math.ceil(estimatedWait / 1000);
|
|
92
|
+
const update = () => {
|
|
93
|
+
const indicator = document.querySelector('.streaming-indicator');
|
|
94
|
+
if (!indicator) return;
|
|
95
|
+
if (remaining > 0) {
|
|
96
|
+
indicator.textContent = `Thinking... (~${remaining}s)`;
|
|
97
|
+
remaining--;
|
|
98
|
+
this._countdownTimer = setTimeout(update, 1000);
|
|
99
|
+
} else {
|
|
100
|
+
indicator.textContent = 'Thinking... (taking longer than expected)';
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
this._countdownTimer = setTimeout(update, 100);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_clearThinkingCountdown() {
|
|
108
|
+
if (this._countdownTimer) { clearTimeout(this._countdownTimer); this._countdownTimer = null; }
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_setupDebugHooks() {
|
|
113
|
+
if (typeof window === 'undefined') return;
|
|
114
|
+
const self = this;
|
|
115
|
+
window.__debug = {
|
|
116
|
+
getState: () => ({
|
|
117
|
+
latencyEma: self.wsManager?._latencyEma || null,
|
|
118
|
+
serverProcessingEstimate: self._serverProcessingEstimate,
|
|
119
|
+
latencyTrend: self.wsManager?.latency?.trend || null
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
getSyncState: () => ({
|
|
123
|
+
currentConversation: self.state.currentConversation,
|
|
124
|
+
isStreaming: self._convIsStreaming(self.state.currentConversation?.id),
|
|
125
|
+
streamingConversations: Array.from(self.state.streamingConversations),
|
|
126
|
+
convMachineStates: typeof convMachineAPI !== 'undefined' ? Object.fromEntries([...window.__convMachines].map(([k, a]) => [k, a.getSnapshot().value])) : {},
|
|
127
|
+
toolInstallMachineStates: typeof window.__toolInstallMachines !== 'undefined' ? Object.fromEntries([...window.__toolInstallMachines].map(([k, a]) => [k, a.getSnapshot().value])) : {},
|
|
128
|
+
voiceMachineState: typeof window.__voiceMachine !== 'undefined' ? window.__voiceMachine.getSnapshot().value : 'unknown',
|
|
129
|
+
convListMachineState: typeof window.__convListMachine !== 'undefined' ? window.__convListMachine.getSnapshot().value : 'unknown',
|
|
130
|
+
promptMachineState: typeof window.__promptMachine !== 'undefined' ? window.__promptMachine.getSnapshot().value : 'unknown',
|
|
131
|
+
wsConnectionState: self.wsManager?._wsActor?.getSnapshot().value || 'unknown',
|
|
132
|
+
rendererEventQueueLength: self.renderer?.eventQueue?.length || 0,
|
|
133
|
+
rendererEventHistoryLength: self.renderer?.eventHistory?.length || 0,
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
getMessageState: () => {
|
|
137
|
+
const output = document.querySelector('.conversation-messages');
|
|
138
|
+
if (!output) return { error: 'No conversation output found' };
|
|
139
|
+
const messageCount = output.querySelectorAll('.message').length;
|
|
140
|
+
const queueItems = output.querySelectorAll('.queue-item').length;
|
|
141
|
+
const pendingMessages = output.querySelectorAll('.message-sending').length;
|
|
142
|
+
return { messageCount, queueItems, pendingMessages };
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
showLoadingSpinner() {
|
|
149
|
+
document.documentElement.style.pointerEvents = 'auto';
|
|
150
|
+
const indicator = document.querySelector('[data-model-dl-indicator]');
|
|
151
|
+
if (indicator && !indicator.classList.contains('visible')) {
|
|
152
|
+
indicator.classList.add('visible');
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
hideLoadingSpinner() {
|
|
158
|
+
const indicator = document.querySelector('[data-model-dl-indicator]');
|
|
159
|
+
if (indicator && indicator.classList.contains('visible')) {
|
|
160
|
+
indicator.classList.remove('visible');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
_makeLoadRequest(conversationId) {
|
|
3
|
+
const requestId = ++this._currentRequestId;
|
|
4
|
+
const abortController = new AbortController();
|
|
5
|
+
|
|
6
|
+
if (this._loadInProgress[conversationId]) {
|
|
7
|
+
const prevReq = this._loadInProgress[conversationId];
|
|
8
|
+
try {
|
|
9
|
+
prevReq.abortController.abort();
|
|
10
|
+
} catch (e) {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this._loadInProgress[conversationId] = {
|
|
14
|
+
requestId,
|
|
15
|
+
abortController,
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
prevConversationId: this.state.currentConversation?.id
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return { requestId, abortController: abortController.signal };
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_verifyRequestId(conversationId, requestId) {
|
|
25
|
+
const current = this._loadInProgress[conversationId];
|
|
26
|
+
if (!current) return false;
|
|
27
|
+
if (current.requestId !== requestId) return false;
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_completeLoadRequest(conversationId, requestId) {
|
|
33
|
+
const req = this._loadInProgress[conversationId];
|
|
34
|
+
if (req && req.requestId === requestId) {
|
|
35
|
+
delete this._loadInProgress[conversationId];
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async _loadConvRender(conversationId, convSignal, prevConversationId, availableFallback, fullData) {
|
|
41
|
+
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
|
|
42
|
+
|
|
43
|
+
window.ConversationState?.selectConversation(conversationId, 'server_load', 1);
|
|
44
|
+
this.state.currentConversation = conversation;
|
|
45
|
+
window.dispatchEvent(new CustomEvent('conversation-changed', { detail: { conversationId, conversation } }));
|
|
46
|
+
const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this._convIsStreaming(conversationId);
|
|
47
|
+
this.applyAgentAndModelSelection(conversation, hasActivity);
|
|
48
|
+
|
|
49
|
+
const queuePromise = window.wsClient.rpc('q.ls', { id: conversationId }).catch(() => ({ queue: [] }));
|
|
50
|
+
const chunks = (rawChunks || []).map(chunk => ({
|
|
51
|
+
...chunk,
|
|
52
|
+
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const { queue: queueResult } = await queuePromise;
|
|
56
|
+
const queuedMessageIds = new Set((queueResult || []).map(q => q.messageId));
|
|
57
|
+
const userMessages = (allMessages || []).filter(m => m.role === 'user' && !queuedMessageIds.has(m.id));
|
|
58
|
+
const hasMoreChunks = totalChunks && chunks.length < totalChunks;
|
|
59
|
+
|
|
60
|
+
const clientKnowsStreaming = this._convIsStreaming(conversationId);
|
|
61
|
+
const shouldResumeStreaming = latestSession &&
|
|
62
|
+
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
63
|
+
|
|
64
|
+
if (shouldResumeStreaming) {
|
|
65
|
+
this._setConvStreaming(conversationId, true, latestSession?.id, null);
|
|
66
|
+
window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_start', conversationId, sessionId: latestSession?.id } }));
|
|
67
|
+
} else {
|
|
68
|
+
this._setConvStreaming(conversationId, false);
|
|
69
|
+
window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_complete', conversationId } }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.ui.messageInput) {
|
|
73
|
+
this.ui.messageInput.disabled = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const outputEl = document.getElementById('output');
|
|
77
|
+
if (outputEl) {
|
|
78
|
+
const wdInfo = conversation.workingDirectory ? `${this.escapeHtml(conversation.workingDirectory)}` : '';
|
|
79
|
+
const timestamp = new Date(conversation.created_at).toLocaleDateString();
|
|
80
|
+
const metaParts = [timestamp];
|
|
81
|
+
if (wdInfo) metaParts.push(wdInfo);
|
|
82
|
+
outputEl.innerHTML = `
|
|
83
|
+
<div class="conversation-header">
|
|
84
|
+
<h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
|
|
85
|
+
<p class="text-secondary">${metaParts.join(' - ')}</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="conversation-messages"></div>
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const messagesEl = outputEl.querySelector('.conversation-messages');
|
|
91
|
+
|
|
92
|
+
if (hasMoreChunks) {
|
|
93
|
+
const loadMoreBtn = document.createElement('button');
|
|
94
|
+
loadMoreBtn.className = 'btn btn-secondary';
|
|
95
|
+
loadMoreBtn.style.cssText = 'width:100%;margin-bottom:1rem;padding:0.5rem;font-size:0.8rem;';
|
|
96
|
+
loadMoreBtn.textContent = `Load earlier messages (${totalChunks - chunks.length} more chunks)`;
|
|
97
|
+
loadMoreBtn.addEventListener('click', async () => {
|
|
98
|
+
loadMoreBtn.disabled = true;
|
|
99
|
+
loadMoreBtn.textContent = 'Loading...';
|
|
100
|
+
try {
|
|
101
|
+
try {
|
|
102
|
+
await window.wsClient.rpc('conv.full', { id: conversationId, allChunks: true });
|
|
103
|
+
} catch (wsErr) {}
|
|
104
|
+
this.invalidateCache(conversationId);
|
|
105
|
+
await this.loadConversationMessages(conversationId);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
loadMoreBtn.textContent = 'Failed to load. Try again.';
|
|
108
|
+
loadMoreBtn.disabled = false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
messagesEl.appendChild(loadMoreBtn);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (chunks.length > 0) {
|
|
115
|
+
const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
|
|
116
|
+
performance.mark(`conv-render-start:${conversationId}`);
|
|
117
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
118
|
+
if (!convSignal.aborted) this._renderConversationContent(messagesEl, chunks, userMessages, activeSessionId);
|
|
119
|
+
performance.mark(`conv-render-complete:${conversationId}`);
|
|
120
|
+
performance.measure(`conv-render:${conversationId}`, `conv-render-start:${conversationId}`, `conv-render-complete:${conversationId}`);
|
|
121
|
+
performance.measure(`conv-data-fetch:${conversationId}`, `conv-load-start:${conversationId}`, `conv-data-received:${conversationId}`);
|
|
122
|
+
} else {
|
|
123
|
+
if (!convSignal.aborted) messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!convSignal.aborted && shouldResumeStreaming && latestSession && chunks.length === 0) {
|
|
127
|
+
const streamDiv = document.createElement('div');
|
|
128
|
+
streamDiv.id = `streaming-${latestSession.id}`;
|
|
129
|
+
streamDiv.className = 'streaming-message';
|
|
130
|
+
const indicatorDiv = document.createElement('div');
|
|
131
|
+
indicatorDiv.className = 'streaming-indicator';
|
|
132
|
+
indicatorDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
|
|
133
|
+
indicatorDiv.innerHTML = `
|
|
134
|
+
<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>
|
|
135
|
+
<span class="streaming-indicator-label">Agent is starting...</span>
|
|
136
|
+
`;
|
|
137
|
+
streamDiv.appendChild(indicatorDiv);
|
|
138
|
+
messagesEl.appendChild(streamDiv);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (shouldResumeStreaming && latestSession) {
|
|
142
|
+
this._setConvStreaming(conversationId, true, latestSession.id, null);
|
|
143
|
+
this.state.currentSession = {
|
|
144
|
+
id: latestSession.id,
|
|
145
|
+
conversationId: conversationId,
|
|
146
|
+
agentId: conversation.agentType || null,
|
|
147
|
+
startTime: latestSession.created_at
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (this.wsManager.isConnected) {
|
|
151
|
+
this.wsManager.subscribeToSession(latestSession.id);
|
|
152
|
+
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.updateUrlForConversation(conversationId, latestSession.id);
|
|
156
|
+
|
|
157
|
+
this._flushBgCache(conversationId, latestSession.id);
|
|
158
|
+
|
|
159
|
+
this.syncPromptState(conversationId);
|
|
160
|
+
} else {
|
|
161
|
+
this.syncPromptState(conversationId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.ui.sendButton) {
|
|
165
|
+
this.ui.sendButton.disabled = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.restoreScrollPosition(conversationId);
|
|
169
|
+
this.setupScrollUpDetection(conversationId);
|
|
170
|
+
|
|
171
|
+
this.fetchAndRenderQueue(conversationId);
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|