agentgui 1.0.727 → 1.0.729
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/CLAUDE.md +19 -8
- package/lib/tool-install-machine.js +157 -0
- package/lib/tool-manager.js +1 -2
- package/lib/tool-provisioner.js +6 -1
- package/lib/tool-spawner.js +24 -11
- package/lib/ws-handlers-util.js +5 -1
- package/package.json +1 -1
- package/server.js +24 -5
- package/static/index.html +5 -0
- package/static/js/client.js +75 -167
- package/static/js/conv-list-machine.js +137 -0
- package/static/js/conversations.js +39 -74
- package/static/js/prompt-machine.js +108 -0
- package/static/js/tool-install-machine.js +155 -0
- package/static/js/tools-manager-ui.js +119 -0
- package/static/js/tools-manager.js +164 -435
- package/static/js/voice-machine.js +145 -0
- package/static/js/voice.js +132 -119
package/static/js/client.js
CHANGED
|
@@ -75,16 +75,6 @@ class AgentGUIClient {
|
|
|
75
75
|
this._loadInProgress = {}; // { [conversationId]: { requestId, abortController, timestamp, prevConversationId } }
|
|
76
76
|
this._currentRequestId = 0; // Auto-incrementing request counter
|
|
77
77
|
|
|
78
|
-
// Prompt area state machine: READY | LOADING | STREAMING | QUEUED | DISABLED
|
|
79
|
-
// Controls atomic transitions to prevent inconsistent UI states
|
|
80
|
-
this._promptState = 'READY'; // Initial state
|
|
81
|
-
this._promptStateTransitions = {
|
|
82
|
-
'READY': ['LOADING', 'STREAMING', 'DISABLED'],
|
|
83
|
-
'LOADING': ['READY', 'STREAMING', 'DISABLED'],
|
|
84
|
-
'STREAMING': ['QUEUED', 'READY'],
|
|
85
|
-
'QUEUED': ['STREAMING', 'READY'],
|
|
86
|
-
'DISABLED': ['READY']
|
|
87
|
-
};
|
|
88
78
|
|
|
89
79
|
this._scrollTarget = 0;
|
|
90
80
|
this._scrollAnimating = false;
|
|
@@ -799,6 +789,7 @@ class AgentGUIClient {
|
|
|
799
789
|
|
|
800
790
|
async handleStreamingStart(data) {
|
|
801
791
|
console.log('Streaming started:', data);
|
|
792
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'STREAMING', conversationId: data.conversationId });
|
|
802
793
|
this._clearThinkingCountdown();
|
|
803
794
|
if (this._lastSendTime > 0) {
|
|
804
795
|
const actual = Date.now() - this._lastSendTime;
|
|
@@ -880,58 +871,7 @@ class AgentGUIClient {
|
|
|
880
871
|
}));
|
|
881
872
|
const userMsgs = (fullData.messages || []).filter(m => m.role === 'user');
|
|
882
873
|
if (priorChunks.length > 0) {
|
|
883
|
-
|
|
884
|
-
const sessionGroups = {};
|
|
885
|
-
priorChunks.forEach(c => {
|
|
886
|
-
if (!sessionGroups[c.sessionId]) { sessionGroups[c.sessionId] = []; sessionOrder.push(c.sessionId); }
|
|
887
|
-
sessionGroups[c.sessionId].push(c);
|
|
888
|
-
});
|
|
889
|
-
const priorFrag = document.createDocumentFragment();
|
|
890
|
-
let ui = 0;
|
|
891
|
-
sessionOrder.forEach(sid => {
|
|
892
|
-
const sList = sessionGroups[sid];
|
|
893
|
-
const sStart = sList[0].created_at;
|
|
894
|
-
while (ui < userMsgs.length && userMsgs[ui].created_at <= sStart) {
|
|
895
|
-
const m = userMsgs[ui++];
|
|
896
|
-
const uDiv = document.createElement('div');
|
|
897
|
-
uDiv.className = 'message message-user';
|
|
898
|
-
uDiv.setAttribute('data-msg-id', m.id);
|
|
899
|
-
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
900
|
-
priorFrag.appendChild(uDiv);
|
|
901
|
-
}
|
|
902
|
-
const mDiv = document.createElement('div');
|
|
903
|
-
mDiv.className = 'message message-assistant';
|
|
904
|
-
mDiv.id = `message-${sid}`;
|
|
905
|
-
mDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
906
|
-
const bEl = mDiv.querySelector('.message-blocks');
|
|
907
|
-
const bFrag = document.createDocumentFragment();
|
|
908
|
-
sList.forEach(chunk => {
|
|
909
|
-
if (!chunk.block?.type) return;
|
|
910
|
-
if (chunk.block.type === 'tool_result') {
|
|
911
|
-
const lastInFrag = bFrag.lastElementChild;
|
|
912
|
-
if (lastInFrag?.classList?.contains('block-tool-use')) {
|
|
913
|
-
this.renderer.mergeResultIntoToolUse(lastInFrag, chunk.block);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
const el = this.renderer.renderBlock(chunk.block, chunk, bFrag);
|
|
918
|
-
if (!el) return;
|
|
919
|
-
bFrag.appendChild(el);
|
|
920
|
-
});
|
|
921
|
-
bEl.appendChild(bFrag);
|
|
922
|
-
const ts = document.createElement('div'); ts.className = 'message-timestamp'; ts.textContent = new Date(sList[sList.length - 1].created_at).toLocaleString();
|
|
923
|
-
mDiv.appendChild(ts);
|
|
924
|
-
priorFrag.appendChild(mDiv);
|
|
925
|
-
});
|
|
926
|
-
while (ui < userMsgs.length) {
|
|
927
|
-
const m = userMsgs[ui++];
|
|
928
|
-
const uDiv = document.createElement('div');
|
|
929
|
-
uDiv.className = 'message message-user';
|
|
930
|
-
uDiv.setAttribute('data-msg-id', m.id);
|
|
931
|
-
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
932
|
-
priorFrag.appendChild(uDiv);
|
|
933
|
-
}
|
|
934
|
-
messagesEl.appendChild(priorFrag);
|
|
874
|
+
this._renderConversationContent(messagesEl, priorChunks, userMsgs, null);
|
|
935
875
|
} else {
|
|
936
876
|
messagesEl.appendChild(this.renderMessagesFragment(fullData.messages || []));
|
|
937
877
|
}
|
|
@@ -955,7 +895,6 @@ class AgentGUIClient {
|
|
|
955
895
|
`;
|
|
956
896
|
messagesEl.appendChild(streamingDiv);
|
|
957
897
|
} else {
|
|
958
|
-
// Reuse existing div - ensure streaming class and single indicator
|
|
959
898
|
streamingDiv.classList.add('streaming-message');
|
|
960
899
|
streamingDiv.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
|
|
961
900
|
const indDiv = document.createElement('div');
|
|
@@ -1118,6 +1057,7 @@ class AgentGUIClient {
|
|
|
1118
1057
|
|
|
1119
1058
|
handleStreamingError(data) {
|
|
1120
1059
|
console.error('Streaming error:', data);
|
|
1060
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1121
1061
|
this._clearThinkingCountdown();
|
|
1122
1062
|
|
|
1123
1063
|
// Hide stop and inject buttons on error
|
|
@@ -1195,6 +1135,7 @@ class AgentGUIClient {
|
|
|
1195
1135
|
|
|
1196
1136
|
handleStreamingComplete(data) {
|
|
1197
1137
|
console.log('Streaming completed:', data);
|
|
1138
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1198
1139
|
this._clearThinkingCountdown();
|
|
1199
1140
|
|
|
1200
1141
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
@@ -2130,12 +2071,75 @@ class AgentGUIClient {
|
|
|
2130
2071
|
}
|
|
2131
2072
|
}
|
|
2132
2073
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2074
|
+
_renderConversationContent(messagesContainer, chunks, userMessages, activeSessionId) {
|
|
2075
|
+
if (!chunks || chunks.length === 0) return;
|
|
2076
|
+
const sessionMap = new Map();
|
|
2077
|
+
for (const chunk of chunks) {
|
|
2078
|
+
if (!sessionMap.has(chunk.sessionId)) sessionMap.set(chunk.sessionId, []);
|
|
2079
|
+
sessionMap.get(chunk.sessionId).push(chunk);
|
|
2080
|
+
}
|
|
2081
|
+
const frag = document.createDocumentFragment();
|
|
2082
|
+
let ui = 0;
|
|
2083
|
+
for (const [sid, list] of sessionMap) {
|
|
2084
|
+
const sessionStart = list[0].created_at;
|
|
2085
|
+
while (ui < userMessages.length && userMessages[ui].created_at <= sessionStart) {
|
|
2086
|
+
const m = userMessages[ui++];
|
|
2087
|
+
const uDiv = document.createElement('div');
|
|
2088
|
+
uDiv.className = 'message message-user';
|
|
2089
|
+
uDiv.setAttribute('data-msg-id', m.id);
|
|
2090
|
+
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
2091
|
+
frag.appendChild(uDiv);
|
|
2092
|
+
}
|
|
2093
|
+
const isActive = sid === activeSessionId;
|
|
2094
|
+
const msgDiv = document.createElement('div');
|
|
2095
|
+
msgDiv.className = `message message-assistant${isActive ? ' streaming-message' : ''}`;
|
|
2096
|
+
msgDiv.id = isActive ? `streaming-${sid}` : `message-${sid}`;
|
|
2097
|
+
msgDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
2098
|
+
const blocksEl = msgDiv.querySelector('.message-blocks');
|
|
2099
|
+
const blockFrag = document.createDocumentFragment();
|
|
2100
|
+
const deferred = [];
|
|
2101
|
+
for (const chunk of list) {
|
|
2102
|
+
if (!chunk.block?.type) continue;
|
|
2103
|
+
if (chunk.block.type === 'tool_result') { deferred.push(chunk); continue; }
|
|
2104
|
+
const el = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
|
|
2105
|
+
if (!el) continue;
|
|
2106
|
+
el.classList.add('block-loaded');
|
|
2107
|
+
blockFrag.appendChild(el);
|
|
2108
|
+
}
|
|
2109
|
+
blocksEl.appendChild(blockFrag);
|
|
2110
|
+
for (const chunk of deferred) {
|
|
2111
|
+
const tid = chunk.block.tool_use_id;
|
|
2112
|
+
const toolUseEl = (tid ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`) : null)
|
|
2113
|
+
|| (blocksEl.lastElementChild?.classList.contains('block-tool-use') ? blocksEl.lastElementChild : null);
|
|
2114
|
+
if (toolUseEl) this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
|
|
2115
|
+
}
|
|
2116
|
+
if (isActive) {
|
|
2117
|
+
const ind = document.createElement('div');
|
|
2118
|
+
ind.className = 'streaming-indicator';
|
|
2119
|
+
ind.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
|
|
2120
|
+
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>';
|
|
2121
|
+
msgDiv.appendChild(ind);
|
|
2122
|
+
} else {
|
|
2123
|
+
const ts = document.createElement('div');
|
|
2124
|
+
ts.className = 'message-timestamp';
|
|
2125
|
+
ts.textContent = new Date(list[list.length - 1].created_at).toLocaleString();
|
|
2126
|
+
msgDiv.appendChild(ts);
|
|
2127
|
+
}
|
|
2128
|
+
frag.appendChild(msgDiv);
|
|
2129
|
+
}
|
|
2130
|
+
while (ui < userMessages.length) {
|
|
2131
|
+
const m = userMessages[ui++];
|
|
2132
|
+
const uDiv = document.createElement('div');
|
|
2133
|
+
uDiv.className = 'message message-user';
|
|
2134
|
+
uDiv.setAttribute('data-msg-id', m.id);
|
|
2135
|
+
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
2136
|
+
frag.appendChild(uDiv);
|
|
2137
|
+
}
|
|
2138
|
+
messagesContainer.appendChild(frag);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2136
2141
|
renderChunk(chunk) {
|
|
2137
2142
|
if (!chunk || !chunk.block) return;
|
|
2138
|
-
// Deduplicate: skip if already rendered via WebSocket streaming_progress
|
|
2139
2143
|
const seq = chunk.sequence;
|
|
2140
2144
|
if (seq !== undefined) {
|
|
2141
2145
|
const seen = (this._renderedSeqs = this._renderedSeqs || {})[chunk.sessionId] || (this._renderedSeqs[chunk.sessionId] = new Set());
|
|
@@ -2521,15 +2525,14 @@ class AgentGUIClient {
|
|
|
2521
2525
|
*/
|
|
2522
2526
|
disableControls() {
|
|
2523
2527
|
if (this.ui.sendButton) this.ui.sendButton.disabled = true;
|
|
2528
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'DISABLED' });
|
|
2524
2529
|
}
|
|
2525
2530
|
|
|
2526
|
-
/**
|
|
2527
|
-
* Enable UI controls after execution completes or fails
|
|
2528
|
-
*/
|
|
2529
2531
|
enableControls() {
|
|
2530
2532
|
if (this.ui.sendButton) {
|
|
2531
2533
|
this.ui.sendButton.disabled = !this.wsManager?.isConnected;
|
|
2532
2534
|
}
|
|
2535
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
2533
2536
|
this.updateBusyPromptArea(this.state.currentConversation?.id);
|
|
2534
2537
|
}
|
|
2535
2538
|
|
|
@@ -2847,103 +2850,8 @@ class AgentGUIClient {
|
|
|
2847
2850
|
}
|
|
2848
2851
|
|
|
2849
2852
|
if (chunks.length > 0) {
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
chunks.forEach(chunk => {
|
|
2853
|
-
if (!sessionChunks[chunk.sessionId]) {
|
|
2854
|
-
sessionChunks[chunk.sessionId] = [];
|
|
2855
|
-
sessionOrder.push(chunk.sessionId);
|
|
2856
|
-
}
|
|
2857
|
-
sessionChunks[chunk.sessionId].push(chunk);
|
|
2858
|
-
});
|
|
2859
|
-
|
|
2860
|
-
const frag = document.createDocumentFragment();
|
|
2861
|
-
let userMsgIdx = 0;
|
|
2862
|
-
|
|
2863
|
-
sessionOrder.forEach((sessionId) => {
|
|
2864
|
-
const sessionChunkList = sessionChunks[sessionId];
|
|
2865
|
-
const sessionStart = sessionChunkList[0].created_at;
|
|
2866
|
-
|
|
2867
|
-
while (userMsgIdx < userMessages.length && userMessages[userMsgIdx].created_at <= sessionStart) {
|
|
2868
|
-
const msg = userMessages[userMsgIdx];
|
|
2869
|
-
const userDiv = document.createElement('div');
|
|
2870
|
-
userDiv.className = 'message message-user';
|
|
2871
|
-
userDiv.setAttribute('data-msg-id', msg.id);
|
|
2872
|
-
userDiv.innerHTML = `
|
|
2873
|
-
<div class="message-role">User</div>
|
|
2874
|
-
${this.renderMessageContent(msg.content)}
|
|
2875
|
-
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
2876
|
-
`;
|
|
2877
|
-
frag.appendChild(userDiv);
|
|
2878
|
-
userMsgIdx++;
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
const isCurrentActiveSession = shouldResumeStreaming && latestSession && latestSession.id === sessionId;
|
|
2882
|
-
const messageDiv = document.createElement('div');
|
|
2883
|
-
messageDiv.className = `message message-assistant${isCurrentActiveSession ? ' streaming-message' : ''}`;
|
|
2884
|
-
messageDiv.id = isCurrentActiveSession ? `streaming-${sessionId}` : `message-${sessionId}`;
|
|
2885
|
-
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
2886
|
-
|
|
2887
|
-
const blocksEl = messageDiv.querySelector('.message-blocks');
|
|
2888
|
-
const blockFrag = document.createDocumentFragment();
|
|
2889
|
-
const toolResultBlocks = new Map();
|
|
2890
|
-
|
|
2891
|
-
sessionChunkList.forEach(chunk => {
|
|
2892
|
-
if (!chunk.block?.type) return;
|
|
2893
|
-
if (chunk.block.type === 'tool_result') {
|
|
2894
|
-
toolResultBlocks.set(chunk.id, chunk);
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
const element = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
|
|
2898
|
-
if (!element) return;
|
|
2899
|
-
element.classList.add('block-loaded');
|
|
2900
|
-
blockFrag.appendChild(element);
|
|
2901
|
-
});
|
|
2902
|
-
|
|
2903
|
-
blocksEl.appendChild(blockFrag);
|
|
2904
|
-
|
|
2905
|
-
toolResultBlocks.forEach((chunk) => {
|
|
2906
|
-
const toolUseId = chunk.block.tool_use_id;
|
|
2907
|
-
const toolUseEl = toolUseId
|
|
2908
|
-
? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${toolUseId}"]`)
|
|
2909
|
-
: blocksEl.lastElementChild?.classList?.contains('block-type-tool_use') ? blocksEl.lastElementChild : null;
|
|
2910
|
-
if (!toolUseEl) return;
|
|
2911
|
-
this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
|
|
2912
|
-
});
|
|
2913
|
-
|
|
2914
|
-
if (isCurrentActiveSession) {
|
|
2915
|
-
const indicatorDiv = document.createElement('div');
|
|
2916
|
-
indicatorDiv.className = 'streaming-indicator';
|
|
2917
|
-
indicatorDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
|
|
2918
|
-
indicatorDiv.innerHTML = `
|
|
2919
|
-
<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>
|
|
2920
|
-
<span class="streaming-indicator-label">Processing...</span>
|
|
2921
|
-
`;
|
|
2922
|
-
messageDiv.appendChild(indicatorDiv);
|
|
2923
|
-
} else {
|
|
2924
|
-
const ts = document.createElement('div');
|
|
2925
|
-
ts.className = 'message-timestamp';
|
|
2926
|
-
ts.textContent = new Date(sessionChunkList[sessionChunkList.length - 1].created_at).toLocaleString();
|
|
2927
|
-
messageDiv.appendChild(ts);
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
frag.appendChild(messageDiv);
|
|
2931
|
-
});
|
|
2932
|
-
|
|
2933
|
-
while (userMsgIdx < userMessages.length) {
|
|
2934
|
-
const msg = userMessages[userMsgIdx];
|
|
2935
|
-
const userDiv = document.createElement('div');
|
|
2936
|
-
userDiv.className = 'message message-user';
|
|
2937
|
-
userDiv.setAttribute('data-msg-id', msg.id);
|
|
2938
|
-
userDiv.innerHTML = `
|
|
2939
|
-
<div class="message-role">User</div>
|
|
2940
|
-
${this.renderMessageContent(msg.content)}
|
|
2941
|
-
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
2942
|
-
`;
|
|
2943
|
-
frag.appendChild(userDiv);
|
|
2944
|
-
userMsgIdx++;
|
|
2945
|
-
}
|
|
2946
|
-
if (!convSignal.aborted) messagesEl.appendChild(frag);
|
|
2853
|
+
const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
|
|
2854
|
+
if (!convSignal.aborted) this._renderConversationContent(messagesEl, chunks, userMessages, activeSessionId);
|
|
2947
2855
|
} else {
|
|
2948
2856
|
if (!convSignal.aborted) messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
|
|
2949
2857
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
const { createMachine, createActor, assign } = XState;
|
|
3
|
+
|
|
4
|
+
const convListMachine = createMachine({
|
|
5
|
+
id: 'conv-list',
|
|
6
|
+
initial: 'unloaded',
|
|
7
|
+
context: {
|
|
8
|
+
conversations: [],
|
|
9
|
+
activeId: null,
|
|
10
|
+
streamingIds: [],
|
|
11
|
+
version: 0,
|
|
12
|
+
lastPollAt: null,
|
|
13
|
+
},
|
|
14
|
+
states: {
|
|
15
|
+
unloaded: {
|
|
16
|
+
on: {
|
|
17
|
+
LOAD_START: 'loading',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
loading: {
|
|
21
|
+
on: {
|
|
22
|
+
LOAD_DONE: {
|
|
23
|
+
target: 'loaded',
|
|
24
|
+
actions: assign(({ context, event }) => {
|
|
25
|
+
const incoming = event.conversations || [];
|
|
26
|
+
let conversations;
|
|
27
|
+
if (incoming.length === 0 && context.conversations.length > 0) {
|
|
28
|
+
conversations = context.conversations;
|
|
29
|
+
} else if (incoming.length > 0 && incoming.length < context.conversations.length) {
|
|
30
|
+
const polledIds = new Set(incoming.map(c => c.id));
|
|
31
|
+
const kept = context.conversations.filter(c => !polledIds.has(c.id));
|
|
32
|
+
conversations = incoming.map(pc => {
|
|
33
|
+
const cached = context.conversations.find(c => c.id === pc.id);
|
|
34
|
+
return cached ? Object.assign({}, cached, pc) : pc;
|
|
35
|
+
}).concat(kept);
|
|
36
|
+
} else {
|
|
37
|
+
conversations = incoming;
|
|
38
|
+
}
|
|
39
|
+
return { conversations, version: context.version + 1, lastPollAt: Date.now() };
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
LOAD_ERROR: 'error',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
loaded: {
|
|
46
|
+
on: {
|
|
47
|
+
LOAD_START: 'loading',
|
|
48
|
+
ADD: {
|
|
49
|
+
actions: assign(({ context, event }) => {
|
|
50
|
+
if (context.conversations.some(c => c.id === event.conversation.id)) return {};
|
|
51
|
+
return { conversations: [event.conversation, ...context.conversations], version: context.version + 1 };
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
UPDATE: {
|
|
55
|
+
actions: assign(({ context, event }) => {
|
|
56
|
+
const idx = context.conversations.findIndex(c => c.id === event.conversation.id);
|
|
57
|
+
if (idx < 0) return {};
|
|
58
|
+
const updated = [...context.conversations];
|
|
59
|
+
updated[idx] = Object.assign({}, updated[idx], event.conversation);
|
|
60
|
+
return { conversations: updated, version: context.version + 1 };
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
DELETE: {
|
|
64
|
+
actions: assign(({ context, event }) => ({
|
|
65
|
+
conversations: context.conversations.filter(c => c.id !== event.id),
|
|
66
|
+
activeId: context.activeId === event.id ? null : context.activeId,
|
|
67
|
+
streamingIds: context.streamingIds.filter(id => id !== event.id),
|
|
68
|
+
version: context.version + 1,
|
|
69
|
+
})),
|
|
70
|
+
},
|
|
71
|
+
CLEAR_ALL: {
|
|
72
|
+
actions: assign({ conversations: [], activeId: null, streamingIds: [], version: 0 }),
|
|
73
|
+
},
|
|
74
|
+
SET_STREAMING: {
|
|
75
|
+
actions: assign(({ context, event }) => ({
|
|
76
|
+
streamingIds: context.streamingIds.includes(event.id)
|
|
77
|
+
? context.streamingIds
|
|
78
|
+
: [...context.streamingIds, event.id],
|
|
79
|
+
})),
|
|
80
|
+
},
|
|
81
|
+
CLEAR_STREAMING: {
|
|
82
|
+
actions: assign(({ context, event }) => ({
|
|
83
|
+
streamingIds: context.streamingIds.filter(id => id !== event.id),
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
SELECT: {
|
|
87
|
+
actions: assign(({ event }) => ({ activeId: event.id })),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
error: {
|
|
92
|
+
on: {
|
|
93
|
+
LOAD_START: 'loading',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const actor = createActor(convListMachine);
|
|
100
|
+
actor.start();
|
|
101
|
+
|
|
102
|
+
function sendEvent(event) {
|
|
103
|
+
actor.send(event);
|
|
104
|
+
return actor.getSnapshot();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getState() {
|
|
108
|
+
return actor.getSnapshot().value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getContext() {
|
|
112
|
+
return actor.getSnapshot().context;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getConversations() {
|
|
116
|
+
return actor.getSnapshot().context.conversations;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getStreamingIds() {
|
|
120
|
+
return actor.getSnapshot().context.streamingIds;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isStreaming(id) {
|
|
124
|
+
return actor.getSnapshot().context.streamingIds.includes(id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getActiveId() {
|
|
128
|
+
return actor.getSnapshot().context.activeId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function subscribe(fn) {
|
|
132
|
+
return actor.subscribe(fn);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.__convListMachine = actor;
|
|
136
|
+
window.convListMachineAPI = { send: sendEvent, getState, getContext, getConversations, getStreamingIds, isStreaming, getActiveId, subscribe };
|
|
137
|
+
})();
|
|
@@ -15,19 +15,12 @@ function pathBasename(p) {
|
|
|
15
15
|
|
|
16
16
|
class ConversationManager {
|
|
17
17
|
constructor() {
|
|
18
|
-
this.conversations = [];
|
|
19
|
-
this.activeId = null;
|
|
20
18
|
this.listEl = document.querySelector('[data-conversation-list]');
|
|
21
19
|
this.emptyEl = document.querySelector('[data-conversation-empty]');
|
|
22
20
|
this.newBtn = document.querySelector('[data-new-conversation]');
|
|
23
21
|
this.sidebarEl = document.querySelector('[data-sidebar]');
|
|
24
|
-
this.streamingConversations = new Set();
|
|
25
22
|
this.agents = new Map();
|
|
26
23
|
|
|
27
|
-
this._conversationVersion = 0;
|
|
28
|
-
this._lastMutationSource = null;
|
|
29
|
-
this._lastMutationTime = 0;
|
|
30
|
-
|
|
31
24
|
this.folderBrowser = {
|
|
32
25
|
modal: null,
|
|
33
26
|
listEl: null,
|
|
@@ -43,6 +36,23 @@ class ConversationManager {
|
|
|
43
36
|
this.init();
|
|
44
37
|
}
|
|
45
38
|
|
|
39
|
+
get conversations() {
|
|
40
|
+
return window.convListMachineAPI ? window.convListMachineAPI.getConversations() : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get activeId() {
|
|
44
|
+
return window.convListMachineAPI ? window.convListMachineAPI.getActiveId() : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
set activeId(id) {
|
|
48
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'SELECT', id });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get streamingConversations() {
|
|
52
|
+
const ids = window.convListMachineAPI ? window.convListMachineAPI.getStreamingIds() : [];
|
|
53
|
+
return { has: (id) => ids.includes(id), add: (id) => window.convListMachineAPI?.send({ type: 'SET_STREAMING', id }), delete: (id) => window.convListMachineAPI?.send({ type: 'CLEAR_STREAMING', id }), clear: () => ids.forEach(id => window.convListMachineAPI?.send({ type: 'CLEAR_STREAMING', id })) };
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
async init() {
|
|
47
57
|
this.newBtn?.addEventListener('click', () => this.openFolderBrowser());
|
|
48
58
|
this.setupDelegatedListeners();
|
|
@@ -80,27 +90,18 @@ class ConversationManager {
|
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
_updateConversations(newArray, source
|
|
93
|
+
_updateConversations(newArray, source) {
|
|
94
|
+
if (!window.convListMachineAPI) return { version: 0, timestamp: Date.now(), oldLen: 0, newLen: 0 };
|
|
84
95
|
const oldLen = this.conversations.length;
|
|
85
96
|
const newLen = Array.isArray(newArray) ? newArray.length : 0;
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this._lastMutationSource = source;
|
|
91
|
-
this._lastMutationTime = timestamp;
|
|
92
|
-
|
|
93
|
-
window._conversationCacheVersion = mutationId;
|
|
94
|
-
|
|
95
|
-
if (context.verbose) {
|
|
96
|
-
console.log(`[ConvMgr] mutation #${mutationId} (${source}): ${oldLen} → ${newLen} items, ts=${timestamp}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { version: mutationId, timestamp, oldLen, newLen };
|
|
97
|
+
window.convListMachineAPI.send({ type: 'LOAD_DONE', conversations: Array.isArray(newArray) ? newArray : [] });
|
|
98
|
+
const version = window.convListMachineAPI.getContext().version;
|
|
99
|
+
window._conversationCacheVersion = version;
|
|
100
|
+
return { version, timestamp: Date.now(), oldLen, newLen };
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
getConversationCacheVersion() {
|
|
103
|
-
return
|
|
104
|
+
return window.convListMachineAPI ? window.convListMachineAPI.getContext().version : 0;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
getAgentDisplayName(agentId) {
|
|
@@ -315,9 +316,8 @@ class ConversationManager {
|
|
|
315
316
|
this.deleteAllBtn.disabled = true;
|
|
316
317
|
await window.wsClient.rpc('conv.del.all', {});
|
|
317
318
|
console.log('[ConversationManager] Deleted all conversations');
|
|
318
|
-
|
|
319
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'CLEAR_ALL' });
|
|
319
320
|
window.ConversationState?.clear('delete_all');
|
|
320
|
-
this.activeId = null;
|
|
321
321
|
window.dispatchEvent(new CustomEvent('conversation-deselected'));
|
|
322
322
|
this.render();
|
|
323
323
|
} catch (err) {
|
|
@@ -424,6 +424,7 @@ class ConversationManager {
|
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
async loadConversations() {
|
|
427
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'LOAD_START' });
|
|
427
428
|
try {
|
|
428
429
|
const base = window.__BASE_URL || '/gm';
|
|
429
430
|
const res = await fetch(base + '/api/conversations');
|
|
@@ -431,44 +432,25 @@ class ConversationManager {
|
|
|
431
432
|
const data = await res.json();
|
|
432
433
|
const convList = data.conversations || [];
|
|
433
434
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (convList.length > 0) {
|
|
437
|
-
// If poll returns fewer conversations than cached, merge to avoid dropping items
|
|
438
|
-
// due to transient server errors or partial responses
|
|
439
|
-
if (convList.length < this.conversations.length) {
|
|
440
|
-
const polledIds = new Set(convList.map(c => c.id));
|
|
441
|
-
const kept = this.conversations.filter(c => !polledIds.has(c.id));
|
|
442
|
-
// Update polled items in place, append any cached items not in poll result
|
|
443
|
-
const merged = convList.map(pc => {
|
|
444
|
-
const cached = this.conversations.find(c => c.id === pc.id);
|
|
445
|
-
return cached ? Object.assign({}, cached, pc) : pc;
|
|
446
|
-
}).concat(kept);
|
|
447
|
-
this._updateConversations(merged, 'poll_merge');
|
|
448
|
-
} else {
|
|
449
|
-
this._updateConversations(convList, 'poll');
|
|
450
|
-
}
|
|
451
|
-
} else if (this.conversations.length === 0) {
|
|
452
|
-
// First load and empty - show empty state, but don't clear on subsequent polls
|
|
453
|
-
this._updateConversations(convList, 'poll');
|
|
435
|
+
if (window.convListMachineAPI) {
|
|
436
|
+
window.convListMachineAPI.send({ type: 'LOAD_DONE', conversations: convList });
|
|
454
437
|
}
|
|
455
|
-
// If convList is empty but this.conversations has items, do nothing - keep existing
|
|
456
438
|
|
|
457
439
|
const clientStreamingMap = window.agentGuiClient?.state?.streamingConversations;
|
|
458
440
|
for (const conv of this.conversations) {
|
|
459
441
|
const serverStreaming = conv.isStreaming === 1 || conv.isStreaming === true;
|
|
460
442
|
const clientStreaming = clientStreamingMap ? clientStreamingMap.has(conv.id) : false;
|
|
461
443
|
if (serverStreaming || clientStreaming) {
|
|
462
|
-
|
|
444
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'SET_STREAMING', id: conv.id });
|
|
463
445
|
} else {
|
|
464
|
-
|
|
446
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'CLEAR_STREAMING', id: conv.id });
|
|
465
447
|
}
|
|
466
448
|
}
|
|
467
449
|
|
|
468
450
|
this.render();
|
|
469
451
|
} catch (err) {
|
|
470
452
|
console.error('Failed to load conversations:', err);
|
|
471
|
-
|
|
453
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'LOAD_ERROR' });
|
|
472
454
|
if (this.conversations.length === 0) {
|
|
473
455
|
this.showEmpty('Failed to load conversations');
|
|
474
456
|
}
|
|
@@ -595,7 +577,7 @@ class ConversationManager {
|
|
|
595
577
|
console.error('[ConvMgr] activeId mutation rejected:', result.reason);
|
|
596
578
|
return;
|
|
597
579
|
}
|
|
598
|
-
|
|
580
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'SELECT', id: convId });
|
|
599
581
|
|
|
600
582
|
document.querySelectorAll('.conversation-item').forEach(item => {
|
|
601
583
|
item.classList.remove('active');
|
|
@@ -622,35 +604,20 @@ class ConversationManager {
|
|
|
622
604
|
}
|
|
623
605
|
|
|
624
606
|
addConversation(conv) {
|
|
625
|
-
if (
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const newConvs = [conv, ...this.conversations];
|
|
629
|
-
this._updateConversations(newConvs, 'add', { convId: conv.id });
|
|
607
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'ADD', conversation: conv });
|
|
630
608
|
this.render();
|
|
631
609
|
}
|
|
632
610
|
|
|
633
611
|
updateConversation(convId, updates) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const updated = Object.assign({}, this.conversations[idx], updates);
|
|
637
|
-
const newConvs = [
|
|
638
|
-
...this.conversations.slice(0, idx),
|
|
639
|
-
updated,
|
|
640
|
-
...this.conversations.slice(idx + 1)
|
|
641
|
-
];
|
|
642
|
-
this._updateConversations(newConvs, 'update', { convId });
|
|
643
|
-
this.render();
|
|
644
|
-
}
|
|
612
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'UPDATE', conversation: Object.assign({ id: convId }, updates) });
|
|
613
|
+
this.render();
|
|
645
614
|
}
|
|
646
615
|
|
|
647
616
|
deleteConversation(convId) {
|
|
648
617
|
const wasActive = this.activeId === convId;
|
|
649
|
-
|
|
650
|
-
this._updateConversations(newConvs, 'delete', { convId });
|
|
618
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'DELETE', id: convId });
|
|
651
619
|
if (wasActive) {
|
|
652
620
|
window.ConversationState?.deleteConversation(convId, 1);
|
|
653
|
-
this.activeId = null;
|
|
654
621
|
window.dispatchEvent(new CustomEvent('conversation-deselected'));
|
|
655
622
|
}
|
|
656
623
|
this.render();
|
|
@@ -667,16 +634,14 @@ class ConversationManager {
|
|
|
667
634
|
} else if (msg.type === 'conversation_deleted') {
|
|
668
635
|
this.deleteConversation(msg.conversationId);
|
|
669
636
|
} else if (msg.type === 'all_conversations_deleted') {
|
|
670
|
-
|
|
637
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'CLEAR_ALL' });
|
|
671
638
|
window.ConversationState?.clear('all_deleted');
|
|
672
|
-
this.activeId = null;
|
|
673
|
-
this.streamingConversations.clear();
|
|
674
639
|
this.showEmpty('No conversations yet');
|
|
675
640
|
} else if (msg.type === 'streaming_start' && msg.conversationId) {
|
|
676
|
-
|
|
641
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'SET_STREAMING', id: msg.conversationId });
|
|
677
642
|
this.render();
|
|
678
643
|
} else if ((msg.type === 'streaming_complete' || msg.type === 'streaming_error') && msg.conversationId) {
|
|
679
|
-
|
|
644
|
+
if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'CLEAR_STREAMING', id: msg.conversationId });
|
|
680
645
|
this.render();
|
|
681
646
|
}
|
|
682
647
|
});
|