@agent-link/server 0.1.31 → 0.1.33

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/context.d.ts CHANGED
@@ -9,7 +9,6 @@ export interface AgentSession {
9
9
  sessionKey: Uint8Array | null;
10
10
  connectedAt: Date;
11
11
  isAlive: boolean;
12
- processing: boolean;
13
12
  }
14
13
  export interface WebClient {
15
14
  ws: WebSocket;
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAwBrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAuBrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
package/dist/ws-agent.js CHANGED
@@ -22,7 +22,6 @@ export function handleAgentConnection(ws, req) {
22
22
  sessionKey,
23
23
  connectedAt: new Date(),
24
24
  isAlive: true,
25
- processing: false,
26
25
  };
27
26
  agents.set(agentId, agent);
28
27
  sessionToAgent.set(sessionId, agentId);
@@ -73,13 +72,8 @@ async function handleAgentMessage(agentId, raw) {
73
72
  // Intercept workdir_changed to keep server state in sync
74
73
  if (msg.type === 'workdir_changed' && typeof msg.workDir === 'string') {
75
74
  agent.workDir = msg.workDir;
76
- agent.processing = false;
77
75
  console.log(`[Agent] ${agent.name} changed workDir to: ${msg.workDir}`);
78
76
  }
79
- // Track processing state so new web clients get the correct status on connect
80
- if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
81
- agent.processing = false;
82
- }
83
77
  // Forward agent messages to all web clients connected to this session
84
78
  // Re-encrypt with each client's own session key
85
79
  for (const [, client] of webClients) {
@@ -1 +1 @@
1
- {"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
package/dist/ws-client.js CHANGED
@@ -34,7 +34,6 @@ export function handleWebConnection(ws, req) {
34
34
  name: agent.name,
35
35
  hostname: agent.hostname,
36
36
  workDir: agent.workDir,
37
- processing: agent.processing,
38
37
  } : null,
39
38
  }));
40
39
  console.log(`[Web] Client ${clientId.slice(0, 8)} connected to session ${sessionId}, agent: ${agent ? agent.name : 'none'}`);
@@ -65,13 +64,6 @@ async function handleWebMessage(clientId, raw) {
65
64
  encryptAndSend(client.ws, { type: 'error', message: 'Agent not connected' }, client.sessionKey);
66
65
  return;
67
66
  }
68
- // Track processing state: chat starts a turn, cancel_execution ends it
69
- if (msg.type === 'chat') {
70
- agent.processing = true;
71
- }
72
- else if (msg.type === 'cancel_execution') {
73
- agent.processing = false;
74
- }
75
67
  // Forward web client message to agent, encrypted with agent's session key
76
68
  encryptAndSend(agent.ws, msg, agent.sessionKey);
77
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "@agent-link/server",
3
- "version": "0.1.31",
4
- "description": "AgentLink relay server",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "files": [
9
- "dist",
10
- "web"
11
- ],
12
- "bin": {
13
- "agentlink-server": "dist/cli.js"
14
- },
15
- "scripts": {
16
- "build": "tsc",
17
- "prepublishOnly": "npm run build",
18
- "dev": "tsx watch src/index.ts",
19
- "start": "node dist/index.js"
20
- },
21
- "engines": {
22
- "node": ">=18.0.0"
23
- },
24
- "dependencies": {
25
- "commander": "^12.0.0",
26
- "express": "^4.18.2",
27
- "tweetnacl": "^1.0.3",
28
- "tweetnacl-util": "^0.15.1",
29
- "ws": "^8.16.0"
30
- },
31
- "devDependencies": {
32
- "@types/express": "^4.17.21",
33
- "@types/ws": "^8.5.10",
34
- "tsx": "^4.7.0"
35
- }
36
- }
1
+ {
2
+ "name": "@agent-link/server",
3
+ "version": "0.1.33",
4
+ "description": "AgentLink relay server",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "web"
11
+ ],
12
+ "bin": {
13
+ "agentlink-server": "dist/cli.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build",
18
+ "dev": "tsx watch src/index.ts",
19
+ "start": "node dist/index.js"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "dependencies": {
25
+ "commander": "^12.0.0",
26
+ "express": "^4.18.2",
27
+ "tweetnacl": "^1.0.3",
28
+ "tweetnacl-util": "^0.15.1",
29
+ "ws": "^8.16.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^4.17.21",
33
+ "@types/ws": "^8.5.10",
34
+ "tsx": "^4.7.0"
35
+ }
36
+ }
@@ -1,346 +1,342 @@
1
- // ── WebSocket connection, message routing, reconnection ──────────────────────
2
- import { encrypt, decrypt, isEncrypted, decodeKey } from '../encryption.js';
3
- import { isContextSummary } from './messageHelpers.js';
4
-
5
- const MAX_RECONNECT_ATTEMPTS = 50;
6
- const RECONNECT_BASE_DELAY = 1000;
7
- const RECONNECT_MAX_DELAY = 15000;
8
-
9
- /**
10
- * Creates the WebSocket connection controller.
11
- * @param {object} deps - All reactive state and callbacks needed
12
- */
13
- export function createConnection(deps) {
14
- const {
15
- status, agentName, hostname, workDir, sessionId, error,
16
- messages, isProcessing, isCompacting, visibleLimit,
17
- historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
18
- folderPickerLoading, folderPickerEntries, folderPickerPath,
19
- streaming, sidebar,
20
- scrollToBottom,
21
- } = deps;
22
-
23
- let ws = null;
24
- let sessionKey = null;
25
- let reconnectAttempts = 0;
26
- let reconnectTimer = null;
27
-
28
- function wsSend(msg) {
29
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
30
- if (sessionKey) {
31
- const encrypted = encrypt(msg, sessionKey);
32
- ws.send(JSON.stringify(encrypted));
33
- } else {
34
- ws.send(JSON.stringify(msg));
35
- }
36
- }
37
-
38
- function getSessionId() {
39
- const match = window.location.pathname.match(/^\/s\/([^/]+)/);
40
- return match ? match[1] : null;
41
- }
42
-
43
- function finalizeStreamingMsg(scheduleHighlight) {
44
- const sid = streaming.getStreamingMessageId();
45
- if (sid === null) return;
46
- const streamMsg = messages.value.find(m => m.id === sid);
47
- if (streamMsg) {
48
- streamMsg.isStreaming = false;
49
- if (isContextSummary(streamMsg.content)) {
50
- streamMsg.role = 'context-summary';
51
- streamMsg.contextExpanded = false;
52
- }
53
- }
54
- streaming.setStreamingMessageId(null);
55
- if (scheduleHighlight) scheduleHighlight();
56
- }
57
-
58
- function handleClaudeOutput(msg, scheduleHighlight) {
59
- const data = msg.data;
60
- if (!data) return;
61
-
62
- if (data.type === 'content_block_delta' && data.delta) {
63
- streaming.appendPending(data.delta);
64
- streaming.startReveal();
65
- return;
66
- }
67
-
68
- if (data.type === 'tool_use' && data.tools) {
69
- streaming.flushReveal();
70
- finalizeStreamingMsg(scheduleHighlight);
71
-
72
- for (const tool of data.tools) {
73
- messages.value.push({
74
- id: streaming.nextId(), role: 'tool',
75
- toolId: tool.id, toolName: tool.name || 'unknown',
76
- toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
77
- hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite'), timestamp: new Date(),
78
- });
79
- }
80
- scrollToBottom();
81
- return;
82
- }
83
-
84
- if (data.type === 'user' && data.tool_use_result) {
85
- const result = data.tool_use_result;
86
- const results = Array.isArray(result) ? result : [result];
87
- for (const r of results) {
88
- const toolMsg = [...messages.value].reverse().find(
89
- m => m.role === 'tool' && m.toolId === r.tool_use_id
90
- );
91
- if (toolMsg) {
92
- toolMsg.toolOutput = typeof r.content === 'string'
93
- ? r.content : JSON.stringify(r.content, null, 2);
94
- toolMsg.hasResult = true;
95
- }
96
- }
97
- scrollToBottom();
98
- return;
99
- }
100
- }
101
-
102
- function connect(scheduleHighlight) {
103
- const sid = getSessionId();
104
- if (!sid) {
105
- status.value = 'No Session';
106
- error.value = 'No session ID in URL. Use a session URL provided by agentlink start.';
107
- return;
108
- }
109
- sessionId.value = sid;
110
- status.value = 'Connecting...';
111
- error.value = '';
112
-
113
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
114
- const wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
115
- ws = new WebSocket(wsUrl);
116
-
117
- ws.onopen = () => { error.value = ''; reconnectAttempts = 0; };
118
-
119
- ws.onmessage = (event) => {
120
- let msg;
121
- const parsed = JSON.parse(event.data);
122
-
123
- if (parsed.type === 'connected') {
124
- msg = parsed;
125
- if (typeof parsed.sessionKey === 'string') {
126
- sessionKey = decodeKey(parsed.sessionKey);
127
- }
128
- } else if (sessionKey && isEncrypted(parsed)) {
129
- msg = decrypt(parsed, sessionKey);
130
- if (!msg) {
131
- console.error('[WS] Failed to decrypt message');
132
- return;
133
- }
134
- } else {
135
- msg = parsed;
136
- }
137
-
138
- if (msg.type === 'connected') {
139
- if (msg.agent) {
140
- status.value = 'Connected';
141
- agentName.value = msg.agent.name;
142
- hostname.value = msg.agent.hostname || '';
143
- workDir.value = msg.agent.workDir;
144
- // Restore processing state (e.g. after page refresh mid-turn)
145
- if (msg.agent.processing) {
146
- isProcessing.value = true;
147
- }
148
- const savedDir = localStorage.getItem('agentlink-workdir');
149
- if (savedDir && savedDir !== msg.agent.workDir) {
150
- wsSend({ type: 'change_workdir', workDir: savedDir });
151
- }
152
- sidebar.requestSessionList();
153
- } else {
154
- status.value = 'Waiting';
155
- error.value = 'Agent is not connected yet.';
156
- }
157
- } else if (msg.type === 'agent_disconnected') {
158
- status.value = 'Waiting';
159
- agentName.value = '';
160
- hostname.value = '';
161
- error.value = 'Agent disconnected. Waiting for reconnect...';
162
- isProcessing.value = false;
163
- isCompacting.value = false;
164
- } else if (msg.type === 'agent_reconnected') {
165
- status.value = 'Connected';
166
- error.value = '';
167
- if (msg.agent) {
168
- agentName.value = msg.agent.name;
169
- hostname.value = msg.agent.hostname || '';
170
- workDir.value = msg.agent.workDir;
171
- }
172
- sidebar.requestSessionList();
173
- } else if (msg.type === 'error') {
174
- status.value = 'Error';
175
- error.value = msg.message;
176
- isProcessing.value = false;
177
- isCompacting.value = false;
178
- } else if (msg.type === 'claude_output') {
179
- handleClaudeOutput(msg, scheduleHighlight);
180
- } else if (msg.type === 'command_output') {
181
- streaming.flushReveal();
182
- finalizeStreamingMsg(scheduleHighlight);
183
- messages.value.push({
184
- id: streaming.nextId(), role: 'user',
185
- content: msg.content, isCommandOutput: true,
186
- timestamp: new Date(),
187
- });
188
- scrollToBottom();
189
- } else if (msg.type === 'context_compaction') {
190
- if (msg.status === 'started') {
191
- isCompacting.value = true;
192
- } else if (msg.status === 'completed') {
193
- isCompacting.value = false;
194
- }
195
- } else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
196
- isProcessing.value = false;
197
- isCompacting.value = false;
198
- streaming.flushReveal();
199
- finalizeStreamingMsg(scheduleHighlight);
200
- if (msg.type === 'execution_cancelled') {
201
- messages.value.push({
202
- id: streaming.nextId(), role: 'system',
203
- content: 'Generation stopped.', timestamp: new Date(),
204
- });
205
- scrollToBottom();
206
- }
207
- } else if (msg.type === 'ask_user_question') {
208
- streaming.flushReveal();
209
- finalizeStreamingMsg(scheduleHighlight);
210
- for (let i = messages.value.length - 1; i >= 0; i--) {
211
- const m = messages.value[i];
212
- if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
213
- messages.value.splice(i, 1);
214
- break;
215
- }
216
- if (m.role === 'user') break;
217
- }
218
- const questions = msg.questions || [];
219
- const selectedAnswers = {};
220
- const customTexts = {};
221
- for (let i = 0; i < questions.length; i++) {
222
- selectedAnswers[i] = questions[i].multiSelect ? [] : null;
223
- customTexts[i] = '';
224
- }
225
- messages.value.push({
226
- id: streaming.nextId(),
227
- role: 'ask-question',
228
- requestId: msg.requestId,
229
- questions,
230
- answered: false,
231
- selectedAnswers,
232
- customTexts,
233
- timestamp: new Date(),
234
- });
235
- scrollToBottom();
236
- } else if (msg.type === 'sessions_list') {
237
- historySessions.value = msg.sessions || [];
238
- loadingSessions.value = false;
239
- } else if (msg.type === 'conversation_resumed') {
240
- currentClaudeSessionId.value = msg.claudeSessionId;
241
- if (msg.history && Array.isArray(msg.history)) {
242
- const batch = [];
243
- for (const h of msg.history) {
244
- if (h.role === 'user') {
245
- if (isContextSummary(h.content)) {
246
- batch.push({
247
- id: streaming.nextId(), role: 'context-summary',
248
- content: h.content, contextExpanded: false,
249
- timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
250
- });
251
- } else {
252
- batch.push({
253
- id: streaming.nextId(), role: 'user',
254
- content: h.content, isCommandOutput: !!h.isCommandOutput,
255
- timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
256
- });
257
- }
258
- } else if (h.role === 'assistant') {
259
- const last = batch[batch.length - 1];
260
- if (last && last.role === 'assistant' && !last.isStreaming) {
261
- last.content += '\n\n' + h.content;
262
- } else {
263
- batch.push({
264
- id: streaming.nextId(), role: 'assistant',
265
- content: h.content, isStreaming: false,
266
- timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
267
- });
268
- }
269
- } else if (h.role === 'tool') {
270
- batch.push({
271
- id: streaming.nextId(), role: 'tool',
272
- toolId: h.toolId || '', toolName: h.toolName || 'unknown',
273
- toolInput: h.toolInput || '', hasResult: true,
274
- expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite'), timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
275
- });
276
- }
277
- }
278
- messages.value = batch;
279
- }
280
- loadingHistory.value = false;
281
- messages.value.push({
282
- id: streaming.nextId(), role: 'system',
283
- content: 'Session restored. You can continue the conversation.',
284
- timestamp: new Date(),
285
- });
286
- scrollToBottom();
287
- } else if (msg.type === 'directory_listing') {
288
- folderPickerLoading.value = false;
289
- folderPickerEntries.value = (msg.entries || [])
290
- .filter(e => e.type === 'directory')
291
- .sort((a, b) => a.name.localeCompare(b.name));
292
- if (msg.dirPath != null) folderPickerPath.value = msg.dirPath;
293
- } else if (msg.type === 'workdir_changed') {
294
- workDir.value = msg.workDir;
295
- localStorage.setItem('agentlink-workdir', msg.workDir);
296
- messages.value = [];
297
- visibleLimit.value = 50;
298
- streaming.setMessageIdCounter(0);
299
- streaming.setStreamingMessageId(null);
300
- streaming.reset();
301
- currentClaudeSessionId.value = null;
302
- isProcessing.value = false;
303
- messages.value.push({
304
- id: streaming.nextId(), role: 'system',
305
- content: 'Working directory changed to: ' + msg.workDir,
306
- timestamp: new Date(),
307
- });
308
- sidebar.requestSessionList();
309
- }
310
- };
311
-
312
- ws.onclose = () => {
313
- sessionKey = null;
314
- const wasConnected = status.value === 'Connected' || status.value === 'Connecting...';
315
- isProcessing.value = false;
316
- isCompacting.value = false;
317
-
318
- if (wasConnected || reconnectAttempts > 0) {
319
- scheduleReconnect(scheduleHighlight);
320
- }
321
- };
322
-
323
- ws.onerror = () => {};
324
- }
325
-
326
- function scheduleReconnect(scheduleHighlight) {
327
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
328
- status.value = 'Disconnected';
329
- error.value = 'Unable to reconnect. Please refresh the page.';
330
- return;
331
- }
332
- const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts), RECONNECT_MAX_DELAY);
333
- reconnectAttempts++;
334
- status.value = 'Reconnecting...';
335
- error.value = 'Connection lost. Reconnecting... (attempt ' + reconnectAttempts + ')';
336
- if (reconnectTimer) clearTimeout(reconnectTimer);
337
- reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(scheduleHighlight); }, delay);
338
- }
339
-
340
- function closeWs() {
341
- if (reconnectTimer) clearTimeout(reconnectTimer);
342
- if (ws) ws.close();
343
- }
344
-
345
- return { connect, wsSend, closeWs };
346
- }
1
+ // ── WebSocket connection, message routing, reconnection ──────────────────────
2
+ import { encrypt, decrypt, isEncrypted, decodeKey } from '../encryption.js';
3
+ import { isContextSummary } from './messageHelpers.js';
4
+
5
+ const MAX_RECONNECT_ATTEMPTS = 50;
6
+ const RECONNECT_BASE_DELAY = 1000;
7
+ const RECONNECT_MAX_DELAY = 15000;
8
+
9
+ /**
10
+ * Creates the WebSocket connection controller.
11
+ * @param {object} deps - All reactive state and callbacks needed
12
+ */
13
+ export function createConnection(deps) {
14
+ const {
15
+ status, agentName, hostname, workDir, sessionId, error,
16
+ messages, isProcessing, isCompacting, visibleLimit,
17
+ historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
18
+ folderPickerLoading, folderPickerEntries, folderPickerPath,
19
+ streaming, sidebar,
20
+ scrollToBottom,
21
+ } = deps;
22
+
23
+ let ws = null;
24
+ let sessionKey = null;
25
+ let reconnectAttempts = 0;
26
+ let reconnectTimer = null;
27
+
28
+ function wsSend(msg) {
29
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
30
+ if (sessionKey) {
31
+ const encrypted = encrypt(msg, sessionKey);
32
+ ws.send(JSON.stringify(encrypted));
33
+ } else {
34
+ ws.send(JSON.stringify(msg));
35
+ }
36
+ }
37
+
38
+ function getSessionId() {
39
+ const match = window.location.pathname.match(/^\/s\/([^/]+)/);
40
+ return match ? match[1] : null;
41
+ }
42
+
43
+ function finalizeStreamingMsg(scheduleHighlight) {
44
+ const sid = streaming.getStreamingMessageId();
45
+ if (sid === null) return;
46
+ const streamMsg = messages.value.find(m => m.id === sid);
47
+ if (streamMsg) {
48
+ streamMsg.isStreaming = false;
49
+ if (isContextSummary(streamMsg.content)) {
50
+ streamMsg.role = 'context-summary';
51
+ streamMsg.contextExpanded = false;
52
+ }
53
+ }
54
+ streaming.setStreamingMessageId(null);
55
+ if (scheduleHighlight) scheduleHighlight();
56
+ }
57
+
58
+ function handleClaudeOutput(msg, scheduleHighlight) {
59
+ const data = msg.data;
60
+ if (!data) return;
61
+
62
+ if (data.type === 'content_block_delta' && data.delta) {
63
+ streaming.appendPending(data.delta);
64
+ streaming.startReveal();
65
+ return;
66
+ }
67
+
68
+ if (data.type === 'tool_use' && data.tools) {
69
+ streaming.flushReveal();
70
+ finalizeStreamingMsg(scheduleHighlight);
71
+
72
+ for (const tool of data.tools) {
73
+ messages.value.push({
74
+ id: streaming.nextId(), role: 'tool',
75
+ toolId: tool.id, toolName: tool.name || 'unknown',
76
+ toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
77
+ hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite'), timestamp: new Date(),
78
+ });
79
+ }
80
+ scrollToBottom();
81
+ return;
82
+ }
83
+
84
+ if (data.type === 'user' && data.tool_use_result) {
85
+ const result = data.tool_use_result;
86
+ const results = Array.isArray(result) ? result : [result];
87
+ for (const r of results) {
88
+ const toolMsg = [...messages.value].reverse().find(
89
+ m => m.role === 'tool' && m.toolId === r.tool_use_id
90
+ );
91
+ if (toolMsg) {
92
+ toolMsg.toolOutput = typeof r.content === 'string'
93
+ ? r.content : JSON.stringify(r.content, null, 2);
94
+ toolMsg.hasResult = true;
95
+ }
96
+ }
97
+ scrollToBottom();
98
+ return;
99
+ }
100
+ }
101
+
102
+ function connect(scheduleHighlight) {
103
+ const sid = getSessionId();
104
+ if (!sid) {
105
+ status.value = 'No Session';
106
+ error.value = 'No session ID in URL. Use a session URL provided by agentlink start.';
107
+ return;
108
+ }
109
+ sessionId.value = sid;
110
+ status.value = 'Connecting...';
111
+ error.value = '';
112
+
113
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
114
+ const wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
115
+ ws = new WebSocket(wsUrl);
116
+
117
+ ws.onopen = () => { error.value = ''; reconnectAttempts = 0; };
118
+
119
+ ws.onmessage = (event) => {
120
+ let msg;
121
+ const parsed = JSON.parse(event.data);
122
+
123
+ if (parsed.type === 'connected') {
124
+ msg = parsed;
125
+ if (typeof parsed.sessionKey === 'string') {
126
+ sessionKey = decodeKey(parsed.sessionKey);
127
+ }
128
+ } else if (sessionKey && isEncrypted(parsed)) {
129
+ msg = decrypt(parsed, sessionKey);
130
+ if (!msg) {
131
+ console.error('[WS] Failed to decrypt message');
132
+ return;
133
+ }
134
+ } else {
135
+ msg = parsed;
136
+ }
137
+
138
+ if (msg.type === 'connected') {
139
+ if (msg.agent) {
140
+ status.value = 'Connected';
141
+ agentName.value = msg.agent.name;
142
+ hostname.value = msg.agent.hostname || '';
143
+ workDir.value = msg.agent.workDir;
144
+ const savedDir = localStorage.getItem('agentlink-workdir');
145
+ if (savedDir && savedDir !== msg.agent.workDir) {
146
+ wsSend({ type: 'change_workdir', workDir: savedDir });
147
+ }
148
+ sidebar.requestSessionList();
149
+ } else {
150
+ status.value = 'Waiting';
151
+ error.value = 'Agent is not connected yet.';
152
+ }
153
+ } else if (msg.type === 'agent_disconnected') {
154
+ status.value = 'Waiting';
155
+ agentName.value = '';
156
+ hostname.value = '';
157
+ error.value = 'Agent disconnected. Waiting for reconnect...';
158
+ isProcessing.value = false;
159
+ isCompacting.value = false;
160
+ } else if (msg.type === 'agent_reconnected') {
161
+ status.value = 'Connected';
162
+ error.value = '';
163
+ if (msg.agent) {
164
+ agentName.value = msg.agent.name;
165
+ hostname.value = msg.agent.hostname || '';
166
+ workDir.value = msg.agent.workDir;
167
+ }
168
+ sidebar.requestSessionList();
169
+ } else if (msg.type === 'error') {
170
+ status.value = 'Error';
171
+ error.value = msg.message;
172
+ isProcessing.value = false;
173
+ isCompacting.value = false;
174
+ } else if (msg.type === 'claude_output') {
175
+ handleClaudeOutput(msg, scheduleHighlight);
176
+ } else if (msg.type === 'command_output') {
177
+ streaming.flushReveal();
178
+ finalizeStreamingMsg(scheduleHighlight);
179
+ messages.value.push({
180
+ id: streaming.nextId(), role: 'user',
181
+ content: msg.content, isCommandOutput: true,
182
+ timestamp: new Date(),
183
+ });
184
+ scrollToBottom();
185
+ } else if (msg.type === 'context_compaction') {
186
+ if (msg.status === 'started') {
187
+ isCompacting.value = true;
188
+ } else if (msg.status === 'completed') {
189
+ isCompacting.value = false;
190
+ }
191
+ } else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
192
+ isProcessing.value = false;
193
+ isCompacting.value = false;
194
+ streaming.flushReveal();
195
+ finalizeStreamingMsg(scheduleHighlight);
196
+ if (msg.type === 'execution_cancelled') {
197
+ messages.value.push({
198
+ id: streaming.nextId(), role: 'system',
199
+ content: 'Generation stopped.', timestamp: new Date(),
200
+ });
201
+ scrollToBottom();
202
+ }
203
+ } else if (msg.type === 'ask_user_question') {
204
+ streaming.flushReveal();
205
+ finalizeStreamingMsg(scheduleHighlight);
206
+ for (let i = messages.value.length - 1; i >= 0; i--) {
207
+ const m = messages.value[i];
208
+ if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
209
+ messages.value.splice(i, 1);
210
+ break;
211
+ }
212
+ if (m.role === 'user') break;
213
+ }
214
+ const questions = msg.questions || [];
215
+ const selectedAnswers = {};
216
+ const customTexts = {};
217
+ for (let i = 0; i < questions.length; i++) {
218
+ selectedAnswers[i] = questions[i].multiSelect ? [] : null;
219
+ customTexts[i] = '';
220
+ }
221
+ messages.value.push({
222
+ id: streaming.nextId(),
223
+ role: 'ask-question',
224
+ requestId: msg.requestId,
225
+ questions,
226
+ answered: false,
227
+ selectedAnswers,
228
+ customTexts,
229
+ timestamp: new Date(),
230
+ });
231
+ scrollToBottom();
232
+ } else if (msg.type === 'sessions_list') {
233
+ historySessions.value = msg.sessions || [];
234
+ loadingSessions.value = false;
235
+ } else if (msg.type === 'conversation_resumed') {
236
+ currentClaudeSessionId.value = msg.claudeSessionId;
237
+ if (msg.history && Array.isArray(msg.history)) {
238
+ const batch = [];
239
+ for (const h of msg.history) {
240
+ if (h.role === 'user') {
241
+ if (isContextSummary(h.content)) {
242
+ batch.push({
243
+ id: streaming.nextId(), role: 'context-summary',
244
+ content: h.content, contextExpanded: false,
245
+ timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
246
+ });
247
+ } else {
248
+ batch.push({
249
+ id: streaming.nextId(), role: 'user',
250
+ content: h.content, isCommandOutput: !!h.isCommandOutput,
251
+ timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
252
+ });
253
+ }
254
+ } else if (h.role === 'assistant') {
255
+ const last = batch[batch.length - 1];
256
+ if (last && last.role === 'assistant' && !last.isStreaming) {
257
+ last.content += '\n\n' + h.content;
258
+ } else {
259
+ batch.push({
260
+ id: streaming.nextId(), role: 'assistant',
261
+ content: h.content, isStreaming: false,
262
+ timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
263
+ });
264
+ }
265
+ } else if (h.role === 'tool') {
266
+ batch.push({
267
+ id: streaming.nextId(), role: 'tool',
268
+ toolId: h.toolId || '', toolName: h.toolName || 'unknown',
269
+ toolInput: h.toolInput || '', hasResult: true,
270
+ expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite'), timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
271
+ });
272
+ }
273
+ }
274
+ messages.value = batch;
275
+ }
276
+ loadingHistory.value = false;
277
+ messages.value.push({
278
+ id: streaming.nextId(), role: 'system',
279
+ content: 'Session restored. You can continue the conversation.',
280
+ timestamp: new Date(),
281
+ });
282
+ scrollToBottom();
283
+ } else if (msg.type === 'directory_listing') {
284
+ folderPickerLoading.value = false;
285
+ folderPickerEntries.value = (msg.entries || [])
286
+ .filter(e => e.type === 'directory')
287
+ .sort((a, b) => a.name.localeCompare(b.name));
288
+ if (msg.dirPath != null) folderPickerPath.value = msg.dirPath;
289
+ } else if (msg.type === 'workdir_changed') {
290
+ workDir.value = msg.workDir;
291
+ localStorage.setItem('agentlink-workdir', msg.workDir);
292
+ messages.value = [];
293
+ visibleLimit.value = 50;
294
+ streaming.setMessageIdCounter(0);
295
+ streaming.setStreamingMessageId(null);
296
+ streaming.reset();
297
+ currentClaudeSessionId.value = null;
298
+ isProcessing.value = false;
299
+ messages.value.push({
300
+ id: streaming.nextId(), role: 'system',
301
+ content: 'Working directory changed to: ' + msg.workDir,
302
+ timestamp: new Date(),
303
+ });
304
+ sidebar.requestSessionList();
305
+ }
306
+ };
307
+
308
+ ws.onclose = () => {
309
+ sessionKey = null;
310
+ const wasConnected = status.value === 'Connected' || status.value === 'Connecting...';
311
+ isProcessing.value = false;
312
+ isCompacting.value = false;
313
+
314
+ if (wasConnected || reconnectAttempts > 0) {
315
+ scheduleReconnect(scheduleHighlight);
316
+ }
317
+ };
318
+
319
+ ws.onerror = () => {};
320
+ }
321
+
322
+ function scheduleReconnect(scheduleHighlight) {
323
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
324
+ status.value = 'Disconnected';
325
+ error.value = 'Unable to reconnect. Please refresh the page.';
326
+ return;
327
+ }
328
+ const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts), RECONNECT_MAX_DELAY);
329
+ reconnectAttempts++;
330
+ status.value = 'Reconnecting...';
331
+ error.value = 'Connection lost. Reconnecting... (attempt ' + reconnectAttempts + ')';
332
+ if (reconnectTimer) clearTimeout(reconnectTimer);
333
+ reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(scheduleHighlight); }, delay);
334
+ }
335
+
336
+ function closeWs() {
337
+ if (reconnectTimer) clearTimeout(reconnectTimer);
338
+ if (ws) ws.close();
339
+ }
340
+
341
+ return { connect, wsSend, closeWs };
342
+ }
@@ -1,186 +1,186 @@
1
- // ── Sidebar: session management, folder picker, grouped sessions ─────────────
2
- const { computed } = Vue;
3
-
4
- /**
5
- * Creates sidebar functionality bound to reactive state.
6
- * @param {object} deps
7
- * @param {Function} deps.wsSend
8
- * @param {import('vue').Ref} deps.messages
9
- * @param {import('vue').Ref} deps.isProcessing
10
- * @param {import('vue').Ref} deps.sidebarOpen
11
- * @param {import('vue').Ref} deps.historySessions
12
- * @param {import('vue').Ref} deps.currentClaudeSessionId
13
- * @param {import('vue').Ref} deps.needsResume
14
- * @param {import('vue').Ref} deps.loadingSessions
15
- * @param {import('vue').Ref} deps.loadingHistory
16
- * @param {import('vue').Ref} deps.workDir
17
- * @param {import('vue').Ref} deps.visibleLimit
18
- * @param {import('vue').Ref} deps.folderPickerOpen
19
- * @param {import('vue').Ref} deps.folderPickerPath
20
- * @param {import('vue').Ref} deps.folderPickerEntries
21
- * @param {import('vue').Ref} deps.folderPickerLoading
22
- * @param {import('vue').Ref} deps.folderPickerSelected
23
- * @param {object} deps.streaming - streaming controller
24
- */
25
- export function createSidebar(deps) {
26
- const {
27
- wsSend, messages, isProcessing, sidebarOpen,
28
- historySessions, currentClaudeSessionId, needsResume,
29
- loadingSessions, loadingHistory, workDir, visibleLimit,
30
- folderPickerOpen, folderPickerPath, folderPickerEntries,
31
- folderPickerLoading, folderPickerSelected, streaming,
32
- } = deps;
33
-
34
- // ── Session management ──
35
-
36
- function requestSessionList() {
37
- loadingSessions.value = true;
38
- wsSend({ type: 'list_sessions' });
39
- }
40
-
41
- function resumeSession(session) {
42
- if (isProcessing.value) return;
43
- if (window.innerWidth <= 768) sidebarOpen.value = false;
44
- messages.value = [];
45
- visibleLimit.value = 50;
46
- streaming.setMessageIdCounter(0);
47
- streaming.setStreamingMessageId(null);
48
- streaming.reset();
49
-
50
- currentClaudeSessionId.value = session.sessionId;
51
- needsResume.value = true;
52
- loadingHistory.value = true;
53
-
54
- wsSend({
55
- type: 'resume_conversation',
56
- claudeSessionId: session.sessionId,
57
- });
58
- }
59
-
60
- function newConversation() {
61
- if (isProcessing.value) return;
62
- if (window.innerWidth <= 768) sidebarOpen.value = false;
63
- messages.value = [];
64
- visibleLimit.value = 50;
65
- streaming.setMessageIdCounter(0);
66
- streaming.setStreamingMessageId(null);
67
- streaming.reset();
68
- currentClaudeSessionId.value = null;
69
- needsResume.value = false;
70
-
71
- messages.value.push({
72
- id: streaming.nextId(), role: 'system',
73
- content: 'New conversation started.',
74
- timestamp: new Date(),
75
- });
76
- }
77
-
78
- function toggleSidebar() {
79
- sidebarOpen.value = !sidebarOpen.value;
80
- }
81
-
82
- // ── Folder picker ──
83
-
84
- function openFolderPicker() {
85
- folderPickerOpen.value = true;
86
- folderPickerSelected.value = '';
87
- folderPickerLoading.value = true;
88
- folderPickerPath.value = workDir.value || '';
89
- folderPickerEntries.value = [];
90
- wsSend({ type: 'list_directory', dirPath: workDir.value || '' });
91
- }
92
-
93
- function loadFolderPickerDir(dirPath) {
94
- folderPickerLoading.value = true;
95
- folderPickerSelected.value = '';
96
- folderPickerEntries.value = [];
97
- wsSend({ type: 'list_directory', dirPath });
98
- }
99
-
100
- function folderPickerNavigateUp() {
101
- if (!folderPickerPath.value) return;
102
- const isWin = folderPickerPath.value.includes('\\');
103
- const parts = folderPickerPath.value.replace(/[/\\]$/, '').split(/[/\\]/);
104
- parts.pop();
105
- if (parts.length === 0) {
106
- folderPickerPath.value = '';
107
- loadFolderPickerDir('');
108
- } else if (isWin && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
109
- folderPickerPath.value = parts[0] + '\\';
110
- loadFolderPickerDir(parts[0] + '\\');
111
- } else {
112
- const sep = isWin ? '\\' : '/';
113
- const parent = parts.join(sep);
114
- folderPickerPath.value = parent;
115
- loadFolderPickerDir(parent);
116
- }
117
- }
118
-
119
- function folderPickerSelectItem(entry) {
120
- folderPickerSelected.value = entry.name;
121
- }
122
-
123
- function folderPickerEnter(entry) {
124
- const sep = folderPickerPath.value.includes('\\') || /^[A-Z]:/.test(entry.name) ? '\\' : '/';
125
- let newPath;
126
- if (!folderPickerPath.value) {
127
- newPath = entry.name + (entry.name.endsWith('\\') ? '' : '\\');
128
- } else {
129
- newPath = folderPickerPath.value.replace(/[/\\]$/, '') + sep + entry.name;
130
- }
131
- folderPickerPath.value = newPath;
132
- folderPickerSelected.value = '';
133
- loadFolderPickerDir(newPath);
134
- }
135
-
136
- function folderPickerGoToPath() {
137
- const path = folderPickerPath.value.trim();
138
- if (!path) {
139
- loadFolderPickerDir('');
140
- return;
141
- }
142
- folderPickerSelected.value = '';
143
- loadFolderPickerDir(path);
144
- }
145
-
146
- function confirmFolderPicker() {
147
- let path = folderPickerPath.value;
148
- if (!path) return;
149
- if (folderPickerSelected.value) {
150
- const sep = path.includes('\\') ? '\\' : '/';
151
- path = path.replace(/[/\\]$/, '') + sep + folderPickerSelected.value;
152
- }
153
- folderPickerOpen.value = false;
154
- wsSend({ type: 'change_workdir', workDir: path });
155
- }
156
-
157
- // ── Grouped sessions ──
158
-
159
- const groupedSessions = computed(() => {
160
- if (!historySessions.value.length) return [];
161
- const now = new Date();
162
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
163
- const yesterdayStart = todayStart - 86400000;
164
- const weekStart = todayStart - 6 * 86400000;
165
-
166
- const groups = {};
167
- for (const s of historySessions.value) {
168
- let label;
169
- if (s.lastModified >= todayStart) label = 'Today';
170
- else if (s.lastModified >= yesterdayStart) label = 'Yesterday';
171
- else if (s.lastModified >= weekStart) label = 'This week';
172
- else label = 'Earlier';
173
- if (!groups[label]) groups[label] = [];
174
- groups[label].push(s);
175
- }
176
- const order = ['Today', 'Yesterday', 'This week', 'Earlier'];
177
- return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
178
- });
179
-
180
- return {
181
- requestSessionList, resumeSession, newConversation, toggleSidebar,
182
- openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
183
- folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
184
- groupedSessions,
185
- };
186
- }
1
+ // ── Sidebar: session management, folder picker, grouped sessions ─────────────
2
+ const { computed } = Vue;
3
+
4
+ /**
5
+ * Creates sidebar functionality bound to reactive state.
6
+ * @param {object} deps
7
+ * @param {Function} deps.wsSend
8
+ * @param {import('vue').Ref} deps.messages
9
+ * @param {import('vue').Ref} deps.isProcessing
10
+ * @param {import('vue').Ref} deps.sidebarOpen
11
+ * @param {import('vue').Ref} deps.historySessions
12
+ * @param {import('vue').Ref} deps.currentClaudeSessionId
13
+ * @param {import('vue').Ref} deps.needsResume
14
+ * @param {import('vue').Ref} deps.loadingSessions
15
+ * @param {import('vue').Ref} deps.loadingHistory
16
+ * @param {import('vue').Ref} deps.workDir
17
+ * @param {import('vue').Ref} deps.visibleLimit
18
+ * @param {import('vue').Ref} deps.folderPickerOpen
19
+ * @param {import('vue').Ref} deps.folderPickerPath
20
+ * @param {import('vue').Ref} deps.folderPickerEntries
21
+ * @param {import('vue').Ref} deps.folderPickerLoading
22
+ * @param {import('vue').Ref} deps.folderPickerSelected
23
+ * @param {object} deps.streaming - streaming controller
24
+ */
25
+ export function createSidebar(deps) {
26
+ const {
27
+ wsSend, messages, isProcessing, sidebarOpen,
28
+ historySessions, currentClaudeSessionId, needsResume,
29
+ loadingSessions, loadingHistory, workDir, visibleLimit,
30
+ folderPickerOpen, folderPickerPath, folderPickerEntries,
31
+ folderPickerLoading, folderPickerSelected, streaming,
32
+ } = deps;
33
+
34
+ // ── Session management ──
35
+
36
+ function requestSessionList() {
37
+ loadingSessions.value = true;
38
+ wsSend({ type: 'list_sessions' });
39
+ }
40
+
41
+ function resumeSession(session) {
42
+ if (isProcessing.value) return;
43
+ if (window.innerWidth <= 768) sidebarOpen.value = false;
44
+ messages.value = [];
45
+ visibleLimit.value = 50;
46
+ streaming.setMessageIdCounter(0);
47
+ streaming.setStreamingMessageId(null);
48
+ streaming.reset();
49
+
50
+ currentClaudeSessionId.value = session.sessionId;
51
+ needsResume.value = true;
52
+ loadingHistory.value = true;
53
+
54
+ wsSend({
55
+ type: 'resume_conversation',
56
+ claudeSessionId: session.sessionId,
57
+ });
58
+ }
59
+
60
+ function newConversation() {
61
+ if (isProcessing.value) return;
62
+ if (window.innerWidth <= 768) sidebarOpen.value = false;
63
+ messages.value = [];
64
+ visibleLimit.value = 50;
65
+ streaming.setMessageIdCounter(0);
66
+ streaming.setStreamingMessageId(null);
67
+ streaming.reset();
68
+ currentClaudeSessionId.value = null;
69
+ needsResume.value = false;
70
+
71
+ messages.value.push({
72
+ id: streaming.nextId(), role: 'system',
73
+ content: 'New conversation started.',
74
+ timestamp: new Date(),
75
+ });
76
+ }
77
+
78
+ function toggleSidebar() {
79
+ sidebarOpen.value = !sidebarOpen.value;
80
+ }
81
+
82
+ // ── Folder picker ──
83
+
84
+ function openFolderPicker() {
85
+ folderPickerOpen.value = true;
86
+ folderPickerSelected.value = '';
87
+ folderPickerLoading.value = true;
88
+ folderPickerPath.value = workDir.value || '';
89
+ folderPickerEntries.value = [];
90
+ wsSend({ type: 'list_directory', dirPath: workDir.value || '' });
91
+ }
92
+
93
+ function loadFolderPickerDir(dirPath) {
94
+ folderPickerLoading.value = true;
95
+ folderPickerSelected.value = '';
96
+ folderPickerEntries.value = [];
97
+ wsSend({ type: 'list_directory', dirPath });
98
+ }
99
+
100
+ function folderPickerNavigateUp() {
101
+ if (!folderPickerPath.value) return;
102
+ const isWin = folderPickerPath.value.includes('\\');
103
+ const parts = folderPickerPath.value.replace(/[/\\]$/, '').split(/[/\\]/);
104
+ parts.pop();
105
+ if (parts.length === 0) {
106
+ folderPickerPath.value = '';
107
+ loadFolderPickerDir('');
108
+ } else if (isWin && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
109
+ folderPickerPath.value = parts[0] + '\\';
110
+ loadFolderPickerDir(parts[0] + '\\');
111
+ } else {
112
+ const sep = isWin ? '\\' : '/';
113
+ const parent = parts.join(sep);
114
+ folderPickerPath.value = parent;
115
+ loadFolderPickerDir(parent);
116
+ }
117
+ }
118
+
119
+ function folderPickerSelectItem(entry) {
120
+ folderPickerSelected.value = entry.name;
121
+ }
122
+
123
+ function folderPickerEnter(entry) {
124
+ const sep = folderPickerPath.value.includes('\\') || /^[A-Z]:/.test(entry.name) ? '\\' : '/';
125
+ let newPath;
126
+ if (!folderPickerPath.value) {
127
+ newPath = entry.name + (entry.name.endsWith('\\') ? '' : '\\');
128
+ } else {
129
+ newPath = folderPickerPath.value.replace(/[/\\]$/, '') + sep + entry.name;
130
+ }
131
+ folderPickerPath.value = newPath;
132
+ folderPickerSelected.value = '';
133
+ loadFolderPickerDir(newPath);
134
+ }
135
+
136
+ function folderPickerGoToPath() {
137
+ const path = folderPickerPath.value.trim();
138
+ if (!path) {
139
+ loadFolderPickerDir('');
140
+ return;
141
+ }
142
+ folderPickerSelected.value = '';
143
+ loadFolderPickerDir(path);
144
+ }
145
+
146
+ function confirmFolderPicker() {
147
+ let path = folderPickerPath.value;
148
+ if (!path) return;
149
+ if (folderPickerSelected.value) {
150
+ const sep = path.includes('\\') ? '\\' : '/';
151
+ path = path.replace(/[/\\]$/, '') + sep + folderPickerSelected.value;
152
+ }
153
+ folderPickerOpen.value = false;
154
+ wsSend({ type: 'change_workdir', workDir: path });
155
+ }
156
+
157
+ // ── Grouped sessions ──
158
+
159
+ const groupedSessions = computed(() => {
160
+ if (!historySessions.value.length) return [];
161
+ const now = new Date();
162
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
163
+ const yesterdayStart = todayStart - 86400000;
164
+ const weekStart = todayStart - 6 * 86400000;
165
+
166
+ const groups = {};
167
+ for (const s of historySessions.value) {
168
+ let label;
169
+ if (s.lastModified >= todayStart) label = 'Today';
170
+ else if (s.lastModified >= yesterdayStart) label = 'Yesterday';
171
+ else if (s.lastModified >= weekStart) label = 'This week';
172
+ else label = 'Earlier';
173
+ if (!groups[label]) groups[label] = [];
174
+ groups[label].push(s);
175
+ }
176
+ const order = ['Today', 'Yesterday', 'This week', 'Earlier'];
177
+ return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
178
+ });
179
+
180
+ return {
181
+ requestSessionList, resumeSession, newConversation, toggleSidebar,
182
+ openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
183
+ folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
184
+ groupedSessions,
185
+ };
186
+ }