@agent-link/server 0.1.187 → 0.1.189
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/dist/auth-manager.d.ts +36 -0
- package/dist/auth-manager.js +96 -0
- package/dist/auth-manager.js.map +1 -0
- package/dist/http.d.ts +4 -0
- package/dist/http.js +85 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +5 -84
- package/dist/index.js.map +1 -1
- package/dist/message-relay.d.ts +17 -0
- package/dist/message-relay.js +23 -0
- package/dist/message-relay.js.map +1 -0
- package/dist/session-manager.d.ts +44 -0
- package/dist/session-manager.js +83 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/ws-agent.js +19 -27
- package/dist/ws-agent.js.map +1 -1
- package/dist/ws-client.js +31 -37
- package/dist/ws-client.js.map +1 -1
- package/package.json +3 -3
- package/web/dist/assets/index-DIO7Hox0.js +320 -0
- package/web/dist/assets/index-DIO7Hox0.js.map +1 -0
- package/web/dist/assets/index-Y1FN_mFe.css +1 -0
- package/web/{index.html → dist/index.html} +2 -19
- package/dist/auth.d.ts +0 -13
- package/dist/auth.js +0 -65
- package/dist/auth.js.map +0 -1
- package/dist/context.d.ts +0 -52
- package/dist/context.js +0 -60
- package/dist/context.js.map +0 -1
- package/web/app.js +0 -2881
- package/web/css/ask-question.css +0 -333
- package/web/css/base.css +0 -270
- package/web/css/btw.css +0 -148
- package/web/css/chat.css +0 -176
- package/web/css/file-browser.css +0 -499
- package/web/css/input.css +0 -671
- package/web/css/loop.css +0 -674
- package/web/css/markdown.css +0 -169
- package/web/css/responsive.css +0 -314
- package/web/css/sidebar.css +0 -593
- package/web/css/team.css +0 -1277
- package/web/css/tools.css +0 -327
- package/web/encryption.js +0 -56
- package/web/modules/appHelpers.js +0 -100
- package/web/modules/askQuestion.js +0 -63
- package/web/modules/backgroundRouting.js +0 -269
- package/web/modules/connection.js +0 -731
- package/web/modules/fileAttachments.js +0 -125
- package/web/modules/fileBrowser.js +0 -398
- package/web/modules/filePreview.js +0 -213
- package/web/modules/i18n.js +0 -101
- package/web/modules/loop.js +0 -338
- package/web/modules/loopTemplates.js +0 -110
- package/web/modules/markdown.js +0 -83
- package/web/modules/messageHelpers.js +0 -206
- package/web/modules/sidebar.js +0 -402
- package/web/modules/streaming.js +0 -116
- package/web/modules/team.js +0 -396
- package/web/modules/teamTemplates.js +0 -360
- package/web/vendor/highlight.min.js +0 -1213
- package/web/vendor/marked.min.js +0 -6
- package/web/vendor/nacl-fast.min.js +0 -1
- package/web/vendor/nacl-util.min.js +0 -1
- package/web/vendor/pako.min.js +0 -2
- package/web/vendor/vue.global.prod.js +0 -13
- /package/web/{favicon.svg → dist/favicon.svg} +0 -0
- /package/web/{images → dist/images}/chat-iPad.webp +0 -0
- /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
- /package/web/{images → dist/images}/loop-iPad.webp +0 -0
- /package/web/{images → dist/images}/team-iPad.webp +0 -0
- /package/web/{landing.html → dist/landing.html} +0 -0
- /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
- /package/web/{locales → dist/locales}/en.json +0 -0
- /package/web/{locales → dist/locales}/zh.json +0 -0
- /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
- /package/web/{vendor → dist/vendor}/github.min.css +0 -0
|
@@ -1,731 +0,0 @@
|
|
|
1
|
-
// ── WebSocket connection, message routing, reconnection ──────────────────────
|
|
2
|
-
import { encrypt, decrypt, isEncrypted, decodeKey } from '../encryption.js';
|
|
3
|
-
import { isContextSummary } from './messageHelpers.js';
|
|
4
|
-
import { buildHistoryBatch, finalizeLastStreaming, routeToBackgroundConversation } from './backgroundRouting.js';
|
|
5
|
-
|
|
6
|
-
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
7
|
-
const RECONNECT_BASE_DELAY = 1000;
|
|
8
|
-
const RECONNECT_MAX_DELAY = 15000;
|
|
9
|
-
|
|
10
|
-
function findLast(arr, predicate) {
|
|
11
|
-
for (let i = arr.length - 1; i >= 0; i--) {
|
|
12
|
-
if (predicate(arr[i])) return arr[i];
|
|
13
|
-
}
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Creates the WebSocket connection controller.
|
|
19
|
-
* @param {object} deps - All reactive state and callbacks needed
|
|
20
|
-
*/
|
|
21
|
-
export function createConnection(deps) {
|
|
22
|
-
const {
|
|
23
|
-
status, agentName, hostname, workDir, sessionId, error,
|
|
24
|
-
serverVersion, agentVersion, latency,
|
|
25
|
-
messages, isProcessing, isCompacting, visibleLimit, queuedMessages, usageStats,
|
|
26
|
-
historySessions, currentClaudeSessionId, needsResume, loadingSessions, loadingHistory,
|
|
27
|
-
folderPickerLoading, folderPickerEntries, folderPickerPath,
|
|
28
|
-
authRequired, authPassword, authError, authAttempts, authLocked,
|
|
29
|
-
streaming, sidebar,
|
|
30
|
-
scrollToBottom,
|
|
31
|
-
workdirSwitching,
|
|
32
|
-
// Multi-session parallel
|
|
33
|
-
currentConversationId, processingConversations, conversationCache,
|
|
34
|
-
switchConversation,
|
|
35
|
-
// Memory management
|
|
36
|
-
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
37
|
-
// Side question (/btw)
|
|
38
|
-
btwState, btwPending,
|
|
39
|
-
// Plan mode
|
|
40
|
-
setPlanMode,
|
|
41
|
-
// i18n
|
|
42
|
-
t,
|
|
43
|
-
} = deps;
|
|
44
|
-
|
|
45
|
-
// Dequeue callback — set after creation to resolve circular dependency
|
|
46
|
-
let _dequeueNext = () => {};
|
|
47
|
-
function setDequeueNext(fn) { _dequeueNext = fn; }
|
|
48
|
-
|
|
49
|
-
// File browser — set after creation to resolve circular dependency
|
|
50
|
-
let fileBrowser = null;
|
|
51
|
-
function setFileBrowser(fb) { fileBrowser = fb; }
|
|
52
|
-
|
|
53
|
-
// File preview — set after creation to resolve circular dependency
|
|
54
|
-
let filePreview = null;
|
|
55
|
-
function setFilePreview(fp) { filePreview = fp; }
|
|
56
|
-
|
|
57
|
-
// Team module — set after creation to resolve circular dependency
|
|
58
|
-
let team = null;
|
|
59
|
-
function setTeam(t) { team = t; }
|
|
60
|
-
|
|
61
|
-
// Loop module — set after creation to resolve circular dependency
|
|
62
|
-
let loop = null;
|
|
63
|
-
function setLoop(l) { loop = l; }
|
|
64
|
-
|
|
65
|
-
let ws = null;
|
|
66
|
-
let sessionKey = null;
|
|
67
|
-
let reconnectAttempts = 0;
|
|
68
|
-
let reconnectTimer = null;
|
|
69
|
-
let pingTimer = null;
|
|
70
|
-
let idleCheckTimer = null;
|
|
71
|
-
const toolMsgMap = new Map(); // toolId -> message (for fast tool_result lookup)
|
|
72
|
-
|
|
73
|
-
// ── toolMsgMap save/restore for conversation switching ──
|
|
74
|
-
function getToolMsgMap() { return new Map(toolMsgMap); }
|
|
75
|
-
function restoreToolMsgMap(map) { toolMsgMap.clear(); for (const [k, v] of map) toolMsgMap.set(k, v); }
|
|
76
|
-
function clearToolMsgMap() { toolMsgMap.clear(); }
|
|
77
|
-
|
|
78
|
-
// ── Background conversation routing ──
|
|
79
|
-
// Delegated to backgroundRouting.js module.
|
|
80
|
-
|
|
81
|
-
function wsSend(msg) {
|
|
82
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
83
|
-
if (sessionKey) {
|
|
84
|
-
const encrypted = encrypt(msg, sessionKey);
|
|
85
|
-
ws.send(JSON.stringify(encrypted));
|
|
86
|
-
} else {
|
|
87
|
-
ws.send(JSON.stringify(msg));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function startPing() {
|
|
92
|
-
stopPing();
|
|
93
|
-
// Send first ping immediately, then every 10s
|
|
94
|
-
wsSend({ type: 'ping', ts: Date.now() });
|
|
95
|
-
pingTimer = setInterval(() => {
|
|
96
|
-
wsSend({ type: 'ping', ts: Date.now() });
|
|
97
|
-
}, 10000);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function stopPing() {
|
|
101
|
-
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
|
|
102
|
-
latency.value = null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Idle-check: if isProcessing stays true with no claude_output for 15s,
|
|
106
|
-
// poll the agent to reconcile stale state (guards against lost turn_completed).
|
|
107
|
-
const IDLE_CHECK_MS = 15000;
|
|
108
|
-
function resetIdleCheck() {
|
|
109
|
-
if (idleCheckTimer) { clearTimeout(idleCheckTimer); idleCheckTimer = null; }
|
|
110
|
-
if (isProcessing.value) {
|
|
111
|
-
idleCheckTimer = setTimeout(() => {
|
|
112
|
-
idleCheckTimer = null;
|
|
113
|
-
if (isProcessing.value) wsSend({ type: 'query_active_conversations' });
|
|
114
|
-
}, IDLE_CHECK_MS);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function clearIdleCheck() {
|
|
118
|
-
if (idleCheckTimer) { clearTimeout(idleCheckTimer); idleCheckTimer = null; }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function getSessionId() {
|
|
122
|
-
const match = window.location.pathname.match(/^\/s\/([^/]+)/);
|
|
123
|
-
return match ? match[1] : null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function finalizeStreamingMsg(scheduleHighlight) {
|
|
127
|
-
const sid = streaming.getStreamingMessageId();
|
|
128
|
-
if (sid === null) return;
|
|
129
|
-
const streamMsg = messages.value.find(m => m.id === sid);
|
|
130
|
-
if (streamMsg) {
|
|
131
|
-
streamMsg.isStreaming = false;
|
|
132
|
-
if (isContextSummary(streamMsg.content)) {
|
|
133
|
-
streamMsg.role = 'context-summary';
|
|
134
|
-
streamMsg.contextExpanded = false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
streaming.setStreamingMessageId(null);
|
|
138
|
-
if (scheduleHighlight) scheduleHighlight();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function handleClaudeOutput(msg, scheduleHighlight) {
|
|
142
|
-
const data = msg.data;
|
|
143
|
-
if (!data) return;
|
|
144
|
-
|
|
145
|
-
// Safety net: if streaming output arrives but isProcessing is false
|
|
146
|
-
// (e.g. after reconnect before active_conversations response), self-correct
|
|
147
|
-
if (!isProcessing.value) {
|
|
148
|
-
isProcessing.value = true;
|
|
149
|
-
resetIdleCheck();
|
|
150
|
-
if (currentConversationId && currentConversationId.value) {
|
|
151
|
-
processingConversations.value[currentConversationId.value] = true;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (data.type === 'content_block_delta' && data.delta) {
|
|
156
|
-
streaming.appendPending(data.delta);
|
|
157
|
-
streaming.startReveal();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (data.type === 'tool_use' && data.tools) {
|
|
162
|
-
streaming.flushReveal();
|
|
163
|
-
finalizeStreamingMsg(scheduleHighlight);
|
|
164
|
-
|
|
165
|
-
for (const tool of data.tools) {
|
|
166
|
-
const toolMsg = {
|
|
167
|
-
id: streaming.nextId(), role: 'tool',
|
|
168
|
-
toolId: tool.id, toolName: tool.name || 'unknown',
|
|
169
|
-
toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
|
|
170
|
-
hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite' || tool.name === 'Agent'), timestamp: new Date(),
|
|
171
|
-
};
|
|
172
|
-
messages.value.push(toolMsg);
|
|
173
|
-
if (tool.id) toolMsgMap.set(tool.id, toolMsg);
|
|
174
|
-
}
|
|
175
|
-
scrollToBottom();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (data.type === 'user' && data.tool_use_result) {
|
|
180
|
-
const result = data.tool_use_result;
|
|
181
|
-
const results = Array.isArray(result) ? result : [result];
|
|
182
|
-
for (const r of results) {
|
|
183
|
-
const toolMsg = toolMsgMap.get(r.tool_use_id);
|
|
184
|
-
if (toolMsg) {
|
|
185
|
-
toolMsg.toolOutput = typeof r.content === 'string'
|
|
186
|
-
? r.content : JSON.stringify(r.content, null, 2);
|
|
187
|
-
toolMsg.hasResult = true;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
scrollToBottom();
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function connect(scheduleHighlight) {
|
|
196
|
-
const sid = getSessionId();
|
|
197
|
-
if (!sid) {
|
|
198
|
-
status.value = 'No Session';
|
|
199
|
-
error.value = t('error.noSessionId');
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
sessionId.value = sid;
|
|
203
|
-
status.value = 'Connecting...';
|
|
204
|
-
error.value = '';
|
|
205
|
-
|
|
206
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
207
|
-
let wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
|
|
208
|
-
// Include saved auth token for automatic re-authentication
|
|
209
|
-
const savedToken = localStorage.getItem(`agentlink-auth-${sid}`);
|
|
210
|
-
if (savedToken) {
|
|
211
|
-
wsUrl += `&authToken=${encodeURIComponent(savedToken)}`;
|
|
212
|
-
}
|
|
213
|
-
ws = new WebSocket(wsUrl);
|
|
214
|
-
|
|
215
|
-
ws.onopen = () => { error.value = ''; reconnectAttempts = 0; };
|
|
216
|
-
|
|
217
|
-
ws.onmessage = (event) => {
|
|
218
|
-
let msg;
|
|
219
|
-
const parsed = JSON.parse(event.data);
|
|
220
|
-
|
|
221
|
-
// Auth messages are always plaintext (before session key exchange)
|
|
222
|
-
if (parsed.type === 'auth_required') {
|
|
223
|
-
authRequired.value = true;
|
|
224
|
-
authError.value = '';
|
|
225
|
-
authLocked.value = false;
|
|
226
|
-
status.value = 'Authentication Required';
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (parsed.type === 'auth_failed') {
|
|
230
|
-
authError.value = parsed.message || t('error.incorrectPassword');
|
|
231
|
-
authAttempts.value = parsed.attemptsRemaining != null
|
|
232
|
-
? t('error.attemptsRemaining', { n: parsed.attemptsRemaining })
|
|
233
|
-
: null;
|
|
234
|
-
authPassword.value = '';
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (parsed.type === 'auth_locked') {
|
|
238
|
-
authLocked.value = true;
|
|
239
|
-
authRequired.value = false;
|
|
240
|
-
authError.value = parsed.message || t('error.tooManyAttempts');
|
|
241
|
-
status.value = 'Locked';
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (parsed.type === 'connected') {
|
|
246
|
-
msg = parsed;
|
|
247
|
-
if (typeof parsed.sessionKey === 'string') {
|
|
248
|
-
sessionKey = decodeKey(parsed.sessionKey);
|
|
249
|
-
}
|
|
250
|
-
} else if (sessionKey && isEncrypted(parsed)) {
|
|
251
|
-
msg = decrypt(parsed, sessionKey);
|
|
252
|
-
if (!msg) {
|
|
253
|
-
console.error('[WS] Failed to decrypt message');
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
msg = parsed;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Team messages: route before normal conversation routing ──
|
|
261
|
-
if (team && (msg.type?.startsWith('team_') || msg.type === 'teams_list' || (msg.type === 'claude_output' && msg.teamId))) {
|
|
262
|
-
if (msg.type === 'claude_output' && msg.teamId) {
|
|
263
|
-
team.handleTeamAgentOutput(msg);
|
|
264
|
-
} else {
|
|
265
|
-
team.handleTeamMessage(msg);
|
|
266
|
-
}
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ── Loop messages: route before normal conversation routing ──
|
|
271
|
-
if (loop && (msg.type?.startsWith('loop_') || msg.type === 'loops_list')) {
|
|
272
|
-
loop.handleLoopMessage(msg);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ── Multi-session: route messages to background conversations ──
|
|
277
|
-
// Messages with a conversationId that doesn't match the current foreground
|
|
278
|
-
// conversation are routed to their cached background state.
|
|
279
|
-
if (msg.conversationId && currentConversationId
|
|
280
|
-
&& currentConversationId.value
|
|
281
|
-
&& msg.conversationId !== currentConversationId.value) {
|
|
282
|
-
routeToBackgroundConversation({ conversationCache, processingConversations, sidebar, wsSend }, msg.conversationId, msg);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (msg.type === 'connected') {
|
|
287
|
-
// Reset auth state
|
|
288
|
-
authRequired.value = false;
|
|
289
|
-
authPassword.value = '';
|
|
290
|
-
authError.value = '';
|
|
291
|
-
authAttempts.value = null;
|
|
292
|
-
authLocked.value = false;
|
|
293
|
-
// Save auth token for automatic re-authentication
|
|
294
|
-
if (msg.authToken) {
|
|
295
|
-
localStorage.setItem(`agentlink-auth-${sessionId.value}`, msg.authToken);
|
|
296
|
-
}
|
|
297
|
-
if (msg.serverVersion) serverVersion.value = msg.serverVersion;
|
|
298
|
-
if (msg.agent) {
|
|
299
|
-
status.value = 'Connected';
|
|
300
|
-
agentName.value = msg.agent.name;
|
|
301
|
-
hostname.value = msg.agent.hostname || '';
|
|
302
|
-
workDir.value = msg.agent.workDir;
|
|
303
|
-
agentVersion.value = msg.agent.version || '';
|
|
304
|
-
sidebar.loadWorkdirHistory();
|
|
305
|
-
sidebar.addToWorkdirHistory(msg.agent.workDir);
|
|
306
|
-
const savedDir = localStorage.getItem(`agentlink-workdir-${sessionId.value}`);
|
|
307
|
-
if (savedDir && savedDir !== msg.agent.workDir) {
|
|
308
|
-
workdirSwitching.value = true;
|
|
309
|
-
setTimeout(() => { workdirSwitching.value = false; }, 10000);
|
|
310
|
-
wsSend({ type: 'change_workdir', workDir: savedDir });
|
|
311
|
-
}
|
|
312
|
-
sidebar.requestSessionList();
|
|
313
|
-
if (team) team.requestTeamsList();
|
|
314
|
-
if (loop) loop.requestLoopsList();
|
|
315
|
-
startPing();
|
|
316
|
-
wsSend({ type: 'query_active_conversations' });
|
|
317
|
-
} else {
|
|
318
|
-
status.value = 'Waiting';
|
|
319
|
-
error.value = t('error.agentNotConnected');
|
|
320
|
-
}
|
|
321
|
-
} else if (msg.type === 'pong') {
|
|
322
|
-
if (typeof msg.ts === 'number') {
|
|
323
|
-
latency.value = Date.now() - msg.ts;
|
|
324
|
-
}
|
|
325
|
-
} else if (msg.type === 'agent_disconnected') {
|
|
326
|
-
stopPing();
|
|
327
|
-
status.value = 'Waiting';
|
|
328
|
-
agentName.value = '';
|
|
329
|
-
hostname.value = '';
|
|
330
|
-
error.value = t('error.agentDisconnected');
|
|
331
|
-
isProcessing.value = false;
|
|
332
|
-
isCompacting.value = false;
|
|
333
|
-
queuedMessages.value = [];
|
|
334
|
-
loadingSessions.value = false;
|
|
335
|
-
// Clear processing state for all background conversations
|
|
336
|
-
if (conversationCache) {
|
|
337
|
-
for (const [convId, cached] of Object.entries(conversationCache.value)) {
|
|
338
|
-
cached.isProcessing = false;
|
|
339
|
-
cached.isCompacting = false;
|
|
340
|
-
processingConversations.value[convId] = false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (currentConversationId && currentConversationId.value) {
|
|
344
|
-
processingConversations.value[currentConversationId.value] = false;
|
|
345
|
-
}
|
|
346
|
-
} else if (msg.type === 'agent_reconnected') {
|
|
347
|
-
status.value = 'Connected';
|
|
348
|
-
error.value = '';
|
|
349
|
-
if (msg.agent) {
|
|
350
|
-
agentName.value = msg.agent.name;
|
|
351
|
-
hostname.value = msg.agent.hostname || '';
|
|
352
|
-
workDir.value = msg.agent.workDir;
|
|
353
|
-
agentVersion.value = msg.agent.version || '';
|
|
354
|
-
workDir.value = msg.agent.workDir;
|
|
355
|
-
sidebar.addToWorkdirHistory(msg.agent.workDir);
|
|
356
|
-
}
|
|
357
|
-
sidebar.requestSessionList();
|
|
358
|
-
if (team) team.requestTeamsList();
|
|
359
|
-
if (loop) loop.requestLoopsList();
|
|
360
|
-
startPing();
|
|
361
|
-
wsSend({ type: 'query_active_conversations' });
|
|
362
|
-
} else if (msg.type === 'active_conversations') {
|
|
363
|
-
// Agent's response is authoritative — first clear all processing state,
|
|
364
|
-
// then re-apply only for conversations the agent reports as active.
|
|
365
|
-
// This corrects any stale isProcessing=true left by the safety net or
|
|
366
|
-
// from turns that finished while the socket was down.
|
|
367
|
-
const activeSet = new Set();
|
|
368
|
-
const convs = msg.conversations || [];
|
|
369
|
-
for (const entry of convs) {
|
|
370
|
-
if (entry.conversationId) activeSet.add(entry.conversationId);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Clear foreground
|
|
374
|
-
const wasForegroundProcessing = isProcessing.value;
|
|
375
|
-
if (!activeSet.has(currentConversationId && currentConversationId.value)) {
|
|
376
|
-
isProcessing.value = false;
|
|
377
|
-
isCompacting.value = false;
|
|
378
|
-
}
|
|
379
|
-
// Clear all cached background conversations
|
|
380
|
-
if (conversationCache) {
|
|
381
|
-
for (const [convId, cached] of Object.entries(conversationCache.value)) {
|
|
382
|
-
if (!activeSet.has(convId)) {
|
|
383
|
-
cached.isProcessing = false;
|
|
384
|
-
cached.isCompacting = false;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// Clear processingConversations map
|
|
389
|
-
if (processingConversations) {
|
|
390
|
-
for (const convId of Object.keys(processingConversations.value)) {
|
|
391
|
-
if (!activeSet.has(convId)) {
|
|
392
|
-
processingConversations.value[convId] = false;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Now set state for actually active conversations
|
|
398
|
-
for (const entry of convs) {
|
|
399
|
-
const convId = entry.conversationId;
|
|
400
|
-
if (!convId) continue;
|
|
401
|
-
if (currentConversationId && currentConversationId.value === convId) {
|
|
402
|
-
// Foreground conversation
|
|
403
|
-
isProcessing.value = true;
|
|
404
|
-
isCompacting.value = !!entry.isCompacting;
|
|
405
|
-
} else if (conversationCache && conversationCache.value[convId]) {
|
|
406
|
-
// Background conversation
|
|
407
|
-
const cached = conversationCache.value[convId];
|
|
408
|
-
cached.isProcessing = true;
|
|
409
|
-
cached.isCompacting = !!entry.isCompacting;
|
|
410
|
-
}
|
|
411
|
-
if (processingConversations) {
|
|
412
|
-
processingConversations.value[convId] = true;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Restore active team state on reconnect
|
|
417
|
-
if (team && msg.activeTeam) {
|
|
418
|
-
team.handleActiveTeamRestore(msg.activeTeam, workDir.value);
|
|
419
|
-
}
|
|
420
|
-
resetIdleCheck();
|
|
421
|
-
// If foreground was processing but no longer is, dequeue pending messages
|
|
422
|
-
if (wasForegroundProcessing && !isProcessing.value) _dequeueNext();
|
|
423
|
-
} else if (msg.type === 'error') {
|
|
424
|
-
// Route btw-related errors to the overlay instead of the message list
|
|
425
|
-
if (btwPending && btwPending.value && msg.message && msg.message.includes('btw_question')) {
|
|
426
|
-
btwPending.value = false;
|
|
427
|
-
if (btwState && btwState.value) {
|
|
428
|
-
btwState.value.error = msg.message;
|
|
429
|
-
btwState.value.done = true;
|
|
430
|
-
}
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
streaming.flushReveal();
|
|
434
|
-
finalizeStreamingMsg(scheduleHighlight);
|
|
435
|
-
messages.value.push({
|
|
436
|
-
id: streaming.nextId(), role: 'system',
|
|
437
|
-
content: msg.message, isError: true,
|
|
438
|
-
timestamp: new Date(),
|
|
439
|
-
});
|
|
440
|
-
scrollToBottom();
|
|
441
|
-
isProcessing.value = false;
|
|
442
|
-
isCompacting.value = false;
|
|
443
|
-
loadingSessions.value = false;
|
|
444
|
-
clearIdleCheck();
|
|
445
|
-
if (currentConversationId && currentConversationId.value) {
|
|
446
|
-
processingConversations.value[currentConversationId.value] = false;
|
|
447
|
-
}
|
|
448
|
-
// Forward error to Loop module for inline display
|
|
449
|
-
if (loop && loop.loopError) {
|
|
450
|
-
loop.loopError.value = msg.message || '';
|
|
451
|
-
}
|
|
452
|
-
_dequeueNext();
|
|
453
|
-
} else if (msg.type === 'claude_output') {
|
|
454
|
-
handleClaudeOutput(msg, scheduleHighlight);
|
|
455
|
-
resetIdleCheck();
|
|
456
|
-
} else if (msg.type === 'session_started') {
|
|
457
|
-
// Claude session ID captured — update and refresh sidebar
|
|
458
|
-
currentClaudeSessionId.value = msg.claudeSessionId;
|
|
459
|
-
sidebar.requestSessionList();
|
|
460
|
-
} else if (msg.type === 'command_output') {
|
|
461
|
-
streaming.flushReveal();
|
|
462
|
-
finalizeStreamingMsg(scheduleHighlight);
|
|
463
|
-
messages.value.push({
|
|
464
|
-
id: streaming.nextId(), role: 'system',
|
|
465
|
-
content: msg.content, isCommandOutput: true,
|
|
466
|
-
timestamp: new Date(),
|
|
467
|
-
});
|
|
468
|
-
scrollToBottom();
|
|
469
|
-
} else if (msg.type === 'context_compaction') {
|
|
470
|
-
if (msg.status === 'started') {
|
|
471
|
-
isCompacting.value = true;
|
|
472
|
-
messages.value.push({
|
|
473
|
-
id: streaming.nextId(), role: 'system',
|
|
474
|
-
content: t('system.contextCompacting'), isCompactStart: true,
|
|
475
|
-
timestamp: new Date(),
|
|
476
|
-
});
|
|
477
|
-
scrollToBottom();
|
|
478
|
-
} else if (msg.status === 'completed') {
|
|
479
|
-
isCompacting.value = false;
|
|
480
|
-
// Update the start message to show completed
|
|
481
|
-
const startMsg = findLast(messages.value, m => m.isCompactStart && !m.compactDone);
|
|
482
|
-
if (startMsg) {
|
|
483
|
-
startMsg.content = t('system.contextCompacted');
|
|
484
|
-
startMsg.compactDone = true;
|
|
485
|
-
}
|
|
486
|
-
scrollToBottom();
|
|
487
|
-
}
|
|
488
|
-
} else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
|
|
489
|
-
streaming.flushReveal();
|
|
490
|
-
finalizeStreamingMsg(scheduleHighlight);
|
|
491
|
-
isProcessing.value = false;
|
|
492
|
-
isCompacting.value = false;
|
|
493
|
-
clearIdleCheck();
|
|
494
|
-
toolMsgMap.clear();
|
|
495
|
-
if (msg.usage) usageStats.value = msg.usage;
|
|
496
|
-
if (currentConversationId && currentConversationId.value) {
|
|
497
|
-
processingConversations.value[currentConversationId.value] = false;
|
|
498
|
-
}
|
|
499
|
-
if (msg.type === 'execution_cancelled') {
|
|
500
|
-
needsResume.value = true;
|
|
501
|
-
messages.value.push({
|
|
502
|
-
id: streaming.nextId(), role: 'system',
|
|
503
|
-
content: t('system.generationStopped'), timestamp: new Date(),
|
|
504
|
-
});
|
|
505
|
-
scrollToBottom();
|
|
506
|
-
}
|
|
507
|
-
sidebar.requestSessionList();
|
|
508
|
-
_dequeueNext();
|
|
509
|
-
} else if (msg.type === 'ask_user_question') {
|
|
510
|
-
streaming.flushReveal();
|
|
511
|
-
finalizeStreamingMsg(scheduleHighlight);
|
|
512
|
-
for (let i = messages.value.length - 1; i >= 0; i--) {
|
|
513
|
-
const m = messages.value[i];
|
|
514
|
-
if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
|
|
515
|
-
messages.value.splice(i, 1);
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
if (m.role === 'user') break;
|
|
519
|
-
}
|
|
520
|
-
const questions = msg.questions || [];
|
|
521
|
-
const selectedAnswers = {};
|
|
522
|
-
const customTexts = {};
|
|
523
|
-
for (let i = 0; i < questions.length; i++) {
|
|
524
|
-
selectedAnswers[i] = questions[i].multiSelect ? [] : null;
|
|
525
|
-
customTexts[i] = '';
|
|
526
|
-
}
|
|
527
|
-
messages.value.push({
|
|
528
|
-
id: streaming.nextId(),
|
|
529
|
-
role: 'ask-question',
|
|
530
|
-
requestId: msg.requestId,
|
|
531
|
-
questions,
|
|
532
|
-
answered: false,
|
|
533
|
-
selectedAnswers,
|
|
534
|
-
customTexts,
|
|
535
|
-
timestamp: new Date(),
|
|
536
|
-
});
|
|
537
|
-
scrollToBottom();
|
|
538
|
-
} else if (msg.type === 'sessions_list') {
|
|
539
|
-
historySessions.value = msg.sessions || [];
|
|
540
|
-
loadingSessions.value = false;
|
|
541
|
-
} else if (msg.type === 'session_deleted') {
|
|
542
|
-
historySessions.value = historySessions.value.filter(s => s.sessionId !== msg.sessionId);
|
|
543
|
-
} else if (msg.type === 'session_renamed') {
|
|
544
|
-
const session = historySessions.value.find(s => s.sessionId === msg.sessionId);
|
|
545
|
-
if (session) session.title = msg.newTitle;
|
|
546
|
-
} else if (msg.type === 'conversation_resumed') {
|
|
547
|
-
currentClaudeSessionId.value = msg.claudeSessionId;
|
|
548
|
-
if (msg.history && Array.isArray(msg.history)) {
|
|
549
|
-
messages.value = buildHistoryBatch(msg.history, () => streaming.nextId());
|
|
550
|
-
toolMsgMap.clear();
|
|
551
|
-
}
|
|
552
|
-
// Detect plan mode from agent-provided flag
|
|
553
|
-
if (msg.planMode != null) {
|
|
554
|
-
if (setPlanMode) setPlanMode(!!msg.planMode);
|
|
555
|
-
}
|
|
556
|
-
loadingHistory.value = false;
|
|
557
|
-
// Restore live status from agent (compacting / processing)
|
|
558
|
-
if (msg.isCompacting) {
|
|
559
|
-
isCompacting.value = true;
|
|
560
|
-
isProcessing.value = true;
|
|
561
|
-
messages.value.push({
|
|
562
|
-
id: streaming.nextId(), role: 'system',
|
|
563
|
-
content: t('system.contextCompacting'), isCompactStart: true,
|
|
564
|
-
timestamp: new Date(),
|
|
565
|
-
});
|
|
566
|
-
} else if (msg.isProcessing) {
|
|
567
|
-
isProcessing.value = true;
|
|
568
|
-
messages.value.push({
|
|
569
|
-
id: streaming.nextId(), role: 'system',
|
|
570
|
-
content: t('system.agentProcessing'),
|
|
571
|
-
timestamp: new Date(),
|
|
572
|
-
});
|
|
573
|
-
} else {
|
|
574
|
-
messages.value.push({
|
|
575
|
-
id: streaming.nextId(), role: 'system',
|
|
576
|
-
content: t('system.sessionRestored'),
|
|
577
|
-
timestamp: new Date(),
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
scrollToBottom();
|
|
581
|
-
} else if (msg.type === 'directory_listing') {
|
|
582
|
-
if (msg.source === 'file_browser' && fileBrowser) {
|
|
583
|
-
fileBrowser.handleDirectoryListing(msg);
|
|
584
|
-
} else {
|
|
585
|
-
folderPickerLoading.value = false;
|
|
586
|
-
folderPickerEntries.value = (msg.entries || [])
|
|
587
|
-
.filter(e => e.type === 'directory')
|
|
588
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
589
|
-
if (msg.dirPath != null) folderPickerPath.value = msg.dirPath;
|
|
590
|
-
}
|
|
591
|
-
} else if (msg.type === 'file_content') {
|
|
592
|
-
if (filePreview) filePreview.handleFileContent(msg);
|
|
593
|
-
} else if (msg.type === 'memory_list') {
|
|
594
|
-
memoryLoading.value = false;
|
|
595
|
-
memoryFiles.value = msg.files || [];
|
|
596
|
-
memoryDir.value = msg.memoryDir || null;
|
|
597
|
-
} else if (msg.type === 'memory_updated') {
|
|
598
|
-
memorySaving.value = false;
|
|
599
|
-
if (msg.success) {
|
|
600
|
-
memoryEditing.value = false;
|
|
601
|
-
memoryEditContent.value = '';
|
|
602
|
-
// Refresh list and preview
|
|
603
|
-
wsSend({ type: 'list_memory' });
|
|
604
|
-
if (filePreview) filePreview.refreshPreview();
|
|
605
|
-
}
|
|
606
|
-
} else if (msg.type === 'memory_deleted') {
|
|
607
|
-
if (msg.success) {
|
|
608
|
-
memoryFiles.value = memoryFiles.value.filter(f => f.name !== msg.filename);
|
|
609
|
-
// Close preview if open (might be showing the deleted file)
|
|
610
|
-
if (filePreview) filePreview.closePreview();
|
|
611
|
-
}
|
|
612
|
-
} else if (msg.type === 'btw_answer') {
|
|
613
|
-
if (btwPending) btwPending.value = false;
|
|
614
|
-
if (btwState && btwState.value) {
|
|
615
|
-
btwState.value.answer += msg.delta;
|
|
616
|
-
if (msg.done) {
|
|
617
|
-
btwState.value.done = true;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
} else if (msg.type === 'plan_mode_changed') {
|
|
621
|
-
if (setPlanMode) setPlanMode(msg.enabled);
|
|
622
|
-
// For the immediate path (no injected turn), clear isProcessing here
|
|
623
|
-
// because turn_completed will never arrive.
|
|
624
|
-
if (msg.immediate) {
|
|
625
|
-
isProcessing.value = false;
|
|
626
|
-
if (currentConversationId.value) {
|
|
627
|
-
processingConversations.value[currentConversationId.value] = false;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
// For the injected path, turn_completed handles isProcessing naturally.
|
|
631
|
-
} else if (msg.type === 'workdir_changed') {
|
|
632
|
-
workdirSwitching.value = false;
|
|
633
|
-
workDir.value = msg.workDir;
|
|
634
|
-
localStorage.setItem(`agentlink-workdir-${sessionId.value}`, msg.workDir);
|
|
635
|
-
sidebar.addToWorkdirHistory(msg.workDir);
|
|
636
|
-
if (fileBrowser) fileBrowser.onWorkdirChanged();
|
|
637
|
-
if (filePreview) filePreview.onWorkdirChanged();
|
|
638
|
-
|
|
639
|
-
// Multi-session: switch to a new blank conversation for the new workdir.
|
|
640
|
-
// Background conversations keep running and receiving output in their cache.
|
|
641
|
-
if (switchConversation) {
|
|
642
|
-
const newConvId = crypto.randomUUID();
|
|
643
|
-
switchConversation(newConvId);
|
|
644
|
-
} else {
|
|
645
|
-
// Fallback for old code path (no switchConversation)
|
|
646
|
-
messages.value = [];
|
|
647
|
-
queuedMessages.value = [];
|
|
648
|
-
toolMsgMap.clear();
|
|
649
|
-
visibleLimit.value = 50;
|
|
650
|
-
streaming.setMessageIdCounter(0);
|
|
651
|
-
streaming.setStreamingMessageId(null);
|
|
652
|
-
streaming.reset();
|
|
653
|
-
currentClaudeSessionId.value = null;
|
|
654
|
-
isProcessing.value = false;
|
|
655
|
-
}
|
|
656
|
-
messages.value.push({
|
|
657
|
-
id: streaming.nextId(), role: 'system',
|
|
658
|
-
content: t('system.workdirChanged', { dir: msg.workDir }),
|
|
659
|
-
timestamp: new Date(),
|
|
660
|
-
});
|
|
661
|
-
// Clear old history immediately so UI doesn't show stale data
|
|
662
|
-
historySessions.value = [];
|
|
663
|
-
if (team) {
|
|
664
|
-
team.teamsList.value = [];
|
|
665
|
-
team.teamState.value = null;
|
|
666
|
-
team.historicalTeam.value = null;
|
|
667
|
-
if (team.viewMode.value === 'team') {
|
|
668
|
-
team.viewMode.value = 'chat';
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (loop) loop.loopsList.value = [];
|
|
672
|
-
memoryFiles.value = [];
|
|
673
|
-
memoryDir.value = null;
|
|
674
|
-
memoryPanelOpen.value = false;
|
|
675
|
-
memoryEditing.value = false;
|
|
676
|
-
sidebar.requestSessionList();
|
|
677
|
-
if (team) team.requestTeamsList();
|
|
678
|
-
if (loop) loop.requestLoopsList();
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
ws.onclose = () => {
|
|
683
|
-
sessionKey = null;
|
|
684
|
-
stopPing();
|
|
685
|
-
clearIdleCheck();
|
|
686
|
-
const wasConnected = status.value === 'Connected' || status.value === 'Connecting...';
|
|
687
|
-
isProcessing.value = false;
|
|
688
|
-
isCompacting.value = false;
|
|
689
|
-
queuedMessages.value = [];
|
|
690
|
-
loadingSessions.value = false;
|
|
691
|
-
loadingHistory.value = false;
|
|
692
|
-
|
|
693
|
-
// Don't auto-reconnect if auth-locked or still in auth prompt
|
|
694
|
-
if (authLocked.value || authRequired.value) return;
|
|
695
|
-
|
|
696
|
-
if (wasConnected || reconnectAttempts > 0) {
|
|
697
|
-
scheduleReconnect(scheduleHighlight);
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
ws.onerror = () => {};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function scheduleReconnect(scheduleHighlight) {
|
|
705
|
-
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
706
|
-
status.value = 'Disconnected';
|
|
707
|
-
error.value = t('error.unableToReconnect');
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts), RECONNECT_MAX_DELAY);
|
|
711
|
-
reconnectAttempts++;
|
|
712
|
-
status.value = 'Reconnecting...';
|
|
713
|
-
error.value = t('error.connectionLost', { n: reconnectAttempts });
|
|
714
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
715
|
-
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(scheduleHighlight); }, delay);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function closeWs() {
|
|
719
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
720
|
-
if (ws) ws.close();
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function submitPassword() {
|
|
724
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
725
|
-
const pwd = authPassword.value.trim();
|
|
726
|
-
if (!pwd) return;
|
|
727
|
-
ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, setLoop, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap };
|
|
731
|
-
}
|