@agent-link/server 0.1.166 → 0.1.168

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.166",
3
+ "version": "0.1.168",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -67,6 +67,7 @@ const App = {
67
67
  const usageStats = ref(null);
68
68
  const inputRef = ref(null);
69
69
  const slashMenuIndex = ref(0);
70
+ const slashMenuOpen = ref(false);
70
71
 
71
72
  // Sidebar state
72
73
  const sidebarOpen = ref(window.innerWidth > 768);
@@ -425,10 +426,12 @@ const App = {
425
426
 
426
427
  // ── Slash command menu ──
427
428
  const slashMenuVisible = computed(() => {
429
+ if (slashMenuOpen.value) return true;
428
430
  const txt = inputText.value;
429
431
  return txt.startsWith('/') && !/\s/.test(txt.slice(1));
430
432
  });
431
433
  const filteredSlashCommands = computed(() => {
434
+ if (slashMenuOpen.value && !inputText.value.startsWith('/')) return SLASH_COMMANDS;
432
435
  const txt = inputText.value.toLowerCase();
433
436
  return SLASH_COMMANDS.filter(c => c.command.startsWith(txt));
434
437
  });
@@ -524,10 +527,23 @@ const App = {
524
527
  }
525
528
 
526
529
  function selectSlashCommand(cmd) {
530
+ slashMenuOpen.value = false;
527
531
  inputText.value = cmd.command;
528
532
  sendMessage();
529
533
  }
530
534
 
535
+ function openSlashMenu() {
536
+ slashMenuOpen.value = !slashMenuOpen.value;
537
+ slashMenuIndex.value = 0;
538
+ }
539
+
540
+ function _slashMenuClickOutside(e) {
541
+ if (slashMenuOpen.value && !e.target.closest('.slash-btn') && !e.target.closest('.slash-menu')) {
542
+ slashMenuOpen.value = false;
543
+ }
544
+ }
545
+ document.addEventListener('click', _slashMenuClickOutside);
546
+
531
547
  function handleKeydown(e) {
532
548
  // Slash menu key handling
533
549
  if (slashMenuVisible.value && filteredSlashCommands.value.length > 0 && !e.isComposing) {
@@ -554,6 +570,7 @@ const App = {
554
570
  }
555
571
  if (e.key === 'Escape') {
556
572
  e.preventDefault();
573
+ slashMenuOpen.value = false;
557
574
  inputText.value = '';
558
575
  return;
559
576
  }
@@ -617,6 +634,7 @@ const App = {
617
634
  closeWs(); streaming.cleanup(); cleanupScroll(); cleanupHighlight();
618
635
  window.removeEventListener('resize', _resizeHandler);
619
636
  document.removeEventListener('click', _workdirMenuClickHandler);
637
+ document.removeEventListener('click', _slashMenuClickOutside);
620
638
  document.removeEventListener('keydown', _workdirMenuKeyHandler);
621
639
  });
622
640
 
@@ -625,7 +643,7 @@ const App = {
625
643
  serverVersion, agentVersion, latency,
626
644
  messages, visibleMessages, hasMoreMessages, loadMoreMessages,
627
645
  inputText, isProcessing, isCompacting, canSend, hasInput, inputRef, queuedMessages, usageStats,
628
- slashMenuVisible, filteredSlashCommands, slashMenuIndex, selectSlashCommand,
646
+ slashMenuVisible, filteredSlashCommands, slashMenuIndex, slashMenuOpen, selectSlashCommand, openSlashMenu,
629
647
  sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
630
648
  getRenderedContent, copyMessage, toggleTool,
631
649
  isPrevAssistant: _isPrevAssistant,
@@ -2483,9 +2501,14 @@ const App = {
2483
2501
  </div>
2484
2502
  </div>
2485
2503
  <div class="input-bottom-row">
2486
- <button class="attach-btn" @click="triggerFileInput" :disabled="status !== 'Connected' || isCompacting || attachments.length >= 5" :title="t('input.attachFiles')">
2487
- <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
2488
- </button>
2504
+ <div class="input-bottom-left">
2505
+ <button class="attach-btn" @click="triggerFileInput" :disabled="status !== 'Connected' || isCompacting || attachments.length >= 5" :title="t('input.attachFiles')">
2506
+ <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
2507
+ </button>
2508
+ <button class="slash-btn" @click="openSlashMenu" :disabled="status !== 'Connected' || isCompacting" :title="t('input.slashCommands')">
2509
+ <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 21 11 3h2L9 21H7Z"/></svg>
2510
+ </button>
2511
+ </div>
2489
2512
  <button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" :title="t('input.stopGeneration')">
2490
2513
  <svg viewBox="0 0 24 24" width="14" height="14"><rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor"/></svg>
2491
2514
  </button>
package/web/css/input.css CHANGED
@@ -84,6 +84,12 @@
84
84
  padding: 0.1rem 0.25rem 0;
85
85
  }
86
86
 
87
+ .input-bottom-left {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 2px;
91
+ }
92
+
87
93
  .send-btn {
88
94
  background: var(--accent);
89
95
  color: #fff;
@@ -548,6 +554,31 @@
548
554
  cursor: not-allowed;
549
555
  }
550
556
 
557
+ /* ── Slash command button ── */
558
+ .slash-btn {
559
+ background: none;
560
+ border: none;
561
+ color: var(--text-secondary);
562
+ cursor: pointer;
563
+ display: flex;
564
+ align-items: center;
565
+ justify-content: center;
566
+ width: 32px;
567
+ height: 32px;
568
+ border-radius: 8px;
569
+ transition: color 0.15s, background 0.15s;
570
+ }
571
+
572
+ .slash-btn:hover {
573
+ color: var(--text-primary);
574
+ background: var(--bg-tertiary);
575
+ }
576
+
577
+ .slash-btn:disabled {
578
+ opacity: 0.3;
579
+ cursor: not-allowed;
580
+ }
581
+
551
582
  /* ── Drag-over highlight on input card ── */
552
583
  .input-card.drag-over {
553
584
  border-color: var(--accent);
@@ -177,6 +177,7 @@
177
177
  "input.stopGeneration": "Stop generation",
178
178
  "input.send": "Send",
179
179
  "input.remove": "Remove",
180
+ "input.slashCommands": "Slash commands",
180
181
 
181
182
  "folderPicker.title": "Select Working Directory",
182
183
  "folderPicker.pathPlaceholder": "Enter path...",
@@ -177,6 +177,7 @@
177
177
  "input.stopGeneration": "停止生成",
178
178
  "input.send": "发送",
179
179
  "input.remove": "移除",
180
+ "input.slashCommands": "快捷命令",
180
181
 
181
182
  "folderPicker.title": "选择工作目录",
182
183
  "folderPicker.pathPlaceholder": "输入路径...",
@@ -56,6 +56,7 @@ export function createConnection(deps) {
56
56
  let reconnectAttempts = 0;
57
57
  let reconnectTimer = null;
58
58
  let pingTimer = null;
59
+ let idleCheckTimer = null;
59
60
  const toolMsgMap = new Map(); // toolId -> message (for fast tool_result lookup)
60
61
 
61
62
  // ── toolMsgMap save/restore for conversation switching ──
@@ -90,6 +91,22 @@ export function createConnection(deps) {
90
91
  latency.value = null;
91
92
  }
92
93
 
94
+ // Idle-check: if isProcessing stays true with no claude_output for 15s,
95
+ // poll the agent to reconcile stale state (guards against lost turn_completed).
96
+ const IDLE_CHECK_MS = 15000;
97
+ function resetIdleCheck() {
98
+ if (idleCheckTimer) { clearTimeout(idleCheckTimer); idleCheckTimer = null; }
99
+ if (isProcessing.value) {
100
+ idleCheckTimer = setTimeout(() => {
101
+ idleCheckTimer = null;
102
+ if (isProcessing.value) wsSend({ type: 'query_active_conversations' });
103
+ }, IDLE_CHECK_MS);
104
+ }
105
+ }
106
+ function clearIdleCheck() {
107
+ if (idleCheckTimer) { clearTimeout(idleCheckTimer); idleCheckTimer = null; }
108
+ }
109
+
93
110
  function getSessionId() {
94
111
  const match = window.location.pathname.match(/^\/s\/([^/]+)/);
95
112
  return match ? match[1] : null;
@@ -118,6 +135,7 @@ export function createConnection(deps) {
118
135
  // (e.g. after reconnect before active_conversations response), self-correct
119
136
  if (!isProcessing.value) {
120
137
  isProcessing.value = true;
138
+ resetIdleCheck();
121
139
  if (currentConversationId && currentConversationId.value) {
122
140
  processingConversations.value[currentConversationId.value] = true;
123
141
  }
@@ -387,6 +405,7 @@ export function createConnection(deps) {
387
405
  if (team && msg.activeTeam) {
388
406
  team.handleActiveTeamRestore(msg.activeTeam);
389
407
  }
408
+ resetIdleCheck();
390
409
  } else if (msg.type === 'error') {
391
410
  streaming.flushReveal();
392
411
  finalizeStreamingMsg(scheduleHighlight);
@@ -399,6 +418,7 @@ export function createConnection(deps) {
399
418
  isProcessing.value = false;
400
419
  isCompacting.value = false;
401
420
  loadingSessions.value = false;
421
+ clearIdleCheck();
402
422
  if (currentConversationId && currentConversationId.value) {
403
423
  processingConversations.value[currentConversationId.value] = false;
404
424
  }
@@ -409,6 +429,7 @@ export function createConnection(deps) {
409
429
  _dequeueNext();
410
430
  } else if (msg.type === 'claude_output') {
411
431
  handleClaudeOutput(msg, scheduleHighlight);
432
+ resetIdleCheck();
412
433
  } else if (msg.type === 'session_started') {
413
434
  // Claude session ID captured — update and refresh sidebar
414
435
  currentClaudeSessionId.value = msg.claudeSessionId;
@@ -446,6 +467,7 @@ export function createConnection(deps) {
446
467
  finalizeStreamingMsg(scheduleHighlight);
447
468
  isProcessing.value = false;
448
469
  isCompacting.value = false;
470
+ clearIdleCheck();
449
471
  toolMsgMap.clear();
450
472
  if (msg.usage) usageStats.value = msg.usage;
451
473
  if (currentConversationId && currentConversationId.value) {
@@ -607,6 +629,7 @@ export function createConnection(deps) {
607
629
  ws.onclose = () => {
608
630
  sessionKey = null;
609
631
  stopPing();
632
+ clearIdleCheck();
610
633
  const wasConnected = status.value === 'Connected' || status.value === 'Connecting...';
611
634
  isProcessing.value = false;
612
635
  isCompacting.value = false;