claude-simple 0.2.28 → 0.2.31
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.31.min.js +1308 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.31.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 +80 -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 +66 -18
- 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/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.31",
|
|
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,6 +41,7 @@ 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
|
|
|
@@ -78,6 +77,10 @@ async function streamSSE(path, body, handlers) {
|
|
|
78
77
|
let data;
|
|
79
78
|
try { data = JSON.parse(raw); } catch { continue; }
|
|
80
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
|
+
}
|
|
81
84
|
handlers.any?.(event, data);
|
|
82
85
|
event = 'message';
|
|
83
86
|
} else if (line.trim() === '') {
|
|
@@ -376,16 +379,19 @@ function appendMsg(kind, text, opts = {}) {
|
|
|
376
379
|
} else if (kind === 'assistant') {
|
|
377
380
|
el.innerHTML = `<div class="ma-bubble assistant md">${mdHtml(text)}</div>`;
|
|
378
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;
|
|
379
385
|
} else if (kind === 'error') {
|
|
380
386
|
el.innerHTML = `<div class="ma-bubble error">${esc(text)}</div>`;
|
|
381
387
|
} else if (kind === 'system') {
|
|
382
388
|
el.innerHTML = `<div class="ma-sys">${esc(text)}</div>`;
|
|
383
389
|
} else if (kind === 'tool') {
|
|
384
|
-
el.innerHTML = `<
|
|
385
|
-
<span class="ma-tool-name">${esc(opts.name || 'tool')}</span>
|
|
386
|
-
<span class="ma-tool-phase">${esc(opts.phase || '')}</span>
|
|
387
|
-
<pre class="ma-tool-args">${esc(JSON.stringify(opts.args || {}, null,
|
|
388
|
-
</
|
|
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>`;
|
|
389
395
|
}
|
|
390
396
|
box.appendChild(el);
|
|
391
397
|
box.scrollTop = box.scrollHeight;
|
|
@@ -409,14 +415,15 @@ function updateToolChip(id, phase, result) {
|
|
|
409
415
|
el.classList.toggle('error', phase === 'error');
|
|
410
416
|
const ph = el.querySelector('.ma-tool-phase');
|
|
411
417
|
if (ph) ph.textContent = phase;
|
|
412
|
-
if (result && phase !== 'start') {
|
|
418
|
+
if (result !== undefined && phase !== 'start') {
|
|
413
419
|
let pre = el.querySelector('.ma-tool-result');
|
|
414
420
|
if (!pre) {
|
|
415
421
|
pre = document.createElement('pre');
|
|
416
422
|
pre.className = 'ma-tool-result';
|
|
423
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
417
424
|
el.appendChild(pre);
|
|
418
425
|
}
|
|
419
|
-
pre.textContent = JSON.stringify(result, null,
|
|
426
|
+
pre.textContent = JSON.stringify(result, null, 2);
|
|
420
427
|
}
|
|
421
428
|
}
|
|
422
429
|
|
|
@@ -806,6 +813,10 @@ function renderHistoryMessages(messages) {
|
|
|
806
813
|
continue;
|
|
807
814
|
}
|
|
808
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
|
+
}
|
|
809
820
|
if (m.tool_calls?.length) {
|
|
810
821
|
for (const tc of m.tool_calls) {
|
|
811
822
|
const name = tc.function?.name || tc.name || 'tool';
|
|
@@ -821,9 +832,6 @@ function renderHistoryMessages(messages) {
|
|
|
821
832
|
});
|
|
822
833
|
}
|
|
823
834
|
}
|
|
824
|
-
if (m.content) {
|
|
825
|
-
appendMsg('assistant', typeof m.content === 'string' ? m.content : userContentToText(m.content));
|
|
826
|
-
}
|
|
827
835
|
continue;
|
|
828
836
|
}
|
|
829
837
|
if (m.role === 'tool') {
|
|
@@ -833,7 +841,7 @@ function renderHistoryMessages(messages) {
|
|
|
833
841
|
} catch { /* keep string */ }
|
|
834
842
|
// attach to last matching tool chip if possible
|
|
835
843
|
const id = m.tool_call_id || '';
|
|
836
|
-
if (id) updateToolChip(id, 'done', result);
|
|
844
|
+
if (id) updateToolChip(id, typeof result === 'string' && result.startsWith('error:') ? 'error' : 'done', result);
|
|
837
845
|
else {
|
|
838
846
|
appendMsg('tool', '', {
|
|
839
847
|
name: 'tool_result',
|
|
@@ -850,11 +858,12 @@ function renderHistoryMessages(messages) {
|
|
|
850
858
|
if (!pre) {
|
|
851
859
|
pre = document.createElement('pre');
|
|
852
860
|
pre.className = 'ma-tool-result';
|
|
861
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
853
862
|
last.appendChild(pre);
|
|
854
863
|
}
|
|
855
864
|
pre.textContent = typeof result === 'string'
|
|
856
865
|
? result.slice(0, 500)
|
|
857
|
-
: JSON.stringify(result, null,
|
|
866
|
+
: JSON.stringify(result, null, 2);
|
|
858
867
|
}
|
|
859
868
|
}
|
|
860
869
|
}
|
|
@@ -942,6 +951,7 @@ async function sendChat() {
|
|
|
942
951
|
|
|
943
952
|
let asstEl = null;
|
|
944
953
|
let content = '';
|
|
954
|
+
let reasoningEl = null;
|
|
945
955
|
|
|
946
956
|
try {
|
|
947
957
|
await streamSSE('/api/ai/chat', {
|
|
@@ -959,6 +969,19 @@ async function sendChat() {
|
|
|
959
969
|
localStorage.setItem(LAST_SESSION_KEY, d.id);
|
|
960
970
|
updateSessionChrome();
|
|
961
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),
|
|
962
985
|
content: (d) => {
|
|
963
986
|
content = d.content || '';
|
|
964
987
|
if (!asstEl) asstEl = appendMsg('assistant', '');
|
|
@@ -985,7 +1008,11 @@ async function sendChat() {
|
|
|
985
1008
|
messageCount: (currentSessionMeta?.messageCount || 0) + 2,
|
|
986
1009
|
};
|
|
987
1010
|
}
|
|
988
|
-
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;
|
|
989
1016
|
updateSessionChrome();
|
|
990
1017
|
refreshHistoryCache();
|
|
991
1018
|
},
|
package/public/api.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function api(method, path, body, signal, opts = {}) {
|
|
|
63
63
|
method,
|
|
64
64
|
signal,
|
|
65
65
|
headers,
|
|
66
|
-
|
|
66
|
+
cache: opts.cache || (method.toUpperCase() === 'GET' ? 'no-store' : 'default'),
|
|
67
67
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
68
68
|
});
|
|
69
69
|
if (res.status === 304) return NOT_MODIFIED;
|
package/public/app.js
CHANGED
|
@@ -1,17 +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
9
|
import { initNotes } from './notes.js';
|
|
10
10
|
import { initTerminal } from './terminal.js';
|
|
11
|
-
import { initImagePaste } from './chat.js';
|
|
11
|
+
import { initImagePaste } from './chat.js?v=ui-24';
|
|
12
12
|
import { initMetaAgent } from './agent-panel.js';
|
|
13
13
|
import { initRouter, applyInitialRoute, parseHashPath } from './router.js';
|
|
14
|
-
import { initPwa } from './pwa.js';
|
|
14
|
+
import { initPwa } from './pwa.js?v=ui-24';
|
|
15
15
|
import { initMobileChrome } from './mobile.js';
|
|
16
16
|
|
|
17
17
|
function markBootReady() {
|
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) ─────────────────
|
|
@@ -436,6 +457,9 @@ function markTurnDone() {
|
|
|
436
457
|
flushPendingUserText();
|
|
437
458
|
}
|
|
438
459
|
|
|
460
|
+
let chatOutputRevision = 0;
|
|
461
|
+
export function getChatOutputRevision() { return chatOutputRevision; }
|
|
462
|
+
|
|
439
463
|
function handleWsMessage(msg) {
|
|
440
464
|
// The socket can still receive a final frame from the previously viewed
|
|
441
465
|
// session while its subscription is changing. Do not let that frame alter
|
|
@@ -464,11 +488,16 @@ function handleWsMessage(msg) {
|
|
|
464
488
|
})[msg.type];
|
|
465
489
|
if (noteEvent) window.dispatchEvent(new CustomEvent(noteEvent, { detail: msg }));
|
|
466
490
|
}
|
|
491
|
+
if (msg._hubReplay === true) {
|
|
492
|
+
document.dispatchEvent(new CustomEvent('external-session-updated', { detail: msg }));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
467
495
|
// Server outbox may pack multiple events into one frame
|
|
468
496
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
469
497
|
for (const item of msg.items) handleWsMessage(item);
|
|
470
498
|
return;
|
|
471
499
|
}
|
|
500
|
+
if (['assistant', 'assistant_stream_start', 'assistant_delta', 'thought_delta', 'tool_use'].includes(msg.type)) chatOutputRevision++;
|
|
472
501
|
if (msg.type === 'session-created') {
|
|
473
502
|
const selectedModel = currentModel.peek();
|
|
474
503
|
ctx.sessionId = msg.sessionId;
|
|
@@ -542,7 +571,7 @@ function handleWsMessage(msg) {
|
|
|
542
571
|
ctx.lock = { ...(ctx.lock || {}), held: false, foreign: true, holder: msg.lock?.holder || ctx.lock?.holder || 'another user' };
|
|
543
572
|
applyComposerAccess();
|
|
544
573
|
}
|
|
545
|
-
appendMsg('error', 'Error', msg.message);
|
|
574
|
+
appendMsg('error', 'Error', formatTurnError(msg.message));
|
|
546
575
|
markTurnDone();
|
|
547
576
|
return;
|
|
548
577
|
}
|
|
@@ -661,7 +690,7 @@ function ensureAssistantStream(ts) {
|
|
|
661
690
|
liveAsstEl = msgs.lastElementChild;
|
|
662
691
|
liveAsstBody = liveAsstEl.querySelector('.msg-stream-body');
|
|
663
692
|
$('stream-status')?.remove();
|
|
664
|
-
stickBottom(msgs
|
|
693
|
+
stickBottom(msgs);
|
|
665
694
|
}
|
|
666
695
|
|
|
667
696
|
function appendAssistantDelta(chunk) {
|
|
@@ -670,7 +699,7 @@ function appendAssistantDelta(chunk) {
|
|
|
670
699
|
if (!liveAsstBody) return;
|
|
671
700
|
// Keep raw text while streaming; re-render markdown on finalize
|
|
672
701
|
liveAsstBody.textContent += chunk;
|
|
673
|
-
stickBottom($('messages')
|
|
702
|
+
stickBottom($('messages'));
|
|
674
703
|
}
|
|
675
704
|
|
|
676
705
|
function finalizeAssistantStream() {
|
|
@@ -708,7 +737,7 @@ function ensureThoughtStream() {
|
|
|
708
737
|
</div>`);
|
|
709
738
|
liveThoughtEl = msgs.lastElementChild;
|
|
710
739
|
liveThoughtBody = liveThoughtEl.querySelector('.thought-body');
|
|
711
|
-
stickBottom(msgs
|
|
740
|
+
stickBottom(msgs);
|
|
712
741
|
}
|
|
713
742
|
|
|
714
743
|
function appendThoughtDelta(chunk) {
|
|
@@ -718,7 +747,7 @@ function appendThoughtDelta(chunk) {
|
|
|
718
747
|
liveThoughtBody.textContent += chunk;
|
|
719
748
|
// Keep thought panel scrolled to end
|
|
720
749
|
liveThoughtBody.scrollTop = liveThoughtBody.scrollHeight;
|
|
721
|
-
stickBottom($('messages')
|
|
750
|
+
stickBottom($('messages'));
|
|
722
751
|
}
|
|
723
752
|
|
|
724
753
|
function finalizeThoughtStream() {
|
|
@@ -1024,7 +1053,7 @@ export function sendMessage() {
|
|
|
1024
1053
|
if (!ok) appendSystemMsg('连接未就绪,命令已入队,重连后发送');
|
|
1025
1054
|
} else {
|
|
1026
1055
|
flushToolBatch();
|
|
1027
|
-
appendMsg('user', 'You', text, { ts: now });
|
|
1056
|
+
appendMsg('user', 'You', text, { ts: now, forceStick: true });
|
|
1028
1057
|
const agent = currentAgent.peek() || 'claude';
|
|
1029
1058
|
const effort = currentEffort.peek()
|
|
1030
1059
|
|| getDefaultEffort(agent, currentModel.peek())
|
|
@@ -1204,7 +1233,7 @@ export function appendMsg(role, _label, text, opts = {}) {
|
|
|
1204
1233
|
msgs.insertAdjacentHTML('beforeend', bubbleHtml(role, text, ts));
|
|
1205
1234
|
enhanceMarkdown(msgs.lastElementChild);
|
|
1206
1235
|
if (role === 'assistant') addAssistantMessageActions(msgs.lastElementChild, text, opts);
|
|
1207
|
-
stickBottom(msgs,
|
|
1236
|
+
stickBottom(msgs, !!opts.forceStick);
|
|
1208
1237
|
}
|
|
1209
1238
|
|
|
1210
1239
|
export function appendSystemMsg(text) { appendMsg('system', '', text); }
|
|
@@ -1494,9 +1523,29 @@ export function appendContextBar(ctxInfo) {
|
|
|
1494
1523
|
appendTokenBar(`▣ ${parts.join(' · ')}`);
|
|
1495
1524
|
}
|
|
1496
1525
|
|
|
1526
|
+
export function beginHistoryRender() {
|
|
1527
|
+
const msgs = $('messages');
|
|
1528
|
+
if (!msgs) return;
|
|
1529
|
+
msgs.dataset.historyPainting = '1';
|
|
1530
|
+
msgs.innerHTML = '';
|
|
1531
|
+
toolBatch = null;
|
|
1532
|
+
assistantMessageRefs.clear();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export function endHistoryRender(scrollTop = null) {
|
|
1536
|
+
const msgs = $('messages');
|
|
1537
|
+
if (!msgs) return;
|
|
1538
|
+
delete msgs.dataset.historyPainting;
|
|
1539
|
+
if (scrollTop == null) stickBottom(msgs, true);
|
|
1540
|
+
else {
|
|
1541
|
+
msgs.scrollTop = Math.max(0, scrollTop);
|
|
1542
|
+
ensureMessageScroll(msgs).setFollowing(isNearBottom(msgs));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1497
1546
|
export function clearMessages() {
|
|
1498
1547
|
const msgs = $('messages'), perms = $('permission-requests');
|
|
1499
|
-
if (msgs)
|
|
1548
|
+
if (msgs) { msgs.innerHTML = ''; delete msgs.dataset.historyPainting; }
|
|
1500
1549
|
if (perms) perms.innerHTML = '';
|
|
1501
1550
|
ctx.shellBubble = null;
|
|
1502
1551
|
toolBatch = null;
|
|
@@ -1517,7 +1566,7 @@ function appendQueuedGrokPrompt(text, sessionId = ctx.sessionId) {
|
|
|
1517
1566
|
const key = queuedGrokPromptKey(text, sessionId);
|
|
1518
1567
|
if (!text || renderedGrokQueuePrompts.has(key)) return;
|
|
1519
1568
|
renderedGrokQueuePrompts.add(key);
|
|
1520
|
-
appendMsg('user', 'You', text, { ts: Date.now() });
|
|
1569
|
+
appendMsg('user', 'You', text, { ts: Date.now(), forceStick: true });
|
|
1521
1570
|
$('messages')?.lastElementChild?.classList.add('queued-user-msg');
|
|
1522
1571
|
}
|
|
1523
1572
|
|
|
@@ -1756,7 +1805,7 @@ function renderPermissionRequest(msg) {
|
|
|
1756
1805
|
<button class="btn btn-error btn-xs" data-perm-id="${id}" data-perm-allow="0">Deny</button>
|
|
1757
1806
|
</div>
|
|
1758
1807
|
</div>`);
|
|
1759
|
-
$('messages')
|
|
1808
|
+
stickBottom($('messages'));
|
|
1760
1809
|
}
|
|
1761
1810
|
|
|
1762
1811
|
/** Configure marked once: GFM + code chrome + table wrap. */
|
|
@@ -1826,7 +1875,7 @@ function renderMarkdown(text) {
|
|
|
1826
1875
|
if (typeof marked === 'undefined') return esc(text);
|
|
1827
1876
|
try {
|
|
1828
1877
|
ensureMarked();
|
|
1829
|
-
return
|
|
1878
|
+
return parseMarkdownWithMath(text, { fallback: () => esc(text) });
|
|
1830
1879
|
} catch {
|
|
1831
1880
|
return esc(text);
|
|
1832
1881
|
}
|
|
@@ -1883,6 +1932,15 @@ export function initChat() {
|
|
|
1883
1932
|
|
|
1884
1933
|
watch(currentProject, () => syncWakeLock());
|
|
1885
1934
|
|
|
1935
|
+
delegate.on('click', '#messages a, #chat-log a, .md-body a', (e, el) => {
|
|
1936
|
+
const href = el.getAttribute('href') || '';
|
|
1937
|
+
const cwd = currentProject.peek()?.path || '';
|
|
1938
|
+
if (openWorkspaceFile(href, cwd)) {
|
|
1939
|
+
e.preventDefault();
|
|
1940
|
+
e.stopPropagation();
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1886
1944
|
delegate.on('click', '#ws-banner-retry', e => {
|
|
1887
1945
|
e.preventDefault();
|
|
1888
1946
|
refreshConnection();
|
|
@@ -2006,7 +2064,10 @@ export function initChat() {
|
|
|
2006
2064
|
new ResizeObserver(updateNav).observe(msgsEl);
|
|
2007
2065
|
new MutationObserver(updateNav).observe(msgsEl, { childList: true, subtree: true });
|
|
2008
2066
|
topBtn.addEventListener('click', () => msgsEl.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
2009
|
-
botBtn.addEventListener('click', () =>
|
|
2067
|
+
botBtn.addEventListener('click', () => {
|
|
2068
|
+
ensureMessageScroll(msgsEl)?.setFollowing(true);
|
|
2069
|
+
msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: 'smooth' });
|
|
2070
|
+
});
|
|
2010
2071
|
updateNav();
|
|
2011
2072
|
}
|
|
2012
2073
|
|
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) {
|
package/public/git.js
CHANGED
|
@@ -74,7 +74,7 @@ export function initGitTab() {
|
|
|
74
74
|
area.innerHTML = '<div class="p-4 text-sm text-base-content/50">Enter a project root above, or open a session with a working directory.</div>';
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
const id =
|
|
77
|
+
const id = '_';
|
|
78
78
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
79
79
|
try {
|
|
80
80
|
const data = await api('GET', `/api/projects/${id}/git/status?root=${encodeURIComponent(root)}`, undefined, signal);
|
|
@@ -126,7 +126,7 @@ function loadGraph() {
|
|
|
126
126
|
const root = gitRoot.peek() || proj?.path;
|
|
127
127
|
const content = $('git-graph-content');
|
|
128
128
|
if (!root || !content) return;
|
|
129
|
-
const id =
|
|
129
|
+
const id = '_';
|
|
130
130
|
content.innerHTML = '<span class="text-base-content/40">Loading…</span>';
|
|
131
131
|
api('GET', `/api/projects/${id}/git/log?root=${encodeURIComponent(root)}`)
|
|
132
132
|
.then(d => { content.innerHTML = d.graph ? renderGraph(d.graph) : '<span class="text-base-content/40">No commits</span>'; })
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/public/index.html
CHANGED
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<script src="/vendor/mermaid.min.js"></script>
|
|
86
86
|
<link rel="stylesheet" href="/vendor/katex.min.css">
|
|
87
87
|
<script src="/vendor/katex.min.js"></script>
|
|
88
|
-
<link rel="stylesheet" href="style.css?v=ui-
|
|
88
|
+
<link rel="stylesheet" href="style.css?v=ui-25">
|
|
89
89
|
</head>
|
|
90
90
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
91
91
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
@@ -101,6 +101,6 @@
|
|
|
101
101
|
}
|
|
102
102
|
}, 10000);
|
|
103
103
|
</script>
|
|
104
|
-
<script type="module" src="app.js?v=ui-
|
|
104
|
+
<script type="module" src="app.js?v=ui-25"></script>
|
|
105
105
|
</body>
|
|
106
106
|
</html>
|
package/public/lib.js
CHANGED
|
File without changes
|
|
File without changes
|
package/public/markdown.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// placeholder. It is not HTML, but a Markdown parser will otherwise pass it
|
|
4
4
|
// through as an element and a malformed placeholder can corrupt the rest of a
|
|
5
5
|
// message's DOM.
|
|
6
|
+
import { protectMath, restoreMath } from './math.js';
|
|
7
|
+
|
|
6
8
|
const TRUNCATED_TRANSCRIPT_MARKER = /(?:<|\[)\s*(?:…\s*)?truncated\s+([\d,]+)\s+bytes\s*(?:>|\])/gi;
|
|
7
9
|
|
|
8
10
|
export function normalizeMarkdownText(value) {
|
|
@@ -10,3 +12,17 @@ export function normalizeMarkdownText(value) {
|
|
|
10
12
|
return `\n\n> ⚠️ 原始记录已截断(${bytes} bytes)\n\n`;
|
|
11
13
|
});
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function parseMarkdownWithMath(value, { fallback } = {}) {
|
|
17
|
+
const { text, slots } = protectMath(normalizeMarkdownText(value));
|
|
18
|
+
if (typeof marked === 'undefined') {
|
|
19
|
+
const raw = fallback ? fallback(text) : text;
|
|
20
|
+
return restoreMath(raw, slots);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return restoreMath(marked.parse(text, { breaks: true, gfm: true }), slots);
|
|
24
|
+
} catch {
|
|
25
|
+
const raw = fallback ? fallback(text) : text;
|
|
26
|
+
return restoreMath(raw, slots);
|
|
27
|
+
}
|
|
28
|
+
}
|