agentgui 1.0.828 → 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/lib/jsonl-watcher.js +2 -2
- package/package.json +2 -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,116 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
async handleStreamingResumed(data) {
|
|
3
|
+
this._dbg('Streaming resumed:', data);
|
|
4
|
+
const conv = this.state.currentConversation || { id: data.conversationId };
|
|
5
|
+
await this.handleStreamingStart({
|
|
6
|
+
type: 'streaming_start',
|
|
7
|
+
sessionId: data.sessionId,
|
|
8
|
+
conversationId: data.conversationId,
|
|
9
|
+
agentId: conv.agentType || conv.agentId || 'claude-code',
|
|
10
|
+
resumed: true,
|
|
11
|
+
timestamp: data.timestamp
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
handleStreamingProgress(data) {
|
|
17
|
+
try { return this._handleStreamingProgressInner(data); } catch (e) { console.error('[render-error] streaming progress:', e); }
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
_handleStreamingProgressInner(data) {
|
|
21
|
+
if (!data.block || !data.sessionId) return;
|
|
22
|
+
|
|
23
|
+
const seen = this._renderedSeqs[data.sessionId] || (this._renderedSeqs[data.sessionId] = new Set());
|
|
24
|
+
if (data.seq !== undefined) {
|
|
25
|
+
if (seen.has(data.seq)) return;
|
|
26
|
+
seen.add(data.seq);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const block = data.block;
|
|
30
|
+
|
|
31
|
+
const convId = data.conversationId;
|
|
32
|
+
if (convId) {
|
|
33
|
+
let entry = this._bgCache.get(convId);
|
|
34
|
+
if (!entry) {
|
|
35
|
+
if (this._bgCache.size >= this.BG_CACHE_MAX) {
|
|
36
|
+
const oldestKey = this._bgCache.keys().next().value;
|
|
37
|
+
this._bgCache.delete(oldestKey);
|
|
38
|
+
}
|
|
39
|
+
entry = { items: [], seqSet: new Set(), sessionId: data.sessionId };
|
|
40
|
+
this._bgCache.set(convId, entry);
|
|
41
|
+
}
|
|
42
|
+
if (data.seq === undefined || !entry.seqSet.has(data.seq)) {
|
|
43
|
+
if (data.seq !== undefined) entry.seqSet.add(data.seq);
|
|
44
|
+
entry.sessionId = data.sessionId;
|
|
45
|
+
try {
|
|
46
|
+
const packed = typeof msgpackr !== 'undefined' ? msgpackr.pack(block) : block;
|
|
47
|
+
entry.items.push({ seq: data.seq, packed });
|
|
48
|
+
} catch (_) { entry.items.push({ seq: data.seq, packed: block }); }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.state.currentSession?.id !== data.sessionId) return;
|
|
53
|
+
|
|
54
|
+
const streamingEl = document.getElementById(`streaming-${data.sessionId}`);
|
|
55
|
+
if (!streamingEl) return;
|
|
56
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
57
|
+
if (!blocksEl) return;
|
|
58
|
+
|
|
59
|
+
if (block.type === 'tool_result') {
|
|
60
|
+
const tid = block.tool_use_id;
|
|
61
|
+
const toolUseEl = (tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`))
|
|
62
|
+
|| (blocksEl.lastElementChild?.classList?.contains('block-tool-use') ? blocksEl.lastElementChild : null);
|
|
63
|
+
if (toolUseEl) {
|
|
64
|
+
this.renderer.mergeResultIntoToolUse(toolUseEl, block);
|
|
65
|
+
this.scrollToBottom();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (block.type === 'tool_status') {
|
|
71
|
+
const tid = block.tool_use_id;
|
|
72
|
+
const toolUseEl = tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`);
|
|
73
|
+
if (toolUseEl) {
|
|
74
|
+
const summary = toolUseEl.querySelector(':scope > summary');
|
|
75
|
+
if (summary) {
|
|
76
|
+
let badge = summary.querySelector('.folded-tool-status-live');
|
|
77
|
+
if (!badge) { badge = document.createElement('span'); badge.className = 'folded-tool-status-live'; summary.appendChild(badge); }
|
|
78
|
+
const isRunning = block.status === 'in_progress';
|
|
79
|
+
badge.style.cssText = 'margin-left:auto;font-size:0.65rem;opacity:0.7;display:inline-flex;align-items:center;gap:0.25rem';
|
|
80
|
+
badge.innerHTML = (isRunning ? '<span class="animate-spin" style="display:inline-block;width:0.6rem;height:0.6rem;border:1.5px solid var(--color-border);border-top-color:var(--color-info);border-radius:50%"></span>' : '')
|
|
81
|
+
+ '<span>' + (isRunning ? 'Running' : (block.status || '')) + '</span>';
|
|
82
|
+
}
|
|
83
|
+
this.scrollToBottom();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (block.type === 'hook_progress') {
|
|
89
|
+
const hookEvent = (block.hookEvent || '').split(':')[0];
|
|
90
|
+
if (hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') {
|
|
91
|
+
const lastTool = blocksEl.querySelector('.block-tool-use:last-of-type') || blocksEl.lastElementChild;
|
|
92
|
+
if (lastTool?.classList?.contains('block-tool-use')) {
|
|
93
|
+
const summary = lastTool.querySelector(':scope > summary');
|
|
94
|
+
if (summary) {
|
|
95
|
+
let hookBadge = summary.querySelector('.folded-tool-hook');
|
|
96
|
+
if (!hookBadge) { hookBadge = document.createElement('span'); hookBadge.className = 'folded-tool-hook'; hookBadge.style.cssText = 'margin-left:0.375rem;font-size:0.6rem;opacity:0.4;font-weight:400'; summary.appendChild(hookBadge); }
|
|
97
|
+
hookBadge.textContent = block.hookEvent?.split(':').pop() || hookEvent;
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (block.type === 'result' && block.total_cost_usd) {
|
|
106
|
+
this._sessionCost = (this._sessionCost || 0) + block.total_cost_usd;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const el = this.renderer.renderBlock(block, data, blocksEl);
|
|
110
|
+
if (el) {
|
|
111
|
+
blocksEl.appendChild(el);
|
|
112
|
+
this.scrollToBottom();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
renderBlockContent(block) {
|
|
3
|
+
if (block.type === 'text' && block.text) {
|
|
4
|
+
const text = block.text;
|
|
5
|
+
if (this.isHtmlContent(text)) {
|
|
6
|
+
return `<div class="html-content">${this.sanitizeHtml(text)}</div>`;
|
|
7
|
+
}
|
|
8
|
+
const parts = this.parseMarkdownCodeBlocks(text);
|
|
9
|
+
if (parts.length === 1 && parts[0].type === 'text') {
|
|
10
|
+
return this.escapeHtml(text);
|
|
11
|
+
}
|
|
12
|
+
return parts.map(part => {
|
|
13
|
+
if (part.type === 'html') {
|
|
14
|
+
return `<div class="html-content">${this.sanitizeHtml(part.content)}</div>`;
|
|
15
|
+
} else if (part.type === 'code') {
|
|
16
|
+
return this.renderCodeBlock(part.language, part.code);
|
|
17
|
+
}
|
|
18
|
+
return this.escapeHtml(part.content);
|
|
19
|
+
}).join('');
|
|
20
|
+
}
|
|
21
|
+
const fieldsHtml = Object.entries(block)
|
|
22
|
+
.filter(([key]) => key !== 'type')
|
|
23
|
+
.map(([key, value]) => {
|
|
24
|
+
let displayValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
25
|
+
if (displayValue.length > 100) displayValue = displayValue.substring(0, 100) + '...';
|
|
26
|
+
return `<div style="font-size:0.75rem;margin-bottom:0.25rem"><span style="font-weight:600">${this.escapeHtml(key)}:</span> <code>${this.escapeHtml(displayValue)}</code></div>`;
|
|
27
|
+
}).join('');
|
|
28
|
+
return `<div style="padding:0.5rem;background:var(--color-bg-secondary);border-radius:0.375rem;border:1px solid var(--color-border)"><div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;margin-bottom:0.25rem">${this.escapeHtml(block.type)}</div>${fieldsHtml}</div>`;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
scrollToBottom(force = false) {
|
|
33
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
34
|
+
if (!scrollContainer) return;
|
|
35
|
+
const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
|
|
36
|
+
|
|
37
|
+
if (this._userScrolledUp && !force) {
|
|
38
|
+
this._unseenCount = (this._unseenCount || 0) + 1;
|
|
39
|
+
this._showNewContentPill();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!force && distFromBottom > 150) {
|
|
44
|
+
this._unseenCount = (this._unseenCount || 0) + 1;
|
|
45
|
+
this._showNewContentPill();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
50
|
+
this._removeNewContentPill();
|
|
51
|
+
this._scrollAnimating = false;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_showNewContentPill() {
|
|
56
|
+
let pill = document.getElementById('new-content-pill');
|
|
57
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
58
|
+
if (!scrollContainer) return;
|
|
59
|
+
if (!pill) {
|
|
60
|
+
pill = document.createElement('button');
|
|
61
|
+
pill.id = 'new-content-pill';
|
|
62
|
+
pill.className = 'new-content-pill';
|
|
63
|
+
pill.addEventListener('click', () => {
|
|
64
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
65
|
+
this._removeNewContentPill();
|
|
66
|
+
});
|
|
67
|
+
scrollContainer.appendChild(pill);
|
|
68
|
+
}
|
|
69
|
+
pill.textContent = (this._unseenCount || 1) + ' new';
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_removeNewContentPill() {
|
|
74
|
+
this._unseenCount = 0;
|
|
75
|
+
const pill = document.getElementById('new-content-pill');
|
|
76
|
+
if (pill) pill.remove();
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
handleStreamingError(data) {
|
|
81
|
+
console.error('Streaming error:', data);
|
|
82
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
83
|
+
this._clearThinkingCountdown();
|
|
84
|
+
if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
|
|
85
|
+
|
|
86
|
+
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
87
|
+
if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
|
|
88
|
+
if (this.ui.sendButton) this.ui.sendButton.style.display = '';
|
|
89
|
+
|
|
90
|
+
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
91
|
+
|
|
92
|
+
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
93
|
+
this._dbg('Streaming error for non-active conversation:', conversationId);
|
|
94
|
+
this._setConvStreaming(conversationId, false);
|
|
95
|
+
this.updateBusyPromptArea(conversationId);
|
|
96
|
+
this.emit('streaming:error', data);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._setConvStreaming(conversationId, false);
|
|
101
|
+
this.updateBusyPromptArea(conversationId);
|
|
102
|
+
|
|
103
|
+
const queueEl = document.querySelector('.queue-indicator');
|
|
104
|
+
if (queueEl) queueEl.remove();
|
|
105
|
+
|
|
106
|
+
if (data.isPrematureEnd) {
|
|
107
|
+
this.renderer.queueEvent({
|
|
108
|
+
type: 'streaming_error',
|
|
109
|
+
isPrematureEnd: true,
|
|
110
|
+
exitCode: data.exitCode,
|
|
111
|
+
error: data.error,
|
|
112
|
+
stderrText: data.stderrText,
|
|
113
|
+
sessionId: data.sessionId,
|
|
114
|
+
conversationId: data.conversationId,
|
|
115
|
+
timestamp: data.timestamp || Date.now()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
120
|
+
|
|
121
|
+
const outputEl2 = document.getElementById('output');
|
|
122
|
+
if (outputEl2) {
|
|
123
|
+
outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => {
|
|
124
|
+
ind.innerHTML = `<span style="color:var(--color-error);">Error: ${this.escapeHtml(data.error || 'Unknown error')}</span>`;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
129
|
+
if (streamingEl) {
|
|
130
|
+
streamingEl.classList.remove('streaming-message');
|
|
131
|
+
const indicator = streamingEl.querySelector('.streaming-indicator');
|
|
132
|
+
if (indicator) {
|
|
133
|
+
indicator.innerHTML = `<span style="color:var(--color-error);">Error: ${this.escapeHtml(data.error || 'Unknown error')}</span>`;
|
|
134
|
+
}
|
|
135
|
+
streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
|
|
136
|
+
} else {
|
|
137
|
+
const outputEl3 = document.getElementById('output');
|
|
138
|
+
const messagesEl3 = outputEl3 && outputEl3.querySelector('.conversation-messages');
|
|
139
|
+
if (messagesEl3 && data.error) {
|
|
140
|
+
const errDiv = document.createElement('div');
|
|
141
|
+
errDiv.className = 'message';
|
|
142
|
+
errDiv.style = 'padding:0.75rem;border:1px solid var(--color-error, #e53e3e);border-radius:4px;margin:0.5rem 0;';
|
|
143
|
+
errDiv.innerHTML = `<span style="color:var(--color-error, #e53e3e);">Error: ${this.escapeHtml(data.error)}</span>`;
|
|
144
|
+
messagesEl3.appendChild(errDiv);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.unlockAgentAndModel();
|
|
149
|
+
this.enableControls();
|
|
150
|
+
this.emit('streaming:error', data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
handleStreamingComplete(data) {
|
|
3
|
+
this._dbg('Streaming completed:', data);
|
|
4
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
5
|
+
this._clearThinkingCountdown();
|
|
6
|
+
if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
|
|
7
|
+
|
|
8
|
+
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
9
|
+
if (conversationId) this.invalidateCache(conversationId);
|
|
10
|
+
|
|
11
|
+
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
12
|
+
this._dbg('Streaming completed for non-active conversation:', conversationId);
|
|
13
|
+
this._setConvStreaming(conversationId, false);
|
|
14
|
+
this._dbg('[SYNC] streaming_complete - non-active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size });
|
|
15
|
+
this.updateBusyPromptArea(conversationId);
|
|
16
|
+
this.emit('streaming:complete', data);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this._setConvStreaming(conversationId, false);
|
|
21
|
+
this._dbg('[SYNC] streaming_complete - active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size, interrupted: data.interrupted });
|
|
22
|
+
this.updateBusyPromptArea(conversationId);
|
|
23
|
+
|
|
24
|
+
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
25
|
+
|
|
26
|
+
if (sessionId && this.wsManager) {
|
|
27
|
+
try {
|
|
28
|
+
this.wsManager.unsubscribeFromSession(sessionId);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const queueEl = document.querySelector('.queue-indicator');
|
|
34
|
+
if (queueEl) queueEl.remove();
|
|
35
|
+
|
|
36
|
+
const outputEl2 = document.getElementById('output');
|
|
37
|
+
if (outputEl2) {
|
|
38
|
+
outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
|
|
39
|
+
outputEl2.querySelectorAll('.event-streaming-start, .event-streaming-complete').forEach(block => block.remove());
|
|
40
|
+
}
|
|
41
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
42
|
+
if (streamingEl) {
|
|
43
|
+
streamingEl.classList.remove('streaming-message');
|
|
44
|
+
const prevTextEl = streamingEl.querySelector('.streaming-text-current');
|
|
45
|
+
if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
|
|
46
|
+
|
|
47
|
+
streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
|
|
48
|
+
|
|
49
|
+
const ts = document.createElement('div');
|
|
50
|
+
ts.className = 'message-timestamp';
|
|
51
|
+
ts.textContent = new Date().toLocaleString();
|
|
52
|
+
streamingEl.appendChild(ts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (conversationId) {
|
|
56
|
+
this.saveScrollPosition(conversationId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._recoverMissedChunks().catch(err => {
|
|
60
|
+
console.warn('Chunk recovery failed:', err.message);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.enableControls();
|
|
64
|
+
this.emit('streaming:complete', data);
|
|
65
|
+
|
|
66
|
+
this._promptPushIfWeOwnRemote();
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async _promptPushIfWeOwnRemote() {
|
|
71
|
+
try {
|
|
72
|
+
const { ownsRemote, hasChanges, hasUnpushed, remoteUrl } = await window.wsClient.rpc('git.check');
|
|
73
|
+
if (ownsRemote && (hasChanges || hasUnpushed)) {
|
|
74
|
+
const conv = this.state.currentConversation;
|
|
75
|
+
if (conv) {
|
|
76
|
+
this.streamToConversation(conv.id, 'Push the changes to the remote repository.', conv.agentId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn('Auto-push check failed:', e);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
handleConversationCreated(data) {
|
|
86
|
+
if (data.conversation) {
|
|
87
|
+
if (this.state.conversations.some(c => c.id === data.conversation.id)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.state.conversations.push(data.conversation);
|
|
91
|
+
this.emit('conversation:created', data.conversation);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
handleMessageCreated(data) {
|
|
97
|
+
if (data.conversationId !== this.state.currentConversation?.id || !data.message) {
|
|
98
|
+
this.emit('message:created', data);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._dbg('[SYNC] message_created:', { msgId: data.message.id, role: data.message.role, convId: data.conversationId });
|
|
103
|
+
|
|
104
|
+
if (data.message.role === 'user' && this.state.currentConversation) {
|
|
105
|
+
this.state.currentConversation.messageCount = (this.state.currentConversation.messageCount || 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (data.message.role === 'assistant' && this._convIsStreaming(data.conversationId)) {
|
|
109
|
+
this.emit('message:created', data);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const outputEl = document.querySelector('.conversation-messages');
|
|
114
|
+
if (!outputEl) {
|
|
115
|
+
this.emit('message:created', data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (data.message.role === 'user') {
|
|
120
|
+
const pending = outputEl.querySelector('.message-sending');
|
|
121
|
+
if (pending) {
|
|
122
|
+
pending.id = '';
|
|
123
|
+
pending.setAttribute('data-msg-id', data.message.id);
|
|
124
|
+
pending.classList.remove('message-sending');
|
|
125
|
+
const ts = pending.querySelector('.message-timestamp');
|
|
126
|
+
if (ts) {
|
|
127
|
+
ts.style.opacity = '1';
|
|
128
|
+
ts.textContent = new Date(data.message.created_at).toLocaleString();
|
|
129
|
+
}
|
|
130
|
+
this.emit('message:created', data);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const pendingById = outputEl.querySelector('[id^="pending-"]');
|
|
134
|
+
if (pendingById) {
|
|
135
|
+
pendingById.id = '';
|
|
136
|
+
pendingById.setAttribute('data-msg-id', data.message.id);
|
|
137
|
+
const ts = pendingById.querySelector('.message-timestamp');
|
|
138
|
+
if (ts) {
|
|
139
|
+
ts.style.opacity = '1';
|
|
140
|
+
ts.textContent = new Date(data.message.created_at).toLocaleString();
|
|
141
|
+
}
|
|
142
|
+
this.emit('message:created', data);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const existingMsg = outputEl.querySelector(`[data-msg-id="${data.message.id}"]`);
|
|
146
|
+
if (existingMsg) {
|
|
147
|
+
this.emit('message:created', data);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
outputEl.querySelectorAll('p.text-secondary').forEach(p => p.remove());
|
|
153
|
+
const safeRole = this.escapeHtml(data.message.role || 'user');
|
|
154
|
+
const safeId = this.escapeHtml(data.message.id || '');
|
|
155
|
+
const messageHtml = `
|
|
156
|
+
<div class="message message-${safeRole}" data-msg-id="${safeId}">
|
|
157
|
+
<div class="message-role">${safeRole.charAt(0).toUpperCase() + safeRole.slice(1)}</div>
|
|
158
|
+
${this.renderMessageContent(data.message.content)}
|
|
159
|
+
<div class="message-timestamp">${new Date(data.message.created_at).toLocaleString()}</div>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
outputEl.insertAdjacentHTML('beforeend', messageHtml);
|
|
163
|
+
this.scrollToBottom();
|
|
164
|
+
this.emit('message:created', data);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
handleConversationUpdated(data) {
|
|
169
|
+
if (data.conversation && data.conversation.id === this.state.currentConversation?.id) {
|
|
170
|
+
this.state.currentConversation = data.conversation;
|
|
171
|
+
}
|
|
172
|
+
this.emit('conversation:updated', data);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
handleQueueStatus(data) {
|
|
177
|
+
if (typeof convMachineAPI !== 'undefined') convMachineAPI.send(data.conversationId, { type: 'QUEUE_UPDATE', queueLength: data.queueLength || 0 });
|
|
178
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
179
|
+
this.fetchAndRenderQueue(data.conversationId);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
handleQueueUpdated(data) {
|
|
184
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
185
|
+
this.fetchAndRenderQueue(data.conversationId);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
handleQueueItemDequeued(data) {
|
|
190
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
191
|
+
this.fetchAndRenderQueue(data.conversationId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Object.assign(AgentGUIClient.prototype, {
|
|
2
|
+
_setupUIButtonEvents() {
|
|
3
|
+
this.ui.stopButton = document.getElementById('stopBtn');
|
|
4
|
+
this.ui.injectButton = document.getElementById('injectBtn');
|
|
5
|
+
this.ui.queueButton = document.getElementById('queueBtn');
|
|
6
|
+
|
|
7
|
+
if (this.ui.stopButton) {
|
|
8
|
+
this.ui.stopButton.addEventListener('click', async () => {
|
|
9
|
+
if (!this.state.currentConversation) return;
|
|
10
|
+
try {
|
|
11
|
+
const data = await window.wsClient.rpc('conv.cancel', { id: this.state.currentConversation.id });
|
|
12
|
+
this._dbg('Stop response:', data);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error('Failed to stop:', err);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (this.ui.injectButton) {
|
|
20
|
+
this.ui.injectButton.addEventListener('click', async () => {
|
|
21
|
+
if (!this.state.currentConversation) return;
|
|
22
|
+
const isStreaming = this._convIsStreaming(this.state.currentConversation.id);
|
|
23
|
+
|
|
24
|
+
if (isStreaming) {
|
|
25
|
+
const message = this.ui.messageInput?.value || '';
|
|
26
|
+
if (!message.trim()) {
|
|
27
|
+
this.showError('Please enter a message to steer');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const steerMsg = message;
|
|
32
|
+
if (this.ui.messageInput) {
|
|
33
|
+
this.ui.messageInput.value = '';
|
|
34
|
+
this.ui.messageInput.style.height = 'auto';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: steerMsg })
|
|
38
|
+
.catch(err => {
|
|
39
|
+
console.error('Failed to steer:', err);
|
|
40
|
+
this.showError('Failed to steer: ' + err.message);
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
|
|
44
|
+
if (!instructions) return;
|
|
45
|
+
window.wsClient.rpc('conv.inject', { id: this.state.currentConversation.id, content: instructions })
|
|
46
|
+
.catch(err => console.error('Failed to inject:', err));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.ui.queueButton) {
|
|
52
|
+
this.ui.queueButton.addEventListener('click', async () => {
|
|
53
|
+
if (!this.state.currentConversation) return;
|
|
54
|
+
const message = this.ui.messageInput?.value || '';
|
|
55
|
+
if (!message.trim()) {
|
|
56
|
+
this.showError('Please enter a message to queue');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const data = await window.wsClient.rpc('msg.send', { id: this.state.currentConversation.id, content: message });
|
|
61
|
+
this._dbg('Queue response:', data);
|
|
62
|
+
if (this.ui.messageInput) {
|
|
63
|
+
this.ui.messageInput.value = '';
|
|
64
|
+
this.ui.messageInput.style.height = 'auto';
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error('Failed to queue:', err);
|
|
68
|
+
this.showError('Failed to queue: ' + err.message);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.ui.messageInput) {
|
|
74
|
+
this.ui.messageInput.addEventListener('keydown', (e) => {
|
|
75
|
+
if (e.key === 'Enter' && e.ctrlKey) {
|
|
76
|
+
this.startExecution();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.ui.messageInput.addEventListener('input', () => {
|
|
81
|
+
const el = this.ui.messageInput;
|
|
82
|
+
el.style.height = 'auto';
|
|
83
|
+
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
_setupUIWindowEvents() {
|
|
89
|
+
document.addEventListener('keydown', (e) => {
|
|
90
|
+
if (e.key === 'n' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const newBtn = document.querySelector('[data-new-conversation], #newConversationBtn, .new-conversation-btn');
|
|
93
|
+
if (newBtn) newBtn.click();
|
|
94
|
+
}
|
|
95
|
+
if (e.key === 'b' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
const toggleBtn = document.querySelector('[data-sidebar-toggle]');
|
|
98
|
+
if (toggleBtn) toggleBtn.click();
|
|
99
|
+
}
|
|
100
|
+
if (e.key === 'Escape') {
|
|
101
|
+
const activeEl = document.activeElement;
|
|
102
|
+
if (activeEl && activeEl.tagName === 'TEXTAREA') { activeEl.blur(); return; }
|
|
103
|
+
if (this.state.isStreaming) {
|
|
104
|
+
const cancelBtn = document.querySelector('#cancelBtn, [data-cancel-btn]');
|
|
105
|
+
if (cancelBtn && cancelBtn.offsetParent !== null) cancelBtn.click();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
111
|
+
if (themeToggle) {
|
|
112
|
+
themeToggle.addEventListener('click', () => this.toggleTheme());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.ui.outputEl) {
|
|
116
|
+
this.ui.outputEl.addEventListener('click', async (e) => {
|
|
117
|
+
const editBtn = e.target.closest('[data-edit-msg]');
|
|
118
|
+
if (!editBtn || !this.state.currentConversation) return;
|
|
119
|
+
const msgEl = editBtn.closest('.message-user');
|
|
120
|
+
const textEl = msgEl?.querySelector('.message-text');
|
|
121
|
+
if (!textEl) return;
|
|
122
|
+
const original = textEl.textContent || '';
|
|
123
|
+
const edited = await window.UIDialog?.prompt('Edit message:', original, 'Edit & Re-run');
|
|
124
|
+
if (!edited || edited === original) return;
|
|
125
|
+
this.ui.messageInput.value = edited;
|
|
126
|
+
this.startExecution();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.setupScrollTracking();
|
|
131
|
+
|
|
132
|
+
window.addEventListener('create-new-conversation', (event) => {
|
|
133
|
+
this.unlockAgentAndModel();
|
|
134
|
+
const detail = event.detail || {};
|
|
135
|
+
this.createNewConversation(detail.workingDirectory, detail.title);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
window.addEventListener('preparing-new-conversation', () => {
|
|
139
|
+
this.unlockAgentAndModel();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
window.addEventListener('conversation-selected', async (event) => {
|
|
143
|
+
const conversationId = event.detail.conversationId;
|
|
144
|
+
if (this._isLoadingConversation && this._loadingConversationId === conversationId) return;
|
|
145
|
+
this._loadingConversationId = conversationId;
|
|
146
|
+
this.updateUrlForConversation(conversationId);
|
|
147
|
+
this._isLoadingConversation = true;
|
|
148
|
+
try {
|
|
149
|
+
await this.loadConversationMessages(conversationId);
|
|
150
|
+
} finally {
|
|
151
|
+
this._isLoadingConversation = false;
|
|
152
|
+
this._loadingConversationId = null;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
window.addEventListener('conversation-deselected', () => {
|
|
157
|
+
window.ConversationState?.clear('deselected');
|
|
158
|
+
this.state.currentConversation = null;
|
|
159
|
+
this.state.currentSession = null;
|
|
160
|
+
this.updateUrlForConversation(null);
|
|
161
|
+
this.enableControls();
|
|
162
|
+
this._showWelcomeScreen();
|
|
163
|
+
if (this.ui.messageInput) {
|
|
164
|
+
this.ui.messageInput.value = '';
|
|
165
|
+
this.ui.messageInput.style.height = 'auto';
|
|
166
|
+
}
|
|
167
|
+
this.unlockAgentAndModel();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|