@agent-link/server 0.1.18 → 0.1.20

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 (3) hide show
  1. package/package.json +1 -1
  2. package/web/app.js +55 -22
  3. package/web/style.css +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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
@@ -102,6 +112,15 @@ const App = {
102
112
  const sessionId = ref('');
103
113
  const error = ref('');
104
114
  const messages = ref([]);
115
+ const visibleLimit = ref(50);
116
+ const hasMoreMessages = computed(() => messages.value.length > visibleLimit.value);
117
+ const visibleMessages = computed(() => {
118
+ if (messages.value.length <= visibleLimit.value) return messages.value;
119
+ return messages.value.slice(messages.value.length - visibleLimit.value);
120
+ });
121
+ function loadMoreMessages() {
122
+ visibleLimit.value += 50;
123
+ }
105
124
  const inputText = ref('');
106
125
  const isProcessing = ref(false);
107
126
  const isCompacting = ref(false);
@@ -273,8 +292,8 @@ const App = {
273
292
  // Progressive text reveal state
274
293
  let pendingText = '';
275
294
  let revealTimer = null;
276
- const CHARS_PER_TICK = 3;
277
- const TICK_MS = 12;
295
+ const CHARS_PER_TICK = 5;
296
+ const TICK_MS = 16;
278
297
 
279
298
  function startReveal() {
280
299
  if (revealTimer !== null) return;
@@ -336,11 +355,14 @@ const App = {
336
355
  return match ? match[1] : null;
337
356
  }
338
357
 
358
+ let _scrollTimer = null;
339
359
  function scrollToBottom() {
340
- nextTick(() => {
360
+ if (_scrollTimer) return;
361
+ _scrollTimer = setTimeout(() => {
362
+ _scrollTimer = null;
341
363
  const el = document.querySelector('.message-list');
342
364
  if (el) el.scrollTop = el.scrollHeight;
343
- });
365
+ }, 50);
344
366
  }
345
367
 
346
368
  // ── Auto-resize textarea ──
@@ -407,7 +429,7 @@ const App = {
407
429
 
408
430
  // ── Rendered markdown for assistant messages ──
409
431
  function getRenderedContent(msg) {
410
- if (msg.role !== 'assistant') return msg.content;
432
+ if (msg.role !== 'assistant' && !msg.isCommandOutput) return msg.content;
411
433
  return renderMarkdown(msg.content);
412
434
  }
413
435
 
@@ -421,11 +443,10 @@ const App = {
421
443
  }
422
444
 
423
445
  // ── Check if previous message is also assistant (to suppress repeated label) ──
424
- function isPrevAssistant(msg) {
425
- const idx = messages.value.indexOf(msg);
446
+ function isPrevAssistant(idx) {
426
447
  if (idx <= 0) return false;
427
- const prev = messages.value[idx - 1];
428
- return prev.role === 'assistant' || prev.role === 'tool';
448
+ const prev = visibleMessages.value[idx - 1];
449
+ return prev && (prev.role === 'assistant' || prev.role === 'tool');
429
450
  }
430
451
 
431
452
  // ── Context summary toggle ──
@@ -664,6 +685,7 @@ const App = {
664
685
  if (window.innerWidth <= 768) sidebarOpen.value = false;
665
686
  // Clear current conversation
666
687
  messages.value = [];
688
+ visibleLimit.value = 50;
667
689
  messageIdCounter = 0;
668
690
  streamingMessageId = null;
669
691
  pendingText = '';
@@ -685,6 +707,7 @@ const App = {
685
707
  // Auto-close sidebar on mobile
686
708
  if (window.innerWidth <= 768) sidebarOpen.value = false;
687
709
  messages.value = [];
710
+ visibleLimit.value = 50;
688
711
  messageIdCounter = 0;
689
712
  streamingMessageId = null;
690
713
  pendingText = '';
@@ -973,6 +996,7 @@ const App = {
973
996
  } else if (msg.type === 'workdir_changed') {
974
997
  workDir.value = msg.workDir;
975
998
  messages.value = [];
999
+ visibleLimit.value = 50;
976
1000
  messageIdCounter = 0;
977
1001
  streamingMessageId = null;
978
1002
  pendingText = '';
@@ -1045,24 +1069,29 @@ const App = {
1045
1069
  }
1046
1070
  }
1047
1071
 
1048
- // Apply syntax highlighting after DOM updates
1049
- watch(messages, () => {
1050
- nextTick(() => {
1072
+ // Apply syntax highlighting after DOM updates (throttled)
1073
+ let _hlTimer = null;
1074
+ function scheduleHighlight() {
1075
+ if (_hlTimer) return;
1076
+ _hlTimer = setTimeout(() => {
1077
+ _hlTimer = null;
1051
1078
  if (typeof hljs !== 'undefined') {
1052
1079
  document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
1053
1080
  hljs.highlightElement(block);
1054
1081
  block.dataset.highlighted = 'true';
1055
1082
  });
1056
1083
  }
1057
- });
1058
- }, { deep: true });
1084
+ }, 300);
1085
+ }
1086
+ watch(messages, () => { nextTick(scheduleHighlight); }, { deep: true });
1059
1087
 
1060
1088
  onMounted(() => { connect(); });
1061
1089
  onUnmounted(() => { if (ws) ws.close(); });
1062
1090
 
1063
1091
  return {
1064
1092
  status, agentName, hostname, workDir, sessionId, error,
1065
- messages, inputText, isProcessing, isCompacting, canSend, inputRef,
1093
+ messages, visibleMessages, hasMoreMessages, loadMoreMessages,
1094
+ inputText, isProcessing, isCompacting, canSend, inputRef,
1066
1095
  sendMessage, handleKeydown, cancelExecution,
1067
1096
  getRenderedContent, copyMessage, toggleTool, isPrevAssistant, toggleContextSummary,
1068
1097
  getToolIcon, getToolSummary, isEditTool, getEditDiffHtml, getFormattedToolInput, autoResize,
@@ -1192,7 +1221,11 @@ const App = {
1192
1221
  <span>Loading conversation history...</span>
1193
1222
  </div>
1194
1223
 
1195
- <div v-for="msg in messages" :key="msg.id" :class="['message', 'message-' + msg.role]">
1224
+ <div v-if="hasMoreMessages" class="load-more-wrapper">
1225
+ <button class="load-more-btn" @click="loadMoreMessages">Load earlier messages</button>
1226
+ </div>
1227
+
1228
+ <div v-for="(msg, msgIdx) in visibleMessages" :key="msg.id" :class="['message', 'message-' + msg.role]">
1196
1229
 
1197
1230
  <!-- User message -->
1198
1231
  <template v-if="msg.role === 'user'">
@@ -1214,7 +1247,7 @@ const App = {
1214
1247
 
1215
1248
  <!-- Assistant message (markdown) -->
1216
1249
  <template v-else-if="msg.role === 'assistant'">
1217
- <div v-if="!isPrevAssistant(msg)" class="message-role-label assistant-label">Claude</div>
1250
+ <div v-if="!isPrevAssistant(msgIdx)" class="message-role-label assistant-label">Claude</div>
1218
1251
  <div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]">
1219
1252
  <div class="message-actions">
1220
1253
  <button class="icon-btn" @click="copyMessage(msg)" :title="msg.copied ? 'Copied!' : 'Copy'">
package/web/style.css CHANGED
@@ -1171,6 +1171,30 @@ body {
1171
1171
  animation: spin 0.8s linear infinite;
1172
1172
  }
1173
1173
 
1174
+ /* ── Load more button ── */
1175
+ .load-more-wrapper {
1176
+ display: flex;
1177
+ justify-content: center;
1178
+ padding: 0.75rem 0;
1179
+ }
1180
+
1181
+ .load-more-btn {
1182
+ background: var(--bg-tertiary);
1183
+ color: var(--text-secondary);
1184
+ border: 1px solid var(--border);
1185
+ border-radius: 6px;
1186
+ padding: 0.4rem 1.2rem;
1187
+ font-size: 0.8rem;
1188
+ cursor: pointer;
1189
+ transition: background 0.15s, color 0.15s;
1190
+ }
1191
+
1192
+ .load-more-btn:hover {
1193
+ background: var(--accent);
1194
+ color: #fff;
1195
+ border-color: var(--accent);
1196
+ }
1197
+
1174
1198
  /* ── Typing indicator ── */
1175
1199
  .typing-indicator {
1176
1200
  display: flex;