claude-simple 0.2.7 → 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 +0 -0
- package/dist/app.0.2.8.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.7.min.css → style.0.2.8.min.css} +1 -1
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +0 -0
- package/public/api.js +2 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +37 -6
- 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 +0 -0
- 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 +46 -26
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +2 -3
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +5 -14
- package/public/shell/session-nav.js +18 -1
- package/public/shell/templates.js +18 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +24 -6
- package/public/state.js +49 -7
- package/public/style.css +0 -0
- package/public/sw.js +1 -1
- package/public/terminal.js +0 -0
- 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 +0 -0
- package/src/agents/codex.js +0 -0
- package/src/agents/grok.js +180 -299
- package/src/agents/meta-agent.js +0 -0
- package/src/agents/models.js +0 -0
- package/src/agents/sessions.js +0 -0
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +0 -0
- 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.7.min.js +0 -1039
package/gateway.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/public/addon-fit.min.js
CHANGED
|
File without changes
|
package/public/agent-panel.js
CHANGED
|
File without changes
|
package/public/api.js
CHANGED
|
@@ -22,6 +22,8 @@ function isHubOnlyPath(path) {
|
|
|
22
22
|
const p = path.split('?')[0];
|
|
23
23
|
return p === '/api/hub'
|
|
24
24
|
|| p === '/api/machines'
|
|
25
|
+
|| p === '/api/notifications'
|
|
26
|
+
|| p === '/api/notifications/read'
|
|
25
27
|
|| p === '/api/ai/notes'
|
|
26
28
|
|| p.startsWith('/api/ai/notes/')
|
|
27
29
|
|| p.startsWith('/api/auth/');
|
package/public/app.js
CHANGED
|
File without changes
|
package/public/base.css
CHANGED
|
File without changes
|
package/public/cache.js
CHANGED
|
File without changes
|
package/public/chat.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { watch, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
|
|
4
4
|
currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
|
|
5
|
-
addCustomModel, chatDensity, wsStatus } from './state.js';
|
|
5
|
+
addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
|
|
6
6
|
import { sendWs, api, authHeaders, flushWsQueue, clearWsQueue } from './api.js';
|
|
7
7
|
import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
|
|
8
|
+
import { pushNotification } from './notifications.js';
|
|
8
9
|
|
|
9
10
|
function assistantLabel() {
|
|
10
11
|
return AGENT_LABELS[currentAgent.peek()] || 'Assistant';
|
|
@@ -327,12 +328,20 @@ function handleWsMessage(msg) {
|
|
|
327
328
|
return;
|
|
328
329
|
}
|
|
329
330
|
if (msg.type === 'session-created') {
|
|
331
|
+
const selectedModel = currentModel.peek();
|
|
330
332
|
ctx.sessionId = msg.sessionId;
|
|
331
|
-
if (msg.agent) setAgent(msg.agent);
|
|
333
|
+
if (msg.agent && msg.agent !== currentAgent.peek()) setAgent(msg.agent);
|
|
334
|
+
// The first turn selected a model before a session id existed. Bind that
|
|
335
|
+
// choice as soon as the edge creates the session.
|
|
336
|
+
setCurrentModel(selectedModel, msg.agent || currentAgent.peek(), msg.sessionId);
|
|
332
337
|
// Notify shell to refresh session list — use custom event to avoid circular import
|
|
333
338
|
document.dispatchEvent(new CustomEvent('sessions-changed'));
|
|
334
339
|
return;
|
|
335
340
|
}
|
|
341
|
+
if (msg.type === 'notification' && msg.notification) {
|
|
342
|
+
pushNotification(msg.notification);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
336
345
|
if (msg.type === 'result') {
|
|
337
346
|
finalizeThoughtStream();
|
|
338
347
|
finalizeAssistantStream();
|
|
@@ -579,8 +588,7 @@ function handleSlashCommand(text) {
|
|
|
579
588
|
if (a !== currentAgent.peek()) {
|
|
580
589
|
ctx.sessionId = null;
|
|
581
590
|
setAgent(a);
|
|
582
|
-
|
|
583
|
-
localStorage.setItem('model', currentModel.peek());
|
|
591
|
+
setCurrentModel(getDefaultModel(a));
|
|
584
592
|
document.dispatchEvent(new CustomEvent('agent-changed'));
|
|
585
593
|
appendSystemMsg(`Agent → ${AGENT_LABELS[a]} · model ${currentModel.peek()} · new session`);
|
|
586
594
|
} else {
|
|
@@ -600,8 +608,7 @@ function handleSlashCommand(text) {
|
|
|
600
608
|
// Persist free-form ids so they stay in the model dropdown
|
|
601
609
|
const known = getModelsForAgent(agent).some(m => m.value === resolved);
|
|
602
610
|
if (!known) addCustomModel(agent, resolved);
|
|
603
|
-
|
|
604
|
-
localStorage.setItem('model', resolved);
|
|
611
|
+
setCurrentModel(resolved);
|
|
605
612
|
appendSystemMsg(`Model set to ${resolved}${known ? '' : ' (saved as custom ★)'}`);
|
|
606
613
|
document.dispatchEvent(new CustomEvent('agent-changed'));
|
|
607
614
|
return true;
|
|
@@ -766,6 +773,27 @@ export function sendMessage() {
|
|
|
766
773
|
|
|
767
774
|
// While a turn is running, queue the next message (common Grok complaint).
|
|
768
775
|
if (isProcessing.peek()) {
|
|
776
|
+
// Grok owns its queue on the edge through a long-lived ACP process. Send
|
|
777
|
+
// immediately so a page reload cannot discard this follow-up in the tab.
|
|
778
|
+
if (currentAgent.peek() === 'grok' && ctx.sessionId) {
|
|
779
|
+
const agent = 'grok';
|
|
780
|
+
const ok = sendWs({
|
|
781
|
+
type: 'command', cmd: 'turn.start', agent, command: text, sessionId: ctx.sessionId,
|
|
782
|
+
options: {
|
|
783
|
+
agent, cwd: currentProject.peek().path, sessionId: ctx.sessionId,
|
|
784
|
+
configDir: ctx.configDir, model: currentModel.peek(),
|
|
785
|
+
...(currentEffort.peek() && { effort: currentEffort.peek() }),
|
|
786
|
+
...(currentPermission.peek() && { permissionMode: currentPermission.peek() }),
|
|
787
|
+
...(currentPermission.peek() === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
input.value = '';
|
|
791
|
+
autoResize(input);
|
|
792
|
+
updateInputMode('');
|
|
793
|
+
clearPendingAttachments();
|
|
794
|
+
appendSystemMsg(ok ? '已加入 Grok 服务端队列,刷新页面不会丢失' : '连接未就绪,消息将在重连后发送');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
769
797
|
pendingUserTexts.push({ key: draftKey(), text });
|
|
770
798
|
input.value = '';
|
|
771
799
|
autoResize(input);
|
|
@@ -806,6 +834,9 @@ export function sendMessage() {
|
|
|
806
834
|
const effort = currentEffort.peek();
|
|
807
835
|
const permission = currentPermission.peek();
|
|
808
836
|
const agent = currentAgent.peek() || 'claude';
|
|
837
|
+
// Existing sessions retain their own picker value. New sessions are bound
|
|
838
|
+
// on the session-created event above.
|
|
839
|
+
if (ctx.sessionId) setCurrentModel(currentModel.peek(), agent, ctx.sessionId);
|
|
809
840
|
const ok = sendWs({
|
|
810
841
|
type: 'command',
|
|
811
842
|
cmd: 'turn.start',
|
package/public/files.js
CHANGED
|
File without changes
|
package/public/git.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/public/index.html
CHANGED
|
File without changes
|
package/public/lib.js
CHANGED
|
File without changes
|
|
File without changes
|
package/public/mobile.js
CHANGED
|
File without changes
|
|
@@ -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,42 +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
|
|
229
|
+
const RECENT_SESSIONS_KEY = 'recentSessionsV1';
|
|
230
230
|
|
|
231
|
-
function
|
|
231
|
+
function recentSessionKey(s) {
|
|
232
232
|
return `${s.machineId || ''}:${s.agent || 'claude'}:${s.sessionId}`;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
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() {
|
|
236
241
|
try {
|
|
237
|
-
const saved = JSON.parse(localStorage.getItem(
|
|
238
|
-
return
|
|
239
|
-
|
|
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();
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
/** Remove an item from this device's quick-jump list; the actual session remains intact. */
|
|
243
266
|
export function dismissRecentSession(session) {
|
|
244
267
|
if (!session?.sessionId) return;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
renderRecentSessionsMenu(getDashboardSessions());
|
|
268
|
+
const key = recentSessionKey(session);
|
|
269
|
+
saveRecentSessions(getRecentSessions().filter(s => recentSessionKey(s) !== key));
|
|
270
|
+
updateRecentSessionsList();
|
|
271
|
+
renderRecentSessionsMenu();
|
|
250
272
|
}
|
|
251
273
|
|
|
252
|
-
function sortRecentSessions(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.filter(s => s && s.sessionId && !dismissed.has(recentDismissKey(s)))
|
|
256
|
-
.sort((a, b) => (coerceTs(b.updatedAt) || 0) - (coerceTs(a.updatedAt) || 0))
|
|
274
|
+
function sortRecentSessions(limit) {
|
|
275
|
+
return getRecentSessions()
|
|
276
|
+
.sort((a, b) => (coerceTs(b.lastOpenedAt) || 0) - (coerceTs(a.lastOpenedAt) || 0))
|
|
257
277
|
.slice(0, limit);
|
|
258
278
|
}
|
|
259
279
|
|
|
260
|
-
export function updateRecentSessionsList(
|
|
280
|
+
export function updateRecentSessionsList() {
|
|
261
281
|
const container = $('recent-sessions-list');
|
|
262
282
|
if (!container) return;
|
|
263
283
|
|
|
264
|
-
const recent = sortRecentSessions(
|
|
284
|
+
const recent = sortRecentSessions(6);
|
|
265
285
|
|
|
266
286
|
if (!recent.length) {
|
|
267
287
|
container.innerHTML = `<div class="col-span-full text-center text-xs text-base-content/40 py-4">No recent sessions</div>`;
|
|
@@ -338,31 +358,31 @@ export function positionRecentMenu() {
|
|
|
338
358
|
menu.style.maxHeight = `${Math.min(maxH, Math.round(window.innerHeight * 0.7))}px`;
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
export function openRecentMenu(
|
|
361
|
+
export function openRecentMenu() {
|
|
342
362
|
const menu = $('recent-menu');
|
|
343
363
|
if (!menu) return;
|
|
344
|
-
renderRecentSessionsMenu(
|
|
364
|
+
renderRecentSessionsMenu();
|
|
345
365
|
menu.classList.remove('hidden');
|
|
346
366
|
menu.classList.add('recent-menu-open');
|
|
347
367
|
positionRecentMenu();
|
|
348
368
|
$('recent-menu-backdrop')?.classList.remove('hidden');
|
|
349
369
|
}
|
|
350
370
|
|
|
351
|
-
export function toggleRecentMenu(
|
|
371
|
+
export function toggleRecentMenu() {
|
|
352
372
|
const menu = $('recent-menu');
|
|
353
373
|
if (!menu) return;
|
|
354
|
-
if (menu.classList.contains('hidden')) openRecentMenu(
|
|
374
|
+
if (menu.classList.contains('hidden')) openRecentMenu();
|
|
355
375
|
else closeRecentMenu();
|
|
356
376
|
}
|
|
357
377
|
|
|
358
|
-
/** Topbar quick-jump: last N sessions by
|
|
359
|
-
export function renderRecentSessionsMenu(
|
|
378
|
+
/** Topbar quick-jump: the last N sessions explicitly opened by this user. */
|
|
379
|
+
export function renderRecentSessionsMenu() {
|
|
360
380
|
const listEl = $('recent-menu-list');
|
|
361
381
|
const emptyEl = $('recent-menu-empty');
|
|
362
382
|
const countEl = $('recent-menu-count');
|
|
363
383
|
if (!listEl) return;
|
|
364
384
|
|
|
365
|
-
const recent = sortRecentSessions(
|
|
385
|
+
const recent = sortRecentSessions(RECENT_MENU_LIMIT);
|
|
366
386
|
if (countEl) countEl.textContent = recent.length ? `${recent.length}` : '';
|
|
367
387
|
|
|
368
388
|
if (!recent.length) {
|
package/public/shell/hub.js
CHANGED
|
File without changes
|
package/public/shell/layout.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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';
|
|
@@ -204,8 +204,7 @@ export async function startNewSession() {
|
|
|
204
204
|
ctx.configDir = configDir || null;
|
|
205
205
|
ctx.agent = agent;
|
|
206
206
|
setAgent(agent);
|
|
207
|
-
|
|
208
|
-
localStorage.setItem('model', currentModel.peek());
|
|
207
|
+
setCurrentModel(getDefaultModel(agent) || currentModel.peek());
|
|
209
208
|
refreshModelSelect();
|
|
210
209
|
batch(() => {
|
|
211
210
|
currentProject.value = { id: name, name, path: result.path, machineId: selectedMachineId.peek() || ctx.machineId };
|
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
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { setHash, syncHash } from '../router.js';
|
|
16
16
|
import { refreshModelSelect } from './session-list.js';
|
|
17
17
|
import { renderSessionNotesBar } from './notes.js';
|
|
18
|
-
import { updateDashboard, getDashboardSessions } from './dashboard.js';
|
|
18
|
+
import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
|
|
19
19
|
|
|
20
20
|
/** Show which edge machine Files/Git/Shell paths belong to. */
|
|
21
21
|
function syncMachinePathHints(machineId) {
|
|
@@ -137,6 +137,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
137
137
|
setSelectedMachine(resolvedMachine);
|
|
138
138
|
try { ctx.ws?.close(); } catch {}
|
|
139
139
|
ctx.ws = null;
|
|
140
|
+
// A recent entry can belong to another edge. Route the session request
|
|
141
|
+
// immediately, then refresh the surrounding machine UI in the background.
|
|
142
|
+
import('./hub.js').then(({ syncTopbarMachine }) => syncTopbarMachine()).catch(() => {});
|
|
143
|
+
import('./session-list.js').then(({ loadAllSessions }) => loadAllSessions()).catch(() => {});
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
ctx.sessionId = sid;
|
|
@@ -179,6 +183,19 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
179
183
|
machineId: resolvedMachine,
|
|
180
184
|
tab: targetTab,
|
|
181
185
|
});
|
|
186
|
+
// Recent is a deliberate browsing history, not an automatic list of every
|
|
187
|
+
// session on the currently selected machine. Keep the target machine in
|
|
188
|
+
// the entry so a later click can route back to the right edge.
|
|
189
|
+
addRecentSession({
|
|
190
|
+
sessionId: sid,
|
|
191
|
+
cwd: edgeCwd || resolvedCwd,
|
|
192
|
+
configDir: resolvedConfig,
|
|
193
|
+
agent: resolvedAgent,
|
|
194
|
+
machineId: resolvedMachine,
|
|
195
|
+
display: fromList?.display || fromLast?.display || '',
|
|
196
|
+
projectName: fromList?.projectName || fromLast?.projectName || '',
|
|
197
|
+
updatedAt: fromList?.updatedAt || fromLast?.updatedAt || null,
|
|
198
|
+
});
|
|
182
199
|
|
|
183
200
|
const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
|
|
184
201
|
const cacheKey = `${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
|
|
@@ -69,6 +69,13 @@ export const AppShell = (opts = {}) => {
|
|
|
69
69
|
<span class="text-[10px] opacity-50">▾</span>
|
|
70
70
|
</button>
|
|
71
71
|
</div>
|
|
72
|
+
<div id="notifications-wrap" class="relative flex-shrink-0">
|
|
73
|
+
<button type="button" id="btn-notifications" class="btn btn-ghost btn-xs border border-base-300 gap-1 min-h-8 px-2"
|
|
74
|
+
title="通知" aria-haspopup="dialog" aria-expanded="false">
|
|
75
|
+
<span>🔔</span>
|
|
76
|
+
<span id="notifications-badge" class="hidden inline-flex min-w-5 h-5 px-1 items-center justify-center rounded-full bg-error text-error-content text-[10px] font-bold">0</span>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
72
79
|
<span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
|
|
73
80
|
<span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
|
|
74
81
|
<div class="flex gap-2 topbar-actions">
|
|
@@ -93,6 +100,17 @@ export const AppShell = (opts = {}) => {
|
|
|
93
100
|
<div id="recent-menu-list" class="flex flex-col gap-0.5"></div>
|
|
94
101
|
<p id="recent-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无最近会话</p>
|
|
95
102
|
</div>
|
|
103
|
+
<div id="notifications-backdrop" class="hidden fixed inset-0 z-[55] bg-black/40" aria-hidden="true"></div>
|
|
104
|
+
<div id="notifications-menu" class="hidden fixed z-[60] w-[min(26rem,calc(100vw-1rem))] max-h-[min(30rem,75vh)] overflow-y-auto
|
|
105
|
+
rounded-lg border border-base-300 bg-base-100 shadow-xl p-2"
|
|
106
|
+
role="dialog" aria-label="通知中心">
|
|
107
|
+
<div class="flex items-center justify-between px-1 pb-1.5 mb-1 border-b border-base-300 sticky top-0 bg-base-100 z-[1]">
|
|
108
|
+
<span class="text-[10px] uppercase tracking-wide text-base-content/45 font-semibold">通知</span>
|
|
109
|
+
<button type="button" id="btn-notifications-read-all" class="btn btn-ghost btn-xs px-1 text-[10px]">全部已读</button>
|
|
110
|
+
</div>
|
|
111
|
+
<div id="notifications-menu-list" class="flex flex-col gap-1"></div>
|
|
112
|
+
<p id="notifications-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无通知</p>
|
|
113
|
+
</div>
|
|
96
114
|
<!-- Hub: only when no remembered machine (or last one offline) -->
|
|
97
115
|
<div id="machine-picker" class="hidden fixed inset-0 z-[60] bg-base-100 flex items-center justify-center p-4">
|
|
98
116
|
<div class="w-full max-w-md">
|
package/public/shell/util.js
CHANGED
|
File without changes
|