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/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,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) ─────────────────
|
|
@@ -464,6 +485,10 @@ function handleWsMessage(msg) {
|
|
|
464
485
|
})[msg.type];
|
|
465
486
|
if (noteEvent) window.dispatchEvent(new CustomEvent(noteEvent, { detail: msg }));
|
|
466
487
|
}
|
|
488
|
+
if (msg._hubReplay === true) {
|
|
489
|
+
document.dispatchEvent(new CustomEvent('external-session-updated', { detail: msg }));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
467
492
|
// Server outbox may pack multiple events into one frame
|
|
468
493
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
469
494
|
for (const item of msg.items) handleWsMessage(item);
|
|
@@ -542,7 +567,7 @@ function handleWsMessage(msg) {
|
|
|
542
567
|
ctx.lock = { ...(ctx.lock || {}), held: false, foreign: true, holder: msg.lock?.holder || ctx.lock?.holder || 'another user' };
|
|
543
568
|
applyComposerAccess();
|
|
544
569
|
}
|
|
545
|
-
appendMsg('error', 'Error', msg.message);
|
|
570
|
+
appendMsg('error', 'Error', formatTurnError(msg.message));
|
|
546
571
|
markTurnDone();
|
|
547
572
|
return;
|
|
548
573
|
}
|
|
@@ -661,7 +686,7 @@ function ensureAssistantStream(ts) {
|
|
|
661
686
|
liveAsstEl = msgs.lastElementChild;
|
|
662
687
|
liveAsstBody = liveAsstEl.querySelector('.msg-stream-body');
|
|
663
688
|
$('stream-status')?.remove();
|
|
664
|
-
stickBottom(msgs
|
|
689
|
+
stickBottom(msgs);
|
|
665
690
|
}
|
|
666
691
|
|
|
667
692
|
function appendAssistantDelta(chunk) {
|
|
@@ -670,7 +695,7 @@ function appendAssistantDelta(chunk) {
|
|
|
670
695
|
if (!liveAsstBody) return;
|
|
671
696
|
// Keep raw text while streaming; re-render markdown on finalize
|
|
672
697
|
liveAsstBody.textContent += chunk;
|
|
673
|
-
stickBottom($('messages')
|
|
698
|
+
stickBottom($('messages'));
|
|
674
699
|
}
|
|
675
700
|
|
|
676
701
|
function finalizeAssistantStream() {
|
|
@@ -708,7 +733,7 @@ function ensureThoughtStream() {
|
|
|
708
733
|
</div>`);
|
|
709
734
|
liveThoughtEl = msgs.lastElementChild;
|
|
710
735
|
liveThoughtBody = liveThoughtEl.querySelector('.thought-body');
|
|
711
|
-
stickBottom(msgs
|
|
736
|
+
stickBottom(msgs);
|
|
712
737
|
}
|
|
713
738
|
|
|
714
739
|
function appendThoughtDelta(chunk) {
|
|
@@ -718,7 +743,7 @@ function appendThoughtDelta(chunk) {
|
|
|
718
743
|
liveThoughtBody.textContent += chunk;
|
|
719
744
|
// Keep thought panel scrolled to end
|
|
720
745
|
liveThoughtBody.scrollTop = liveThoughtBody.scrollHeight;
|
|
721
|
-
stickBottom($('messages')
|
|
746
|
+
stickBottom($('messages'));
|
|
722
747
|
}
|
|
723
748
|
|
|
724
749
|
function finalizeThoughtStream() {
|
|
@@ -1024,7 +1049,7 @@ export function sendMessage() {
|
|
|
1024
1049
|
if (!ok) appendSystemMsg('连接未就绪,命令已入队,重连后发送');
|
|
1025
1050
|
} else {
|
|
1026
1051
|
flushToolBatch();
|
|
1027
|
-
appendMsg('user', 'You', text, { ts: now });
|
|
1052
|
+
appendMsg('user', 'You', text, { ts: now, forceStick: true });
|
|
1028
1053
|
const agent = currentAgent.peek() || 'claude';
|
|
1029
1054
|
const effort = currentEffort.peek()
|
|
1030
1055
|
|| getDefaultEffort(agent, currentModel.peek())
|
|
@@ -1204,7 +1229,7 @@ export function appendMsg(role, _label, text, opts = {}) {
|
|
|
1204
1229
|
msgs.insertAdjacentHTML('beforeend', bubbleHtml(role, text, ts));
|
|
1205
1230
|
enhanceMarkdown(msgs.lastElementChild);
|
|
1206
1231
|
if (role === 'assistant') addAssistantMessageActions(msgs.lastElementChild, text, opts);
|
|
1207
|
-
stickBottom(msgs,
|
|
1232
|
+
stickBottom(msgs, !!opts.forceStick);
|
|
1208
1233
|
}
|
|
1209
1234
|
|
|
1210
1235
|
export function appendSystemMsg(text) { appendMsg('system', '', text); }
|
|
@@ -1494,9 +1519,29 @@ export function appendContextBar(ctxInfo) {
|
|
|
1494
1519
|
appendTokenBar(`▣ ${parts.join(' · ')}`);
|
|
1495
1520
|
}
|
|
1496
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
|
+
|
|
1497
1542
|
export function clearMessages() {
|
|
1498
1543
|
const msgs = $('messages'), perms = $('permission-requests');
|
|
1499
|
-
if (msgs)
|
|
1544
|
+
if (msgs) { msgs.innerHTML = ''; delete msgs.dataset.historyPainting; }
|
|
1500
1545
|
if (perms) perms.innerHTML = '';
|
|
1501
1546
|
ctx.shellBubble = null;
|
|
1502
1547
|
toolBatch = null;
|
|
@@ -1517,7 +1562,7 @@ function appendQueuedGrokPrompt(text, sessionId = ctx.sessionId) {
|
|
|
1517
1562
|
const key = queuedGrokPromptKey(text, sessionId);
|
|
1518
1563
|
if (!text || renderedGrokQueuePrompts.has(key)) return;
|
|
1519
1564
|
renderedGrokQueuePrompts.add(key);
|
|
1520
|
-
appendMsg('user', 'You', text, { ts: Date.now() });
|
|
1565
|
+
appendMsg('user', 'You', text, { ts: Date.now(), forceStick: true });
|
|
1521
1566
|
$('messages')?.lastElementChild?.classList.add('queued-user-msg');
|
|
1522
1567
|
}
|
|
1523
1568
|
|
|
@@ -1756,7 +1801,7 @@ function renderPermissionRequest(msg) {
|
|
|
1756
1801
|
<button class="btn btn-error btn-xs" data-perm-id="${id}" data-perm-allow="0">Deny</button>
|
|
1757
1802
|
</div>
|
|
1758
1803
|
</div>`);
|
|
1759
|
-
$('messages')
|
|
1804
|
+
stickBottom($('messages'));
|
|
1760
1805
|
}
|
|
1761
1806
|
|
|
1762
1807
|
/** Configure marked once: GFM + code chrome + table wrap. */
|
|
@@ -1826,7 +1871,7 @@ function renderMarkdown(text) {
|
|
|
1826
1871
|
if (typeof marked === 'undefined') return esc(text);
|
|
1827
1872
|
try {
|
|
1828
1873
|
ensureMarked();
|
|
1829
|
-
return
|
|
1874
|
+
return parseMarkdownWithMath(text, { fallback: () => esc(text) });
|
|
1830
1875
|
} catch {
|
|
1831
1876
|
return esc(text);
|
|
1832
1877
|
}
|
|
@@ -1883,6 +1928,15 @@ export function initChat() {
|
|
|
1883
1928
|
|
|
1884
1929
|
watch(currentProject, () => syncWakeLock());
|
|
1885
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
|
+
|
|
1886
1940
|
delegate.on('click', '#ws-banner-retry', e => {
|
|
1887
1941
|
e.preventDefault();
|
|
1888
1942
|
refreshConnection();
|
|
@@ -2006,7 +2060,10 @@ export function initChat() {
|
|
|
2006
2060
|
new ResizeObserver(updateNav).observe(msgsEl);
|
|
2007
2061
|
new MutationObserver(updateNav).observe(msgsEl, { childList: true, subtree: true });
|
|
2008
2062
|
topBtn.addEventListener('click', () => msgsEl.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
2009
|
-
botBtn.addEventListener('click', () =>
|
|
2063
|
+
botBtn.addEventListener('click', () => {
|
|
2064
|
+
ensureMessageScroll(msgsEl)?.setFollowing(true);
|
|
2065
|
+
msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: 'smooth' });
|
|
2066
|
+
});
|
|
2010
2067
|
updateNav();
|
|
2011
2068
|
}
|
|
2012
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) {
|
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
|
+
}
|