claude-simple 0.2.28 → 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 +8 -2
- 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 +47 -20
- package/public/api.js +1 -1
- package/public/app.js +5 -5
- package/public/base.css +0 -0
- package/public/cache.js +19 -0
- package/public/chat.js +76 -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/notifications.js +0 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +1 -1
- package/public/router.js +0 -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 +49 -16
- package/public/shell/session-nav.js +64 -17
- package/public/shell/templates.js +0 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +0 -0
- package/public/state.js +7 -7
- package/public/style.css +5 -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 +6 -1
- package/src/agents/meta-agent.js +7 -4
- package/src/agents/models.js +21 -12
- package/src/agents/sessions.js +133 -69
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +80 -36
- 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/dist/app.0.2.28.min.js +0 -1220
- package/dist/style.0.2.28.min.css +0 -2007
package/public/math.js
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
|
-
// KaTeX enhancement for Markdown output.
|
|
2
|
-
//
|
|
1
|
+
// KaTeX enhancement for Markdown output. Math is extracted before Marked so
|
|
2
|
+
// `\[...\]` is not treated as an escaped bracket and `_` inside TeX is not
|
|
3
|
+
// turned into emphasis. A leftover pass still runs on the DOM for any tokens
|
|
4
|
+
// that survive as text.
|
|
5
|
+
|
|
6
|
+
const LATEXISH = /\\[a-zA-Z]+|[_^]\{|\\[,;!]|\\[{}()[\]]|\\leq\b|\\geq\b|\\neq\b|\\in\b|\\sum\b|\\int\b|\\frac\b|\\min\b|\\max\b|\\mathrm\b|\\text\b|\\left\b|\\right\b/;
|
|
7
|
+
|
|
8
|
+
export function looksLikeLatex(s) {
|
|
9
|
+
return LATEXISH.test(String(s || ''));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function htmlEscape(s) {
|
|
13
|
+
return String(s)
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function atLineStart(text, i) {
|
|
20
|
+
return i === 0 || text[i - 1] === '\n';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchFence(text, i) {
|
|
24
|
+
if (atLineStart(text, i) && (text.startsWith('```', i) || text.startsWith('~~~', i))) {
|
|
25
|
+
const fence = text.startsWith('```', i) ? '```' : '~~~';
|
|
26
|
+
const close = text.indexOf('\n' + fence, i + 3);
|
|
27
|
+
if (close === -1) return { end: text.length };
|
|
28
|
+
const after = close + 1 + fence.length;
|
|
29
|
+
return { end: after };
|
|
30
|
+
}
|
|
31
|
+
if (text[i] === '`') {
|
|
32
|
+
let n = 1;
|
|
33
|
+
while (text[i + n] === '`') n++;
|
|
34
|
+
let j = i + n;
|
|
35
|
+
while (j < text.length) {
|
|
36
|
+
if (text[j] === '`') {
|
|
37
|
+
let k = 0;
|
|
38
|
+
while (text[j + k] === '`') k++;
|
|
39
|
+
if (k === n) return { end: j + k };
|
|
40
|
+
j += k;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
return { end: i + n };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchMath(text, i) {
|
|
51
|
+
const rest = text.slice(i);
|
|
52
|
+
|
|
53
|
+
if (rest.startsWith('$$')) {
|
|
54
|
+
const end = rest.indexOf('$$', 2);
|
|
55
|
+
if (end >= 2) {
|
|
56
|
+
const body = rest.slice(2, end);
|
|
57
|
+
if (body.trim()) return { body, display: true, end: i + end + 2 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rest.startsWith('\\[')) {
|
|
62
|
+
const end = rest.indexOf('\\]', 2);
|
|
63
|
+
if (end >= 2) {
|
|
64
|
+
const body = rest.slice(2, end);
|
|
65
|
+
if (body.trim()) return { body, display: true, end: i + end + 2 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (rest.startsWith('\\(')) {
|
|
70
|
+
const end = rest.indexOf('\\)', 2);
|
|
71
|
+
if (end >= 2) {
|
|
72
|
+
const body = rest.slice(2, end);
|
|
73
|
+
if (body.trim()) return { body, display: false, end: i + end + 2 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (rest[0] === '$' && rest[1] !== '$') {
|
|
78
|
+
const m = rest.match(/^\$([^$\n]|\\\$)+?(?<!\s)\$/);
|
|
79
|
+
if (m && m[0][1] !== ' ') {
|
|
80
|
+
const body = m[0].slice(1, -1);
|
|
81
|
+
if (body.trim()) return { body, display: false, end: i + m[0].length };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (atLineStart(text, i)) {
|
|
86
|
+
const block = rest.match(/^[ \t]*\[[ \t]*\n([\s\S]*?)\n[ \t]*\][ \t]*(?=\n|$)/);
|
|
87
|
+
if (block && looksLikeLatex(block[1])) {
|
|
88
|
+
return { body: block[1], display: true, end: i + block[0].length };
|
|
89
|
+
}
|
|
90
|
+
const line = rest.match(/^[ \t]*\[([^\]\n]+)\](?!\()/);
|
|
91
|
+
if (line && looksLikeLatex(line[1])) {
|
|
92
|
+
return { body: line[1], display: true, end: i + line[0].length };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function protectMath(src) {
|
|
100
|
+
const slots = [];
|
|
101
|
+
const text = String(src ?? '');
|
|
102
|
+
let out = '';
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < text.length) {
|
|
105
|
+
const fence = matchFence(text, i);
|
|
106
|
+
if (fence) {
|
|
107
|
+
out += text.slice(i, fence.end);
|
|
108
|
+
i = fence.end;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const math = matchMath(text, i);
|
|
112
|
+
if (math) {
|
|
113
|
+
const id = slots.length;
|
|
114
|
+
slots.push({ body: math.body, display: math.display });
|
|
115
|
+
out += `@@MATH${id}@@`;
|
|
116
|
+
i = math.end;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
out += text[i];
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
return { text: out, slots };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function restoreMath(html, slots) {
|
|
126
|
+
if (!slots?.length) return html;
|
|
127
|
+
return String(html).replace(/@@MATH(\d+)@@/g, (full, id) => {
|
|
128
|
+
const slot = slots[Number(id)];
|
|
129
|
+
if (!slot) return full;
|
|
130
|
+
const open = slot.display ? '\\[' : '\\(';
|
|
131
|
+
const close = slot.display ? '\\]' : '\\)';
|
|
132
|
+
return htmlEscape(`${open}${slot.body}${close}`);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
3
135
|
|
|
4
136
|
function ignored(node) {
|
|
5
137
|
return node.parentElement?.closest('pre, code, .mermaid-diagram, .katex, script, style');
|
|
@@ -10,9 +142,9 @@ function nextToken(text, from) {
|
|
|
10
142
|
{ re: /\\\[([\s\S]+?)\\\]/g, display: true },
|
|
11
143
|
{ re: /\$\$([\s\S]+?)\$\$/g, display: true },
|
|
12
144
|
{ re: /\\\(([\s\S]+?)\\\)/g, display: false },
|
|
13
|
-
// A closing dollar must not be escaped or preceded by whitespace. This
|
|
14
|
-
// leaves ordinary currency such as "$5 and $6" untouched.
|
|
15
145
|
{ re: /(?<!\\)\$([^$\n]|\\\$)+?(?<!\s)(?<!\\)\$/g, display: false },
|
|
146
|
+
{ re: /(?:^|\n)[ \t]*\[[ \t]*\n([\s\S]*?)\n[ \t]*\][ \t]*(?=\n|$)/g, display: true, latex: true },
|
|
147
|
+
{ re: /(?:^|\n)[ \t]*\[([^\]\n]+\\[a-zA-Z{}_^][^\]\n]*)\](?!\()/g, display: true, latex: true },
|
|
16
148
|
];
|
|
17
149
|
let best = null;
|
|
18
150
|
for (const pattern of patterns) {
|
|
@@ -20,10 +152,16 @@ function nextToken(text, from) {
|
|
|
20
152
|
const match = pattern.re.exec(text);
|
|
21
153
|
if (!match || (best && match.index >= best.match.index)) continue;
|
|
22
154
|
const raw = match[0];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
155
|
+
let body;
|
|
156
|
+
if (match[1] != null && (pattern.latex || raw.startsWith('\\[') || raw.startsWith('\\(') || raw.startsWith('$$') || raw.startsWith('$'))) {
|
|
157
|
+
body = match[1];
|
|
158
|
+
} else {
|
|
159
|
+
body = raw.startsWith('\\[') || raw.startsWith('\\(')
|
|
160
|
+
? raw.slice(2, -2)
|
|
161
|
+
: raw.startsWith('$$') ? raw.slice(2, -2) : raw.slice(1, -1);
|
|
162
|
+
}
|
|
26
163
|
if (!body.trim()) continue;
|
|
164
|
+
if (pattern.latex && !looksLikeLatex(body)) continue;
|
|
27
165
|
best = { match, body, display: pattern.display };
|
|
28
166
|
}
|
|
29
167
|
return best;
|
package/public/mobile.js
CHANGED
|
File without changes
|
package/public/notifications.js
CHANGED
|
File without changes
|
package/public/offline.html
CHANGED
|
File without changes
|
package/public/pwa.js
CHANGED
|
@@ -69,7 +69,7 @@ export function initPwa() {
|
|
|
69
69
|
|
|
70
70
|
// Register after first paint so install doesn't compete with boot.
|
|
71
71
|
const register = () => {
|
|
72
|
-
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
|
72
|
+
navigator.serviceWorker.register('/sw.js?v=33', { scope: '/' })
|
|
73
73
|
.then((reg) => {
|
|
74
74
|
reg.addEventListener('updatefound', () => {
|
|
75
75
|
const sw = reg.installing;
|
package/public/router.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Pure helpers for chat scroll-follow. Keep reading history from being yanked. */
|
|
2
|
+
|
|
3
|
+
export const NEAR_BOTTOM_PX = 80;
|
|
4
|
+
|
|
5
|
+
export function isNearBottom(el, threshold = NEAR_BOTTOM_PX) {
|
|
6
|
+
if (!el) return true;
|
|
7
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Wheel toward older messages (negative deltaY) unpins immediately. */
|
|
11
|
+
export function followAfterWheel(deltaY, following) {
|
|
12
|
+
if (deltaY < 0) return false;
|
|
13
|
+
return following;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function followAfterScroll(nearBottom) {
|
|
17
|
+
return nearBottom;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldStick({ force = false, following = true, userScrolling = false } = {}) {
|
|
21
|
+
if (force) return true;
|
|
22
|
+
if (userScrolling) return false;
|
|
23
|
+
return following;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bind a messages container. Returns { stick, setFollowing, isFollowing, destroy }.
|
|
28
|
+
*/
|
|
29
|
+
export function bindScrollFollow(el) {
|
|
30
|
+
let following = true;
|
|
31
|
+
let userScrolling = false;
|
|
32
|
+
let touchStartY = 0;
|
|
33
|
+
|
|
34
|
+
const onScroll = () => {
|
|
35
|
+
following = isNearBottom(el);
|
|
36
|
+
userScrolling = false;
|
|
37
|
+
};
|
|
38
|
+
const onWheel = (e) => {
|
|
39
|
+
following = followAfterWheel(e.deltaY, following);
|
|
40
|
+
if (e.deltaY < 0) userScrolling = true;
|
|
41
|
+
};
|
|
42
|
+
const onTouchStart = (e) => {
|
|
43
|
+
touchStartY = e.touches?.[0]?.clientY || 0;
|
|
44
|
+
};
|
|
45
|
+
const onTouchMove = (e) => {
|
|
46
|
+
const y = e.touches?.[0]?.clientY || 0;
|
|
47
|
+
if (y - touchStartY > 8) {
|
|
48
|
+
following = false;
|
|
49
|
+
userScrolling = true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
54
|
+
el.addEventListener('wheel', onWheel, { passive: true });
|
|
55
|
+
el.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
56
|
+
el.addEventListener('touchmove', onTouchMove, { passive: true });
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
stick(force = false) {
|
|
60
|
+
if (shouldStick({ force, following, userScrolling })) {
|
|
61
|
+
el.scrollTop = el.scrollHeight;
|
|
62
|
+
following = true;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
setFollowing(v) {
|
|
66
|
+
following = !!v;
|
|
67
|
+
},
|
|
68
|
+
isFollowing() {
|
|
69
|
+
return following;
|
|
70
|
+
},
|
|
71
|
+
destroy() {
|
|
72
|
+
el.removeEventListener('scroll', onScroll);
|
|
73
|
+
el.removeEventListener('wheel', onWheel);
|
|
74
|
+
el.removeEventListener('touchstart', onTouchStart);
|
|
75
|
+
el.removeEventListener('touchmove', onTouchMove);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
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,10 +10,10 @@ 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
19
|
function sessionsFingerprint(list) {
|
|
@@ -84,11 +84,30 @@ function orderBranchSessions(sessions) {
|
|
|
84
84
|
return out;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
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 } = {}) {
|
|
88
108
|
const q = new URLSearchParams();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
q.set('limit', '2000');
|
|
109
|
+
q.set('limit', String(SESSION_PAGE_SIZE));
|
|
110
|
+
if (offset) q.set('offset', String(offset));
|
|
92
111
|
// A Hub keeps this catalog locally from Edge deltas. Always use it in Hub
|
|
93
112
|
// mode: an Edge scan can be slow while an agent is busy, and must never make
|
|
94
113
|
// the sidebar (or a page refresh) fail with a 502.
|
|
@@ -99,7 +118,13 @@ function sessionsQuery({ hubIndex = false } = {}) {
|
|
|
99
118
|
|
|
100
119
|
/** @param {{ waitFresh?: boolean, hubIndex?: boolean }} opts waitFresh=false → paint cache first, refresh in bg */
|
|
101
120
|
export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {}) {
|
|
102
|
-
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);
|
|
103
128
|
if (cached.length) {
|
|
104
129
|
// Hub: if we already selected a machine, prefer cache entries for that machine
|
|
105
130
|
const mid = selectedMachineId.peek();
|
|
@@ -112,10 +137,18 @@ export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {
|
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
const fetchFresh = async () => {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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());
|
|
119
152
|
if (sessionsFingerprint(list) !== sessionsFingerprint(sessionsData.peek())) {
|
|
120
153
|
sessionsData.value = list;
|
|
121
154
|
setCachedSessions(list);
|
|
@@ -400,10 +433,8 @@ export function renderSessionList() {
|
|
|
400
433
|
const view = sidebarView.value;
|
|
401
434
|
const sessions = Array.isArray(filteredSessions.value) ? filteredSessions.value : [];
|
|
402
435
|
const groups = Array.isArray(projectGroups.value) ? projectGroups.value : [];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
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>' : '');
|
|
407
438
|
}
|
|
408
439
|
|
|
409
440
|
export function syncSidebarChrome() {
|
|
@@ -498,7 +529,9 @@ export function mdToHtml(text) {
|
|
|
498
529
|
if (typeof marked === 'undefined') return `<pre class="whitespace-pre-wrap text-xs">${esc(text)}</pre>`;
|
|
499
530
|
try {
|
|
500
531
|
// Prefer shared renderer if chat has configured marked
|
|
501
|
-
return
|
|
532
|
+
return parseMarkdownWithMath(text, {
|
|
533
|
+
fallback: (t) => `<pre class="whitespace-pre-wrap text-xs">${esc(t)}</pre>`,
|
|
534
|
+
});
|
|
502
535
|
} catch {
|
|
503
536
|
return `<pre class="whitespace-pre-wrap text-xs">${esc(text)}</pre>`;
|
|
504
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';
|
|
@@ -229,13 +230,43 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
229
230
|
});
|
|
230
231
|
|
|
231
232
|
const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
|
|
232
|
-
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;
|
|
233
237
|
|
|
234
|
-
const paintMessages = (msgs, context, meta = {}) => {
|
|
235
|
-
if (ctx.sessionId !== sid) return;
|
|
236
|
-
|
|
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('当前显示离线缓存,正在等待机器恢复连接');
|
|
237
267
|
if (!msgs.length) {
|
|
238
268
|
appendSystemMsg(`${label} · ${sid.slice(0, 8)}… — no history`);
|
|
269
|
+
endHistoryRender();
|
|
239
270
|
return;
|
|
240
271
|
}
|
|
241
272
|
flushToolBatch();
|
|
@@ -243,7 +274,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
243
274
|
const CHUNK = 40;
|
|
244
275
|
let i = 0;
|
|
245
276
|
const paintChunk = () => {
|
|
246
|
-
if (ctx.sessionId !== sid) return;
|
|
277
|
+
if (ctx.sessionId !== sid || epoch !== paintEpoch) return;
|
|
247
278
|
const end = Math.min(i + CHUNK, msgs.length);
|
|
248
279
|
for (; i < end; i++) {
|
|
249
280
|
const m = msgs[i];
|
|
@@ -258,7 +289,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
258
289
|
? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
|
|
259
290
|
: String(m.content ?? ''));
|
|
260
291
|
if (content) {
|
|
261
|
-
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 });
|
|
262
293
|
}
|
|
263
294
|
} else if (typ === 'tool_use' || m.name) {
|
|
264
295
|
renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
|
|
@@ -279,15 +310,19 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
279
310
|
? ` (showing last ${msgs.length} of ${meta.total || '?'})`
|
|
280
311
|
: '';
|
|
281
312
|
appendSystemMsg(`── ${label} history · ${turnCount} turn${turnCount !== 1 ? 's' : ''}${more} ──`);
|
|
313
|
+
endHistoryRender(prepend ? previousTop + container.scrollHeight - previousHeight : (following ? null : previousTop));
|
|
282
314
|
};
|
|
283
315
|
paintChunk();
|
|
284
316
|
};
|
|
285
317
|
|
|
286
318
|
const loadHistory = async ({ preferCache = true } = {}) => {
|
|
319
|
+
const request = ++requestEpoch;
|
|
287
320
|
// Instant re-open from in-memory cache (never trust empty cache — race/stale)
|
|
288
|
-
const cached = historyCache.get(cacheKey);
|
|
289
|
-
if (
|
|
290
|
-
|
|
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);
|
|
291
326
|
}
|
|
292
327
|
if (!cached?.msgs?.length) appendSystemMsg(`Loading ${label} history…`);
|
|
293
328
|
try {
|
|
@@ -298,10 +333,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
298
333
|
}
|
|
299
334
|
if (mid && ctx.machineId !== mid) setSelectedMachine(mid);
|
|
300
335
|
|
|
301
|
-
//
|
|
336
|
+
// Recent messages first; older pages remain available on demand.
|
|
302
337
|
const fetchMsgs = async (agent) => {
|
|
303
338
|
const q = new URLSearchParams({
|
|
304
|
-
tail: '
|
|
339
|
+
tail: '80',
|
|
305
340
|
compact: '1',
|
|
306
341
|
});
|
|
307
342
|
if (agent) q.set('agent', agent);
|
|
@@ -317,6 +352,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
317
352
|
};
|
|
318
353
|
|
|
319
354
|
let data = await fetchMsgs(resolvedAgent);
|
|
355
|
+
if (request !== requestEpoch || ctx.sessionId !== sid) return;
|
|
320
356
|
let msgs = Array.isArray(data) ? data : (data?.messages || []);
|
|
321
357
|
// Prefer stated agent; if empty, retry scanning all agents on the edge.
|
|
322
358
|
if (!msgs.length && resolvedAgent) {
|
|
@@ -325,8 +361,21 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
325
361
|
}
|
|
326
362
|
|
|
327
363
|
const context = Array.isArray(data) ? null : (data?.context || null);
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
}
|
|
330
379
|
else historyCache.delete(cacheKey);
|
|
331
380
|
// A reconnect can receive new stream frames while this verification read
|
|
332
381
|
// is in flight. Keep those live frames authoritative; the fresh response
|
|
@@ -334,9 +383,8 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
334
383
|
if (isProcessing.peek() || $('messages')?.querySelector('.live-stream')) return;
|
|
335
384
|
paintMessages(msgs, context, meta);
|
|
336
385
|
} catch (e) {
|
|
337
|
-
if (ctx.sessionId !== sid) return;
|
|
386
|
+
if (ctx.sessionId !== sid || request !== requestEpoch) return;
|
|
338
387
|
historyCache.delete(cacheKey);
|
|
339
|
-
$('messages').innerHTML = '';
|
|
340
388
|
appendSystemMsg(`${label} · ${sid.slice(0, 8)}… (history unavailable: ${e.message})`);
|
|
341
389
|
}
|
|
342
390
|
};
|
|
@@ -408,7 +456,6 @@ function applyProjectPaths(edgeCwd, resolvedMachine, sid) {
|
|
|
408
456
|
|
|
409
457
|
// In-memory chat history cache (instant re-open within TTL)
|
|
410
458
|
const historyCache = new Map();
|
|
411
|
-
const HISTORY_CACHE_TTL = 3 * 60 * 1000;
|
|
412
459
|
|
|
413
460
|
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
|
414
461
|
export function switchTab(name) { currentTab.value = name; }
|
|
File without changes
|
package/public/shell/util.js
CHANGED
|
File without changes
|
package/public/shell.js
CHANGED
|
File without changes
|