@agent-link/server 0.1.114 → 0.1.116

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.114",
3
+ "version": "0.1.116",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  import { createStreaming } from './modules/streaming.js';
17
17
  import { createSidebar } from './modules/sidebar.js';
18
18
  import { createConnection } from './modules/connection.js';
19
+ import { createFileBrowser } from './modules/fileBrowser.js';
19
20
 
20
21
  // ── App ─────────────────────────────────────────────────────────────────────
21
22
  const App = {
@@ -94,6 +95,16 @@ const App = {
94
95
  const currentConversationId = ref(crypto.randomUUID()); // currently visible conversation
95
96
  const processingConversations = ref({}); // conversationId → boolean
96
97
 
98
+ // File browser state
99
+ const filePanelOpen = ref(false);
100
+ const filePanelWidth = ref(parseInt(localStorage.getItem('agentlink-file-panel-width'), 10) || 280);
101
+ const fileTreeRoot = ref(null);
102
+ const fileTreeLoading = ref(false);
103
+ const fileContextMenu = ref(null);
104
+ const sidebarView = ref('sessions'); // 'sessions' | 'files' (mobile only)
105
+ const isMobile = ref(window.innerWidth <= 768);
106
+ const workdirMenuOpen = ref(false);
107
+
97
108
  // ── switchConversation: save current → load target ──
98
109
  // Defined here and used by sidebar.newConversation, sidebar.resumeSession, workdir_changed
99
110
  // Needs access to streaming / connection which are created later, so we use late-binding refs.
@@ -233,7 +244,7 @@ const App = {
233
244
  switchConversation,
234
245
  });
235
246
 
236
- const { connect, wsSend, closeWs, submitPassword, setDequeueNext, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
247
+ const { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
237
248
  status, agentName, hostname, workDir, sessionId, error,
238
249
  serverVersion, agentVersion, latency,
239
250
  messages, isProcessing, isCompacting, visibleLimit, queuedMessages,
@@ -254,6 +265,32 @@ const App = {
254
265
  _restoreToolMsgMap = restoreToolMsgMap;
255
266
  _clearToolMsgMap = clearToolMsgMap;
256
267
 
268
+ // File browser module
269
+ const fileBrowser = createFileBrowser({
270
+ wsSend, workDir, inputText, inputRef, sendMessage,
271
+ filePanelOpen, filePanelWidth, fileTreeRoot, fileTreeLoading, fileContextMenu,
272
+ sidebarOpen, sidebarView,
273
+ });
274
+ setFileBrowser(fileBrowser);
275
+
276
+ // Track mobile state on resize
277
+ let _resizeHandler = () => { isMobile.value = window.innerWidth <= 768; };
278
+ window.addEventListener('resize', _resizeHandler);
279
+
280
+ // Close workdir menu on outside click or Escape
281
+ let _workdirMenuClickHandler = (e) => {
282
+ if (!workdirMenuOpen.value) return;
283
+ const row = document.querySelector('.sidebar-workdir-path-row');
284
+ const menu = document.querySelector('.workdir-menu');
285
+ if ((row && row.contains(e.target)) || (menu && menu.contains(e.target))) return;
286
+ workdirMenuOpen.value = false;
287
+ };
288
+ let _workdirMenuKeyHandler = (e) => {
289
+ if (e.key === 'Escape' && workdirMenuOpen.value) workdirMenuOpen.value = false;
290
+ };
291
+ document.addEventListener('click', _workdirMenuClickHandler);
292
+ document.addEventListener('keydown', _workdirMenuKeyHandler);
293
+
257
294
  // ── Computed ──
258
295
  const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
259
296
  const canSend = computed(() =>
@@ -377,7 +414,7 @@ const App = {
377
414
 
378
415
  // ── Lifecycle ──
379
416
  onMounted(() => { connect(scheduleHighlight); });
380
- onUnmounted(() => { closeWs(); streaming.cleanup(); });
417
+ onUnmounted(() => { closeWs(); streaming.cleanup(); window.removeEventListener('resize', _resizeHandler); document.removeEventListener('click', _workdirMenuClickHandler); document.removeEventListener('keydown', _workdirMenuKeyHandler); });
381
418
 
382
419
  return {
383
420
  status, agentName, hostname, workDir, sessionId, error,
@@ -441,6 +478,25 @@ const App = {
441
478
  handleDragLeave: fileAttach.handleDragLeave,
442
479
  handleDrop: fileAttach.handleDrop,
443
480
  handlePaste: fileAttach.handlePaste,
481
+ // File browser
482
+ filePanelOpen, filePanelWidth, fileTreeRoot, fileTreeLoading, fileContextMenu,
483
+ sidebarView, isMobile, fileBrowser,
484
+ flattenedTree: fileBrowser.flattenedTree,
485
+ workdirMenuOpen,
486
+ toggleWorkdirMenu() { workdirMenuOpen.value = !workdirMenuOpen.value; },
487
+ workdirMenuBrowse() {
488
+ workdirMenuOpen.value = false;
489
+ if (isMobile.value) { sidebarView.value = 'files'; fileBrowser.openPanel(); }
490
+ else { fileBrowser.togglePanel(); }
491
+ },
492
+ workdirMenuChangeDir() {
493
+ workdirMenuOpen.value = false;
494
+ sidebar.openFolderPicker();
495
+ },
496
+ workdirMenuCopyPath() {
497
+ workdirMenuOpen.value = false;
498
+ navigator.clipboard.writeText(workDir.value);
499
+ },
444
500
  };
445
501
  },
446
502
  template: `
@@ -478,9 +534,48 @@ const App = {
478
534
 
479
535
  <div v-else class="main-body">
480
536
  <!-- Sidebar backdrop (mobile) -->
481
- <div v-if="sidebarOpen" class="sidebar-backdrop" @click="toggleSidebar"></div>
537
+ <div v-if="sidebarOpen" class="sidebar-backdrop" @click="toggleSidebar(); sidebarView = 'sessions'"></div>
482
538
  <!-- Sidebar -->
483
539
  <aside v-if="sidebarOpen" class="sidebar">
540
+ <!-- Mobile: file browser view -->
541
+ <div v-if="isMobile && sidebarView === 'files'" class="file-panel-mobile">
542
+ <div class="file-panel-mobile-header">
543
+ <button class="file-panel-mobile-back" @click="sidebarView = 'sessions'">
544
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
545
+ Sessions
546
+ </button>
547
+ <button class="file-panel-btn" @click="fileBrowser.refreshTree()" title="Refresh">
548
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
549
+ </button>
550
+ </div>
551
+ <div class="file-panel-breadcrumb" :title="workDir">{{ workDir }}</div>
552
+ <div v-if="fileTreeLoading" class="file-panel-loading">Loading...</div>
553
+ <div v-else-if="!fileTreeRoot || !fileTreeRoot.children || fileTreeRoot.children.length === 0" class="file-panel-empty">
554
+ No files found.
555
+ </div>
556
+ <div v-else class="file-tree">
557
+ <template v-for="item in flattenedTree" :key="item.node.path">
558
+ <div
559
+ class="file-tree-item"
560
+ :class="{ folder: item.node.type === 'directory' }"
561
+ :style="{ paddingLeft: (item.depth * 16 + 8) + 'px' }"
562
+ @click="item.node.type === 'directory' ? fileBrowser.toggleFolder(item.node) : fileBrowser.onFileClick($event, item.node)"
563
+ >
564
+ <span v-if="item.node.type === 'directory'" class="file-tree-arrow" :class="{ expanded: item.node.expanded }">&#9654;</span>
565
+ <span v-else class="file-tree-file-icon">
566
+ <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
567
+ </span>
568
+ <span class="file-tree-name" :title="item.node.path">{{ item.node.name }}</span>
569
+ <span v-if="item.node.loading" class="file-tree-spinner"></span>
570
+ </div>
571
+ <div v-if="item.node.type === 'directory' && item.node.expanded && item.node.children && item.node.children.length === 0 && !item.node.loading" class="file-tree-empty" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">(empty)</div>
572
+ <div v-if="item.node.error" class="file-tree-error" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ item.node.error }}</div>
573
+ </template>
574
+ </div>
575
+ </div>
576
+
577
+ <!-- Normal sidebar content (sessions view) -->
578
+ <template v-else>
484
579
  <div class="sidebar-section">
485
580
  <div class="sidebar-workdir">
486
581
  <div v-if="hostname" class="sidebar-hostname">
@@ -489,11 +584,25 @@ const App = {
489
584
  </div>
490
585
  <div class="sidebar-workdir-header">
491
586
  <div class="sidebar-workdir-label">Working Directory</div>
492
- <button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory">
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>
494
- </button>
495
587
  </div>
496
- <div class="sidebar-workdir-path" :title="workDir">{{ workDir }}</div>
588
+ <div class="sidebar-workdir-path-row" @click.stop="toggleWorkdirMenu()">
589
+ <div class="sidebar-workdir-path" :title="workDir">{{ workDir }}</div>
590
+ <svg class="sidebar-workdir-chevron" :class="{ open: workdirMenuOpen }" viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
591
+ </div>
592
+ <div v-if="workdirMenuOpen" class="workdir-menu">
593
+ <div class="workdir-menu-item" @click.stop="workdirMenuBrowse()">
594
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13h8v2H8v-2z"/></svg>
595
+ <span>Browse files</span>
596
+ </div>
597
+ <div class="workdir-menu-item" @click.stop="workdirMenuChangeDir()">
598
+ <svg viewBox="0 0 24 24" width="14" height="14"><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>
599
+ <span>Change directory</span>
600
+ </div>
601
+ <div class="workdir-menu-item" @click.stop="workdirMenuCopyPath()">
602
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
603
+ <span>Copy path</span>
604
+ </div>
605
+ </div>
497
606
  <div v-if="filteredWorkdirHistory.length > 0" class="workdir-history">
498
607
  <div class="workdir-history-label">Recent Directories</div>
499
608
  <div class="workdir-history-list">
@@ -585,8 +694,51 @@ const App = {
585
694
  <span v-if="serverVersion && agentVersion" class="sidebar-version-sep">/</span>
586
695
  <span v-if="agentVersion">agent {{ agentVersion }}</span>
587
696
  </div>
697
+ </template>
588
698
  </aside>
589
699
 
700
+ <!-- File browser panel (desktop) -->
701
+ <Transition name="file-panel">
702
+ <div v-if="filePanelOpen && !isMobile" class="file-panel" :style="{ width: filePanelWidth + 'px' }">
703
+ <div class="file-panel-resize-handle" @mousedown="fileBrowser.onResizeStart($event)" @touchstart="fileBrowser.onResizeStart($event)"></div>
704
+ <div class="file-panel-header">
705
+ <span class="file-panel-title">Files</span>
706
+ <div class="file-panel-actions">
707
+ <button class="file-panel-btn" @click="fileBrowser.refreshTree()" title="Refresh">
708
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
709
+ </button>
710
+ <button class="file-panel-btn" @click="filePanelOpen = false" title="Close">
711
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
712
+ </button>
713
+ </div>
714
+ </div>
715
+ <div class="file-panel-breadcrumb" :title="workDir">{{ workDir }}</div>
716
+ <div v-if="fileTreeLoading" class="file-panel-loading">Loading...</div>
717
+ <div v-else-if="!fileTreeRoot || !fileTreeRoot.children || fileTreeRoot.children.length === 0" class="file-panel-empty">
718
+ No files found.
719
+ </div>
720
+ <div v-else class="file-tree">
721
+ <template v-for="item in flattenedTree" :key="item.node.path">
722
+ <div
723
+ class="file-tree-item"
724
+ :class="{ folder: item.node.type === 'directory' }"
725
+ :style="{ paddingLeft: (item.depth * 16 + 8) + 'px' }"
726
+ @click="item.node.type === 'directory' ? fileBrowser.toggleFolder(item.node) : fileBrowser.onFileClick($event, item.node)"
727
+ >
728
+ <span v-if="item.node.type === 'directory'" class="file-tree-arrow" :class="{ expanded: item.node.expanded }">&#9654;</span>
729
+ <span v-else class="file-tree-file-icon">
730
+ <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
731
+ </span>
732
+ <span class="file-tree-name" :title="item.node.path">{{ item.node.name }}</span>
733
+ <span v-if="item.node.loading" class="file-tree-spinner"></span>
734
+ </div>
735
+ <div v-if="item.node.type === 'directory' && item.node.expanded && item.node.children && item.node.children.length === 0 && !item.node.loading" class="file-tree-empty" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">(empty)</div>
736
+ <div v-if="item.node.error" class="file-tree-error" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ item.node.error }}</div>
737
+ </template>
738
+ </div>
739
+ </div>
740
+ </Transition>
741
+
590
742
  <!-- Chat area -->
591
743
  <div class="chat-area">
592
744
  <div class="message-list" @scroll="onMessageListScroll">
@@ -893,6 +1045,26 @@ const App = {
893
1045
  </div>
894
1046
  </div>
895
1047
  </div>
1048
+
1049
+ <!-- File context menu -->
1050
+ <div
1051
+ v-if="fileContextMenu"
1052
+ class="file-context-menu"
1053
+ :style="{ left: fileContextMenu.x + 'px', top: fileContextMenu.y + 'px' }"
1054
+ >
1055
+ <div class="file-context-item" @click="fileBrowser.askClaudeRead()">
1056
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM5 15h14v2H5zm0-4h14v2H5zm0-4h14v2H5z"/></svg>
1057
+ Ask Claude to read
1058
+ </div>
1059
+ <div class="file-context-item" @click="fileBrowser.copyPath()">
1060
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
1061
+ {{ fileContextMenu.copied ? 'Copied!' : 'Copy path' }}
1062
+ </div>
1063
+ <div class="file-context-item" @click="fileBrowser.insertPath()">
1064
+ <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
1065
+ Insert path to input
1066
+ </div>
1067
+ </div>
896
1068
  </div>
897
1069
  `
898
1070
  };
package/web/favicon.svg CHANGED
@@ -1,4 +1,10 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect width="32" height="32" rx="6" fill="#6c63ff"/>
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#3b82f6"/>
5
+ <stop offset="100%" stop-color="#f59e0b"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="32" height="32" rx="6" fill="url(#g)"/>
3
9
  <text x="16" y="23" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="22" font-weight="800" fill="#fff">A</text>
4
10
  </svg>
package/web/iPad.webp ADDED
Binary file
Binary file