zen-gitsync 2.17.6 → 2.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/package.json +1 -1
- package/scripts/release.js +139 -48
- package/src/cli/ai/agent.js +108 -324
- package/src/cli/ai/cli.integration.test.js +124 -0
- package/src/cli/ai/context.js +90 -0
- package/src/cli/ai/modelSetup.js +38 -0
- package/src/cli/ai/runtime.test.js +181 -0
- package/src/cli/ai/safety.js +27 -4
- package/src/cli/ai/safety.test.js +13 -0
- package/src/cli/ai/telemetry.js +43 -0
- package/src/cli/ai/termui.js +181 -51
- package/src/cli/ai/termui.test.js +165 -16
- package/src/cli/ai/tools.js +27 -5
- package/src/cli/ai/tools.test.js +23 -0
- package/src/cli/ai/transport.js +89 -0
- package/src/cli/ai/turn.js +126 -0
- package/src/config.js +34 -4
- package/src/ui/public/assets/AgentView-CdJaeRh0.js +5 -0
- package/src/ui/public/assets/AgentView-DAY4S5WN.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-DV6KuLqb.js → AppVersionBadge-CiijSYfM.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-DGXDAOkR.js → BranchSelector-BS7ajwvv.js} +1 -1
- package/src/ui/public/assets/{CommitForm-BYBj7UUa.js → CommitForm-CEOSqXCA.js} +3 -3
- package/src/ui/public/assets/{CommonDialog-DWMCeujE.js → CommonDialog-CAaEi-4T.js} +1 -1
- package/src/ui/public/assets/{EditorView-D-TEQZso.js → EditorView-DyaFyRDD.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-C4ZANajI.js → FileDiffViewer-BvX9cQjR.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-DUv7mBuG.js → FlowExecutionViewer-uXrzZjsW.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-BiSuvzL7.js → FlowOrchestrationWorkspace-DIBpUSt_.js} +1 -1
- package/src/ui/public/assets/{IconButton-BltPKVbV.js → IconButton-CnoW2on6.js} +1 -1
- package/src/ui/public/assets/{LogList-BY1Haq9r.js → LogList-D2espSVo.js} +1 -1
- package/src/ui/public/assets/{MindmapView-BKsvCnYv.js → MindmapView-CZF-rZVs.js} +1 -1
- package/src/ui/public/assets/{MonitorView-B3_NaJRy.js → MonitorView-DjLr7qOG.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DP3eDVRp.js → ProjectStartupButton-DzsU-QIO.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-Bl9b_iRv.js → RemoteManagerDialog-CuuG2ucT.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-TQ3DxDnq.js → RemoteRepoCard-BGf8L0of.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-BF-Mzv3V.js → SourceMapView-CFb02m6z.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-DxazW7dI.js → SvgIcon-SRAeYjnN.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-BN6gWFWX.js → TemplateManager-gedqAXl8.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-CPwvIfP6.js → UserInputNode-BOCuMQW6.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-CRoR21LQ.js +13 -0
- package/src/ui/public/assets/WorkbenchView-v3QFn6Ln.css +1 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-C8OE7U9H.js +19 -0
- package/src/ui/public/assets/configStore-CuMElTgV.js +1 -0
- package/src/ui/public/assets/{gitStore-CYrdjXPU.js → gitStore-CpcS-MzM.js} +1 -1
- package/src/ui/public/assets/index-cApp6w0b.css +1 -0
- package/src/ui/public/assets/index-yXm9JLGv.js +24 -0
- package/src/ui/public/assets/{previewSandbox-D_j5PWX1.js → previewSandbox-DkXYEpLP.js} +0 -0
- package/src/ui/public/assets/{toolsStore-BiC2mmC3.js → toolsStore-DGxlRe3Y.js} +1 -1
- package/src/ui/public/assets/vendor-4ojsjDYc.css +1 -0
- package/src/ui/public/assets/{vendor-92fKgtnz.js → vendor-dO4B7wQf.js} +28 -28
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/routes/config.js +8 -3
- package/src/ui/server/routes/fileOpen.js +48 -2
- package/src/ui/server/routes/fs.js +37 -0
- package/src/ui/server/routes/gAiLauncher.test.js +70 -0
- package/src/ui/server/routes/workbench/index.js +37 -6
- package/src/ui/server/routes/workbench/jobStore.js +4 -0
- package/src/ui/server/routes/workbench/taskRunner.js +276 -96
- package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +126 -0
- package/src/ui/server/utils/directoryFetch.js +209 -0
- package/src/ui/server/utils/directoryFetch.test.js +232 -0
- package/src/ui/public/assets/AgentView-BC3F0F6m.js +0 -4
- package/src/ui/public/assets/AgentView-CzUeyQfR.css +0 -1
- package/src/ui/public/assets/WorkbenchView-CDu61WWs.js +0 -13
- package/src/ui/public/assets/WorkbenchView-DEejub9Z.css +0 -1
- package/src/ui/public/assets/_plugin-vue_export-helper-B7A-NgBp.js +0 -19
- package/src/ui/public/assets/configStore-Dx84lvTX.js +0 -1
- package/src/ui/public/assets/index-DNHOYMCg.js +0 -24
- package/src/ui/public/assets/index-Dsfmp6w8.css +0 -1
- package/src/ui/public/assets/vendor-B1UrVTTI.css +0 -1
package/src/cli/ai/termui.js
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
//
|
|
17
17
|
// - 盒式输入框(Codex composer 风格):╭───╮ + ❯ 提示符 + ╰───╯
|
|
18
18
|
// - 等待 spinner(ora):首个 token 到达前持续转动
|
|
19
|
-
// -
|
|
19
|
+
// - 思考过程:琥珀色标题 + 灰色流式预览,可切换完整显示
|
|
20
20
|
// - 工具调用块(Claude Code 风格):⚙ 工具头 + 智能参数摘要,
|
|
21
21
|
// 结果用 │ 缩进槽,按退出码/错误前缀着色
|
|
22
|
-
// -
|
|
22
|
+
// - 正文:独立回答标题 + 自适应行宽的轻量 markdown 渲染
|
|
23
23
|
// (**bold**、`code`、# 标题、``` 代码块、- 列表)
|
|
24
24
|
//
|
|
25
25
|
// 设计约束:
|
|
@@ -29,12 +29,15 @@
|
|
|
29
29
|
|
|
30
30
|
import chalk from 'chalk'
|
|
31
31
|
import ora from 'ora'
|
|
32
|
+
import stringWidth from 'string-width'
|
|
33
|
+
import { stripVTControlCharacters } from 'node:util'
|
|
32
34
|
import { boxenAdaptive } from '../ui.js'
|
|
33
35
|
|
|
34
36
|
// ──────────────────────────────────────────────
|
|
35
37
|
// 常量
|
|
36
38
|
// ──────────────────────────────────────────────
|
|
37
39
|
export const DISPLAY_RESULT_LIMIT = 600 // 工具结果回显截断长度(完整结果仍进上下文)
|
|
40
|
+
export const THINKING_PREVIEW_LINES = 12
|
|
38
41
|
|
|
39
42
|
// 回显截断的"最小省略量":只超出一两个字符时截断反而碍事(实测:git status
|
|
40
43
|
// 输出 614 字符被截,省略 14 字符还把路径从中间切断),不值得就不截
|
|
@@ -54,7 +57,39 @@ export function termWidth(fallback = 100) {
|
|
|
54
57
|
|
|
55
58
|
/** 去掉 ANSI 转义(测试断言用) */
|
|
56
59
|
export function stripAnsi(s) {
|
|
57
|
-
return String(s)
|
|
60
|
+
return stripVTControlCharacters(String(s))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Normalize before grapheme segmentation: Intl.Segmenter treats CRLF as one
|
|
64
|
+
// cluster, so checking only for '\n' would leave unindented physical newlines
|
|
65
|
+
// inside a row and incorrectly carry its width into the next output line.
|
|
66
|
+
function normalizeTerminalNewlines(text) {
|
|
67
|
+
return String(text).replace(/\r\n?/g, '\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Wrap terminal cells, preserving colour and CJK/emoji widths. */
|
|
71
|
+
export function wrapTerminalText(text, width = 100) {
|
|
72
|
+
const rows = []
|
|
73
|
+
let row = '', cells = 0, lastBreak = -1, breakCells = 0
|
|
74
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
75
|
+
for (const part of normalizeTerminalNewlines(text).split(/(\x1b\[[0-9;]*m)/g)) {
|
|
76
|
+
if (/^\x1b\[/.test(part)) { row += part; continue }
|
|
77
|
+
for (const { segment } of segmenter.segment(part.replace(/\t/g, ' '))) {
|
|
78
|
+
if (segment === '\n') { rows.push(row); row = ''; cells = 0; lastBreak = -1; continue }
|
|
79
|
+
const size = stringWidth(segment)
|
|
80
|
+
if (cells && cells + size > Math.max(1, width)) {
|
|
81
|
+
if (lastBreak > 0 && breakCells >= width / 3) {
|
|
82
|
+
rows.push(row.slice(0, lastBreak)); row = row.slice(lastBreak); cells -= breakCells
|
|
83
|
+
} else { rows.push(row); row = ''; cells = 0 }
|
|
84
|
+
lastBreak = -1
|
|
85
|
+
}
|
|
86
|
+
row += segment
|
|
87
|
+
cells += size
|
|
88
|
+
if (/^[ \t]$/.test(segment)) { lastBreak = row.length; breakCells = cells }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
rows.push(row)
|
|
92
|
+
return rows
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
/**
|
|
@@ -116,7 +151,12 @@ export const SLASH_COMMANDS = [
|
|
|
116
151
|
{ cmd: '/addmodel', descZh: '添加模型配置(向导)', descEn: 'Add a model (wizard)' },
|
|
117
152
|
{ cmd: '/cd', descZh: '切换工作目录', descEn: 'Change working directory' },
|
|
118
153
|
{ cmd: '/image', descZh: '附加 / 查看图片', descEn: 'Attach / list images' },
|
|
119
|
-
{ cmd: '/think', descZh: '
|
|
154
|
+
{ cmd: '/think', descZh: '切换思考显示:完整 / 隐藏', descEn: 'Toggle full / hidden thinking' },
|
|
155
|
+
{ cmd: '/think full', descZh: '完整显示后续思考(默认)', descEn: 'Show future thinking in full (default)' },
|
|
156
|
+
{ cmd: '/think compact', descZh: '预览前 12 行思考', descEn: 'Preview the first 12 thinking lines' },
|
|
157
|
+
{ cmd: '/think off', descZh: '隐藏思考(不影响模型推理)', descEn: 'Hide thinking (model still reasons)' },
|
|
158
|
+
{ cmd: '/tools', descZh: '切换工具结果:精简 / 完整', descEn: 'Toggle compact / full tool output' },
|
|
159
|
+
{ cmd: '/stats', descZh: '查看耗时与 Token 用量', descEn: 'Show timing and token usage' },
|
|
120
160
|
{ cmd: '/new', descZh: '开启新对话', descEn: 'Start a new chat' },
|
|
121
161
|
{ cmd: '/resume', descZh: '恢复之前的对话', descEn: 'Resume a previous chat' },
|
|
122
162
|
{ cmd: '/clear', descZh: '清空对话历史', descEn: 'Clear conversation' },
|
|
@@ -126,7 +166,7 @@ export const SLASH_COMMANDS = [
|
|
|
126
166
|
|
|
127
167
|
/**
|
|
128
168
|
* 按当前输入过滤斜杠命令(纯函数,便于单测)。
|
|
129
|
-
* -
|
|
169
|
+
* - 输入以 / 开头时提示,支持 /think f 等已知子命令的参数补全
|
|
130
170
|
* - 前缀匹配,大小写不敏感
|
|
131
171
|
* - 精确等于某命令且无后续参数时仍然展示该命令(便于确认拼写)
|
|
132
172
|
* @returns {{cmd:string, desc:string}[]}
|
|
@@ -134,8 +174,8 @@ export const SLASH_COMMANDS = [
|
|
|
134
174
|
export function filterSlashCommands(input, locale) {
|
|
135
175
|
const zh = !String(locale || '').startsWith('en')
|
|
136
176
|
const line = String(input || '')
|
|
137
|
-
if (!line.startsWith('/')
|
|
138
|
-
const q = line.toLowerCase()
|
|
177
|
+
if (!line.startsWith('/')) return []
|
|
178
|
+
const q = line.toLowerCase().replace(/\s+/g, ' ')
|
|
139
179
|
return SLASH_COMMANDS
|
|
140
180
|
.filter((c) => c.cmd.startsWith(q))
|
|
141
181
|
.map((c) => ({ cmd: c.cmd, desc: zh ? c.descZh : c.descEn }))
|
|
@@ -155,9 +195,10 @@ export function renderSlashHintBody(matches, selectedIndex = -1, locale = 'zh-CN
|
|
|
155
195
|
: -1
|
|
156
196
|
const zh = !String(locale || '').startsWith('en')
|
|
157
197
|
const lines = []
|
|
198
|
+
const commandWidth = Math.max(12, ...matches.map(m => String(m.cmd).length))
|
|
158
199
|
for (let i = 0; i < matches.length; i++) {
|
|
159
200
|
const m = matches[i]
|
|
160
|
-
const command = String(m.cmd).padEnd(
|
|
201
|
+
const command = String(m.cmd).padEnd(commandWidth)
|
|
161
202
|
if (i === sel) {
|
|
162
203
|
lines.push(`${chalk.cyanBright('›')} ${chalk.cyan(command)} ${chalk.white(m.desc)}`)
|
|
163
204
|
} else {
|
|
@@ -340,8 +381,9 @@ export function drawInputBottom(write = (s) => process.stdout.write(s)) {
|
|
|
340
381
|
// 首个 token 到达前转动;到达后 stop() 清行,接着流式输出。
|
|
341
382
|
// 非 TTY 时 ora 自动退化为只打印一次文本。
|
|
342
383
|
|
|
343
|
-
export function startSpinner(text) {
|
|
344
|
-
const
|
|
384
|
+
export function startSpinner(text, { spinnerFactory = ora } = {}) {
|
|
385
|
+
const started = performance.now()
|
|
386
|
+
const spinner = spinnerFactory({
|
|
345
387
|
// 琥珀色加粗,与思考内容同色系,dim 太浅看不清
|
|
346
388
|
text: chalk.hex('#e8a33d').bold(text),
|
|
347
389
|
spinner: 'dots',
|
|
@@ -355,9 +397,19 @@ export function startSpinner(text) {
|
|
|
355
397
|
discardStdin: false,
|
|
356
398
|
})
|
|
357
399
|
spinner.start()
|
|
400
|
+
const timer = spinner.isSpinning ? setInterval(() => {
|
|
401
|
+
spinner.text = chalk.hex('#e8a33d')(text) + chalk.gray(` · ${formatDuration(performance.now() - started)}`)
|
|
402
|
+
}, 100) : null
|
|
403
|
+
timer?.unref()
|
|
404
|
+
let stopped = false
|
|
358
405
|
return {
|
|
359
406
|
stop() {
|
|
360
|
-
|
|
407
|
+
// ora.stop() moves the cursor even after it has stopped. Repeated calls
|
|
408
|
+
// during streaming would overwrite the text already on the current row.
|
|
409
|
+
if (stopped) return
|
|
410
|
+
stopped = true
|
|
411
|
+
clearInterval(timer)
|
|
412
|
+
spinner.stop()
|
|
361
413
|
},
|
|
362
414
|
}
|
|
363
415
|
}
|
|
@@ -392,7 +444,11 @@ function renderInline(line, resetFg = '') {
|
|
|
392
444
|
*/
|
|
393
445
|
export function createAssistantWriter({
|
|
394
446
|
showThinking = true,
|
|
395
|
-
thinkingHeader = '
|
|
447
|
+
thinkingHeader = '思考',
|
|
448
|
+
answerHeader = '回答',
|
|
449
|
+
thinkingLimit = Infinity,
|
|
450
|
+
thinkingHint = '… 思考预览已收起,/think full 显示完整过程',
|
|
451
|
+
width = Math.min(100, termWidth() - 4),
|
|
396
452
|
write = (s) => process.stdout.write(s),
|
|
397
453
|
} = {}) {
|
|
398
454
|
let mode = null // null | 'thinking' | 'content'
|
|
@@ -401,18 +457,36 @@ export function createAssistantWriter({
|
|
|
401
457
|
let contentLines = 0 // 已输出正文行数(首行带 ➤ 子弹头)
|
|
402
458
|
let lastBlank = false // 上一行是空白行(连续空行合并,避免模型输出头部/分隔空行刷屏)
|
|
403
459
|
let thinkAtLineStart = true // 思考流当前是否在行首(用于逐行缩进 + 计算与正文的分隔)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
460
|
+
let thinkingCells = 0, thinkingRows = 0, thinkingClipped = false
|
|
461
|
+
let answerShown = false
|
|
462
|
+
// 回答正文统一缩进;标题作为独立的视觉锚点。
|
|
463
|
+
const BULLET_FIRST = ' '
|
|
464
|
+
const BULLET_REST = ' '
|
|
408
465
|
// whiteBright ANSI 码:行内 code 的 \x1b[39m 会清掉外层色,用此恢复正文亮白色
|
|
409
|
-
const WB = '\x1b[97m'
|
|
466
|
+
const WB = chalk.level ? '\x1b[97m' : ''
|
|
410
467
|
// 思考内容整体右移,与正文文字左缘对齐,视觉上成为独立子块
|
|
411
|
-
const THINK_INDENT = '
|
|
412
|
-
//
|
|
413
|
-
const thinkStyle = (s) => chalk.hex('#
|
|
414
|
-
//
|
|
415
|
-
const THINK_ICON = '
|
|
468
|
+
const THINK_INDENT = chalk.gray(' │ ')
|
|
469
|
+
// 思考正文降低视觉权重,仅标题用琥珀色。
|
|
470
|
+
const thinkStyle = (s) => chalk.hex('#aab2c0')(s)
|
|
471
|
+
// 使用单色符号,避免 emoji 在不同终端中的宽度差异。
|
|
472
|
+
const THINK_ICON = chalk.hex('#e8a33d')('◇')
|
|
473
|
+
const clipThinking = () => {
|
|
474
|
+
if (!thinkAtLineStart) write('\n')
|
|
475
|
+
write(wrapTerminalText(thinkingHint, Math.max(8, width - 2)).map(row => THINK_INDENT + chalk.gray(row)).join('\n') + '\n')
|
|
476
|
+
thinkingClipped = true
|
|
477
|
+
thinkAtLineStart = true
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const showAnswerHeader = () => {
|
|
481
|
+
if (answerShown) return
|
|
482
|
+
answerShown = true
|
|
483
|
+
write(chalk.greenBright.bold(`◆ ${answerHeader}`) + '\n\n')
|
|
484
|
+
}
|
|
485
|
+
const writeContentRows = (prefix, body, withNewline) => {
|
|
486
|
+
showAnswerHeader()
|
|
487
|
+
const rows = wrapTerminalText(body, Math.max(8, width))
|
|
488
|
+
write(rows.map(row => prefix + row).join('\n') + (withNewline ? '\n' : ''))
|
|
489
|
+
}
|
|
416
490
|
|
|
417
491
|
const emitContentLine = (raw, withNewline = true) => {
|
|
418
492
|
// 围栏标记行:切换状态,用一个淡淡的槽线代替裸 ```
|
|
@@ -421,7 +495,8 @@ export function createAssistantWriter({
|
|
|
421
495
|
const bullet = contentLines === 0 ? BULLET_FIRST : BULLET_REST
|
|
422
496
|
contentLines++
|
|
423
497
|
lastBlank = false
|
|
424
|
-
|
|
498
|
+
const language = raw.trim().slice(3).trim() || 'code'
|
|
499
|
+
writeContentRows(bullet, chalk.gray(inFence ? '┌─ ' + language : '└' + '─'.repeat(12)), withNewline)
|
|
425
500
|
return
|
|
426
501
|
}
|
|
427
502
|
// 围栏内:代码行原样带槽线(空行也保留,代码格式不能动)
|
|
@@ -429,7 +504,8 @@ export function createAssistantWriter({
|
|
|
429
504
|
const bullet = contentLines === 0 ? BULLET_FIRST : BULLET_REST
|
|
430
505
|
contentLines++
|
|
431
506
|
lastBlank = false
|
|
432
|
-
|
|
507
|
+
showAnswerHeader()
|
|
508
|
+
write(wrapTerminalText(raw, Math.max(8, width - 2)).map(row => bullet + chalk.gray('│ ') + row).join('\n') + (withNewline ? '\n' : ''))
|
|
433
509
|
return
|
|
434
510
|
}
|
|
435
511
|
// 空白行:首个正文行之前不输出;连续空行合并为一行
|
|
@@ -447,7 +523,7 @@ export function createAssistantWriter({
|
|
|
447
523
|
const body = h
|
|
448
524
|
? chalk.bold.whiteBright(renderInline(h[2], WB))
|
|
449
525
|
: chalk.whiteBright(renderInline(raw, WB))
|
|
450
|
-
|
|
526
|
+
writeContentRows(bullet, body, withNewline)
|
|
451
527
|
}
|
|
452
528
|
|
|
453
529
|
const flushLineBuf = (withNewline = true) => {
|
|
@@ -459,30 +535,34 @@ export function createAssistantWriter({
|
|
|
459
535
|
return {
|
|
460
536
|
/** 思考段:橙黄斜体 + 整体右缩进(不做 markdown),段头只打印一次 */
|
|
461
537
|
writeThinking(text) {
|
|
462
|
-
if (!showThinking || !text) return
|
|
538
|
+
if (!showThinking || !text || thinkingClipped) return
|
|
463
539
|
if (mode !== 'thinking') {
|
|
464
540
|
flushLineBuf()
|
|
465
541
|
mode = 'thinking'
|
|
466
542
|
// 图标 🧠 靠最左(0 缩进),与 ⚙ 工具头、➤ 正文对齐同一左缘
|
|
467
543
|
// 段头前空一行与上文(spinner / 上一轮输出)分隔,视觉更清晰
|
|
468
|
-
write('\n' + THINK_ICON + '
|
|
544
|
+
write('\n' + THINK_ICON + ' ' + chalk.hex('#e8a33d').bold(thinkingHeader) + '\n')
|
|
469
545
|
thinkAtLineStart = true
|
|
470
546
|
}
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (thinkAtLineStart) write(THINK_INDENT)
|
|
479
|
-
write(thinkStyle(seg))
|
|
480
|
-
thinkAtLineStart = false
|
|
547
|
+
// 保留跨分片的行宽和缩进;预览合并空白行,让行数用于实际内容。
|
|
548
|
+
// 完整模式仍保留模型返回的空行。
|
|
549
|
+
for (const char of text) {
|
|
550
|
+
if (Number.isFinite(thinkingLimit) && char === '\n' && thinkAtLineStart) continue
|
|
551
|
+
if (thinkingRows >= thinkingLimit) {
|
|
552
|
+
clipThinking()
|
|
553
|
+
break
|
|
481
554
|
}
|
|
482
|
-
if (
|
|
483
|
-
write('\n')
|
|
484
|
-
|
|
555
|
+
if (char === '\n' || thinkingCells + stringWidth(char) > Math.max(8, width - 2)) {
|
|
556
|
+
write('\n'); thinkingRows++; thinkingCells = 0; thinkAtLineStart = true
|
|
557
|
+
if (char === '\n') continue
|
|
558
|
+
if (thinkingRows >= thinkingLimit) {
|
|
559
|
+
clipThinking(); break
|
|
560
|
+
}
|
|
485
561
|
}
|
|
562
|
+
if (thinkAtLineStart) write(THINK_INDENT)
|
|
563
|
+
write(thinkStyle(char))
|
|
564
|
+
thinkingCells += stringWidth(char)
|
|
565
|
+
thinkAtLineStart = false
|
|
486
566
|
}
|
|
487
567
|
},
|
|
488
568
|
|
|
@@ -535,6 +615,43 @@ export function formatDuration(ms) {
|
|
|
535
615
|
return `${m}m${rem}s`
|
|
536
616
|
}
|
|
537
617
|
|
|
618
|
+
export function renderTurnSummary(stats, { locale = 'zh-CN', session, width = Math.min(100, termWidth() - 4) } = {}) {
|
|
619
|
+
const zh = !String(locale).startsWith('en')
|
|
620
|
+
const number = n => n == null ? '—' : Math.round(n).toLocaleString('en-US')
|
|
621
|
+
const usageLine = (usage, reported, requests) => {
|
|
622
|
+
if (!usage) return zh ? 'Token:服务端未返回用量' : 'Tokens: not reported by provider'
|
|
623
|
+
const parts = [
|
|
624
|
+
`Token ${number(usage.totalTokens)}`,
|
|
625
|
+
`${zh ? '输入' : 'in'} ${number(usage.inputTokens)}`,
|
|
626
|
+
`${zh ? '输出' : 'out'} ${number(usage.outputTokens)}`,
|
|
627
|
+
]
|
|
628
|
+
if (usage.cachedTokens != null) parts.push(`${zh ? '输入含缓存' : 'cached input'} ${number(usage.cachedTokens)}`)
|
|
629
|
+
if (usage.reasoningTokens != null) parts.push(`${zh ? '输出含推理' : 'reasoning output'} ${number(usage.reasoningTokens)}`)
|
|
630
|
+
if (reported < requests) parts.push(zh ? `部分用量(${reported}/${requests} 次请求)` : `partial usage (${reported}/${requests} requests)`)
|
|
631
|
+
return parts.join(' · ')
|
|
632
|
+
}
|
|
633
|
+
const status = zh
|
|
634
|
+
? ({ completed: '完成', cancelled: '已停止', failed: '失败', limit: '达到调用上限' }[stats.status] || '进行中')
|
|
635
|
+
: ({ completed: 'Done', cancelled: 'Stopped', failed: 'Failed', limit: 'Tool limit reached' }[stats.status] || 'Running')
|
|
636
|
+
const time = stats.completedAt ? new Date(stats.completedAt).toLocaleTimeString(zh ? 'zh-CN' : 'en-GB', { hour12: false }) : ''
|
|
637
|
+
const timing = [status + (time ? ` ${time}` : ''), `${zh ? '总耗时' : 'total'} ${formatDuration(stats.totalMs)}`]
|
|
638
|
+
if (stats.firstTokenMs != null) timing.push(`${zh ? '首响应' : 'first token'} ${formatDuration(stats.firstTokenMs)}`)
|
|
639
|
+
if (stats.firstAnswerMs != null) timing.push(`${zh ? '正文等待' : 'first answer'} ${formatDuration(stats.firstAnswerMs)}`)
|
|
640
|
+
const lines = [
|
|
641
|
+
timing.join(' · '),
|
|
642
|
+
`${zh ? '模型' : 'model'} ${formatDuration(stats.llmMs)} · ${zh ? '工具' : 'tools'} ${formatDuration(stats.toolsMs)} (${stats.toolCalls}) · ${zh ? '请求' : 'requests'} ${stats.requests}`,
|
|
643
|
+
usageLine(stats.usage, stats.usageRequests, stats.requests),
|
|
644
|
+
]
|
|
645
|
+
if (session) lines.push(`${zh ? '会话累计' : 'Session'} (${session.turns}) · ${usageLine(session.usage, session.usageRequests, session.requests)}`)
|
|
646
|
+
const rows = lines.flatMap(line => wrapTerminalText(line, Math.max(12, width)))
|
|
647
|
+
return '\n' + chalk.gray(' ' + '─'.repeat(Math.max(12, Math.min(56, width)))) + '\n'
|
|
648
|
+
+ rows.map(row => ' ' + chalk.hex('#aab2c0')(row)).join('\n') + '\n'
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function printTurnSummary(stats, options, write = s => process.stdout.write(s)) {
|
|
652
|
+
write(renderTurnSummary(stats, options))
|
|
653
|
+
}
|
|
654
|
+
|
|
538
655
|
// ──────────────────────────────────────────────
|
|
539
656
|
// 工具调用块(Claude Code 风格)
|
|
540
657
|
// ──────────────────────────────────────────────
|
|
@@ -568,13 +685,14 @@ export function summarizeToolArgs(name, args, { chars = '字符' } = {}) {
|
|
|
568
685
|
}
|
|
569
686
|
|
|
570
687
|
/** 工具头:▶ name 参数摘要(青色三角表示"工具执行";name 加粗白色,摘要灰色,避免整块青色) */
|
|
571
|
-
export function printToolHeader(name, summary, write = (s) => process.stdout.write(s)) {
|
|
688
|
+
export function printToolHeader(name, summary, write = (s) => process.stdout.write(s), { locale = 'zh-CN' } = {}) {
|
|
572
689
|
// ▶ 保留青色作为工具执行的标识色;name 用白色加粗与下方结果区分;
|
|
573
690
|
// summary 用浅灰(#a0aec0)— dim 在黑底下太暗看不清,浅灰可读且仍比 name 低调
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
691
|
+
const labels = { run_command: '执行命令', read_file: '读取文件', write_file: '写入文件', edit_file: '修改文件', list_files: '浏览目录', search_text: '搜索内容' }
|
|
692
|
+
const rows = wrapTerminalText(summary || '', Math.max(16, Math.min(100, termWidth() - 6)))
|
|
693
|
+
const label = String(locale).startsWith('en') ? name : labels[name] || name
|
|
694
|
+
write('\n' + chalk.cyan('▸ ') + chalk.bold.white(label) + (label !== name ? chalk.gray(` ${name}`) : '') + '\n')
|
|
695
|
+
if (summary) write(rows.map(row => ' ' + chalk.hex('#aab2c0')(row)).join('\n') + '\n')
|
|
578
696
|
}
|
|
579
697
|
|
|
580
698
|
/**
|
|
@@ -590,22 +708,31 @@ export function printToolHeader(name, summary, write = (s) => process.stdout.wri
|
|
|
590
708
|
* @param {(s: string) => void} [write] - 输出函数,默认写 stdout
|
|
591
709
|
* @param {number} [durationMs] - 执行耗时(毫秒),有值时在结果末尾追加 ⏱ 计时行
|
|
592
710
|
*/
|
|
593
|
-
export function printToolResult(result, write = (s) => process.stdout.write(s), durationMs) {
|
|
594
|
-
const text =
|
|
711
|
+
export function printToolResult(result, write = (s) => process.stdout.write(s), durationMs, { full = false, locale = 'zh-CN', width = Math.min(100, termWidth() - 6) } = {}) {
|
|
712
|
+
const text = normalizeTerminalNewlines(stripAnsi(result))
|
|
595
713
|
// 先把 "$ <command>\n" 这一行回显从展示里剥掉(退出码仍在第二行里识别)
|
|
596
|
-
const visible = text.replace(/^\$[^\n]*\n/, '')
|
|
714
|
+
const visible = text.replace(/^\$[^\n]*\n/, '').replace(/^\(exit \d+\)\n?/, '')
|
|
597
715
|
const exitMatch = text.match(/^\$[^\n]*\n\(exit (\d+)\)/)
|
|
598
716
|
const exitCode = exitMatch ? Number(exitMatch[1]) : null
|
|
717
|
+
const isCancelled = /命令已由用户停止|Cancelled by user/.test(text)
|
|
599
718
|
const isError = exitCode !== null && exitCode !== 0
|
|
600
719
|
|| /^(错误|已拒绝|Error)/.test(text.trim())
|
|
601
720
|
// 正文用柔和灰蓝(#94a3b8)— 不再用青色,避免与工具头同色连成一片;
|
|
602
721
|
// 错误用琥珀色(#f0a020)比黄色更醒目
|
|
603
722
|
const colorize = isError ? chalk.hex('#f0a020') : chalk.hex('#94a3b8')
|
|
604
|
-
const
|
|
605
|
-
|
|
723
|
+
const zh = !String(locale).startsWith('en')
|
|
724
|
+
let lines = wrapTerminalText(visible.trimEnd(), Math.max(8, width))
|
|
725
|
+
const maxRows = isError ? 10 : 6
|
|
726
|
+
if (!full && lines.length > maxRows) {
|
|
727
|
+
const omitted = lines.length - maxRows + 1
|
|
728
|
+
const hint = width < 35 ? `… +${omitted}` : zh ? `… 回显省略 ${omitted} 行 · /tools full` : `… +${omitted} lines · /tools full`
|
|
729
|
+
lines = [...lines.slice(0, 3), hint, ...lines.slice(-(maxRows - 4))]
|
|
730
|
+
}
|
|
731
|
+
const rendered = lines.map((l) => chalk.gray(' │ ') + colorize(l)).join('\n')
|
|
606
732
|
// 有耗时时在结果末尾追加 ⏱ 计时行(与结果块同缩进)
|
|
607
733
|
const timingLine = (durationMs != null && Number.isFinite(durationMs))
|
|
608
|
-
? '\n' + chalk.
|
|
734
|
+
? '\n' + chalk.gray(' └ ') + colorize(isCancelled ? (zh ? '已停止' : 'Stopped') : isError ? (zh ? '失败' : 'Failed') : (zh ? '完成' : 'Done'))
|
|
735
|
+
+ chalk.gray(` · ${formatDuration(durationMs)}${exitCode !== null ? ` · exit ${exitCode}` : ''}`)
|
|
609
736
|
: ''
|
|
610
737
|
write(rendered + timingLine + '\n')
|
|
611
738
|
}
|
|
@@ -640,6 +767,9 @@ export default {
|
|
|
640
767
|
summarizeToolArgs,
|
|
641
768
|
printToolHeader,
|
|
642
769
|
printToolResult,
|
|
770
|
+
printTurnSummary,
|
|
771
|
+
renderTurnSummary,
|
|
772
|
+
wrapTerminalText,
|
|
643
773
|
printOk,
|
|
644
774
|
printWarn,
|
|
645
775
|
printError,
|