@agent-link/server 0.1.186 → 0.1.188

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 (54) hide show
  1. package/dist/index.js +13 -15
  2. package/dist/index.js.map +1 -1
  3. package/package.json +3 -3
  4. package/web/dist/assets/index-C9bIrYkZ.js +320 -0
  5. package/web/dist/assets/index-C9bIrYkZ.js.map +1 -0
  6. package/web/dist/assets/index-Y1FN_mFe.css +1 -0
  7. package/web/{index.html → dist/index.html} +2 -19
  8. package/web/app.js +0 -2881
  9. package/web/css/ask-question.css +0 -333
  10. package/web/css/base.css +0 -270
  11. package/web/css/btw.css +0 -148
  12. package/web/css/chat.css +0 -176
  13. package/web/css/file-browser.css +0 -499
  14. package/web/css/input.css +0 -671
  15. package/web/css/loop.css +0 -674
  16. package/web/css/markdown.css +0 -169
  17. package/web/css/responsive.css +0 -314
  18. package/web/css/sidebar.css +0 -593
  19. package/web/css/team.css +0 -1277
  20. package/web/css/tools.css +0 -327
  21. package/web/encryption.js +0 -56
  22. package/web/modules/appHelpers.js +0 -100
  23. package/web/modules/askQuestion.js +0 -63
  24. package/web/modules/backgroundRouting.js +0 -269
  25. package/web/modules/connection.js +0 -731
  26. package/web/modules/fileAttachments.js +0 -125
  27. package/web/modules/fileBrowser.js +0 -379
  28. package/web/modules/filePreview.js +0 -213
  29. package/web/modules/i18n.js +0 -101
  30. package/web/modules/loop.js +0 -338
  31. package/web/modules/loopTemplates.js +0 -110
  32. package/web/modules/markdown.js +0 -83
  33. package/web/modules/messageHelpers.js +0 -206
  34. package/web/modules/sidebar.js +0 -402
  35. package/web/modules/streaming.js +0 -116
  36. package/web/modules/team.js +0 -396
  37. package/web/modules/teamTemplates.js +0 -360
  38. package/web/vendor/highlight.min.js +0 -1213
  39. package/web/vendor/marked.min.js +0 -6
  40. package/web/vendor/nacl-fast.min.js +0 -1
  41. package/web/vendor/nacl-util.min.js +0 -1
  42. package/web/vendor/pako.min.js +0 -2
  43. package/web/vendor/vue.global.prod.js +0 -13
  44. /package/web/{favicon.svg → dist/favicon.svg} +0 -0
  45. /package/web/{images → dist/images}/chat-iPad.webp +0 -0
  46. /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
  47. /package/web/{images → dist/images}/loop-iPad.webp +0 -0
  48. /package/web/{images → dist/images}/team-iPad.webp +0 -0
  49. /package/web/{landing.html → dist/landing.html} +0 -0
  50. /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
  51. /package/web/{locales → dist/locales}/en.json +0 -0
  52. /package/web/{locales → dist/locales}/zh.json +0 -0
  53. /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
  54. /package/web/{vendor → dist/vendor}/github.min.css +0 -0
@@ -1,125 +0,0 @@
1
- // ── File attachment handling ──────────────────────────────────────────────────
2
-
3
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
4
- const MAX_FILES = 5;
5
- const ACCEPTED_EXTENSIONS = [
6
- '.pdf', '.json', '.md', '.py', '.js', '.ts', '.tsx', '.jsx', '.css',
7
- '.html', '.xml', '.yaml', '.yml', '.toml', '.sh', '.sql', '.csv',
8
- '.c', '.cpp', '.h', '.hpp', '.java', '.go', '.rs', '.rb', '.php',
9
- '.swift', '.kt', '.scala', '.r', '.m', '.vue', '.svelte', '.txt',
10
- '.log', '.cfg', '.ini', '.env', '.gitignore', '.dockerfile',
11
- ];
12
-
13
- function isAcceptedFile(file) {
14
- if (file.type.startsWith('image/')) return true;
15
- if (file.type.startsWith('text/')) return true;
16
- const ext = '.' + file.name.split('.').pop().toLowerCase();
17
- return ACCEPTED_EXTENSIONS.includes(ext);
18
- }
19
-
20
- export function formatFileSize(bytes) {
21
- if (bytes < 1024) return bytes + ' B';
22
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
23
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
24
- }
25
-
26
- function readFileAsBase64(file) {
27
- return new Promise((resolve, reject) => {
28
- const reader = new FileReader();
29
- reader.onload = () => {
30
- const base64 = reader.result.split(',')[1];
31
- resolve(base64);
32
- };
33
- reader.onerror = reject;
34
- reader.readAsDataURL(file);
35
- });
36
- }
37
-
38
- /**
39
- * Creates file attachment handlers bound to Vue reactive state.
40
- * @param {import('vue').Ref} attachments - ref([])
41
- * @param {import('vue').Ref} fileInputRef - ref(null)
42
- * @param {import('vue').Ref} dragOver - ref(false)
43
- */
44
- export function createFileAttachments(attachments, fileInputRef, dragOver) {
45
-
46
- async function addFiles(fileList) {
47
- const currentCount = attachments.value.length;
48
- const remaining = MAX_FILES - currentCount;
49
- if (remaining <= 0) return;
50
-
51
- const files = Array.from(fileList).slice(0, remaining);
52
- for (const file of files) {
53
- if (!isAcceptedFile(file)) continue;
54
- if (file.size > MAX_FILE_SIZE) continue;
55
- if (attachments.value.some(a => a.name === file.name && a.size === file.size)) continue;
56
-
57
- const data = await readFileAsBase64(file);
58
- const isImage = file.type.startsWith('image/');
59
- let thumbUrl = null;
60
- if (isImage) {
61
- thumbUrl = URL.createObjectURL(file);
62
- }
63
- attachments.value.push({
64
- name: file.name,
65
- mimeType: file.type || 'application/octet-stream',
66
- size: file.size,
67
- data,
68
- isImage,
69
- thumbUrl,
70
- });
71
- }
72
- }
73
-
74
- function removeAttachment(index) {
75
- const att = attachments.value[index];
76
- if (att.thumbUrl) URL.revokeObjectURL(att.thumbUrl);
77
- attachments.value.splice(index, 1);
78
- }
79
-
80
- function triggerFileInput() {
81
- if (fileInputRef.value) fileInputRef.value.click();
82
- }
83
-
84
- function handleFileSelect(e) {
85
- if (e.target.files) addFiles(e.target.files);
86
- e.target.value = '';
87
- }
88
-
89
- function handleDragOver(e) {
90
- e.preventDefault();
91
- dragOver.value = true;
92
- }
93
-
94
- function handleDragLeave(e) {
95
- e.preventDefault();
96
- dragOver.value = false;
97
- }
98
-
99
- function handleDrop(e) {
100
- e.preventDefault();
101
- dragOver.value = false;
102
- if (e.dataTransfer?.files) addFiles(e.dataTransfer.files);
103
- }
104
-
105
- function handlePaste(e) {
106
- const items = e.clipboardData?.items;
107
- if (!items) return;
108
- const files = [];
109
- for (const item of items) {
110
- if (item.kind === 'file') {
111
- const file = item.getAsFile();
112
- if (file) files.push(file);
113
- }
114
- }
115
- if (files.length > 0) {
116
- e.preventDefault();
117
- addFiles(files);
118
- }
119
- }
120
-
121
- return {
122
- addFiles, removeAttachment, triggerFileInput, handleFileSelect,
123
- handleDragOver, handleDragLeave, handleDrop, handlePaste,
124
- };
125
- }
@@ -1,379 +0,0 @@
1
- // ── File Browser: tree state, lazy loading, context menu, file actions ────────
2
- const { computed, nextTick } = Vue;
3
-
4
- /**
5
- * Creates the file browser controller.
6
- * @param {object} deps - Reactive state and callbacks
7
- */
8
- export function createFileBrowser(deps) {
9
- const {
10
- wsSend, workDir, inputText, inputRef,
11
- filePanelOpen, fileTreeRoot, fileTreeLoading, fileContextMenu,
12
- sidebarOpen, sidebarView, filePanelWidth,
13
- } = deps;
14
-
15
- // Map of dirPath → TreeNode awaiting directory_listing response
16
- const pendingRequests = new Map();
17
-
18
- // ── Tree helpers ──
19
-
20
- function buildPath(parentPath, name) {
21
- if (!parentPath) return name;
22
- const sep = parentPath.includes('\\') ? '\\' : '/';
23
- return parentPath.replace(/[/\\]$/, '') + sep + name;
24
- }
25
-
26
- function makeNode(entry, parentPath) {
27
- return {
28
- path: buildPath(parentPath, entry.name),
29
- name: entry.name,
30
- type: entry.type,
31
- expanded: false,
32
- children: entry.type === 'directory' ? null : undefined,
33
- loading: false,
34
- error: null,
35
- };
36
- }
37
-
38
- // ── Flattened tree for rendering ──
39
-
40
- const flattenedTree = computed(() => {
41
- const root = fileTreeRoot.value;
42
- if (!root || !root.children) return [];
43
- const result = [];
44
- function walk(children, depth) {
45
- for (const node of children) {
46
- result.push({ node, depth });
47
- if (node.type === 'directory' && node.expanded && node.children) {
48
- walk(node.children, depth + 1);
49
- }
50
- }
51
- }
52
- walk(root.children, 0);
53
- return result;
54
- });
55
-
56
- // ── Panel open/close ──
57
-
58
- function openPanel() {
59
- filePanelOpen.value = true;
60
- if (!fileTreeRoot.value || fileTreeRoot.value.path !== workDir.value) {
61
- loadRoot();
62
- }
63
- }
64
-
65
- function closePanel() {
66
- filePanelOpen.value = false;
67
- closeContextMenu();
68
- }
69
-
70
- function togglePanel() {
71
- if (filePanelOpen.value) {
72
- closePanel();
73
- } else {
74
- openPanel();
75
- }
76
- }
77
-
78
- // ── Loading ──
79
-
80
- function loadRoot() {
81
- const dir = workDir.value;
82
- if (!dir) return;
83
- fileTreeLoading.value = true;
84
- fileTreeRoot.value = {
85
- path: dir,
86
- name: dir,
87
- type: 'directory',
88
- expanded: true,
89
- children: null,
90
- loading: true,
91
- error: null,
92
- };
93
- pendingRequests.set(dir, fileTreeRoot.value);
94
- wsSend({ type: 'list_directory', dirPath: dir, source: 'file_browser' });
95
- }
96
-
97
- function loadDirectory(node) {
98
- if (node.loading) return;
99
- node.loading = true;
100
- node.error = null;
101
- pendingRequests.set(node.path, node);
102
- wsSend({ type: 'list_directory', dirPath: node.path, source: 'file_browser' });
103
- }
104
-
105
- // ── Folder expand/collapse ──
106
-
107
- function toggleFolder(node) {
108
- if (node.type !== 'directory') return;
109
- if (node.expanded) {
110
- node.expanded = false;
111
- closeContextMenu();
112
- } else {
113
- node.expanded = true;
114
- if (node.children === null) {
115
- loadDirectory(node);
116
- }
117
- }
118
- }
119
-
120
- // ── Handle directory_listing response ──
121
-
122
- function handleDirectoryListing(msg) {
123
- const dirPath = msg.dirPath;
124
- const node = pendingRequests.get(dirPath);
125
- pendingRequests.delete(dirPath);
126
-
127
- // Check if this is the root loading
128
- if (fileTreeRoot.value && fileTreeRoot.value.path === dirPath) {
129
- fileTreeLoading.value = false;
130
- }
131
-
132
- if (!node) {
133
- // No pending request for this path — could be a stale response after
134
- // workdir change. Try to find the node in the tree by path.
135
- const found = findNodeByPath(dirPath);
136
- if (found) {
137
- applyListing(found, msg);
138
- }
139
- return;
140
- }
141
-
142
- applyListing(node, msg);
143
- }
144
-
145
- function applyListing(node, msg) {
146
- node.loading = false;
147
- if (msg.error) {
148
- node.error = msg.error;
149
- node.children = [];
150
- return;
151
- }
152
- const entries = msg.entries || [];
153
- node.children = entries.map(e => makeNode(e, node.path));
154
- node.expanded = true;
155
- }
156
-
157
- function findNodeByPath(targetPath) {
158
- const root = fileTreeRoot.value;
159
- if (!root) return null;
160
- if (root.path === targetPath) return root;
161
- if (!root.children) return null;
162
- function search(children) {
163
- for (const node of children) {
164
- if (node.path === targetPath) return node;
165
- if (node.type === 'directory' && node.children) {
166
- const found = search(node.children);
167
- if (found) return found;
168
- }
169
- }
170
- return null;
171
- }
172
- return search(root.children);
173
- }
174
-
175
- // ── Refresh ──
176
-
177
- function refreshTree() {
178
- if (!fileTreeRoot.value) {
179
- loadRoot();
180
- return;
181
- }
182
- // Re-fetch all expanded directories
183
- refreshNode(fileTreeRoot.value);
184
- }
185
-
186
- function refreshNode(node) {
187
- if (node.type !== 'directory') return;
188
- if (node.expanded && node.children !== null) {
189
- // Re-fetch this directory
190
- node.children = null;
191
- loadDirectory(node);
192
- }
193
- // Note: children of this node will be re-fetched when loadDirectory
194
- // completes and rebuilds the children array. No need to recurse.
195
- }
196
-
197
- // ── Workdir changed ──
198
-
199
- function onWorkdirChanged() {
200
- pendingRequests.clear();
201
- fileTreeRoot.value = null;
202
- fileTreeLoading.value = false;
203
- closeContextMenu();
204
- if (filePanelOpen.value) {
205
- loadRoot();
206
- }
207
- }
208
-
209
- // ── Context menu ──
210
-
211
- function onFileClick(event, node) {
212
- if (node.type === 'directory') return;
213
- event.stopPropagation();
214
-
215
- // Position the menu near the click, adjusting for viewport edges
216
- let x = event.clientX;
217
- let y = event.clientY;
218
- const menuWidth = 220;
219
- const menuHeight = 120; // approx 3 items
220
- if (x + menuWidth > window.innerWidth) {
221
- x = window.innerWidth - menuWidth - 8;
222
- }
223
- if (y + menuHeight > window.innerHeight) {
224
- y = y - menuHeight;
225
- }
226
- if (x < 0) x = 8;
227
- if (y < 0) y = 8;
228
-
229
- fileContextMenu.value = {
230
- x,
231
- y,
232
- path: node.path,
233
- name: node.name,
234
- };
235
- }
236
-
237
- function closeContextMenu() {
238
- fileContextMenu.value = null;
239
- }
240
-
241
- // ── File actions ──
242
-
243
- function askClaudeRead() {
244
- const menu = fileContextMenu.value;
245
- if (!menu) return;
246
- const path = menu.path;
247
- closeContextMenu();
248
- inputText.value = `Read the file ${path}`;
249
- // Close sidebar on mobile
250
- if (window.innerWidth <= 768) {
251
- sidebarOpen.value = false;
252
- sidebarView.value = 'sessions';
253
- }
254
- nextTick(() => {
255
- if (inputRef.value) inputRef.value.focus();
256
- });
257
- }
258
-
259
- function copyPath() {
260
- const menu = fileContextMenu.value;
261
- if (!menu) return;
262
- const path = menu.path;
263
- navigator.clipboard.writeText(path).catch(() => {
264
- // Fallback: some browsers block clipboard in non-secure contexts
265
- });
266
- // Brief "Copied!" feedback — store temporarily in menu state
267
- fileContextMenu.value = { ...menu, copied: true };
268
- setTimeout(() => {
269
- closeContextMenu();
270
- }, 1000);
271
- }
272
-
273
- function insertPath() {
274
- const menu = fileContextMenu.value;
275
- if (!menu) return;
276
- const path = menu.path;
277
- closeContextMenu();
278
- const textarea = inputRef.value;
279
- if (textarea) {
280
- const start = textarea.selectionStart || inputText.value.length;
281
- const end = textarea.selectionEnd || inputText.value.length;
282
- const text = inputText.value;
283
- inputText.value = text.slice(0, start) + path + text.slice(end);
284
- nextTick(() => {
285
- const newPos = start + path.length;
286
- textarea.setSelectionRange(newPos, newPos);
287
- textarea.focus();
288
- });
289
- } else {
290
- inputText.value += path;
291
- }
292
- // Close sidebar on mobile
293
- if (window.innerWidth <= 768) {
294
- sidebarOpen.value = false;
295
- sidebarView.value = 'sessions';
296
- }
297
- }
298
-
299
- // ── Global click handler for dismissing context menu ──
300
-
301
- function setupGlobalListeners() {
302
- document.addEventListener('click', (e) => {
303
- if (!fileContextMenu.value) return;
304
- const menuEl = document.querySelector('.file-context-menu');
305
- if (menuEl && menuEl.contains(e.target)) return;
306
- closeContextMenu();
307
- });
308
- document.addEventListener('keydown', (e) => {
309
- if (e.key === 'Escape' && fileContextMenu.value) {
310
- closeContextMenu();
311
- }
312
- });
313
- }
314
-
315
- // ── Resize handle (mouse + touch) ──
316
-
317
- let _resizing = false;
318
- let _startX = 0;
319
- let _startWidth = 0;
320
- const MIN_WIDTH = 160;
321
- const MAX_WIDTH = 600;
322
-
323
- function onResizeStart(e) {
324
- e.preventDefault();
325
- _resizing = true;
326
- _startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
327
- _startWidth = filePanelWidth.value;
328
- document.body.style.cursor = 'col-resize';
329
- document.body.style.userSelect = 'none';
330
- if (e.type === 'touchstart') {
331
- document.addEventListener('touchmove', onResizeMove, { passive: false });
332
- document.addEventListener('touchend', onResizeEnd);
333
- } else {
334
- document.addEventListener('mousemove', onResizeMove);
335
- document.addEventListener('mouseup', onResizeEnd);
336
- }
337
- }
338
-
339
- function onResizeMove(e) {
340
- if (!_resizing) return;
341
- if (e.type === 'touchmove') e.preventDefault();
342
- const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
343
- const delta = clientX - _startX;
344
- const newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, _startWidth + delta));
345
- filePanelWidth.value = newWidth;
346
- }
347
-
348
- function onResizeEnd() {
349
- if (!_resizing) return;
350
- _resizing = false;
351
- document.body.style.cursor = '';
352
- document.body.style.userSelect = '';
353
- document.removeEventListener('mousemove', onResizeMove);
354
- document.removeEventListener('mouseup', onResizeEnd);
355
- document.removeEventListener('touchmove', onResizeMove);
356
- document.removeEventListener('touchend', onResizeEnd);
357
- localStorage.setItem('agentlink-file-panel-width', String(filePanelWidth.value));
358
- }
359
-
360
- // Set up listeners immediately
361
- setupGlobalListeners();
362
-
363
- return {
364
- openPanel,
365
- closePanel,
366
- togglePanel,
367
- toggleFolder,
368
- onFileClick,
369
- closeContextMenu,
370
- askClaudeRead,
371
- copyPath,
372
- insertPath,
373
- refreshTree,
374
- handleDirectoryListing,
375
- onWorkdirChanged,
376
- flattenedTree,
377
- onResizeStart,
378
- };
379
- }
@@ -1,213 +0,0 @@
1
- // ── File Preview: panel state, content rendering, resize handle ────────
2
-
3
- /**
4
- * Creates the file preview controller.
5
- * @param {object} deps - Reactive state and callbacks
6
- */
7
- export function createFilePreview(deps) {
8
- const {
9
- wsSend,
10
- previewPanelOpen,
11
- previewPanelWidth,
12
- previewFile,
13
- previewLoading,
14
- previewMarkdownRendered,
15
- sidebarView,
16
- sidebarOpen,
17
- isMobile,
18
- renderMarkdown,
19
- } = deps;
20
-
21
- // ── Open / Close ──
22
-
23
- function openPreview(filePath) {
24
- // Skip re-fetch if same file already loaded
25
- if (previewFile.value && previewFile.value.filePath === filePath && !previewFile.value.error) {
26
- if (isMobile.value) {
27
- sidebarView.value = 'preview';
28
- sidebarOpen.value = true;
29
- } else {
30
- previewPanelOpen.value = true;
31
- }
32
- return;
33
- }
34
- if (isMobile.value) {
35
- sidebarView.value = 'preview';
36
- sidebarOpen.value = true;
37
- } else {
38
- previewPanelOpen.value = true;
39
- }
40
- previewLoading.value = true;
41
- previewFile.value = null;
42
- wsSend({ type: 'read_file', filePath });
43
- }
44
-
45
- function closePreview() {
46
- if (isMobile.value) {
47
- sidebarView.value = 'files';
48
- } else {
49
- previewPanelOpen.value = false;
50
- }
51
- }
52
-
53
- // ── Handle file_content response ──
54
-
55
- function handleFileContent(msg) {
56
- previewLoading.value = false;
57
- previewMarkdownRendered.value = false;
58
- previewFile.value = {
59
- filePath: msg.filePath,
60
- fileName: msg.fileName,
61
- content: msg.content,
62
- encoding: msg.encoding,
63
- mimeType: msg.mimeType,
64
- truncated: msg.truncated,
65
- totalSize: msg.totalSize,
66
- error: msg.error || null,
67
- };
68
- }
69
-
70
- // ── Workdir changed → close preview ──
71
-
72
- function onWorkdirChanged() {
73
- previewPanelOpen.value = false;
74
- previewFile.value = null;
75
- previewLoading.value = false;
76
- if (sidebarView.value === 'preview') {
77
- sidebarView.value = 'sessions';
78
- }
79
- }
80
-
81
- // ── Syntax highlighting helpers ──
82
-
83
- const LANG_MAP = {
84
- ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
85
- mjs: 'javascript', cjs: 'javascript', py: 'python', rb: 'ruby',
86
- rs: 'rust', go: 'go', java: 'java', c: 'c', h: 'c',
87
- cpp: 'cpp', hpp: 'cpp', cs: 'csharp', swift: 'swift', kt: 'kotlin',
88
- lua: 'lua', r: 'r', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
89
- fish: 'bash', ps1: 'powershell', bat: 'dos', cmd: 'dos',
90
- json: 'json', json5: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini',
91
- xml: 'xml', html: 'xml', htm: 'xml', css: 'css', scss: 'scss', less: 'less',
92
- md: 'markdown', txt: 'plaintext', log: 'plaintext', graphql: 'graphql',
93
- proto: 'protobuf', vue: 'xml', svelte: 'xml', ini: 'ini', cfg: 'ini',
94
- conf: 'ini', env: 'bash',
95
- };
96
-
97
- function detectLanguage(fileName) {
98
- const ext = (fileName || '').split('.').pop()?.toLowerCase();
99
- return LANG_MAP[ext] || ext || 'plaintext';
100
- }
101
-
102
- function highlightCode(code, fileName) {
103
- if (!code) return '';
104
- if (!window.hljs) return escapeHtml(code);
105
- const lang = detectLanguage(fileName);
106
- try {
107
- return window.hljs.highlight(code, { language: lang }).value;
108
- } catch {
109
- try {
110
- return window.hljs.highlightAuto(code).value;
111
- } catch {
112
- return escapeHtml(code);
113
- }
114
- }
115
- }
116
-
117
- function escapeHtml(str) {
118
- return str
119
- .replace(/&/g, '&amp;')
120
- .replace(/</g, '&lt;')
121
- .replace(/>/g, '&gt;')
122
- .replace(/"/g, '&quot;');
123
- }
124
-
125
- // ── File size formatting ──
126
-
127
- function formatFileSize(bytes) {
128
- if (bytes == null) return '';
129
- if (bytes < 1024) return bytes + ' B';
130
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
131
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
132
- }
133
-
134
- // ── Resize handle (mouse + touch) ──
135
-
136
- let _resizing = false;
137
- let _startX = 0;
138
- let _startWidth = 0;
139
- const MIN_WIDTH = 200;
140
- const MAX_WIDTH = 800;
141
-
142
- function onResizeStart(e) {
143
- e.preventDefault();
144
- _resizing = true;
145
- _startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
146
- _startWidth = previewPanelWidth.value;
147
- document.body.style.cursor = 'col-resize';
148
- document.body.style.userSelect = 'none';
149
- if (e.type === 'touchstart') {
150
- document.addEventListener('touchmove', onResizeMove, { passive: false });
151
- document.addEventListener('touchend', onResizeEnd);
152
- } else {
153
- document.addEventListener('mousemove', onResizeMove);
154
- document.addEventListener('mouseup', onResizeEnd);
155
- }
156
- }
157
-
158
- function onResizeMove(e) {
159
- if (!_resizing) return;
160
- if (e.type === 'touchmove') e.preventDefault();
161
- const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
162
- // Left edge resize: dragging left = wider, dragging right = narrower
163
- const delta = _startX - clientX;
164
- const newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, _startWidth + delta));
165
- previewPanelWidth.value = newWidth;
166
- }
167
-
168
- function onResizeEnd() {
169
- if (!_resizing) return;
170
- _resizing = false;
171
- document.body.style.cursor = '';
172
- document.body.style.userSelect = '';
173
- document.removeEventListener('mousemove', onResizeMove);
174
- document.removeEventListener('mouseup', onResizeEnd);
175
- document.removeEventListener('touchmove', onResizeMove);
176
- document.removeEventListener('touchend', onResizeEnd);
177
- localStorage.setItem('agentlink-preview-panel-width', String(previewPanelWidth.value));
178
- }
179
-
180
- // ── Markdown preview ──
181
-
182
- function isMarkdownFile(fileName) {
183
- const ext = (fileName || '').split('.').pop()?.toLowerCase();
184
- return ext === 'md' || ext === 'mdx';
185
- }
186
-
187
- function renderedMarkdownHtml(content) {
188
- return renderMarkdown(content || '');
189
- }
190
-
191
- /** Force re-fetch the currently open preview file (e.g. after editing) */
192
- function refreshPreview() {
193
- if (!previewFile.value?.filePath) return;
194
- const filePath = previewFile.value.filePath;
195
- previewFile.value = null;
196
- previewLoading.value = true;
197
- wsSend({ type: 'read_file', filePath });
198
- }
199
-
200
- return {
201
- openPreview,
202
- closePreview,
203
- refreshPreview,
204
- handleFileContent,
205
- onWorkdirChanged,
206
- detectLanguage,
207
- highlightCode,
208
- formatFileSize,
209
- onResizeStart,
210
- isMarkdownFile,
211
- renderedMarkdownHtml,
212
- };
213
- }