@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.
Files changed (76) hide show
  1. package/dist/auth-manager.d.ts +36 -0
  2. package/dist/auth-manager.js +96 -0
  3. package/dist/auth-manager.js.map +1 -0
  4. package/dist/http.d.ts +4 -0
  5. package/dist/http.js +85 -0
  6. package/dist/http.js.map +1 -0
  7. package/dist/index.js +5 -84
  8. package/dist/index.js.map +1 -1
  9. package/dist/message-relay.d.ts +17 -0
  10. package/dist/message-relay.js +23 -0
  11. package/dist/message-relay.js.map +1 -0
  12. package/dist/session-manager.d.ts +44 -0
  13. package/dist/session-manager.js +83 -0
  14. package/dist/session-manager.js.map +1 -0
  15. package/dist/ws-agent.js +19 -27
  16. package/dist/ws-agent.js.map +1 -1
  17. package/dist/ws-client.js +31 -37
  18. package/dist/ws-client.js.map +1 -1
  19. package/package.json +3 -3
  20. package/web/dist/assets/index-DIO7Hox0.js +320 -0
  21. package/web/dist/assets/index-DIO7Hox0.js.map +1 -0
  22. package/web/dist/assets/index-Y1FN_mFe.css +1 -0
  23. package/web/{index.html → dist/index.html} +2 -19
  24. package/dist/auth.d.ts +0 -13
  25. package/dist/auth.js +0 -65
  26. package/dist/auth.js.map +0 -1
  27. package/dist/context.d.ts +0 -52
  28. package/dist/context.js +0 -60
  29. package/dist/context.js.map +0 -1
  30. package/web/app.js +0 -2881
  31. package/web/css/ask-question.css +0 -333
  32. package/web/css/base.css +0 -270
  33. package/web/css/btw.css +0 -148
  34. package/web/css/chat.css +0 -176
  35. package/web/css/file-browser.css +0 -499
  36. package/web/css/input.css +0 -671
  37. package/web/css/loop.css +0 -674
  38. package/web/css/markdown.css +0 -169
  39. package/web/css/responsive.css +0 -314
  40. package/web/css/sidebar.css +0 -593
  41. package/web/css/team.css +0 -1277
  42. package/web/css/tools.css +0 -327
  43. package/web/encryption.js +0 -56
  44. package/web/modules/appHelpers.js +0 -100
  45. package/web/modules/askQuestion.js +0 -63
  46. package/web/modules/backgroundRouting.js +0 -269
  47. package/web/modules/connection.js +0 -731
  48. package/web/modules/fileAttachments.js +0 -125
  49. package/web/modules/fileBrowser.js +0 -398
  50. package/web/modules/filePreview.js +0 -213
  51. package/web/modules/i18n.js +0 -101
  52. package/web/modules/loop.js +0 -338
  53. package/web/modules/loopTemplates.js +0 -110
  54. package/web/modules/markdown.js +0 -83
  55. package/web/modules/messageHelpers.js +0 -206
  56. package/web/modules/sidebar.js +0 -402
  57. package/web/modules/streaming.js +0 -116
  58. package/web/modules/team.js +0 -396
  59. package/web/modules/teamTemplates.js +0 -360
  60. package/web/vendor/highlight.min.js +0 -1213
  61. package/web/vendor/marked.min.js +0 -6
  62. package/web/vendor/nacl-fast.min.js +0 -1
  63. package/web/vendor/nacl-util.min.js +0 -1
  64. package/web/vendor/pako.min.js +0 -2
  65. package/web/vendor/vue.global.prod.js +0 -13
  66. /package/web/{favicon.svg → dist/favicon.svg} +0 -0
  67. /package/web/{images → dist/images}/chat-iPad.webp +0 -0
  68. /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
  69. /package/web/{images → dist/images}/loop-iPad.webp +0 -0
  70. /package/web/{images → dist/images}/team-iPad.webp +0 -0
  71. /package/web/{landing.html → dist/landing.html} +0 -0
  72. /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
  73. /package/web/{locales → dist/locales}/en.json +0 -0
  74. /package/web/{locales → dist/locales}/zh.json +0 -0
  75. /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
  76. /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
- }