claude-simple 0.2.27 → 0.2.30
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 +17 -1
- package/bin/edge-ctl.js +151 -4
- package/client.js +29 -4
- package/dist/addon-fit.min.js +0 -0
- package/dist/app.0.2.30.min.js +1308 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.30.min.css +2012 -0
- package/dist/xterm.min.css +0 -0
- package/dist/xterm.min.js +0 -0
- package/gateway.js +0 -0
- package/package.json +2 -2
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +54 -22
- package/public/api.js +8 -5
- package/public/app.js +7 -5
- package/public/base.css +0 -0
- package/public/cache.js +19 -0
- package/public/chat.js +110 -19
- package/public/files.js +26 -10
- package/public/git.js +2 -2
- 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/markdown.js +16 -0
- package/public/math.js +145 -7
- package/public/mobile.js +0 -0
- package/public/notes.js +316 -0
- package/public/notifications.js +0 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +1 -1
- package/public/router.js +4 -0
- package/public/scroll-follow.js +78 -0
- package/public/settings.js +41 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +0 -0
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +0 -0
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +79 -21
- package/public/shell/session-nav.js +70 -21
- package/public/shell/templates.js +3 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +15 -2
- package/public/state.js +7 -7
- package/public/style.css +161 -0
- package/public/sw.js +10 -5
- package/public/terminal.js +0 -0
- package/public/wake-lock.js +0 -0
- package/public/workspace-path.js +61 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agent-quota.js +228 -0
- package/src/agents/claude.js +0 -0
- package/src/agents/codex.js +2 -0
- package/src/agents/cursor.js +2 -1
- package/src/agents/gemini.js +2 -1
- package/src/agents/grok.js +22 -1
- package/src/agents/meta-agent.js +7 -4
- package/src/agents/models.js +21 -12
- package/src/agents/sessions.js +270 -81
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +111 -51
- package/src/auto-update.js +2 -0
- package/src/collab.js +3 -0
- package/src/platform.js +133 -0
- package/src/project-api-path.js +14 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/chat-broadcast.js +12 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/port-websocket.js +80 -0
- package/src/runtime/protocol.js +0 -0
- package/src/session-history.js +31 -0
- package/dist/app.0.2.27.min.js +0 -1182
- package/dist/style.0.2.27.min.css +0 -1851
package/public/settings.js
CHANGED
|
@@ -33,11 +33,19 @@ function settingsMarkup() {
|
|
|
33
33
|
<nav class="set-nav" aria-label="设置分区">
|
|
34
34
|
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start is-on" data-set-sec="claude">Claude</button>
|
|
35
35
|
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="workspaces">Workspaces</button>
|
|
36
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="quota">额度</button>
|
|
36
37
|
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="users" id="set-nav-users">Users</button>
|
|
37
38
|
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="password">Password</button>
|
|
38
39
|
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="mcp">MCP</button>
|
|
39
40
|
</nav>
|
|
40
41
|
<div class="set-main">
|
|
42
|
+
<section class="set-panel" data-set-panel="quota" hidden>
|
|
43
|
+
<h2 class="text-lg font-semibold">订阅额度</h2>
|
|
44
|
+
<p class="text-sm text-base-content/60 mb-4">读取当前机器的 CLI 登录账号。额度每 5 分钟更新,API Key 和第三方网关可能无法提供订阅额度。</p>
|
|
45
|
+
<button type="button" id="quota-refresh" class="btn btn-sm btn-ghost">刷新</button>
|
|
46
|
+
<select id="quota-profile" class="select select-bordered select-sm" aria-label="Claude 配置账号"><option value="">Claude 默认账号</option></select>
|
|
47
|
+
<div id="quota-list" class="set-list mt-3"></div>
|
|
48
|
+
</section>
|
|
41
49
|
<section class="set-panel" data-set-panel="claude">
|
|
42
50
|
<h2 class="text-lg font-semibold">Claude env</h2>
|
|
43
51
|
<p class="text-sm text-base-content/60 mb-4">Workspace 的 <b>name</b> 对应一套 ANTHROPIC_*。Default 是底,同名覆盖。</p>
|
|
@@ -119,6 +127,8 @@ function settingsMarkup() {
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
function bind() {
|
|
130
|
+
$('quota-refresh')?.addEventListener('click', loadQuotas);
|
|
131
|
+
$('quota-profile')?.addEventListener('change', loadQuotas);
|
|
122
132
|
$('set-back')?.addEventListener('click', closeSettings);
|
|
123
133
|
document.querySelectorAll('[data-set-sec]').forEach(btn => {
|
|
124
134
|
btn.addEventListener('click', () => showSection(btn.dataset.setSec));
|
|
@@ -144,6 +154,37 @@ function showSection(id) {
|
|
|
144
154
|
document.querySelectorAll('[data-set-panel]').forEach(p => {
|
|
145
155
|
p.hidden = p.dataset.setPanel !== activeSection;
|
|
146
156
|
});
|
|
157
|
+
if (activeSection === 'quota') void loadQuotas();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let quotaEpoch = 0;
|
|
161
|
+
async function loadQuotas() {
|
|
162
|
+
const epoch = ++quotaEpoch;
|
|
163
|
+
const list = $('quota-list');
|
|
164
|
+
if (!list) return;
|
|
165
|
+
list.textContent = '正在读取当前机器的额度…';
|
|
166
|
+
const machine = selectedMachineId.peek();
|
|
167
|
+
const profile = $('quota-profile');
|
|
168
|
+
const selected = profile?.value || '';
|
|
169
|
+
if (profile) {
|
|
170
|
+
profile.innerHTML = '<option value="">Claude 默认账号</option>' + (workspacesData.peek() || []).map(w => `<option value="${esc(w.configDir)}">${esc(w.name || w.configDir)}</option>`).join('');
|
|
171
|
+
profile.value = selected;
|
|
172
|
+
}
|
|
173
|
+
if (hubMode.peek() && !machine) { list.textContent = '请先选择机器'; return; }
|
|
174
|
+
const rows = await Promise.all(['claude', 'grok', 'codex'].map(async agent => {
|
|
175
|
+
const query = new URLSearchParams({ agent });
|
|
176
|
+
if (agent === 'claude' && profile?.value) query.set('configDir', profile.value);
|
|
177
|
+
try { return await api('GET', `/api/agents/quota?${query}`, undefined, undefined, { machineId: machine || undefined }); }
|
|
178
|
+
catch (e) { return { agent, meters: [], message: e.message }; }
|
|
179
|
+
}));
|
|
180
|
+
if (epoch !== quotaEpoch || machine !== selectedMachineId.peek()) return;
|
|
181
|
+
list.innerHTML = rows.map(row => `<div class="border border-base-300 rounded-lg p-4 mb-3">
|
|
182
|
+
<h3 class="font-semibold">${esc(row.agent)} ${esc(row.plan || '')}</h3>
|
|
183
|
+
${(row.meters || []).map(m => `<div class="mt-3"><div class="text-sm">${esc(m.label)} · 已用 ${esc(m.usedPercent)}% · 剩余 ${esc(m.remainingPercent)}%</div>
|
|
184
|
+
<progress class="progress progress-primary w-full" max="100" value="${Number(m.usedPercent)}"></progress>
|
|
185
|
+
${m.resetsAt ? `<div class="text-xs opacity-60">重置:${esc(new Date(m.resetsAt).toLocaleString())}</div>` : ''}</div>`).join('') || `<p class="text-sm opacity-60 mt-2">${esc(row.message || '暂时无法读取额度')}</p>`}
|
|
186
|
+
${row.fetchedAt ? `<div class="text-xs opacity-50 mt-2">更新于 ${esc(new Date(row.fetchedAt).toLocaleString())}</div>` : ''}
|
|
187
|
+
</div>`).join('');
|
|
147
188
|
}
|
|
148
189
|
|
|
149
190
|
export function openSettings(section) {
|
package/public/shell/boot.js
CHANGED
|
File without changes
|
|
File without changes
|
package/public/shell/hub.js
CHANGED
|
File without changes
|
package/public/shell/layout.js
CHANGED
|
File without changes
|
package/public/shell/notes.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -10,12 +10,34 @@ import {
|
|
|
10
10
|
getModelsForAgent, getDefaultModel, getEffortsForModel, getDefaultEffort,
|
|
11
11
|
addCustomModel, removeCustomModel, getCustomModels, setCurrentModel, ctx,
|
|
12
12
|
} from '../state.js';
|
|
13
|
-
import { api
|
|
13
|
+
import { api } from '../api.js';
|
|
14
14
|
import { getCachedSessions, setCachedSessions, getCachedWorkspaces, setCachedWorkspaces } from '../cache.js';
|
|
15
15
|
import { formatTime, agentBadge, agentCountPills, shortPath } from './util.js';
|
|
16
|
-
import {
|
|
16
|
+
import { parseMarkdownWithMath } from '../markdown.js';
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
function sessionsFingerprint(list) {
|
|
20
|
+
if (!Array.isArray(list) || !list.length) return '';
|
|
21
|
+
return list.map(s => `${s.sessionId}:${s.updatedAt || 0}:${s.turnCount || 0}:${s.agent || ''}`).join('|');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function markActiveSessionRow(sessionId, agent) {
|
|
25
|
+
const wantAgent = agent || 'claude';
|
|
26
|
+
document.querySelectorAll('#session-list .session-row').forEach(row => {
|
|
27
|
+
const on = row.dataset.sessionId === sessionId
|
|
28
|
+
&& (row.dataset.sessionAgent || 'claude') === wantAgent;
|
|
29
|
+
row.classList.toggle('border-primary', on);
|
|
30
|
+
row.classList.toggle('bg-primary/5', on);
|
|
31
|
+
row.classList.toggle('border-transparent', !on);
|
|
32
|
+
const title = row.querySelector('.session-title-text');
|
|
33
|
+
if (title) {
|
|
34
|
+
title.classList.toggle('text-primary', on);
|
|
35
|
+
title.classList.toggle('font-medium', on);
|
|
36
|
+
title.classList.toggle('text-base-content/85', !on);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
function normalizeSessions(v) {
|
|
20
42
|
if (Array.isArray(v)) return v;
|
|
21
43
|
if (v && Array.isArray(v.results)) return v.results;
|
|
@@ -62,21 +84,47 @@ function orderBranchSessions(sessions) {
|
|
|
62
84
|
return out;
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
const SESSION_PAGE_SIZE = 200;
|
|
88
|
+
let sessionPageCount = 1;
|
|
89
|
+
let sessionListMachine = null;
|
|
90
|
+
let sessionListRequest = 0;
|
|
91
|
+
let sessionListHasMore = false;
|
|
92
|
+
let loadingSessionPage = false;
|
|
93
|
+
|
|
94
|
+
async function loadMoreSessions() {
|
|
95
|
+
if (loadingSessionPage) return;
|
|
96
|
+
loadingSessionPage = true;
|
|
97
|
+
try {
|
|
98
|
+
sessionPageCount++;
|
|
99
|
+
await loadAllSessions();
|
|
100
|
+
} finally { loadingSessionPage = false; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
document.addEventListener('click', (event) => {
|
|
104
|
+
if (event.target.closest?.('[data-load-more-sessions]')) loadMoreSessions().catch(console.warn);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function sessionsQuery({ hubIndex = false, offset = 0 } = {}) {
|
|
66
108
|
const q = new URLSearchParams();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
if (
|
|
109
|
+
q.set('limit', String(SESSION_PAGE_SIZE));
|
|
110
|
+
if (offset) q.set('offset', String(offset));
|
|
111
|
+
// A Hub keeps this catalog locally from Edge deltas. Always use it in Hub
|
|
112
|
+
// mode: an Edge scan can be slow while an agent is busy, and must never make
|
|
113
|
+
// the sidebar (or a page refresh) fail with a 502.
|
|
114
|
+
if (hubMode.peek() || hubIndex) q.set('index', '1');
|
|
73
115
|
const s = q.toString();
|
|
74
116
|
return s ? `/api/sessions?${s}` : '/api/sessions';
|
|
75
117
|
}
|
|
76
118
|
|
|
77
119
|
/** @param {{ waitFresh?: boolean, hubIndex?: boolean }} opts waitFresh=false → paint cache first, refresh in bg */
|
|
78
120
|
export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {}) {
|
|
79
|
-
const
|
|
121
|
+
const machine = selectedMachineId.peek() || null;
|
|
122
|
+
if (machine !== sessionListMachine) {
|
|
123
|
+
sessionListMachine = machine;
|
|
124
|
+
sessionPageCount = 1;
|
|
125
|
+
}
|
|
126
|
+
const request = ++sessionListRequest;
|
|
127
|
+
const cached = normalizeSessions(await getCachedSessions()).slice(0, SESSION_PAGE_SIZE * sessionPageCount);
|
|
80
128
|
if (cached.length) {
|
|
81
129
|
// Hub: if we already selected a machine, prefer cache entries for that machine
|
|
82
130
|
const mid = selectedMachineId.peek();
|
|
@@ -89,12 +137,22 @@ export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {
|
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
const fetchFresh = async () => {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
const list = [];
|
|
141
|
+
for (let page = 0; page < sessionPageCount; page++) {
|
|
142
|
+
const path = sessionsQuery({ hubIndex, offset: page * SESSION_PAGE_SIZE });
|
|
143
|
+
const fresh = await api('GET', path, undefined, undefined, { machineId: machine || undefined });
|
|
144
|
+
if (request !== sessionListRequest) return sessionsData.peek();
|
|
145
|
+
const rows = normalizeSessions(fresh);
|
|
146
|
+
list.push(...rows);
|
|
147
|
+
sessionListHasMore = rows.length === SESSION_PAGE_SIZE;
|
|
148
|
+
if (!sessionListHasMore) break;
|
|
149
|
+
}
|
|
150
|
+
const unique = new Map(list.map(s => [`${s.machineId || ''}:${s.agent}:${s.sessionId}`, s]));
|
|
151
|
+
list.splice(0, list.length, ...unique.values());
|
|
152
|
+
if (sessionsFingerprint(list) !== sessionsFingerprint(sessionsData.peek())) {
|
|
153
|
+
sessionsData.value = list;
|
|
154
|
+
setCachedSessions(list);
|
|
155
|
+
}
|
|
98
156
|
return list;
|
|
99
157
|
};
|
|
100
158
|
|
|
@@ -375,10 +433,8 @@ export function renderSessionList() {
|
|
|
375
433
|
const view = sidebarView.value;
|
|
376
434
|
const sessions = Array.isArray(filteredSessions.value) ? filteredSessions.value : [];
|
|
377
435
|
const groups = Array.isArray(projectGroups.value) ? projectGroups.value : [];
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
return renderProjectGroups(groups);
|
|
436
|
+
const body = view === 'timeline' ? renderTimeline(sessions) : renderProjectGroups(groups);
|
|
437
|
+
return body + (sessionListHasMore ? '<button class="btn btn-sm btn-ghost w-full" data-load-more-sessions>加载更多会话</button>' : '');
|
|
382
438
|
}
|
|
383
439
|
|
|
384
440
|
export function syncSidebarChrome() {
|
|
@@ -473,7 +529,9 @@ export function mdToHtml(text) {
|
|
|
473
529
|
if (typeof marked === 'undefined') return `<pre class="whitespace-pre-wrap text-xs">${esc(text)}</pre>`;
|
|
474
530
|
try {
|
|
475
531
|
// Prefer shared renderer if chat has configured marked
|
|
476
|
-
return
|
|
532
|
+
return parseMarkdownWithMath(text, {
|
|
533
|
+
fallback: (t) => `<pre class="whitespace-pre-wrap text-xs">${esc(t)}</pre>`,
|
|
534
|
+
});
|
|
477
535
|
} catch {
|
|
478
536
|
return `<pre class="whitespace-pre-wrap text-xs">${esc(text)}</pre>`;
|
|
479
537
|
}
|
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx, hubMode,
|
|
7
7
|
} from '../state.js';
|
|
8
8
|
import { api } from '../api.js';
|
|
9
|
+
import { getCachedTranscript, setCachedTranscript, recentTranscript } from '../cache.js';
|
|
9
10
|
import { getLastSessionContext, setLastSessionContext } from './session-context.js';
|
|
10
11
|
import {
|
|
11
|
-
connectWS, clearMessages, appendMsg, appendSystemMsg, renderToolUse,
|
|
12
|
+
connectWS, clearMessages, beginHistoryRender, endHistoryRender, appendMsg, appendSystemMsg, renderToolUse,
|
|
12
13
|
appendHistoryTokenBar, appendContextBar, flushToolBatch, coerceTs,
|
|
13
14
|
stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch, setExternalSessionReadOnly,
|
|
14
15
|
} from '../chat.js';
|
|
@@ -111,6 +112,7 @@ export function goHome() {
|
|
|
111
112
|
|
|
112
113
|
const topbar = $('topbar-project');
|
|
113
114
|
if (topbar) topbar.textContent = 'Select a session';
|
|
115
|
+
import('../notes.js').then(({ closeNotes }) => closeNotes({ restore: false })).catch(() => {});
|
|
114
116
|
const welcome = $('welcome');
|
|
115
117
|
if (welcome) {
|
|
116
118
|
welcome.classList.remove('hidden');
|
|
@@ -152,6 +154,8 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
152
154
|
const resolvedCwd = cwd || fromList?.cwd || fromLast?.cwd || null;
|
|
153
155
|
const resolvedConfig = configDir || fromList?.configDir || fromLast?.configDir || null;
|
|
154
156
|
|
|
157
|
+
import('../notes.js').then(({ closeNotes }) => closeNotes({ restore: false })).catch(() => {});
|
|
158
|
+
|
|
155
159
|
// Capture before changing ctx, then restore only after the new identity is set.
|
|
156
160
|
stashComposerDraft();
|
|
157
161
|
parkQueuedMessages();
|
|
@@ -199,10 +203,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
199
203
|
// arrive immediately on socket-open and must belong to the new history.
|
|
200
204
|
connectWS();
|
|
201
205
|
switchTab(targetTab);
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
+
import('./session-list.js').then(({ markActiveSessionRow }) => {
|
|
207
|
+
markActiveSessionRow(sid, resolvedAgent);
|
|
208
|
+
}).catch(() => {});
|
|
206
209
|
|
|
207
210
|
setLastSessionContext({
|
|
208
211
|
sessionId: sid,
|
|
@@ -227,13 +230,43 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
227
230
|
});
|
|
228
231
|
|
|
229
232
|
const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
|
|
230
|
-
const cacheKey = `${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
|
|
233
|
+
const cacheKey = `${ctx.token || ''}:${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
|
|
234
|
+
let paintEpoch = 0;
|
|
235
|
+
let requestEpoch = 0;
|
|
236
|
+
let historyWindow = null;
|
|
231
237
|
|
|
232
|
-
const paintMessages = (msgs, context, meta = {}) => {
|
|
233
|
-
if (ctx.sessionId !== sid) return;
|
|
234
|
-
|
|
238
|
+
const paintMessages = (msgs, context, meta = {}, prepend = false) => {
|
|
239
|
+
if (ctx.sessionId !== sid || ctx.agent !== resolvedAgent || (ctx.machineId || null) !== resolvedMachine) return;
|
|
240
|
+
const epoch = ++paintEpoch;
|
|
241
|
+
const container = $('messages');
|
|
242
|
+
const hadRows = !!container.querySelector('.msg-row');
|
|
243
|
+
const previousTop = container.scrollTop;
|
|
244
|
+
const previousHeight = container.scrollHeight;
|
|
245
|
+
const following = !hadRows || container.scrollHeight - container.scrollTop - container.clientHeight < 80;
|
|
246
|
+
beginHistoryRender();
|
|
247
|
+
if (meta.oldestIndex > 0) {
|
|
248
|
+
const older = document.createElement('button');
|
|
249
|
+
older.className = 'btn btn-sm btn-ghost';
|
|
250
|
+
older.textContent = '加载更早的消息';
|
|
251
|
+
older.onclick = async () => {
|
|
252
|
+
older.disabled = true;
|
|
253
|
+
try {
|
|
254
|
+
const q = new URLSearchParams({ tail: '80', compact: '1', before: String(meta.oldestIndex), agent: resolvedAgent });
|
|
255
|
+
if (resolvedMachine) q.set('machine', resolvedMachine);
|
|
256
|
+
const page = await api('GET', `/api/sessions/${sid}/messages?${q}`, undefined, undefined, { machineId: resolvedMachine || undefined });
|
|
257
|
+
if (epoch !== paintEpoch || ctx.sessionId !== sid) return;
|
|
258
|
+
const merged = [...page.messages, ...msgs];
|
|
259
|
+
const next = { ...meta, oldestIndex: page.oldestIndex, truncated: page.oldestIndex > 0 };
|
|
260
|
+
historyWindow = { msgs: merged, context, meta: next };
|
|
261
|
+
paintMessages(merged, context, next, true);
|
|
262
|
+
} catch (e) { older.disabled = false; older.textContent = `加载失败,点击重试 (${e.message})`; }
|
|
263
|
+
};
|
|
264
|
+
$('messages').appendChild(older);
|
|
265
|
+
}
|
|
266
|
+
if (meta.stale) appendSystemMsg('当前显示离线缓存,正在等待机器恢复连接');
|
|
235
267
|
if (!msgs.length) {
|
|
236
268
|
appendSystemMsg(`${label} · ${sid.slice(0, 8)}… — no history`);
|
|
269
|
+
endHistoryRender();
|
|
237
270
|
return;
|
|
238
271
|
}
|
|
239
272
|
flushToolBatch();
|
|
@@ -241,7 +274,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
241
274
|
const CHUNK = 40;
|
|
242
275
|
let i = 0;
|
|
243
276
|
const paintChunk = () => {
|
|
244
|
-
if (ctx.sessionId !== sid) return;
|
|
277
|
+
if (ctx.sessionId !== sid || epoch !== paintEpoch) return;
|
|
245
278
|
const end = Math.min(i + CHUNK, msgs.length);
|
|
246
279
|
for (; i < end; i++) {
|
|
247
280
|
const m = msgs[i];
|
|
@@ -256,7 +289,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
256
289
|
? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
|
|
257
290
|
: String(m.content ?? ''));
|
|
258
291
|
if (content) {
|
|
259
|
-
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts, historyIndex: i });
|
|
292
|
+
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts, historyIndex: (meta.oldestIndex || 0) + i });
|
|
260
293
|
}
|
|
261
294
|
} else if (typ === 'tool_use' || m.name) {
|
|
262
295
|
renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
|
|
@@ -277,15 +310,19 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
277
310
|
? ` (showing last ${msgs.length} of ${meta.total || '?'})`
|
|
278
311
|
: '';
|
|
279
312
|
appendSystemMsg(`── ${label} history · ${turnCount} turn${turnCount !== 1 ? 's' : ''}${more} ──`);
|
|
313
|
+
endHistoryRender(prepend ? previousTop + container.scrollHeight - previousHeight : (following ? null : previousTop));
|
|
280
314
|
};
|
|
281
315
|
paintChunk();
|
|
282
316
|
};
|
|
283
317
|
|
|
284
318
|
const loadHistory = async ({ preferCache = true } = {}) => {
|
|
319
|
+
const request = ++requestEpoch;
|
|
285
320
|
// Instant re-open from in-memory cache (never trust empty cache — race/stale)
|
|
286
|
-
const cached = historyCache.get(cacheKey);
|
|
287
|
-
if (
|
|
288
|
-
|
|
321
|
+
const cached = historyCache.get(cacheKey) || (preferCache ? await getCachedTranscript(cacheKey) : null);
|
|
322
|
+
if (request !== requestEpoch || ctx.sessionId !== sid) return;
|
|
323
|
+
if (preferCache && cached && cached.msgs?.length) {
|
|
324
|
+
const recent = recentTranscript(cached);
|
|
325
|
+
paintMessages(recent.msgs, recent.context, recent.meta);
|
|
289
326
|
}
|
|
290
327
|
if (!cached?.msgs?.length) appendSystemMsg(`Loading ${label} history…`);
|
|
291
328
|
try {
|
|
@@ -296,10 +333,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
296
333
|
}
|
|
297
334
|
if (mid && ctx.machineId !== mid) setSelectedMachine(mid);
|
|
298
335
|
|
|
299
|
-
//
|
|
336
|
+
// Recent messages first; older pages remain available on demand.
|
|
300
337
|
const fetchMsgs = async (agent) => {
|
|
301
338
|
const q = new URLSearchParams({
|
|
302
|
-
tail: '
|
|
339
|
+
tail: '80',
|
|
303
340
|
compact: '1',
|
|
304
341
|
});
|
|
305
342
|
if (agent) q.set('agent', agent);
|
|
@@ -315,6 +352,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
315
352
|
};
|
|
316
353
|
|
|
317
354
|
let data = await fetchMsgs(resolvedAgent);
|
|
355
|
+
if (request !== requestEpoch || ctx.sessionId !== sid) return;
|
|
318
356
|
let msgs = Array.isArray(data) ? data : (data?.messages || []);
|
|
319
357
|
// Prefer stated agent; if empty, retry scanning all agents on the edge.
|
|
320
358
|
if (!msgs.length && resolvedAgent) {
|
|
@@ -323,8 +361,21 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
323
361
|
}
|
|
324
362
|
|
|
325
363
|
const context = Array.isArray(data) ? null : (data?.context || null);
|
|
326
|
-
|
|
327
|
-
|
|
364
|
+
if (request !== requestEpoch || ctx.sessionId !== sid) return;
|
|
365
|
+
const meta = Array.isArray(data) ? {} : { truncated: data?.truncated, total: data?.total, oldestIndex: data?.oldestIndex || 0, stale: data?.stale };
|
|
366
|
+
if (historyWindow && meta.oldestIndex > historyWindow.meta.oldestIndex && historyWindow.meta.oldestIndex > 0) {
|
|
367
|
+
const keep = meta.oldestIndex - historyWindow.meta.oldestIndex;
|
|
368
|
+
if (keep <= historyWindow.msgs.length) {
|
|
369
|
+
msgs = [...historyWindow.msgs.slice(0, keep), ...msgs];
|
|
370
|
+
meta.oldestIndex = historyWindow.meta.oldestIndex;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
historyWindow = { msgs, context, meta };
|
|
374
|
+
if (msgs.length && !meta.stale) {
|
|
375
|
+
const snapshot = { msgs, context, meta, at: Date.now() };
|
|
376
|
+
historyCache.set(cacheKey, recentTranscript(snapshot));
|
|
377
|
+
setCachedTranscript(cacheKey, snapshot).catch(() => {});
|
|
378
|
+
}
|
|
328
379
|
else historyCache.delete(cacheKey);
|
|
329
380
|
// A reconnect can receive new stream frames while this verification read
|
|
330
381
|
// is in flight. Keep those live frames authoritative; the fresh response
|
|
@@ -332,9 +383,8 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
332
383
|
if (isProcessing.peek() || $('messages')?.querySelector('.live-stream')) return;
|
|
333
384
|
paintMessages(msgs, context, meta);
|
|
334
385
|
} catch (e) {
|
|
335
|
-
if (ctx.sessionId !== sid) return;
|
|
386
|
+
if (ctx.sessionId !== sid || request !== requestEpoch) return;
|
|
336
387
|
historyCache.delete(cacheKey);
|
|
337
|
-
$('messages').innerHTML = '';
|
|
338
388
|
appendSystemMsg(`${label} · ${sid.slice(0, 8)}… (history unavailable: ${e.message})`);
|
|
339
389
|
}
|
|
340
390
|
};
|
|
@@ -406,7 +456,6 @@ function applyProjectPaths(edgeCwd, resolvedMachine, sid) {
|
|
|
406
456
|
|
|
407
457
|
// In-memory chat history cache (instant re-open within TTL)
|
|
408
458
|
const historyCache = new Map();
|
|
409
|
-
const HISTORY_CACHE_TTL = 3 * 60 * 1000;
|
|
410
459
|
|
|
411
460
|
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
|
412
461
|
export function switchTab(name) { currentTab.value = name; }
|
|
@@ -85,6 +85,9 @@ export const AppShell = (opts = {}) => {
|
|
|
85
85
|
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary topbar-icon-btn" title="Meta Agent — 报告 / 问答 / VLM" aria-label="打开 Meta Agent">
|
|
86
86
|
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6L12 2Z"/><path d="m19 16 .7 2.3L22 19l-2.3.7L19 22l-.7-2.3L16 19l2.3-.7L19 16Z"/></svg><span class="topbar-icon-label">Agent</span>
|
|
87
87
|
</button>
|
|
88
|
+
<button id="btn-notes" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="记事" aria-label="记事">
|
|
89
|
+
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M5 4h11l3 3v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z"/><path d="M8 9h7M8 13h7M8 17h4"/></svg><span class="topbar-icon-label">记事</span>
|
|
90
|
+
</button>
|
|
88
91
|
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="New session" aria-label="新建会话">
|
|
89
92
|
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v8M8 12h8"/></svg><span class="topbar-icon-label">New</span>
|
|
90
93
|
</button>
|
package/public/shell/util.js
CHANGED
|
File without changes
|
package/public/shell.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './chat.js';
|
|
23
23
|
import { syncHash, currentSessionShareUrl } from './router.js';
|
|
24
24
|
import { initSettings, openSettings } from './settings.js';
|
|
25
|
+
import { openNotes } from './notes.js';
|
|
25
26
|
import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
|
|
26
27
|
import {
|
|
27
28
|
initNotifications, refreshNotifications, toggleNotificationsMenu, closeNotificationsMenu,
|
|
@@ -224,14 +225,25 @@ export function initShell(opts = {}) {
|
|
|
224
225
|
void filteredSessions.value;
|
|
225
226
|
void projectGroups.value;
|
|
226
227
|
void sidebarView.value;
|
|
227
|
-
void activityLoading.value;
|
|
228
228
|
void activityHits.value;
|
|
229
229
|
void sessionMetaMap.value;
|
|
230
230
|
const el = $('session-list');
|
|
231
231
|
if (!el) return;
|
|
232
232
|
// Keep inline rename input mounted until blur/Enter
|
|
233
233
|
if (el.querySelector('.session-rename-input')) return;
|
|
234
|
-
|
|
234
|
+
const html = renderSessionList();
|
|
235
|
+
if (el._lastHtml === html) {
|
|
236
|
+
syncSidebarChrome();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const scroll = el.scrollTop;
|
|
240
|
+
el.innerHTML = html;
|
|
241
|
+
el._lastHtml = html;
|
|
242
|
+
el.scrollTop = scroll;
|
|
243
|
+
syncSidebarChrome();
|
|
244
|
+
});
|
|
245
|
+
effect(() => {
|
|
246
|
+
void activityLoading.value;
|
|
235
247
|
syncSidebarChrome();
|
|
236
248
|
});
|
|
237
249
|
|
|
@@ -443,6 +455,7 @@ export function initShell(opts = {}) {
|
|
|
443
455
|
document.getElementById('btn-pwa-install')?.classList.remove('hidden');
|
|
444
456
|
});
|
|
445
457
|
delegate.on('click', '#btn-settings', openSettings);
|
|
458
|
+
delegate.on('click', '#btn-notes', () => openNotes());
|
|
446
459
|
delegate.on('click', '#btn-collapse-chat-chrome', () => setChatChromeCollapsed(true));
|
|
447
460
|
delegate.on('click', '#btn-expand-chat-chrome', () => setChatChromeCollapsed(false));
|
|
448
461
|
delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
|
package/public/state.js
CHANGED
|
@@ -56,7 +56,7 @@ export function setCurrentModel(model, agent = currentAgent.peek(), sessionId =
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export const currentAgent = signal(initialAgent); // 'claude'|'codex'|'grok'|'gemini'|'cursor'
|
|
59
|
-
export const currentModel = signal('
|
|
59
|
+
export const currentModel = signal('sonnet');
|
|
60
60
|
export const currentEffort = signal('');
|
|
61
61
|
// All local agents are trusted by default; users can still choose another mode
|
|
62
62
|
// from the expanded composer options before starting a turn.
|
|
@@ -164,10 +164,10 @@ export const AGENT_LABELS = {
|
|
|
164
164
|
/** Static fallbacks when /api/agents has not loaded yet. */
|
|
165
165
|
export const AGENT_MODELS = {
|
|
166
166
|
claude: [
|
|
167
|
-
{ value: '
|
|
168
|
-
{ value: '
|
|
169
|
-
{ value: '
|
|
170
|
-
{ value: '
|
|
167
|
+
{ value: 'fable', label: 'fable' },
|
|
168
|
+
{ value: 'opus', label: 'opus' },
|
|
169
|
+
{ value: 'sonnet', label: 'sonnet' },
|
|
170
|
+
{ value: 'haiku', label: 'haiku' },
|
|
171
171
|
],
|
|
172
172
|
codex: [
|
|
173
173
|
{ value: 'gpt-5.4', label: 'gpt-5.4' },
|
|
@@ -204,7 +204,7 @@ export const AGENT_MODELS = {
|
|
|
204
204
|
};
|
|
205
205
|
|
|
206
206
|
export const AGENT_DEFAULT_MODEL = {
|
|
207
|
-
claude: '
|
|
207
|
+
claude: 'sonnet',
|
|
208
208
|
codex: 'gpt-5.4',
|
|
209
209
|
grok: 'grok-4.5',
|
|
210
210
|
gemini: 'gemini-3.7-flash-high',
|
|
@@ -304,7 +304,7 @@ export function getDefaultModel(agent) {
|
|
|
304
304
|
const a = (agent || 'claude').toLowerCase();
|
|
305
305
|
const meta = agentsMeta.peek()?.defaults?.[a];
|
|
306
306
|
if (meta?.model) return String(meta.model);
|
|
307
|
-
return AGENT_DEFAULT_MODEL[a] || getModelsForAgent(a)[0]?.value || '
|
|
307
|
+
return AGENT_DEFAULT_MODEL[a] || getModelsForAgent(a)[0]?.value || 'sonnet';
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/** Effort levels for current (or given) model under agent. */
|