dgxay-code-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh.md +50 -8
- package/README.md +50 -9
- package/package.json +1 -1
- package/src/cli/ansi.js +38 -8
- package/src/cli/approval.js +11 -11
- package/src/cli/editor.js +456 -202
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +34 -34
- package/src/cli/print.js +5 -5
- package/src/cli/render.js +147 -76
- package/src/cli/repl.js +205 -141
- package/src/cli/slash.js +29 -28
- package/src/cli/ui.js +8 -8
- package/src/commands/chat.js +45 -45
- package/src/commands/config.js +81 -81
- package/src/commands/context.js +5 -5
- package/src/commands/doctor.js +34 -34
- package/src/commands/export.js +13 -13
- package/src/commands/mcp.js +51 -51
- package/src/commands/model.js +38 -38
- package/src/commands/oauth.js +61 -61
- package/src/commands/permission.js +19 -19
- package/src/commands/provider.js +70 -70
- package/src/commands/run.js +20 -20
- package/src/commands/skill.js +15 -15
- package/src/commands/workspace.js +18 -18
- package/src/core/agent.js +19 -18
- package/src/core/llm.js +10 -10
- package/src/core/mcp.js +5 -5
- package/src/core/oauth.js +36 -36
- package/src/core/prompt.js +46 -46
- package/src/core/sessions.js +7 -7
- package/src/core/tools.js +24 -24
- package/src/services/input.js +1 -1
- package/src/services/settings.js +28 -28
- package/src/services/skills.js +1 -1
- package/src/services/terminal.js +3 -3
package/src/cli/render.js
CHANGED
|
@@ -3,30 +3,27 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 流式渲染:把 agent 事件流翻译成终端输出。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 于是每次重绘都会把整块内容当成新行往下追加,出现「一段一行」的堆积。
|
|
6
|
+
* 终端兼容性:只用 `\r` 回行首、空格擦尾、`\b` 退光标这些最基础能力,
|
|
7
|
+
* 不依赖光标上移(部分 Windows 控制台会忽略)。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 用户看到的是一行不断变长的文字;
|
|
15
|
-
* - 收尾阶段:抹掉这行预览,再用 Markdown 渲染器把整段完整输出一次,
|
|
16
|
-
* 表格 / 代码块 / 标题才能正确对齐,且只发生一次。
|
|
9
|
+
* 三段输出:
|
|
10
|
+
* 1. 思考:实时显示计时 + 思考内容(超长时只保留末尾几行),结束后折叠成一行摘要;
|
|
11
|
+
* 2. 正文:流式期间只在最后一行刷新预览,结束时一次性按 Markdown 排版;
|
|
12
|
+
* 3. 工具:单行状态 + 结果摘要。
|
|
17
13
|
*/
|
|
18
14
|
|
|
19
15
|
const { COLORS, SIGN, displayWidth, truncate, termWidth, renderMarkdown } = require('./ansi');
|
|
16
|
+
const { t } = require('./i18n');
|
|
20
17
|
|
|
21
|
-
const
|
|
22
|
-
list_directory: '
|
|
23
|
-
read_file: '
|
|
24
|
-
write_file: '
|
|
25
|
-
create_directory: '
|
|
26
|
-
delete_path: '
|
|
27
|
-
run_command: '
|
|
28
|
-
fetch_url: '
|
|
29
|
-
install_mcp_server: '
|
|
18
|
+
const TOOL_KEYS = {
|
|
19
|
+
list_directory: 'tool_list',
|
|
20
|
+
read_file: 'tool_read',
|
|
21
|
+
write_file: 'tool_write',
|
|
22
|
+
create_directory: 'tool_mkdir',
|
|
23
|
+
delete_path: 'tool_delete',
|
|
24
|
+
run_command: 'tool_command',
|
|
25
|
+
fetch_url: 'tool_fetch',
|
|
26
|
+
install_mcp_server: 'tool_mcp'
|
|
30
27
|
};
|
|
31
28
|
|
|
32
29
|
function toolSummary(name, argsRaw) {
|
|
@@ -36,14 +33,14 @@ function toolSummary(name, argsRaw) {
|
|
|
36
33
|
} catch {
|
|
37
34
|
args = {};
|
|
38
35
|
}
|
|
39
|
-
const title =
|
|
36
|
+
const title = t(TOOL_KEYS[name] || name);
|
|
40
37
|
const target = args.path || args.url || args.command || args.name || '';
|
|
41
38
|
return target
|
|
42
39
|
? `${title} ${truncate(String(target).replace(/\s+/g, ' '), Math.max(20, termWidth() - 24))}`
|
|
43
40
|
: title;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
/**
|
|
43
|
+
/** 按显示宽度折行(纯文本预览用) */
|
|
47
44
|
function foldLine(text, width) {
|
|
48
45
|
const limit = Math.max(20, width);
|
|
49
46
|
if (displayWidth(text) <= limit) return [text];
|
|
@@ -64,30 +61,40 @@ function foldLine(text, width) {
|
|
|
64
61
|
return out.length ? out : [''];
|
|
65
62
|
}
|
|
66
63
|
|
|
64
|
+
function formatSeconds(ms) {
|
|
65
|
+
const seconds = ms / 1000;
|
|
66
|
+
return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
|
|
67
|
+
}
|
|
68
|
+
|
|
67
69
|
class StreamRenderer {
|
|
68
70
|
constructor(options = {}) {
|
|
69
71
|
this.out = options.output || process.stdout;
|
|
70
72
|
this.editor = options.editor || null;
|
|
71
|
-
// 只有真正的交互终端才做「行内实时刷新」;管道输出保持纯净
|
|
72
73
|
const tty = Boolean(this.out.isTTY);
|
|
73
74
|
this.live = options.redraw !== undefined ? Boolean(options.redraw) : tty;
|
|
74
75
|
this.width = Math.max(20, (options.width || termWidth()) - 1);
|
|
76
|
+
/** 是否显示思考内容(默认 true;DGXAY_SHOW_REASONING=0 可关闭) */
|
|
77
|
+
this.showReasoning = process.env.DGXAY_SHOW_REASONING !== '0';
|
|
78
|
+
/** 思考内容最多显示几行 */
|
|
79
|
+
this.reasoningLines = Number(process.env.DGXAY_REASONING_LINES) || 6;
|
|
80
|
+
this.timer = null;
|
|
75
81
|
this.reset();
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
reset() {
|
|
79
85
|
this.started = false;
|
|
80
|
-
/** 已收到的完整正文 */
|
|
81
86
|
this.text = '';
|
|
82
|
-
/** 当前物理行屏幕上的内容(用于 \r 覆盖清尾) */
|
|
83
87
|
this.onScreen = '';
|
|
84
|
-
/** 收到的换行数,用来决定收尾时是否需要重排 */
|
|
85
88
|
this.newlines = 0;
|
|
86
89
|
this.tools = new Map();
|
|
90
|
+
|
|
87
91
|
this.reasoning = '';
|
|
88
92
|
this.reasoningStart = 0;
|
|
89
93
|
this.reasoningMs = 0;
|
|
94
|
+
this.reasoningRows = 0;
|
|
95
|
+
this.reasoningDone = false;
|
|
90
96
|
this.suspended = false;
|
|
97
|
+
this.statusShown = false;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
/* ------------------------------ 编辑器协调 ------------------------------ */
|
|
@@ -104,39 +111,109 @@ class StreamRenderer {
|
|
|
104
111
|
this.editor?.resume();
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
/* ------------------------------
|
|
114
|
+
/* ------------------------------ 基础绘制 ------------------------------ */
|
|
108
115
|
|
|
109
|
-
/**
|
|
110
|
-
* 把「当前物理行」替换成 text:\r 回到行首,写出 text,再用空格擦掉多余的尾部。
|
|
111
|
-
* 全程不换行、不上移,因此无论终端是否支持光标控制都不会堆积。
|
|
112
|
-
*/
|
|
113
116
|
repaintLine(text) {
|
|
114
117
|
const visible = displayWidth(text);
|
|
115
118
|
const previous = displayWidth(this.onScreen);
|
|
116
119
|
const pad = Math.max(0, previous - visible);
|
|
117
120
|
this.out.write(`\r${text}${' '.repeat(pad)}`);
|
|
118
|
-
if (pad) this.out.write(
|
|
121
|
+
if (pad) this.out.write('\b'.repeat(pad));
|
|
119
122
|
this.onScreen = text;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
/** 结束当前物理行 */
|
|
123
125
|
breakLine() {
|
|
124
126
|
this.out.write('\n');
|
|
125
127
|
this.onScreen = '';
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
/* ------------------------------ 正文 ------------------------------ */
|
|
129
|
-
|
|
130
130
|
ensureHeader() {
|
|
131
131
|
if (this.started) return;
|
|
132
132
|
this.started = true;
|
|
133
133
|
this.suspend();
|
|
134
|
-
this.out.write(`\n${COLORS.accent('•')} ${COLORS.bold('
|
|
134
|
+
this.out.write(`\n${COLORS.accent('•')} ${COLORS.bold(t('assistant'))}\n`);
|
|
135
135
|
this.onScreen = '';
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
/* ------------------------------ 思考 ------------------------------ */
|
|
139
|
+
|
|
140
|
+
/** 开始思考:立刻给出提示,用户不用干等 */
|
|
141
|
+
beginReasoning() {
|
|
142
|
+
this.ensureHeader();
|
|
143
|
+
if (this.reasoningStart) return;
|
|
144
|
+
this.reasoningStart = Date.now();
|
|
145
|
+
this.reasoningDone = false;
|
|
146
|
+
this.reasoningRows = 0;
|
|
147
|
+
this.paintReasoning();
|
|
148
|
+
// 每 100ms 刷新一次计时,让用户看到“还在跑”
|
|
149
|
+
this.timer = setInterval(() => this.paintReasoning(), 100);
|
|
150
|
+
this.timer.unref?.();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 把已收到的思考内容画出来(末尾若干行),并带上实时计时。
|
|
155
|
+
* 只用 \r + 空格覆盖当前物理行,不依赖光标上移:
|
|
156
|
+
* - 计时和最新一行思考内容合并在同一行刷新;
|
|
157
|
+
* - 更早的思考内容不逐行打印,避免需要回退光标。
|
|
158
|
+
*/
|
|
159
|
+
paintReasoning() {
|
|
160
|
+
if (!this.reasoningStart || this.reasoningDone) return;
|
|
161
|
+
if (!this.live) return;
|
|
162
|
+
const elapsed = Date.now() - this.reasoningStart;
|
|
163
|
+
const width = Math.max(20, this.width - 6);
|
|
164
|
+
|
|
165
|
+
if (!this.showReasoning) {
|
|
166
|
+
this.repaintLine(COLORS.faint(` ${SIGN.run} ${t('thinking')} ${formatSeconds(elapsed)}`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 最新一行思考内容(把换行压成空格,保持单行刷新)
|
|
171
|
+
const latest = this.reasoning.replace(/\s+/g, ' ').trim();
|
|
172
|
+
const tailText = latest.length > width ? `…${latest.slice(-width)}` : latest;
|
|
173
|
+
const head = `${COLORS.faint(` ${SIGN.run} ${t('thinking')} ${formatSeconds(elapsed)}`)}`;
|
|
174
|
+
this.repaintLine(tailText ? `${head} ${COLORS.faint(tailText)}` : head);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 思考结束:当前位置就是摘要行,直接覆盖即可,不需要上移光标 */
|
|
178
|
+
endReasoning() {
|
|
179
|
+
if (!this.reasoningStart || this.reasoningDone) return;
|
|
180
|
+
this.reasoningDone = true;
|
|
181
|
+
this.reasoningMs = Date.now() - this.reasoningStart;
|
|
182
|
+
if (this.timer) {
|
|
183
|
+
clearInterval(this.timer);
|
|
184
|
+
this.timer = null;
|
|
185
|
+
}
|
|
186
|
+
if (this.live) {
|
|
187
|
+
// 覆盖掉“思考中”的实时行
|
|
188
|
+
this.repaintLine('');
|
|
189
|
+
this.out.write('\r');
|
|
190
|
+
this.onScreen = '';
|
|
191
|
+
}
|
|
192
|
+
if (this.reasoning.trim()) {
|
|
193
|
+
this.out.write(
|
|
194
|
+
`${COLORS.faint(` ⌁ ${t('thought_for')} ${formatSeconds(this.reasoningMs)} · ${this.reasoning.length} ${t('chars')}`)}\n`
|
|
195
|
+
);
|
|
196
|
+
// 详细内容用灰色缩进段落呈现,最多若干行,避免刷屏
|
|
197
|
+
if (this.showReasoning) {
|
|
198
|
+
const width = Math.max(20, this.width - 4);
|
|
199
|
+
const lines = [];
|
|
200
|
+
for (const line of this.reasoning.split('\n')) {
|
|
201
|
+
for (const piece of foldLine(line, width)) lines.push(piece);
|
|
202
|
+
}
|
|
203
|
+
const max = Math.max(1, this.reasoningLines);
|
|
204
|
+
const shown = lines.length > max ? lines.slice(-max) : lines;
|
|
205
|
+
if (lines.length > max) this.out.write(`${COLORS.faint(' │ …')}\n`);
|
|
206
|
+
for (const piece of shown) {
|
|
207
|
+
if (piece.trim()) this.out.write(`${COLORS.faint(` │ ${piece}`)}\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ------------------------------ 正文 ------------------------------ */
|
|
214
|
+
|
|
139
215
|
writeText(text) {
|
|
216
|
+
this.endReasoning();
|
|
140
217
|
this.ensureHeader();
|
|
141
218
|
this.text += text;
|
|
142
219
|
this.newlines += (text.match(/\n/g) || []).length;
|
|
@@ -145,20 +222,14 @@ class StreamRenderer {
|
|
|
145
222
|
const lines = this.text.split('\n');
|
|
146
223
|
const tail = lines[lines.length - 1];
|
|
147
224
|
const folded = foldLine(tail, this.width);
|
|
148
|
-
|
|
149
|
-
const preview = folded[folded.length - 1];
|
|
150
|
-
this.repaintLine(preview);
|
|
225
|
+
this.repaintLine(folded[folded.length - 1]);
|
|
151
226
|
}
|
|
152
227
|
|
|
153
|
-
/**
|
|
154
|
-
* 收尾:抹掉预览行,按 Markdown 完整输出正文(只发生一次)。
|
|
155
|
-
*/
|
|
156
228
|
finalizeText() {
|
|
157
229
|
if (!this.text) {
|
|
158
230
|
this.onScreen = '';
|
|
159
231
|
return;
|
|
160
232
|
}
|
|
161
|
-
// 预览行 → 清空(仅交互终端才有预览行)
|
|
162
233
|
if (this.live && this.onScreen) this.repaintLine('');
|
|
163
234
|
const rendered = renderMarkdown(this.text, { width: this.width });
|
|
164
235
|
if (rendered && rendered.trim()) {
|
|
@@ -176,48 +247,50 @@ class StreamRenderer {
|
|
|
176
247
|
switch (event.type) {
|
|
177
248
|
case 'status':
|
|
178
249
|
return;
|
|
179
|
-
case 'delta':
|
|
180
|
-
if (!event.text) return;
|
|
181
|
-
this.writeText(event.text);
|
|
182
|
-
return;
|
|
183
250
|
case 'reasoning':
|
|
184
251
|
if (!event.text) return;
|
|
185
|
-
if (!this.reasoningStart) this.reasoningStart = Date.now();
|
|
186
252
|
this.reasoning += event.text;
|
|
187
|
-
this.
|
|
253
|
+
this.beginReasoning();
|
|
254
|
+
if (this.showReasoning && this.live) this.paintReasoning();
|
|
255
|
+
return;
|
|
256
|
+
case 'delta':
|
|
257
|
+
if (!event.text) return;
|
|
258
|
+
this.writeText(event.text);
|
|
188
259
|
return;
|
|
189
260
|
case 'tool':
|
|
190
261
|
this.onTool(event);
|
|
191
262
|
return;
|
|
192
263
|
case 'review-start':
|
|
193
264
|
this.endInline();
|
|
194
|
-
this.out.write(`${COLORS.faint(` ${SIGN.run}
|
|
265
|
+
this.out.write(`${COLORS.faint(` ${SIGN.run} ${t('reviewing')} ${event.name}…`)}\n`);
|
|
195
266
|
return;
|
|
196
267
|
case 'review':
|
|
197
268
|
this.endInline();
|
|
198
269
|
if (event.decision === 'allow') {
|
|
199
270
|
this.out.write(
|
|
200
|
-
`${COLORS.green(` ${SIGN.ok}`)} ${COLORS.green('
|
|
271
|
+
`${COLORS.green(` ${SIGN.ok}`)} ${COLORS.green(t('review_allow'))} ${COLORS.faint(
|
|
201
272
|
`${event.reviewer || ''} · ${event.reason || ''}`
|
|
202
273
|
)}\n`
|
|
203
274
|
);
|
|
204
275
|
} else {
|
|
205
276
|
this.out.write(
|
|
206
|
-
`${COLORS.yellow(` ${SIGN.run}`)} ${COLORS.yellow('
|
|
207
|
-
event.reason || ''
|
|
208
|
-
)}\n`
|
|
277
|
+
`${COLORS.yellow(` ${SIGN.run}`)} ${COLORS.yellow(t('review_suspect'))} ${COLORS.faint(event.reason || '')}\n`
|
|
209
278
|
);
|
|
210
279
|
}
|
|
211
280
|
return;
|
|
212
281
|
case 'waiting':
|
|
213
282
|
this.endInline();
|
|
214
|
-
this.out.write(`${COLORS.faint(` ${SIGN.run}
|
|
283
|
+
this.out.write(`${COLORS.faint(` ${SIGN.run} ${t('waiting_confirm')} ${event.name}…`)}\n`);
|
|
215
284
|
return;
|
|
216
285
|
case 'file':
|
|
217
286
|
this.endInline();
|
|
218
287
|
this.out.write(
|
|
219
288
|
`${COLORS.green(` ${SIGN.ok}`)} ${COLORS.dim(
|
|
220
|
-
event.action === 'deleted'
|
|
289
|
+
event.action === 'deleted'
|
|
290
|
+
? t('file_deleted')
|
|
291
|
+
: event.action === 'updated'
|
|
292
|
+
? t('file_updated')
|
|
293
|
+
: t('file_created')
|
|
221
294
|
)} ${COLORS.cyan(event.display || event.path)}${event.bytes ? COLORS.faint(` (${event.bytes} B)`) : ''}\n`
|
|
222
295
|
);
|
|
223
296
|
return;
|
|
@@ -231,8 +304,8 @@ class StreamRenderer {
|
|
|
231
304
|
}
|
|
232
305
|
}
|
|
233
306
|
|
|
234
|
-
/** 正文中途插入了其他内容(工具调用、审查提示等):先把已有正文按 Markdown 落地 */
|
|
235
307
|
endInline() {
|
|
308
|
+
this.endReasoning();
|
|
236
309
|
if (this.text) this.finalizeText();
|
|
237
310
|
if (this.onScreen) this.breakLine();
|
|
238
311
|
}
|
|
@@ -264,61 +337,59 @@ class StreamRenderer {
|
|
|
264
337
|
);
|
|
265
338
|
}
|
|
266
339
|
if (event.review?.reason) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
340
|
+
const source = event.review.source === 'auto' ? t('reviewing') : t('approval_op');
|
|
341
|
+
this.out.write(`${COLORS.faint(` ${source} · ${event.review.reason}`)}\n`);
|
|
270
342
|
}
|
|
271
343
|
}
|
|
272
344
|
|
|
273
345
|
toolDetail(event) {
|
|
274
346
|
const output = String(event.output || '');
|
|
275
|
-
if (event.name === 'read_file') return `${output.length}
|
|
347
|
+
if (event.name === 'read_file') return `${output.length} ${t('chars')}`;
|
|
276
348
|
if (event.name === 'run_command') {
|
|
277
349
|
const exit = output.match(/\[exit\] code=(\S+)/);
|
|
278
|
-
if (exit) return
|
|
279
|
-
return `${output.split('\n').filter(Boolean).length}
|
|
350
|
+
if (exit) return `${t('exit_code')} ${exit[1]}`;
|
|
351
|
+
return `${output.split('\n').filter(Boolean).length} ${t('lines')}`;
|
|
280
352
|
}
|
|
281
|
-
if (event.name === 'list_directory') return `${output.split('\n').filter(Boolean).length}
|
|
353
|
+
if (event.name === 'list_directory') return `${output.split('\n').filter(Boolean).length} ${t('items')}`;
|
|
282
354
|
if (event.name === 'fetch_url') {
|
|
283
355
|
const match = output.match(/^HTTP (\d{3})/);
|
|
284
356
|
return match ? `HTTP ${match[1]}` : '';
|
|
285
357
|
}
|
|
286
|
-
if (event.name === 'install_mcp_server') return '
|
|
358
|
+
if (event.name === 'install_mcp_server') return 'saved';
|
|
287
359
|
return '';
|
|
288
360
|
}
|
|
289
361
|
|
|
290
362
|
finish(event) {
|
|
291
|
-
if (this.reasoningStart && !this.reasoningMs) this.reasoningMs = Date.now() - this.reasoningStart;
|
|
292
363
|
const hadText = Boolean(this.text.trim());
|
|
364
|
+
this.endReasoning();
|
|
293
365
|
|
|
294
366
|
if (event.type === 'error') {
|
|
295
367
|
this.endInline();
|
|
296
|
-
this.out.write(`\n${COLORS.red(`${SIGN.fail}
|
|
368
|
+
this.out.write(`\n${COLORS.red(`${SIGN.fail} ${t('generation_error')}: ${event.message}`)}\n`);
|
|
297
369
|
} else if (event.type === 'aborted') {
|
|
298
370
|
this.finalizeText();
|
|
299
|
-
this.out.write(`${COLORS.yellow(`${SIGN.bullet}
|
|
371
|
+
this.out.write(`${COLORS.yellow(`${SIGN.bullet} ${t('stopped')}`)}\n`);
|
|
300
372
|
} else {
|
|
301
373
|
this.finalizeText();
|
|
302
374
|
if (!hadText && !this.tools.size && !this.reasoning.trim()) {
|
|
303
|
-
this.out.write(`${COLORS.faint('
|
|
375
|
+
this.out.write(`${COLORS.faint(t('no_content'))}\n`);
|
|
304
376
|
}
|
|
305
377
|
}
|
|
306
378
|
|
|
307
|
-
if (this.reasoning.trim()) {
|
|
308
|
-
const ms = this.reasoningMs || 0;
|
|
309
|
-
const seconds = ms > 0 ? (ms / 1000).toFixed(ms < 10000 ? 1 : 0) : '0';
|
|
310
|
-
this.out.write(`${COLORS.faint(` ⌁ 已思考 ${seconds}s · ${this.reasoning.length} 字符`)}\n`);
|
|
311
|
-
}
|
|
312
379
|
if (event.usage) {
|
|
313
380
|
const usage = event.usage;
|
|
314
381
|
const input = usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? null;
|
|
315
382
|
const output = usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? null;
|
|
316
383
|
if (input != null || output != null) {
|
|
317
|
-
this.out.write(`${COLORS.faint(`
|
|
384
|
+
this.out.write(`${COLORS.faint(` ${input ?? '?'} ${t('in_out')} ${output ?? '?'} ${t('out')}`)}\n`);
|
|
318
385
|
}
|
|
319
386
|
}
|
|
387
|
+
if (this.timer) {
|
|
388
|
+
clearInterval(this.timer);
|
|
389
|
+
this.timer = null;
|
|
390
|
+
}
|
|
320
391
|
this.resume();
|
|
321
392
|
}
|
|
322
393
|
}
|
|
323
394
|
|
|
324
|
-
module.exports = { StreamRenderer, toolSummary,
|
|
395
|
+
module.exports = { StreamRenderer, toolSummary, foldLine, TOOL_KEYS, formatSeconds };
|