claude-simple 0.2.6 → 0.2.8
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 +2 -0
- package/dist/app.0.2.8.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.8.min.css +1598 -0
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +245 -1
- package/public/api.js +4 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +118 -17
- package/public/files.js +0 -0
- package/public/git.js +0 -0
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/mobile.js +0 -0
- package/public/notifications.js +215 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +0 -0
- package/public/router.js +0 -0
- package/public/settings.js +0 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +72 -23
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +41 -12
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +27 -28
- package/public/shell/session-nav.js +36 -1
- package/public/shell/templates.js +45 -7
- package/public/shell/util.js +0 -0
- package/public/shell.js +56 -7
- package/public/state.js +53 -13
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/public/wake-lock.js +0 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +180 -298
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/models.js +0 -0
- package/src/agents/sessions.js +106 -26
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +88 -11
- package/src/auto-update.js +0 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { $, delegate, esc } from './lib.js';
|
|
2
|
+
import { api } from './api.js';
|
|
3
|
+
import { coerceTs } from './shell/util.js';
|
|
4
|
+
|
|
5
|
+
let notifications = [];
|
|
6
|
+
let unreadCount = 0;
|
|
7
|
+
let openSession = null;
|
|
8
|
+
|
|
9
|
+
function fmtTime(ts) {
|
|
10
|
+
const ms = coerceTs(ts);
|
|
11
|
+
if (ms == null) return '';
|
|
12
|
+
const d = new Date(ms);
|
|
13
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
14
|
+
return d.toLocaleString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function updateBadge() {
|
|
18
|
+
const badge = $('notifications-badge');
|
|
19
|
+
if (!badge) return;
|
|
20
|
+
if (!unreadCount) {
|
|
21
|
+
badge.classList.add('hidden');
|
|
22
|
+
badge.textContent = '0';
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
badge.classList.remove('hidden');
|
|
26
|
+
badge.textContent = unreadCount > 99 ? '99+' : String(unreadCount);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positionNotificationsMenu() {
|
|
30
|
+
const menu = $('notifications-menu');
|
|
31
|
+
const btn = $('btn-notifications');
|
|
32
|
+
const topbar = $('topbar');
|
|
33
|
+
if (!menu || !btn || !topbar) return;
|
|
34
|
+
const btnRect = btn.getBoundingClientRect();
|
|
35
|
+
const topbarRect = topbar.getBoundingClientRect();
|
|
36
|
+
const width = Math.min(416, window.innerWidth - 16);
|
|
37
|
+
const left = Math.max(8, Math.min(window.innerWidth - width - 8, btnRect.right - width));
|
|
38
|
+
menu.style.top = `${Math.round(topbarRect.bottom + 8)}px`;
|
|
39
|
+
menu.style.left = `${Math.round(left)}px`;
|
|
40
|
+
menu.style.width = `${Math.round(width)}px`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderNotifications() {
|
|
44
|
+
const listEl = $('notifications-menu-list');
|
|
45
|
+
const emptyEl = $('notifications-menu-empty');
|
|
46
|
+
if (!listEl || !emptyEl) return;
|
|
47
|
+
if (!notifications.length) {
|
|
48
|
+
listEl.innerHTML = '';
|
|
49
|
+
emptyEl.classList.remove('hidden');
|
|
50
|
+
updateBadge();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
emptyEl.classList.add('hidden');
|
|
54
|
+
listEl.innerHTML = notifications.map((item) => `
|
|
55
|
+
<button type="button"
|
|
56
|
+
class="w-full text-left rounded-lg border px-3 py-2 transition-colors hover:bg-base-200/70 ${item.unread ? 'border-primary/35 bg-primary/5' : 'border-base-300 bg-base-100'}"
|
|
57
|
+
data-notification-id="${esc(item.id)}"
|
|
58
|
+
data-notification-session="${esc(item.sessionId || '')}"
|
|
59
|
+
data-notification-agent="${esc(item.agent || 'claude')}"
|
|
60
|
+
data-notification-cwd="${esc(item.cwd || '')}"
|
|
61
|
+
data-notification-machine="${esc(item.machineId || '')}">
|
|
62
|
+
<div class="flex items-start gap-2">
|
|
63
|
+
<span class="text-sm leading-5">🔔</span>
|
|
64
|
+
<div class="min-w-0 flex-1">
|
|
65
|
+
<div class="flex items-center gap-2">
|
|
66
|
+
<span class="text-xs font-semibold truncate">${esc(item.title || 'Session done')}</span>
|
|
67
|
+
${item.unread ? '<span class="inline-block w-2 h-2 rounded-full bg-primary flex-shrink-0"></span>' : ''}
|
|
68
|
+
</div>
|
|
69
|
+
${item.body ? `<div class="text-[11px] text-base-content/55 mt-0.5 line-clamp-2">${esc(item.body)}</div>` : ''}
|
|
70
|
+
<div class="flex items-center gap-2 mt-1 text-[10px] text-base-content/35">
|
|
71
|
+
${item.projectName ? `<span class="font-mono truncate">${esc(item.projectName)}</span>` : ''}
|
|
72
|
+
${item.machineId ? `<span class="px-1 py-0.5 rounded bg-base-300 text-base-content/55 font-mono">${esc(item.machineId)}</span>` : ''}
|
|
73
|
+
<span>${esc(fmtTime(item.createdAt))}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</button>`).join('');
|
|
78
|
+
updateBadge();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function closeNotificationsMenu() {
|
|
82
|
+
$('notifications-menu')?.classList.add('hidden');
|
|
83
|
+
$('notifications-backdrop')?.classList.add('hidden');
|
|
84
|
+
$('btn-notifications')?.setAttribute('aria-expanded', 'false');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function openNotificationsMenu() {
|
|
88
|
+
positionNotificationsMenu();
|
|
89
|
+
$('notifications-menu')?.classList.remove('hidden');
|
|
90
|
+
$('notifications-backdrop')?.classList.remove('hidden');
|
|
91
|
+
$('btn-notifications')?.setAttribute('aria-expanded', 'true');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function toggleNotificationsMenu() {
|
|
95
|
+
const menu = $('notifications-menu');
|
|
96
|
+
if (!menu) return;
|
|
97
|
+
if (menu.classList.contains('hidden')) openNotificationsMenu();
|
|
98
|
+
else closeNotificationsMenu();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mergeNotifications(rows) {
|
|
102
|
+
const map = new Map();
|
|
103
|
+
for (const item of rows || []) {
|
|
104
|
+
if (!item?.id) continue;
|
|
105
|
+
map.set(String(item.id), item);
|
|
106
|
+
}
|
|
107
|
+
notifications = [...map.values()].sort((a, b) => (coerceTs(b.createdAt) || 0) - (coerceTs(a.createdAt) || 0));
|
|
108
|
+
unreadCount = notifications.reduce((sum, item) => sum + (item.unread ? 1 : 0), 0);
|
|
109
|
+
renderNotifications();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function refreshNotifications() {
|
|
113
|
+
try {
|
|
114
|
+
const data = await api('GET', '/api/notifications?limit=80');
|
|
115
|
+
notifications = Array.isArray(data?.notifications) ? data.notifications : [];
|
|
116
|
+
unreadCount = Number(data?.unreadCount) || notifications.reduce((sum, item) => sum + (item.unread ? 1 : 0), 0);
|
|
117
|
+
renderNotifications();
|
|
118
|
+
} catch {
|
|
119
|
+
notifications = [];
|
|
120
|
+
unreadCount = 0;
|
|
121
|
+
renderNotifications();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function markNotificationsRead(ids = null) {
|
|
126
|
+
const data = await api('POST', '/api/notifications/read', ids?.length ? { ids } : {});
|
|
127
|
+
notifications = Array.isArray(data?.notifications) ? data.notifications : notifications.map(item => (
|
|
128
|
+
!ids || ids.includes(item.id) ? { ...item, unread: false } : item
|
|
129
|
+
));
|
|
130
|
+
unreadCount = Number(data?.unreadCount) || notifications.reduce((sum, item) => sum + (item.unread ? 1 : 0), 0);
|
|
131
|
+
renderNotifications();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function maybeShowBrowserNotification(item) {
|
|
135
|
+
if (typeof Notification === 'undefined') return;
|
|
136
|
+
if (document.visibilityState === 'visible') return;
|
|
137
|
+
if (Notification.permission !== 'granted') return;
|
|
138
|
+
try {
|
|
139
|
+
const n = new Notification(item.title || 'Session done', {
|
|
140
|
+
body: item.body || item.projectName || item.sessionId || '',
|
|
141
|
+
tag: item.id,
|
|
142
|
+
});
|
|
143
|
+
n.onclick = () => {
|
|
144
|
+
window.focus();
|
|
145
|
+
n.close();
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function pushNotification(item) {
|
|
153
|
+
if (!item?.id) return;
|
|
154
|
+
const existing = notifications.find(row => row.id === item.id);
|
|
155
|
+
if (existing) {
|
|
156
|
+
mergeNotifications([item, ...notifications.filter(row => row.id !== item.id)]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
mergeNotifications([item, ...notifications]);
|
|
160
|
+
maybeShowBrowserNotification(item);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function initNotifications(opts = {}) {
|
|
164
|
+
openSession = typeof opts.openSession === 'function' ? opts.openSession : null;
|
|
165
|
+
renderNotifications();
|
|
166
|
+
refreshNotifications();
|
|
167
|
+
|
|
168
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
169
|
+
document.addEventListener('click', () => {
|
|
170
|
+
try { Notification.requestPermission().catch(() => {}); } catch {}
|
|
171
|
+
}, { once: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
delegate.on('click', '#notifications-backdrop', (e) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
closeNotificationsMenu();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
delegate.on('click', '#btn-notifications-read-all', async (e) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
await markNotificationsRead();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
delegate.on('click', '[data-notification-id]', async (_, el) => {
|
|
187
|
+
const id = el.dataset.notificationId;
|
|
188
|
+
if (!id) return;
|
|
189
|
+
await markNotificationsRead([id]);
|
|
190
|
+
closeNotificationsMenu();
|
|
191
|
+
if (!openSession || !el.dataset.notificationSession) return;
|
|
192
|
+
openSession(
|
|
193
|
+
el.dataset.notificationSession,
|
|
194
|
+
el.dataset.notificationCwd || null,
|
|
195
|
+
null,
|
|
196
|
+
el.dataset.notificationAgent || 'claude',
|
|
197
|
+
el.dataset.notificationMachine || null,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
document.addEventListener('click', (e) => {
|
|
202
|
+
const menu = $('notifications-menu');
|
|
203
|
+
const btn = $('btn-notifications');
|
|
204
|
+
const target = e.target;
|
|
205
|
+
if (!menu || menu.classList.contains('hidden')) return;
|
|
206
|
+
if (menu.contains(target) || btn?.contains(target)) return;
|
|
207
|
+
closeNotificationsMenu();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
window.addEventListener('resize', () => {
|
|
211
|
+
if ($('notifications-menu') && !$('notifications-menu').classList.contains('hidden')) {
|
|
212
|
+
positionNotificationsMenu();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
package/public/offline.html
CHANGED
|
File without changes
|
package/public/pwa.js
CHANGED
|
File without changes
|
package/public/router.js
CHANGED
|
File without changes
|
package/public/settings.js
CHANGED
|
File without changes
|
package/public/shell/boot.js
CHANGED
|
File without changes
|
|
@@ -176,7 +176,7 @@ export function updateDashboard(sessions) {
|
|
|
176
176
|
}).join('');
|
|
177
177
|
|
|
178
178
|
showSelectedDaySessions(sessions);
|
|
179
|
-
updateRecentSessionsList(
|
|
179
|
+
updateRecentSessionsList();
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export function showSelectedDaySessions(sessions) {
|
|
@@ -226,19 +226,62 @@ export function showSelectedDaySessions(sessions) {
|
|
|
226
226
|
|
|
227
227
|
/** How many sessions in the topbar “最近” quick-jump menu. */
|
|
228
228
|
export const RECENT_MENU_LIMIT = 15;
|
|
229
|
+
const RECENT_SESSIONS_KEY = 'recentSessionsV1';
|
|
229
230
|
|
|
230
|
-
function
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
function recentSessionKey(s) {
|
|
232
|
+
return `${s.machineId || ''}:${s.agent || 'claude'}:${s.sessionId}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The quick-jump list is deliberately opt-in: it starts empty and is populated
|
|
237
|
+
* only after a session is opened. Unlike sessionsData, this survives machine
|
|
238
|
+
* switches and contains sessions from every edge machine.
|
|
239
|
+
*/
|
|
240
|
+
export function getRecentSessions() {
|
|
241
|
+
try {
|
|
242
|
+
const saved = JSON.parse(localStorage.getItem(RECENT_SESSIONS_KEY) || '[]');
|
|
243
|
+
return Array.isArray(saved)
|
|
244
|
+
? saved.filter(s => s && s.sessionId)
|
|
245
|
+
: [];
|
|
246
|
+
} catch { return []; }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function saveRecentSessions(sessions) {
|
|
250
|
+
try { localStorage.setItem(RECENT_SESSIONS_KEY, JSON.stringify(sessions)); } catch { /* quota/private mode */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Add an explicitly opened session to the cross-machine quick-jump list. */
|
|
254
|
+
export function addRecentSession(session) {
|
|
255
|
+
if (!session?.sessionId) return;
|
|
256
|
+
const key = recentSessionKey(session);
|
|
257
|
+
const entry = { ...session, lastOpenedAt: Date.now() };
|
|
258
|
+
const recent = getRecentSessions().filter(s => recentSessionKey(s) !== key);
|
|
259
|
+
recent.unshift(entry);
|
|
260
|
+
saveRecentSessions(recent.slice(0, RECENT_MENU_LIMIT));
|
|
261
|
+
updateRecentSessionsList();
|
|
262
|
+
renderRecentSessionsMenu();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Remove an item from this device's quick-jump list; the actual session remains intact. */
|
|
266
|
+
export function dismissRecentSession(session) {
|
|
267
|
+
if (!session?.sessionId) return;
|
|
268
|
+
const key = recentSessionKey(session);
|
|
269
|
+
saveRecentSessions(getRecentSessions().filter(s => recentSessionKey(s) !== key));
|
|
270
|
+
updateRecentSessionsList();
|
|
271
|
+
renderRecentSessionsMenu();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sortRecentSessions(limit) {
|
|
275
|
+
return getRecentSessions()
|
|
276
|
+
.sort((a, b) => (coerceTs(b.lastOpenedAt) || 0) - (coerceTs(a.lastOpenedAt) || 0))
|
|
234
277
|
.slice(0, limit);
|
|
235
278
|
}
|
|
236
279
|
|
|
237
|
-
export function updateRecentSessionsList(
|
|
280
|
+
export function updateRecentSessionsList() {
|
|
238
281
|
const container = $('recent-sessions-list');
|
|
239
282
|
if (!container) return;
|
|
240
283
|
|
|
241
|
-
const recent = sortRecentSessions(
|
|
284
|
+
const recent = sortRecentSessions(6);
|
|
242
285
|
|
|
243
286
|
if (!recent.length) {
|
|
244
287
|
container.innerHTML = `<div class="col-span-full text-center text-xs text-base-content/40 py-4">No recent sessions</div>`;
|
|
@@ -315,31 +358,31 @@ export function positionRecentMenu() {
|
|
|
315
358
|
menu.style.maxHeight = `${Math.min(maxH, Math.round(window.innerHeight * 0.7))}px`;
|
|
316
359
|
}
|
|
317
360
|
|
|
318
|
-
export function openRecentMenu(
|
|
361
|
+
export function openRecentMenu() {
|
|
319
362
|
const menu = $('recent-menu');
|
|
320
363
|
if (!menu) return;
|
|
321
|
-
renderRecentSessionsMenu(
|
|
364
|
+
renderRecentSessionsMenu();
|
|
322
365
|
menu.classList.remove('hidden');
|
|
323
366
|
menu.classList.add('recent-menu-open');
|
|
324
367
|
positionRecentMenu();
|
|
325
368
|
$('recent-menu-backdrop')?.classList.remove('hidden');
|
|
326
369
|
}
|
|
327
370
|
|
|
328
|
-
export function toggleRecentMenu(
|
|
371
|
+
export function toggleRecentMenu() {
|
|
329
372
|
const menu = $('recent-menu');
|
|
330
373
|
if (!menu) return;
|
|
331
|
-
if (menu.classList.contains('hidden')) openRecentMenu(
|
|
374
|
+
if (menu.classList.contains('hidden')) openRecentMenu();
|
|
332
375
|
else closeRecentMenu();
|
|
333
376
|
}
|
|
334
377
|
|
|
335
|
-
/** Topbar quick-jump: last N sessions by
|
|
336
|
-
export function renderRecentSessionsMenu(
|
|
378
|
+
/** Topbar quick-jump: the last N sessions explicitly opened by this user. */
|
|
379
|
+
export function renderRecentSessionsMenu() {
|
|
337
380
|
const listEl = $('recent-menu-list');
|
|
338
381
|
const emptyEl = $('recent-menu-empty');
|
|
339
382
|
const countEl = $('recent-menu-count');
|
|
340
383
|
if (!listEl) return;
|
|
341
384
|
|
|
342
|
-
const recent = sortRecentSessions(
|
|
385
|
+
const recent = sortRecentSessions(RECENT_MENU_LIMIT);
|
|
343
386
|
if (countEl) countEl.textContent = recent.length ? `${recent.length}` : '';
|
|
344
387
|
|
|
345
388
|
if (!recent.length) {
|
|
@@ -357,14 +400,14 @@ export function renderRecentSessionsMenu(sessions) {
|
|
|
357
400
|
? `<span class="text-[9px] font-mono text-base-content/35 truncate max-w-[4.5rem]" title="${esc(s.machineId)}">@${esc(s.machineId)}</span>`
|
|
358
401
|
: '';
|
|
359
402
|
return `
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
403
|
+
<div class="recent-menu-item w-full text-left px-2 py-1.5 rounded-md flex items-start gap-2
|
|
404
|
+
${active ? 'bg-primary/15 text-primary' : 'hover:bg-base-200'}">
|
|
405
|
+
<button type="button" class="flex min-w-0 flex-1 items-start gap-2 text-left" aria-label="打开 ${esc(sessionDisplayTitle(s))}"
|
|
406
|
+
data-session-id="${esc(s.sessionId)}"
|
|
407
|
+
data-session-cwd="${esc(s.cwd || '')}"
|
|
408
|
+
data-session-config-dir="${esc(s.configDir || '')}"
|
|
409
|
+
data-session-agent="${esc(s.agent || 'claude')}"
|
|
410
|
+
data-session-machine="${esc(s.machineId || '')}">
|
|
368
411
|
<span class="flex-shrink-0 mt-0.5">${agentBadge(s.agent)}</span>
|
|
369
412
|
<span class="flex-1 min-w-0">
|
|
370
413
|
<span class="block text-xs font-semibold truncate leading-snug">${esc(sessionDisplayTitle(s))}</span>
|
|
@@ -375,7 +418,13 @@ export function renderRecentSessionsMenu(sessions) {
|
|
|
375
418
|
${machine}
|
|
376
419
|
${active ? '<span class="text-[9px] opacity-70">当前</span>' : ''}
|
|
377
420
|
</span>
|
|
378
|
-
|
|
421
|
+
</button>
|
|
422
|
+
<button type="button" class="recent-remove btn btn-ghost btn-xs min-h-7 h-7 w-7 px-0 flex-shrink-0 text-base-content/45 hover:text-error"
|
|
423
|
+
title="从最近会话移除" aria-label="从最近会话移除 ${esc(sessionDisplayTitle(s))}"
|
|
424
|
+
data-dismiss-recent-session="${esc(s.sessionId)}"
|
|
425
|
+
data-dismiss-recent-agent="${esc(s.agent || 'claude')}"
|
|
426
|
+
data-dismiss-recent-machine="${esc(s.machineId || '')}">×</button>
|
|
427
|
+
</div>`;
|
|
379
428
|
}).join('');
|
|
380
429
|
}
|
|
381
430
|
|
package/public/shell/hub.js
CHANGED
|
File without changes
|
package/public/shell/layout.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { batch, esc, $ } from '../lib.js';
|
|
3
3
|
import {
|
|
4
4
|
workspacesData, sessionFilter, filesPath, viewingFile, currentProject,
|
|
5
|
-
filesRoot, gitRoot, currentModel, currentAgent, setAgent,
|
|
5
|
+
filesRoot, gitRoot, currentModel, currentAgent, setAgent, setCurrentModel,
|
|
6
6
|
hubMode, machinesList, selectedMachineId, setSelectedMachine,
|
|
7
7
|
getDefaultModel, AGENT_LABELS, ctx,
|
|
8
8
|
} from '../state.js';
|
|
9
9
|
// filesRoot/gitRoot used when starting a new session (default paths)
|
|
10
10
|
import { api } from '../api.js';
|
|
11
|
-
import { connectWS, clearMessages, appendSystemMsg, applyChatDensity } from '../chat.js';
|
|
11
|
+
import { connectWS, clearMessages, appendSystemMsg, applyChatDensity, stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch } from '../chat.js';
|
|
12
12
|
import { refreshModelSelect } from './session-list.js';
|
|
13
13
|
import { switchTab } from './session-nav.js';
|
|
14
14
|
import { showMachinePicker } from './hub.js';
|
|
@@ -16,27 +16,37 @@ import { syncFocusScope } from '../mobile.js';
|
|
|
16
16
|
|
|
17
17
|
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
|
18
18
|
export function openSidebar() {
|
|
19
|
-
$('sidebar')
|
|
19
|
+
const sidebar = $('sidebar');
|
|
20
|
+
if (!sidebar) return;
|
|
21
|
+
sidebar.classList.add('open');
|
|
20
22
|
$('sidebar-overlay').classList.remove('hidden');
|
|
23
|
+
$('hamburger')?.setAttribute('aria-expanded', 'true');
|
|
21
24
|
syncFocusScope();
|
|
25
|
+
// Put the next action where it belongs. Delaying lets the drawer transition
|
|
26
|
+
// start first, which is more reliable in mobile Safari.
|
|
27
|
+
setTimeout(() => $('session-search')?.focus({ preventScroll: true }), 180);
|
|
22
28
|
}
|
|
23
29
|
export function closeSidebar() {
|
|
24
|
-
$('sidebar')
|
|
30
|
+
const sidebar = $('sidebar');
|
|
31
|
+
if (!sidebar) return;
|
|
32
|
+
sidebar.classList.remove('open');
|
|
25
33
|
$('sidebar-overlay').classList.add('hidden');
|
|
34
|
+
$('hamburger')?.setAttribute('aria-expanded', 'false');
|
|
26
35
|
syncFocusScope();
|
|
36
|
+
$('hamburger')?.focus({ preventScroll: true });
|
|
27
37
|
}
|
|
28
38
|
export function closeSidebarOnMobile() { if (window.innerWidth <= 640) closeSidebar(); }
|
|
29
39
|
|
|
30
40
|
export function initSidebarToggle() {
|
|
31
41
|
const btn = $('sidebar-toggle');
|
|
32
42
|
const sidebar = $('sidebar');
|
|
33
|
-
|
|
34
|
-
let
|
|
43
|
+
if (!btn || !sidebar) return;
|
|
44
|
+
let collapsed = localStorage.getItem('sidebarCollapsed') === '1';
|
|
45
|
+
let savedWidth = localStorage.getItem('sidebarWidth') || sidebar.style.width || '280px';
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
collapsed
|
|
47
|
+
const applyCollapsed = () => {
|
|
48
|
+
sidebar.classList.toggle('desktop-collapsed', collapsed);
|
|
38
49
|
if (collapsed) {
|
|
39
|
-
savedWidth = sidebar.style.width || '260px';
|
|
40
50
|
sidebar.style.width = '0';
|
|
41
51
|
sidebar.style.minWidth = '0';
|
|
42
52
|
sidebar.style.overflow = 'hidden';
|
|
@@ -46,6 +56,17 @@ export function initSidebarToggle() {
|
|
|
46
56
|
sidebar.style.overflow = '';
|
|
47
57
|
}
|
|
48
58
|
btn.textContent = collapsed ? '▶' : '◀';
|
|
59
|
+
btn.title = collapsed ? '展开侧栏' : '收起侧栏';
|
|
60
|
+
btn.setAttribute('aria-label', btn.title);
|
|
61
|
+
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
62
|
+
};
|
|
63
|
+
applyCollapsed();
|
|
64
|
+
|
|
65
|
+
btn.addEventListener('click', () => {
|
|
66
|
+
collapsed = !collapsed;
|
|
67
|
+
if (collapsed) savedWidth = sidebar.style.width || savedWidth;
|
|
68
|
+
localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0');
|
|
69
|
+
applyCollapsed();
|
|
49
70
|
});
|
|
50
71
|
}
|
|
51
72
|
|
|
@@ -71,6 +92,8 @@ export function initSidebarResize() {
|
|
|
71
92
|
dragging = false;
|
|
72
93
|
document.body.style.userSelect = '';
|
|
73
94
|
document.body.style.cursor = '';
|
|
95
|
+
const width = Math.round(sidebar.getBoundingClientRect().width);
|
|
96
|
+
if (width >= 160) localStorage.setItem('sidebarWidth', `${width}px`);
|
|
74
97
|
});
|
|
75
98
|
}
|
|
76
99
|
|
|
@@ -79,7 +102,9 @@ export function applyTheme(theme) {
|
|
|
79
102
|
document.documentElement.setAttribute('data-theme', theme);
|
|
80
103
|
localStorage.setItem('theme', theme);
|
|
81
104
|
const btn = $('btn-theme');
|
|
82
|
-
|
|
105
|
+
const icon = theme === 'dark' || theme === 'night' || theme === 'black' ? '🌙' : '☀️';
|
|
106
|
+
if (btn) btn.textContent = icon;
|
|
107
|
+
document.querySelectorAll('[data-sidebar-action="theme"]').forEach(el => { el.textContent = icon; });
|
|
83
108
|
// Swap highlight.js stylesheet for light/dark
|
|
84
109
|
let link = document.getElementById('hljs-theme');
|
|
85
110
|
if (!link) {
|
|
@@ -171,12 +196,15 @@ export async function startNewSession() {
|
|
|
171
196
|
$('modal-new-session').close();
|
|
172
197
|
|
|
173
198
|
const name = result.path.split('/').filter(Boolean).pop() || result.path;
|
|
199
|
+
stashComposerDraft();
|
|
200
|
+
parkQueuedMessages();
|
|
201
|
+
discardComposerAttachments();
|
|
202
|
+
detachProcessingForSessionSwitch();
|
|
174
203
|
ctx.sessionId = null;
|
|
175
204
|
ctx.configDir = configDir || null;
|
|
176
205
|
ctx.agent = agent;
|
|
177
206
|
setAgent(agent);
|
|
178
|
-
|
|
179
|
-
localStorage.setItem('model', currentModel.peek());
|
|
207
|
+
setCurrentModel(getDefaultModel(agent) || currentModel.peek());
|
|
180
208
|
refreshModelSelect();
|
|
181
209
|
batch(() => {
|
|
182
210
|
currentProject.value = { id: name, name, path: result.path, machineId: selectedMachineId.peek() || ctx.machineId };
|
|
@@ -194,6 +222,7 @@ export async function startNewSession() {
|
|
|
194
222
|
pv.classList.remove('hidden');
|
|
195
223
|
pv.style.display = 'flex';
|
|
196
224
|
clearMessages();
|
|
225
|
+
restoreComposerDraft({ sessionId: null });
|
|
197
226
|
connectWS();
|
|
198
227
|
switchTab('chat');
|
|
199
228
|
appendSystemMsg(`New ${AGENT_LABELS[agent] || agent} session · ${result.path}`);
|
package/public/shell/notes.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
sessionMetaMap, LOW_TURN_THRESHOLD, metaKey, sessionDisplayTitle,
|
|
9
9
|
hubMode, selectedMachineId, agentsMeta, currentEffort,
|
|
10
10
|
getModelsForAgent, getDefaultModel, getEffortsForModel,
|
|
11
|
-
addCustomModel, removeCustomModel, getCustomModels, ctx,
|
|
11
|
+
addCustomModel, removeCustomModel, getCustomModels, setCurrentModel, ctx,
|
|
12
12
|
} from '../state.js';
|
|
13
13
|
import { api, NOT_MODIFIED } from '../api.js';
|
|
14
14
|
import { getCachedSessions, setCachedSessions, getCachedWorkspaces, setCachedWorkspaces } from '../cache.js';
|
|
@@ -158,15 +158,8 @@ export async function loadAgentsMeta() {
|
|
|
158
158
|
try {
|
|
159
159
|
const data = await api('GET', '/api/agents');
|
|
160
160
|
agentsMeta.value = data;
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
const models = getModelsForAgent(agent);
|
|
164
|
-
const cur = currentModel.peek();
|
|
165
|
-
if (cur && !models.some(m => m.value === cur)) {
|
|
166
|
-
const def = getDefaultModel(agent);
|
|
167
|
-
currentModel.value = def;
|
|
168
|
-
localStorage.setItem('model', def);
|
|
169
|
-
}
|
|
161
|
+
// Do not replace a user's saved model merely because discovery has not
|
|
162
|
+
// listed it yet. refreshModelSelect renders it as a custom entry instead.
|
|
170
163
|
refreshModelSelect();
|
|
171
164
|
return data;
|
|
172
165
|
} catch (e) {
|
|
@@ -210,8 +203,7 @@ export function refreshModelSelect() {
|
|
|
210
203
|
).join('');
|
|
211
204
|
if (!models.some(m => m.value === cur)) {
|
|
212
205
|
const def = getDefaultModel(agent) || models[0]?.value;
|
|
213
|
-
|
|
214
|
-
localStorage.setItem('model', def);
|
|
206
|
+
setCurrentModel(def);
|
|
215
207
|
sel.value = def;
|
|
216
208
|
cur = def;
|
|
217
209
|
} else {
|
|
@@ -241,8 +233,7 @@ export function promptCustomModel({ editCurrent = false } = {}) {
|
|
|
241
233
|
removeCustomModel(agent, initial);
|
|
242
234
|
}
|
|
243
235
|
addCustomModel(agent, id);
|
|
244
|
-
|
|
245
|
-
localStorage.setItem('model', id);
|
|
236
|
+
setCurrentModel(id);
|
|
246
237
|
refreshModelSelect();
|
|
247
238
|
}
|
|
248
239
|
|
|
@@ -430,7 +421,7 @@ export function mdToHtml(text) {
|
|
|
430
421
|
}
|
|
431
422
|
}
|
|
432
423
|
|
|
433
|
-
//
|
|
424
|
+
// Memory is shown as an agent-specific file list. Content is loaded on click.
|
|
434
425
|
export async function loadMemoryTab() {
|
|
435
426
|
const body = $('memory-area');
|
|
436
427
|
if (!body) return;
|
|
@@ -440,22 +431,30 @@ export async function loadMemoryTab() {
|
|
|
440
431
|
}
|
|
441
432
|
body.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
442
433
|
try {
|
|
443
|
-
const
|
|
434
|
+
const agent = ctx.agent || currentAgent.peek() || 'claude';
|
|
435
|
+
const q = new URLSearchParams({ agent });
|
|
444
436
|
const mem = await api('GET', `/api/sessions/${ctx.sessionId}/memory?${q}`);
|
|
445
|
-
const
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
for (const f of (mem.files || [])) {
|
|
451
|
-
parts.push(`<div class="mb-3 border border-base-300 rounded-lg overflow-hidden">
|
|
452
|
-
<div class="px-3 py-1.5 bg-base-300/50 text-xs font-mono text-base-content/70">${esc(f.name)}</div>
|
|
453
|
-
<div class="px-3 py-2 md">${mdToHtml(f.content)}</div>
|
|
454
|
-
</div>`);
|
|
437
|
+
const files = mem.files || [];
|
|
438
|
+
if (!files.length) {
|
|
439
|
+
body.innerHTML = '<div class="p-4 text-base-content/40 text-xs">No memory files available for this agent/session.</div>';
|
|
440
|
+
return;
|
|
455
441
|
}
|
|
456
|
-
|
|
457
|
-
|
|
442
|
+
body.innerHTML = `<div class="p-3 text-[11px] text-base-content/45">${esc(agent)} · ${files.length} memory file${files.length === 1 ? '' : 's'} · click to open</div>
|
|
443
|
+
<div class="px-3 pb-4 flex flex-col gap-1">${files.map(f => `<button type="button" class="memory-file text-left px-3 py-2 rounded-lg border border-base-300 hover:border-primary hover:bg-primary/5" data-memory-file="${esc(f.id)}" data-memory-agent="${esc(agent)}"><span class="font-mono text-xs">${esc(f.name)}</span><span class="ml-2 text-[10px] text-base-content/40">${esc(f.scope || '')}</span></button>`).join('')}<div id="memory-file-content"></div></div>`;
|
|
458
444
|
} catch (e) {
|
|
459
445
|
body.innerHTML = `<div class="p-4 text-error text-xs">${esc(e?.message ?? String(e))}</div>`;
|
|
460
446
|
}
|
|
461
447
|
}
|
|
448
|
+
|
|
449
|
+
export async function openMemoryFile(fileId, agent) {
|
|
450
|
+
const out = $('memory-file-content');
|
|
451
|
+
if (!out || !ctx.sessionId || !fileId) return;
|
|
452
|
+
out.innerHTML = '<div class="p-3 text-xs text-base-content/40">Loading file…</div>';
|
|
453
|
+
try {
|
|
454
|
+
const q = new URLSearchParams({ agent: agent || ctx.agent || currentAgent.peek() || 'claude', file: fileId });
|
|
455
|
+
const row = await api('GET', `/api/sessions/${ctx.sessionId}/memory/file?${q}`);
|
|
456
|
+
out.innerHTML = `<div class="mt-2 border border-base-300 rounded-lg overflow-hidden"><div class="px-3 py-1.5 bg-base-300/50 text-xs font-mono">${esc(row.name)}</div><div class="px-3 py-2 md">${mdToHtml(row.content)}</div></div>`;
|
|
457
|
+
} catch (e) {
|
|
458
|
+
out.innerHTML = `<div class="mt-2 p-3 text-error text-xs">${esc(e?.message ?? String(e))}</div>`;
|
|
459
|
+
}
|
|
460
|
+
}
|