@agent-link/server 0.1.183 → 0.1.185

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.183",
3
+ "version": "0.1.185",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -406,26 +406,17 @@ const App = {
406
406
  });
407
407
  setFilePreview(filePreview);
408
408
 
409
- // Track mobile state on resize
410
- let _resizeHandler = () => { isMobile.value = window.innerWidth <= 768; };
409
+ // Track mobile state on resize (rAF-throttled)
410
+ let _resizeRafId = 0;
411
+ let _resizeHandler = () => {
412
+ if (_resizeRafId) return;
413
+ _resizeRafId = requestAnimationFrame(() => {
414
+ _resizeRafId = 0;
415
+ isMobile.value = window.innerWidth <= 768;
416
+ });
417
+ };
411
418
  window.addEventListener('resize', _resizeHandler);
412
419
 
413
- // Fix Chrome mobile: when virtual keyboard dismisses, Chrome doesn't
414
- // restore scroll position (Safari does this natively). Detect keyboard
415
- // dismiss via visualViewport height increase and reset page scroll.
416
- let _vvResizeHandler = null;
417
- if (window.visualViewport) {
418
- let _lastVVHeight = window.visualViewport.height;
419
- _vvResizeHandler = () => {
420
- const h = window.visualViewport.height;
421
- if (h > _lastVVHeight && !document.activeElement?.matches?.('input, textarea')) {
422
- window.scrollTo(0, 0);
423
- }
424
- _lastVVHeight = h;
425
- };
426
- window.visualViewport.addEventListener('resize', _vvResizeHandler);
427
- }
428
-
429
420
  // Close workdir menu on outside click or Escape
430
421
  let _workdirMenuClickHandler = (e) => {
431
422
  if (!workdirMenuOpen.value) return;
@@ -442,10 +433,11 @@ const App = {
442
433
 
443
434
  // ── Computed ──
444
435
  const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
436
+ const hasPendingQuestion = computed(() => messages.value.some(m => m.role === 'ask-question' && !m.answered));
445
437
  const canSend = computed(() =>
446
- status.value === 'Connected' && hasInput.value && !isCompacting.value
447
- && !messages.value.some(m => m.role === 'ask-question' && !m.answered)
438
+ status.value === 'Connected' && hasInput.value && !isCompacting.value && !hasPendingQuestion.value
448
439
  );
440
+ const hasStreamingMessage = computed(() => messages.value.some(m => m.isStreaming));
449
441
 
450
442
  // ── Slash command menu ──
451
443
  const slashMenuVisible = computed(() => {
@@ -701,7 +693,6 @@ const App = {
701
693
  onUnmounted(() => {
702
694
  closeWs(); streaming.cleanup(); cleanupScroll(); cleanupHighlight();
703
695
  window.removeEventListener('resize', _resizeHandler);
704
- if (_vvResizeHandler && window.visualViewport) window.visualViewport.removeEventListener('resize', _vvResizeHandler);
705
696
  document.removeEventListener('click', _workdirMenuClickHandler);
706
697
  document.removeEventListener('click', _slashMenuClickOutside);
707
698
  document.removeEventListener('keydown', _workdirMenuKeyHandler);
@@ -712,7 +703,7 @@ const App = {
712
703
  status, agentName, hostname, workDir, sessionId, error,
713
704
  serverVersion, agentVersion, latency,
714
705
  messages, visibleMessages, hasMoreMessages, loadMoreMessages,
715
- inputText, isProcessing, isCompacting, canSend, hasInput, inputRef, queuedMessages, usageStats,
706
+ inputText, isProcessing, isCompacting, canSend, hasInput, hasStreamingMessage, inputRef, queuedMessages, usageStats,
716
707
  slashMenuVisible, filteredSlashCommands, slashMenuIndex, slashMenuOpen, selectSlashCommand, openSlashMenu,
717
708
  sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
718
709
  // Side question (/btw)
@@ -2050,7 +2041,7 @@ const App = {
2050
2041
  </span>
2051
2042
  <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2052
2043
  </div>
2053
- <div v-show="msg.expanded" class="tool-expand">
2044
+ <div v-if="msg.expanded" class="tool-expand">
2054
2045
  <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2055
2046
  <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2056
2047
  <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
@@ -2107,7 +2098,7 @@ const App = {
2107
2098
  </span>
2108
2099
  <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2109
2100
  </div>
2110
- <div v-show="msg.expanded" class="tool-expand">
2101
+ <div v-if="msg.expanded" class="tool-expand">
2111
2102
  <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2112
2103
  <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2113
2104
  <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
@@ -2411,7 +2402,7 @@ const App = {
2411
2402
  </span>
2412
2403
  <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2413
2404
  </div>
2414
- <div v-show="msg.expanded" class="tool-expand team-agent-tool-expand">
2405
+ <div v-if="msg.expanded" class="tool-expand team-agent-tool-expand">
2415
2406
  <pre v-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
2416
2407
  <div v-if="msg.toolOutput" class="team-agent-tool-result">
2417
2408
  <div class="team-agent-tool-result-label">{{ t('team.agentResult') }}</div>
@@ -2432,7 +2423,7 @@ const App = {
2432
2423
  </span>
2433
2424
  <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2434
2425
  </div>
2435
- <div v-show="msg.expanded" class="tool-expand">
2426
+ <div v-if="msg.expanded" class="tool-expand">
2436
2427
  <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2437
2428
  <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2438
2429
  <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
@@ -2507,7 +2498,7 @@ const App = {
2507
2498
  </div>
2508
2499
  </div>
2509
2500
 
2510
- <div v-if="isProcessing && !messages.some(m => m.isStreaming)" class="typing-indicator">
2501
+ <div v-if="isProcessing && !hasStreamingMessage" class="typing-indicator">
2511
2502
  <span></span><span></span><span></span>
2512
2503
  </div>
2513
2504
  </div>
package/web/css/base.css CHANGED
@@ -236,6 +236,25 @@ body {
236
236
  margin-top: 0.75rem;
237
237
  }
238
238
 
239
+ /* ── Shared keyframes ── */
240
+ @keyframes spin {
241
+ to { transform: rotate(360deg); }
242
+ }
243
+
244
+ @keyframes pulse {
245
+ 0%, 100% { opacity: 1; }
246
+ 50% { opacity: 0.3; }
247
+ }
248
+
249
+ /* ── Reduced motion ── */
250
+ @media (prefers-reduced-motion: reduce) {
251
+ *, *::before, *::after {
252
+ animation-duration: 0.01ms !important;
253
+ animation-iteration-count: 1 !important;
254
+ transition-duration: 0.01ms !important;
255
+ }
256
+ }
257
+
239
258
  /* ── Main body (sidebar + chat) ── */
240
259
  .main-body {
241
260
  flex: 1;
@@ -151,10 +151,6 @@
151
151
  margin-left: auto;
152
152
  }
153
153
 
154
- @keyframes spin {
155
- to { transform: rotate(360deg); }
156
- }
157
-
158
154
  .file-tree-empty {
159
155
  padding: 4px 8px;
160
156
  font-size: 0.75rem;
package/web/css/input.css CHANGED
@@ -225,7 +225,6 @@
225
225
  align-items: center;
226
226
  justify-content: center;
227
227
  gap: 16px;
228
- backdrop-filter: blur(2px);
229
228
  }
230
229
  .workdir-switching-spinner {
231
230
  width: 36px;
@@ -233,10 +232,7 @@
233
232
  border: 3px solid rgba(255, 255, 255, 0.2);
234
233
  border-top-color: rgba(255, 255, 255, 0.8);
235
234
  border-radius: 50%;
236
- animation: workdir-spin 0.7s linear infinite;
237
- }
238
- @keyframes workdir-spin {
239
- to { transform: rotate(360deg); }
235
+ animation: spin 0.7s linear infinite;
240
236
  }
241
237
  .workdir-switching-text {
242
238
  color: rgba(255, 255, 255, 0.9);
package/web/css/loop.css CHANGED
@@ -185,7 +185,7 @@
185
185
  border: 1px solid var(--border);
186
186
  border-radius: 6px;
187
187
  cursor: pointer;
188
- transition: all 0.15s;
188
+ transition: color 0.15s, border-color 0.15s;
189
189
  white-space: nowrap;
190
190
  }
191
191
  .loop-action-btn:hover:not(:disabled) {
@@ -353,11 +353,7 @@
353
353
  }
354
354
  .loop-exec-status-running {
355
355
  color: var(--accent);
356
- animation: loop-spin 1s linear infinite;
357
- }
358
- @keyframes loop-spin {
359
- from { transform: rotate(0deg); }
360
- to { transform: rotate(360deg); }
356
+ animation: spin 1s linear infinite;
361
357
  }
362
358
  .loop-exec-status-success {
363
359
  color: #10B981;
@@ -428,11 +424,7 @@
428
424
  height: 8px;
429
425
  border-radius: 50%;
430
426
  background: var(--accent);
431
- animation: loop-pulse 1.5s ease-in-out infinite;
432
- }
433
- @keyframes loop-pulse {
434
- 0%, 100% { opacity: 1; }
435
- 50% { opacity: 0.3; }
427
+ animation: pulse 1.5s ease-in-out infinite;
436
428
  }
437
429
 
438
430
  /* ── Modal dialog (generic) ── */
@@ -210,11 +210,6 @@
210
210
  transform: rotate(-90deg);
211
211
  }
212
212
 
213
- @keyframes spin {
214
- from { transform: rotate(0deg); }
215
- to { transform: rotate(360deg); }
216
- }
217
-
218
213
  .spinning {
219
214
  animation: spin 0.8s linear infinite;
220
215
  }
@@ -329,12 +324,7 @@
329
324
  background: var(--accent);
330
325
  margin-right: 6px;
331
326
  vertical-align: middle;
332
- animation: pulse-dot 1.5s ease-in-out infinite;
333
- }
334
-
335
- @keyframes pulse-dot {
336
- 0%, 100% { opacity: 1; }
337
- 50% { opacity: 0.3; }
327
+ animation: pulse 1.5s ease-in-out infinite;
338
328
  }
339
329
 
340
330
  .session-meta {
package/web/css/team.css CHANGED
@@ -21,7 +21,7 @@
21
21
  font-size: 0.75rem;
22
22
  font-weight: 500;
23
23
  cursor: pointer;
24
- transition: all 0.15s;
24
+ transition: color 0.15s, background 0.15s, box-shadow 0.15s;
25
25
  }
26
26
 
27
27
  .team-mode-btn.active {
@@ -194,7 +194,7 @@
194
194
  color: var(--text-secondary);
195
195
  font-size: 0.78rem;
196
196
  cursor: pointer;
197
- transition: all 0.15s;
197
+ transition: color 0.15s, border-color 0.15s;
198
198
  }
199
199
  .team-lead-prompt-reset:hover {
200
200
  color: var(--text-primary);
@@ -258,7 +258,7 @@
258
258
  color: var(--text-secondary);
259
259
  font-size: 0.85rem;
260
260
  cursor: pointer;
261
- transition: all 0.15s;
261
+ transition: color 0.15s, border-color 0.15s;
262
262
  }
263
263
 
264
264
  .team-create-cancel:hover {
@@ -293,7 +293,7 @@
293
293
  padding: 14px 16px;
294
294
  border: 1px solid var(--border);
295
295
  border-radius: 10px;
296
- transition: all 0.15s;
296
+ transition: border-color 0.15s, background 0.15s;
297
297
  background: linear-gradient(135deg, rgba(255,255,255,0.02) 0%, transparent 100%);
298
298
  }
299
299
 
@@ -354,7 +354,7 @@
354
354
  border: 1px solid var(--accent);
355
355
  border-radius: 6px;
356
356
  cursor: pointer;
357
- transition: all 0.15s;
357
+ transition: color 0.15s, background 0.15s;
358
358
  white-space: nowrap;
359
359
  }
360
360
 
@@ -500,7 +500,7 @@
500
500
  color: var(--error);
501
501
  font-size: 0.8rem;
502
502
  cursor: pointer;
503
- transition: all 0.15s;
503
+ transition: background 0.15s;
504
504
  }
505
505
 
506
506
  .team-dissolve-btn:hover {
@@ -515,7 +515,7 @@
515
515
  color: var(--text-secondary);
516
516
  font-size: 0.8rem;
517
517
  cursor: pointer;
518
- transition: all 0.15s;
518
+ transition: color 0.15s, border-color 0.15s;
519
519
  }
520
520
 
521
521
  .team-back-btn:hover {
@@ -531,7 +531,7 @@
531
531
  color: var(--accent);
532
532
  font-size: 0.8rem;
533
533
  cursor: pointer;
534
- transition: all 0.15s;
534
+ transition: background 0.15s;
535
535
  }
536
536
 
537
537
  .team-new-btn:hover {
@@ -1111,7 +1111,7 @@
1111
1111
  color: var(--text-secondary);
1112
1112
  font-size: 0.78rem;
1113
1113
  cursor: pointer;
1114
- transition: all 0.15s;
1114
+ transition: color 0.15s, border-color 0.15s;
1115
1115
  margin-right: 4px;
1116
1116
  }
1117
1117
 
package/web/css/tools.css CHANGED
@@ -97,8 +97,8 @@
97
97
  }
98
98
 
99
99
  @keyframes toolExpand {
100
- from { opacity: 0; max-height: 0; }
101
- to { opacity: 1; max-height: 500px; }
100
+ from { opacity: 0; }
101
+ to { opacity: 1; }
102
102
  }
103
103
 
104
104
  .tool-block {
@@ -44,7 +44,8 @@ export function createHighlightScheduler() {
44
44
  _hlTimer = setTimeout(() => {
45
45
  _hlTimer = null;
46
46
  if (typeof hljs !== 'undefined') {
47
- document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
47
+ const root = document.querySelector('.message-list') || document;
48
+ root.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
48
49
  hljs.highlightElement(block);
49
50
  block.dataset.highlighted = 'true';
50
51
  });
@@ -1,6 +1,13 @@
1
1
  // ── History batch building & background conversation routing ──────────────────
2
2
  import { isContextSummary } from './messageHelpers.js';
3
3
 
4
+ function findLast(arr, predicate) {
5
+ for (let i = arr.length - 1; i >= 0; i--) {
6
+ if (predicate(arr[i])) return arr[i];
7
+ }
8
+ return undefined;
9
+ }
10
+
4
11
  /**
5
12
  * Convert a history array (from conversation_resumed) into a batch of UI messages.
6
13
  * @param {Array} history - Array of {role, content, ...} from the agent
@@ -133,11 +140,14 @@ export function routeToBackgroundConversation(deps, convId, msg) {
133
140
  const msgs = cache.messages;
134
141
  const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
135
142
  if (last && last.role === 'assistant' && last.isStreaming) {
136
- last.content += data.delta;
143
+ if (!last._chunks) last._chunks = [last.content];
144
+ last._chunks.push(data.delta);
145
+ last.content = last._chunks.join('');
137
146
  } else {
138
147
  msgs.push({
139
148
  id: ++cache.messageIdCounter, role: 'assistant',
140
149
  content: data.delta, isStreaming: true, timestamp: new Date(),
150
+ _chunks: [data.delta],
141
151
  });
142
152
  }
143
153
  } else if (data.type === 'tool_use' && data.tools) {
@@ -206,7 +216,7 @@ export function routeToBackgroundConversation(deps, convId, msg) {
206
216
  });
207
217
  } else if (msg.status === 'completed') {
208
218
  cache.isCompacting = false;
209
- const startMsg = [...cache.messages].reverse().find(m => m.isCompactStart && !m.compactDone);
219
+ const startMsg = findLast(cache.messages, m => m.isCompactStart && !m.compactDone);
210
220
  if (startMsg) {
211
221
  startMsg.content = 'Context compacted';
212
222
  startMsg.compactDone = true;
@@ -7,6 +7,13 @@ const MAX_RECONNECT_ATTEMPTS = 50;
7
7
  const RECONNECT_BASE_DELAY = 1000;
8
8
  const RECONNECT_MAX_DELAY = 15000;
9
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
+
10
17
  /**
11
18
  * Creates the WebSocket connection controller.
12
19
  * @param {object} deps - All reactive state and callbacks needed
@@ -469,7 +476,7 @@ export function createConnection(deps) {
469
476
  } else if (msg.status === 'completed') {
470
477
  isCompacting.value = false;
471
478
  // Update the start message to show completed
472
- const startMsg = [...messages.value].reverse().find(m => m.isCompactStart && !m.compactDone);
479
+ const startMsg = findLast(messages.value, m => m.isCompactStart && !m.compactDone);
473
480
  if (startMsg) {
474
481
  startMsg.content = t('system.contextCompacted');
475
482
  startMsg.compactDone = true;
@@ -4,6 +4,13 @@ import { renderMarkdown } from './markdown.js';
4
4
  // ── Helpers ──────────────────────────────────────────────────────────────────
5
5
  const CONTEXT_SUMMARY_PREFIX = 'This session is being continued from a previous conversation';
6
6
 
7
+ function parseToolInput(msg) {
8
+ if (msg._parsedInput !== undefined) return msg._parsedInput;
9
+ try { msg._parsedInput = JSON.parse(msg.toolInput); }
10
+ catch { msg._parsedInput = null; }
11
+ return msg._parsedInput;
12
+ }
13
+
7
14
  export function isContextSummary(text) {
8
15
  return typeof text === 'string' && text.trimStart().startsWith(CONTEXT_SUMMARY_PREFIX);
9
16
  }
@@ -28,6 +35,10 @@ export function formatTimestamp(ts) {
28
35
 
29
36
  export function getRenderedContent(msg) {
30
37
  if (msg.role !== 'assistant' && !msg.isCommandOutput) return msg.content;
38
+ if (msg.isStreaming) {
39
+ const t = msg.content || '';
40
+ return t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
41
+ }
31
42
  return renderMarkdown(msg.content);
32
43
  }
33
44
 
@@ -55,9 +66,9 @@ export function toggleTool(msg) {
55
66
 
56
67
  export function getToolSummary(msg, t) {
57
68
  const name = msg.toolName;
58
- const input = msg.toolInput;
69
+ const obj = parseToolInput(msg);
70
+ if (!obj) return '';
59
71
  try {
60
- const obj = JSON.parse(input);
61
72
  if (name === 'Read' && obj.file_path) return obj.file_path;
62
73
  if (name === 'Edit' && obj.file_path) return obj.file_path;
63
74
  if (name === 'Write' && obj.file_path) return obj.file_path;
@@ -83,8 +94,9 @@ export function isEditTool(msg) {
83
94
 
84
95
  export function getFormattedToolInput(msg, t) {
85
96
  if (!msg.toolInput) return null;
97
+ const obj = parseToolInput(msg);
98
+ if (!obj) return null;
86
99
  try {
87
- const obj = JSON.parse(msg.toolInput);
88
100
  const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
89
101
  const name = msg.toolName;
90
102
 
@@ -169,8 +181,9 @@ export function getFormattedToolInput(msg, t) {
169
181
  }
170
182
 
171
183
  export function getEditDiffHtml(msg, t) {
184
+ const obj = parseToolInput(msg);
185
+ if (!obj) return null;
172
186
  try {
173
- const obj = JSON.parse(msg.toolInput);
174
187
  if (!obj.old_string && !obj.new_string) return null;
175
188
  const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
176
189
  const filePath = obj.file_path || '';
@@ -34,19 +34,20 @@ export function createStreaming({ messages, scrollToBottom }) {
34
34
  ? messages.value.find(m => m.id === streamingMessageId)
35
35
  : null;
36
36
 
37
+ const chunk = pendingText.slice(0, CHARS_PER_TICK);
38
+ pendingText = pendingText.slice(CHARS_PER_TICK);
39
+
37
40
  if (!streamMsg) {
38
41
  const id = ++messageIdCounter;
39
- const chunk = pendingText.slice(0, CHARS_PER_TICK);
40
- pendingText = pendingText.slice(CHARS_PER_TICK);
41
42
  messages.value.push({
42
43
  id, role: 'assistant', content: chunk,
43
44
  isStreaming: true, timestamp: new Date(),
45
+ _chunks: [chunk],
44
46
  });
45
47
  streamingMessageId = id;
46
48
  } else {
47
- const chunk = pendingText.slice(0, CHARS_PER_TICK);
48
- pendingText = pendingText.slice(CHARS_PER_TICK);
49
- streamMsg.content += chunk;
49
+ streamMsg._chunks.push(chunk);
50
+ streamMsg.content = streamMsg._chunks.join('');
50
51
  }
51
52
  scrollToBottom();
52
53
  if (pendingText) revealTimer = setTimeout(revealTick, TICK_MS);
@@ -58,12 +59,15 @@ export function createStreaming({ messages, scrollToBottom }) {
58
59
  const streamMsg = streamingMessageId !== null
59
60
  ? messages.value.find(m => m.id === streamingMessageId) : null;
60
61
  if (streamMsg) {
61
- streamMsg.content += pendingText;
62
+ if (!streamMsg._chunks) streamMsg._chunks = [streamMsg.content];
63
+ streamMsg._chunks.push(pendingText);
64
+ streamMsg.content = streamMsg._chunks.join('');
62
65
  } else {
63
66
  const id = ++messageIdCounter;
64
67
  messages.value.push({
65
68
  id, role: 'assistant', content: pendingText,
66
69
  isStreaming: true, timestamp: new Date(),
70
+ _chunks: [pendingText],
67
71
  });
68
72
  streamingMessageId = id;
69
73
  }