claude-simple 0.2.7 → 0.2.9
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.9.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.7.min.css → style.0.2.9.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 +42 -8
- 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 +22 -18
- 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 +64 -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 +415 -271
- package/src/agents/meta-agent.js +0 -0
- package/src/agents/models.js +2 -1
- package/src/agents/sessions.js +0 -0
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +12 -2
- 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
|
+
getDefaultEffort, 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,28 @@ 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
|
+
// Send immediately so a page reload cannot discard this follow-up in the
|
|
777
|
+
// tab. In hub mode the hub serializes it; standalone uses the edge queue.
|
|
778
|
+
if (currentAgent.peek() === 'grok' && ctx.sessionId) {
|
|
779
|
+
const agent = 'grok';
|
|
780
|
+
const effort = currentEffort.peek() || getDefaultEffort(agent, currentModel.peek()) || 'low';
|
|
781
|
+
const ok = sendWs({
|
|
782
|
+
type: 'command', cmd: 'turn.start', agent, command: text, sessionId: ctx.sessionId,
|
|
783
|
+
options: {
|
|
784
|
+
agent, cwd: currentProject.peek().path, sessionId: ctx.sessionId,
|
|
785
|
+
configDir: ctx.configDir, model: currentModel.peek(),
|
|
786
|
+
effort,
|
|
787
|
+
...(currentPermission.peek() && { permissionMode: currentPermission.peek() }),
|
|
788
|
+
...(currentPermission.peek() === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
input.value = '';
|
|
792
|
+
autoResize(input);
|
|
793
|
+
updateInputMode('');
|
|
794
|
+
clearPendingAttachments();
|
|
795
|
+
appendSystemMsg(ok ? '已加入 Grok 服务端队列,刷新页面不会丢失' : '连接未就绪,消息将在重连后发送');
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
769
798
|
pendingUserTexts.push({ key: draftKey(), text });
|
|
770
799
|
input.value = '';
|
|
771
800
|
autoResize(input);
|
|
@@ -803,9 +832,14 @@ export function sendMessage() {
|
|
|
803
832
|
} else {
|
|
804
833
|
flushToolBatch();
|
|
805
834
|
appendMsg('user', 'You', text, { ts: now });
|
|
806
|
-
const effort = currentEffort.peek();
|
|
807
|
-
const permission = currentPermission.peek();
|
|
808
835
|
const agent = currentAgent.peek() || 'claude';
|
|
836
|
+
const effort = currentEffort.peek()
|
|
837
|
+
|| getDefaultEffort(agent, currentModel.peek())
|
|
838
|
+
|| (agent === 'grok' ? 'low' : '');
|
|
839
|
+
const permission = currentPermission.peek();
|
|
840
|
+
// Existing sessions retain their own picker value. New sessions are bound
|
|
841
|
+
// on the session-created event above.
|
|
842
|
+
if (ctx.sessionId) setCurrentModel(currentModel.peek(), agent, ctx.sessionId);
|
|
809
843
|
const ok = sendWs({
|
|
810
844
|
type: 'command',
|
|
811
845
|
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
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
sidebarView, agentFilter, timeRange, activityHits, activityLoading,
|
|
8
8
|
sessionMetaMap, LOW_TURN_THRESHOLD, metaKey, sessionDisplayTitle,
|
|
9
9
|
hubMode, selectedMachineId, agentsMeta, currentEffort,
|
|
10
|
-
getModelsForAgent, getDefaultModel, getEffortsForModel,
|
|
11
|
-
addCustomModel, removeCustomModel, getCustomModels, ctx,
|
|
10
|
+
getModelsForAgent, getDefaultModel, getEffortsForModel, getDefaultEffort,
|
|
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) {
|
|
@@ -181,15 +174,28 @@ export function refreshEffortSelect() {
|
|
|
181
174
|
const agent = currentAgent.peek() || 'claude';
|
|
182
175
|
const model = currentModel.peek();
|
|
183
176
|
const efforts = getEffortsForModel(agent, model);
|
|
184
|
-
|
|
177
|
+
let cur = currentEffort.peek() || '';
|
|
178
|
+
if (!cur) {
|
|
179
|
+
const def = getDefaultEffort(agent, model);
|
|
180
|
+
if (def && efforts.includes(def)) {
|
|
181
|
+
cur = def;
|
|
182
|
+
currentEffort.value = def;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
185
|
const opts = [`<option value="">effort: off</option>`]
|
|
186
186
|
.concat(efforts.map(e =>
|
|
187
187
|
`<option value="${esc(e)}" ${e === cur ? 'selected' : ''}>${esc(e)}</option>`
|
|
188
188
|
));
|
|
189
189
|
sel.innerHTML = opts.join('');
|
|
190
190
|
if (cur && !efforts.includes(cur)) {
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
const def = getDefaultEffort(agent, model);
|
|
192
|
+
if (def && efforts.includes(def)) {
|
|
193
|
+
currentEffort.value = def;
|
|
194
|
+
sel.value = def;
|
|
195
|
+
} else {
|
|
196
|
+
currentEffort.value = '';
|
|
197
|
+
sel.value = '';
|
|
198
|
+
}
|
|
193
199
|
} else {
|
|
194
200
|
sel.value = cur;
|
|
195
201
|
}
|
|
@@ -210,8 +216,7 @@ export function refreshModelSelect() {
|
|
|
210
216
|
).join('');
|
|
211
217
|
if (!models.some(m => m.value === cur)) {
|
|
212
218
|
const def = getDefaultModel(agent) || models[0]?.value;
|
|
213
|
-
|
|
214
|
-
localStorage.setItem('model', def);
|
|
219
|
+
setCurrentModel(def);
|
|
215
220
|
sel.value = def;
|
|
216
221
|
cur = def;
|
|
217
222
|
} else {
|
|
@@ -241,8 +246,7 @@ export function promptCustomModel({ editCurrent = false } = {}) {
|
|
|
241
246
|
removeCustomModel(agent, initial);
|
|
242
247
|
}
|
|
243
248
|
addCustomModel(agent, id);
|
|
244
|
-
|
|
245
|
-
localStorage.setItem('model', id);
|
|
249
|
+
setCurrentModel(id);
|
|
246
250
|
refreshModelSelect();
|
|
247
251
|
}
|
|
248
252
|
|
|
@@ -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}`;
|