EasyAgent/edited_records.txt

4094 lines
156 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

• Edited src/ui/spinner.js (+14 -0)
16 this.leadingWritten = false;
17 + this.thinkingLineReady = false;
18 }
26 this.active = true;
27 + this.thinkingLineReady = false;
28 if (this.leadingLines > 0 && !this.leadingWritten) {
35
36 + ensureThinkingLine() {
37 + if (this.thinkingLineReady) return;
38 + process.stdout.write('\n');
39 + this.thinkingLineReady = true;
40 + readline.moveCursor(process.stdout, 0, -1);
41 + readline.cursorTo(process.stdout, 0);
42 + }
43 +
44 render() {
64 if (hasThinkingLine) {
65 + this.ensureThinkingLine();
66 const width = process.stdout.columns || 80;
102 if (hasThinkingLine) {
103 + this.ensureThinkingLine();
104 const width = process.stdout.columns || 80;
126 }
127 + this.thinkingLineReady = false;
128 }
133 this.active = false;
134 + this.thinkingLineReady = false;
135 readline.clearLine(process.stdout, 0);
• Edited src/tools/edit_file.js (+7 -0)
47 try {
48 + if (!fs.existsSync(target)) {
49 + return { success: false, error: '文件不存在' };
50 + }
51 + const stat = fs.statSync(target);
52 + if (!stat.isFile()) {
53 + return { success: false, error: '目标不是文件' };
54 + }
55 const original = fs.readFileSync(target, 'utf8');
• Edited src/ui/tool_display.js (+33 -2)
21
22 +function splitCommandLines(command) {
23 + const text = String(command ?? '');
24 + const parts = text.split(/\r?\n/);
25 + if (parts.length > 1 && parts[parts.length - 1] === '') parts.pop();
26 + return parts.length ? parts : [''];
27 +}
28 +
29 +function buildRunCommandLines(label, command) {
30 + const parts = splitCommandLines(command);
31 + const firstLine = parts[0] ?? '';
32 + const startLine = `${label} ${firstLine}`;
33 + if (parts.length <= 1) {
34 + return { startLine, finalLine: startLine };
35 + }
36 + const preview = parts.slice(1, 3);
37 + const lines = [startLine, ...preview.map((line) => ` ${line}`), ` 总指令${parts.length}行`];
38 + return { startLine, finalLine: lines.join('\n') };
39 +}
40 +
41 function buildStartLine(name, args) {
43 if (name === 'run_command') {
25 - return `${label} ${args.command} (timeout=${args.timeout}s)`;
44 + const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
45 + const { startLine } = buildRunCommandLines(label, command);
46 + return `${startLine} (timeout=${args.timeout}s)`;
47 }
70
71 +function buildFinalLine(name, args) {
72 + if (name === 'run_command') {
73 + const label = blue(toolNameMap(name));
74 + const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
75 + const { finalLine } = buildRunCommandLines(label, command);
76 + return `${finalLine} (timeout=${args.timeout}s)`;
77 + }
78 + return buildStartLine(name, args);
79 +}
80 +
81 function startToolDisplay(line) {
112 const tail = lines.slice(-5);
82 - const summary = '运行完成';
113 + const summary = '运行结果';
114 return [summary, ...tail];
• Edited src/ui/tool_display.js (+6 -6)
28
29 -function buildRunCommandLines(label, command) {
29 +function buildRunCommandLines(label, command, suffix = '') {
30 const parts = splitCommandLines(command);
31 const firstLine = parts[0] ?? '';
32 - const startLine = `${label} ${firstLine}`;
32 + const startLine = `${label} ${firstLine}${suffix}`;
33 if (parts.length <= 1) {
44 const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
45 - const { startLine } = buildRunCommandLines(label, command);
46 - return `${startLine} (timeout=${args.timeout}s)`;
45 + const { startLine } = buildRunCommandLines(label, command, ` (timeout=${args.timeout}s)`);
46 + return startLine;
47 }
74 const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
75 - const { finalLine } = buildRunCommandLines(label, command);
76 - return `${finalLine} (timeout=${args.timeout}s)`;
75 + const { finalLine } = buildRunCommandLines(label, command, ` (timeout=${args.timeout}s)`);
76 + return finalLine;
77 }
• Edited src/ui/tool_display.js (+1 -1)
194
195 -module.exports = { buildStartLine, startToolDisplay, formatResultLines, printResultLines };
195 +module.exports = { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines };
• Edited src/cli/index.js (+5 -4)
16 const { Spinner, truncateThinking } = require('../ui/spinner');
17 -const { buildStartLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
17 +const { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
18 const { createConversation, updateConversation } = require('../storage/conversation_store');
341 try { args = JSON.parse(call.function.arguments || '{}'); } catch (_) {}
342 - const line = buildStartLine(call.function.name, args);
343 - const indicator = startToolDisplay(line);
342 + const startLine = buildStartLine(call.function.name, args);
343 + const finalLine = buildFinalLine(call.function.name, args);
344 + const indicator = startToolDisplay(startLine);
345 const toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
345 - indicator.stop(line);
346 + indicator.stop(finalLine);
347 const resultLines = formatResultLines(call.function.name, args, toolResult.raw || { success: toolResult.success, error: t
oolResult.error });
• Edited src/cli/commands.js (+2 -2)
7 const { runResumeMenu } = require('../ui/resume_menu');
8 -const { buildStartLine, formatResultLines, printResultLines } = require('../ui/tool_display');
8 +const { buildStartLine, buildFinalLine, formatResultLines, printResultLines } = require('../ui/tool_display');
9 const { createIndentedWriter } = require('../ui/indented_writer');
275 }
276 - const line = buildStartLine(name, args);
276 + const line = buildFinalLine(name, args);
277 // 静态回放:直接输出完成态
• Edited src/cli/commands.js (+1 -1)
7 const { runResumeMenu } = require('../ui/resume_menu');
8 -const { buildStartLine, buildFinalLine, formatResultLines, printResultLines } = require('../ui/tool_display');
8 +const { buildFinalLine, formatResultLines, printResultLines } = require('../ui/tool_display');
9 const { createIndentedWriter } = require('../ui/indented_writer');
• Edited src/ui/tool_display.js (+56 -4)
7 const DOT_OFF = '◦';
8 +const ANSI_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
9
22
23 +function stripAnsi(text) {
24 + return String(text ?? '').replace(ANSI_REGEX, '');
25 +}
26 +
27 +function truncatePlain(text, maxCols) {
28 + const str = String(text ?? '');
29 + if (maxCols <= 0) return '';
30 + if (str.length <= maxCols) return str;
31 + if (maxCols <= 3) return str.slice(0, maxCols);
32 + return `${str.slice(0, maxCols - 3)}...`;
33 +}
34 +
35 +function truncateVisible(text, maxCols) {
36 + const str = String(text ?? '');
37 + if (maxCols <= 0) return '';
38 + if (stripAnsi(str).length <= maxCols) return str;
39 + const useEllipsis = maxCols > 3;
40 + const limit = maxCols - (useEllipsis ? 3 : 0);
41 + let out = '';
42 + let visible = 0;
43 + const ansiRegex = new RegExp(ANSI_REGEX.source, 'y');
44 + for (let i = 0; i < str.length && visible < limit; ) {
45 + ansiRegex.lastIndex = i;
46 + const match = ansiRegex.exec(str);
47 + if (match && match.index === i) {
48 + out += match[0];
49 + i += match[0].length;
50 + continue;
51 + }
52 + out += str[i];
53 + visible += 1;
54 + i += 1;
55 + }
56 + if (useEllipsis) out += '...';
57 + if (str.includes('\x1b[')) out += '\x1b[0m';
58 + return out;
59 +}
60 +
61 +function normalizePreviewLine(line) {
62 + return String(line ?? '').replace(/^\s+/, '');
63 +}
64 +
65 function splitCommandLines(command) {
72 function buildRunCommandLines(label, command, suffix = '') {
73 + const width = Number(process.stdout.columns) || 80;
74 const parts = splitCommandLines(command);
75 const firstLine = parts[0] ?? '';
32 - const startLine = `${label} ${firstLine}${suffix}`;
76 + const labelLen = stripAnsi(label).length;
77 + const suffixLen = stripAnsi(suffix).length;
78 + const available = width - labelLen - 1 - suffixLen;
79 + const commandLine = available > 0 ? truncatePlain(firstLine, available) : '';
80 + const startLine = truncateVisible(`${label} ${commandLine}${suffix}`, width);
81 if (parts.length <= 1) {
83 }
36 - const preview = parts.slice(1, 3);
37 - const lines = [startLine, ...preview.map((line) => ` ${line}`), ` 总指令${parts.length}行`];
84 + const preview = parts.slice(1, 3).map(normalizePreviewLine);
85 + const lines = [startLine, ...preview.map((line) => ` ${truncatePlain(line, Math.max(0, width - 2))}`), ` 总指令
${parts.length}行`
];
86 return { startLine, finalLine: lines.join('\n') };
130 let on = false;
131 + const rawLine = String(line ?? '').replace(/\r?\n/g, ' ');
132 function render() {
135 const dot = on ? DOT_ON : DOT_OFF;
87 - process.stdout.write(`${dot} ${line}`);
136 + const width = Number(process.stdout.columns) || 80;
137 + const maxLine = Math.max(0, width - 2);
138 + const displayLine = truncateVisible(rawLine, maxLine);
139 + process.stdout.write(`${dot} ${displayLine}`);
140 }
• Edited src/ui/tool_display.js (+17 -6)
131 const rawLine = String(line ?? '').replace(/\r?\n/g, ' ');
132 + let lastLines = 1;
133 + function clearPrevious() {
134 + if (lastLines > 1) readline.moveCursor(process.stdout, 0, -(lastLines - 1));
135 + for (let i = 0; i < lastLines; i += 1) {
136 + readline.clearLine(process.stdout, 0);
137 + if (i < lastLines - 1) readline.moveCursor(process.stdout, 0, 1);
138 + }
139 + if (lastLines > 1) readline.moveCursor(process.stdout, 0, -(lastLines - 1));
140 + readline.cursorTo(process.stdout, 0);
141 + }
142 function render() {
133 - readline.clearLine(process.stdout, 0);
134 - readline.cursorTo(process.stdout, 0);
143 + clearPrevious();
144 const dot = on ? DOT_ON : DOT_OFF;
145 const width = Number(process.stdout.columns) || 80;
137 - const maxLine = Math.max(0, width - 2);
146 + const maxLine = Math.max(0, width - 3);
147 const displayLine = truncateVisible(rawLine, maxLine);
139 - process.stdout.write(`${dot} ${displayLine}`);
148 + const lineText = `${dot} ${displayLine}`;
149 + process.stdout.write(lineText);
150 + const lineLen = stripAnsi(lineText).length;
151 + lastLines = Math.max(1, Math.ceil(lineLen / Math.max(1, width)));
152 }
160 clearInterval(timer);
149 - readline.clearLine(process.stdout, 0);
150 - readline.cursorTo(process.stdout, 0);
161 + clearPrevious();
162 process.stdout.write(`${DOT_ON} ${finalLine}\n`);
• Edited src/ui/tool_display.js (+384 -12)
8 const ANSI_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
9 +const COMBINING_RANGES = [
10 + [0x0300, 0x036f],
11 + [0x0483, 0x0489],
12 + [0x0591, 0x05bd],
13 + [0x05bf, 0x05bf],
14 + [0x05c1, 0x05c2],
15 + [0x05c4, 0x05c5],
16 + [0x05c7, 0x05c7],
17 + [0x0610, 0x061a],
18 + [0x064b, 0x065f],
19 + [0x0670, 0x0670],
20 + [0x06d6, 0x06dc],
21 + [0x06df, 0x06e4],
22 + [0x06e7, 0x06e8],
23 + [0x06ea, 0x06ed],
24 + [0x0711, 0x0711],
25 + [0x0730, 0x074a],
26 + [0x07a6, 0x07b0],
27 + [0x07eb, 0x07f3],
28 + [0x0816, 0x0819],
29 + [0x081b, 0x0823],
30 + [0x0825, 0x0827],
31 + [0x0829, 0x082d],
32 + [0x0859, 0x085b],
33 + [0x0900, 0x0902],
34 + [0x093a, 0x093a],
35 + [0x093c, 0x093c],
36 + [0x0941, 0x0948],
37 + [0x094d, 0x094d],
38 + [0x0951, 0x0957],
39 + [0x0962, 0x0963],
40 + [0x0981, 0x0981],
41 + [0x09bc, 0x09bc],
42 + [0x09c1, 0x09c4],
43 + [0x09cd, 0x09cd],
44 + [0x09e2, 0x09e3],
45 + [0x0a01, 0x0a02],
46 + [0x0a3c, 0x0a3c],
47 + [0x0a41, 0x0a42],
48 + [0x0a47, 0x0a48],
49 + [0x0a4b, 0x0a4d],
50 + [0x0a51, 0x0a51],
51 + [0x0a70, 0x0a71],
52 + [0x0a75, 0x0a75],
53 + [0x0a81, 0x0a82],
54 + [0x0abc, 0x0abc],
55 + [0x0ac1, 0x0ac5],
56 + [0x0ac7, 0x0ac8],
57 + [0x0acd, 0x0acd],
58 + [0x0ae2, 0x0ae3],
59 + [0x0b01, 0x0b01],
60 + [0x0b3c, 0x0b3c],
61 + [0x0b3f, 0x0b3f],
62 + [0x0b41, 0x0b44],
63 + [0x0b4d, 0x0b4d],
64 + [0x0b56, 0x0b56],
65 + [0x0b62, 0x0b63],
66 + [0x0b82, 0x0b82],
67 + [0x0bc0, 0x0bc0],
68 + [0x0bcd, 0x0bcd],
69 + [0x0c00, 0x0c00],
70 + [0x0c3e, 0x0c40],
71 + [0x0c46, 0x0c48],
72 + [0x0c4a, 0x0c4d],
73 + [0x0c55, 0x0c56],
74 + [0x0c62, 0x0c63],
75 + [0x0c81, 0x0c81],
76 + [0x0cbc, 0x0cbc],
77 + [0x0cbf, 0x0cbf],
78 + [0x0cc6, 0x0cc6],
79 + [0x0ccc, 0x0ccd],
80 + [0x0ce2, 0x0ce3],
81 + [0x0d00, 0x0d01],
82 + [0x0d3b, 0x0d3c],
83 + [0x0d41, 0x0d44],
84 + [0x0d4d, 0x0d4d],
85 + [0x0d62, 0x0d63],
86 + [0x0dca, 0x0dca],
87 + [0x0dd2, 0x0dd4],
88 + [0x0dd6, 0x0dd6],
89 + [0x0e31, 0x0e31],
90 + [0x0e34, 0x0e3a],
91 + [0x0e47, 0x0e4e],
92 + [0x0eb1, 0x0eb1],
93 + [0x0eb4, 0x0ebc],
94 + [0x0ec8, 0x0ecd],
95 + [0x0f18, 0x0f19],
96 + [0x0f35, 0x0f35],
97 + [0x0f37, 0x0f37],
98 + [0x0f39, 0x0f39],
99 + [0x0f71, 0x0f7e],
100 + [0x0f80, 0x0f84],
101 + [0x0f86, 0x0f87],
102 + [0x0f8d, 0x0f97],
103 + [0x0f99, 0x0fbc],
104 + [0x0fc6, 0x0fc6],
105 + [0x102d, 0x1030],
106 + [0x1032, 0x1037],
107 + [0x1039, 0x103a],
108 + [0x103d, 0x103e],
109 + [0x1058, 0x1059],
110 + [0x105e, 0x1060],
111 + [0x1071, 0x1074],
112 + [0x1082, 0x1082],
113 + [0x1085, 0x1086],
114 + [0x108d, 0x108d],
115 + [0x109d, 0x109d],
116 + [0x135d, 0x135f],
117 + [0x1712, 0x1714],
118 + [0x1732, 0x1734],
119 + [0x1752, 0x1753],
120 + [0x1772, 0x1773],
121 + [0x17b4, 0x17b5],
122 + [0x17b7, 0x17bd],
123 + [0x17c6, 0x17c6],
124 + [0x17c9, 0x17d3],
125 + [0x17dd, 0x17dd],
126 + [0x180b, 0x180d],
127 + [0x1885, 0x1886],
128 + [0x18a9, 0x18a9],
129 + [0x1920, 0x1922],
130 + [0x1927, 0x1928],
131 + [0x1932, 0x1932],
132 + [0x1939, 0x193b],
133 + [0x1a17, 0x1a18],
134 + [0x1a1b, 0x1a1b],
135 + [0x1a56, 0x1a56],
136 + [0x1a58, 0x1a5e],
137 + [0x1a60, 0x1a60],
138 + [0x1a62, 0x1a62],
139 + [0x1a65, 0x1a6c],
140 + [0x1a73, 0x1a7c],
141 + [0x1a7f, 0x1a7f],
142 + [0x1ab0, 0x1ace],
143 + [0x1b00, 0x1b03],
144 + [0x1b34, 0x1b34],
145 + [0x1b36, 0x1b3a],
146 + [0x1b3c, 0x1b3c],
147 + [0x1b42, 0x1b42],
148 + [0x1b6b, 0x1b73],
149 + [0x1b80, 0x1b81],
150 + [0x1ba2, 0x1ba5],
151 + [0x1ba8, 0x1ba9],
152 + [0x1bab, 0x1bad],
153 + [0x1be6, 0x1be6],
154 + [0x1be8, 0x1be9],
155 + [0x1bed, 0x1bed],
156 + [0x1bef, 0x1bf1],
157 + [0x1c2c, 0x1c33],
158 + [0x1c36, 0x1c37],
159 + [0x1cd0, 0x1cd2],
160 + [0x1cd4, 0x1ce0],
161 + [0x1ce2, 0x1ce8],
162 + [0x1ced, 0x1ced],
163 + [0x1cf4, 0x1cf4],
164 + [0x1cf8, 0x1cf9],
165 + [0x1dc0, 0x1df9],
166 + [0x1dfb, 0x1dff],
167 + [0x200b, 0x200f],
168 + [0x202a, 0x202e],
169 + [0x2060, 0x2064],
170 + [0x2066, 0x206f],
171 + [0x20d0, 0x20f0],
172 + [0x2cef, 0x2cf1],
173 + [0x2d7f, 0x2d7f],
174 + [0x2de0, 0x2dff],
175 + [0x302a, 0x302f],
176 + [0x3099, 0x309a],
177 + [0xa66f, 0xa672],
178 + [0xa674, 0xa67d],
179 + [0xa69e, 0xa69f],
180 + [0xa6f0, 0xa6f1],
181 + [0xa802, 0xa802],
182 + [0xa806, 0xa806],
183 + [0xa80b, 0xa80b],
184 + [0xa825, 0xa826],
185 + [0xa8c4, 0xa8c5],
186 + [0xa8e0, 0xa8f1],
187 + [0xa926, 0xa92d],
188 + [0xa947, 0xa951],
189 + [0xa980, 0xa982],
190 + [0xa9b3, 0xa9b3],
191 + [0xa9b6, 0xa9b9],
192 + [0xa9bc, 0xa9bc],
193 + [0xa9e5, 0xa9e5],
194 + [0xaa29, 0xaa2e],
195 + [0xaa31, 0xaa32],
196 + [0xaa35, 0xaa36],
197 + [0xaa43, 0xaa43],
198 + [0xaa4c, 0xaa4c],
199 + [0xaa7c, 0xaa7c],
200 + [0xaab0, 0xaab0],
201 + [0xaab2, 0xaab4],
202 + [0xaab7, 0xaab8],
203 + [0xaabe, 0xaabf],
204 + [0xaac1, 0xaac1],
205 + [0xaaec, 0xaaed],
206 + [0xaaf6, 0xaaf6],
207 + [0xabe5, 0xabe5],
208 + [0xabe8, 0xabe8],
209 + [0xabed, 0xabed],
210 + [0xfb1e, 0xfb1e],
211 + [0xfe00, 0xfe0f],
212 + [0xfe20, 0xfe2f],
213 + [0xfeff, 0xfeff],
214 + [0xfff9, 0xfffb],
215 + [0x101fd, 0x101fd],
216 + [0x102e0, 0x102e0],
217 + [0x10376, 0x1037a],
218 + [0x10a01, 0x10a03],
219 + [0x10a05, 0x10a06],
220 + [0x10a0c, 0x10a0f],
221 + [0x10a38, 0x10a3a],
222 + [0x10a3f, 0x10a3f],
223 + [0x10ae5, 0x10ae6],
224 + [0x10d24, 0x10d27],
225 + [0x10eab, 0x10eac],
226 + [0x10f46, 0x10f50],
227 + [0x11001, 0x11001],
228 + [0x11038, 0x11046],
229 + [0x1107f, 0x11081],
230 + [0x110b3, 0x110b6],
231 + [0x110b9, 0x110ba],
232 + [0x110c2, 0x110c2],
233 + [0x11100, 0x11102],
234 + [0x11127, 0x1112b],
235 + [0x1112d, 0x11134],
236 + [0x11173, 0x11173],
237 + [0x11180, 0x11181],
238 + [0x111b6, 0x111be],
239 + [0x111c9, 0x111cc],
240 + [0x1122f, 0x11231],
241 + [0x11234, 0x11234],
242 + [0x11236, 0x11237],
243 + [0x1123e, 0x1123e],
244 + [0x112df, 0x112df],
245 + [0x112e3, 0x112ea],
246 + [0x11300, 0x11301],
247 + [0x1133b, 0x1133c],
248 + [0x11340, 0x11340],
249 + [0x11366, 0x1136c],
250 + [0x11370, 0x11374],
251 + [0x11438, 0x1143f],
252 + [0x11442, 0x11444],
253 + [0x11446, 0x11446],
254 + [0x1145e, 0x1145e],
255 + [0x114b3, 0x114b8],
256 + [0x114ba, 0x114ba],
257 + [0x114bf, 0x114c0],
258 + [0x114c2, 0x114c3],
259 + [0x115b2, 0x115b5],
260 + [0x115bc, 0x115bd],
261 + [0x115bf, 0x115c0],
262 + [0x115dc, 0x115dd],
263 + [0x11633, 0x1163a],
264 + [0x1163d, 0x1163d],
265 + [0x1163f, 0x11640],
266 + [0x116ab, 0x116ab],
267 + [0x116ad, 0x116ad],
268 + [0x116b0, 0x116b5],
269 + [0x116b7, 0x116b7],
270 + [0x1171d, 0x1171f],
271 + [0x11722, 0x11725],
272 + [0x11727, 0x1172b],
273 + [0x1182f, 0x11837],
274 + [0x11839, 0x1183a],
275 + [0x1193b, 0x1193c],
276 + [0x1193e, 0x1193e],
277 + [0x11943, 0x11943],
278 + [0x119d4, 0x119d7],
279 + [0x119da, 0x119db],
280 + [0x119e0, 0x119e0],
281 + [0x11a01, 0x11a0a],
282 + [0x11a33, 0x11a38],
283 + [0x11a3b, 0x11a3e],
284 + [0x11a47, 0x11a47],
285 + [0x11a51, 0x11a56],
286 + [0x11a59, 0x11a5b],
287 + [0x11a8a, 0x11a96],
288 + [0x11a98, 0x11a99],
289 + [0x11c30, 0x11c36],
290 + [0x11c38, 0x11c3d],
291 + [0x11c3f, 0x11c3f],
292 + [0x11c92, 0x11ca7],
293 + [0x11caa, 0x11cb0],
294 + [0x11cb2, 0x11cb3],
295 + [0x11cb5, 0x11cb6],
296 + [0x11d31, 0x11d36],
297 + [0x11d3a, 0x11d3a],
298 + [0x11d3c, 0x11d3d],
299 + [0x11d3f, 0x11d45],
300 + [0x11d47, 0x11d47],
301 + [0x11d90, 0x11d91],
302 + [0x11ef3, 0x11ef4],
303 + [0x16af0, 0x16af4],
304 + [0x16b30, 0x16b36],
305 + [0x16f4f, 0x16f4f],
306 + [0x16f8f, 0x16f92],
307 + [0x16fe4, 0x16fe4],
308 + [0x1bc9d, 0x1bc9e],
309 + [0x1cf00, 0x1cf2d],
310 + [0x1cf30, 0x1cf46],
311 + [0x1d167, 0x1d169],
312 + [0x1d17b, 0x1d182],
313 + [0x1d185, 0x1d18b],
314 + [0x1d1aa, 0x1d1ad],
315 + [0x1d242, 0x1d244],
316 + [0xe0100, 0xe01ef],
317 +];
318
335
336 +function isCombining(codePoint) {
337 + for (const [start, end] of COMBINING_RANGES) {
338 + if (codePoint >= start && codePoint <= end) return true;
339 + }
340 + return false;
341 +}
342 +
343 +function isFullWidth(codePoint) {
344 + if (codePoint >= 0x1100 && (
345 + codePoint <= 0x115f ||
346 + codePoint === 0x2329 ||
347 + codePoint === 0x232a ||
348 + (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
349 + (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
350 + (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
351 + (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
352 + (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
353 + (codePoint >= 0xff00 && codePoint <= 0xff60) ||
354 + (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
355 + (codePoint >= 0x20000 && codePoint <= 0x3fffd)
356 + )) {
357 + return true;
358 + }
359 + return false;
360 +}
361 +
362 +function visibleWidth(text) {
363 + const str = stripAnsi(text);
364 + let width = 0;
365 + for (let i = 0; i < str.length; i += 1) {
366 + const codePoint = str.codePointAt(i);
367 + if (codePoint > 0xffff) i += 1;
368 + if (codePoint === 0) continue;
369 + if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
370 + if (isCombining(codePoint)) continue;
371 + width += isFullWidth(codePoint) ? 2 : 1;
372 + }
373 + return width;
374 +}
375 +
376 function truncatePlain(text, maxCols) {
378 if (maxCols <= 0) return '';
30 - if (str.length <= maxCols) return str;
31 - if (maxCols <= 3) return str.slice(0, maxCols);
32 - return `${str.slice(0, maxCols - 3)}...`;
379 + if (visibleWidth(str) <= maxCols) return str;
380 + const useEllipsis = maxCols > 3;
381 + const limit = maxCols - (useEllipsis ? 3 : 0);
382 + let out = '';
383 + let width = 0;
384 + for (let i = 0; i < str.length && width < limit; i += 1) {
385 + const codePoint = str.codePointAt(i);
386 + if (codePoint > 0xffff) i += 1;
387 + if (codePoint === 0) continue;
388 + if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
389 + if (isCombining(codePoint)) continue;
390 + const nextWidth = isFullWidth(codePoint) ? 2 : 1;
391 + if (width + nextWidth > limit) break;
392 + out += String.fromCodePoint(codePoint);
393 + width += nextWidth;
394 + }
395 + if (useEllipsis) out += '...';
396 + return out;
397 }
401 if (maxCols <= 0) return '';
38 - if (stripAnsi(str).length <= maxCols) return str;
402 + if (visibleWidth(str) <= maxCols) return str;
403 const useEllipsis = maxCols > 3;
405 let out = '';
42 - let visible = 0;
406 + let width = 0;
407 const ansiRegex = new RegExp(ANSI_REGEX.source, 'y');
44 - for (let i = 0; i < str.length && visible < limit; ) {
408 + for (let i = 0; i < str.length && width < limit; ) {
409 ansiRegex.lastIndex = i;
415 }
52 - out += str[i];
53 - visible += 1;
54 - i += 1;
416 + const codePoint = str.codePointAt(i);
417 + const char = String.fromCodePoint(codePoint);
418 + if (codePoint > 0xffff) i += 2;
419 + else i += 1;
420 + if (codePoint === 0) continue;
421 + if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
422 + if (isCombining(codePoint)) continue;
423 + const nextWidth = isFullWidth(codePoint) ? 2 : 1;
424 + if (width + nextWidth > limit) break;
425 + out += char;
426 + width += nextWidth;
427 }
447 const firstLine = parts[0] ?? '';
76 - const labelLen = stripAnsi(label).length;
77 - const suffixLen = stripAnsi(suffix).length;
448 + const labelLen = visibleWidth(label);
449 + const suffixLen = visibleWidth(suffix);
450 const available = width - labelLen - 1 - suffixLen;
521 process.stdout.write(lineText);
150 - const lineLen = stripAnsi(lineText).length;
522 + const lineLen = visibleWidth(lineText);
523 lastLines = Math.max(1, Math.ceil(lineLen / Math.max(1, width)));
• Edited prompts/system.txt (+1 -0)
2 目标:帮助用户完成开发任务,优先高信息密度输出。
3 +输出限制:禁止使用 Markdownmd格式内容无法渲染必须使用纯文字格式输出。
• Edited src/core/context.js (+16 -11)
34 const systemName = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux';
35 - const lines = [
36 - basePrompt.trim(),
37 - '',
38 - `时间: ${now.toISOString()}`,
39 - `工作区: ${opts.workspace}`,
40 - `系统: ${systemName} (${os.release()})`,
41 - `终端: ${getTerminalType()}`,
42 - `权限: ${opts.allowMode}`,
43 - `Git: ${getGitInfo(opts.workspace)}`,
44 - ];
45 - return lines.join('\n');
35 + const replacements = {
36 + current_time: now.toISOString(),
37 + path: opts.workspace,
38 + workspace: opts.workspace,
39 + system: `${systemName} (${os.release()})`,
40 + terminal: getTerminalType(),
41 + allow_mode: opts.allowMode,
42 + permissions: opts.allowMode,
43 + git: getGitInfo(opts.workspace),
44 + };
45 + let prompt = basePrompt;
46 + for (const [key, value] of Object.entries(replacements)) {
47 + const token = `{${key}}`;
48 + prompt = prompt.split(token).join(String(value));
49 + }
50 + return prompt.trim();
51 }
• Edited prompts/system.txt (+7 -0)
3 输出限制:禁止使用 Markdownmd格式内容无法渲染必须使用纯文字格式输出。
4 +
5 +- 当前时间:{current_time}
6 +- 工作区路径:{path}
7 +- 系统信息:{system}
8 +- 终端类型:{terminal}
9 +- 权限:{allow_mode}
10 +- Git{git}
• Edited src/core/context.js (+4 -1)
32 const now = new Date();
33 + const tzOffset = now.getTimezoneOffset();
34 + const localMs = now.getTime() - tzOffset * 60 * 1000;
35 + const localIso = new Date(localMs).toISOString().slice(0, 16);
36 const platform = os.platform();
38 const replacements = {
36 - current_time: now.toISOString(),
39 + current_time: localIso,
40 path: opts.workspace,
• Edited prompts/system.txt (+1 -0)
9 - 权限:{allow_mode}
10 +- 只读限制说明:{allow_note}
11 - Git{git}
• Edited src/core/context.js (+14 -1)
35 const localIso = new Date(localMs).toISOString().slice(0, 16);
36 + const allowNote = opts.allowMode === 'read_only'
37 + ? '已禁用 edit_file、run_command。若用户要求使用请告知当前无权限需要请求切换权限。'
38 + : '';
39 const platform = os.platform();
40 const systemName = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux';
41 + let prompt = basePrompt;
42 + if (allowNote) {
43 + prompt = prompt.split('{allow_note}').join(allowNote);
44 + } else {
45 + const marker = '__ALLOW_NOTE__';
46 + prompt = prompt.split('{allow_note}').join(marker);
47 + prompt = prompt
48 + .split('\n')
49 + .filter((line) => !line.includes(marker))
50 + .join('\n');
51 + }
52 const replacements = {
61 };
48 - let prompt = basePrompt;
62 for (const [key, value] of Object.entries(replacements)) {
• Edited src/tools/dispatcher.js (+3 -2)
155 if (allowMode === 'read_only' && (name === 'edit_file' || name === 'run_command')) {
156 + const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用请先请求切换权限。';
157 return {
159 tool: name,
159 - error: '当前为只读模式,禁止修改文件或运行指令',
160 - formatted: '当前为只读模式,禁止修改文件或运行指令',
160 + error: note,
161 + formatted: note,
162 };
• Edited prompts/system.txt (+0 -1)
9 - 权限:{allow_mode}
10 -- 只读限制说明:{allow_note}
10 - Git{git}
• Edited src/core/context.js (+5 -15)
35 const localIso = new Date(localMs).toISOString().slice(0, 16);
36 - const allowNote = opts.allowMode === 'read_only'
37 - ? '已禁用 edit_file、run_command。若用户要求使用请告知当前无权限需要请求切换权限。'
38 - : '';
36 const platform = os.platform();
38 let prompt = basePrompt;
42 - if (allowNote) {
43 - prompt = prompt.split('{allow_note}').join(allowNote);
44 - } else {
45 - const marker = '__ALLOW_NOTE__';
46 - prompt = prompt.split('{allow_note}').join(marker);
47 - prompt = prompt
48 - .split('\n')
49 - .filter((line) => !line.includes(marker))
50 - .join('\n');
51 - }
39 + const allowModeValue = opts.allowMode === 'read_only'
40 + ? `${opts.allowMode}\n 已禁用 edit_file、run_command。若用户要求使用请告知当前无权限需要请求切换权限。`
41 + : opts.allowMode;
42 const replacements = {
47 terminal: getTerminalType(),
58 - allow_mode: opts.allowMode,
59 - permissions: opts.allowMode,
48 + allow_mode: allowModeValue,
49 + permissions: allowModeValue,
50 git: getGitInfo(opts.workspace),
• Edited src/ui/select_prompt.js (+3 -1)
25 },
26 - });
26 + }, { clearPromptOnDone: true });
27 + readline.clearLine(process.stdout, 0);
28 + readline.cursorTo(process.stdout, 0);
29 return value;
• Edited src/ui/tool_display.js (+1 -410)
7 const DOT_OFF = '◦';
8 -const ANSI_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
9 -const COMBINING_RANGES = [
10 - [0x0300, 0x036f],
11 - [0x0483, 0x0489],
12 - [0x0591, 0x05bd],
13 - [0x05bf, 0x05bf],
14 - [0x05c1, 0x05c2],
15 - [0x05c4, 0x05c5],
16 - [0x05c7, 0x05c7],
17 - [0x0610, 0x061a],
18 - [0x064b, 0x065f],
19 - [0x0670, 0x0670],
20 - [0x06d6, 0x06dc],
21 - [0x06df, 0x06e4],
22 - [0x06e7, 0x06e8],
23 - [0x06ea, 0x06ed],
24 - [0x0711, 0x0711],
25 - [0x0730, 0x074a],
26 - [0x07a6, 0x07b0],
27 - [0x07eb, 0x07f3],
28 - [0x0816, 0x0819],
29 - [0x081b, 0x0823],
30 - [0x0825, 0x0827],
31 - [0x0829, 0x082d],
32 - [0x0859, 0x085b],
33 - [0x0900, 0x0902],
34 - [0x093a, 0x093a],
35 - [0x093c, 0x093c],
36 - [0x0941, 0x0948],
37 - [0x094d, 0x094d],
38 - [0x0951, 0x0957],
39 - [0x0962, 0x0963],
40 - [0x0981, 0x0981],
41 - [0x09bc, 0x09bc],
42 - [0x09c1, 0x09c4],
43 - [0x09cd, 0x09cd],
44 - [0x09e2, 0x09e3],
45 - [0x0a01, 0x0a02],
46 - [0x0a3c, 0x0a3c],
47 - [0x0a41, 0x0a42],
48 - [0x0a47, 0x0a48],
49 - [0x0a4b, 0x0a4d],
50 - [0x0a51, 0x0a51],
51 - [0x0a70, 0x0a71],
52 - [0x0a75, 0x0a75],
53 - [0x0a81, 0x0a82],
54 - [0x0abc, 0x0abc],
55 - [0x0ac1, 0x0ac5],
56 - [0x0ac7, 0x0ac8],
57 - [0x0acd, 0x0acd],
58 - [0x0ae2, 0x0ae3],
59 - [0x0b01, 0x0b01],
60 - [0x0b3c, 0x0b3c],
61 - [0x0b3f, 0x0b3f],
62 - [0x0b41, 0x0b44],
63 - [0x0b4d, 0x0b4d],
64 - [0x0b56, 0x0b56],
65 - [0x0b62, 0x0b63],
66 - [0x0b82, 0x0b82],
67 - [0x0bc0, 0x0bc0],
68 - [0x0bcd, 0x0bcd],
69 - [0x0c00, 0x0c00],
70 - [0x0c3e, 0x0c40],
71 - [0x0c46, 0x0c48],
72 - [0x0c4a, 0x0c4d],
73 - [0x0c55, 0x0c56],
74 - [0x0c62, 0x0c63],
75 - [0x0c81, 0x0c81],
76 - [0x0cbc, 0x0cbc],
77 - [0x0cbf, 0x0cbf],
78 - [0x0cc6, 0x0cc6],
79 - [0x0ccc, 0x0ccd],
80 - [0x0ce2, 0x0ce3],
81 - [0x0d00, 0x0d01],
82 - [0x0d3b, 0x0d3c],
83 - [0x0d41, 0x0d44],
84 - [0x0d4d, 0x0d4d],
85 - [0x0d62, 0x0d63],
86 - [0x0dca, 0x0dca],
87 - [0x0dd2, 0x0dd4],
88 - [0x0dd6, 0x0dd6],
89 - [0x0e31, 0x0e31],
90 - [0x0e34, 0x0e3a],
91 - [0x0e47, 0x0e4e],
92 - [0x0eb1, 0x0eb1],
93 - [0x0eb4, 0x0ebc],
94 - [0x0ec8, 0x0ecd],
95 - [0x0f18, 0x0f19],
96 - [0x0f35, 0x0f35],
97 - [0x0f37, 0x0f37],
98 - [0x0f39, 0x0f39],
99 - [0x0f71, 0x0f7e],
100 - [0x0f80, 0x0f84],
101 - [0x0f86, 0x0f87],
102 - [0x0f8d, 0x0f97],
103 - [0x0f99, 0x0fbc],
104 - [0x0fc6, 0x0fc6],
105 - [0x102d, 0x1030],
106 - [0x1032, 0x1037],
107 - [0x1039, 0x103a],
108 - [0x103d, 0x103e],
109 - [0x1058, 0x1059],
110 - [0x105e, 0x1060],
111 - [0x1071, 0x1074],
112 - [0x1082, 0x1082],
113 - [0x1085, 0x1086],
114 - [0x108d, 0x108d],
115 - [0x109d, 0x109d],
116 - [0x135d, 0x135f],
117 - [0x1712, 0x1714],
118 - [0x1732, 0x1734],
119 - [0x1752, 0x1753],
120 - [0x1772, 0x1773],
121 - [0x17b4, 0x17b5],
122 - [0x17b7, 0x17bd],
123 - [0x17c6, 0x17c6],
124 - [0x17c9, 0x17d3],
125 - [0x17dd, 0x17dd],
126 - [0x180b, 0x180d],
127 - [0x1885, 0x1886],
128 - [0x18a9, 0x18a9],
129 - [0x1920, 0x1922],
130 - [0x1927, 0x1928],
131 - [0x1932, 0x1932],
132 - [0x1939, 0x193b],
133 - [0x1a17, 0x1a18],
134 - [0x1a1b, 0x1a1b],
135 - [0x1a56, 0x1a56],
136 - [0x1a58, 0x1a5e],
137 - [0x1a60, 0x1a60],
138 - [0x1a62, 0x1a62],
139 - [0x1a65, 0x1a6c],
140 - [0x1a73, 0x1a7c],
141 - [0x1a7f, 0x1a7f],
142 - [0x1ab0, 0x1ace],
143 - [0x1b00, 0x1b03],
144 - [0x1b34, 0x1b34],
145 - [0x1b36, 0x1b3a],
146 - [0x1b3c, 0x1b3c],
147 - [0x1b42, 0x1b42],
148 - [0x1b6b, 0x1b73],
149 - [0x1b80, 0x1b81],
150 - [0x1ba2, 0x1ba5],
151 - [0x1ba8, 0x1ba9],
152 - [0x1bab, 0x1bad],
153 - [0x1be6, 0x1be6],
154 - [0x1be8, 0x1be9],
155 - [0x1bed, 0x1bed],
156 - [0x1bef, 0x1bf1],
157 - [0x1c2c, 0x1c33],
158 - [0x1c36, 0x1c37],
159 - [0x1cd0, 0x1cd2],
160 - [0x1cd4, 0x1ce0],
161 - [0x1ce2, 0x1ce8],
162 - [0x1ced, 0x1ced],
163 - [0x1cf4, 0x1cf4],
164 - [0x1cf8, 0x1cf9],
165 - [0x1dc0, 0x1df9],
166 - [0x1dfb, 0x1dff],
167 - [0x200b, 0x200f],
168 - [0x202a, 0x202e],
169 - [0x2060, 0x2064],
170 - [0x2066, 0x206f],
171 - [0x20d0, 0x20f0],
172 - [0x2cef, 0x2cf1],
173 - [0x2d7f, 0x2d7f],
174 - [0x2de0, 0x2dff],
175 - [0x302a, 0x302f],
176 - [0x3099, 0x309a],
177 - [0xa66f, 0xa672],
178 - [0xa674, 0xa67d],
179 - [0xa69e, 0xa69f],
180 - [0xa6f0, 0xa6f1],
181 - [0xa802, 0xa802],
182 - [0xa806, 0xa806],
183 - [0xa80b, 0xa80b],
184 - [0xa825, 0xa826],
185 - [0xa8c4, 0xa8c5],
186 - [0xa8e0, 0xa8f1],
187 - [0xa926, 0xa92d],
188 - [0xa947, 0xa951],
189 - [0xa980, 0xa982],
190 - [0xa9b3, 0xa9b3],
191 - [0xa9b6, 0xa9b9],
192 - [0xa9bc, 0xa9bc],
193 - [0xa9e5, 0xa9e5],
194 - [0xaa29, 0xaa2e],
195 - [0xaa31, 0xaa32],
196 - [0xaa35, 0xaa36],
197 - [0xaa43, 0xaa43],
198 - [0xaa4c, 0xaa4c],
199 - [0xaa7c, 0xaa7c],
200 - [0xaab0, 0xaab0],
201 - [0xaab2, 0xaab4],
202 - [0xaab7, 0xaab8],
203 - [0xaabe, 0xaabf],
204 - [0xaac1, 0xaac1],
205 - [0xaaec, 0xaaed],
206 - [0xaaf6, 0xaaf6],
207 - [0xabe5, 0xabe5],
208 - [0xabe8, 0xabe8],
209 - [0xabed, 0xabed],
210 - [0xfb1e, 0xfb1e],
211 - [0xfe00, 0xfe0f],
212 - [0xfe20, 0xfe2f],
213 - [0xfeff, 0xfeff],
214 - [0xfff9, 0xfffb],
215 - [0x101fd, 0x101fd],
216 - [0x102e0, 0x102e0],
217 - [0x10376, 0x1037a],
218 - [0x10a01, 0x10a03],
219 - [0x10a05, 0x10a06],
220 - [0x10a0c, 0x10a0f],
221 - [0x10a38, 0x10a3a],
222 - [0x10a3f, 0x10a3f],
223 - [0x10ae5, 0x10ae6],
224 - [0x10d24, 0x10d27],
225 - [0x10eab, 0x10eac],
226 - [0x10f46, 0x10f50],
227 - [0x11001, 0x11001],
228 - [0x11038, 0x11046],
229 - [0x1107f, 0x11081],
230 - [0x110b3, 0x110b6],
231 - [0x110b9, 0x110ba],
232 - [0x110c2, 0x110c2],
233 - [0x11100, 0x11102],
234 - [0x11127, 0x1112b],
235 - [0x1112d, 0x11134],
236 - [0x11173, 0x11173],
237 - [0x11180, 0x11181],
238 - [0x111b6, 0x111be],
239 - [0x111c9, 0x111cc],
240 - [0x1122f, 0x11231],
241 - [0x11234, 0x11234],
242 - [0x11236, 0x11237],
243 - [0x1123e, 0x1123e],
244 - [0x112df, 0x112df],
245 - [0x112e3, 0x112ea],
246 - [0x11300, 0x11301],
247 - [0x1133b, 0x1133c],
248 - [0x11340, 0x11340],
249 - [0x11366, 0x1136c],
250 - [0x11370, 0x11374],
251 - [0x11438, 0x1143f],
252 - [0x11442, 0x11444],
253 - [0x11446, 0x11446],
254 - [0x1145e, 0x1145e],
255 - [0x114b3, 0x114b8],
256 - [0x114ba, 0x114ba],
257 - [0x114bf, 0x114c0],
258 - [0x114c2, 0x114c3],
259 - [0x115b2, 0x115b5],
260 - [0x115bc, 0x115bd],
261 - [0x115bf, 0x115c0],
262 - [0x115dc, 0x115dd],
263 - [0x11633, 0x1163a],
264 - [0x1163d, 0x1163d],
265 - [0x1163f, 0x11640],
266 - [0x116ab, 0x116ab],
267 - [0x116ad, 0x116ad],
268 - [0x116b0, 0x116b5],
269 - [0x116b7, 0x116b7],
270 - [0x1171d, 0x1171f],
271 - [0x11722, 0x11725],
272 - [0x11727, 0x1172b],
273 - [0x1182f, 0x11837],
274 - [0x11839, 0x1183a],
275 - [0x1193b, 0x1193c],
276 - [0x1193e, 0x1193e],
277 - [0x11943, 0x11943],
278 - [0x119d4, 0x119d7],
279 - [0x119da, 0x119db],
280 - [0x119e0, 0x119e0],
281 - [0x11a01, 0x11a0a],
282 - [0x11a33, 0x11a38],
283 - [0x11a3b, 0x11a3e],
284 - [0x11a47, 0x11a47],
285 - [0x11a51, 0x11a56],
286 - [0x11a59, 0x11a5b],
287 - [0x11a8a, 0x11a96],
288 - [0x11a98, 0x11a99],
289 - [0x11c30, 0x11c36],
290 - [0x11c38, 0x11c3d],
291 - [0x11c3f, 0x11c3f],
292 - [0x11c92, 0x11ca7],
293 - [0x11caa, 0x11cb0],
294 - [0x11cb2, 0x11cb3],
295 - [0x11cb5, 0x11cb6],
296 - [0x11d31, 0x11d36],
297 - [0x11d3a, 0x11d3a],
298 - [0x11d3c, 0x11d3d],
299 - [0x11d3f, 0x11d45],
300 - [0x11d47, 0x11d47],
301 - [0x11d90, 0x11d91],
302 - [0x11ef3, 0x11ef4],
303 - [0x16af0, 0x16af4],
304 - [0x16b30, 0x16b36],
305 - [0x16f4f, 0x16f4f],
306 - [0x16f8f, 0x16f92],
307 - [0x16fe4, 0x16fe4],
308 - [0x1bc9d, 0x1bc9e],
309 - [0x1cf00, 0x1cf2d],
310 - [0x1cf30, 0x1cf46],
311 - [0x1d167, 0x1d169],
312 - [0x1d17b, 0x1d182],
313 - [0x1d185, 0x1d18b],
314 - [0x1d1aa, 0x1d1ad],
315 - [0x1d242, 0x1d244],
316 - [0xe0100, 0xe01ef],
317 -];
8 +const { stripAnsi, visibleWidth, truncatePlain, truncateVisible } = require('../utils/text_width');
9
20 return map[name] || name;
330 -}
331 -
332 -function stripAnsi(text) {
333 - return String(text ?? '').replace(ANSI_REGEX, '');
334 -}
335 -
336 -function isCombining(codePoint) {
337 - for (const [start, end] of COMBINING_RANGES) {
338 - if (codePoint >= start && codePoint <= end) return true;
339 - }
340 - return false;
341 -}
342 -
343 -function isFullWidth(codePoint) {
344 - if (codePoint >= 0x1100 && (
345 - codePoint <= 0x115f ||
346 - codePoint === 0x2329 ||
347 - codePoint === 0x232a ||
348 - (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
349 - (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
350 - (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
351 - (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
352 - (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
353 - (codePoint >= 0xff00 && codePoint <= 0xff60) ||
354 - (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
355 - (codePoint >= 0x20000 && codePoint <= 0x3fffd)
356 - )) {
357 - return true;
358 - }
359 - return false;
360 -}
361 -
362 -function visibleWidth(text) {
363 - const str = stripAnsi(text);
364 - let width = 0;
365 - for (let i = 0; i < str.length; i += 1) {
366 - const codePoint = str.codePointAt(i);
367 - if (codePoint > 0xffff) i += 1;
368 - if (codePoint === 0) continue;
369 - if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
370 - if (isCombining(codePoint)) continue;
371 - width += isFullWidth(codePoint) ? 2 : 1;
372 - }
373 - return width;
374 -}
375 -
376 -function truncatePlain(text, maxCols) {
377 - const str = String(text ?? '');
378 - if (maxCols <= 0) return '';
379 - if (visibleWidth(str) <= maxCols) return str;
380 - const useEllipsis = maxCols > 3;
381 - const limit = maxCols - (useEllipsis ? 3 : 0);
382 - let out = '';
383 - let width = 0;
384 - for (let i = 0; i < str.length && width < limit; i += 1) {
385 - const codePoint = str.codePointAt(i);
386 - if (codePoint > 0xffff) i += 1;
387 - if (codePoint === 0) continue;
388 - if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
389 - if (isCombining(codePoint)) continue;
390 - const nextWidth = isFullWidth(codePoint) ? 2 : 1;
391 - if (width + nextWidth > limit) break;
392 - out += String.fromCodePoint(codePoint);
393 - width += nextWidth;
394 - }
395 - if (useEllipsis) out += '...';
396 - return out;
21 }
22
399 -function truncateVisible(text, maxCols) {
400 - const str = String(text ?? '');
401 - if (maxCols <= 0) return '';
402 - if (visibleWidth(str) <= maxCols) return str;
403 - const useEllipsis = maxCols > 3;
404 - const limit = maxCols - (useEllipsis ? 3 : 0);
405 - let out = '';
406 - let width = 0;
407 - const ansiRegex = new RegExp(ANSI_REGEX.source, 'y');
408 - for (let i = 0; i < str.length && width < limit; ) {
409 - ansiRegex.lastIndex = i;
410 - const match = ansiRegex.exec(str);
411 - if (match && match.index === i) {
412 - out += match[0];
413 - i += match[0].length;
414 - continue;
415 - }
416 - const codePoint = str.codePointAt(i);
417 - const char = String.fromCodePoint(codePoint);
418 - if (codePoint > 0xffff) i += 2;
419 - else i += 1;
420 - if (codePoint === 0) continue;
421 - if (codePoint < 32 || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
422 - if (isCombining(codePoint)) continue;
423 - const nextWidth = isFullWidth(codePoint) ? 2 : 1;
424 - if (width + nextWidth > limit) break;
425 - out += char;
426 - width += nextWidth;
427 - }
428 - if (useEllipsis) out += '...';
429 - if (str.includes('\x1b[')) out += '\x1b[0m';
430 - return out;
431 -}
23
• Edited src/cli/index.js (+6 -4)
16 const { Spinner, truncateThinking } = require('../ui/spinner');
17 +const { renderBanner } = require('../ui/banner');
18 const { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
41
41 -console.log('');
42 -console.log('eagent');
43 -console.log(`model: ${state.modelKey} | allow: ${state.allowMode} | cwd: ${WORKSPACE_NAME}`);
44 -console.log('');
42 +renderBanner({
43 + modelKey: state.modelKey,
44 + workspace: WORKSPACE,
45 + conversationId: state.conversation?.id,
46 +});
47
• Edited src/ui/banner.js (+19 -13)
5
6 -function renderBanner({ modelKey, workspace, conversationId }) {
6 +function renderBox({ title, lines }) {
7 const cols = Number(process.stdout.columns) || 80;
8 - const title = `>_ Welcome to ${bold('EasyAgent')}`;
9 - const lines = [
10 - `model: ${modelKey || ''}`,
11 - `path: ${workspace || ''}`,
12 - `conversation: ${conversationId || 'none'}`,
13 - ];
8 + const safeTitle = title ? String(title) : '';
9 + const safeLines = Array.isArray(lines) ? lines.map((line) => String(line)) : [];
10 if (cols < 20) {
11 console.log('');
16 - console.log(title);
17 - lines.forEach((line) => console.log(line));
12 + if (safeTitle) console.log(safeTitle);
13 + safeLines.forEach((line) => console.log(line));
14 console.log('');
16 }
21 - const contentWidth = Math.max(visibleWidth(title), ...lines.map(visibleWidth));
17 + const contentWidth = Math.max(visibleWidth(safeTitle), ...safeLines.map(visibleWidth));
18 const innerWidth = Math.max(10, Math.min(cols - 2, contentWidth + 2));
23 console.log(top);
28 - console.log(renderLine(title));
29 - lines.forEach((line) => console.log(renderLine(line)));
24 + if (safeTitle) console.log(renderLine(safeTitle));
25 + safeLines.forEach((line) => console.log(renderLine(line)));
26 console.log(top);
29
34 -module.exports = { renderBanner };
30 +function renderBanner({ modelKey, workspace, conversationId }) {
31 + const title = `>_ Welcome to ${bold('EasyAgent')}`;
32 + const lines = [
33 + `model: ${modelKey || ''}`,
34 + `path: ${workspace || ''}`,
35 + `conversation: ${conversationId || 'none'}`,
36 + ];
37 + renderBox({ title, lines });
38 +}
39 +
40 +module.exports = { renderBanner, renderBox };
• Edited src/cli/commands.js (+11 -5)
8 const { buildFinalLine, formatResultLines, printResultLines } = require('../ui/tool_display');
9 +const { renderBox } = require('../ui/banner');
10 const { createIndentedWriter } = require('../ui/indented_writer');
178 if (cmd === '/status') {
178 - console.log(`model: ${state.modelKey} | 思考: ${state.thinkingMode ? 'thinking' : 'fast'}`);
179 - console.log(`workspace: ${workspace}`);
180 - console.log(`allow: ${state.allowMode}`);
181 - console.log(`conversation: ${state.conversation?.id || 'none'}`);
182 - console.log(`token usage: ${state.tokenUsage}`);
179 + const title = 'Status';
180 + const lines = [
181 + `model: ${state.modelKey}`,
182 + `thinking: ${state.thinkingMode ? 'thinking' : 'fast'}`,
183 + `workspace: ${workspace}`,
184 + `allow: ${state.allowMode}`,
185 + `conversation: ${state.conversation?.id || 'none'}`,
186 + `token usage: ${state.tokenUsage}`,
187 + ];
188 + renderBox({ title, lines });
189 return { exit: false };
• Edited src/cli/index.js (+1 -1)
25 const USERNAME = os.userInfo().username || 'user';
26 -const PROMPT = `${USERNAME}@${WORKSPACE_NAME} `;
26 +const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `;
27
• Edited src/core/state.js (+1 -1)
9 thinkingMode: true,
10 - tokenUsage: 0,
10 + tokenUsage: { prompt: 0, completion: 0, total: 0 },
11 conversation: null,
• Edited src/storage/conversation_store.js (+5 -1)
6 const { toISO } = require('../utils/time');
7 +const { normalizeTokenUsage } = require('../utils/token_usage');
8
85 allow_mode: metadata.allow_mode || 'full_access',
85 - token_usage: metadata.token_usage || 0,
86 + token_usage: normalizeTokenUsage(metadata.token_usage),
87 cwd: metadata.cwd || '',
126 ...metadataUpdates,
127 + token_usage: metadataUpdates && Object.prototype.hasOwnProperty.call(metadataUpdates, 'token_usage')
128 + ? normalizeTokenUsage(metadataUpdates.token_usage)
129 + : normalizeTokenUsage(conversation.metadata?.token_usage),
130 };
• Edited src/cli/commands.js (+7 -3)
11 const { cyan, green } = require('../utils/colors');
12 +const { normalizeTokenUsage } = require('../utils/token_usage');
13 const { Spinner } = require('../ui/spinner');
79 state.allowMode = conv.metadata?.allow_mode || state.allowMode;
79 - state.tokenUsage = conv.metadata?.token_usage || 0;
80 + state.tokenUsage = normalizeTokenUsage(conv.metadata?.token_usage);
81 console.log(`已加载对话: ${conv.id}`);
129 state.allowMode = conv.metadata?.allow_mode || state.allowMode;
129 - state.tokenUsage = conv.metadata?.token_usage || 0;
130 + state.tokenUsage = normalizeTokenUsage(conv.metadata?.token_usage);
131 console.log(`已加载对话: ${conv.id}`);
179 if (cmd === '/status') {
180 + const usage = normalizeTokenUsage(state.tokenUsage);
181 const title = 'Status';
187 `conversation: ${state.conversation?.id || 'none'}`,
186 - `token usage: ${state.tokenUsage}`,
188 + `tokens(in): ${usage.prompt}`,
189 + `tokens(out): ${usage.completion}`,
190 + `tokens(total): ${usage.total}`,
191 ];
• Edited src/cli/index.js (+16 -2)
19 const { createConversation, updateConversation } = require('../storage/conversation_store');
20 +const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
21 const { gray, cyan, green } = require('../utils/colors');
238 let usageTotal = null;
239 + let usagePrompt = null;
240 + let usageCompletion = null;
241 let firstContent = true;
258 if (!choice) continue;
256 - if (chunk.usage && chunk.usage.total_tokens) usageTotal = chunk.usage.total_tokens;
259 + const usage = (choice && (choice.usage || choice.delta?.usage)) || chunk.usage;
260 + if (usage) {
261 + if (Number.isFinite(usage.prompt_tokens)) usagePrompt = usage.prompt_tokens;
262 + if (Number.isFinite(usage.completion_tokens)) usageCompletion = usage.completion_tokens;
263 + if (Number.isFinite(usage.total_tokens)) usageTotal = usage.total_tokens;
264 + }
265 const delta = choice.delta || {};
332
325 - if (usageTotal) state.tokenUsage = usageTotal;
333 + if (usageTotal || usagePrompt || usageCompletion) {
334 + state.tokenUsage = applyUsage(normalizeTokenUsage(state.tokenUsage), {
335 + prompt_tokens: usagePrompt,
336 + completion_tokens: usageCompletion,
337 + total_tokens: usageTotal,
338 + });
339 + }
340
• Edited src/cli/index.js (+1 -1)
332
333 - if (usageTotal || usagePrompt || usageCompletion) {
333 + if (usageTotal !== null || usagePrompt !== null || usageCompletion !== null) {
334 state.tokenUsage = applyUsage(normalizeTokenUsage(state.tokenUsage), {
• Edited src/cli/commands.js (+3 -3)
187 `conversation: ${state.conversation?.id || 'none'}`,
188 - `tokens(in): ${usage.prompt}`,
189 - `tokens(out): ${usage.completion}`,
190 - `tokens(total): ${usage.total}`,
188 + `输入token: ${usage.prompt}`,
189 + `输出token: ${usage.completion}`,
190 + `当前上下文总token: ${usage.total}`,
191 ];
• Edited src/cli/index.js (+5 -1)
51 let menuSearchTerm = '';
52 +let menuLastSearchTerm = '';
53 let menuJustClosedAt = 0;
94 menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
95 + menuLastSearchTerm = menuSearchTerm;
96 drainStdin();
135 const tooOld = Date.now() - menuJustClosedAt > 800;
134 - const normalizedMenu = String(menuSearchTerm).trim().replace(/^\/+/, '');
136 + const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, '');
137 const normalizedInput = input.replace(/^\/+/, '');
139 menuJustClosedAt = 0;
140 + menuLastSearchTerm = '';
141 rl.prompt();
144 menuJustClosedAt = 0;
145 + menuLastSearchTerm = '';
146 }
• Edited src/cli/index.js (+7 -0)
72 commandMenuActive = true;
73 + if (rl) {
74 + rl.pause();
75 + rl.line = '';
76 + rl.cursor = 0;
77 + readline.clearLine(process.stdout, 0);
78 + readline.cursorTo(process.stdout, 0);
79 + }
80 menuSearchTerm = '';
• Edited src/cli/index.js (+5 -0)
147 menuLastSearchTerm = '';
148 + if (process.stdout.isTTY) {
149 + readline.moveCursor(process.stdout, 0, -1);
150 + readline.clearLine(process.stdout, 0);
151 + readline.cursorTo(process.stdout, 0);
152 + }
153 rl.prompt();
• Edited src/cli/index.js (+44 -0)
27 const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `;
28 +const DEBUG_LOG_ENABLED = process.env.EAGENT_DEBUG === '1';
29 +const DEBUG_LOG_PATH = path.join(WORKSPACE, 'eagent-debug.log');
30 +
31 +function debugLog(event, data = {}) {
32 + if (!DEBUG_LOG_ENABLED) return;
33 + try {
34 + const payload = {
35 + ts: new Date().toISOString(),
36 + event,
37 + ...data,
38 + };
39 + fs.appendFileSync(DEBUG_LOG_PATH, `${JSON.stringify(payload)}\n`);
40 + } catch (_) {}
41 +}
42
61 });
62 +debugLog('startup', { prompt: PROMPT, pid: process.pid });
63
79 if (key && key.name === 'backspace' && menuSearchTerm === '') {
80 + debugLog('menu_backspace_abort', { str, key: key.name });
81 if (menuAbortController && !menuAbortController.signal.aborted) {
87 if (str === '/' && (rl.line === '' || rl.line === '/')) {
88 + debugLog('menu_open_requested', { str, rl_line: rl.line });
89 commandMenuActive = true;
106 menuSearchTerm = input || '';
107 + debugLog('menu_input', { input: menuSearchTerm });
108 },
111 .then((result) => {
112 + debugLog('menu_result', { result });
113 if (result && result.chosen && !result.cancelled) {
117 .finally(() => {
118 + debugLog('menu_close_before', {
119 + injected: menuInjectedCommand,
120 + lastTerm: menuLastSearchTerm,
121 + searchTerm: menuSearchTerm,
122 + });
123 commandMenuActive = false;
136 menuInjectedCommand = null;
137 + debugLog('menu_inject', { injected });
138 setImmediate(() => injectLine(injected));
139 }
140 + debugLog('menu_close_after', {
141 + justClosedAt: menuJustClosedAt,
142 + lastTerm: menuLastSearchTerm,
143 + });
144 });
164 rl.on('line', async (line) => {
165 + debugLog('line', {
166 + line,
167 + commandMenuActive,
168 + menuJustClosedAt,
169 + menuLastSearchTerm,
170 + menuSearchTerm,
171 + });
172 if (commandMenuActive) {
180 const normalizedInput = input.replace(/^\/+/, '');
181 + const willConsume = !tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu);
182 + debugLog('menu_close_check', {
183 + tooOld,
184 + normalizedMenu,
185 + normalizedInput,
186 + willConsume,
187 + });
188 if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
237 if (!rl) return;
238 + debugLog('inject_line', { text });
239 rl.write(text);
• Edited src/cli/index.js (+5 -1)
171 });
172 + if (rl) {
173 + rl.line = '';
174 + rl.cursor = 0;
175 + }
176 if (commandMenuActive) {
173 - rl.prompt();
177 + debugLog('line_ignored_menu', { line });
178 return;
• Edited src/cli/index.js (+3 -0)
130 menuSearchTerm = '';
131 + if (process.stdout.isTTY) {
132 + readline.clearScreenDown(process.stdout);
133 + }
134 readline.clearLine(process.stdout, 0);
• Edited src/cli/index.js (+27 -3)
29 const DEBUG_LOG_PATH = path.join(WORKSPACE, 'eagent-debug.log');
30 +const MENU_PAGE_SIZE = 6;
31
62 });
62 -debugLog('startup', { prompt: PROMPT, pid: process.pid });
63 +debugLog('startup', {
64 + prompt: PROMPT,
65 + pid: process.pid,
66 + tty: process.stdout.isTTY,
67 + columns: process.stdout.columns,
68 + rows: process.stdout.rows,
69 +});
70
94 if (str === '/' && (rl.line === '' || rl.line === '/')) {
88 - debugLog('menu_open_requested', { str, rl_line: rl.line });
95 + debugLog('menu_open_requested', {
96 + str,
97 + rl_line: rl.line,
98 + rl_cursor: rl.cursor,
99 + tty: process.stdout.isTTY,
100 + columns: process.stdout.columns,
101 + rows: process.stdout.rows,
102 + });
103 commandMenuActive = true;
115 prompt: PROMPT,
102 - pageSize: 6,
116 + pageSize: MENU_PAGE_SIZE,
117 colorEnabled: process.stdout.isTTY,
135 searchTerm: menuSearchTerm,
136 + tty: process.stdout.isTTY,
137 + columns: process.stdout.columns,
138 + rows: process.stdout.rows,
139 });
148 if (process.stdout.isTTY) {
149 + debugLog('menu_close_clear', { method: 'clearScreenDown' });
150 readline.clearScreenDown(process.stdout);
153 readline.cursorTo(process.stdout, 0);
154 + debugLog('prompt', { reason: 'menu_close' });
155 rl.prompt(true);
217 if (process.stdout.isTTY) {
218 + debugLog('menu_close_clear', { method: 'moveCursorUp+clearLine' });
219 readline.moveCursor(process.stdout, 0, -1);
222 }
223 + debugLog('prompt', { reason: 'menu_close_consume' });
224 rl.prompt();
230 if (!input) {
231 + debugLog('prompt', { reason: 'empty_line' });
232 rl.prompt();
238 if (result && result.exit) return;
239 + debugLog('prompt', { reason: 'command_done', command: input });
240 rl.prompt();
249 await runAssistantLoop();
250 + debugLog('prompt', { reason: 'assistant_done' });
251 rl.prompt();
• Edited src/cli/index.js (+6 -1)
154 debugLog('prompt', { reason: 'menu_close' });
155 - rl.prompt(true);
155 + if (!menuInjectedCommand) {
156 + debugLog('prompt', { reason: 'menu_close' });
157 + rl.prompt(true);
158 + } else {
159 + debugLog('prompt', { reason: 'menu_close_skip_for_inject' });
160 + }
161 if (menuInjectedCommand) {
• Edited src/cli/index.js (+11 -7)
151 }
152 - readline.clearLine(process.stdout, 0);
153 - readline.cursorTo(process.stdout, 0);
154 - debugLog('prompt', { reason: 'menu_close' });
155 - if (!menuInjectedCommand) {
156 - debugLog('prompt', { reason: 'menu_close' });
157 - rl.prompt(true);
152 + // Clear possible echoes from the base readline line (current + previous line).
153 + if (process.stdout.isTTY) {
154 + readline.clearLine(process.stdout, 0);
155 + readline.cursorTo(process.stdout, 0);
156 + readline.moveCursor(process.stdout, 0, -1);
157 + readline.clearLine(process.stdout, 0);
158 + readline.cursorTo(process.stdout, 0);
159 } else {
159 - debugLog('prompt', { reason: 'menu_close_skip_for_inject' });
160 + readline.clearLine(process.stdout, 0);
161 + readline.cursorTo(process.stdout, 0);
162 }
163 + debugLog('prompt', { reason: menuInjectedCommand ? 'menu_close_before_inject' : 'menu_close' });
164 + rl.prompt(true);
165 if (menuInjectedCommand) {
• Edited src/cli/index.js (+0 -68)
27 const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `;
28 -const DEBUG_LOG_ENABLED = process.env.EAGENT_DEBUG === '1';
29 -const DEBUG_LOG_PATH = path.join(WORKSPACE, 'eagent-debug.log');
28 const MENU_PAGE_SIZE = 6;
29
32 -function debugLog(event, data = {}) {
33 - if (!DEBUG_LOG_ENABLED) return;
34 - try {
35 - const payload = {
36 - ts: new Date().toISOString(),
37 - event,
38 - ...data,
39 - };
40 - fs.appendFileSync(DEBUG_LOG_PATH, `${JSON.stringify(payload)}\n`);
41 - } catch (_) {}
42 -}
43 -
30 const config = ensureConfig();
47 conversationId: state.conversation?.id,
62 -});
63 -debugLog('startup', {
64 - prompt: PROMPT,
65 - pid: process.pid,
66 - tty: process.stdout.isTTY,
67 - columns: process.stdout.columns,
68 - rows: process.stdout.rows,
48 });
65 if (key && key.name === 'backspace' && menuSearchTerm === '') {
87 - debugLog('menu_backspace_abort', { str, key: key.name });
66 if (menuAbortController && !menuAbortController.signal.aborted) {
72 if (str === '/' && (rl.line === '' || rl.line === '/')) {
95 - debugLog('menu_open_requested', {
96 - str,
97 - rl_line: rl.line,
98 - rl_cursor: rl.cursor,
99 - tty: process.stdout.isTTY,
100 - columns: process.stdout.columns,
101 - rows: process.stdout.rows,
102 - });
73 commandMenuActive = true;
90 menuSearchTerm = input || '';
121 - debugLog('menu_input', { input: menuSearchTerm });
91 },
94 .then((result) => {
126 - debugLog('menu_result', { result });
95 if (result && result.chosen && !result.cancelled) {
99 .finally(() => {
132 - debugLog('menu_close_before', {
133 - injected: menuInjectedCommand,
134 - lastTerm: menuLastSearchTerm,
135 - searchTerm: menuSearchTerm,
136 - tty: process.stdout.isTTY,
137 - columns: process.stdout.columns,
138 - rows: process.stdout.rows,
139 - });
100 commandMenuActive = false;
108 if (process.stdout.isTTY) {
149 - debugLog('menu_close_clear', { method: 'clearScreenDown' });
109 readline.clearScreenDown(process.stdout);
121 }
163 - debugLog('prompt', { reason: menuInjectedCommand ? 'menu_close_before_inject' : 'menu_close' });
122 rl.prompt(true);
125 menuInjectedCommand = null;
168 - debugLog('menu_inject', { injected });
126 setImmediate(() => injectLine(injected));
127 }
171 - debugLog('menu_close_after', {
172 - justClosedAt: menuJustClosedAt,
173 - lastTerm: menuLastSearchTerm,
174 - });
128 });
148 rl.on('line', async (line) => {
196 - debugLog('line', {
197 - line,
198 - commandMenuActive,
199 - menuJustClosedAt,
200 - menuLastSearchTerm,
201 - menuSearchTerm,
202 - });
149 if (rl) {
153 if (commandMenuActive) {
208 - debugLog('line_ignored_menu', { line });
154 return;
160 const normalizedInput = input.replace(/^\/+/, '');
216 - const willConsume = !tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu);
217 - debugLog('menu_close_check', {
218 - tooOld,
219 - normalizedMenu,
220 - normalizedInput,
221 - willConsume,
222 - });
161 if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
164 if (process.stdout.isTTY) {
227 - debugLog('menu_close_clear', { method: 'moveCursorUp+clearLine' });
165 readline.moveCursor(process.stdout, 0, -1);
168 }
232 - debugLog('prompt', { reason: 'menu_close_consume' });
169 rl.prompt();
175 if (!input) {
240 - debugLog('prompt', { reason: 'empty_line' });
176 rl.prompt();
182 if (result && result.exit) return;
248 - debugLog('prompt', { reason: 'command_done', command: input });
183 rl.prompt();
192 await runAssistantLoop();
259 - debugLog('prompt', { reason: 'assistant_done' });
193 rl.prompt();
210 if (!rl) return;
278 - debugLog('inject_line', { text });
211 rl.write(text);
• Edited src/cli/index.js (+62 -1)
22 const { createIndentedWriter } = require('../ui/indented_writer');
23 +const { createStatusBar } = require('../ui/status_bar');
24
57 let menuAbortController = null;
58 +let statusBar = null;
59 +let escPendingCancel = false;
60 +let activeStreamController = null;
61
66
67 +statusBar = createStatusBar({
68 + getTokens: () => normalizeTokenUsage(state.tokenUsage).total || 0,
69 + maxTokens: '256k',
70 +});
71 +statusBar.render();
72 +
73 process.stdin.on('keypress', (str, key) => {
81 }
82 + if (key && key.name === 'escape') {
83 + escPendingCancel = true;
84 + if (activeStreamController && typeof activeStreamController.abort === 'function') {
85 + activeStreamController.abort();
86 + }
87 + return;
88 + }
89 if (str === '/' && (rl.line === '' || rl.line === '/')) {
165 rl.on('line', async (line) => {
166 + if (statusBar) statusBar.render();
167 if (rl) {
170 }
171 + if (escPendingCancel) {
172 + escPendingCancel = false;
173 + rl.prompt();
174 + return;
175 + }
176 if (commandMenuActive) {
282 while (continueLoop) {
283 + if (statusBar) statusBar.setMode('running');
284 hideCursor();
311 try {
288 - for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode })) {
312 + activeStreamController = new AbortController();
313 + for await (const chunk of streamChat({
314 + config,
315 + messages,
316 + tools,
317 + thinkingMode: state.thinkingMode,
318 + abortSignal: activeStreamController.signal,
319 + })) {
320 const choice = chunk.choices && chunk.choices[0];
372 } catch (err) {
373 + activeStreamController = null;
374 clearTimeout(thinkingDelay);
380 showCursor();
381 + if (escPendingCancel) {
382 + escPendingCancel = false;
383 + statusBar.setMode('input');
384 + console.log('已取消本次响应');
385 + return;
386 + }
387 console.log(`错误: ${err.message || err}`);
390
391 + activeStreamController = null;
392 +
393 + if (escPendingCancel) {
394 + escPendingCancel = false;
395 + clearTimeout(thinkingDelay);
396 + if (state.thinkingMode) {
397 + spinner.stop('○');
398 + } else {
399 + spinner.stopSilent();
400 + }
401 + showCursor();
402 + statusBar.setMode('input');
403 + console.log('已取消本次响应');
404 + return;
405 + }
406 +
407 clearTimeout(thinkingDelay);
441 for (const call of toolCallList) {
442 + if (escPendingCancel) {
443 + escPendingCancel = false;
444 + statusBar.setMode('input');
445 + console.log('工具被用户取消');
446 + return;
447 + }
448 let args = {};
480 showCursor();
481 + if (statusBar) statusBar.setMode('input');
482 }
• Edited src/model/client.js (+7 -1)
4
5 -async function* streamChat({ config, messages, tools, thinkingMode }) {
5 +async function* streamChat({ config, messages, tools, thinkingMode, abortSignal }) {
6 const profile = getModelProfile(config);
33 body: JSON.stringify(payload),
34 + signal: abortSignal,
35 });
36 } catch (err) {
37 + if (abortSignal && abortSignal.aborted) {
38 + const error = new Error('请求已取消');
39 + error.code = 'aborted';
40 + throw error;
41 + }
42 const cause = err && err.cause ? ` (${err.cause.code || err.cause.message || err.cause})` : '';
• Edited src/cli/index.js (+1 -0)
88 }
89 + if (statusBar) statusBar.render();
90 if (str === '/' && (rl.line === '' || rl.line === '/')) {
• Edited src/cli/index.js (+0 -2)
213 userWriter.writeLine(`${cyan('用户:')}${line}`);
214 - state.messages.push({ role: 'user', content: line });
215 - persistConversation();
214 await runAssistantLoop();
• Edited src/cli/index.js (+11 -1)
280 const showCursor = () => process.stdout.write('\x1b[?25h');
281 + let localMessages = null;
282 while (continueLoop) {
309
309 - const messages = buildApiMessages();
310 + if (!localMessages) {
311 + localMessages = buildApiMessages();
312 + }
313 + const messages = localMessages;
314 try {
429
430 + const userMsg = localMessages && localMessages[localMessages.length - 1];
431 + if (userMsg && userMsg.role === 'user') {
432 + state.messages.push(userMsg);
433 + persistConversation();
434 + }
435 +
436 const toolCallList = Object.keys(toolCalls)
• Edited src/cli/index.js (+20 -7)
213 userWriter.writeLine(`${cyan('用户:')}${line}`);
214 - await runAssistantLoop();
214 + await runAssistantLoop(line);
215 rl.prompt();
275
276 -async function runAssistantLoop() {
276 +async function runAssistantLoop(userInput) {
277 let continueLoop = true;
311 localMessages = buildApiMessages();
312 + if (userInput) {
313 + localMessages.push({ role: 'user', content: userInput });
314 + }
315 }
449 if (state.thinkingMode) assistantMsg.reasoning_content = fullThinkingBuffer || '';
447 - state.messages.push(assistantMsg);
448 - persistConversation();
450 + const toolMsgs = [];
451
464 const toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
465 + if (escPendingCancel) {
466 + escPendingCancel = false;
467 + indicator.stop(finalLine);
468 + statusBar.setMode('input');
469 + console.log('工具被用户取消');
470 + return;
471 + }
472 indicator.stop(finalLine);
475
467 - const toolMsg = {
476 + toolMsgs.push({
477 role: 'tool',
480 tool_raw: toolResult.raw,
472 - };
481 + });
482 + }
483 +
484 + state.messages.push(assistantMsg);
485 + for (const toolMsg of toolMsgs) {
486 state.messages.push(toolMsg);
474 - persistConversation();
487 }
488 + persistConversation();
489 continueLoop = true;
• Edited src/cli/index.js (+0 -0)
• Edited src/model/client.js (+5 -0)
55 while (true) {
56 + if (abortSignal && abortSignal.aborted) {
57 + const error = new Error('请求已取消');
58 + error.code = 'aborted';
59 + throw error;
60 + }
61 const { value, done } = await reader.read();
• Edited src/cli/index.js (+2 -0)
369 assistantContent += delta.content;
370 + if (statusBar) statusBar.render();
371 }
475 printResultLines(resultLines);
476 + if (statusBar) statusBar.render();
477
• Edited src/cli/index.js (+4 -1)
60 let activeStreamController = null;
61 +let isRunning = false;
62
82 }
82 - if (key && key.name === 'escape') {
83 + if (key && key.name === 'escape' && isRunning) {
84 escPendingCancel = true;
282 let localMessages = null;
283 + isRunning = true;
284 while (continueLoop) {
507 if (statusBar) statusBar.setMode('input');
508 + isRunning = false;
509 }
• Edited src/cli/index.js (+10 -6)
283 isRunning = true;
284 + const finishRun = () => {
285 + if (statusBar) statusBar.setMode('input');
286 + isRunning = false;
287 + };
288 while (continueLoop) {
395 escPendingCancel = false;
392 - statusBar.setMode('input');
396 + finishRun();
397 console.log('已取消本次响应');
400 console.log(`错误: ${err.message || err}`);
401 + finishRun();
402 return;
415 showCursor();
411 - statusBar.setMode('input');
416 + finishRun();
417 console.log('已取消本次响应');
462 escPendingCancel = false;
458 - statusBar.setMode('input');
463 + finishRun();
464 console.log('工具被用户取消');
475 indicator.stop(finalLine);
471 - statusBar.setMode('input');
476 + finishRun();
477 console.log('工具被用户取消');
511 showCursor();
507 - if (statusBar) statusBar.setMode('input');
508 - isRunning = false;
512 + finishRun();
513 }
• Edited src/cli/index.js (+5 -2)
282 let localMessages = null;
283 + let userPersisted = false;
284 isRunning = true;
318 localMessages = buildApiMessages();
318 - if (userInput) {
319 + if (userInput && !userPersisted) {
320 localMessages.push({ role: 'user', content: userInput });
442 const userMsg = localMessages && localMessages[localMessages.length - 1];
442 - if (userMsg && userMsg.role === 'user') {
443 + if (userMsg && userMsg.role === 'user' && !userPersisted) {
444 state.messages.push(userMsg);
445 persistConversation();
446 + userPersisted = true;
447 }
499 persistConversation();
500 + localMessages = null;
501 continueLoop = true;
• Edited src/ui/status_bar.js (+18 -2)
18 let enabled = true;
19 + let scrollApplied = false;
20
24 const rows = process.stdout.rows || 24;
25 + if (rows < 2) return;
26 const left = mode === 'running' ? '按下Esc停止' : '输入/查看所有指令';
37 const line = `${displayLeft}${' '.repeat(Math.max(1, gap))}${right}`;
38 + const safeLine = line.slice(0, cols).padEnd(cols, ' ');
39
40 process.stdout.write('\x1b7'); // save cursor
41 + process.stdout.write(`\x1b[1;${rows - 1}r`); // reserve last line
42 + scrollApplied = true;
43 readline.cursorTo(process.stdout, 0, rows - 1);
44 readline.clearLine(process.stdout, 0);
40 - process.stdout.write(line.slice(0, cols));
45 + process.stdout.write(safeLine);
46 process.stdout.write('\x1b8'); // restore cursor
59
60 + const destroy = () => {
61 + if (!process.stdout.isTTY) return;
62 + if (scrollApplied) {
63 + process.stdout.write('\x1b[r'); // reset scroll region
64 + scrollApplied = false;
65 + }
66 + const rows = process.stdout.rows || 24;
67 + readline.cursorTo(process.stdout, 0, rows - 1);
68 + readline.clearLine(process.stdout, 0);
69 + };
70 +
71 if (process.stdout.isTTY) {
74
59 - return { render, setMode, setEnabled };
75 + return { render, setMode, setEnabled, destroy };
76 }
• Edited src/cli/index.js (+1 -0)
219 rl.on('close', () => {
220 + if (statusBar) statusBar.destroy();
221 process.stdout.write('\n');
• Edited src/ui/status_bar.js (+19 -5)
19 let scrollApplied = false;
20 + let lastRows = null;
21
25 const rows = process.stdout.rows || 24;
25 - if (rows < 2) return;
26 + if (rows < 3) return;
27 const left = mode === 'running' ? '按下Esc停止' : '输入/查看所有指令';
41 process.stdout.write('\x1b7'); // save cursor
41 - process.stdout.write(`\x1b[1;${rows - 1}r`); // reserve last line
42 - scrollApplied = true;
42 + if (!scrollApplied || lastRows !== rows) {
43 + process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
44 + scrollApplied = true;
45 + lastRows = rows;
46 + }
47 + // keep one blank line above status line
48 + readline.cursorTo(process.stdout, 0, rows - 2);
49 + readline.clearLine(process.stdout, 0);
50 + // render status line on last row
51 readline.cursorTo(process.stdout, 0, rows - 1);
74 const rows = process.stdout.rows || 24;
67 - readline.cursorTo(process.stdout, 0, rows - 1);
68 - readline.clearLine(process.stdout, 0);
75 + if (rows >= 2) {
76 + readline.cursorTo(process.stdout, 0, rows - 2);
77 + readline.clearLine(process.stdout, 0);
78 + }
79 + if (rows >= 1) {
80 + readline.cursorTo(process.stdout, 0, rows - 1);
81 + readline.clearLine(process.stdout, 0);
82 + }
83 };
• Edited src/ui/status_bar.js (+7 -17)
3 const readline = require('readline');
4 -
5 -function stripAnsi(text) {
6 - return String(text).replace(/\x1b\[[0-9;]*m/g, '');
7 -}
8 -
9 -function truncateToWidth(text, width) {
10 - if (width <= 0) return '';
11 - const plain = stripAnsi(text);
12 - if (plain.length <= width) return text;
13 - return plain.slice(0, Math.max(0, width - 1)) + '…';
14 -}
4 +const { visibleWidth, truncateVisible, padEndVisible } = require('../utils/text_width');
5
19 const right = `当前上下文token ${total}/${maxTokens}`;
30 - const leftPlain = stripAnsi(left);
31 - const rightPlain = stripAnsi(right);
32 - let gap = cols - leftPlain.length - rightPlain.length;
20 + const leftWidth = visibleWidth(left);
21 + const rightWidth = visibleWidth(right);
22 + let gap = cols - leftWidth - rightWidth;
23 let displayLeft = left;
24 if (gap < 1) {
35 - displayLeft = truncateToWidth(left, Math.max(1, cols - rightPlain.length - 1));
36 - gap = cols - stripAnsi(displayLeft).length - rightPlain.length;
25 + displayLeft = truncateVisible(left, Math.max(1, cols - rightWidth - 1));
26 + gap = cols - visibleWidth(displayLeft) - rightWidth;
27 }
28 const line = `${displayLeft}${' '.repeat(Math.max(1, gap))}${right}`;
39 - const safeLine = line.slice(0, cols).padEnd(cols, ' ');
29 + const safeLine = padEndVisible(truncateVisible(line, cols), cols);
30
• Edited src/ui/status_bar.js (+0 -7)
36 }
37 - // keep one blank line above status line
38 - readline.cursorTo(process.stdout, 0, rows - 2);
39 - readline.clearLine(process.stdout, 0);
37 // render status line on last row
61 const rows = process.stdout.rows || 24;
65 - if (rows >= 2) {
66 - readline.cursorTo(process.stdout, 0, rows - 2);
67 - readline.clearLine(process.stdout, 0);
68 - }
62 if (rows >= 1) {
• Edited src/ui/status_bar.js (+1 -1)
32 if (!scrollApplied || lastRows !== rows) {
33 - process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
33 + process.stdout.write(`\x1b[1;${rows - 1}r`); // reserve last line
34 scrollApplied = true;
• Edited src/ui/status_bar.js (+4 -1)
32 if (!scrollApplied || lastRows !== rows) {
33 - process.stdout.write(`\x1b[1;${rows - 1}r`); // reserve last line
33 + process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
34 scrollApplied = true;
36 }
37 + // keep one blank spacer line above status line
38 + readline.cursorTo(process.stdout, 0, rows - 2);
39 + readline.clearLine(process.stdout, 0);
40 // render status line on last row
• Edited src/ui/status_bar.js (+4 -0)
64 const rows = process.stdout.rows || 24;
65 + if (rows >= 2) {
66 + readline.cursorTo(process.stdout, 0, rows - 2);
67 + readline.clearLine(process.stdout, 0);
68 + }
69 if (rows >= 1) {
• Edited src/cli/index.js (+13 -0)
62
63 +function promptWithStatus(force = false) {
64 + if (!rl) return;
65 + if (statusBar) statusBar.render();
66 + if (process.stdout.isTTY) {
67 + const rows = process.stdout.rows || 24;
68 + if (rows >= 3) {
69 + readline.cursorTo(process.stdout, 0, rows - 3);
70 + readline.clearLine(process.stdout, 0);
71 + }
72 + }
73 + rl.prompt(force);
74 +}
75 +
76 readline.emitKeypressEvents(process.stdin);
• Edited src/cli/index.js (+7 -7)
153 }
154 - rl.prompt(true);
154 + promptWithStatus(true);
155 if (menuInjectedCommand) {
177 rl.setPrompt(PROMPT);
178 - rl.prompt();
178 + promptWithStatus();
179
187 escPendingCancel = false;
188 - rl.prompt();
188 + promptWithStatus();
189 return;
206 }
207 - rl.prompt();
207 + promptWithStatus();
208 return;
213 if (!input) {
214 - rl.prompt();
214 + promptWithStatus();
215 return;
220 if (result && result.exit) return;
221 - rl.prompt();
221 + promptWithStatus();
222 return;
228 await runAssistantLoop(line);
229 - rl.prompt();
229 + promptWithStatus();
230 });
• Edited src/cli/index.js (+1 -1)
67 const rows = process.stdout.rows || 24;
68 - if (rows >= 3) {
68 + if (rows >= 8) {
69 readline.cursorTo(process.stdout, 0, rows - 3);
• Edited src/cli/index.js (+1 -0)
49 });
50 +console.log('');
51
• Edited src/cli/index.js (+1 -1)
68 const rows = process.stdout.rows || 24;
69 - if (rows >= 8) {
69 + if (rows >= 9) {
70 readline.cursorTo(process.stdout, 0, rows - 3);
• Edited src/cli/index.js (+1 -0)
50 console.log('');
51 +console.log('');
52
• Edited src/cli/index.js (+1 -0)
302 if (statusBar) statusBar.setMode('input');
303 + if (statusBar) statusBar.render();
304 isRunning = false;
• Edited src/cli/index.js (+1 -1)
66 if (!rl) return;
67 - if (statusBar) statusBar.render();
67 if (process.stdout.isTTY) {
74 rl.prompt(force);
75 + if (statusBar) statusBar.render();
76 }
• Edited src/tools/edit_file.js (+1 -1)
60 const replacements = original.split(oldString).length - 1;
61 - fs.writeFileSync(target, updated, 'utf8');
61 + fs.writeFileSync(target, updated, { encoding: 'utf8', flag: 'r+' });
62 const diff = diffSummary(original, updated);
• Edited src/ui/status_bar.js (+1 -0)
32 if (!scrollApplied || lastRows !== rows) {
33 + process.stdout.write('\x1b[r'); // reset scroll region
34 process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
• Edited src/tools/edit_file.js (+1 -1)
60 const replacements = original.split(oldString).length - 1;
61 - fs.writeFileSync(target, updated, { encoding: 'utf8', flag: 'r+' });
61 + fs.writeFileSync(target, updated, 'utf8');
62 const diff = diffSummary(original, updated);
• Edited src/tools/edit_file.js (+7 -4)
47 try {
48 + let creating = false;
49 if (!fs.existsSync(target)) {
49 - return { success: false, error: '文件不存在' };
50 + creating = true;
51 + fs.mkdirSync(path.dirname(target), { recursive: true });
52 }
57 const original = fs.readFileSync(target, 'utf8');
56 - if (!original.includes(oldString)) {
58 + if (!creating && !original.includes(oldString)) {
59 return { success: false, error: 'old_string 未匹配到内容' };
60 }
59 - const updated = original.split(oldString).join(newString);
60 - const replacements = original.split(oldString).length - 1;
61 + const updated = creating ? newString : original.split(oldString).join(newString);
62 + let replacements = creating ? 0 : original.split(oldString).length - 1;
63 + if (creating && newString) replacements = 1;
64 fs.writeFileSync(target, updated, 'utf8');
• Edited src/tools/edit_file.js (+4 -0)
51 fs.mkdirSync(path.dirname(target), { recursive: true });
52 + fs.writeFileSync(target, '', 'utf8');
53 }
58 const original = fs.readFileSync(target, 'utf8');
59 + if (!creating && oldString === '') {
60 + return { success: false, error: 'old_string 不能为空,请从 read_file 内容中精确复制' };
61 + }
62 if (!creating && !original.includes(oldString)) {
• Edited src/cli/index.js (+5 -5)
330 if (!showThinkingLabel) return '';
331 - return { label: ' 思考中...', thinking: thinkingBuffer, colorThinking: true };
331 + return { label: '思考中...', thinking: thinkingBuffer, colorThinking: true };
332 });
380 if (state.thinkingMode) {
381 - spinner.stop(thinkingActive ? '○ 思考完成' : '○');
381 + spinner.stop(thinkingActive ? '∙ 思考完成' : '∙');
382 } else {
406 if (state.thinkingMode) {
407 - spinner.stop('○');
407 + spinner.stop('∙');
408 } else {
428 if (state.thinkingMode) {
429 - spinner.stop('○');
429 + spinner.stop('∙');
430 } else {
441 if (state.thinkingMode) {
442 - spinner.stop(thinkingActive ? '○ 思考完成' : '○');
442 + spinner.stop(thinkingActive ? '∙ 思考完成' : '∙');
443 } else {
• Edited src/cli/index.js (+3 -1)
229 userWriter.writeLine(`${cyan('用户:')}${line}`);
230 - await runAssistantLoop(line);
230 + state.messages.push({ role: 'user', content: line });
231 + persistConversation();
232 + await runAssistantLoop();
233 promptWithStatus();
• Edited src/cli/index.js (+2 -10)
294
295 -async function runAssistantLoop(userInput) {
295 +async function runAssistantLoop() {
296 let continueLoop = true;
299 const showCursor = () => process.stdout.write('\x1b[?25h');
300 - let localMessages = null;
301 - let userPersisted = false;
300 isRunning = true;
333
336 - if (!localMessages) {
337 - localMessages = buildApiMessages();
338 - if (userInput && !userPersisted) {
339 - localMessages.push({ role: 'user', content: userInput });
340 - }
341 - }
342 - const messages = localMessages;
334 + const messages = buildApiMessages();
335 try {
• Edited src/cli/index.js (+0 -7)
452
453 - const userMsg = localMessages && localMessages[localMessages.length - 1];
454 - if (userMsg && userMsg.role === 'user' && !userPersisted) {
455 - state.messages.push(userMsg);
456 - persistConversation();
457 - userPersisted = true;
458 - }
459 -
453 const toolCallList = Object.keys(toolCalls)
• Edited src/cli/index.js (+29 -16)
463 if (state.thinkingMode) assistantMsg.reasoning_content = fullThinkingBuffer || '';
464 - const toolMsgs = [];
464 + state.messages.push(assistantMsg);
465 + persistConversation();
466
467 for (const call of toolCallList) {
467 - if (escPendingCancel) {
468 - escPendingCancel = false;
469 - finishRun();
470 - console.log('工具被用户取消');
471 - return;
472 - }
468 let args = {};
472 const indicator = startToolDisplay(startLine);
478 - const toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
473 if (escPendingCancel) {
475 indicator.stop(finalLine);
476 + const cancelledRaw = { success: false, error: '任务被用户取消' };
477 + const resultLines = formatResultLines(call.function.name, args, cancelledRaw);
478 + printResultLines(resultLines);
479 + state.messages.push({
480 + role: 'tool',
481 + tool_call_id: call.id,
482 + content: '任务被用户取消',
483 + tool_raw: cancelledRaw,
484 + });
485 + persistConversation();
486 finishRun();
483 - console.log('工具被用户取消');
487 return;
488 }
489 + let toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
490 + if (escPendingCancel) {
491 + escPendingCancel = false;
492 + toolResult = {
493 + success: false,
494 + tool: call.function.name,
495 + error: '任务被用户取消',
496 + raw: { success: false, error: '任务被用户取消' },
497 + formatted: '任务被用户取消',
498 + };
499 + }
500 indicator.stop(finalLine);
504
491 - toolMsgs.push({
505 + state.messages.push({
506 role: 'tool',
510 });
511 + persistConversation();
512 + if (toolResult.raw && toolResult.raw.success === false && toolResult.error === '任务被用户取消') {
513 + finishRun();
514 + return;
515 + }
516 }
517
499 - state.messages.push(assistantMsg);
500 - for (const toolMsg of toolMsgs) {
501 - state.messages.push(toolMsg);
502 - }
503 - persistConversation();
504 - localMessages = null;
518 continueLoop = true;
• Edited src/ui/tool_display.js (+1 -0)
132 const msg = raw && raw.error ? raw.error : '执行失败';
133 + if (msg === '任务被用户取消') return [red(msg)];
134 return [red(`失败: ${msg}`)];
• Edited src/tools/run_command.js (+18 -2)
11
12 -function runCommandTool(workspace, args) {
12 +function runCommandTool(workspace, args, abortSignal) {
13 return new Promise((resolve) => {
17 if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
18 + if (abortSignal && abortSignal.aborted) {
19 + return resolve({ success: false, error: '任务被用户取消', cancelled: true });
20 + }
21 const timeoutMs = Math.max(0, timeoutSec) * 1000;
19 - exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
22 + let finished = false;
23 + const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
24 + if (finished) return;
25 + finished = true;
26 + if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
27 const output = [stdout, stderr].filter(Boolean).join('');
40 });
41 + const onAbort = () => {
42 + if (finished) return;
43 + finished = true;
44 + try {
45 + child.kill('SIGTERM');
46 + } catch (_) {}
47 + return resolve({ success: false, error: '任务被用户取消', cancelled: true });
48 + };
49 + if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true });
50 });
• Edited src/tools/web_search.js (+16 -2)
2
3 -async function webSearchTool(config, args) {
3 +async function webSearchTool(config, args, abortSignal) {
4 + if (abortSignal && abortSignal.aborted) {
5 + return { success: false, error: '任务被用户取消', cancelled: true };
6 + }
7 const body = {
23 body: JSON.stringify(body),
24 + signal: abortSignal,
25 });
31 } catch (err) {
32 + if (err && err.name === 'AbortError') {
33 + return { success: false, error: '任务被用户取消', cancelled: true };
34 + }
35 return { success: false, error: err.message || String(err) };
38
32 -async function extractWebpageTool(config, args, savePath) {
39 +async function extractWebpageTool(config, args, savePath, abortSignal) {
40 + if (abortSignal && abortSignal.aborted) {
41 + return { success: false, error: '任务被用户取消', cancelled: true };
42 + }
43 const body = {
52 body: JSON.stringify(body),
53 + signal: abortSignal,
54 });
66 } catch (err) {
67 + if (err && err.name === 'AbortError') {
68 + return { success: false, error: '任务被用户取消', cancelled: true };
69 + }
70 return { success: false, error: err.message || String(err) };
• Edited src/tools/dispatcher.js (+15 -5)
140
141 -async function executeTool({ workspace, config, allowMode, toolCall }) {
141 +async function executeTool({ workspace, config, allowMode, toolCall, abortSignal }) {
142 const name = toolCall.function.name;
164
165 + if (abortSignal && abortSignal.aborted) {
166 + return {
167 + success: false,
168 + tool: name,
169 + error: '任务被用户取消',
170 + formatted: '任务被用户取消',
171 + raw: { success: false, error: '任务被用户取消', cancelled: true },
172 + };
173 + }
174 +
175 let raw;
177 else if (name === 'edit_file') raw = editFileTool(workspace, args);
168 - else if (name === 'run_command') raw = await runCommandTool(workspace, args);
169 - else if (name === 'web_search') raw = await webSearchTool(config, args);
178 + else if (name === 'run_command') raw = await runCommandTool(workspace, args, abortSignal);
179 + else if (name === 'web_search') raw = await webSearchTool(config, args, abortSignal);
180 else if (name === 'extract_webpage') {
186 } else {
177 - raw = await extractWebpageTool(config, args, targetPath);
187 + raw = await extractWebpageTool(config, args, targetPath, abortSignal);
188 }
189 } else {
180 - raw = await extractWebpageTool(config, args, targetPath);
190 + raw = await extractWebpageTool(config, args, targetPath, abortSignal);
191 }
• Edited src/cli/index.js (+4 -0)
63 let isRunning = false;
64 +let activeToolController = null;
65
103 }
104 + if (activeToolController && typeof activeToolController.abort === 'function') {
105 + activeToolController.abort();
106 + }
107 return;
• Edited src/cli/index.js (+11 -3)
479 indicator.stop(finalLine);
480 - const cancelledRaw = { success: false, error: '任务被用户取消' };
480 + const cancelledRaw = { success: false, error: '任务被用户取消', cancelled: true };
481 const resultLines = formatResultLines(call.function.name, args, cancelledRaw);
492 }
493 - let toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
493 + activeToolController = new AbortController();
494 + let toolResult = await executeTool({
495 + workspace: WORKSPACE,
496 + config,
497 + allowMode: state.allowMode,
498 + toolCall: call,
499 + abortSignal: activeToolController.signal,
500 + });
501 + activeToolController = null;
502 if (escPendingCancel) {
507 error: '任务被用户取消',
500 - raw: { success: false, error: '任务被用户取消' },
508 + raw: { success: false, error: '任务被用户取消', cancelled: true },
509 formatted: '任务被用户取消',
• Edited src/cli/index.js (+1 -1)
523 persistConversation();
524 - if (toolResult.raw && toolResult.raw.success === false && toolResult.error === '任务被用户取消') {
524 + if (toolResult.raw && toolResult.raw.success === false && toolResult.raw.error === '任务被用户取消') {
525 finishRun();
• Edited src/ui/tool_display.js (+1 -0)
219 });
220 + process.stdout.write('\n');
221 }
• Edited src/cli/index.js (+4 -6)
403 clearTimeout(thinkingDelay);
404 - if (state.thinkingMode) {
404 + if (escPendingCancel) {
405 + spinner.stopSilent();
406 + } else if (state.thinkingMode) {
407 spinner.stop('∙');
427 clearTimeout(thinkingDelay);
426 - if (state.thinkingMode) {
427 - spinner.stop('∙');
428 - } else {
429 - spinner.stopSilent();
430 - }
428 + spinner.stopSilent();
429 showCursor();
• Edited src/cli/index.js (+8 -3)
20 const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
21 -const { gray, cyan, green } = require('../utils/colors');
21 +const { gray, cyan, green, red } = require('../utils/colors');
22 const { createIndentedWriter } = require('../ui/indented_writer');
78
79 +function printCancelLine() {
80 + console.log('');
81 + process.stdout.write(` └ ${red('已取消本次响应')}\n\n`);
82 +}
83 +
84 readline.emitKeypressEvents(process.stdin);
419 finishRun();
415 - console.log('已取消本次响应');
420 + printCancelLine();
421 return;
435 finishRun();
431 - console.log('已取消本次响应');
436 + printCancelLine();
437 return;
• Edited src/cli/index.js (+1 -1)
80 console.log('');
81 - process.stdout.write(` └ ${red('已取消本次响应')}\n\n`);
81 + process.stdout.write(` ${red('已取消本次响应')}\n\n`);
82 }
• Edited src/ui/status_bar.js (+7 -1)
18 const total = typeof getTokens === 'function' ? getTokens() : 0;
19 - const right = `当前上下文token ${total}/${maxTokens}`;
19 + const formatCount = (value) => {
20 + const num = Number(value) || 0;
21 + if (num < 1000) return String(num);
22 + const k = Math.round((num / 1000) * 10) / 10;
23 + return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
24 + };
25 + const right = `当前上下文 ${formatCount(total)}/${maxTokens}`;
26 const leftWidth = visibleWidth(left);
• Edited prompts/system.txt (+1 -0)
5 - 当前时间:{current_time}
6 +- 当前模型:{model_id}
7 - 工作区路径:{path}
• Edited src/core/context.js (+1 -0)
50 git: getGitInfo(opts.workspace),
51 + model_id: opts.modelId || '',
52 };
• Edited src/cli/index.js (+1 -1)
277 function buildApiMessages() {
278 - const system = buildSystemPrompt(systemPrompt, { workspace: WORKSPACE, allowMode: state.allowMode });
278 + const system = buildSystemPrompt(systemPrompt, { workspace: WORKSPACE, allowMode: state.allowMode, modelId: state.modelId
|| state.modelKey });
279 const messages = [{ role: 'system', content: system }];
• Edited src/core/context.js (+1 -1)
39 const allowModeValue = opts.allowMode === 'read_only'
40 - ? `${opts.allowMode}\n 已禁用 edit_file、run_command。若用户要求使用请告知当前无权限需要请求切换权限。`
40 + ? `${opts.allowMode}\n 已禁用 edit_file、run_command。若用户要求使用请告知当前无权限需要用户输入 /allow 切换权限。`
41 : opts.allowMode;
• Edited src/tools/dispatcher.js (+1 -1)
155 if (allowMode === 'read_only' && (name === 'edit_file' || name === 'run_command')) {
156 - const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用请先请求切换权限。';
156 + const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用请告知当前无权限需要用户输入 /allow 切换权限。';
157 return {
• Edited src/storage/conversation_store.js (+12 -3)
36 for (const msg of messages) {
37 - if (msg.role === 'user' && typeof msg.content === 'string' && msg.content.trim()) {
38 - const content = msg.content.trim();
39 - return content.length > 50 ? `${content.slice(0, 50)}...` : content;
37 + if (msg.role === 'user') {
38 + let content = '';
39 + if (typeof msg.content === 'string') {
40 + content = msg.content.trim();
41 + } else if (Array.isArray(msg.content)) {
42 + content = msg.content
43 + .filter((part) => part && part.type === 'text' && typeof part.text === 'string')
44 + .map((part) => part.text)
45 + .join(' ')
46 + .trim();
47 + }
48 + if (content) return content.length > 50 ? `${content.slice(0, 50)}...` : content;
49 }
• Edited src/cli/index.js (+132 -1)
20 const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
21 -const { gray, cyan, green, red } = require('../utils/colors');
21 +const { gray, cyan, green, red, blue } = require('../utils/colors');
22 const { createIndentedWriter } = require('../ui/indented_writer');
23 const { createStatusBar } = require('../ui/status_bar');
24 +const { visibleWidth } = require('../utils/text_width');
25 +const { readMediafileTool } = require('../tools/read_mediafile');
26
66 let activeToolController = null;
67 +let currentMedia = { tokens: [], text: '' };
68 +
69 +const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.heic']);
70 +const VIDEO_EXTS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v']);
71 +
72 +function getPathExt(p) {
73 + const idx = p.lastIndexOf('.');
74 + if (idx === -1) return '';
75 + return p.slice(idx).toLowerCase();
76 +}
77 +
78 +function decodeEscapedPath(p) {
79 + return p.replace(/\\ /g, ' ').replace(/\\\\/g, '\\');
80 +}
81 +
82 +function findMediaMatches(line) {
83 + const matches = [];
84 + const quoted = /'([^']+)'/g;
85 + let m;
86 + while ((m = quoted.exec(line)) !== null) {
87 + matches.push({ raw: m[0], path: m[1], index: m.index });
88 + }
89 + const extGroup = Array.from(new Set([...IMAGE_EXTS, ...VIDEO_EXTS]))
90 + .map((e) => e.replace('.', '\\.'))
91 + .join('|');
92 + const unquoted = new RegExp(`/((?:\\\\ |[^\\s])+?)\\.(${extGroup})`, 'g');
93 + while ((m = unquoted.exec(line)) !== null) {
94 + const raw = `/${m[1]}.${m[2]}`;
95 + matches.push({ raw, path: raw, index: m.index });
96 + }
97 + matches.sort((a, b) => a.index - b.index);
98 + return matches;
99 +}
100 +
101 +function applyMediaTokens(line) {
102 + if (!line) return { line: '', tokens: [] };
103 + const matches = findMediaMatches(line);
104 + if (!matches.length) return { line, tokens: [] };
105 +
106 + let images = 0;
107 + let videos = 0;
108 + let cursor = 0;
109 + let out = '';
110 + const tokens = [];
111 +
112 + for (const match of matches) {
113 + if (match.index < cursor) continue;
114 + const before = line.slice(cursor, match.index);
115 + out += before;
116 + const decoded = decodeEscapedPath(match.path);
117 + const ext = getPathExt(decoded);
118 + const isImage = IMAGE_EXTS.has(ext);
119 + const isVideo = VIDEO_EXTS.has(ext);
120 + if (!isImage && !isVideo) {
121 + out += match.raw;
122 + cursor = match.index + match.raw.length;
123 + continue;
124 + }
125 + if (isImage && images >= 9) {
126 + out += match.raw;
127 + cursor = match.index + match.raw.length;
128 + continue;
129 + }
130 + if (isVideo && videos >= 1) {
131 + out += match.raw;
132 + cursor = match.index + match.raw.length;
133 + continue;
134 + }
135 + const token = isImage ? `[图片 #${images + 1}]` : `[视频 #${videos + 1}]`;
136 + tokens.push({ token, path: decoded, type: isImage ? 'image' : 'video' });
137 + out += token;
138 + if (isImage) images += 1;
139 + if (isVideo) videos += 1;
140 + cursor = match.index + match.raw.length;
141 + }
142 + out += line.slice(cursor);
143 + return { line: out, tokens };
144 +}
145 +
146 +function colorizeTokens(line) {
147 + return line.replace(/\[(图片|视频) #\d+\]/g, (t) => blue(t));
148 +}
149 +
150 +function refreshInputLine() {
151 + if (!rl || !process.stdout.isTTY || commandMenuActive) return;
152 + const line = rl.line || '';
153 + const colorLine = colorizeTokens(line);
154 + readline.clearLine(process.stdout, 0);
155 + readline.cursorTo(process.stdout, 0);
156 + process.stdout.write(PROMPT + colorLine);
157 + const cursorCol = visibleWidth(PROMPT) + visibleWidth(line.slice(0, rl.cursor || 0));
158 + readline.cursorTo(process.stdout, cursorCol);
159 +}
160 +
161 +function buildUserContent(line, tokens) {
162 + if (!tokens.length) return line;
163 + const parts = [];
164 + const tokenRe = /\[(图片|视频) #\d+\]/g;
165 + let last = 0;
166 + let m;
167 + while ((m = tokenRe.exec(line)) !== null) {
168 + if (m.index > last) {
169 + const text = line.slice(last, m.index);
170 + if (text) parts.push({ type: 'text', text });
171 + }
172 + const token = m[0];
173 + const info = tokens.find((t) => t.token === token);
174 + if (info) {
175 + const media = readMediafileTool(WORKSPACE, { path: info.path });
176 + if (media && media.success) {
177 + const url = `data:${media.mime};base64,${media.b64}`;
178 + parts.push({
179 + type: media.type === 'image' ? 'image_url' : 'video_url',
180 + [media.type === 'image' ? 'image_url' : 'video_url']: { url },
181 + });
182 + } else {
183 + parts.push({ type: 'text', text: token });
184 + }
185 + } else {
186 + parts.push({ type: 'text', text: token });
187 + }
188 + last = tokenRe.lastIndex;
189 + }
190 + if (last < line.length) {
191 + const tail = line.slice(last);
192 + if (tail) parts.push({ type: 'text', text: tail });
193 + }
194 + return parts;
195 +}
196
• Edited src/cli/index.js (+11 -0)
244 }
245 + if (!isRunning) {
246 + const applied = applyMediaTokens(rl.line || '');
247 + if (applied.line !== (rl.line || '')) {
248 + rl.line = applied.line;
249 + rl.cursor = rl.line.length;
250 + currentMedia = { tokens: applied.tokens, text: applied.line };
251 + } else if (currentMedia.text !== applied.line) {
252 + currentMedia = { tokens: applied.tokens, text: applied.line };
253 + }
254 + refreshInputLine();
255 + }
256 if (statusBar) statusBar.render();
• Edited src/cli/index.js (+8 -3)
346 }
347 - const input = line.trim();
347 + const applied = applyMediaTokens(line);
348 + const normalizedLine = applied.line;
349 + currentMedia = { tokens: applied.tokens, text: normalizedLine };
350 + const input = normalizedLine.trim();
351 if (menuJustClosedAt) {
382 const userWriter = createIndentedWriter(' ');
380 - userWriter.writeLine(`${cyan('用户:')}${line}`);
381 - state.messages.push({ role: 'user', content: line });
383 + const displayLine = colorizeTokens(normalizedLine);
384 + userWriter.writeLine(`${cyan('用户:')}${displayLine}`);
385 + const content = buildUserContent(normalizedLine, currentMedia.tokens);
386 + state.messages.push({ role: 'user', content });
387 persistConversation();
• Edited src/cli/index.js (+1 -1)
89 const extGroup = Array.from(new Set([...IMAGE_EXTS, ...VIDEO_EXTS]))
90 - .map((e) => e.replace('.', '\\.'))
90 + .map((e) => e.slice(1))
91 .join('|');
• Edited src/cli/index.js (+7 -0)
31 const MENU_PAGE_SIZE = 6;
32 +const SLASH_COMMANDS = new Set(['/new', '/resume', '/allow', '/model', '/status', '/compact', '/config', '/help', '/exit']);
33
197
198 +function isSlashCommand(line) {
199 + if (!line || line[0] !== '/') return false;
200 + const cmd = line.split(/\s+/)[0];
201 + return SLASH_COMMANDS.has(cmd);
202 +}
203 +
204 function promptWithStatus(force = false) {
• Edited src/cli/index.js (+2 -1)
252 if (!isRunning) {
253 - const applied = applyMediaTokens(rl.line || '');
253 + const rawLine = rl.line || '';
254 + const applied = isSlashCommand(rawLine) ? { line: rawLine, tokens: [] } : applyMediaTokens(rawLine);
255 if (applied.line !== (rl.line || '')) {
• Edited src/cli/index.js (+1 -1)
354 }
355 - const applied = applyMediaTokens(line);
355 + const applied = isSlashCommand(line) ? { line, tokens: [] } : applyMediaTokens(line);
356 const normalizedLine = applied.line;
• Edited src/cli/index.js (+5 -0)
117 const decoded = decodeEscapedPath(match.path);
118 + if (!fs.existsSync(decoded)) {
119 + out += match.raw;
120 + cursor = match.index + match.raw.length;
121 + continue;
122 + }
123 const ext = getPathExt(decoded);
• Edited src/cli/index.js (+92 -52)
68 let currentMedia = { tokens: [], text: '' };
69 +let pendingSlashTimer = null;
70
167
168 +function removeTokenAtCursor(line, cursor) {
169 + const tokenRe = /\[(图片|视频) #\d+\]/g;
170 + let m;
171 + while ((m = tokenRe.exec(line)) !== null) {
172 + const start = m.index;
173 + const end = m.index + m[0].length;
174 + if (cursor > start && cursor <= end) {
175 + const nextLine = line.slice(0, start) + line.slice(end);
176 + return { line: nextLine, cursor: start, removed: true };
177 + }
178 + }
179 + return { line, cursor, removed: false };
180 +}
181 +
182 function buildUserContent(line, tokens) {
261 }
262 + if (!isRunning && key && key.name === 'backspace') {
263 + const line = rl.line || '';
264 + const cursor = rl.cursor || 0;
265 + const updated = removeTokenAtCursor(line, cursor);
266 + if (updated.removed) {
267 + rl.line = updated.line;
268 + rl.cursor = updated.cursor;
269 + refreshInputLine();
270 + return;
271 + }
272 + }
273 if (key && key.name === 'escape' && isRunning) {
282 }
283 + if (pendingSlashTimer && (str !== '/' || (rl.line && rl.line !== '/'))) {
284 + clearTimeout(pendingSlashTimer);
285 + pendingSlashTimer = null;
286 + }
287 if (!isRunning) {
300 if (str === '/' && (rl.line === '' || rl.line === '/')) {
271 - commandMenuActive = true;
272 - if (rl) {
273 - rl.pause();
274 - rl.line = '';
275 - rl.cursor = 0;
276 - readline.clearLine(process.stdout, 0);
277 - readline.cursorTo(process.stdout, 0);
301 + if (pendingSlashTimer) {
302 + clearTimeout(pendingSlashTimer);
303 + pendingSlashTimer = null;
304 }
279 - menuSearchTerm = '';
280 - menuAbortController = new AbortController();
281 - void openCommandMenu({
282 - rl,
283 - prompt: PROMPT,
284 - pageSize: MENU_PAGE_SIZE,
285 - colorEnabled: process.stdout.isTTY,
286 - resetAnsi: '\x1b[0m',
287 - onInput: (input) => {
288 - menuSearchTerm = input || '';
289 - },
290 - abortSignal: menuAbortController.signal,
291 - })
292 - .then((result) => {
293 - if (result && result.chosen && !result.cancelled) {
294 - menuInjectedCommand = result.chosen;
295 - }
296 - })
297 - .finally(() => {
298 - commandMenuActive = false;
299 - menuAbortController = null;
300 - menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
301 - menuLastSearchTerm = menuSearchTerm;
302 - drainStdin();
303 - rl.line = '';
304 - rl.cursor = 0;
305 - menuSearchTerm = '';
306 - if (process.stdout.isTTY) {
307 - readline.clearScreenDown(process.stdout);
308 - }
309 - // Clear possible echoes from the base readline line (current + previous line).
310 - if (process.stdout.isTTY) {
311 - readline.clearLine(process.stdout, 0);
312 - readline.cursorTo(process.stdout, 0);
313 - readline.moveCursor(process.stdout, 0, -1);
314 - readline.clearLine(process.stdout, 0);
315 - readline.cursorTo(process.stdout, 0);
316 - } else {
305 + pendingSlashTimer = setTimeout(() => {
306 + pendingSlashTimer = null;
307 + if (rl.line === '' || rl.line === '/') {
308 + commandMenuActive = true;
309 + if (rl) {
310 + rl.pause();
311 + rl.line = '';
312 + rl.cursor = 0;
313 readline.clearLine(process.stdout, 0);
315 }
320 - promptWithStatus(true);
321 - if (menuInjectedCommand) {
322 - const injected = menuInjectedCommand;
323 - menuInjectedCommand = null;
324 - setImmediate(() => injectLine(injected));
325 - }
326 - });
316 + menuSearchTerm = '';
317 + menuAbortController = new AbortController();
318 + void openCommandMenu({
319 + rl,
320 + prompt: PROMPT,
321 + pageSize: MENU_PAGE_SIZE,
322 + colorEnabled: process.stdout.isTTY,
323 + resetAnsi: '\x1b[0m',
324 + onInput: (input) => {
325 + menuSearchTerm = input || '';
326 + },
327 + abortSignal: menuAbortController.signal,
328 + })
329 + .then((result) => {
330 + if (result && result.chosen && !result.cancelled) {
331 + menuInjectedCommand = result.chosen;
332 + }
333 + })
334 + .finally(() => {
335 + commandMenuActive = false;
336 + menuAbortController = null;
337 + menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
338 + menuLastSearchTerm = menuSearchTerm;
339 + drainStdin();
340 + rl.line = '';
341 + rl.cursor = 0;
342 + menuSearchTerm = '';
343 + if (process.stdout.isTTY) {
344 + readline.clearScreenDown(process.stdout);
345 + }
346 + // Clear possible echoes from the base readline line (current + previous line).
347 + if (process.stdout.isTTY) {
348 + readline.clearLine(process.stdout, 0);
349 + readline.cursorTo(process.stdout, 0);
350 + readline.moveCursor(process.stdout, 0, -1);
351 + readline.clearLine(process.stdout, 0);
352 + readline.cursorTo(process.stdout, 0);
353 + } else {
354 + readline.clearLine(process.stdout, 0);
355 + readline.cursorTo(process.stdout, 0);
356 + }
357 + promptWithStatus(true);
358 + if (menuInjectedCommand) {
359 + const injected = menuInjectedCommand;
360 + menuInjectedCommand = null;
361 + setImmediate(() => injectLine(injected));
362 + }
363 + });
364 + }
365 + }, 80);
366 + return;
367 }
• Edited src/cli/index.js (+9 -1)
399 }
400 - const applied = isSlashCommand(line) ? { line, tokens: [] } : applyMediaTokens(line);
400 + let applied = { line, tokens: [] };
401 + if (!isSlashCommand(line)) {
402 + const hasToken = /\[(图片|视频) #\d+\]/.test(line);
403 + if (hasToken && currentMedia.tokens.length && currentMedia.text === line) {
404 + applied = { line, tokens: currentMedia.tokens };
405 + } else {
406 + applied = applyMediaTokens(line);
407 + }
408 + }
409 const normalizedLine = applied.line;
• Edited src/cli/index.js (+8 -1)
80 function decodeEscapedPath(p) {
81 - return p.replace(/\\ /g, ' ').replace(/\\\\/g, '\\');
81 + let text = String(p || '');
82 + if (text.startsWith('file://')) {
83 + text = text.replace(/^file:\/\//, '');
84 + }
85 + try {
86 + text = decodeURIComponent(text);
87 + } catch (_) {}
88 + return text.replace(/\\ /g, ' ').replace(/\\\\/g, '\\');
89 }
• Edited src/cli/index.js (+0 -0)
• Edited src/cli/index.js (+9 -29)
190 if (!tokens.length) return line;
191 - const parts = [];
192 - const tokenRe = /\[(图片|视频) #\d+\]/g;
193 - let last = 0;
194 - let m;
195 - while ((m = tokenRe.exec(line)) !== null) {
196 - if (m.index > last) {
197 - const text = line.slice(last, m.index);
198 - if (text) parts.push({ type: 'text', text });
199 - }
200 - const token = m[0];
201 - const info = tokens.find((t) => t.token === token);
202 - if (info) {
203 - const media = readMediafileTool(WORKSPACE, { path: info.path });
204 - if (media && media.success) {
205 - const url = `data:${media.mime};base64,${media.b64}`;
206 - parts.push({
207 - type: media.type === 'image' ? 'image_url' : 'video_url',
208 - [media.type === 'image' ? 'image_url' : 'video_url']: { url },
209 - });
210 - } else {
211 - parts.push({ type: 'text', text: token });
212 - }
213 - } else {
214 - parts.push({ type: 'text', text: token });
191 + const parts = [{ type: 'text', text: line }];
192 + for (const info of tokens) {
193 + const media = readMediafileTool(WORKSPACE, { path: info.path });
194 + if (media && media.success) {
195 + const url = `data:${media.mime};base64,${media.b64}`;
196 + parts.push({
197 + type: media.type === 'image' ? 'image_url' : 'video_url',
198 + [media.type === 'image' ? 'image_url' : 'video_url']: { url },
199 + });
200 }
216 - last = tokenRe.lastIndex;
217 - }
218 - if (last < line.length) {
219 - const tail = line.slice(last);
220 - if (tail) parts.push({ type: 'text', text: tail });
201 }
• Edited src/cli/index.js (+1 -0)
433 const content = buildUserContent(normalizedLine, currentMedia.tokens);
434 + console.log('DEBUG_USER_CONTENT', JSON.stringify(content, null, 2));
435 state.messages.push({ role: 'user', content });
• Edited src/cli/index.js (+19 -0)
68 let currentMedia = { tokens: [], text: '' };
69 +let suppressSlashMenuUntil = 0;
70 let pendingSlashTimer = null;
232
233 +process.stdin.on('data', (chunk) => {
234 + if (isRunning || commandMenuActive || !rl) return;
235 + const text = chunk ? chunk.toString() : '';
236 + if (!text) return;
237 + const looksLikePath = text.includes('file://') || /\.(png|jpe?g|gif|webp|bmp|tiff|heic|mp4|mov|avi|mkv|webm|m4v)\b/i.test(
text);
238 + if (!looksLikePath) return;
239 + suppressSlashMenuUntil = Date.now() + 200;
240 + setImmediate(() => {
241 + const applied = applyMediaTokens(rl.line || '');
242 + if (applied.tokens.length) {
243 + rl.line = applied.line;
244 + rl.cursor = rl.line.length;
245 + currentMedia = { tokens: applied.tokens, text: applied.line };
246 + refreshInputLine();
247 + }
248 + });
249 +});
250 +
251 initReadline();
• Edited src/cli/index.js (+3 -0)
288 }
289 + if (str === '/' && Date.now() < suppressSlashMenuUntil) {
290 + return;
291 + }
292 if (pendingSlashTimer && (str !== '/' || (rl.line && rl.line !== '/'))) {
• Edited src/cli/index.js (+0 -0)
• Edited src/cli/index.js (+9 -1)
297 const rawLine = rl.line || '';
298 - const applied = isSlashCommand(rawLine) ? { line: rawLine, tokens: [] } : applyMediaTokens(rawLine);
298 + let applied = { line: rawLine, tokens: [] };
299 + if (!isSlashCommand(rawLine)) {
300 + const hasToken = /\[(图片|视频) #\d+\]/.test(rawLine);
301 + if (hasToken && currentMedia.tokens.length && currentMedia.text === rawLine) {
302 + applied = { line: rawLine, tokens: currentMedia.tokens };
303 + } else {
304 + applied = applyMediaTokens(rawLine);
305 + }
306 + }
307 if (applied.line !== (rl.line || '')) {
• Edited src/cli/index.js (+0 -1)
463 const content = buildUserContent(normalizedLine, currentMedia.tokens);
464 - console.log('DEBUG_USER_CONTENT', JSON.stringify(content, null, 2));
464 state.messages.push({ role: 'user', content });
• Edited src/cli/index.js (+3 -0)
305 }
306 + if (hasToken && !applied.tokens.length && currentMedia.tokens.length) {
307 + applied = { line: rawLine, tokens: currentMedia.tokens };
308 + }
309 }
• Edited src/cli/index.js (+3 -0)
427 }
428 + if (hasToken && !applied.tokens.length && currentMedia.tokens.length) {
429 + applied = { line, tokens: currentMedia.tokens };
430 + }
431 }
• Edited src/cli/commands.js (+2 -1)
58 allow_mode: state.allowMode,
59 - token_usage: state.tokenUsage,
59 + token_usage: { prompt: 0, completion: 0, total: 0 },
60 cwd: workspace,
63 state.messages = [];
64 + state.tokenUsage = { prompt: 0, completion: 0, total: 0 };
65 console.log(`已创建新对话: ${conv.id}`);
• Edited src/cli/commands.js (+2 -2)
53 if (cmd === '/new') {
54 + state.tokenUsage = normalizeTokenUsage({ prompt: 0, completion: 0, total: 0 });
55 const conv = createConversation(workspace, {
59 allow_mode: state.allowMode,
59 - token_usage: { prompt: 0, completion: 0, total: 0 },
60 + token_usage: state.tokenUsage,
61 cwd: workspace,
64 state.messages = [];
64 - state.tokenUsage = { prompt: 0, completion: 0, total: 0 };
65 console.log(`已创建新对话: ${conv.id}`);
• Edited src/cli/index.js (+1 -1)
458 if (input.startsWith('/')) {
459 - const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE });
459 + const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
460 if (result && result.exit) return;
• Edited src/cli/commands.js (+2 -1)
27 async function handleCommand(input, ctx) {
28 - const { rl, state, config, workspace } = ctx;
28 + const { rl, state, config, workspace, statusBar } = ctx;
29 const persist = () => {
66 persist();
67 + if (statusBar) statusBar.render();
68 return { exit: false };
• Edited src/cli/index.js (+18 -0)
31 const MENU_PAGE_SIZE = 6;
32 +const DEBUG_LOG_DIR = path.join(WORKSPACE, 'logs');
33 +const DEBUG_LOG_PATH = path.join(DEBUG_LOG_DIR, 'cli-debug.log');
34 +const DEBUG_LOG_ENABLED = true;
35 const SLASH_COMMANDS = new Set(['/new', '/resume', '/allow', '/model', '/status', '/compact', '/config', '/help', '/exit']);
232
233 +function debugLog(event, data = {}) {
234 + if (!DEBUG_LOG_ENABLED) return;
235 + try {
236 + if (!fs.existsSync(DEBUG_LOG_DIR)) fs.mkdirSync(DEBUG_LOG_DIR, { recursive: true });
237 + const payload = {
238 + ts: new Date().toISOString(),
239 + event,
240 + ...data,
241 + };
242 + fs.appendFileSync(DEBUG_LOG_PATH, `${JSON.stringify(payload)}\n`);
243 + } catch (_) {}
244 +}
245 +
246 readline.emitKeypressEvents(process.stdin);
273 statusBar.render();
274 +debugLog('statusbar_init', { token_total: normalizeTokenUsage(state.tokenUsage).total || 0 });
275
476 const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
477 + debugLog('after_command', { cmd: input, token_total: normalizeTokenUsage(state.tokenUsage).total || 0 });
478 if (result && result.exit) return;
• Edited src/cli/commands.js (+3 -0)
67 if (statusBar) statusBar.render();
68 + if (typeof ctx.debugLog === 'function') {
69 + ctx.debugLog('command_new', { token_total: state.tokenUsage.total || 0 });
70 + }
71 return { exit: false };
• Edited src/cli/index.js (+1 -1)
475 if (input.startsWith('/')) {
476 - const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
476 + const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar, debugLog });
477 debugLog('after_command', { cmd: input, token_total: normalizeTokenUsage(state.tokenUsage).total || 0 });
• Edited src/cli/index.js (+7 -0)
276 process.stdin.on('keypress', (str, key) => {
277 + debugLog('keypress', { str, key: key && key.name, line: rl ? rl.line : '' });
278 if (commandMenuActive) {
345 if (rl.line === '' || rl.line === '/') {
346 + debugLog('menu_open');
347 commandMenuActive = true;
368 .then((result) => {
369 + debugLog('menu_result', { result });
370 if (result && result.chosen && !result.cancelled) {
374 .finally(() => {
375 + debugLog('menu_close', { injected: menuInjectedCommand, line: rl ? rl.line : '' });
376 commandMenuActive = false;
401 menuInjectedCommand = null;
402 + debugLog('menu_inject', { injected });
403 setImmediate(() => injectLine(injected));
428 rl.on('line', async (line) => {
429 + debugLog('line', { line });
430 if (statusBar) statusBar.render();
481 if (input.startsWith('/')) {
482 + debugLog('slash_command', { input });
483 const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar, debugLog });
• Edited src/cli/index.js (+20 -15)
65 let menuAbortController = null;
66 +let menuJustClosedInjected = false;
67 let statusBar = null;
377 commandMenuActive = false;
377 - menuAbortController = null;
378 - menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
379 - menuLastSearchTerm = menuSearchTerm;
378 + menuAbortController = null;
379 + menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
380 + menuJustClosedInjected = !!menuInjectedCommand;
381 + menuLastSearchTerm = menuSearchTerm;
382 drainStdin();
460 if (menuJustClosedAt) {
459 - const tooOld = Date.now() - menuJustClosedAt > 800;
460 - const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, '');
461 - const normalizedInput = input.replace(/^\/+/, '');
462 - if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
463 - menuJustClosedAt = 0;
464 - menuLastSearchTerm = '';
465 - if (process.stdout.isTTY) {
466 - readline.moveCursor(process.stdout, 0, -1);
467 - readline.clearLine(process.stdout, 0);
468 - readline.cursorTo(process.stdout, 0);
461 + if (!menuJustClosedInjected) {
462 + const tooOld = Date.now() - menuJustClosedAt > 800;
463 + const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, '');
464 + const normalizedInput = input.replace(/^\/+/, '');
465 + if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
466 + menuJustClosedAt = 0;
467 + menuLastSearchTerm = '';
468 + if (process.stdout.isTTY) {
469 + readline.moveCursor(process.stdout, 0, -1);
470 + readline.clearLine(process.stdout, 0);
471 + readline.cursorTo(process.stdout, 0);
472 + }
473 + promptWithStatus();
474 + return;
475 }
470 - promptWithStatus();
471 - return;
476 }
478 menuLastSearchTerm = '';
479 + menuJustClosedInjected = false;
480 }
• Edited src/cli/index.js (+1 -25)
31 const MENU_PAGE_SIZE = 6;
32 -const DEBUG_LOG_DIR = path.join(WORKSPACE, 'logs');
33 -const DEBUG_LOG_PATH = path.join(DEBUG_LOG_DIR, 'cli-debug.log');
34 -const DEBUG_LOG_ENABLED = true;
32 const SLASH_COMMANDS = new Set(['/new', '/resume', '/allow', '/model', '/status', '/compact', '/config', '/help', '/exit']);
230
234 -function debugLog(event, data = {}) {
235 - if (!DEBUG_LOG_ENABLED) return;
236 - try {
237 - if (!fs.existsSync(DEBUG_LOG_DIR)) fs.mkdirSync(DEBUG_LOG_DIR, { recursive: true });
238 - const payload = {
239 - ts: new Date().toISOString(),
240 - event,
241 - ...data,
242 - };
243 - fs.appendFileSync(DEBUG_LOG_PATH, `${JSON.stringify(payload)}\n`);
244 - } catch (_) {}
245 -}
231
259 statusBar.render();
275 -debugLog('statusbar_init', { token_total: normalizeTokenUsage(state.tokenUsage).total || 0 });
260
261 process.stdin.on('keypress', (str, key) => {
278 - debugLog('keypress', { str, key: key && key.name, line: rl ? rl.line : '' });
262 if (commandMenuActive) {
329 if (rl.line === '' || rl.line === '/') {
347 - debugLog('menu_open');
330 commandMenuActive = true;
351 .then((result) => {
370 - debugLog('menu_result', { result });
352 if (result && result.chosen && !result.cancelled) {
356 .finally(() => {
376 - debugLog('menu_close', { injected: menuInjectedCommand, line: rl ? rl.line : '' });
357 commandMenuActive = false;
383 menuInjectedCommand = null;
404 - debugLog('menu_inject', { injected });
384 setImmediate(() => injectLine(injected));
409 rl.on('line', async (line) => {
431 - debugLog('line', { line });
410 if (statusBar) statusBar.render();
464 if (input.startsWith('/')) {
487 - debugLog('slash_command', { input });
488 - const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar, debugLog });
489 - debugLog('after_command', { cmd: input, token_total: normalizeTokenUsage(state.tokenUsage).total || 0 });
465 + const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
466 if (result && result.exit) return;
• Edited src/cli/commands.js (+0 -3)
67 if (statusBar) statusBar.render();
68 - if (typeof ctx.debugLog === 'function') {
69 - ctx.debugLog('command_new', { token_total: state.tokenUsage.total || 0 });
70 - }
68 return { exit: false };
• Edited src/cli/index.js (+9 -0)
230
231 +function stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, thinkingMode) {
232 + if (!spinner) return;
233 + if (thinkingMode && (thinkingActive || showThinkingLabel)) {
234 + spinner.stop('∙ 停止思考');
235 + } else {
236 + spinner.stopSilent();
237 + }
238 +}
239 +
240
• Edited src/cli/index.js (+2 -2)
655 if (escPendingCancel) {
656 - spinner.stopSilent();
656 + stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, state.thinkingMode);
657 } else if (state.thinkingMode) {
678 clearTimeout(thinkingDelay);
679 - spinner.stopSilent();
679 + stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, state.thinkingMode);
680 showCursor();
• Edited src/cli/commands.js (+18 -10)
26
27 +function printNotice(message) {
28 + console.log('');
29 + console.log(message);
30 + console.log('');
31 +}
32 +
33 async function handleCommand(input, ctx) {
70 state.messages = [];
65 - console.log(`已创建新对话: ${conv.id}`);
71 + printNotice(`已创建新对话: ${conv.id}`);
72 persist();
80 if (!conv) {
75 - console.log('未找到对话');
81 + printNotice('未找到对话');
82 return { exit: false };
88 state.tokenUsage = normalizeTokenUsage(conv.metadata?.token_usage);
83 - console.log(`已加载对话: ${conv.id}`);
89 + printNotice(`已加载对话: ${conv.id}`);
90 renderConversation(state.messages);
96 if (!items.length) {
91 - console.log('暂无对话记录');
97 + printNotice('暂无对话记录');
98 return { exit: false };
102 if (!filtered.length) {
97 - console.log('暂无可恢复的对话');
103 + printNotice('暂无可恢复的对话');
104 return { exit: false };
130 if (!conv) {
125 - console.log('未找到对话');
131 + printNotice('未找到对话');
132 return { exit: false };
138 state.tokenUsage = normalizeTokenUsage(conv.metadata?.token_usage);
133 - console.log(`已加载对话: ${conv.id}`);
139 + printNotice(`已加载对话: ${conv.id}`);
140 renderConversation(state.messages);
158 state.allowMode = selected;
153 - console.log(`运行模式已切换为: ${state.allowMode}`);
159 + printNotice(`运行模式已切换为: ${state.allowMode}`);
160 persist();
180 state.thinkingMode = mode === 'thinking';
175 - console.log(`模型已切换为: ${state.modelKey} | 思考模式: ${mode}`);
181 + printNotice(`模型已切换为: ${state.modelKey} | 思考模式: ${mode}`);
182 persist();
204 if (cmd === '/config') {
205 + console.log('');
206 console.log(`base_url: ${config.base_url}`);
208 console.log(`apikey: ${maskKey(config.api_key)}`);
209 + console.log('');
210 return { exit: false };
246 spinner.stop('○');
239 - console.log(`压缩完成:${oldId} -> ${state.conversation.id}`);
247 + printNotice(`压缩完成:${oldId} -> ${state.conversation.id}`);
248 persist();
• Edited src/cli/commands.js (+3 -3)
155 ];
156 - const selected = await runSelect({ rl, message: 'Update Permissions', choices, pageSize: 6 });
156 + const selected = await runSelect({ rl, message: '', choices, pageSize: 6 });
157 if (selected) {
168 ];
169 - const model = await runSelect({ rl, message: 'Update Model', choices: modelChoices, pageSize: 6 });
169 + const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
170 if (!model) return { exit: false };
177 ];
178 - const mode = await runSelect({ rl, message: 'Update Thinking Mode', choices: thinkingChoices, pageSize: 6 });
178 + const mode = await runSelect({ rl, message: '', choices: thinkingChoices, pageSize: 6 });
179 if (mode) {
• Edited src/cli/index.js (+6 -0)
47
48 +function printNotice(message) {
49 + console.log('');
50 + console.log(message);
51 + console.log('');
52 +}
53 +
54 renderBanner({
• Edited src/cli/index.js (+5 -1)
478
479 - if (input.startsWith('/')) {
479 + if (input.startsWith('/') && isSlashCommand(input)) {
480 const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
485
486 + if (input.startsWith('/') && !isSlashCommand(input)) {
487 + printNotice(`无效的命令“${input}”`);
488 + }
489 +
490 console.log('');
• Edited src/ui/command_menu.js (+5 -3)
19 let latestInput = '';
20 + let lastCount = 0;
21
61 : COMMAND_CHOICES;
62 + lastCount = filtered.length;
63 return filtered.map((c) => ({ name: format(c), value: c.value }));
65 }, abortSignal ? { signal: abortSignal, clearPromptOnDone: true } : { clearPromptOnDone: true });
64 - return { chosen, term: latestInput, cancelled: false };
66 + return { chosen, term: latestInput, cancelled: false, noResults: lastCount === 0 };
67 } catch (err) {
68 if (err && (err.name === 'AbortPromptError' || err.name === 'CancelPromptError')) {
67 - return { chosen: null, term: latestInput, cancelled: true };
69 + return { chosen: null, term: latestInput, cancelled: true, noResults: lastCount === 0 };
70 }
71 console.log('指令菜单不可用,请先安装依赖: npm i @inquirer/search');
70 - return { chosen: null, term: '', cancelled: true };
72 + return { chosen: null, term: '', cancelled: true, noResults: false };
73 } finally {
• Edited src/cli/index.js (+11 -0)
368 menuInjectedCommand = result.chosen;
369 + return;
370 + }
371 + if (
372 + result &&
373 + !result.cancelled &&
374 + !result.chosen &&
375 + result.noResults &&
376 + typeof result.term === 'string' &&
377 + result.term.trim().length > 0
378 + ) {
379 + menuInjectedCommand = `/${result.term.trim()}`;
380 }
• Edited src/ui/command_menu.js (+12 -6)
16
17 +function hasCommandMatch(term) {
18 + if (!term) return false;
19 + const t = term.toLowerCase();
20 + return COMMAND_CHOICES.some(
21 + (c) => c.value.toLowerCase().includes(t) || c.desc.toLowerCase().includes(t)
22 + );
23 +}
24 +
25 async function openCommandMenu(options) {
27 let latestInput = '';
20 - let lastCount = 0;
28
68 : COMMAND_CHOICES;
62 - lastCount = filtered.length;
69 return filtered.map((c) => ({ name: format(c), value: c.value }));
71 }, abortSignal ? { signal: abortSignal, clearPromptOnDone: true } : { clearPromptOnDone: true });
66 - return { chosen, term: latestInput, cancelled: false, noResults: lastCount === 0 };
72 + return { chosen, term: latestInput, cancelled: false };
73 } catch (err) {
74 if (err && (err.name === 'AbortPromptError' || err.name === 'CancelPromptError')) {
69 - return { chosen: null, term: latestInput, cancelled: true, noResults: lastCount === 0 };
75 + return { chosen: null, term: latestInput, cancelled: true };
76 }
77 console.log('指令菜单不可用,请先安装依赖: npm i @inquirer/search');
72 - return { chosen: null, term: '', cancelled: true, noResults: false };
78 + return { chosen: null, term: '', cancelled: true };
79 } finally {
87
82 -module.exports = { openCommandMenu };
88 +module.exports = { openCommandMenu, hasCommandMatch };
• Edited src/cli/index.js (+12 -12)
13 const { executeTool } = require('../tools/dispatcher');
14 -const { openCommandMenu } = require('../ui/command_menu');
14 +const { openCommandMenu, hasCommandMatch } = require('../ui/command_menu');
15 const { handleCommand } = require('./commands');
281 }
282 + return;
283 + }
284 + if (key && key.name === 'return') {
285 + const term = String(menuSearchTerm || '').replace(/^\/+/, '').trim();
286 + if (term && !hasCommandMatch(term)) {
287 + menuInjectedCommand = `/${term}`;
288 + if (menuAbortController && !menuAbortController.signal.aborted) {
289 + menuAbortController.abort();
290 + }
291 + }
292 + return;
293 }
379 menuInjectedCommand = result.chosen;
369 - return;
370 - }
371 - if (
372 - result &&
373 - !result.cancelled &&
374 - !result.chosen &&
375 - result.noResults &&
376 - typeof result.term === 'string' &&
377 - result.term.trim().length > 0
378 - ) {
379 - menuInjectedCommand = `/${result.term.trim()}`;
380 }
• Edited src/ui/command_menu.js (+1 -0)
47 highlight: (text) => (colorEnabled ? `\x1b[94m${text}${resetAnsi}` : text),
48 + error: () => '无效的指令',
49 keysHelpTip: () => '',
• Edited src/cli/index.js (+7 -2)
69 let menuJustClosedInjected = false;
70 +let menuInvalidCommand = '';
71 let statusBar = null;
287 if (term && !hasCommandMatch(term)) {
287 - menuInjectedCommand = `/${term}`;
288 + menuInvalidCommand = `/${term}`;
289 if (menuAbortController && !menuAbortController.signal.aborted) {
385 menuAbortController = null;
385 - menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
386 + menuJustClosedAt = menuInjectedCommand || menuInvalidCommand ? Date.now() : 0;
387 menuJustClosedInjected = !!menuInjectedCommand;
406 }
407 + if (menuInvalidCommand) {
408 + printNotice(`无效的命令“${menuInvalidCommand}”`);
409 + menuInvalidCommand = '';
410 + }
411 promptWithStatus(true);
• Edited src/ui/select_prompt.js (+2 -2)
4
5 -async function runSelect({ rl, message, choices, pageSize = 6 }) {
5 +async function runSelect({ rl, message, choices, pageSize = 6, clearPromptOnDone = true }) {
6 rl.pause();
25 },
26 - }, { clearPromptOnDone: true });
26 + }, { clearPromptOnDone });
27 readline.clearLine(process.stdout, 0);
• Edited src/cli/commands.js (+8 -1)
168 ];
169 - const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
169 + const model = await runSelect({
170 + rl,
171 + message: '',
172 + choices: modelChoices,
173 + pageSize: 6,
174 + clearPromptOnDone: false,
175 + });
176 if (!model) return { exit: false };
178 state.modelId = config.model_id || 'kimi-k2.5';
179 + console.log('');
180
• Edited src/ui/select_prompt.js (+2 -2)
4
5 -async function runSelect({ rl, message, choices, pageSize = 6, clearPromptOnDone = true }) {
5 +async function runSelect({ rl, message, choices, pageSize = 6 }) {
6 rl.pause();
25 },
26 - }, { clearPromptOnDone });
26 + }, { clearPromptOnDone: true });
27 readline.clearLine(process.stdout, 0);
• Edited src/cli/commands.js (+1 -8)
168 ];
169 - const model = await runSelect({
170 - rl,
171 - message: '',
172 - choices: modelChoices,
173 - pageSize: 6,
174 - clearPromptOnDone: false,
175 - });
169 + const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
170 if (!model) return { exit: false };
172 state.modelId = config.model_id || 'kimi-k2.5';
179 - console.log('');
173
• Edited src/ui/select_prompt.js (+3 -0)
26 }, { clearPromptOnDone: true });
27 + if (process.stdout.isTTY) {
28 + readline.clearScreenDown(process.stdout);
29 + }
30 readline.clearLine(process.stdout, 0);
• Edited src/ui/select_prompt.js (+28 -0)
2
3 +const fs = require('fs');
4 +const path = require('path');
5 const readline = require('readline');
6
7 +function debugLog(event, data = {}) {
8 + if (process.env.EAGENT_DEBUG !== '1') return;
9 + try {
10 + const dir = path.join(process.cwd(), 'logs');
11 + fs.mkdirSync(dir, { recursive: true });
12 + const payload = {
13 + ts: new Date().toISOString(),
14 + event,
15 + ...data,
16 + };
17 + fs.appendFileSync(path.join(dir, 'cli-debug.log'), `${JSON.stringify(payload)}\n`);
18 + } catch (_) {}
19 +}
20 +
21 async function runSelect({ rl, message, choices, pageSize = 6 }) {
26 readline.cursorTo(process.stdout, 0);
27 + debugLog('select_open', {
28 + message: message || '',
29 + choices: Array.isArray(choices) ? choices.length : 0,
30 + pageSize,
31 + tty: !!process.stdout.isTTY,
32 + rows: process.stdout.rows,
33 + columns: process.stdout.columns,
34 + });
35
37 const { default: select } = await import('@inquirer/select');
38 + debugLog('select_render');
39 const value = await select({
51 }, { clearPromptOnDone: true });
52 + debugLog('select_done', { value: value || null });
53 if (process.stdout.isTTY) {
57 readline.cursorTo(process.stdout, 0);
58 + debugLog('select_cleared');
59 return value;
65 } catch (_) {}
66 + debugLog('select_resume');
67 }
• Edited src/ui/select_prompt.js (+4 -0)
26 readline.cursorTo(process.stdout, 0);
27 + if (process.stdout.isTTY) {
28 + // Allow the select prompt to use the full screen; status bar will reapply later.
29 + process.stdout.write('\x1b[r');
30 + }
31 debugLog('select_open', {
• Edited src/ui/select_prompt.js (+2 -2)
27 if (process.stdout.isTTY) {
28 - // Allow the select prompt to use the full screen; status bar will reapply later.
29 - process.stdout.write('\x1b[r');
28 + process.stdout.write('\x1b7'); // save cursor position
29 }
56 if (process.stdout.isTTY) {
57 + process.stdout.write('\x1b8'); // restore cursor position
58 readline.clearScreenDown(process.stdout);
• Edited src/cli/commands.js (+2 -1)
172 state.modelId = config.model_id || 'kimi-k2.5';
173 + printNotice(`模型已切换为: ${state.modelKey}`);
174
181 state.thinkingMode = mode === 'thinking';
181 - printNotice(`模型已切换为: ${state.modelKey} | 思考模式: ${mode}`);
182 + printNotice(`思考模式: ${mode}`);
183 persist();
• Edited src/cli/commands.js (+8 -0)
165 if (cmd === '/model') {
166 + if (statusBar) statusBar.setEnabled(false);
167 + try {
168 const modelChoices = [
186 }
187 + } finally {
188 + if (statusBar) {
189 + statusBar.setEnabled(true);
190 + statusBar.render();
191 + }
192 + }
193 return { exit: false };
• Edited src/ui/select_prompt.js (+1 -0)
28 process.stdout.write('\x1b7'); // save cursor position
29 + process.stdout.write('\x1b[r'); // reset scroll region for full-screen menus
30 }
• Edited src/ui/select_prompt.js (+1 -2)
27 if (process.stdout.isTTY) {
28 - process.stdout.write('\x1b7'); // save cursor position
28 process.stdout.write('\x1b[r'); // reset scroll region for full-screen menus
56 if (process.stdout.isTTY) {
58 - process.stdout.write('\x1b8'); // restore cursor position
57 + process.stdout.write('\x1b[r');
58 readline.clearScreenDown(process.stdout);
• Edited src/ui/select_prompt.js (+9 -7)
20
21 -async function runSelect({ rl, message, choices, pageSize = 6 }) {
21 +async function runSelect({ rl, message, choices, pageSize = 6, clearOnDone = true }) {
22 rl.pause();
53 },
54 - }, { clearPromptOnDone: true });
54 + }, { clearPromptOnDone: clearOnDone });
55 debugLog('select_done', { value: value || null });
56 - if (process.stdout.isTTY) {
57 - process.stdout.write('\x1b[r');
58 - readline.clearScreenDown(process.stdout);
56 + if (clearOnDone) {
57 + if (process.stdout.isTTY) {
58 + process.stdout.write('\x1b[r');
59 + readline.clearScreenDown(process.stdout);
60 + }
61 + readline.clearLine(process.stdout, 0);
62 + readline.cursorTo(process.stdout, 0);
63 }
60 - readline.clearLine(process.stdout, 0);
61 - readline.cursorTo(process.stdout, 0);
64 debugLog('select_cleared');
• Edited src/cli/commands.js (+14 -2)
170 ];
171 - const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
171 + const model = await runSelect({
172 + rl,
173 + message: '',
174 + choices: modelChoices,
175 + pageSize: 6,
176 + clearOnDone: false,
177 + });
178 if (!model) return { exit: false };
186 ];
181 - const mode = await runSelect({ rl, message: '', choices: thinkingChoices, pageSize: 6 });
187 + const mode = await runSelect({
188 + rl,
189 + message: '',
190 + choices: thinkingChoices,
191 + pageSize: 6,
192 + clearOnDone: false,
193 + });
194 if (mode) {
• Edited src/ui/select_prompt.js (+7 -9)
20
21 -async function runSelect({ rl, message, choices, pageSize = 6, clearOnDone = true }) {
21 +async function runSelect({ rl, message, choices, pageSize = 6 }) {
22 rl.pause();
53 },
54 - }, { clearPromptOnDone: clearOnDone });
54 + }, { clearPromptOnDone: true });
55 debugLog('select_done', { value: value || null });
56 - if (clearOnDone) {
57 - if (process.stdout.isTTY) {
58 - process.stdout.write('\x1b[r');
59 - readline.clearScreenDown(process.stdout);
60 - }
61 - readline.clearLine(process.stdout, 0);
62 - readline.cursorTo(process.stdout, 0);
56 + if (process.stdout.isTTY) {
57 + process.stdout.write('\x1b[r');
58 + readline.clearScreenDown(process.stdout);
59 }
60 + readline.clearLine(process.stdout, 0);
61 + readline.cursorTo(process.stdout, 0);
62 debugLog('select_cleared');
• Edited src/cli/commands.js (+2 -14)
170 ];
171 - const model = await runSelect({
172 - rl,
173 - message: '',
174 - choices: modelChoices,
175 - pageSize: 6,
176 - clearOnDone: false,
177 - });
171 + const model = await runSelect({ rl, message: '', choices: modelChoices, pageSize: 6 });
172 if (!model) return { exit: false };
180 ];
187 - const mode = await runSelect({
188 - rl,
189 - message: '',
190 - choices: thinkingChoices,
191 - pageSize: 6,
192 - clearOnDone: false,
193 - });
181 + const mode = await runSelect({ rl, message: '', choices: thinkingChoices, pageSize: 6 });
182 if (mode) {