jinzd-ai-cli 0.4.304 → 0.4.305
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/dist/{batch-RY2F7BX5.js → batch-OX27K4GG.js} +2 -2
- package/dist/{chunk-7L7T46PI.js → chunk-3ZABGT3S.js} +1 -1
- package/dist/{chunk-RWM2GFRC.js → chunk-45UVJXFQ.js} +6 -2
- package/dist/{chunk-6AGTDWO3.js → chunk-4DH5LZR5.js} +1 -1
- package/dist/{chunk-23SJVOQD.js → chunk-72AR23R2.js} +26 -5
- package/dist/{chunk-OECPGCHP.js → chunk-AXKERUXH.js} +3 -3
- package/dist/{chunk-VOIOUJXI.js → chunk-C2Z77RTU.js} +13 -5
- package/dist/{chunk-F3EJ4MIP.js → chunk-GOIDKTBE.js} +12 -11
- package/dist/{chunk-BSBGPDCX.js → chunk-I6RATYRF.js} +1 -1
- package/dist/{chunk-2PLYXL4P.js → chunk-IPUTC5EX.js} +12 -10
- package/dist/{chunk-7XU4BATV.js → chunk-KBDKKHXB.js} +15 -6
- package/dist/{chunk-4DEMGCK4.js → chunk-LF67HWWR.js} +37 -13
- package/dist/{chunk-RHCRE6XE.js → chunk-NBYSFFCO.js} +1 -1
- package/dist/{chunk-XIMM47EG.js → chunk-NFLE34QG.js} +1 -1
- package/dist/{chunk-BXP6YZ2P.js → chunk-NRSYBKWA.js} +5 -4
- package/dist/{chunk-IEQAE3QG.js → chunk-OAXLMF3U.js} +5 -4
- package/dist/{chunk-2GRGLTAK.js → chunk-PA4C452B.js} +1 -1
- package/dist/{chunk-I2WJ44D3.js → chunk-Q4DAKI7I.js} +1 -1
- package/dist/{chunk-TBTVV3KK.js → chunk-SZ4226XY.js} +1 -1
- package/dist/{chunk-3FI4TE5S.js → chunk-UFK5CTBU.js} +3 -3
- package/dist/{chunk-PPIISCFN.js → chunk-UFW4S3RU.js} +37 -13
- package/dist/{chunk-CKH4KQ4E.js → chunk-WQ3OMEC4.js} +6 -2
- package/dist/{chunk-UUSRWSSX.js → chunk-XJOK5353.js} +15 -6
- package/dist/{chunk-DNGUSTL6.js → chunk-YPOWDFOV.js} +1 -1
- package/dist/{ci-QYTLBM6Z.js → ci-IIMLOXFH.js} +4 -4
- package/dist/{ci-format-E5X27NC2.js → ci-format-FMBJC3EY.js} +2 -2
- package/dist/{constants-JDPKWZJP.js → constants-55QQ3RYE.js} +1 -1
- package/dist/{constants-F5WA5VPO.js → constants-7NVAPQVR.js} +1 -1
- package/dist/{doctor-cli-7NSEVW63.js → doctor-cli-ZHZ2XOF3.js} +4 -4
- package/dist/electron-server.js +232 -106
- package/dist/{hub-43PWTGGO.js → hub-MF53NIM2.js} +4 -4
- package/dist/{hub-server-RF6LURXY.js → hub-server-CBKCP5VD.js} +1 -1
- package/dist/index.js +24 -23
- package/dist/{indexer-RBEKMFT4.js → indexer-FPKY7LCT.js} +2 -2
- package/dist/{indexer-TNDWGFRI.js → indexer-P3FPRTPJ.js} +2 -2
- package/dist/{persistent-memory-A7RPDLR7.js → persistent-memory-LCOKUDTM.js} +2 -2
- package/dist/{persistent-memory-SHG3CJCP.js → persistent-memory-NACQW2VB.js} +2 -2
- package/dist/{pr-LW755EL7.js → pr-NHWE5FW7.js} +4 -4
- package/dist/{run-tests-MJ3SCW27.js → run-tests-EUPJLDXS.js} +2 -2
- package/dist/{run-tests-HV37CM5U.js → run-tests-ZG5X25BJ.js} +2 -2
- package/dist/{semantic-HLAE2O4F.js → semantic-7GM5M4MU.js} +3 -3
- package/dist/{semantic-VPNKXKKK.js → semantic-QOBAXEQ4.js} +3 -3
- package/dist/{server-EZ5ULLDP.js → server-PG7QCH53.js} +193 -82
- package/dist/{server-WI3Q4EDH.js → server-PNYRE3AL.js} +9 -9
- package/dist/{store-MWNHVGJT.js → store-CMSSWZF7.js} +3 -1
- package/dist/{store-VO37H6LS.js → store-SIKHCUYJ.js} +3 -1
- package/dist/{task-orchestrator-PWW5I2KE.js → task-orchestrator-WAJTCE6D.js} +9 -9
- package/dist/{usage-MAIV6XUZ.js → usage-DOES3DOS.js} +2 -2
- package/dist/{vector-store-JBAE6PS4.js → vector-store-JAXWDZYW.js} +1 -1
- package/dist/{vector-store-BBDXB5IQ.js → vector-store-PWIOQFTP.js} +1 -1
- package/dist/web/client/app.js +135 -60
- package/dist/web/client/app.js.br +0 -0
- package/dist/web/client/app.js.gz +0 -0
- package/dist/web/client/chat-input.js +38 -0
- package/dist/web/client/chat-input.js.br +0 -0
- package/dist/web/client/chat-input.js.gz +0 -0
- package/dist/web/client/index.html +1 -1
- package/dist/web/client/index.html.br +0 -0
- package/dist/web/client/index.html.gz +0 -0
- package/dist/web/client/stream-render.js +33 -0
- package/dist/web/client/style.css +2 -1
- package/dist/web/client/style.css.br +0 -0
- package/dist/web/client/style.css.gz +0 -0
- package/package.json +1 -1
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ToolRegistry
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IPUTC5EX.js";
|
|
5
5
|
import "./chunk-GHWZVPUV.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-3ZABGT3S.js";
|
|
7
|
+
import "./chunk-LF67HWWR.js";
|
|
8
|
+
import "./chunk-XJOK5353.js";
|
|
9
|
+
import "./chunk-WQ3OMEC4.js";
|
|
10
|
+
import "./chunk-NRSYBKWA.js";
|
|
11
11
|
import "./chunk-KHYD3WXE.js";
|
|
12
12
|
import "./chunk-5C76XDDF.js";
|
|
13
13
|
import "./chunk-NYNSYLFY.js";
|
|
14
14
|
import "./chunk-3FEZSLGI.js";
|
|
15
15
|
import {
|
|
16
16
|
runTool
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-Q4DAKI7I.js";
|
|
18
18
|
import "./chunk-RS7SKYTS.js";
|
|
19
19
|
import {
|
|
20
20
|
getDangerLevel,
|
|
@@ -24,10 +24,10 @@ import "./chunk-TU3L3PW7.js";
|
|
|
24
24
|
import "./chunk-HOSJZMQS.js";
|
|
25
25
|
import "./chunk-IW3Q7AE5.js";
|
|
26
26
|
import "./chunk-4BKXL7SM.js";
|
|
27
|
-
import "./chunk-
|
|
27
|
+
import "./chunk-I6RATYRF.js";
|
|
28
28
|
import {
|
|
29
29
|
VERSION
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-NBYSFFCO.js";
|
|
31
31
|
|
|
32
32
|
// src/mcp/server.ts
|
|
33
33
|
import { createInterface } from "readline";
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
loadIndex,
|
|
6
6
|
removeFile,
|
|
7
7
|
saveIndex,
|
|
8
|
+
symbolFingerprint,
|
|
8
9
|
upsertFileSymbols
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WQ3OMEC4.js";
|
|
10
11
|
import "./chunk-IW3Q7AE5.js";
|
|
11
12
|
export {
|
|
12
13
|
clearIndex,
|
|
@@ -14,5 +15,6 @@ export {
|
|
|
14
15
|
loadIndex,
|
|
15
16
|
removeFile,
|
|
16
17
|
saveIndex,
|
|
18
|
+
symbolFingerprint,
|
|
17
19
|
upsertFileSymbols
|
|
18
20
|
};
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
loadIndex,
|
|
5
5
|
removeFile,
|
|
6
6
|
saveIndex,
|
|
7
|
+
symbolFingerprint,
|
|
7
8
|
upsertFileSymbols
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-45UVJXFQ.js";
|
|
9
10
|
import "./chunk-6BUTA5VW.js";
|
|
10
11
|
export {
|
|
11
12
|
clearIndex,
|
|
@@ -13,5 +14,6 @@ export {
|
|
|
13
14
|
loadIndex,
|
|
14
15
|
removeFile,
|
|
15
16
|
saveIndex,
|
|
17
|
+
symbolFingerprint,
|
|
16
18
|
upsertFileSymbols
|
|
17
19
|
};
|
|
@@ -4,20 +4,20 @@ import {
|
|
|
4
4
|
googleSearchContext,
|
|
5
5
|
isToolCapableProvider,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IPUTC5EX.js";
|
|
8
8
|
import "./chunk-GHWZVPUV.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-3ZABGT3S.js";
|
|
10
|
+
import "./chunk-LF67HWWR.js";
|
|
11
|
+
import "./chunk-XJOK5353.js";
|
|
12
|
+
import "./chunk-WQ3OMEC4.js";
|
|
13
|
+
import "./chunk-NRSYBKWA.js";
|
|
14
14
|
import "./chunk-KHYD3WXE.js";
|
|
15
15
|
import "./chunk-5C76XDDF.js";
|
|
16
16
|
import "./chunk-NYNSYLFY.js";
|
|
17
17
|
import "./chunk-3FEZSLGI.js";
|
|
18
18
|
import {
|
|
19
19
|
runTool
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-Q4DAKI7I.js";
|
|
21
21
|
import "./chunk-RS7SKYTS.js";
|
|
22
22
|
import {
|
|
23
23
|
getDangerLevel,
|
|
@@ -29,10 +29,10 @@ import {
|
|
|
29
29
|
atomicWriteFileSync
|
|
30
30
|
} from "./chunk-IW3Q7AE5.js";
|
|
31
31
|
import "./chunk-4BKXL7SM.js";
|
|
32
|
-
import "./chunk-
|
|
32
|
+
import "./chunk-I6RATYRF.js";
|
|
33
33
|
import {
|
|
34
34
|
SUBAGENT_ALLOWED_TOOLS
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-NBYSFFCO.js";
|
|
36
36
|
|
|
37
37
|
// src/hub/task-orchestrator.ts
|
|
38
38
|
import { createInterface } from "readline";
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
import "./chunk-RS7SKYTS.js";
|
|
10
10
|
import {
|
|
11
11
|
ConfigManager
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NFLE34QG.js";
|
|
13
13
|
import "./chunk-TU3L3PW7.js";
|
|
14
14
|
import "./chunk-IW3Q7AE5.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-NBYSFFCO.js";
|
|
16
16
|
|
|
17
17
|
// src/cli/usage.ts
|
|
18
18
|
function classifyRowCost(row) {
|
package/dist/web/client/app.js
CHANGED
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
import { escapeHtml, formatToolArgs, formatDuration, sanitizeHtml, formatCostDisplay } from './util.js';
|
|
11
11
|
import { fetchSessionReplay, fetchStatus, logoutAuth, registerFirstUser, submitAuth } from './api.js';
|
|
12
|
+
import { resolveFileReferences, messageBytes, quoteFileReference } from './chat-input.js';
|
|
13
|
+
import { appendStreamText, finishStreamText, flushStreamText } from './stream-render.js';
|
|
14
|
+
|
|
15
|
+
let sendingMessage = false;
|
|
16
|
+
let retrySubmission = null;
|
|
17
|
+
let chatLimits = { maxChatBytes: 1024 * 1024, maxImageBytes: 10 * 1024 * 1024 };
|
|
18
|
+
const chatAcks = new Map();
|
|
19
|
+
let lastPong = 0;
|
|
20
|
+
let pendingPing = 0;
|
|
21
|
+
let reconnectTimer = null;
|
|
22
|
+
function scheduleReconnect() {
|
|
23
|
+
if (reconnectTimer !== null) return;
|
|
24
|
+
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, reconnectDelay);
|
|
25
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 10000);
|
|
26
|
+
}
|
|
12
27
|
|
|
13
28
|
// ── State ──────────────────────────────────────────────────────────
|
|
14
29
|
// All cross-cutting mutable state now lives in state.js (P0-4 Phase 1),
|
|
@@ -47,21 +62,35 @@ function connect() {
|
|
|
47
62
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
48
63
|
// Security: do NOT put token in URL or WebSocket JSON. Cookie is sent automatically.
|
|
49
64
|
ws = new WebSocket(`${protocol}//${location.host}?tabId=${tabId}`);
|
|
65
|
+
const socket = ws;
|
|
50
66
|
|
|
51
67
|
ws.onopen = () => {
|
|
68
|
+
if (ws !== socket) return;
|
|
69
|
+
lastPong = Date.now();
|
|
70
|
+
pendingPing = 0;
|
|
52
71
|
connected = true;
|
|
53
72
|
reconnectDelay = 1000; // Reset backoff on success
|
|
54
73
|
connectionStatus.textContent = '🟢 Connected';
|
|
55
74
|
connectionStatus.className = 'status-connected';
|
|
56
75
|
requestSessionList();
|
|
57
76
|
checkAuthStatus();
|
|
58
|
-
//
|
|
77
|
+
// Detect half-open sockets even when TCP has not delivered a close event.
|
|
59
78
|
clearInterval(heartbeatTimer);
|
|
60
79
|
heartbeatTimer = setInterval(() => {
|
|
61
|
-
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (pendingPing && now - pendingPing >= 30000) {
|
|
82
|
+
ws.close(4000, 'Heartbeat timeout');
|
|
83
|
+
connected = false;
|
|
84
|
+
connectionStatus.textContent = '🔴 Connection timed out — reconnecting...';
|
|
85
|
+
clearInterval(heartbeatTimer);
|
|
86
|
+
scheduleReconnect();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (ws && ws.readyState === WebSocket.OPEN && !pendingPing && now - lastPong >= 15000) {
|
|
90
|
+
pendingPing = now;
|
|
62
91
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
63
92
|
}
|
|
64
|
-
},
|
|
93
|
+
}, 5000);
|
|
65
94
|
// If we had a session & were processing, reset UI state
|
|
66
95
|
if (processing) {
|
|
67
96
|
setProcessing(false);
|
|
@@ -90,24 +119,28 @@ function connect() {
|
|
|
90
119
|
};
|
|
91
120
|
|
|
92
121
|
ws.onclose = () => {
|
|
122
|
+
if (ws !== socket) return;
|
|
93
123
|
connected = false;
|
|
94
124
|
clearInterval(heartbeatTimer);
|
|
125
|
+
for (const ack of chatAcks.values()) ack.reject(new Error('Disconnected before delivery was confirmed. Your draft was kept; reconnect and retry.'));
|
|
126
|
+
chatAcks.clear();
|
|
95
127
|
connectionStatus.textContent = '🔴 Disconnected — reconnecting...';
|
|
96
128
|
connectionStatus.className = 'status-disconnected';
|
|
97
129
|
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
98
|
-
|
|
99
|
-
reconnectDelay = Math.min(reconnectDelay * 2, 10000);
|
|
130
|
+
scheduleReconnect();
|
|
100
131
|
};
|
|
101
132
|
|
|
102
133
|
ws.onerror = () => {
|
|
134
|
+
if (ws !== socket) return;
|
|
103
135
|
connectionStatus.textContent = '🔴 Error';
|
|
104
136
|
connectionStatus.className = 'status-disconnected';
|
|
105
137
|
};
|
|
106
138
|
|
|
107
139
|
ws.onmessage = (event) => {
|
|
140
|
+
if (ws !== socket) return;
|
|
108
141
|
try {
|
|
109
142
|
const msg = JSON.parse(event.data);
|
|
110
|
-
if (msg.type === 'pong')
|
|
143
|
+
if (msg.type === 'pong') { lastPong = Date.now(); pendingPing = 0; return; }
|
|
111
144
|
handleServerMessage(msg);
|
|
112
145
|
} catch (e) {
|
|
113
146
|
console.error('Failed to parse message:', e);
|
|
@@ -117,8 +150,9 @@ function connect() {
|
|
|
117
150
|
|
|
118
151
|
export function send(msg) {
|
|
119
152
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
120
|
-
ws.send(JSON.stringify(msg));
|
|
153
|
+
try { ws.send(JSON.stringify(msg)); return true; } catch { return false; }
|
|
121
154
|
}
|
|
155
|
+
return false;
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
// ── Message handlers ───────────────────────────────────────────────
|
|
@@ -132,6 +166,11 @@ window.handleServerMessage = handleServerMessage;
|
|
|
132
166
|
|
|
133
167
|
function handleServerMessage(msg) {
|
|
134
168
|
switch (msg.type) {
|
|
169
|
+
case 'chat_ack': {
|
|
170
|
+
const waiting = chatAcks.get(msg.messageId);
|
|
171
|
+
if (waiting) { chatAcks.delete(msg.messageId); waiting.resolve(msg); }
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
135
174
|
case 'text_delta': handleTextDelta(msg.delta); break;
|
|
136
175
|
case 'response_done': handleResponseDone(msg); break;
|
|
137
176
|
case 'tool_call_start': handleToolCallStart(msg); break;
|
|
@@ -173,7 +212,7 @@ function handleTextDelta(delta) {
|
|
|
173
212
|
currentAssistantContent = '';
|
|
174
213
|
}
|
|
175
214
|
currentAssistantContent += delta;
|
|
176
|
-
|
|
215
|
+
appendStreamText(currentAssistantEl, delta);
|
|
177
216
|
currentAssistantEl.classList.add('streaming-cursor');
|
|
178
217
|
scrollToBottom();
|
|
179
218
|
}
|
|
@@ -181,9 +220,7 @@ function handleTextDelta(delta) {
|
|
|
181
220
|
function handleResponseDone(msg) {
|
|
182
221
|
if (currentAssistantEl) {
|
|
183
222
|
currentAssistantEl.classList.remove('streaming-cursor');
|
|
184
|
-
|
|
185
|
-
renderMarkdown(currentAssistantEl, msg.content);
|
|
186
|
-
}
|
|
223
|
+
renderMarkdown(currentAssistantEl, msg.content ?? currentAssistantContent);
|
|
187
224
|
} else if (msg.content) {
|
|
188
225
|
const el = createAssistantMessage();
|
|
189
226
|
renderMarkdown(el, msg.content);
|
|
@@ -792,6 +829,7 @@ export function addErrorMessage(text) {
|
|
|
792
829
|
}
|
|
793
830
|
|
|
794
831
|
function renderMarkdown(el, text) {
|
|
832
|
+
finishStreamText(el);
|
|
795
833
|
try {
|
|
796
834
|
// P1-SEC-01: sanitize AI-generated HTML before innerHTML insertion
|
|
797
835
|
el.innerHTML = sanitizeHtml(marked.parse(text));
|
|
@@ -802,11 +840,6 @@ function renderMarkdown(el, text) {
|
|
|
802
840
|
const btn = document.createElement('button');
|
|
803
841
|
btn.className = 'copy-code-btn btn btn-ghost btn-xs';
|
|
804
842
|
btn.textContent = '📋 Copy';
|
|
805
|
-
btn.addEventListener('click', () => {
|
|
806
|
-
navigator.clipboard.writeText(block.textContent || '');
|
|
807
|
-
btn.textContent = 'Copied';
|
|
808
|
-
setTimeout(() => btn.textContent = 'Copy', 1500);
|
|
809
|
-
});
|
|
810
843
|
pre.style.position = 'relative';
|
|
811
844
|
pre.appendChild(btn);
|
|
812
845
|
}
|
|
@@ -817,6 +850,20 @@ function renderMarkdown(el, text) {
|
|
|
817
850
|
}
|
|
818
851
|
|
|
819
852
|
// escapeHtml / formatToolArgs now live in util.js (imported at top).
|
|
853
|
+
// Delegation survives innerHTML snapshots and off-screen history restoration.
|
|
854
|
+
document.addEventListener('click', async event => {
|
|
855
|
+
const button = event.target.closest?.('.copy-code-btn');
|
|
856
|
+
if (!button) return;
|
|
857
|
+
const code = button.closest('pre')?.querySelector('code');
|
|
858
|
+
try {
|
|
859
|
+
await navigator.clipboard.writeText(code?.textContent || '');
|
|
860
|
+
button.textContent = 'Copied';
|
|
861
|
+
} catch {
|
|
862
|
+
button.textContent = 'Copy failed';
|
|
863
|
+
addInfoMessage('Clipboard unavailable. Select the code and copy it manually.');
|
|
864
|
+
}
|
|
865
|
+
setTimeout(() => { button.textContent = '📋 Copy'; }, 1500);
|
|
866
|
+
});
|
|
820
867
|
|
|
821
868
|
/**
|
|
822
869
|
* Create a static tool-call card for session history view.
|
|
@@ -861,13 +908,22 @@ function createHistoryToolCards(assistantMsg, resultMsgs) {
|
|
|
861
908
|
}
|
|
862
909
|
}
|
|
863
910
|
|
|
911
|
+
let scrollFrame = null;
|
|
864
912
|
function scrollToBottom() {
|
|
865
|
-
|
|
913
|
+
if (scrollFrame !== null) return;
|
|
914
|
+
scrollFrame = requestAnimationFrame(() => {
|
|
915
|
+
scrollFrame = null;
|
|
866
916
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
867
917
|
});
|
|
868
918
|
}
|
|
869
919
|
|
|
870
920
|
function setProcessing(value) {
|
|
921
|
+
if (!value && currentAssistantEl) {
|
|
922
|
+
renderMarkdown(currentAssistantEl, currentAssistantContent);
|
|
923
|
+
currentAssistantEl.classList.remove('streaming-cursor');
|
|
924
|
+
currentAssistantEl = null;
|
|
925
|
+
currentAssistantContent = '';
|
|
926
|
+
}
|
|
871
927
|
processing = value;
|
|
872
928
|
// Sync tab processing indicator
|
|
873
929
|
if (activeTabIdx >= 0 && activeTabIdx < sessionTabs.length) {
|
|
@@ -909,7 +965,7 @@ function updateModelSelect(providerId, currentModelId) {
|
|
|
909
965
|
|
|
910
966
|
async function sendMessage() {
|
|
911
967
|
const text = userInput.value.trim();
|
|
912
|
-
if (!text || !connected) return;
|
|
968
|
+
if (!text || !connected || sendingMessage) return;
|
|
913
969
|
|
|
914
970
|
hideFileDropdown();
|
|
915
971
|
|
|
@@ -939,46 +995,53 @@ async function sendMessage() {
|
|
|
939
995
|
return;
|
|
940
996
|
}
|
|
941
997
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
998
|
+
sendingMessage = true;
|
|
999
|
+
const images = pendingImages.slice();
|
|
1000
|
+
const originTab = sessionTabs[activeTabIdx];
|
|
1001
|
+
const originSessionId = originTab?.sessionId;
|
|
1002
|
+
try {
|
|
1003
|
+
const key = JSON.stringify({ text, images });
|
|
1004
|
+
let msg = retrySubmission?.key === key ? retrySubmission.msg : null;
|
|
1005
|
+
if (!msg) {
|
|
1006
|
+
const content = await resolveFileReferences(text, async (path, signal) => {
|
|
1007
|
+
const resp = await fetch(`/api/file-content?path=${encodeURIComponent(path)}`, { signal });
|
|
950
1008
|
const data = await resp.json();
|
|
951
|
-
if (data.content) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
addInfoMessage(`⚠ ${ref}: ${data.error}`);
|
|
956
|
-
}
|
|
957
|
-
} catch {
|
|
958
|
-
addInfoMessage(`⚠ Failed to read ${ref}`);
|
|
959
|
-
}
|
|
1009
|
+
if (!resp.ok || typeof data.content !== 'string') throw new Error(`${path}: ${data.error || 'Could not read file'}`);
|
|
1010
|
+
return data.content;
|
|
1011
|
+
}, AbortSignal.timeout(30000));
|
|
1012
|
+
msg = { type: 'chat', messageId: `${tabId}-${Date.now()}-${Math.random().toString(36).slice(2)}`, content, images };
|
|
960
1013
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1014
|
+
if (sessionTabs[activeTabIdx] !== originTab || originTab?.sessionId !== originSessionId) throw new Error('Session changed while reading files. Review your draft and retry.');
|
|
1015
|
+
if (messageBytes(msg) > chatLimits.maxChatBytes) throw new Error(`Message and attachments exceed ${(chatLimits.maxChatBytes / 1024 / 1024).toFixed(0)} MiB. Remove an attachment or shorten the message. Your draft was kept.`);
|
|
1016
|
+
retrySubmission = { key, msg };
|
|
1017
|
+
const ack = await new Promise((resolve, reject) => {
|
|
1018
|
+
const timer = setTimeout(() => { chatAcks.delete(msg.messageId); reject(new Error('Delivery was not confirmed. Your draft was kept; retry sends the same message ID.')); }, 15000);
|
|
1019
|
+
chatAcks.set(msg.messageId, {
|
|
1020
|
+
resolve: value => { clearTimeout(timer); resolve(value); },
|
|
1021
|
+
reject: error => { clearTimeout(timer); reject(error); },
|
|
1022
|
+
});
|
|
1023
|
+
if (!send(msg)) {
|
|
1024
|
+
chatAcks.get(msg.messageId).reject(new Error('Not connected. Your draft was kept.'));
|
|
1025
|
+
chatAcks.delete(msg.messageId);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
if (!ack.accepted) { retrySubmission = null; throw new Error(ack.error || 'Message was not accepted.'); }
|
|
1029
|
+
retrySubmission = null;
|
|
1030
|
+
if (!ack.duplicate) { addUserMessage(text, images); setProcessing(true); }
|
|
1031
|
+
else { send({ type: 'command', name: 'session', args: ['load', ack.sessionId] }); }
|
|
1032
|
+
if (userInput.value.trim() === text) userInput.value = '';
|
|
1033
|
+
// Do not discard attachments pasted while the original request was resolving.
|
|
1034
|
+
const remaining = pendingImages.filter(img => !images.includes(img));
|
|
1035
|
+
if (remaining.length === 0) clearImagePreview();
|
|
1036
|
+
else pendingImages = remaining;
|
|
1037
|
+
userInput.style.height = 'auto';
|
|
1038
|
+
if (inputHistory[inputHistory.length - 1] !== text) inputHistory.push(text);
|
|
966
1039
|
if (inputHistory.length > 50) inputHistory.shift();
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const msg = { type: 'chat', content: resolvedText };
|
|
973
|
-
if (pendingImages.length > 0) {
|
|
974
|
-
msg.images = pendingImages.map(img => ({ name: img.name, data: img.data, mime: img.mime }));
|
|
975
|
-
}
|
|
976
|
-
send(msg);
|
|
977
|
-
pendingImages = [];
|
|
978
|
-
clearImagePreview();
|
|
979
|
-
userInput.value = '';
|
|
980
|
-
userInput.style.height = 'auto';
|
|
981
|
-
setProcessing(true);
|
|
1040
|
+
historyIndex = -1;
|
|
1041
|
+
savedInputDraft = '';
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
addInfoMessage(`⚠ ${error.message}`);
|
|
1044
|
+
} finally { sendingMessage = false; }
|
|
982
1045
|
}
|
|
983
1046
|
|
|
984
1047
|
btnSend.addEventListener('click', sendMessage);
|
|
@@ -990,6 +1053,7 @@ btnStop.addEventListener('click', () => {
|
|
|
990
1053
|
// keydown and input handlers are in the @ file reference section above
|
|
991
1054
|
|
|
992
1055
|
btnClear.addEventListener('click', () => {
|
|
1056
|
+
if (processing || sendingMessage) { addInfoMessage('Stop the current request before clearing the conversation.'); return; }
|
|
993
1057
|
send({ type: 'command', name: 'clear', args: [] });
|
|
994
1058
|
// Keep welcome message, clear rest
|
|
995
1059
|
const welcome = messagesEl.querySelector('.chat');
|
|
@@ -1847,8 +1911,9 @@ function selectFileItem(index) {
|
|
|
1847
1911
|
fetchFiles(item.path).then(files => showFileDropdown(files));
|
|
1848
1912
|
} else {
|
|
1849
1913
|
// Insert file reference and close
|
|
1850
|
-
|
|
1851
|
-
|
|
1914
|
+
const reference = quoteFileReference(item.path);
|
|
1915
|
+
userInput.value = before + reference + ' ' + after;
|
|
1916
|
+
const newPos = before.length + reference.length + 1;
|
|
1852
1917
|
userInput.setSelectionRange(newPos, newPos);
|
|
1853
1918
|
hideFileDropdown();
|
|
1854
1919
|
}
|
|
@@ -2004,8 +2069,9 @@ function clearImagePreview() {
|
|
|
2004
2069
|
}
|
|
2005
2070
|
|
|
2006
2071
|
function addImageToPreview(file) {
|
|
2072
|
+
if (sendingMessage) { addInfoMessage('Wait for the current message to be accepted before adding attachments.'); return; }
|
|
2007
2073
|
if (!file.type.startsWith('image/')) return;
|
|
2008
|
-
if (file.size >
|
|
2074
|
+
if (file.size > chatLimits.maxImageBytes) {
|
|
2009
2075
|
addErrorMessage(`Image too large (${(file.size / 1024 / 1024).toFixed(1)} MB, max 10 MB): ${file.name}`);
|
|
2010
2076
|
return;
|
|
2011
2077
|
}
|
|
@@ -2214,7 +2280,7 @@ export function insertFileRef(filePath) {
|
|
|
2214
2280
|
const pos = userInput.selectionStart;
|
|
2215
2281
|
const before = val.slice(0, pos);
|
|
2216
2282
|
const after = val.slice(pos);
|
|
2217
|
-
const ref =
|
|
2283
|
+
const ref = quoteFileReference(filePath);
|
|
2218
2284
|
const needsSpace = after.length > 0 && !after.startsWith(' ') && !after.startsWith('\n');
|
|
2219
2285
|
userInput.value = before + ref + (needsSpace ? ' ' : '') + after;
|
|
2220
2286
|
const newPos = pos + ref.length + (needsSpace ? 1 : 0);
|
|
@@ -2272,6 +2338,7 @@ function generateTabId() {
|
|
|
2272
2338
|
|
|
2273
2339
|
/** Save current active tab's DOM state (messages, scroll, streaming state) */
|
|
2274
2340
|
function snapshotActiveTab() {
|
|
2341
|
+
flushStreamText();
|
|
2275
2342
|
if (activeTabIdx < 0 || activeTabIdx >= sessionTabs.length) return;
|
|
2276
2343
|
const tab = sessionTabs[activeTabIdx];
|
|
2277
2344
|
tab.messagesHtml = messagesEl.innerHTML;
|
|
@@ -2329,6 +2396,7 @@ function renderTabBar() {
|
|
|
2329
2396
|
|
|
2330
2397
|
/** Add a new tab. If sessionId provided, load that session; otherwise create new. */
|
|
2331
2398
|
function addTab(sessionId, title) {
|
|
2399
|
+
if (processing || sendingMessage) { addInfoMessage('Stop the current request before opening another session.'); return; }
|
|
2332
2400
|
// Snapshot current tab before adding
|
|
2333
2401
|
snapshotActiveTab();
|
|
2334
2402
|
|
|
@@ -2370,6 +2438,7 @@ function addTab(sessionId, title) {
|
|
|
2370
2438
|
|
|
2371
2439
|
/** Switch to an existing tab by index */
|
|
2372
2440
|
function switchToTab(index) {
|
|
2441
|
+
if (processing || sendingMessage) { addInfoMessage('Wait for the current send, or stop generation before switching sessions.'); return; }
|
|
2373
2442
|
if (index === activeTabIdx || index < 0 || index >= sessionTabs.length) return;
|
|
2374
2443
|
|
|
2375
2444
|
// Snapshot current
|
|
@@ -2398,6 +2467,7 @@ function switchToTab(index) {
|
|
|
2398
2467
|
|
|
2399
2468
|
/** Close a tab */
|
|
2400
2469
|
function closeTab(index) {
|
|
2470
|
+
if (processing || sendingMessage) { addInfoMessage('Stop the current request before closing a session.'); return; }
|
|
2401
2471
|
if (sessionTabs.length <= 1) return; // Must keep at least 1 tab
|
|
2402
2472
|
|
|
2403
2473
|
sessionTabs.splice(index, 1);
|
|
@@ -2577,6 +2647,7 @@ if (window.visualViewport) {
|
|
|
2577
2647
|
let initialHeight = window.visualViewport.height;
|
|
2578
2648
|
|
|
2579
2649
|
window.visualViewport.addEventListener('resize', () => {
|
|
2650
|
+
if (window.visualViewport.scale !== 1) return;
|
|
2580
2651
|
const vh = window.visualViewport.height;
|
|
2581
2652
|
const offset = window.visualViewport.offsetTop;
|
|
2582
2653
|
// When keyboard opens, viewport height shrinks significantly
|
|
@@ -2811,6 +2882,7 @@ export async function handleLogout() {
|
|
|
2811
2882
|
async function checkAuthStatus() {
|
|
2812
2883
|
try {
|
|
2813
2884
|
const data = await fetchStatus();
|
|
2885
|
+
if (data.chatLimits) chatLimits = data.chatLimits;
|
|
2814
2886
|
const btn = document.getElementById('btn-enable-auth');
|
|
2815
2887
|
if (data.auth && !data.auth.enabled && !authUsername) {
|
|
2816
2888
|
btn.classList.remove('hidden');
|
|
@@ -3212,15 +3284,17 @@ function hubEvent(ev) {
|
|
|
3212
3284
|
hubStreamEl._raw = '';
|
|
3213
3285
|
}
|
|
3214
3286
|
hubStreamEl._raw += ev.token;
|
|
3215
|
-
|
|
3287
|
+
appendStreamText(hubStreamEl.querySelector('.hub-body'), ev.token);
|
|
3216
3288
|
scrollToBottom();
|
|
3217
3289
|
break;
|
|
3218
3290
|
}
|
|
3219
3291
|
case 'agent_spoke':
|
|
3292
|
+
if (hubStreamEl) renderMarkdown(hubStreamEl.querySelector('.hub-body'), ev.message?.content ?? hubStreamEl._raw);
|
|
3220
3293
|
// Streaming already rendered the body; finalize the stream slot.
|
|
3221
3294
|
hubStreamEl = null; hubStreamRoleId = null;
|
|
3222
3295
|
break;
|
|
3223
3296
|
case 'agent_passed': {
|
|
3297
|
+
if (hubStreamEl) { finishStreamText(hubStreamEl.querySelector('.hub-body')); hubStreamEl.remove(); }
|
|
3224
3298
|
const role = hubRoles[ev.roleId] || { name: ev.roleId };
|
|
3225
3299
|
const el = document.createElement('div');
|
|
3226
3300
|
el.className = 'text-xs opacity-40 italic pl-2 my-1';
|
|
@@ -3262,6 +3336,7 @@ function hubEvent(ev) {
|
|
|
3262
3336
|
}
|
|
3263
3337
|
|
|
3264
3338
|
function hubDone(msg) {
|
|
3339
|
+
if (hubStreamEl) renderMarkdown(hubStreamEl.querySelector('.hub-body'), hubStreamEl._raw);
|
|
3265
3340
|
document.getElementById('hub-start-btn').classList.remove('hidden');
|
|
3266
3341
|
document.getElementById('hub-abort-btn').classList.add('hidden');
|
|
3267
3342
|
const status = document.getElementById('hub-status');
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Unicode paths; quote paths containing spaces, e.g. @"教案/第一课.md". */
|
|
2
|
+
export function fileReferences(text) {
|
|
3
|
+
const refs = [];
|
|
4
|
+
const pattern = /(^|\s)@(?:"([^"]+)"|'([^']+)'|([^\s<>"']+))/gu;
|
|
5
|
+
for (const match of text.matchAll(pattern)) {
|
|
6
|
+
const token = match[0].slice(match[1].length);
|
|
7
|
+
refs.push({ token, path: match[2] ?? match[3] ?? match[4], offset: match.index + match[1].length });
|
|
8
|
+
}
|
|
9
|
+
return refs;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function quoteFileReference(path) {
|
|
13
|
+
return /\s|["']/.test(path) ? `@${path.includes('"') ? `'${path}'` : `"${path}"`}` : `@${path}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveFileReferences(text, fetchFile, signal) {
|
|
17
|
+
const refs = fileReferences(text);
|
|
18
|
+
const paths = [...new Set(refs.map(ref => ref.path))];
|
|
19
|
+
const contents = new Map();
|
|
20
|
+
let next = 0;
|
|
21
|
+
await Promise.all(Array.from({ length: Math.min(4, paths.length) }, async () => {
|
|
22
|
+
while (next < paths.length) {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
const path = paths[next++];
|
|
25
|
+
contents.set(path, await fetchFile(path, signal));
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
let resolved = text;
|
|
29
|
+
for (const ref of refs.reverse()) {
|
|
30
|
+
const replacement = `[File: ${ref.path}]\n\`\`\`\n${contents.get(ref.path)}\n\`\`\``;
|
|
31
|
+
resolved = resolved.slice(0, ref.offset) + replacement + resolved.slice(ref.offset + ref.token.length);
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function messageBytes(message) {
|
|
37
|
+
return new TextEncoder().encode(JSON.stringify(message)).byteLength;
|
|
38
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en" data-theme="dark">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
6
|
<title>ai-cli Web UI</title>
|
|
7
7
|
<!-- PWA -->
|
|
8
8
|
<link rel="manifest" href="manifest.json">
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Stream text in one append per frame; format Markdown once a response completes.
|
|
2
|
+
const pending = new Map();
|
|
3
|
+
let frame = null;
|
|
4
|
+
function flush() {
|
|
5
|
+
frame = null;
|
|
6
|
+
for (const [el, state] of pending) {
|
|
7
|
+
state.node.appendData(state.delta);
|
|
8
|
+
state.delta = '';
|
|
9
|
+
if (!el.isConnected) pending.delete(el);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function appendStreamText(el, delta) {
|
|
13
|
+
let state = pending.get(el);
|
|
14
|
+
if (!state || state.node.parentNode !== el) {
|
|
15
|
+
const node = document.createTextNode('');
|
|
16
|
+
el.replaceChildren(node);
|
|
17
|
+
el.classList.add('streaming-text');
|
|
18
|
+
state = { node, delta: '' };
|
|
19
|
+
pending.set(el, state);
|
|
20
|
+
}
|
|
21
|
+
state.delta += delta;
|
|
22
|
+
if (frame === null) frame = requestAnimationFrame(flush);
|
|
23
|
+
}
|
|
24
|
+
export function finishStreamText(el) {
|
|
25
|
+
const state = pending.get(el);
|
|
26
|
+
if (state) state.node.appendData(state.delta);
|
|
27
|
+
pending.delete(el);
|
|
28
|
+
el.classList.remove('streaming-text');
|
|
29
|
+
}
|
|
30
|
+
export function flushStreamText() {
|
|
31
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
32
|
+
flush();
|
|
33
|
+
}
|
|
@@ -1050,4 +1050,5 @@ button, a, .session-item, .file-tree-row, .template-item, .tool-item, .mcp-serve
|
|
|
1050
1050
|
}
|
|
1051
1051
|
.diff-file-details .confirm-diff { margin: 0; border-radius: 0; max-height: 180px; }
|
|
1052
1052
|
.memory-meta-badges { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.35rem; }
|
|
1053
|
-
.replay-step.hidden-by-filter { display: none; }
|
|
1053
|
+
.replay-step.hidden-by-filter { display: none; }
|
|
1054
|
+
.streaming-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|