@agent-link/server 0.1.113 → 0.1.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -89,6 +89,75 @@ const App = {
89
89
  const fileInputRef = ref(null);
90
90
  const dragOver = ref(false);
91
91
 
92
+ // Multi-session parallel state
93
+ const conversationCache = ref({}); // conversationId → saved state snapshot
94
+ const currentConversationId = ref(crypto.randomUUID()); // currently visible conversation
95
+ const processingConversations = ref({}); // conversationId → boolean
96
+
97
+ // ── switchConversation: save current → load target ──
98
+ // Defined here and used by sidebar.newConversation, sidebar.resumeSession, workdir_changed
99
+ // Needs access to streaming / connection which are created later, so we use late-binding refs.
100
+ let _getToolMsgMap = () => new Map();
101
+ let _restoreToolMsgMap = () => {};
102
+ let _clearToolMsgMap = () => {};
103
+
104
+ function switchConversation(newConvId) {
105
+ const oldConvId = currentConversationId.value;
106
+
107
+ // Save current state (if there is one)
108
+ if (oldConvId) {
109
+ const streamState = streaming.saveState();
110
+ conversationCache.value[oldConvId] = {
111
+ messages: messages.value,
112
+ isProcessing: isProcessing.value,
113
+ isCompacting: isCompacting.value,
114
+ loadingHistory: loadingHistory.value,
115
+ claudeSessionId: currentClaudeSessionId.value,
116
+ visibleLimit: visibleLimit.value,
117
+ needsResume: needsResume.value,
118
+ streamingState: streamState,
119
+ toolMsgMap: _getToolMsgMap(),
120
+ messageIdCounter: streaming.getMessageIdCounter(),
121
+ queuedMessages: queuedMessages.value,
122
+ };
123
+ }
124
+
125
+ // Load target state
126
+ const cached = conversationCache.value[newConvId];
127
+ if (cached) {
128
+ messages.value = cached.messages;
129
+ isProcessing.value = cached.isProcessing;
130
+ isCompacting.value = cached.isCompacting;
131
+ loadingHistory.value = cached.loadingHistory || false;
132
+ currentClaudeSessionId.value = cached.claudeSessionId;
133
+ visibleLimit.value = cached.visibleLimit;
134
+ needsResume.value = cached.needsResume;
135
+ streaming.restoreState(cached.streamingState || { pendingText: '', streamingMessageId: null, messageIdCounter: cached.messageIdCounter || 0 });
136
+ // Background routing may have incremented messageIdCounter beyond what
137
+ // streamingState recorded at save time — use the authoritative value.
138
+ streaming.setMessageIdCounter(cached.messageIdCounter || 0);
139
+ _restoreToolMsgMap(cached.toolMsgMap || new Map());
140
+ queuedMessages.value = cached.queuedMessages || [];
141
+ } else {
142
+ // New blank conversation
143
+ messages.value = [];
144
+ isProcessing.value = false;
145
+ isCompacting.value = false;
146
+ loadingHistory.value = false;
147
+ currentClaudeSessionId.value = null;
148
+ visibleLimit.value = 50;
149
+ needsResume.value = false;
150
+ streaming.setMessageIdCounter(0);
151
+ streaming.setStreamingMessageId(null);
152
+ streaming.reset();
153
+ _clearToolMsgMap();
154
+ queuedMessages.value = [];
155
+ }
156
+
157
+ currentConversationId.value = newConvId;
158
+ scrollToBottom(true);
159
+ }
160
+
92
161
  // Theme
93
162
  const theme = ref(localStorage.getItem('agentlink-theme') || 'light');
94
163
  function applyTheme() {
@@ -159,9 +228,12 @@ const App = {
159
228
  deleteConfirmOpen, deleteConfirmTitle,
160
229
  renamingSessionId, renameText,
161
230
  hostname, workdirHistory,
231
+ // Multi-session parallel
232
+ currentConversationId, conversationCache, processingConversations,
233
+ switchConversation,
162
234
  });
163
235
 
164
- const { connect, wsSend, closeWs, submitPassword, setDequeueNext } = createConnection({
236
+ const { connect, wsSend, closeWs, submitPassword, setDequeueNext, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
165
237
  status, agentName, hostname, workDir, sessionId, error,
166
238
  serverVersion, agentVersion, latency,
167
239
  messages, isProcessing, isCompacting, visibleLimit, queuedMessages,
@@ -169,11 +241,18 @@ const App = {
169
241
  folderPickerLoading, folderPickerEntries, folderPickerPath,
170
242
  authRequired, authPassword, authError, authAttempts, authLocked,
171
243
  streaming, sidebar, scrollToBottom,
244
+ // Multi-session parallel
245
+ currentConversationId, processingConversations, conversationCache,
246
+ switchConversation,
172
247
  });
173
248
 
174
249
  // Now wire up the forwarding function
175
250
  _wsSend = wsSend;
176
251
  setDequeueNext(dequeueNext);
252
+ // Wire up late-binding toolMsgMap functions for switchConversation
253
+ _getToolMsgMap = getToolMsgMap;
254
+ _restoreToolMsgMap = restoreToolMsgMap;
255
+ _clearToolMsgMap = clearToolMsgMap;
177
256
 
178
257
  // ── Computed ──
179
258
  const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
@@ -205,6 +284,9 @@ const App = {
205
284
  }));
206
285
 
207
286
  const payload = { type: 'chat', prompt: text || '(see attached files)' };
287
+ if (currentConversationId.value) {
288
+ payload.conversationId = currentConversationId.value;
289
+ }
208
290
  if (needsResume.value && currentClaudeSessionId.value) {
209
291
  payload.resumeSessionId = currentClaudeSessionId.value;
210
292
  needsResume.value = false;
@@ -228,6 +310,9 @@ const App = {
228
310
  userMsg.status = 'sent';
229
311
  messages.value.push(userMsg);
230
312
  isProcessing.value = true;
313
+ if (currentConversationId.value) {
314
+ processingConversations.value[currentConversationId.value] = true;
315
+ }
231
316
  wsSend(payload);
232
317
  }
233
318
  scrollToBottom(true);
@@ -236,7 +321,11 @@ const App = {
236
321
 
237
322
  function cancelExecution() {
238
323
  if (!isProcessing.value) return;
239
- wsSend({ type: 'cancel_execution' });
324
+ const cancelPayload = { type: 'cancel_execution' };
325
+ if (currentConversationId.value) {
326
+ cancelPayload.conversationId = currentConversationId.value;
327
+ }
328
+ wsSend(cancelPayload);
240
329
  }
241
330
 
242
331
  function dequeueNext() {
@@ -249,6 +338,9 @@ const App = {
249
338
  };
250
339
  messages.value.push(userMsg);
251
340
  isProcessing.value = true;
341
+ if (currentConversationId.value) {
342
+ processingConversations.value[currentConversationId.value] = true;
343
+ }
252
344
  wsSend(queued.payload);
253
345
  scrollToBottom(true);
254
346
  }
@@ -311,6 +403,8 @@ const App = {
311
403
  requestSessionList: sidebar.requestSessionList,
312
404
  formatRelativeTime,
313
405
  groupedSessions: sidebar.groupedSessions,
406
+ isSessionProcessing: sidebar.isSessionProcessing,
407
+ processingConversations,
314
408
  // Folder picker
315
409
  folderPickerOpen, folderPickerPath, folderPickerEntries,
316
410
  folderPickerLoading, folderPickerSelected,
@@ -395,7 +489,7 @@ const App = {
395
489
  </div>
396
490
  <div class="sidebar-workdir-header">
397
491
  <div class="sidebar-workdir-label">Working Directory</div>
398
- <button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory" :disabled="isProcessing">
492
+ <button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory">
399
493
  <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
400
494
  </button>
401
495
  </div>
@@ -427,7 +521,7 @@ const App = {
427
521
  </button>
428
522
  </div>
429
523
 
430
- <button class="new-conversation-btn" @click="newConversation" :disabled="isProcessing">
524
+ <button class="new-conversation-btn" @click="newConversation">
431
525
  <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
432
526
  New conversation
433
527
  </button>
@@ -443,9 +537,10 @@ const App = {
443
537
  <div class="session-group-label">{{ group.label }}</div>
444
538
  <div
445
539
  v-for="s in group.sessions" :key="s.sessionId"
446
- :class="['session-item', { active: currentClaudeSessionId === s.sessionId }]"
540
+ :class="['session-item', { active: currentClaudeSessionId === s.sessionId, processing: isSessionProcessing(s.sessionId) }]"
447
541
  @click="renamingSessionId !== s.sessionId && resumeSession(s)"
448
542
  :title="s.preview"
543
+ :aria-label="(s.title || s.sessionId.slice(0, 8)) + (isSessionProcessing(s.sessionId) ? ' (processing)' : '')"
449
544
  >
450
545
  <div v-if="renamingSessionId === s.sessionId" class="session-rename-row">
451
546
  <input
@@ -462,7 +557,7 @@ const App = {
462
557
  <div v-else class="session-title">{{ s.title }}</div>
463
558
  <div class="session-meta">
464
559
  <span>{{ formatRelativeTime(s.lastModified) }}</span>
465
- <span v-if="renamingSessionId !== s.sessionId && !isProcessing" class="session-actions">
560
+ <span v-if="renamingSessionId !== s.sessionId" class="session-actions">
466
561
  <button
467
562
  class="session-rename-btn"
468
563
  @click.stop="startRename(s)"
package/web/iPad.webp ADDED
Binary file
Binary file