@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 +1 -1
- package/web/app.js +179 -7
- package/web/favicon.svg +7 -1
- package/web/iPad.webp +0 -0
- package/web/iPhone.webp +0 -0
- package/web/landing.html +993 -281
- package/web/modules/connection.js +15 -6
- package/web/modules/fileBrowser.js +379 -0
- package/web/style.css +329 -13
package/package.json
CHANGED
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 }">▶</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"
|
|
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 }">▶</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
|
-
<
|
|
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
|
package/web/iPhone.webp
ADDED
|
Binary file
|