@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.
- package/dist/index.js +13 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/web/dist/assets/index-C9bIrYkZ.js +320 -0
- package/web/dist/assets/index-C9bIrYkZ.js.map +1 -0
- package/web/dist/assets/index-Y1FN_mFe.css +1 -0
- package/web/{index.html → dist/index.html} +2 -19
- package/web/app.js +0 -2881
- package/web/css/ask-question.css +0 -333
- package/web/css/base.css +0 -270
- package/web/css/btw.css +0 -148
- package/web/css/chat.css +0 -176
- package/web/css/file-browser.css +0 -499
- package/web/css/input.css +0 -671
- package/web/css/loop.css +0 -674
- package/web/css/markdown.css +0 -169
- package/web/css/responsive.css +0 -314
- package/web/css/sidebar.css +0 -593
- package/web/css/team.css +0 -1277
- package/web/css/tools.css +0 -327
- package/web/encryption.js +0 -56
- package/web/modules/appHelpers.js +0 -100
- package/web/modules/askQuestion.js +0 -63
- package/web/modules/backgroundRouting.js +0 -269
- package/web/modules/connection.js +0 -731
- package/web/modules/fileAttachments.js +0 -125
- package/web/modules/fileBrowser.js +0 -379
- package/web/modules/filePreview.js +0 -213
- package/web/modules/i18n.js +0 -101
- package/web/modules/loop.js +0 -338
- package/web/modules/loopTemplates.js +0 -110
- package/web/modules/markdown.js +0 -83
- package/web/modules/messageHelpers.js +0 -206
- package/web/modules/sidebar.js +0 -402
- package/web/modules/streaming.js +0 -116
- package/web/modules/team.js +0 -396
- package/web/modules/teamTemplates.js +0 -360
- package/web/vendor/highlight.min.js +0 -1213
- package/web/vendor/marked.min.js +0 -6
- package/web/vendor/nacl-fast.min.js +0 -1
- package/web/vendor/nacl-util.min.js +0 -1
- package/web/vendor/pako.min.js +0 -2
- package/web/vendor/vue.global.prod.js +0 -13
- /package/web/{favicon.svg → dist/favicon.svg} +0 -0
- /package/web/{images → dist/images}/chat-iPad.webp +0 -0
- /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
- /package/web/{images → dist/images}/loop-iPad.webp +0 -0
- /package/web/{images → dist/images}/team-iPad.webp +0 -0
- /package/web/{landing.html → dist/landing.html} +0 -0
- /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
- /package/web/{locales → dist/locales}/en.json +0 -0
- /package/web/{locales → dist/locales}/zh.json +0 -0
- /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
- /package/web/{vendor → dist/vendor}/github.min.css +0 -0
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// ── Message helpers: formatting, tool summaries, diff display ─────────────────
|
|
2
|
-
import { renderMarkdown } from './markdown.js';
|
|
3
|
-
|
|
4
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
-
const CONTEXT_SUMMARY_PREFIX = 'This session is being continued from a previous conversation';
|
|
6
|
-
|
|
7
|
-
function parseToolInput(msg) {
|
|
8
|
-
if (msg._parsedInput !== undefined) return msg._parsedInput;
|
|
9
|
-
try { msg._parsedInput = JSON.parse(msg.toolInput); }
|
|
10
|
-
catch { msg._parsedInput = null; }
|
|
11
|
-
return msg._parsedInput;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function isContextSummary(text) {
|
|
15
|
-
return typeof text === 'string' && text.trimStart().startsWith(CONTEXT_SUMMARY_PREFIX);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function formatRelativeTime(ts, t) {
|
|
19
|
-
const diff = Date.now() - ts;
|
|
20
|
-
const mins = Math.floor(diff / 60000);
|
|
21
|
-
if (mins < 1) return t ? t('time.justNow') : 'just now';
|
|
22
|
-
if (mins < 60) return t ? t('time.minutesAgo', { n: mins }) : `${mins}m ago`;
|
|
23
|
-
const hours = Math.floor(mins / 60);
|
|
24
|
-
if (hours < 24) return t ? t('time.hoursAgo', { n: hours }) : `${hours}h ago`;
|
|
25
|
-
const days = Math.floor(hours / 24);
|
|
26
|
-
if (days < 30) return t ? t('time.daysAgo', { n: days }) : `${days}d ago`;
|
|
27
|
-
return new Date(ts).toLocaleDateString();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function formatTimestamp(ts) {
|
|
31
|
-
if (!ts) return '';
|
|
32
|
-
const d = ts instanceof Date ? ts : new Date(ts);
|
|
33
|
-
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' · ' + d.toLocaleDateString();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getRenderedContent(msg) {
|
|
37
|
-
if (msg.role !== 'assistant' && !msg.isCommandOutput) return msg.content;
|
|
38
|
-
if (msg.isStreaming) {
|
|
39
|
-
const t = msg.content || '';
|
|
40
|
-
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
|
41
|
-
}
|
|
42
|
-
return renderMarkdown(msg.content);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function copyMessage(msg) {
|
|
46
|
-
try {
|
|
47
|
-
await navigator.clipboard.writeText(msg.content);
|
|
48
|
-
msg.copied = true;
|
|
49
|
-
setTimeout(() => { msg.copied = false; }, 2000);
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function isPrevAssistant(visibleMessages, idx) {
|
|
54
|
-
if (idx <= 0) return false;
|
|
55
|
-
const prev = visibleMessages[idx - 1];
|
|
56
|
-
return prev && (prev.role === 'assistant' || prev.role === 'tool');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function toggleContextSummary(msg) {
|
|
60
|
-
msg.contextExpanded = !msg.contextExpanded;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function toggleTool(msg) {
|
|
64
|
-
msg.expanded = !msg.expanded;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function getToolSummary(msg, t) {
|
|
68
|
-
const name = msg.toolName;
|
|
69
|
-
const obj = parseToolInput(msg);
|
|
70
|
-
if (!obj) return '';
|
|
71
|
-
try {
|
|
72
|
-
if (name === 'Read' && obj.file_path) return obj.file_path;
|
|
73
|
-
if (name === 'Edit' && obj.file_path) return obj.file_path;
|
|
74
|
-
if (name === 'Write' && obj.file_path) return obj.file_path;
|
|
75
|
-
if (name === 'Bash' && obj.command) return obj.command.length > 60 ? obj.command.slice(0, 60) + '...' : obj.command;
|
|
76
|
-
if (name === 'Glob' && obj.pattern) return obj.pattern;
|
|
77
|
-
if (name === 'Grep' && obj.pattern) return obj.pattern;
|
|
78
|
-
if (name === 'TodoWrite' && obj.todos) {
|
|
79
|
-
const doneCount = obj.todos.filter(td => td.status === 'completed').length;
|
|
80
|
-
return t ? t('tool.done', { done: doneCount, total: obj.todos.length }) : `${doneCount}/${obj.todos.length} done`;
|
|
81
|
-
}
|
|
82
|
-
if (name === 'Task' && obj.description) return obj.description;
|
|
83
|
-
if (name === 'Agent' && obj.description) return obj.description;
|
|
84
|
-
if (name === 'Agent' && obj.prompt) return obj.prompt.length > 80 ? obj.prompt.slice(0, 80) + '...' : obj.prompt;
|
|
85
|
-
if (name === 'WebSearch' && obj.query) return obj.query;
|
|
86
|
-
if (name === 'WebFetch' && obj.url) return obj.url.length > 60 ? obj.url.slice(0, 60) + '...' : obj.url;
|
|
87
|
-
} catch {}
|
|
88
|
-
return '';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function isEditTool(msg) {
|
|
92
|
-
return msg.role === 'tool' && msg.toolName === 'Edit' && msg.toolInput;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function getFormattedToolInput(msg, t) {
|
|
96
|
-
if (!msg.toolInput) return null;
|
|
97
|
-
const obj = parseToolInput(msg);
|
|
98
|
-
if (!obj) return null;
|
|
99
|
-
try {
|
|
100
|
-
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
101
|
-
const name = msg.toolName;
|
|
102
|
-
|
|
103
|
-
if (name === 'Read' && obj.file_path) {
|
|
104
|
-
let detail = esc(obj.file_path);
|
|
105
|
-
if (obj.offset && obj.limit) {
|
|
106
|
-
const meta = t ? t('tool.lines', { start: obj.offset, end: obj.offset + obj.limit - 1 }) : `lines ${obj.offset}\u2013${obj.offset + obj.limit - 1}`;
|
|
107
|
-
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
108
|
-
} else if (obj.offset) {
|
|
109
|
-
const meta = t ? t('tool.fromLine', { offset: obj.offset }) : `from line ${obj.offset}`;
|
|
110
|
-
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
111
|
-
} else if (obj.limit) {
|
|
112
|
-
const meta = t ? t('tool.firstLines', { limit: obj.limit }) : `first ${obj.limit} lines`;
|
|
113
|
-
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
114
|
-
}
|
|
115
|
-
return detail;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (name === 'Write' && obj.file_path) {
|
|
119
|
-
const lines = (obj.content || '').split('\n').length;
|
|
120
|
-
const lineCount = t ? t('tool.lineCount', { n: lines }) : `${lines} lines`;
|
|
121
|
-
return esc(obj.file_path) + ` <span class="tool-input-meta">${lineCount}</span>`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (name === 'Bash' && obj.command) {
|
|
125
|
-
let html = '<code class="tool-input-cmd">' + esc(obj.command) + '</code>';
|
|
126
|
-
if (obj.description) html = '<span class="tool-input-meta">' + esc(obj.description) + '</span> ' + html;
|
|
127
|
-
return html;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (name === 'Glob' && obj.pattern) {
|
|
131
|
-
let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
|
|
132
|
-
if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
|
|
133
|
-
return html;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (name === 'Grep' && obj.pattern) {
|
|
137
|
-
let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
|
|
138
|
-
if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
|
|
139
|
-
return html;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (name === 'TodoWrite' && Array.isArray(obj.todos)) {
|
|
143
|
-
let html = '<div class="todo-list">';
|
|
144
|
-
for (const t of obj.todos) {
|
|
145
|
-
const s = t.status;
|
|
146
|
-
const icon = s === 'completed' ? '<span class="todo-icon done">\u2713</span>'
|
|
147
|
-
: s === 'in_progress' ? '<span class="todo-icon active">\u25CF</span>'
|
|
148
|
-
: '<span class="todo-icon">\u25CB</span>';
|
|
149
|
-
const cls = s === 'completed' ? ' todo-done' : s === 'in_progress' ? ' todo-active' : '';
|
|
150
|
-
html += '<div class="todo-item' + cls + '">' + icon + '<span class="todo-text">' + esc(t.content || t.activeForm || '') + '</span></div>';
|
|
151
|
-
}
|
|
152
|
-
html += '</div>';
|
|
153
|
-
return html;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (name === 'Task' || name === 'Agent') {
|
|
157
|
-
let html = '';
|
|
158
|
-
const descLabel = t ? t('tool.description') : 'Description';
|
|
159
|
-
const agentLabel = t ? t('tool.agent') : 'Agent';
|
|
160
|
-
const promptLabel = t ? t('tool.prompt') : 'Prompt';
|
|
161
|
-
if (obj.description) html += '<div class="task-field"><span class="tool-input-meta">' + descLabel + '</span> ' + esc(obj.description) + '</div>';
|
|
162
|
-
if (obj.subagent_type) html += '<div class="task-field"><span class="tool-input-meta">' + agentLabel + '</span> <code class="tool-input-cmd">' + esc(obj.subagent_type) + '</code></div>';
|
|
163
|
-
if (obj.prompt) {
|
|
164
|
-
html += '<div class="task-field"><span class="tool-input-meta">' + promptLabel + '</span></div><div class="task-prompt">' + esc(obj.prompt) + '</div>';
|
|
165
|
-
}
|
|
166
|
-
if (html) return html;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (name === 'WebSearch' && obj.query) {
|
|
170
|
-
return '<code class="tool-input-cmd">' + esc(obj.query) + '</code>';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (name === 'WebFetch' && obj.url) {
|
|
174
|
-
let html = '<a class="tool-link" href="' + esc(obj.url) + '" target="_blank" rel="noopener">' + esc(obj.url) + '</a>';
|
|
175
|
-
if (obj.prompt) html += '<div class="task-field"><span class="tool-input-meta">' + esc(obj.prompt) + '</span></div>';
|
|
176
|
-
return html;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
} catch {}
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function getEditDiffHtml(msg, t) {
|
|
184
|
-
const obj = parseToolInput(msg);
|
|
185
|
-
if (!obj) return null;
|
|
186
|
-
try {
|
|
187
|
-
if (!obj.old_string && !obj.new_string) return null;
|
|
188
|
-
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
189
|
-
const filePath = obj.file_path || '';
|
|
190
|
-
const oldLines = (obj.old_string || '').split('\n');
|
|
191
|
-
const newLines = (obj.new_string || '').split('\n');
|
|
192
|
-
let html = '';
|
|
193
|
-
if (filePath) {
|
|
194
|
-
html += '<div class="diff-file">' + esc(filePath) + (obj.replace_all ? ' <span class="diff-replace-all">' + (t ? t('tool.replaceAll') : '(replace all)') + '</span>' : '') + '</div>';
|
|
195
|
-
}
|
|
196
|
-
html += '<div class="diff-lines">';
|
|
197
|
-
for (const line of oldLines) {
|
|
198
|
-
html += '<div class="diff-removed">' + '<span class="diff-sign">-</span>' + esc(line) + '</div>';
|
|
199
|
-
}
|
|
200
|
-
for (const line of newLines) {
|
|
201
|
-
html += '<div class="diff-added">' + '<span class="diff-sign">+</span>' + esc(line) + '</div>';
|
|
202
|
-
}
|
|
203
|
-
html += '</div>';
|
|
204
|
-
return html;
|
|
205
|
-
} catch { return null; }
|
|
206
|
-
}
|
package/web/modules/sidebar.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
// ── Sidebar: session management, folder picker, grouped sessions ─────────────
|
|
2
|
-
const { computed } = Vue;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates sidebar functionality bound to reactive state.
|
|
6
|
-
* @param {object} deps
|
|
7
|
-
* @param {Function} deps.wsSend
|
|
8
|
-
* @param {import('vue').Ref} deps.messages
|
|
9
|
-
* @param {import('vue').Ref} deps.isProcessing
|
|
10
|
-
* @param {import('vue').Ref} deps.sidebarOpen
|
|
11
|
-
* @param {import('vue').Ref} deps.historySessions
|
|
12
|
-
* @param {import('vue').Ref} deps.currentClaudeSessionId
|
|
13
|
-
* @param {import('vue').Ref} deps.needsResume
|
|
14
|
-
* @param {import('vue').Ref} deps.loadingSessions
|
|
15
|
-
* @param {import('vue').Ref} deps.loadingHistory
|
|
16
|
-
* @param {import('vue').Ref} deps.workDir
|
|
17
|
-
* @param {import('vue').Ref} deps.visibleLimit
|
|
18
|
-
* @param {import('vue').Ref} deps.folderPickerOpen
|
|
19
|
-
* @param {import('vue').Ref} deps.folderPickerPath
|
|
20
|
-
* @param {import('vue').Ref} deps.folderPickerEntries
|
|
21
|
-
* @param {import('vue').Ref} deps.folderPickerLoading
|
|
22
|
-
* @param {import('vue').Ref} deps.folderPickerSelected
|
|
23
|
-
* @param {object} deps.streaming - streaming controller
|
|
24
|
-
* @param {import('vue').Ref} deps.hostname
|
|
25
|
-
* @param {import('vue').Ref} deps.workdirHistory
|
|
26
|
-
*/
|
|
27
|
-
export function createSidebar(deps) {
|
|
28
|
-
const {
|
|
29
|
-
wsSend, messages, isProcessing, sidebarOpen,
|
|
30
|
-
historySessions, currentClaudeSessionId, needsResume,
|
|
31
|
-
loadingSessions, loadingHistory, workDir, visibleLimit,
|
|
32
|
-
folderPickerOpen, folderPickerPath, folderPickerEntries,
|
|
33
|
-
folderPickerLoading, folderPickerSelected, streaming,
|
|
34
|
-
hostname, workdirHistory, workdirSwitching,
|
|
35
|
-
// Multi-session parallel
|
|
36
|
-
currentConversationId, conversationCache, processingConversations,
|
|
37
|
-
switchConversation,
|
|
38
|
-
// i18n
|
|
39
|
-
t,
|
|
40
|
-
} = deps;
|
|
41
|
-
|
|
42
|
-
// Late-binding callback: called when user switches to a normal chat session
|
|
43
|
-
let _onSwitchToChat = null;
|
|
44
|
-
function setOnSwitchToChat(fn) { _onSwitchToChat = fn; }
|
|
45
|
-
|
|
46
|
-
// ── Workdir switching timeout ──
|
|
47
|
-
let _workdirSwitchTimer = null;
|
|
48
|
-
function setWorkdirSwitching() {
|
|
49
|
-
workdirSwitching.value = true;
|
|
50
|
-
clearTimeout(_workdirSwitchTimer);
|
|
51
|
-
_workdirSwitchTimer = setTimeout(() => { workdirSwitching.value = false; }, 10000);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Session management ──
|
|
55
|
-
|
|
56
|
-
let _sessionListTimer = null;
|
|
57
|
-
|
|
58
|
-
function requestSessionList() {
|
|
59
|
-
// Debounce: coalesce rapid calls (e.g. session_started + turn_completed)
|
|
60
|
-
// into a single request. First call fires immediately, subsequent calls
|
|
61
|
-
// within 2s are deferred.
|
|
62
|
-
if (_sessionListTimer) {
|
|
63
|
-
clearTimeout(_sessionListTimer);
|
|
64
|
-
_sessionListTimer = setTimeout(() => {
|
|
65
|
-
_sessionListTimer = null;
|
|
66
|
-
loadingSessions.value = true;
|
|
67
|
-
wsSend({ type: 'list_sessions' });
|
|
68
|
-
}, 2000);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
loadingSessions.value = true;
|
|
72
|
-
wsSend({ type: 'list_sessions' });
|
|
73
|
-
_sessionListTimer = setTimeout(() => { _sessionListTimer = null; }, 2000);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function resumeSession(session) {
|
|
77
|
-
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
78
|
-
if (_onSwitchToChat) _onSwitchToChat();
|
|
79
|
-
|
|
80
|
-
// Multi-session: check if we already have a conversation loaded for this claudeSessionId
|
|
81
|
-
if (switchConversation && conversationCache) {
|
|
82
|
-
// Check cache for existing conversation with this claudeSessionId
|
|
83
|
-
for (const [convId, cached] of Object.entries(conversationCache.value)) {
|
|
84
|
-
if (cached.claudeSessionId === session.sessionId) {
|
|
85
|
-
switchConversation(convId);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Check if current foreground already shows this session
|
|
90
|
-
if (currentClaudeSessionId.value === session.sessionId) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// Create new conversationId, switch to it, then send resume
|
|
94
|
-
const newConvId = crypto.randomUUID();
|
|
95
|
-
switchConversation(newConvId);
|
|
96
|
-
currentClaudeSessionId.value = session.sessionId;
|
|
97
|
-
needsResume.value = true;
|
|
98
|
-
loadingHistory.value = true;
|
|
99
|
-
wsSend({
|
|
100
|
-
type: 'resume_conversation',
|
|
101
|
-
conversationId: newConvId,
|
|
102
|
-
claudeSessionId: session.sessionId,
|
|
103
|
-
});
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Legacy fallback (no multi-session)
|
|
108
|
-
if (isProcessing.value) return;
|
|
109
|
-
messages.value = [];
|
|
110
|
-
visibleLimit.value = 50;
|
|
111
|
-
streaming.setMessageIdCounter(0);
|
|
112
|
-
streaming.setStreamingMessageId(null);
|
|
113
|
-
streaming.reset();
|
|
114
|
-
|
|
115
|
-
currentClaudeSessionId.value = session.sessionId;
|
|
116
|
-
needsResume.value = true;
|
|
117
|
-
loadingHistory.value = true;
|
|
118
|
-
|
|
119
|
-
wsSend({
|
|
120
|
-
type: 'resume_conversation',
|
|
121
|
-
claudeSessionId: session.sessionId,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function newConversation() {
|
|
126
|
-
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
127
|
-
if (_onSwitchToChat) _onSwitchToChat();
|
|
128
|
-
|
|
129
|
-
// Multi-session: just switch to a new blank conversation
|
|
130
|
-
if (switchConversation) {
|
|
131
|
-
const newConvId = crypto.randomUUID();
|
|
132
|
-
switchConversation(newConvId);
|
|
133
|
-
messages.value.push({
|
|
134
|
-
id: streaming.nextId(), role: 'system',
|
|
135
|
-
content: t('system.newConversation'),
|
|
136
|
-
timestamp: new Date(),
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Legacy fallback (no multi-session)
|
|
142
|
-
if (isProcessing.value) return;
|
|
143
|
-
messages.value = [];
|
|
144
|
-
visibleLimit.value = 50;
|
|
145
|
-
streaming.setMessageIdCounter(0);
|
|
146
|
-
streaming.setStreamingMessageId(null);
|
|
147
|
-
streaming.reset();
|
|
148
|
-
currentClaudeSessionId.value = null;
|
|
149
|
-
needsResume.value = false;
|
|
150
|
-
|
|
151
|
-
// Tell the agent to clear its lastClaudeSessionId so the next message
|
|
152
|
-
// starts a fresh session instead of auto-resuming the previous one.
|
|
153
|
-
wsSend({ type: 'new_conversation' });
|
|
154
|
-
|
|
155
|
-
messages.value.push({
|
|
156
|
-
id: streaming.nextId(), role: 'system',
|
|
157
|
-
content: 'New conversation started.',
|
|
158
|
-
timestamp: new Date(),
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function toggleSidebar() {
|
|
163
|
-
sidebarOpen.value = !sidebarOpen.value;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Delete session ──
|
|
167
|
-
|
|
168
|
-
/** Session pending delete confirmation (null = dialog closed) */
|
|
169
|
-
let pendingDeleteSession = null;
|
|
170
|
-
const deleteConfirmOpen = deps.deleteConfirmOpen;
|
|
171
|
-
const deleteConfirmTitle = deps.deleteConfirmTitle;
|
|
172
|
-
|
|
173
|
-
function deleteSession(session) {
|
|
174
|
-
if (currentClaudeSessionId.value === session.sessionId) return; // guard: foreground
|
|
175
|
-
// Guard: check background conversations that are actively processing
|
|
176
|
-
if (conversationCache) {
|
|
177
|
-
for (const [, cached] of Object.entries(conversationCache.value)) {
|
|
178
|
-
if (cached.claudeSessionId === session.sessionId && cached.isProcessing) return;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
pendingDeleteSession = session;
|
|
182
|
-
deleteConfirmTitle.value = session.title || session.sessionId.slice(0, 8);
|
|
183
|
-
deleteConfirmOpen.value = true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function confirmDeleteSession() {
|
|
187
|
-
if (!pendingDeleteSession) return;
|
|
188
|
-
wsSend({ type: 'delete_session', sessionId: pendingDeleteSession.sessionId });
|
|
189
|
-
deleteConfirmOpen.value = false;
|
|
190
|
-
pendingDeleteSession = null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function cancelDeleteSession() {
|
|
194
|
-
deleteConfirmOpen.value = false;
|
|
195
|
-
pendingDeleteSession = null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── Rename session ──
|
|
199
|
-
|
|
200
|
-
const renamingSessionId = deps.renamingSessionId;
|
|
201
|
-
const renameText = deps.renameText;
|
|
202
|
-
|
|
203
|
-
function startRename(session) {
|
|
204
|
-
renamingSessionId.value = session.sessionId;
|
|
205
|
-
renameText.value = session.title || '';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function confirmRename() {
|
|
209
|
-
const sid = renamingSessionId.value;
|
|
210
|
-
const title = renameText.value.trim();
|
|
211
|
-
if (!sid || !title) {
|
|
212
|
-
cancelRename();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
wsSend({ type: 'rename_session', sessionId: sid, newTitle: title });
|
|
216
|
-
renamingSessionId.value = null;
|
|
217
|
-
renameText.value = '';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function cancelRename() {
|
|
221
|
-
renamingSessionId.value = null;
|
|
222
|
-
renameText.value = '';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Folder picker ──
|
|
226
|
-
|
|
227
|
-
function openFolderPicker() {
|
|
228
|
-
folderPickerOpen.value = true;
|
|
229
|
-
folderPickerSelected.value = '';
|
|
230
|
-
folderPickerLoading.value = true;
|
|
231
|
-
folderPickerPath.value = workDir.value || '';
|
|
232
|
-
folderPickerEntries.value = [];
|
|
233
|
-
wsSend({ type: 'list_directory', dirPath: workDir.value || '' });
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function loadFolderPickerDir(dirPath) {
|
|
237
|
-
folderPickerLoading.value = true;
|
|
238
|
-
folderPickerSelected.value = '';
|
|
239
|
-
folderPickerEntries.value = [];
|
|
240
|
-
wsSend({ type: 'list_directory', dirPath });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function folderPickerNavigateUp() {
|
|
244
|
-
if (!folderPickerPath.value) return;
|
|
245
|
-
const isWin = folderPickerPath.value.includes('\\');
|
|
246
|
-
const parts = folderPickerPath.value.replace(/[/\\]$/, '').split(/[/\\]/);
|
|
247
|
-
parts.pop();
|
|
248
|
-
if (parts.length === 0) {
|
|
249
|
-
folderPickerPath.value = '';
|
|
250
|
-
loadFolderPickerDir('');
|
|
251
|
-
} else if (isWin && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
|
|
252
|
-
folderPickerPath.value = parts[0] + '\\';
|
|
253
|
-
loadFolderPickerDir(parts[0] + '\\');
|
|
254
|
-
} else {
|
|
255
|
-
const sep = isWin ? '\\' : '/';
|
|
256
|
-
const parent = parts.join(sep);
|
|
257
|
-
folderPickerPath.value = parent;
|
|
258
|
-
loadFolderPickerDir(parent);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function folderPickerSelectItem(entry) {
|
|
263
|
-
folderPickerSelected.value = entry.name;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function folderPickerEnter(entry) {
|
|
267
|
-
const sep = folderPickerPath.value.includes('\\') || /^[A-Z]:/.test(entry.name) ? '\\' : '/';
|
|
268
|
-
let newPath;
|
|
269
|
-
if (!folderPickerPath.value) {
|
|
270
|
-
newPath = entry.name + (entry.name.endsWith('\\') ? '' : '\\');
|
|
271
|
-
} else {
|
|
272
|
-
newPath = folderPickerPath.value.replace(/[/\\]$/, '') + sep + entry.name;
|
|
273
|
-
}
|
|
274
|
-
folderPickerPath.value = newPath;
|
|
275
|
-
folderPickerSelected.value = '';
|
|
276
|
-
loadFolderPickerDir(newPath);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function folderPickerGoToPath() {
|
|
280
|
-
const path = folderPickerPath.value.trim();
|
|
281
|
-
if (!path) {
|
|
282
|
-
loadFolderPickerDir('');
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
folderPickerSelected.value = '';
|
|
286
|
-
loadFolderPickerDir(path);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function confirmFolderPicker() {
|
|
290
|
-
let path = folderPickerPath.value;
|
|
291
|
-
if (!path) return;
|
|
292
|
-
if (folderPickerSelected.value) {
|
|
293
|
-
const sep = path.includes('\\') ? '\\' : '/';
|
|
294
|
-
path = path.replace(/[/\\]$/, '') + sep + folderPickerSelected.value;
|
|
295
|
-
}
|
|
296
|
-
folderPickerOpen.value = false;
|
|
297
|
-
setWorkdirSwitching();
|
|
298
|
-
wsSend({ type: 'change_workdir', workDir: path });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ── Working directory history ──
|
|
302
|
-
|
|
303
|
-
const WORKDIR_HISTORY_MAX = 10;
|
|
304
|
-
|
|
305
|
-
function getWorkdirHistoryKey() {
|
|
306
|
-
return `agentlink-workdir-history-${hostname.value}`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function loadWorkdirHistory() {
|
|
310
|
-
try {
|
|
311
|
-
const stored = localStorage.getItem(getWorkdirHistoryKey());
|
|
312
|
-
workdirHistory.value = stored ? JSON.parse(stored) : [];
|
|
313
|
-
} catch {
|
|
314
|
-
workdirHistory.value = [];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function saveWorkdirHistory() {
|
|
319
|
-
localStorage.setItem(getWorkdirHistoryKey(), JSON.stringify(workdirHistory.value));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function addToWorkdirHistory(path) {
|
|
323
|
-
if (!path) return;
|
|
324
|
-
const filtered = workdirHistory.value.filter(p => p !== path);
|
|
325
|
-
filtered.unshift(path);
|
|
326
|
-
workdirHistory.value = filtered.slice(0, WORKDIR_HISTORY_MAX);
|
|
327
|
-
saveWorkdirHistory();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function removeFromWorkdirHistory(path) {
|
|
331
|
-
workdirHistory.value = workdirHistory.value.filter(p => p !== path);
|
|
332
|
-
saveWorkdirHistory();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function switchToWorkdir(path) {
|
|
336
|
-
setWorkdirSwitching();
|
|
337
|
-
wsSend({ type: 'change_workdir', workDir: path });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const filteredWorkdirHistory = computed(() => {
|
|
341
|
-
return workdirHistory.value.filter(p => p !== workDir.value);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
// ── isSessionProcessing ──
|
|
345
|
-
// Used by sidebar template to show processing indicator on session items
|
|
346
|
-
function isSessionProcessing(claudeSessionId) {
|
|
347
|
-
if (!conversationCache || !processingConversations) return false;
|
|
348
|
-
// Check cached background conversations
|
|
349
|
-
for (const [convId, cached] of Object.entries(conversationCache.value)) {
|
|
350
|
-
if (cached.claudeSessionId === claudeSessionId && cached.isProcessing) {
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// Check current foreground conversation
|
|
355
|
-
if (currentClaudeSessionId.value === claudeSessionId && isProcessing.value) {
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ── Grouped sessions ──
|
|
362
|
-
|
|
363
|
-
const groupedSessions = computed(() => {
|
|
364
|
-
if (!historySessions.value.length) return [];
|
|
365
|
-
const now = new Date();
|
|
366
|
-
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
367
|
-
const yesterdayStart = todayStart - 86400000;
|
|
368
|
-
const weekStart = todayStart - 6 * 86400000;
|
|
369
|
-
|
|
370
|
-
const GROUP_KEYS = {
|
|
371
|
-
today: 'session.today',
|
|
372
|
-
yesterday: 'session.yesterday',
|
|
373
|
-
thisWeek: 'session.thisWeek',
|
|
374
|
-
earlier: 'session.earlier',
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const groups = {};
|
|
378
|
-
for (const s of historySessions.value) {
|
|
379
|
-
let key;
|
|
380
|
-
if (s.lastModified >= todayStart) key = 'today';
|
|
381
|
-
else if (s.lastModified >= yesterdayStart) key = 'yesterday';
|
|
382
|
-
else if (s.lastModified >= weekStart) key = 'thisWeek';
|
|
383
|
-
else key = 'earlier';
|
|
384
|
-
if (!groups[key]) groups[key] = [];
|
|
385
|
-
groups[key].push(s);
|
|
386
|
-
}
|
|
387
|
-
const order = ['today', 'yesterday', 'thisWeek', 'earlier'];
|
|
388
|
-
return order.filter(k => groups[k]).map(k => ({ label: t(GROUP_KEYS[k]), sessions: groups[k] }));
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
requestSessionList, resumeSession, newConversation, toggleSidebar,
|
|
393
|
-
setOnSwitchToChat,
|
|
394
|
-
deleteSession, confirmDeleteSession, cancelDeleteSession,
|
|
395
|
-
startRename, confirmRename, cancelRename,
|
|
396
|
-
openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
|
|
397
|
-
folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
|
|
398
|
-
groupedSessions, isSessionProcessing,
|
|
399
|
-
loadWorkdirHistory, addToWorkdirHistory, removeFromWorkdirHistory,
|
|
400
|
-
switchToWorkdir, filteredWorkdirHistory,
|
|
401
|
-
};
|
|
402
|
-
}
|