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.
@@ -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
- const sessionOrder = [];
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
- * Render a single chunk to the output
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 sessionOrder = [];
2851
- const sessionChunks = {};
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, context = {}) {
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
- const mutationId = ++this._conversationVersion;
87
- const timestamp = Date.now();
88
-
89
- this.conversations = Array.isArray(newArray) ? newArray : [];
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 this._conversationVersion;
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
- this._updateConversations([], 'clear_all');
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
- // Never clear conversations on poll if the list is empty — preserve existing state
435
- // Empty list likely indicates a server error, not actually empty conversations
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
- this.streamingConversations.add(conv.id);
444
+ if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'SET_STREAMING', id: conv.id });
463
445
  } else {
464
- this.streamingConversations.delete(conv.id);
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
- // Don't show error state if we already have conversations cached - server may be transient issue
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
- this.activeId = convId;
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 (this.conversations.some(c => c.id === conv.id)) {
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
- const idx = this.conversations.findIndex(c => c.id === convId);
635
- if (idx >= 0) {
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
- const newConvs = this.conversations.filter(c => c.id !== convId);
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
- this._updateConversations([], 'ws_clear_all');
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
- this.streamingConversations.add(msg.conversationId);
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
- this.streamingConversations.delete(msg.conversationId);
644
+ if (window.convListMachineAPI) window.convListMachineAPI.send({ type: 'CLEAR_STREAMING', id: msg.conversationId });
680
645
  this.render();
681
646
  }
682
647
  });