@agent-link/server 0.1.145 → 0.1.146
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/package.json +1 -1
- package/web/app.js +4 -46
- package/web/modules/appHelpers.js +84 -0
- package/web/modules/backgroundRouting.js +258 -0
- package/web/modules/connection.js +4 -303
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createConnection } from './modules/connection.js';
|
|
|
19
19
|
import { createFileBrowser } from './modules/fileBrowser.js';
|
|
20
20
|
import { createFilePreview } from './modules/filePreview.js';
|
|
21
21
|
import { createTeam } from './modules/team.js';
|
|
22
|
+
import { createScrollManager, createHighlightScheduler, formatUsage } from './modules/appHelpers.js';
|
|
22
23
|
|
|
23
24
|
// ── App ─────────────────────────────────────────────────────────────────────
|
|
24
25
|
const App = {
|
|
@@ -231,38 +232,10 @@ const App = {
|
|
|
231
232
|
applyTheme();
|
|
232
233
|
|
|
233
234
|
// ── Scroll management ──
|
|
234
|
-
|
|
235
|
-
let _userScrolledUp = false;
|
|
236
|
-
|
|
237
|
-
function onMessageListScroll(e) {
|
|
238
|
-
const el = e.target;
|
|
239
|
-
_userScrolledUp = (el.scrollHeight - el.scrollTop - el.clientHeight) > 80;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function scrollToBottom(force) {
|
|
243
|
-
if (_userScrolledUp && !force) return;
|
|
244
|
-
if (_scrollTimer) return;
|
|
245
|
-
_scrollTimer = setTimeout(() => {
|
|
246
|
-
_scrollTimer = null;
|
|
247
|
-
const el = document.querySelector('.message-list');
|
|
248
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
249
|
-
}, 50);
|
|
250
|
-
}
|
|
235
|
+
const { onScroll: onMessageListScroll, scrollToBottom, cleanup: cleanupScroll } = createScrollManager('.message-list');
|
|
251
236
|
|
|
252
237
|
// ── Highlight.js scheduling ──
|
|
253
|
-
|
|
254
|
-
function scheduleHighlight() {
|
|
255
|
-
if (_hlTimer) return;
|
|
256
|
-
_hlTimer = setTimeout(() => {
|
|
257
|
-
_hlTimer = null;
|
|
258
|
-
if (typeof hljs !== 'undefined') {
|
|
259
|
-
document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
|
|
260
|
-
hljs.highlightElement(block);
|
|
261
|
-
block.dataset.highlighted = 'true';
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}, 300);
|
|
265
|
-
}
|
|
238
|
+
const { scheduleHighlight, cleanup: cleanupHighlight } = createHighlightScheduler();
|
|
266
239
|
|
|
267
240
|
// ── Create module instances ──
|
|
268
241
|
|
|
@@ -475,25 +448,10 @@ const App = {
|
|
|
475
448
|
document.title = name ? `${name} — AgentLink` : 'AgentLink';
|
|
476
449
|
});
|
|
477
450
|
|
|
478
|
-
// ── Usage formatting ──
|
|
479
|
-
function formatTokens(n) {
|
|
480
|
-
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
481
|
-
return String(n);
|
|
482
|
-
}
|
|
483
|
-
function formatUsage(u) {
|
|
484
|
-
if (!u) return '';
|
|
485
|
-
const pct = u.contextWindow ? Math.round(u.inputTokens / u.contextWindow * 100) : 0;
|
|
486
|
-
const ctx = formatTokens(u.inputTokens) + ' / ' + formatTokens(u.contextWindow) + ' (' + pct + '%)';
|
|
487
|
-
const cost = '$' + u.totalCost.toFixed(2);
|
|
488
|
-
const model = u.model.replace(/^claude-/, '').replace(/-\d{8}$/, '').replace(/-1m$/, '');
|
|
489
|
-
const dur = (u.durationMs / 1000).toFixed(1) + 's';
|
|
490
|
-
return 'Context ' + ctx + ' \u00b7 Cost ' + cost + ' \u00b7 ' + model + ' \u00b7 ' + dur;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
451
|
// ── Lifecycle ──
|
|
494
452
|
onMounted(() => { connect(scheduleHighlight); });
|
|
495
453
|
onUnmounted(() => {
|
|
496
|
-
closeWs(); streaming.cleanup();
|
|
454
|
+
closeWs(); streaming.cleanup(); cleanupScroll(); cleanupHighlight();
|
|
497
455
|
window.removeEventListener('resize', _resizeHandler);
|
|
498
456
|
document.removeEventListener('click', _workdirMenuClickHandler);
|
|
499
457
|
document.removeEventListener('keydown', _workdirMenuKeyHandler);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ── UI utility functions for the main App component ──────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create scroll management functions.
|
|
5
|
+
* @param {string} selector - CSS selector for the scrollable element
|
|
6
|
+
* @returns {{ onScroll, scrollToBottom, cleanup }}
|
|
7
|
+
*/
|
|
8
|
+
export function createScrollManager(selector) {
|
|
9
|
+
let _scrollTimer = null;
|
|
10
|
+
let _userScrolledUp = false;
|
|
11
|
+
|
|
12
|
+
function onScroll(e) {
|
|
13
|
+
const el = e.target;
|
|
14
|
+
_userScrolledUp = (el.scrollHeight - el.scrollTop - el.clientHeight) > 80;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scrollToBottom(force) {
|
|
18
|
+
if (_userScrolledUp && !force) return;
|
|
19
|
+
if (_scrollTimer) return;
|
|
20
|
+
_scrollTimer = setTimeout(() => {
|
|
21
|
+
_scrollTimer = null;
|
|
22
|
+
const el = document.querySelector(selector);
|
|
23
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
24
|
+
}, 50);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanup() {
|
|
28
|
+
if (_scrollTimer) { clearTimeout(_scrollTimer); _scrollTimer = null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { onScroll, scrollToBottom, cleanup };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a debounced highlight.js scheduler.
|
|
36
|
+
* @returns {{ scheduleHighlight, cleanup }}
|
|
37
|
+
*/
|
|
38
|
+
export function createHighlightScheduler() {
|
|
39
|
+
let _hlTimer = null;
|
|
40
|
+
|
|
41
|
+
function scheduleHighlight() {
|
|
42
|
+
if (_hlTimer) return;
|
|
43
|
+
_hlTimer = setTimeout(() => {
|
|
44
|
+
_hlTimer = null;
|
|
45
|
+
if (typeof hljs !== 'undefined') {
|
|
46
|
+
document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
|
|
47
|
+
hljs.highlightElement(block);
|
|
48
|
+
block.dataset.highlighted = 'true';
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, 300);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cleanup() {
|
|
55
|
+
if (_hlTimer) { clearTimeout(_hlTimer); _hlTimer = null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { scheduleHighlight, cleanup };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format a token count for display (e.g. 1500 → "1.5k").
|
|
63
|
+
* @param {number} n
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function formatTokens(n) {
|
|
67
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
68
|
+
return String(n);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a usage stats object into a human-readable summary string.
|
|
73
|
+
* @param {object|null} u - Usage stats from turn_completed
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function formatUsage(u) {
|
|
77
|
+
if (!u) return '';
|
|
78
|
+
const pct = u.contextWindow ? Math.round(u.inputTokens / u.contextWindow * 100) : 0;
|
|
79
|
+
const ctx = formatTokens(u.inputTokens) + ' / ' + formatTokens(u.contextWindow) + ' (' + pct + '%)';
|
|
80
|
+
const cost = '$' + u.totalCost.toFixed(2);
|
|
81
|
+
const model = u.model.replace(/^claude-/, '').replace(/-\d{8}$/, '').replace(/-1m$/, '');
|
|
82
|
+
const dur = (u.durationMs / 1000).toFixed(1) + 's';
|
|
83
|
+
return 'Context ' + ctx + ' \u00b7 Cost ' + cost + ' \u00b7 ' + model + ' \u00b7 ' + dur;
|
|
84
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// ── History batch building & background conversation routing ──────────────────
|
|
2
|
+
import { isContextSummary } from './messageHelpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert a history array (from conversation_resumed) into a batch of UI messages.
|
|
6
|
+
* @param {Array} history - Array of {role, content, ...} from the agent
|
|
7
|
+
* @param {() => number} nextId - Function that returns the next message ID
|
|
8
|
+
* @returns {Array} Batch of UI message objects
|
|
9
|
+
*/
|
|
10
|
+
export function buildHistoryBatch(history, nextId) {
|
|
11
|
+
const batch = [];
|
|
12
|
+
for (const h of history) {
|
|
13
|
+
if (h.role === 'user') {
|
|
14
|
+
if (isContextSummary(h.content)) {
|
|
15
|
+
batch.push({
|
|
16
|
+
id: nextId(), role: 'context-summary',
|
|
17
|
+
content: h.content, contextExpanded: false,
|
|
18
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
19
|
+
});
|
|
20
|
+
} else if (h.isCommandOutput) {
|
|
21
|
+
batch.push({
|
|
22
|
+
id: nextId(), role: 'system',
|
|
23
|
+
content: h.content, isCommandOutput: true,
|
|
24
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
batch.push({
|
|
28
|
+
id: nextId(), role: 'user',
|
|
29
|
+
content: h.content,
|
|
30
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} else if (h.role === 'assistant') {
|
|
34
|
+
const last = batch[batch.length - 1];
|
|
35
|
+
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
36
|
+
last.content += '\n\n' + h.content;
|
|
37
|
+
} else {
|
|
38
|
+
batch.push({
|
|
39
|
+
id: nextId(), role: 'assistant',
|
|
40
|
+
content: h.content, isStreaming: false,
|
|
41
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} else if (h.role === 'tool') {
|
|
45
|
+
batch.push({
|
|
46
|
+
id: nextId(), role: 'tool',
|
|
47
|
+
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
48
|
+
toolInput: h.toolInput || '', hasResult: true,
|
|
49
|
+
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'),
|
|
50
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return batch;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Finalize the last streaming assistant message in a message array.
|
|
59
|
+
* @param {Array} msgs - Array of message objects
|
|
60
|
+
*/
|
|
61
|
+
export function finalizeLastStreaming(msgs) {
|
|
62
|
+
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
63
|
+
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
64
|
+
last.isStreaming = false;
|
|
65
|
+
if (isContextSummary(last.content)) {
|
|
66
|
+
last.role = 'context-summary';
|
|
67
|
+
last.contextExpanded = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Route a message to a background (non-foreground) conversation's cache.
|
|
74
|
+
* @param {object} deps - Dependencies: conversationCache, processingConversations, sidebar, wsSend
|
|
75
|
+
* @param {string} convId - The conversation ID
|
|
76
|
+
* @param {object} msg - The incoming message
|
|
77
|
+
*/
|
|
78
|
+
export function routeToBackgroundConversation(deps, convId, msg) {
|
|
79
|
+
const { conversationCache, processingConversations, sidebar, wsSend } = deps;
|
|
80
|
+
const cache = conversationCache.value[convId];
|
|
81
|
+
if (!cache) return;
|
|
82
|
+
|
|
83
|
+
if (msg.type === 'session_started') {
|
|
84
|
+
cache.claudeSessionId = msg.claudeSessionId;
|
|
85
|
+
sidebar.requestSessionList();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (msg.type === 'conversation_resumed') {
|
|
90
|
+
cache.claudeSessionId = msg.claudeSessionId;
|
|
91
|
+
if (msg.history && Array.isArray(msg.history)) {
|
|
92
|
+
const nextId = () => ++cache.messageIdCounter;
|
|
93
|
+
cache.messages = buildHistoryBatch(msg.history, nextId);
|
|
94
|
+
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
95
|
+
}
|
|
96
|
+
cache.loadingHistory = false;
|
|
97
|
+
if (msg.isCompacting) {
|
|
98
|
+
cache.isCompacting = true;
|
|
99
|
+
cache.isProcessing = true;
|
|
100
|
+
processingConversations.value[convId] = true;
|
|
101
|
+
cache.messages.push({
|
|
102
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
103
|
+
content: 'Context compacting...', isCompactStart: true,
|
|
104
|
+
timestamp: new Date(),
|
|
105
|
+
});
|
|
106
|
+
} else if (msg.isProcessing) {
|
|
107
|
+
cache.isProcessing = true;
|
|
108
|
+
processingConversations.value[convId] = true;
|
|
109
|
+
cache.messages.push({
|
|
110
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
111
|
+
content: 'Agent is processing...',
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
cache.messages.push({
|
|
116
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
117
|
+
content: 'Session restored. You can continue the conversation.',
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (msg.type === 'claude_output') {
|
|
125
|
+
if (!cache.isProcessing) {
|
|
126
|
+
cache.isProcessing = true;
|
|
127
|
+
processingConversations.value[convId] = true;
|
|
128
|
+
}
|
|
129
|
+
const data = msg.data;
|
|
130
|
+
if (!data) return;
|
|
131
|
+
if (data.type === 'content_block_delta' && data.delta) {
|
|
132
|
+
const msgs = cache.messages;
|
|
133
|
+
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
134
|
+
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
135
|
+
last.content += data.delta;
|
|
136
|
+
} else {
|
|
137
|
+
msgs.push({
|
|
138
|
+
id: ++cache.messageIdCounter, role: 'assistant',
|
|
139
|
+
content: data.delta, isStreaming: true, timestamp: new Date(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else if (data.type === 'tool_use' && data.tools) {
|
|
143
|
+
const msgs = cache.messages;
|
|
144
|
+
finalizeLastStreaming(msgs);
|
|
145
|
+
for (const tool of data.tools) {
|
|
146
|
+
const toolMsg = {
|
|
147
|
+
id: ++cache.messageIdCounter, role: 'tool',
|
|
148
|
+
toolId: tool.id, toolName: tool.name || 'unknown',
|
|
149
|
+
toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
|
|
150
|
+
hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite' || tool.name === 'Agent'),
|
|
151
|
+
timestamp: new Date(),
|
|
152
|
+
};
|
|
153
|
+
msgs.push(toolMsg);
|
|
154
|
+
if (tool.id) {
|
|
155
|
+
if (!cache.toolMsgMap) cache.toolMsgMap = new Map();
|
|
156
|
+
cache.toolMsgMap.set(tool.id, toolMsg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (data.type === 'user' && data.tool_use_result) {
|
|
160
|
+
const result = data.tool_use_result;
|
|
161
|
+
const results = Array.isArray(result) ? result : [result];
|
|
162
|
+
const tMap = cache.toolMsgMap || new Map();
|
|
163
|
+
for (const r of results) {
|
|
164
|
+
const toolMsg = tMap.get(r.tool_use_id);
|
|
165
|
+
if (toolMsg) {
|
|
166
|
+
toolMsg.toolOutput = typeof r.content === 'string'
|
|
167
|
+
? r.content : JSON.stringify(r.content, null, 2);
|
|
168
|
+
toolMsg.hasResult = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
|
|
173
|
+
finalizeLastStreaming(cache.messages);
|
|
174
|
+
cache.isProcessing = false;
|
|
175
|
+
cache.isCompacting = false;
|
|
176
|
+
if (msg.usage) cache.usageStats = msg.usage;
|
|
177
|
+
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
178
|
+
processingConversations.value[convId] = false;
|
|
179
|
+
if (msg.type === 'execution_cancelled') {
|
|
180
|
+
cache.needsResume = true;
|
|
181
|
+
cache.messages.push({
|
|
182
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
183
|
+
content: 'Generation stopped.', timestamp: new Date(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
sidebar.requestSessionList();
|
|
187
|
+
if (cache.queuedMessages && cache.queuedMessages.length > 0) {
|
|
188
|
+
const queued = cache.queuedMessages.shift();
|
|
189
|
+
cache.messages.push({
|
|
190
|
+
id: ++cache.messageIdCounter, role: 'user', status: 'sent',
|
|
191
|
+
content: queued.content, attachments: queued.attachments,
|
|
192
|
+
timestamp: new Date(),
|
|
193
|
+
});
|
|
194
|
+
cache.isProcessing = true;
|
|
195
|
+
processingConversations.value[convId] = true;
|
|
196
|
+
wsSend(queued.payload);
|
|
197
|
+
}
|
|
198
|
+
} else if (msg.type === 'context_compaction') {
|
|
199
|
+
if (msg.status === 'started') {
|
|
200
|
+
cache.isCompacting = true;
|
|
201
|
+
cache.messages.push({
|
|
202
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
203
|
+
content: 'Context compacting...', isCompactStart: true,
|
|
204
|
+
timestamp: new Date(),
|
|
205
|
+
});
|
|
206
|
+
} else if (msg.status === 'completed') {
|
|
207
|
+
cache.isCompacting = false;
|
|
208
|
+
const startMsg = [...cache.messages].reverse().find(m => m.isCompactStart && !m.compactDone);
|
|
209
|
+
if (startMsg) {
|
|
210
|
+
startMsg.content = 'Context compacted';
|
|
211
|
+
startMsg.compactDone = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else if (msg.type === 'error') {
|
|
215
|
+
finalizeLastStreaming(cache.messages);
|
|
216
|
+
cache.messages.push({
|
|
217
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
218
|
+
content: msg.message, isError: true, timestamp: new Date(),
|
|
219
|
+
});
|
|
220
|
+
cache.isProcessing = false;
|
|
221
|
+
cache.isCompacting = false;
|
|
222
|
+
processingConversations.value[convId] = false;
|
|
223
|
+
} else if (msg.type === 'command_output') {
|
|
224
|
+
finalizeLastStreaming(cache.messages);
|
|
225
|
+
cache.messages.push({
|
|
226
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
227
|
+
content: msg.content, isCommandOutput: true, timestamp: new Date(),
|
|
228
|
+
});
|
|
229
|
+
} else if (msg.type === 'ask_user_question') {
|
|
230
|
+
const msgs = cache.messages;
|
|
231
|
+
finalizeLastStreaming(msgs);
|
|
232
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
233
|
+
const m = msgs[i];
|
|
234
|
+
if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
|
|
235
|
+
msgs.splice(i, 1);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (m.role === 'user') break;
|
|
239
|
+
}
|
|
240
|
+
const questions = msg.questions || [];
|
|
241
|
+
const selectedAnswers = {};
|
|
242
|
+
const customTexts = {};
|
|
243
|
+
for (let i = 0; i < questions.length; i++) {
|
|
244
|
+
selectedAnswers[i] = questions[i].multiSelect ? [] : null;
|
|
245
|
+
customTexts[i] = '';
|
|
246
|
+
}
|
|
247
|
+
msgs.push({
|
|
248
|
+
id: ++cache.messageIdCounter,
|
|
249
|
+
role: 'ask-question',
|
|
250
|
+
requestId: msg.requestId,
|
|
251
|
+
questions,
|
|
252
|
+
answered: false,
|
|
253
|
+
selectedAnswers,
|
|
254
|
+
customTexts,
|
|
255
|
+
timestamp: new Date(),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ── WebSocket connection, message routing, reconnection ──────────────────────
|
|
2
2
|
import { encrypt, decrypt, isEncrypted, decodeKey } from '../encryption.js';
|
|
3
3
|
import { isContextSummary } from './messageHelpers.js';
|
|
4
|
+
import { buildHistoryBatch, finalizeLastStreaming, routeToBackgroundConversation } from './backgroundRouting.js';
|
|
4
5
|
|
|
5
6
|
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
6
7
|
const RECONNECT_BASE_DELAY = 1000;
|
|
@@ -55,265 +56,7 @@ export function createConnection(deps) {
|
|
|
55
56
|
function clearToolMsgMap() { toolMsgMap.clear(); }
|
|
56
57
|
|
|
57
58
|
// ── Background conversation routing ──
|
|
58
|
-
//
|
|
59
|
-
// update its cached state directly (no streaming animation).
|
|
60
|
-
function routeToBackgroundConversation(convId, msg) {
|
|
61
|
-
const cache = conversationCache.value[convId];
|
|
62
|
-
if (!cache) return; // no cache entry — discard
|
|
63
|
-
|
|
64
|
-
if (msg.type === 'session_started') {
|
|
65
|
-
// Claude session ID captured for background conversation
|
|
66
|
-
cache.claudeSessionId = msg.claudeSessionId;
|
|
67
|
-
sidebar.requestSessionList();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (msg.type === 'conversation_resumed') {
|
|
72
|
-
cache.claudeSessionId = msg.claudeSessionId;
|
|
73
|
-
if (msg.history && Array.isArray(msg.history)) {
|
|
74
|
-
const batch = [];
|
|
75
|
-
for (const h of msg.history) {
|
|
76
|
-
if (h.role === 'user') {
|
|
77
|
-
if (isContextSummary(h.content)) {
|
|
78
|
-
batch.push({
|
|
79
|
-
id: ++cache.messageIdCounter, role: 'context-summary',
|
|
80
|
-
content: h.content, contextExpanded: false,
|
|
81
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
82
|
-
});
|
|
83
|
-
} else if (h.isCommandOutput) {
|
|
84
|
-
batch.push({
|
|
85
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
86
|
-
content: h.content, isCommandOutput: true,
|
|
87
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
88
|
-
});
|
|
89
|
-
} else {
|
|
90
|
-
batch.push({
|
|
91
|
-
id: ++cache.messageIdCounter, role: 'user',
|
|
92
|
-
content: h.content,
|
|
93
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
} else if (h.role === 'assistant') {
|
|
97
|
-
const last = batch[batch.length - 1];
|
|
98
|
-
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
99
|
-
last.content += '\n\n' + h.content;
|
|
100
|
-
} else {
|
|
101
|
-
batch.push({
|
|
102
|
-
id: ++cache.messageIdCounter, role: 'assistant',
|
|
103
|
-
content: h.content, isStreaming: false,
|
|
104
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
} else if (h.role === 'tool') {
|
|
108
|
-
batch.push({
|
|
109
|
-
id: ++cache.messageIdCounter, role: 'tool',
|
|
110
|
-
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
111
|
-
toolInput: h.toolInput || '', hasResult: true,
|
|
112
|
-
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'),
|
|
113
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
cache.messages = batch;
|
|
118
|
-
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
119
|
-
}
|
|
120
|
-
cache.loadingHistory = false;
|
|
121
|
-
if (msg.isCompacting) {
|
|
122
|
-
cache.isCompacting = true;
|
|
123
|
-
cache.isProcessing = true;
|
|
124
|
-
processingConversations.value[convId] = true;
|
|
125
|
-
cache.messages.push({
|
|
126
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
127
|
-
content: 'Context compacting...', isCompactStart: true,
|
|
128
|
-
timestamp: new Date(),
|
|
129
|
-
});
|
|
130
|
-
} else if (msg.isProcessing) {
|
|
131
|
-
cache.isProcessing = true;
|
|
132
|
-
processingConversations.value[convId] = true;
|
|
133
|
-
cache.messages.push({
|
|
134
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
135
|
-
content: 'Agent is processing...',
|
|
136
|
-
timestamp: new Date(),
|
|
137
|
-
});
|
|
138
|
-
} else {
|
|
139
|
-
cache.messages.push({
|
|
140
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
141
|
-
content: 'Session restored. You can continue the conversation.',
|
|
142
|
-
timestamp: new Date(),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (msg.type === 'claude_output') {
|
|
149
|
-
// Safety net: restore processing state if output arrives after reconnect
|
|
150
|
-
if (!cache.isProcessing) {
|
|
151
|
-
cache.isProcessing = true;
|
|
152
|
-
processingConversations.value[convId] = true;
|
|
153
|
-
}
|
|
154
|
-
const data = msg.data;
|
|
155
|
-
if (!data) return;
|
|
156
|
-
if (data.type === 'content_block_delta' && data.delta) {
|
|
157
|
-
// Append text to last assistant message (or create new one)
|
|
158
|
-
const msgs = cache.messages;
|
|
159
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
160
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
161
|
-
last.content += data.delta;
|
|
162
|
-
} else {
|
|
163
|
-
msgs.push({
|
|
164
|
-
id: ++cache.messageIdCounter, role: 'assistant',
|
|
165
|
-
content: data.delta, isStreaming: true, timestamp: new Date(),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
} else if (data.type === 'tool_use' && data.tools) {
|
|
169
|
-
// Finalize streaming message
|
|
170
|
-
const msgs = cache.messages;
|
|
171
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
172
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
173
|
-
last.isStreaming = false;
|
|
174
|
-
if (isContextSummary(last.content)) {
|
|
175
|
-
last.role = 'context-summary';
|
|
176
|
-
last.contextExpanded = false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
for (const tool of data.tools) {
|
|
180
|
-
const toolMsg = {
|
|
181
|
-
id: ++cache.messageIdCounter, role: 'tool',
|
|
182
|
-
toolId: tool.id, toolName: tool.name || 'unknown',
|
|
183
|
-
toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
|
|
184
|
-
hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite' || tool.name === 'Agent'),
|
|
185
|
-
timestamp: new Date(),
|
|
186
|
-
};
|
|
187
|
-
msgs.push(toolMsg);
|
|
188
|
-
if (tool.id) {
|
|
189
|
-
if (!cache.toolMsgMap) cache.toolMsgMap = new Map();
|
|
190
|
-
cache.toolMsgMap.set(tool.id, toolMsg);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else if (data.type === 'user' && data.tool_use_result) {
|
|
194
|
-
const result = data.tool_use_result;
|
|
195
|
-
const results = Array.isArray(result) ? result : [result];
|
|
196
|
-
const tMap = cache.toolMsgMap || new Map();
|
|
197
|
-
for (const r of results) {
|
|
198
|
-
const toolMsg = tMap.get(r.tool_use_id);
|
|
199
|
-
if (toolMsg) {
|
|
200
|
-
toolMsg.toolOutput = typeof r.content === 'string'
|
|
201
|
-
? r.content : JSON.stringify(r.content, null, 2);
|
|
202
|
-
toolMsg.hasResult = true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
|
|
207
|
-
// Finalize streaming message
|
|
208
|
-
const msgs = cache.messages;
|
|
209
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
210
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
211
|
-
last.isStreaming = false;
|
|
212
|
-
if (isContextSummary(last.content)) {
|
|
213
|
-
last.role = 'context-summary';
|
|
214
|
-
last.contextExpanded = false;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
cache.isProcessing = false;
|
|
218
|
-
cache.isCompacting = false;
|
|
219
|
-
if (msg.usage) cache.usageStats = msg.usage;
|
|
220
|
-
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
221
|
-
processingConversations.value[convId] = false;
|
|
222
|
-
if (msg.type === 'execution_cancelled') {
|
|
223
|
-
cache.needsResume = true;
|
|
224
|
-
cache.messages.push({
|
|
225
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
226
|
-
content: 'Generation stopped.', timestamp: new Date(),
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
sidebar.requestSessionList();
|
|
230
|
-
// Dequeue next message for this background conversation
|
|
231
|
-
if (cache.queuedMessages && cache.queuedMessages.length > 0) {
|
|
232
|
-
const queued = cache.queuedMessages.shift();
|
|
233
|
-
cache.messages.push({
|
|
234
|
-
id: ++cache.messageIdCounter, role: 'user', status: 'sent',
|
|
235
|
-
content: queued.content, attachments: queued.attachments,
|
|
236
|
-
timestamp: new Date(),
|
|
237
|
-
});
|
|
238
|
-
cache.isProcessing = true;
|
|
239
|
-
processingConversations.value[convId] = true;
|
|
240
|
-
wsSend(queued.payload);
|
|
241
|
-
}
|
|
242
|
-
} else if (msg.type === 'context_compaction') {
|
|
243
|
-
if (msg.status === 'started') {
|
|
244
|
-
cache.isCompacting = true;
|
|
245
|
-
cache.messages.push({
|
|
246
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
247
|
-
content: 'Context compacting...', isCompactStart: true,
|
|
248
|
-
timestamp: new Date(),
|
|
249
|
-
});
|
|
250
|
-
} else if (msg.status === 'completed') {
|
|
251
|
-
cache.isCompacting = false;
|
|
252
|
-
const startMsg = [...cache.messages].reverse().find(m => m.isCompactStart && !m.compactDone);
|
|
253
|
-
if (startMsg) {
|
|
254
|
-
startMsg.content = 'Context compacted';
|
|
255
|
-
startMsg.compactDone = true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
} else if (msg.type === 'error') {
|
|
259
|
-
// Finalize streaming
|
|
260
|
-
const msgs = cache.messages;
|
|
261
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
262
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
263
|
-
last.isStreaming = false;
|
|
264
|
-
}
|
|
265
|
-
cache.messages.push({
|
|
266
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
267
|
-
content: msg.message, isError: true, timestamp: new Date(),
|
|
268
|
-
});
|
|
269
|
-
cache.isProcessing = false;
|
|
270
|
-
cache.isCompacting = false;
|
|
271
|
-
processingConversations.value[convId] = false;
|
|
272
|
-
} else if (msg.type === 'command_output') {
|
|
273
|
-
const msgs = cache.messages;
|
|
274
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
275
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
276
|
-
last.isStreaming = false;
|
|
277
|
-
}
|
|
278
|
-
cache.messages.push({
|
|
279
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
280
|
-
content: msg.content, isCommandOutput: true, timestamp: new Date(),
|
|
281
|
-
});
|
|
282
|
-
} else if (msg.type === 'ask_user_question') {
|
|
283
|
-
// Finalize streaming
|
|
284
|
-
const msgs = cache.messages;
|
|
285
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
286
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
287
|
-
last.isStreaming = false;
|
|
288
|
-
}
|
|
289
|
-
// Remove AskUserQuestion tool msg
|
|
290
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
291
|
-
const m = msgs[i];
|
|
292
|
-
if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
|
|
293
|
-
msgs.splice(i, 1);
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
if (m.role === 'user') break;
|
|
297
|
-
}
|
|
298
|
-
const questions = msg.questions || [];
|
|
299
|
-
const selectedAnswers = {};
|
|
300
|
-
const customTexts = {};
|
|
301
|
-
for (let i = 0; i < questions.length; i++) {
|
|
302
|
-
selectedAnswers[i] = questions[i].multiSelect ? [] : null;
|
|
303
|
-
customTexts[i] = '';
|
|
304
|
-
}
|
|
305
|
-
msgs.push({
|
|
306
|
-
id: ++cache.messageIdCounter,
|
|
307
|
-
role: 'ask-question',
|
|
308
|
-
requestId: msg.requestId,
|
|
309
|
-
questions,
|
|
310
|
-
answered: false,
|
|
311
|
-
selectedAnswers,
|
|
312
|
-
customTexts,
|
|
313
|
-
timestamp: new Date(),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
59
|
+
// Delegated to backgroundRouting.js module.
|
|
317
60
|
|
|
318
61
|
function wsSend(msg) {
|
|
319
62
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -493,7 +236,7 @@ export function createConnection(deps) {
|
|
|
493
236
|
if (msg.conversationId && currentConversationId
|
|
494
237
|
&& currentConversationId.value
|
|
495
238
|
&& msg.conversationId !== currentConversationId.value) {
|
|
496
|
-
routeToBackgroundConversation(msg.conversationId, msg);
|
|
239
|
+
routeToBackgroundConversation({ conversationCache, processingConversations, sidebar, wsSend }, msg.conversationId, msg);
|
|
497
240
|
return;
|
|
498
241
|
}
|
|
499
242
|
|
|
@@ -738,49 +481,7 @@ export function createConnection(deps) {
|
|
|
738
481
|
} else if (msg.type === 'conversation_resumed') {
|
|
739
482
|
currentClaudeSessionId.value = msg.claudeSessionId;
|
|
740
483
|
if (msg.history && Array.isArray(msg.history)) {
|
|
741
|
-
|
|
742
|
-
for (const h of msg.history) {
|
|
743
|
-
if (h.role === 'user') {
|
|
744
|
-
if (isContextSummary(h.content)) {
|
|
745
|
-
batch.push({
|
|
746
|
-
id: streaming.nextId(), role: 'context-summary',
|
|
747
|
-
content: h.content, contextExpanded: false,
|
|
748
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
749
|
-
});
|
|
750
|
-
} else if (h.isCommandOutput) {
|
|
751
|
-
batch.push({
|
|
752
|
-
id: streaming.nextId(), role: 'system',
|
|
753
|
-
content: h.content, isCommandOutput: true,
|
|
754
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
755
|
-
});
|
|
756
|
-
} else {
|
|
757
|
-
batch.push({
|
|
758
|
-
id: streaming.nextId(), role: 'user',
|
|
759
|
-
content: h.content,
|
|
760
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
} else if (h.role === 'assistant') {
|
|
764
|
-
const last = batch[batch.length - 1];
|
|
765
|
-
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
766
|
-
last.content += '\n\n' + h.content;
|
|
767
|
-
} else {
|
|
768
|
-
batch.push({
|
|
769
|
-
id: streaming.nextId(), role: 'assistant',
|
|
770
|
-
content: h.content, isStreaming: false,
|
|
771
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
} else if (h.role === 'tool') {
|
|
775
|
-
batch.push({
|
|
776
|
-
id: streaming.nextId(), role: 'tool',
|
|
777
|
-
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
778
|
-
toolInput: h.toolInput || '', hasResult: true,
|
|
779
|
-
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'), timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
messages.value = batch;
|
|
484
|
+
messages.value = buildHistoryBatch(msg.history, () => streaming.nextId());
|
|
784
485
|
toolMsgMap.clear();
|
|
785
486
|
}
|
|
786
487
|
loadingHistory.value = false;
|