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/dist/xterm.min.css
CHANGED
|
File without changes
|
package/dist/xterm.min.js
CHANGED
|
File without changes
|
package/gateway.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-simple": "server.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"start": "NODE_ENV=production node server.js",
|
|
26
26
|
"dev": "NODE_ENV=development node --watch server.js",
|
|
27
27
|
"serve": "node server.js",
|
|
28
|
-
"test": "node --test",
|
|
28
|
+
"test": "node --test test/*.test.js",
|
|
29
29
|
"gateway": "node gateway.js",
|
|
30
30
|
"gateway:go": "cd gateway && go run .",
|
|
31
31
|
"gateway:build": "cd gateway && make build",
|
package/public/addon-fit.min.js
CHANGED
|
File without changes
|
package/public/agent-panel.js
CHANGED
|
@@ -6,7 +6,8 @@ import { esc, $ } from './lib.js';
|
|
|
6
6
|
import { api, authHeaders, currentMachineId } from './api.js';
|
|
7
7
|
import { hubMode, ctx, currentAgent, currentProject } from './state.js';
|
|
8
8
|
import { renderMermaid } from './mermaid.js';
|
|
9
|
-
import {
|
|
9
|
+
import { parseMarkdownWithMath } from './markdown.js';
|
|
10
|
+
import { renderMath } from './math.js';
|
|
10
11
|
|
|
11
12
|
let panelOpen = false;
|
|
12
13
|
let mode = 'chat'; // chat | report | history | config
|
|
@@ -28,12 +29,9 @@ let currentNoteSnapshot = null;
|
|
|
28
29
|
const LAST_SESSION_KEY = 'ma_last_session_id';
|
|
29
30
|
|
|
30
31
|
function mdHtml(text) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch { /* fallthrough */ }
|
|
35
|
-
}
|
|
36
|
-
return esc(text).replace(/\n/g, '<br>');
|
|
32
|
+
return parseMarkdownWithMath(text, {
|
|
33
|
+
fallback: (t) => esc(t).replace(/\n/g, '<br>'),
|
|
34
|
+
});
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
function highlight(root) {
|
|
@@ -43,12 +41,14 @@ function highlight(root) {
|
|
|
43
41
|
try { hljs.highlightElement(el); } catch {}
|
|
44
42
|
});
|
|
45
43
|
}
|
|
44
|
+
renderMath(root);
|
|
46
45
|
renderMermaid(root);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
/** Fetch SSE with auth (+ hub machine routing). */
|
|
50
49
|
async function streamSSE(path, body, handlers) {
|
|
51
|
-
|
|
50
|
+
// Meta Agent is hub-owned. Do not attach the currently selected edge.
|
|
51
|
+
const headers = authHeaders({ 'Content-Type': 'application/json' }, { hub: true });
|
|
52
52
|
const res = await fetch(path, {
|
|
53
53
|
method: 'POST',
|
|
54
54
|
headers,
|
|
@@ -77,6 +77,10 @@ async function streamSSE(path, body, handlers) {
|
|
|
77
77
|
let data;
|
|
78
78
|
try { data = JSON.parse(raw); } catch { continue; }
|
|
79
79
|
handlers[event]?.(data);
|
|
80
|
+
if (!handlers[event] && handlers.tool) {
|
|
81
|
+
if (event === 'tool_call') handlers.tool({name:data.name, args:data.args, id:data.callId, phase:'start'});
|
|
82
|
+
if (event === 'tool_result') handlers.tool({id:data.callId, phase:data.ok === false ? 'error' : 'done', result:data.ok === false ? data.error : data.result});
|
|
83
|
+
}
|
|
80
84
|
handlers.any?.(event, data);
|
|
81
85
|
event = 'message';
|
|
82
86
|
} else if (line.trim() === '') {
|
|
@@ -375,16 +379,19 @@ function appendMsg(kind, text, opts = {}) {
|
|
|
375
379
|
} else if (kind === 'assistant') {
|
|
376
380
|
el.innerHTML = `<div class="ma-bubble assistant md">${mdHtml(text)}</div>`;
|
|
377
381
|
highlight(el);
|
|
382
|
+
} else if (kind === 'reasoning') {
|
|
383
|
+
el.innerHTML = `<details class="ma-reasoning" open><summary>思考过程</summary><pre style="white-space:pre-wrap"></pre></details>`;
|
|
384
|
+
el.querySelector('pre').textContent = text;
|
|
378
385
|
} else if (kind === 'error') {
|
|
379
386
|
el.innerHTML = `<div class="ma-bubble error">${esc(text)}</div>`;
|
|
380
387
|
} else if (kind === 'system') {
|
|
381
388
|
el.innerHTML = `<div class="ma-sys">${esc(text)}</div>`;
|
|
382
389
|
} else if (kind === 'tool') {
|
|
383
|
-
el.innerHTML = `<
|
|
384
|
-
<span class="ma-tool-name">${esc(opts.name || 'tool')}</span>
|
|
385
|
-
<span class="ma-tool-phase">${esc(opts.phase || '')}</span>
|
|
386
|
-
<pre class="ma-tool-args">${esc(JSON.stringify(opts.args || {}, null,
|
|
387
|
-
</
|
|
390
|
+
el.innerHTML = `<details class="ma-tool" data-tool-id="${esc(opts.id || '')}">
|
|
391
|
+
<summary><span class="ma-tool-name">${esc(opts.name || 'tool')}</span>
|
|
392
|
+
<span class="ma-tool-phase">${esc(opts.phase || '')}</span></summary>
|
|
393
|
+
<pre class="ma-tool-args" style="white-space:pre-wrap">${esc(JSON.stringify(opts.args || {}, null, 2))}</pre>
|
|
394
|
+
</details>`;
|
|
388
395
|
}
|
|
389
396
|
box.appendChild(el);
|
|
390
397
|
box.scrollTop = box.scrollHeight;
|
|
@@ -408,14 +415,15 @@ function updateToolChip(id, phase, result) {
|
|
|
408
415
|
el.classList.toggle('error', phase === 'error');
|
|
409
416
|
const ph = el.querySelector('.ma-tool-phase');
|
|
410
417
|
if (ph) ph.textContent = phase;
|
|
411
|
-
if (result && phase !== 'start') {
|
|
418
|
+
if (result !== undefined && phase !== 'start') {
|
|
412
419
|
let pre = el.querySelector('.ma-tool-result');
|
|
413
420
|
if (!pre) {
|
|
414
421
|
pre = document.createElement('pre');
|
|
415
422
|
pre.className = 'ma-tool-result';
|
|
423
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
416
424
|
el.appendChild(pre);
|
|
417
425
|
}
|
|
418
|
-
pre.textContent = JSON.stringify(result, null,
|
|
426
|
+
pre.textContent = JSON.stringify(result, null, 2);
|
|
419
427
|
}
|
|
420
428
|
}
|
|
421
429
|
|
|
@@ -466,7 +474,11 @@ function insertAtCursor(ta, text) {
|
|
|
466
474
|
|
|
467
475
|
/** Upload blob → server path; insert [imgN](path) into input. */
|
|
468
476
|
async function uploadAndInsertImage(blob, filename) {
|
|
469
|
-
|
|
477
|
+
// Attachments used by Meta Agent belong to the Hub conversation too.
|
|
478
|
+
const headers = authHeaders(
|
|
479
|
+
{ 'x-filename': filename || `paste-${Date.now()}.png` },
|
|
480
|
+
{ hub: true },
|
|
481
|
+
);
|
|
470
482
|
// raw body upload — do not set Content-Type (browser sets multipart/stream)
|
|
471
483
|
delete headers['Content-Type'];
|
|
472
484
|
const res = await fetch('/api/upload-image', {
|
|
@@ -801,6 +813,10 @@ function renderHistoryMessages(messages) {
|
|
|
801
813
|
continue;
|
|
802
814
|
}
|
|
803
815
|
if (m.role === 'assistant') {
|
|
816
|
+
if (m.reasoning) appendMsg('reasoning', m.reasoning).querySelector('details').open = false;
|
|
817
|
+
if (m.content) {
|
|
818
|
+
appendMsg('assistant', typeof m.content === 'string' ? m.content : userContentToText(m.content));
|
|
819
|
+
}
|
|
804
820
|
if (m.tool_calls?.length) {
|
|
805
821
|
for (const tc of m.tool_calls) {
|
|
806
822
|
const name = tc.function?.name || tc.name || 'tool';
|
|
@@ -816,9 +832,6 @@ function renderHistoryMessages(messages) {
|
|
|
816
832
|
});
|
|
817
833
|
}
|
|
818
834
|
}
|
|
819
|
-
if (m.content) {
|
|
820
|
-
appendMsg('assistant', typeof m.content === 'string' ? m.content : userContentToText(m.content));
|
|
821
|
-
}
|
|
822
835
|
continue;
|
|
823
836
|
}
|
|
824
837
|
if (m.role === 'tool') {
|
|
@@ -828,7 +841,7 @@ function renderHistoryMessages(messages) {
|
|
|
828
841
|
} catch { /* keep string */ }
|
|
829
842
|
// attach to last matching tool chip if possible
|
|
830
843
|
const id = m.tool_call_id || '';
|
|
831
|
-
if (id) updateToolChip(id, 'done', result);
|
|
844
|
+
if (id) updateToolChip(id, typeof result === 'string' && result.startsWith('error:') ? 'error' : 'done', result);
|
|
832
845
|
else {
|
|
833
846
|
appendMsg('tool', '', {
|
|
834
847
|
name: 'tool_result',
|
|
@@ -845,11 +858,12 @@ function renderHistoryMessages(messages) {
|
|
|
845
858
|
if (!pre) {
|
|
846
859
|
pre = document.createElement('pre');
|
|
847
860
|
pre.className = 'ma-tool-result';
|
|
861
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
848
862
|
last.appendChild(pre);
|
|
849
863
|
}
|
|
850
864
|
pre.textContent = typeof result === 'string'
|
|
851
865
|
? result.slice(0, 500)
|
|
852
|
-
: JSON.stringify(result, null,
|
|
866
|
+
: JSON.stringify(result, null, 2);
|
|
853
867
|
}
|
|
854
868
|
}
|
|
855
869
|
}
|
|
@@ -937,6 +951,7 @@ async function sendChat() {
|
|
|
937
951
|
|
|
938
952
|
let asstEl = null;
|
|
939
953
|
let content = '';
|
|
954
|
+
let reasoningEl = null;
|
|
940
955
|
|
|
941
956
|
try {
|
|
942
957
|
await streamSSE('/api/ai/chat', {
|
|
@@ -954,6 +969,19 @@ async function sendChat() {
|
|
|
954
969
|
localStorage.setItem(LAST_SESSION_KEY, d.id);
|
|
955
970
|
updateSessionChrome();
|
|
956
971
|
},
|
|
972
|
+
round_start: () => {
|
|
973
|
+
asstEl = null;
|
|
974
|
+
if (reasoningEl) reasoningEl.querySelector('details').open = false;
|
|
975
|
+
reasoningEl = null;
|
|
976
|
+
},
|
|
977
|
+
reasoning: (d) => {
|
|
978
|
+
if (!reasoningEl) reasoningEl = appendMsg('reasoning', '');
|
|
979
|
+
reasoningEl.querySelector('pre').textContent = d.content || '';
|
|
980
|
+
},
|
|
981
|
+
tool_call: (d) => {
|
|
982
|
+
appendMsg('tool', '', { name: d.name, args: d.args, phase: '执行中', id: d.callId });
|
|
983
|
+
},
|
|
984
|
+
tool_result: (d) => updateToolChip(d.callId, d.ok === false ? 'error' : 'done', d.ok === false ? d.error : d.result),
|
|
957
985
|
content: (d) => {
|
|
958
986
|
content = d.content || '';
|
|
959
987
|
if (!asstEl) asstEl = appendMsg('assistant', '');
|
|
@@ -980,7 +1008,11 @@ async function sendChat() {
|
|
|
980
1008
|
messageCount: (currentSessionMeta?.messageCount || 0) + 2,
|
|
981
1009
|
};
|
|
982
1010
|
}
|
|
983
|
-
if (d.content
|
|
1011
|
+
if (d.content) {
|
|
1012
|
+
if (!asstEl) asstEl = appendMsg('assistant', '');
|
|
1013
|
+
updateAssistant(asstEl, d.content);
|
|
1014
|
+
}
|
|
1015
|
+
if (reasoningEl) reasoningEl.querySelector('details').open = false;
|
|
984
1016
|
updateSessionChrome();
|
|
985
1017
|
refreshHistoryCache();
|
|
986
1018
|
},
|
package/public/api.js
CHANGED
|
@@ -24,8 +24,11 @@ function isHubOnlyPath(path) {
|
|
|
24
24
|
|| p === '/api/machines'
|
|
25
25
|
|| p === '/api/notifications'
|
|
26
26
|
|| p === '/api/notifications/read'
|
|
27
|
-
|| p === '/api/
|
|
28
|
-
|| p.startsWith('/api/
|
|
27
|
+
|| p === '/api/notes'
|
|
28
|
+
|| p.startsWith('/api/notes/')
|
|
29
|
+
|| p === '/api/queue'
|
|
30
|
+
|| p.startsWith('/api/queue/')
|
|
31
|
+
|| p.startsWith('/api/ai/')
|
|
29
32
|
|| p.startsWith('/api/auth/')
|
|
30
33
|
|| p.startsWith('/api/collab/');
|
|
31
34
|
}
|
|
@@ -60,7 +63,7 @@ export async function api(method, path, body, signal, opts = {}) {
|
|
|
60
63
|
method,
|
|
61
64
|
signal,
|
|
62
65
|
headers,
|
|
63
|
-
|
|
66
|
+
cache: opts.cache || (method.toUpperCase() === 'GET' ? 'no-store' : 'default'),
|
|
64
67
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
65
68
|
});
|
|
66
69
|
if (res.status === 304) return NOT_MODIFIED;
|
|
@@ -114,11 +117,11 @@ export function authQuery(extra = {}) {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/** Headers for raw fetch (e.g. file upload) in hub mode. */
|
|
117
|
-
export function authHeaders(extra = {}) {
|
|
120
|
+
export function authHeaders(extra = {}, opts = {}) {
|
|
118
121
|
const h = { ...extra };
|
|
119
122
|
if (ctx.token) h.Authorization = `Bearer ${ctx.token}`;
|
|
120
123
|
const mid = currentMachineId();
|
|
121
|
-
if (hubMode.peek() && mid) h['X-Machine-Id'] = mid;
|
|
124
|
+
if (!opts.hub && hubMode.peek() && mid) h['X-Machine-Id'] = mid;
|
|
122
125
|
return h;
|
|
123
126
|
}
|
|
124
127
|
|
package/public/app.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// app.js — Entry point
|
|
2
2
|
import { ctx } from './state.js';
|
|
3
|
-
import { probeHub } from './api.js';
|
|
3
|
+
import { probeHub } from './api.js?v=ui-24';
|
|
4
4
|
import { initShell, showApp, initAuth, resumeSession, switchTab, shouldRestoreSessionOnBoot } from './shell.js';
|
|
5
|
-
import { initChat } from './chat.js';
|
|
6
|
-
import { initFilesTab } from './files.js?v=ui-
|
|
5
|
+
import { initChat } from './chat.js?v=ui-24';
|
|
6
|
+
import { initFilesTab } from './files.js?v=ui-24';
|
|
7
7
|
import { initGitTab } from './git.js';
|
|
8
8
|
import { initSettings } from './settings.js';
|
|
9
|
+
import { initNotes } from './notes.js';
|
|
9
10
|
import { initTerminal } from './terminal.js';
|
|
10
|
-
import { initImagePaste } from './chat.js';
|
|
11
|
+
import { initImagePaste } from './chat.js?v=ui-24';
|
|
11
12
|
import { initMetaAgent } from './agent-panel.js';
|
|
12
13
|
import { initRouter, applyInitialRoute, parseHashPath } from './router.js';
|
|
13
|
-
import { initPwa } from './pwa.js';
|
|
14
|
+
import { initPwa } from './pwa.js?v=ui-24';
|
|
14
15
|
import { initMobileChrome } from './mobile.js';
|
|
15
16
|
|
|
16
17
|
function markBootReady() {
|
|
@@ -36,6 +37,7 @@ initChat();
|
|
|
36
37
|
initFilesTab();
|
|
37
38
|
initGitTab();
|
|
38
39
|
initSettings();
|
|
40
|
+
initNotes();
|
|
39
41
|
initTerminal();
|
|
40
42
|
initImagePaste();
|
|
41
43
|
initMetaAgent();
|
package/public/base.css
CHANGED
|
File without changes
|
package/public/cache.js
CHANGED
|
@@ -24,4 +24,23 @@ export async function setCachedWorkspaces(workspaces) {
|
|
|
24
24
|
catch {}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export async function getCachedTranscript(key) {
|
|
28
|
+
try { return recentTranscript(await db.get(`transcript:${key}`)); }
|
|
29
|
+
catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function setCachedTranscript(key, transcript) {
|
|
33
|
+
try { await db.set(`transcript:${key}`, recentTranscript(transcript), { ttl: 24 * 60 * 60 * 1000 }); }
|
|
34
|
+
catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// lastSessionContext lives in shell/session-context.js (avoid stale cache.js in browsers)
|
|
38
|
+
|
|
39
|
+
export function recentTranscript(snapshot) {
|
|
40
|
+
if (!snapshot?.msgs) return snapshot ?? null;
|
|
41
|
+
const drop = Math.max(0, snapshot.msgs.length - 80);
|
|
42
|
+
return { ...snapshot, msgs: snapshot.msgs.slice(drop), meta: {
|
|
43
|
+
...snapshot.meta, oldestIndex: (snapshot.meta?.oldestIndex || 0) + drop,
|
|
44
|
+
truncated: drop > 0 || !!snapshot.meta?.truncated,
|
|
45
|
+
} };
|
|
46
|
+
}
|
package/public/chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// chat.js — WebSocket, message rendering, send actions
|
|
2
2
|
import { watch, delegate, esc, $ } from './lib.js';
|
|
3
|
+
import { openWorkspaceFile } from './files.js?v=ui-23';
|
|
3
4
|
import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
|
|
4
5
|
currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
|
|
5
6
|
getDefaultEffort, addCustomModel, setCurrentModel, chatDensity, wsStatus, sessionMetaMap, metaKey } from './state.js';
|
|
@@ -9,22 +10,42 @@ import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
|
|
|
9
10
|
import { pushNotification } from './notifications.js';
|
|
10
11
|
import { renderMermaid } from './mermaid.js';
|
|
11
12
|
import { renderMath } from './math.js';
|
|
12
|
-
import {
|
|
13
|
+
import { parseMarkdownWithMath } from './markdown.js';
|
|
14
|
+
import { bindScrollFollow, isNearBottom } from './scroll-follow.js';
|
|
13
15
|
|
|
14
16
|
function assistantLabel() {
|
|
15
17
|
return AGENT_LABELS[currentAgent.peek()] || 'Assistant';
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function formatTurnError(raw) {
|
|
21
|
+
const t = String(raw || '').trim();
|
|
22
|
+
if (/remote compact task|error decoding response body/i.test(t)) {
|
|
23
|
+
return 'Codex 会话压缩失败(网络中断)。再发一条消息重试,或开新会话。';
|
|
24
|
+
}
|
|
25
|
+
return t || '发送失败';
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
// ── Scroll helpers ────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
29
|
+
let messageScroll = null;
|
|
30
|
+
|
|
20
31
|
function atBottom(el) {
|
|
21
|
-
return el
|
|
32
|
+
return isNearBottom(el);
|
|
22
33
|
}
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
function ensureMessageScroll(el) {
|
|
36
|
+
if (!el) return null;
|
|
37
|
+
if (messageScroll && messageScroll.el === el) return messageScroll;
|
|
38
|
+
messageScroll?.destroy?.();
|
|
39
|
+
messageScroll = bindScrollFollow(el);
|
|
40
|
+
messageScroll.el = el;
|
|
41
|
+
return messageScroll;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Scroll to bottom only if the user is following live output.
|
|
25
45
|
function stickBottom(el, force) {
|
|
26
|
-
if (!el) return;
|
|
27
|
-
|
|
46
|
+
if (!el || el.dataset.historyPainting === '1') return;
|
|
47
|
+
const follow = ensureMessageScroll(el);
|
|
48
|
+
follow.stick(!!force);
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
// ── WebSocket (auto-reconnect for mobile radio / background) ─────────────────
|
|
@@ -441,6 +462,33 @@ function handleWsMessage(msg) {
|
|
|
441
462
|
// session while its subscription is changing. Do not let that frame alter
|
|
442
463
|
// the new session's transcript, processing state, or queue.
|
|
443
464
|
if (msg?.sessionId && ctx.sessionId && msg.sessionId !== ctx.sessionId) return;
|
|
465
|
+
// Fan out note/queue WS events to any module that subscribes via the
|
|
466
|
+
// hub-ws-message custom event (e.g. personal notes, queue outbox). Done
|
|
467
|
+
// before session-scope filtering so global widgets (notes page) still
|
|
468
|
+
// receive updates for non-active sessions. We also dispatch the
|
|
469
|
+
// type-specific hub:note-* events used by the legacy notes page.
|
|
470
|
+
if (msg && typeof msg.type === 'string' && (
|
|
471
|
+
msg.type === 'note.added' ||
|
|
472
|
+
msg.type === 'note.updated' ||
|
|
473
|
+
msg.type === 'note.removed' ||
|
|
474
|
+
msg.type === 'queue.item.added' ||
|
|
475
|
+
msg.type === 'queue.item.updated' ||
|
|
476
|
+
msg.type === 'queue.item.removed' ||
|
|
477
|
+
msg.type === 'queue.item.moved' ||
|
|
478
|
+
msg.type === 'queue.item.sending'
|
|
479
|
+
)) {
|
|
480
|
+
window.dispatchEvent(new CustomEvent('hub-ws-message', { detail: msg }));
|
|
481
|
+
const noteEvent = ({
|
|
482
|
+
'note.added': 'hub:note-added',
|
|
483
|
+
'note.updated': 'hub:note-updated',
|
|
484
|
+
'note.removed': 'hub:note-removed',
|
|
485
|
+
})[msg.type];
|
|
486
|
+
if (noteEvent) window.dispatchEvent(new CustomEvent(noteEvent, { detail: msg }));
|
|
487
|
+
}
|
|
488
|
+
if (msg._hubReplay === true) {
|
|
489
|
+
document.dispatchEvent(new CustomEvent('external-session-updated', { detail: msg }));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
444
492
|
// Server outbox may pack multiple events into one frame
|
|
445
493
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
446
494
|
for (const item of msg.items) handleWsMessage(item);
|
|
@@ -519,7 +567,7 @@ function handleWsMessage(msg) {
|
|
|
519
567
|
ctx.lock = { ...(ctx.lock || {}), held: false, foreign: true, holder: msg.lock?.holder || ctx.lock?.holder || 'another user' };
|
|
520
568
|
applyComposerAccess();
|
|
521
569
|
}
|
|
522
|
-
appendMsg('error', 'Error', msg.message);
|
|
570
|
+
appendMsg('error', 'Error', formatTurnError(msg.message));
|
|
523
571
|
markTurnDone();
|
|
524
572
|
return;
|
|
525
573
|
}
|
|
@@ -551,10 +599,21 @@ function handleWsMessage(msg) {
|
|
|
551
599
|
|
|
552
600
|
// ── Live streaming (Grok / future agents) ─────────────────────────────────
|
|
553
601
|
if (msg.type === 'assistant_stream_start') {
|
|
602
|
+
// A replay can arrive before the edge's run-status snapshot. Mark the
|
|
603
|
+
// turn busy as soon as its first stream frame reaches us so the concurrent
|
|
604
|
+
// history read cannot repaint the transcript over live output.
|
|
605
|
+
if (!isProcessing.peek()) {
|
|
606
|
+
isProcessing.value = true;
|
|
607
|
+
syncWakeLock();
|
|
608
|
+
}
|
|
554
609
|
ensureAssistantStream(msg.ts);
|
|
555
610
|
return;
|
|
556
611
|
}
|
|
557
612
|
if (msg.type === 'assistant_delta') {
|
|
613
|
+
if (!isProcessing.peek()) {
|
|
614
|
+
isProcessing.value = true;
|
|
615
|
+
syncWakeLock();
|
|
616
|
+
}
|
|
558
617
|
appendAssistantDelta(msg.data || '');
|
|
559
618
|
return;
|
|
560
619
|
}
|
|
@@ -627,7 +686,7 @@ function ensureAssistantStream(ts) {
|
|
|
627
686
|
liveAsstEl = msgs.lastElementChild;
|
|
628
687
|
liveAsstBody = liveAsstEl.querySelector('.msg-stream-body');
|
|
629
688
|
$('stream-status')?.remove();
|
|
630
|
-
stickBottom(msgs
|
|
689
|
+
stickBottom(msgs);
|
|
631
690
|
}
|
|
632
691
|
|
|
633
692
|
function appendAssistantDelta(chunk) {
|
|
@@ -636,7 +695,7 @@ function appendAssistantDelta(chunk) {
|
|
|
636
695
|
if (!liveAsstBody) return;
|
|
637
696
|
// Keep raw text while streaming; re-render markdown on finalize
|
|
638
697
|
liveAsstBody.textContent += chunk;
|
|
639
|
-
stickBottom($('messages')
|
|
698
|
+
stickBottom($('messages'));
|
|
640
699
|
}
|
|
641
700
|
|
|
642
701
|
function finalizeAssistantStream() {
|
|
@@ -674,7 +733,7 @@ function ensureThoughtStream() {
|
|
|
674
733
|
</div>`);
|
|
675
734
|
liveThoughtEl = msgs.lastElementChild;
|
|
676
735
|
liveThoughtBody = liveThoughtEl.querySelector('.thought-body');
|
|
677
|
-
stickBottom(msgs
|
|
736
|
+
stickBottom(msgs);
|
|
678
737
|
}
|
|
679
738
|
|
|
680
739
|
function appendThoughtDelta(chunk) {
|
|
@@ -684,7 +743,7 @@ function appendThoughtDelta(chunk) {
|
|
|
684
743
|
liveThoughtBody.textContent += chunk;
|
|
685
744
|
// Keep thought panel scrolled to end
|
|
686
745
|
liveThoughtBody.scrollTop = liveThoughtBody.scrollHeight;
|
|
687
|
-
stickBottom($('messages')
|
|
746
|
+
stickBottom($('messages'));
|
|
688
747
|
}
|
|
689
748
|
|
|
690
749
|
function finalizeThoughtStream() {
|
|
@@ -990,7 +1049,7 @@ export function sendMessage() {
|
|
|
990
1049
|
if (!ok) appendSystemMsg('连接未就绪,命令已入队,重连后发送');
|
|
991
1050
|
} else {
|
|
992
1051
|
flushToolBatch();
|
|
993
|
-
appendMsg('user', 'You', text, { ts: now });
|
|
1052
|
+
appendMsg('user', 'You', text, { ts: now, forceStick: true });
|
|
994
1053
|
const agent = currentAgent.peek() || 'claude';
|
|
995
1054
|
const effort = currentEffort.peek()
|
|
996
1055
|
|| getDefaultEffort(agent, currentModel.peek())
|
|
@@ -1170,7 +1229,7 @@ export function appendMsg(role, _label, text, opts = {}) {
|
|
|
1170
1229
|
msgs.insertAdjacentHTML('beforeend', bubbleHtml(role, text, ts));
|
|
1171
1230
|
enhanceMarkdown(msgs.lastElementChild);
|
|
1172
1231
|
if (role === 'assistant') addAssistantMessageActions(msgs.lastElementChild, text, opts);
|
|
1173
|
-
stickBottom(msgs,
|
|
1232
|
+
stickBottom(msgs, !!opts.forceStick);
|
|
1174
1233
|
}
|
|
1175
1234
|
|
|
1176
1235
|
export function appendSystemMsg(text) { appendMsg('system', '', text); }
|
|
@@ -1460,9 +1519,29 @@ export function appendContextBar(ctxInfo) {
|
|
|
1460
1519
|
appendTokenBar(`▣ ${parts.join(' · ')}`);
|
|
1461
1520
|
}
|
|
1462
1521
|
|
|
1522
|
+
export function beginHistoryRender() {
|
|
1523
|
+
const msgs = $('messages');
|
|
1524
|
+
if (!msgs) return;
|
|
1525
|
+
msgs.dataset.historyPainting = '1';
|
|
1526
|
+
msgs.innerHTML = '';
|
|
1527
|
+
toolBatch = null;
|
|
1528
|
+
assistantMessageRefs.clear();
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export function endHistoryRender(scrollTop = null) {
|
|
1532
|
+
const msgs = $('messages');
|
|
1533
|
+
if (!msgs) return;
|
|
1534
|
+
delete msgs.dataset.historyPainting;
|
|
1535
|
+
if (scrollTop == null) stickBottom(msgs, true);
|
|
1536
|
+
else {
|
|
1537
|
+
msgs.scrollTop = Math.max(0, scrollTop);
|
|
1538
|
+
ensureMessageScroll(msgs).setFollowing(isNearBottom(msgs));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1463
1542
|
export function clearMessages() {
|
|
1464
1543
|
const msgs = $('messages'), perms = $('permission-requests');
|
|
1465
|
-
if (msgs)
|
|
1544
|
+
if (msgs) { msgs.innerHTML = ''; delete msgs.dataset.historyPainting; }
|
|
1466
1545
|
if (perms) perms.innerHTML = '';
|
|
1467
1546
|
ctx.shellBubble = null;
|
|
1468
1547
|
toolBatch = null;
|
|
@@ -1483,7 +1562,7 @@ function appendQueuedGrokPrompt(text, sessionId = ctx.sessionId) {
|
|
|
1483
1562
|
const key = queuedGrokPromptKey(text, sessionId);
|
|
1484
1563
|
if (!text || renderedGrokQueuePrompts.has(key)) return;
|
|
1485
1564
|
renderedGrokQueuePrompts.add(key);
|
|
1486
|
-
appendMsg('user', 'You', text, { ts: Date.now() });
|
|
1565
|
+
appendMsg('user', 'You', text, { ts: Date.now(), forceStick: true });
|
|
1487
1566
|
$('messages')?.lastElementChild?.classList.add('queued-user-msg');
|
|
1488
1567
|
}
|
|
1489
1568
|
|
|
@@ -1722,7 +1801,7 @@ function renderPermissionRequest(msg) {
|
|
|
1722
1801
|
<button class="btn btn-error btn-xs" data-perm-id="${id}" data-perm-allow="0">Deny</button>
|
|
1723
1802
|
</div>
|
|
1724
1803
|
</div>`);
|
|
1725
|
-
$('messages')
|
|
1804
|
+
stickBottom($('messages'));
|
|
1726
1805
|
}
|
|
1727
1806
|
|
|
1728
1807
|
/** Configure marked once: GFM + code chrome + table wrap. */
|
|
@@ -1792,7 +1871,7 @@ function renderMarkdown(text) {
|
|
|
1792
1871
|
if (typeof marked === 'undefined') return esc(text);
|
|
1793
1872
|
try {
|
|
1794
1873
|
ensureMarked();
|
|
1795
|
-
return
|
|
1874
|
+
return parseMarkdownWithMath(text, { fallback: () => esc(text) });
|
|
1796
1875
|
} catch {
|
|
1797
1876
|
return esc(text);
|
|
1798
1877
|
}
|
|
@@ -1849,6 +1928,15 @@ export function initChat() {
|
|
|
1849
1928
|
|
|
1850
1929
|
watch(currentProject, () => syncWakeLock());
|
|
1851
1930
|
|
|
1931
|
+
delegate.on('click', '#messages a, #chat-log a, .md-body a', (e, el) => {
|
|
1932
|
+
const href = el.getAttribute('href') || '';
|
|
1933
|
+
const cwd = currentProject.peek()?.path || '';
|
|
1934
|
+
if (openWorkspaceFile(href, cwd)) {
|
|
1935
|
+
e.preventDefault();
|
|
1936
|
+
e.stopPropagation();
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1852
1940
|
delegate.on('click', '#ws-banner-retry', e => {
|
|
1853
1941
|
e.preventDefault();
|
|
1854
1942
|
refreshConnection();
|
|
@@ -1972,7 +2060,10 @@ export function initChat() {
|
|
|
1972
2060
|
new ResizeObserver(updateNav).observe(msgsEl);
|
|
1973
2061
|
new MutationObserver(updateNav).observe(msgsEl, { childList: true, subtree: true });
|
|
1974
2062
|
topBtn.addEventListener('click', () => msgsEl.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
1975
|
-
botBtn.addEventListener('click', () =>
|
|
2063
|
+
botBtn.addEventListener('click', () => {
|
|
2064
|
+
ensureMessageScroll(msgsEl)?.setFollowing(true);
|
|
2065
|
+
msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: 'smooth' });
|
|
2066
|
+
});
|
|
1976
2067
|
updateNav();
|
|
1977
2068
|
}
|
|
1978
2069
|
|
package/public/files.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { effect, asyncEffect, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { currentProject, currentTab, filesRoot, filesPath, viewingFile, ctx } from './state.js';
|
|
4
4
|
import { api, authHeaders, authQuery } from './api.js';
|
|
5
|
+
import { resolveWorkspaceFile } from './workspace-path.js?v=ui-23';
|
|
5
6
|
import { renderMermaid } from './mermaid.js';
|
|
6
7
|
import { renderMath } from './math.js';
|
|
8
|
+
import { parseMarkdownWithMath } from './markdown.js';
|
|
7
9
|
|
|
8
10
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
11
|
function fileIcon(name) {
|
|
@@ -153,12 +155,9 @@ function renderTextFile(path, content, size) {
|
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
function markdownHtml(content) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch { /* Show the source below if parsing fails. */ }
|
|
160
|
-
}
|
|
161
|
-
return esc(content).replace(/\n/g, '<br>');
|
|
158
|
+
return parseMarkdownWithMath(content, {
|
|
159
|
+
fallback: (text) => esc(text).replace(/\n/g, '<br>'),
|
|
160
|
+
});
|
|
162
161
|
}
|
|
163
162
|
|
|
164
163
|
function renderMarkdownFile(path, content, size) {
|
|
@@ -357,7 +356,7 @@ function triggerUpload() {
|
|
|
357
356
|
input.multiple = true;
|
|
358
357
|
input.addEventListener('change', async () => {
|
|
359
358
|
const dir = filesPath.peek();
|
|
360
|
-
const id =
|
|
359
|
+
const id = '_';
|
|
361
360
|
const files = [...input.files];
|
|
362
361
|
let failed = false;
|
|
363
362
|
for (const file of files) {
|
|
@@ -388,7 +387,7 @@ async function downloadFile(proj, filePath) {
|
|
|
388
387
|
alert('No path selected — set root first');
|
|
389
388
|
return;
|
|
390
389
|
}
|
|
391
|
-
const id =
|
|
390
|
+
const id = '_';
|
|
392
391
|
const name = (filePath || '').split('/').pop() || 'download';
|
|
393
392
|
// Prefer Authorization header (no token in URL / logs). authQuery kept as
|
|
394
393
|
// fallback for environments that only support plain navigations.
|
|
@@ -433,7 +432,7 @@ async function downloadFile(proj, filePath) {
|
|
|
433
432
|
async function deleteFile(proj, filePath) {
|
|
434
433
|
if (!confirm(`Delete "${filePath.split('/').pop()}"?`)) return;
|
|
435
434
|
const root = filesRoot.peek() || proj?.path || '';
|
|
436
|
-
const id =
|
|
435
|
+
const id = '_';
|
|
437
436
|
try {
|
|
438
437
|
await api('DELETE', `/api/projects/${id}/file?root=${encodeURIComponent(root)}&path=${encodeURIComponent(filePath)}`);
|
|
439
438
|
filesPath.value = filesPath.peek() + ' ';
|
|
@@ -469,6 +468,23 @@ async function showFile(area, file, data, signal) {
|
|
|
469
468
|
}
|
|
470
469
|
|
|
471
470
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
471
|
+
/** Open a machine-local path from a chat markdown link. */
|
|
472
|
+
export function openWorkspaceFile(href, cwd) {
|
|
473
|
+
const target = resolveWorkspaceFile(href, cwd || filesRoot.peek() || currentProject.peek()?.path || '');
|
|
474
|
+
if (!target) return false;
|
|
475
|
+
if (target.root) filesRoot.value = target.root;
|
|
476
|
+
if (target.isDir) {
|
|
477
|
+
viewingFile.value = null;
|
|
478
|
+
filesPath.value = target.relative;
|
|
479
|
+
} else {
|
|
480
|
+
const parts = target.relative.split('/').filter(Boolean);
|
|
481
|
+
filesPath.value = parts.slice(0, -1).join('/');
|
|
482
|
+
viewingFile.value = target.relative;
|
|
483
|
+
}
|
|
484
|
+
currentTab.value = 'files';
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
472
488
|
export function initFilesTab() {
|
|
473
489
|
effect(() => {
|
|
474
490
|
const bc = $('files-breadcrumb');
|
|
@@ -494,7 +510,7 @@ export function initFilesTab() {
|
|
|
494
510
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">No path yet — open a session (uses that machine's cwd) or type an absolute path and press Go.</div>';
|
|
495
511
|
return;
|
|
496
512
|
}
|
|
497
|
-
const id =
|
|
513
|
+
const id = '_';
|
|
498
514
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
499
515
|
try {
|
|
500
516
|
if (file !== null) {
|