@agent-link/server 0.1.84 → 0.1.86
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/README.md +10 -3
- package/dist/context.d.ts +11 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -1
- package/dist/index.js +7 -19
- package/dist/index.js.map +1 -1
- package/package.json +54 -36
- package/web/app.js +733 -733
- package/web/modules/connection.js +450 -449
- package/web/modules/sidebar.js +262 -262
- package/web/style.css +2233 -2233
package/web/modules/sidebar.js
CHANGED
|
@@ -1,262 +1,262 @@
|
|
|
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,
|
|
35
|
-
} = deps;
|
|
36
|
-
|
|
37
|
-
// ── Session management ──
|
|
38
|
-
|
|
39
|
-
function requestSessionList() {
|
|
40
|
-
loadingSessions.value = true;
|
|
41
|
-
wsSend({ type: 'list_sessions' });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resumeSession(session) {
|
|
45
|
-
if (isProcessing.value) return;
|
|
46
|
-
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
47
|
-
messages.value = [];
|
|
48
|
-
visibleLimit.value = 50;
|
|
49
|
-
streaming.setMessageIdCounter(0);
|
|
50
|
-
streaming.setStreamingMessageId(null);
|
|
51
|
-
streaming.reset();
|
|
52
|
-
|
|
53
|
-
currentClaudeSessionId.value = session.sessionId;
|
|
54
|
-
needsResume.value = true;
|
|
55
|
-
loadingHistory.value = true;
|
|
56
|
-
|
|
57
|
-
wsSend({
|
|
58
|
-
type: 'resume_conversation',
|
|
59
|
-
claudeSessionId: session.sessionId,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function newConversation() {
|
|
64
|
-
if (isProcessing.value) return;
|
|
65
|
-
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
66
|
-
messages.value = [];
|
|
67
|
-
visibleLimit.value = 50;
|
|
68
|
-
streaming.setMessageIdCounter(0);
|
|
69
|
-
streaming.setStreamingMessageId(null);
|
|
70
|
-
streaming.reset();
|
|
71
|
-
currentClaudeSessionId.value = null;
|
|
72
|
-
needsResume.value = false;
|
|
73
|
-
|
|
74
|
-
messages.value.push({
|
|
75
|
-
id: streaming.nextId(), role: 'system',
|
|
76
|
-
content: 'New conversation started.',
|
|
77
|
-
timestamp: new Date(),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function toggleSidebar() {
|
|
82
|
-
sidebarOpen.value = !sidebarOpen.value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Delete session ──
|
|
86
|
-
|
|
87
|
-
/** Session pending delete confirmation (null = dialog closed) */
|
|
88
|
-
let pendingDeleteSession = null;
|
|
89
|
-
const deleteConfirmOpen = deps.deleteConfirmOpen;
|
|
90
|
-
const deleteConfirmTitle = deps.deleteConfirmTitle;
|
|
91
|
-
|
|
92
|
-
function deleteSession(session) {
|
|
93
|
-
if (isProcessing.value) return;
|
|
94
|
-
if (currentClaudeSessionId.value === session.sessionId) return; // guard
|
|
95
|
-
pendingDeleteSession = session;
|
|
96
|
-
deleteConfirmTitle.value = session.title || session.sessionId.slice(0, 8);
|
|
97
|
-
deleteConfirmOpen.value = true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function confirmDeleteSession() {
|
|
101
|
-
if (!pendingDeleteSession) return;
|
|
102
|
-
wsSend({ type: 'delete_session', sessionId: pendingDeleteSession.sessionId });
|
|
103
|
-
deleteConfirmOpen.value = false;
|
|
104
|
-
pendingDeleteSession = null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function cancelDeleteSession() {
|
|
108
|
-
deleteConfirmOpen.value = false;
|
|
109
|
-
pendingDeleteSession = null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── Folder picker ──
|
|
113
|
-
|
|
114
|
-
function openFolderPicker() {
|
|
115
|
-
folderPickerOpen.value = true;
|
|
116
|
-
folderPickerSelected.value = '';
|
|
117
|
-
folderPickerLoading.value = true;
|
|
118
|
-
folderPickerPath.value = workDir.value || '';
|
|
119
|
-
folderPickerEntries.value = [];
|
|
120
|
-
wsSend({ type: 'list_directory', dirPath: workDir.value || '' });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function loadFolderPickerDir(dirPath) {
|
|
124
|
-
folderPickerLoading.value = true;
|
|
125
|
-
folderPickerSelected.value = '';
|
|
126
|
-
folderPickerEntries.value = [];
|
|
127
|
-
wsSend({ type: 'list_directory', dirPath });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function folderPickerNavigateUp() {
|
|
131
|
-
if (!folderPickerPath.value) return;
|
|
132
|
-
const isWin = folderPickerPath.value.includes('\\');
|
|
133
|
-
const parts = folderPickerPath.value.replace(/[/\\]$/, '').split(/[/\\]/);
|
|
134
|
-
parts.pop();
|
|
135
|
-
if (parts.length === 0) {
|
|
136
|
-
folderPickerPath.value = '';
|
|
137
|
-
loadFolderPickerDir('');
|
|
138
|
-
} else if (isWin && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
|
|
139
|
-
folderPickerPath.value = parts[0] + '\\';
|
|
140
|
-
loadFolderPickerDir(parts[0] + '\\');
|
|
141
|
-
} else {
|
|
142
|
-
const sep = isWin ? '\\' : '/';
|
|
143
|
-
const parent = parts.join(sep);
|
|
144
|
-
folderPickerPath.value = parent;
|
|
145
|
-
loadFolderPickerDir(parent);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function folderPickerSelectItem(entry) {
|
|
150
|
-
folderPickerSelected.value = entry.name;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function folderPickerEnter(entry) {
|
|
154
|
-
const sep = folderPickerPath.value.includes('\\') || /^[A-Z]:/.test(entry.name) ? '\\' : '/';
|
|
155
|
-
let newPath;
|
|
156
|
-
if (!folderPickerPath.value) {
|
|
157
|
-
newPath = entry.name + (entry.name.endsWith('\\') ? '' : '\\');
|
|
158
|
-
} else {
|
|
159
|
-
newPath = folderPickerPath.value.replace(/[/\\]$/, '') + sep + entry.name;
|
|
160
|
-
}
|
|
161
|
-
folderPickerPath.value = newPath;
|
|
162
|
-
folderPickerSelected.value = '';
|
|
163
|
-
loadFolderPickerDir(newPath);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function folderPickerGoToPath() {
|
|
167
|
-
const path = folderPickerPath.value.trim();
|
|
168
|
-
if (!path) {
|
|
169
|
-
loadFolderPickerDir('');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
folderPickerSelected.value = '';
|
|
173
|
-
loadFolderPickerDir(path);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function confirmFolderPicker() {
|
|
177
|
-
let path = folderPickerPath.value;
|
|
178
|
-
if (!path) return;
|
|
179
|
-
if (folderPickerSelected.value) {
|
|
180
|
-
const sep = path.includes('\\') ? '\\' : '/';
|
|
181
|
-
path = path.replace(/[/\\]$/, '') + sep + folderPickerSelected.value;
|
|
182
|
-
}
|
|
183
|
-
folderPickerOpen.value = false;
|
|
184
|
-
wsSend({ type: 'change_workdir', workDir: path });
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── Working directory history ──
|
|
188
|
-
|
|
189
|
-
const WORKDIR_HISTORY_MAX = 10;
|
|
190
|
-
|
|
191
|
-
function getWorkdirHistoryKey() {
|
|
192
|
-
return `agentlink-workdir-history-${hostname.value}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function loadWorkdirHistory() {
|
|
196
|
-
try {
|
|
197
|
-
const stored = localStorage.getItem(getWorkdirHistoryKey());
|
|
198
|
-
workdirHistory.value = stored ? JSON.parse(stored) : [];
|
|
199
|
-
} catch {
|
|
200
|
-
workdirHistory.value = [];
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function saveWorkdirHistory() {
|
|
205
|
-
localStorage.setItem(getWorkdirHistoryKey(), JSON.stringify(workdirHistory.value));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function addToWorkdirHistory(path) {
|
|
209
|
-
if (!path) return;
|
|
210
|
-
const filtered = workdirHistory.value.filter(p => p !== path);
|
|
211
|
-
filtered.unshift(path);
|
|
212
|
-
workdirHistory.value = filtered.slice(0, WORKDIR_HISTORY_MAX);
|
|
213
|
-
saveWorkdirHistory();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function removeFromWorkdirHistory(path) {
|
|
217
|
-
workdirHistory.value = workdirHistory.value.filter(p => p !== path);
|
|
218
|
-
saveWorkdirHistory();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function switchToWorkdir(path) {
|
|
222
|
-
if (isProcessing.value) return;
|
|
223
|
-
wsSend({ type: 'change_workdir', workDir: path });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const filteredWorkdirHistory = computed(() => {
|
|
227
|
-
return workdirHistory.value.filter(p => p !== workDir.value);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ── Grouped sessions ──
|
|
231
|
-
|
|
232
|
-
const groupedSessions = computed(() => {
|
|
233
|
-
if (!historySessions.value.length) return [];
|
|
234
|
-
const now = new Date();
|
|
235
|
-
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
236
|
-
const yesterdayStart = todayStart - 86400000;
|
|
237
|
-
const weekStart = todayStart - 6 * 86400000;
|
|
238
|
-
|
|
239
|
-
const groups = {};
|
|
240
|
-
for (const s of historySessions.value) {
|
|
241
|
-
let label;
|
|
242
|
-
if (s.lastModified >= todayStart) label = 'Today';
|
|
243
|
-
else if (s.lastModified >= yesterdayStart) label = 'Yesterday';
|
|
244
|
-
else if (s.lastModified >= weekStart) label = 'This week';
|
|
245
|
-
else label = 'Earlier';
|
|
246
|
-
if (!groups[label]) groups[label] = [];
|
|
247
|
-
groups[label].push(s);
|
|
248
|
-
}
|
|
249
|
-
const order = ['Today', 'Yesterday', 'This week', 'Earlier'];
|
|
250
|
-
return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
requestSessionList, resumeSession, newConversation, toggleSidebar,
|
|
255
|
-
deleteSession, confirmDeleteSession, cancelDeleteSession,
|
|
256
|
-
openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
|
|
257
|
-
folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
|
|
258
|
-
groupedSessions,
|
|
259
|
-
loadWorkdirHistory, addToWorkdirHistory, removeFromWorkdirHistory,
|
|
260
|
-
switchToWorkdir, filteredWorkdirHistory,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
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,
|
|
35
|
+
} = deps;
|
|
36
|
+
|
|
37
|
+
// ── Session management ──
|
|
38
|
+
|
|
39
|
+
function requestSessionList() {
|
|
40
|
+
loadingSessions.value = true;
|
|
41
|
+
wsSend({ type: 'list_sessions' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resumeSession(session) {
|
|
45
|
+
if (isProcessing.value) return;
|
|
46
|
+
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
47
|
+
messages.value = [];
|
|
48
|
+
visibleLimit.value = 50;
|
|
49
|
+
streaming.setMessageIdCounter(0);
|
|
50
|
+
streaming.setStreamingMessageId(null);
|
|
51
|
+
streaming.reset();
|
|
52
|
+
|
|
53
|
+
currentClaudeSessionId.value = session.sessionId;
|
|
54
|
+
needsResume.value = true;
|
|
55
|
+
loadingHistory.value = true;
|
|
56
|
+
|
|
57
|
+
wsSend({
|
|
58
|
+
type: 'resume_conversation',
|
|
59
|
+
claudeSessionId: session.sessionId,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function newConversation() {
|
|
64
|
+
if (isProcessing.value) return;
|
|
65
|
+
if (window.innerWidth <= 768) sidebarOpen.value = false;
|
|
66
|
+
messages.value = [];
|
|
67
|
+
visibleLimit.value = 50;
|
|
68
|
+
streaming.setMessageIdCounter(0);
|
|
69
|
+
streaming.setStreamingMessageId(null);
|
|
70
|
+
streaming.reset();
|
|
71
|
+
currentClaudeSessionId.value = null;
|
|
72
|
+
needsResume.value = false;
|
|
73
|
+
|
|
74
|
+
messages.value.push({
|
|
75
|
+
id: streaming.nextId(), role: 'system',
|
|
76
|
+
content: 'New conversation started.',
|
|
77
|
+
timestamp: new Date(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toggleSidebar() {
|
|
82
|
+
sidebarOpen.value = !sidebarOpen.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Delete session ──
|
|
86
|
+
|
|
87
|
+
/** Session pending delete confirmation (null = dialog closed) */
|
|
88
|
+
let pendingDeleteSession = null;
|
|
89
|
+
const deleteConfirmOpen = deps.deleteConfirmOpen;
|
|
90
|
+
const deleteConfirmTitle = deps.deleteConfirmTitle;
|
|
91
|
+
|
|
92
|
+
function deleteSession(session) {
|
|
93
|
+
if (isProcessing.value) return;
|
|
94
|
+
if (currentClaudeSessionId.value === session.sessionId) return; // guard
|
|
95
|
+
pendingDeleteSession = session;
|
|
96
|
+
deleteConfirmTitle.value = session.title || session.sessionId.slice(0, 8);
|
|
97
|
+
deleteConfirmOpen.value = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function confirmDeleteSession() {
|
|
101
|
+
if (!pendingDeleteSession) return;
|
|
102
|
+
wsSend({ type: 'delete_session', sessionId: pendingDeleteSession.sessionId });
|
|
103
|
+
deleteConfirmOpen.value = false;
|
|
104
|
+
pendingDeleteSession = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cancelDeleteSession() {
|
|
108
|
+
deleteConfirmOpen.value = false;
|
|
109
|
+
pendingDeleteSession = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Folder picker ──
|
|
113
|
+
|
|
114
|
+
function openFolderPicker() {
|
|
115
|
+
folderPickerOpen.value = true;
|
|
116
|
+
folderPickerSelected.value = '';
|
|
117
|
+
folderPickerLoading.value = true;
|
|
118
|
+
folderPickerPath.value = workDir.value || '';
|
|
119
|
+
folderPickerEntries.value = [];
|
|
120
|
+
wsSend({ type: 'list_directory', dirPath: workDir.value || '' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function loadFolderPickerDir(dirPath) {
|
|
124
|
+
folderPickerLoading.value = true;
|
|
125
|
+
folderPickerSelected.value = '';
|
|
126
|
+
folderPickerEntries.value = [];
|
|
127
|
+
wsSend({ type: 'list_directory', dirPath });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function folderPickerNavigateUp() {
|
|
131
|
+
if (!folderPickerPath.value) return;
|
|
132
|
+
const isWin = folderPickerPath.value.includes('\\');
|
|
133
|
+
const parts = folderPickerPath.value.replace(/[/\\]$/, '').split(/[/\\]/);
|
|
134
|
+
parts.pop();
|
|
135
|
+
if (parts.length === 0) {
|
|
136
|
+
folderPickerPath.value = '';
|
|
137
|
+
loadFolderPickerDir('');
|
|
138
|
+
} else if (isWin && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
|
|
139
|
+
folderPickerPath.value = parts[0] + '\\';
|
|
140
|
+
loadFolderPickerDir(parts[0] + '\\');
|
|
141
|
+
} else {
|
|
142
|
+
const sep = isWin ? '\\' : '/';
|
|
143
|
+
const parent = parts.join(sep);
|
|
144
|
+
folderPickerPath.value = parent;
|
|
145
|
+
loadFolderPickerDir(parent);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function folderPickerSelectItem(entry) {
|
|
150
|
+
folderPickerSelected.value = entry.name;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function folderPickerEnter(entry) {
|
|
154
|
+
const sep = folderPickerPath.value.includes('\\') || /^[A-Z]:/.test(entry.name) ? '\\' : '/';
|
|
155
|
+
let newPath;
|
|
156
|
+
if (!folderPickerPath.value) {
|
|
157
|
+
newPath = entry.name + (entry.name.endsWith('\\') ? '' : '\\');
|
|
158
|
+
} else {
|
|
159
|
+
newPath = folderPickerPath.value.replace(/[/\\]$/, '') + sep + entry.name;
|
|
160
|
+
}
|
|
161
|
+
folderPickerPath.value = newPath;
|
|
162
|
+
folderPickerSelected.value = '';
|
|
163
|
+
loadFolderPickerDir(newPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function folderPickerGoToPath() {
|
|
167
|
+
const path = folderPickerPath.value.trim();
|
|
168
|
+
if (!path) {
|
|
169
|
+
loadFolderPickerDir('');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
folderPickerSelected.value = '';
|
|
173
|
+
loadFolderPickerDir(path);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function confirmFolderPicker() {
|
|
177
|
+
let path = folderPickerPath.value;
|
|
178
|
+
if (!path) return;
|
|
179
|
+
if (folderPickerSelected.value) {
|
|
180
|
+
const sep = path.includes('\\') ? '\\' : '/';
|
|
181
|
+
path = path.replace(/[/\\]$/, '') + sep + folderPickerSelected.value;
|
|
182
|
+
}
|
|
183
|
+
folderPickerOpen.value = false;
|
|
184
|
+
wsSend({ type: 'change_workdir', workDir: path });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Working directory history ──
|
|
188
|
+
|
|
189
|
+
const WORKDIR_HISTORY_MAX = 10;
|
|
190
|
+
|
|
191
|
+
function getWorkdirHistoryKey() {
|
|
192
|
+
return `agentlink-workdir-history-${hostname.value}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadWorkdirHistory() {
|
|
196
|
+
try {
|
|
197
|
+
const stored = localStorage.getItem(getWorkdirHistoryKey());
|
|
198
|
+
workdirHistory.value = stored ? JSON.parse(stored) : [];
|
|
199
|
+
} catch {
|
|
200
|
+
workdirHistory.value = [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function saveWorkdirHistory() {
|
|
205
|
+
localStorage.setItem(getWorkdirHistoryKey(), JSON.stringify(workdirHistory.value));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function addToWorkdirHistory(path) {
|
|
209
|
+
if (!path) return;
|
|
210
|
+
const filtered = workdirHistory.value.filter(p => p !== path);
|
|
211
|
+
filtered.unshift(path);
|
|
212
|
+
workdirHistory.value = filtered.slice(0, WORKDIR_HISTORY_MAX);
|
|
213
|
+
saveWorkdirHistory();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function removeFromWorkdirHistory(path) {
|
|
217
|
+
workdirHistory.value = workdirHistory.value.filter(p => p !== path);
|
|
218
|
+
saveWorkdirHistory();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function switchToWorkdir(path) {
|
|
222
|
+
if (isProcessing.value) return;
|
|
223
|
+
wsSend({ type: 'change_workdir', workDir: path });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const filteredWorkdirHistory = computed(() => {
|
|
227
|
+
return workdirHistory.value.filter(p => p !== workDir.value);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── Grouped sessions ──
|
|
231
|
+
|
|
232
|
+
const groupedSessions = computed(() => {
|
|
233
|
+
if (!historySessions.value.length) return [];
|
|
234
|
+
const now = new Date();
|
|
235
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
236
|
+
const yesterdayStart = todayStart - 86400000;
|
|
237
|
+
const weekStart = todayStart - 6 * 86400000;
|
|
238
|
+
|
|
239
|
+
const groups = {};
|
|
240
|
+
for (const s of historySessions.value) {
|
|
241
|
+
let label;
|
|
242
|
+
if (s.lastModified >= todayStart) label = 'Today';
|
|
243
|
+
else if (s.lastModified >= yesterdayStart) label = 'Yesterday';
|
|
244
|
+
else if (s.lastModified >= weekStart) label = 'This week';
|
|
245
|
+
else label = 'Earlier';
|
|
246
|
+
if (!groups[label]) groups[label] = [];
|
|
247
|
+
groups[label].push(s);
|
|
248
|
+
}
|
|
249
|
+
const order = ['Today', 'Yesterday', 'This week', 'Earlier'];
|
|
250
|
+
return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
requestSessionList, resumeSession, newConversation, toggleSidebar,
|
|
255
|
+
deleteSession, confirmDeleteSession, cancelDeleteSession,
|
|
256
|
+
openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
|
|
257
|
+
folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
|
|
258
|
+
groupedSessions,
|
|
259
|
+
loadWorkdirHistory, addToWorkdirHistory, removeFromWorkdirHistory,
|
|
260
|
+
switchToWorkdir, filteredWorkdirHistory,
|
|
261
|
+
};
|
|
262
|
+
}
|