@agent-link/server 0.1.17 → 0.1.19

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 (2) hide show
  1. package/package.json +1 -1
  2. package/web/app.js +44 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "AgentLink relay server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/web/app.js CHANGED
@@ -15,11 +15,16 @@ if (typeof marked !== 'undefined') {
15
15
  });
16
16
  }
17
17
 
18
+ const _mdCache = new Map();
19
+
18
20
  function renderMarkdown(text) {
19
21
  if (!text) return '';
22
+ const cached = _mdCache.get(text);
23
+ if (cached) return cached;
24
+ let html;
20
25
  try {
21
26
  if (typeof marked !== 'undefined') {
22
- let html = marked.parse(text);
27
+ html = marked.parse(text);
23
28
  // Add copy buttons to code blocks
24
29
  html = html.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
25
30
  (match, attrs, code) => {
@@ -36,11 +41,16 @@ function renderMarkdown(text) {
36
41
  </div>`;
37
42
  }
38
43
  );
39
- return html;
44
+ } else {
45
+ html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
40
46
  }
41
- } catch {}
42
- // Fallback: escape HTML
43
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
47
+ } catch {
48
+ html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
49
+ }
50
+ // Only cache completed (non-streaming) messages; streaming text changes every tick
51
+ if (_mdCache.size > 500) _mdCache.clear();
52
+ _mdCache.set(text, html);
53
+ return html;
44
54
  }
45
55
 
46
56
  // Global code copy handler
@@ -273,8 +283,8 @@ const App = {
273
283
  // Progressive text reveal state
274
284
  let pendingText = '';
275
285
  let revealTimer = null;
276
- const CHARS_PER_TICK = 3;
277
- const TICK_MS = 12;
286
+ const CHARS_PER_TICK = 5;
287
+ const TICK_MS = 16;
278
288
 
279
289
  function startReveal() {
280
290
  if (revealTimer !== null) return;
@@ -336,11 +346,14 @@ const App = {
336
346
  return match ? match[1] : null;
337
347
  }
338
348
 
349
+ let _scrollTimer = null;
339
350
  function scrollToBottom() {
340
- nextTick(() => {
351
+ if (_scrollTimer) return;
352
+ _scrollTimer = setTimeout(() => {
353
+ _scrollTimer = null;
341
354
  const el = document.querySelector('.message-list');
342
355
  if (el) el.scrollTop = el.scrollHeight;
343
- });
356
+ }, 50);
344
357
  }
345
358
 
346
359
  // ── Auto-resize textarea ──
@@ -407,7 +420,7 @@ const App = {
407
420
 
408
421
  // ── Rendered markdown for assistant messages ──
409
422
  function getRenderedContent(msg) {
410
- if (msg.role !== 'assistant') return msg.content;
423
+ if (msg.role !== 'assistant' && !msg.isCommandOutput) return msg.content;
411
424
  return renderMarkdown(msg.content);
412
425
  }
413
426
 
@@ -421,8 +434,7 @@ const App = {
421
434
  }
422
435
 
423
436
  // ── Check if previous message is also assistant (to suppress repeated label) ──
424
- function isPrevAssistant(msg) {
425
- const idx = messages.value.indexOf(msg);
437
+ function isPrevAssistant(idx) {
426
438
  if (idx <= 0) return false;
427
439
  const prev = messages.value[idx - 1];
428
440
  return prev.role === 'assistant' || prev.role === 'tool';
@@ -852,6 +864,15 @@ const App = {
852
864
  isCompacting.value = false;
853
865
  } else if (msg.type === 'claude_output') {
854
866
  handleClaudeOutput(msg);
867
+ } else if (msg.type === 'command_output') {
868
+ flushReveal();
869
+ finalizeStreamingMsg();
870
+ messages.value.push({
871
+ id: ++messageIdCounter, role: 'user',
872
+ content: msg.content, isCommandOutput: true,
873
+ timestamp: new Date(),
874
+ });
875
+ scrollToBottom();
855
876
  } else if (msg.type === 'context_compaction') {
856
877
  if (msg.status === 'started') {
857
878
  isCompacting.value = true;
@@ -1034,44 +1055,23 @@ const App = {
1034
1055
  scrollToBottom();
1035
1056
  return;
1036
1057
  }
1037
-
1038
- // Command output (e.g. /cost, /context) — user message with content containing stdout/stderr tags
1039
- if (data.type === 'user' && data.message && data.message.content) {
1040
- const raw = typeof data.message.content === 'string'
1041
- ? data.message.content
1042
- : Array.isArray(data.message.content)
1043
- ? data.message.content.filter(b => b.type === 'text').map(b => b.text || '').join('')
1044
- : '';
1045
- if (raw.trim()) {
1046
- const stdoutMatch = raw.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
1047
- const stderrMatch = raw.match(/<local-command-stderr>([\s\S]*?)<\/local-command-stderr>/);
1048
- const cmdOutput = (stdoutMatch && stdoutMatch[1].trim()) || (stderrMatch && stderrMatch[1].trim());
1049
- if (cmdOutput) {
1050
- flushReveal();
1051
- finalizeStreamingMsg();
1052
- messages.value.push({
1053
- id: ++messageIdCounter, role: 'user',
1054
- content: cmdOutput, isCommandOutput: true,
1055
- timestamp: new Date(),
1056
- });
1057
- scrollToBottom();
1058
- return;
1059
- }
1060
- }
1061
- }
1062
1058
  }
1063
1059
 
1064
- // Apply syntax highlighting after DOM updates
1065
- watch(messages, () => {
1066
- nextTick(() => {
1060
+ // Apply syntax highlighting after DOM updates (throttled)
1061
+ let _hlTimer = null;
1062
+ function scheduleHighlight() {
1063
+ if (_hlTimer) return;
1064
+ _hlTimer = setTimeout(() => {
1065
+ _hlTimer = null;
1067
1066
  if (typeof hljs !== 'undefined') {
1068
1067
  document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
1069
1068
  hljs.highlightElement(block);
1070
1069
  block.dataset.highlighted = 'true';
1071
1070
  });
1072
1071
  }
1073
- });
1074
- }, { deep: true });
1072
+ }, 300);
1073
+ }
1074
+ watch(messages, () => { nextTick(scheduleHighlight); }, { deep: true });
1075
1075
 
1076
1076
  onMounted(() => { connect(); });
1077
1077
  onUnmounted(() => { if (ws) ws.close(); });
@@ -1208,7 +1208,7 @@ const App = {
1208
1208
  <span>Loading conversation history...</span>
1209
1209
  </div>
1210
1210
 
1211
- <div v-for="msg in messages" :key="msg.id" :class="['message', 'message-' + msg.role]">
1211
+ <div v-for="(msg, msgIdx) in messages" :key="msg.id" :class="['message', 'message-' + msg.role]">
1212
1212
 
1213
1213
  <!-- User message -->
1214
1214
  <template v-if="msg.role === 'user'">
@@ -1230,7 +1230,7 @@ const App = {
1230
1230
 
1231
1231
  <!-- Assistant message (markdown) -->
1232
1232
  <template v-else-if="msg.role === 'assistant'">
1233
- <div v-if="!isPrevAssistant(msg)" class="message-role-label assistant-label">Claude</div>
1233
+ <div v-if="!isPrevAssistant(msgIdx)" class="message-role-label assistant-label">Claude</div>
1234
1234
  <div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]">
1235
1235
  <div class="message-actions">
1236
1236
  <button class="icon-btn" @click="copyMessage(msg)" :title="msg.copied ? 'Copied!' : 'Copy'">