pi-zh-cn 0.2.0 → 0.2.2

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.
@@ -1,48 +1,38 @@
1
1
  /**
2
- * pi-zh-cn —— pi 简体中文 TUI 界面汉化扩展
2
+ * pi-zh-cn —— pi 简体中文 TUI 界面汉化扩展(入口)
3
3
  *
4
4
  * 覆盖范围(扩展 API 所及):
5
- * - 流式加载提示(setWorkingMessage)
6
- * - thinking 折叠块标签(setHiddenThinkingLabel)
7
- * - 启动 header(setHeader)
8
- * - 底部状态栏(setFooter)
9
- * - 项目信任弹窗(project_trust handler + 中文 select)
10
- * - 内置斜杠命令描述(addAutocompleteProvider 包一层翻译)
11
- * - 内置 read/bash/edit/write 工具的渲染文案(同名重注册,执行委托原实现)
5
+ * - 流式加载提示 / thinking 折叠块标签 → ui-copy.ts
6
+ * - 启动 header → header.ts
7
+ * - 底部状态栏(/footer 可切换中文与内置样式)→ footer.ts
8
+ * - 项目信任弹窗 → trust.ts
9
+ * - 内置 read/bash/edit/write/grep/find/ls/powershell 工具的渲染文案
10
+ * (同名重注册,执行委托原实现)→ localized-tools.ts,共享模板在 tool-renders.ts
12
11
  *
13
12
  * 不改动发给模型的内容(工具 description 保持英文)。
14
13
  */
15
14
 
16
- import type {
17
- BashToolDetails,
18
- EditToolDetails,
19
- ExtensionAPI,
20
- ReadToolDetails,
21
- } from "@earendil-works/pi-coding-agent";
22
- import {
23
- CONFIG_DIR_NAME,
24
- createBashTool,
25
- createEditTool,
26
- createFindTool,
27
- createGrepTool,
28
- createLsTool,
29
- createPowerShellTool,
30
- createReadTool,
31
- createWriteTool,
32
- getAgentDir,
33
- keyHint,
34
- keyText,
35
- rawKeyHint,
36
- VERSION,
37
- } from "@earendil-works/pi-coding-agent";
38
- import { homedir } from "node:os";
39
- import { readFileSync, realpathSync } from "node:fs";
40
- import { dirname, join, relative } from "node:path";
41
- import type { AssistantMessage } from "@earendil-works/pi-ai";
42
- import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
43
- import { zh } from "./zh.ts";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { registerProjectTrustDialog } from "./trust.ts";
17
+ import { applyUiCopy } from "./ui-copy.ts";
18
+ import { setChineseHeader } from "./header.ts";
19
+ import { setupChineseFooter } from "./footer.ts";
20
+ import { registerLocalizedTools } from "./localized-tools.ts";
44
21
 
22
+ /**
23
+ * 扩展入口(由 pi 扩展加载器调用)。
24
+ *
25
+ * 注册内容:
26
+ * - `project_trust` 中文信任弹窗(进程启动时注册一次);
27
+ * - `/footer` 命令,用于在中文 footer 与内置 footer 之间切换;
28
+ * - `session_start` 时按需应用 UI 文案、中文 header、中文 footer
29
+ * (TUI 模式限定)并重注册内置工具的中文渲染。
30
+ *
31
+ * @param pi pi 提供的扩展 API 实例
32
+ */
45
33
  export default function (pi: ExtensionAPI) {
34
+ registerProjectTrustDialog(pi);
35
+
46
36
  let toggleFooter: (() => void) | undefined;
47
37
 
48
38
  // 只注册一次;当前 session 的 footer 实现由 session_start 更新。
@@ -53,747 +43,14 @@ export default function (pi: ExtensionAPI) {
53
43
  },
54
44
  });
55
45
 
56
- // ---------- 项目信任弹窗(中文选择器代替内置英文界面) ----------
57
- // 复刻 ~/「agent dir」/trust.json 的祖先目录查找逻辑;已有决定的目录不再弹窗。
58
- // 与内置的差异:「信任父文件夹」选项无法通过扩展 API 写入父目录决定,故省略。
59
- const savedTrustDecision = (dir: string): boolean | null => {
60
- let current = dir;
61
- try {
62
- current = realpathSync(current); // 与 core 的 normalizeCwd 一致
63
- } catch {
64
- // 目录不存在时保持原样
65
- }
66
- try {
67
- const store = JSON.parse(readFileSync(join(getAgentDir(), "trust.json"), "utf8"));
68
- for (;;) {
69
- const decision = store[current];
70
- if (typeof decision === "boolean") return decision;
71
- const parent = dirname(current);
72
- if (parent === current) return null;
73
- current = parent;
74
- }
75
- } catch {
76
- return null; // 无 trust.json 或内容无效 → 视作没有已存决定
77
- }
78
- };
79
-
80
- pi.on("project_trust", async (event, ctx) => {
81
- // 无 UI(rpc/print 等模式)或已有保存的决定时返回 undecided,让 core 走默认流程。
82
- if (!ctx.hasUI || savedTrustDecision(event.cwd) !== null) return { trusted: "undecided" };
83
-
84
- const options = [
85
- zh.trust.options.trustAndRemember,
86
- zh.trust.options.trustSessionOnly,
87
- zh.trust.options.distrustAndRemember,
88
- zh.trust.options.distrustSessionOnly,
89
- ];
90
- const selected = await ctx.ui.select(zh.trust.title(event.cwd), options);
91
- switch (selected ?? zh.trust.options.distrustSessionOnly) {
92
- case zh.trust.options.trustAndRemember:
93
- return { trusted: "yes", remember: true };
94
- case zh.trust.options.trustSessionOnly:
95
- return { trusted: "yes" };
96
- case zh.trust.options.distrustAndRemember:
97
- return { trusted: "no", remember: true };
98
- default:
99
- return { trusted: "no" }; // 取消 = 本次不信任(与内置行为一致)
100
- }
101
- });
102
-
103
- // ---------- UI 文案 + 中文 header/footer(session 启动时应用) ----------
104
- let autocompleteTranslated = false;
105
46
  pi.on("session_start", (_event, ctx) => {
106
47
  if (!ctx.hasUI) return;
107
48
 
108
- ctx.ui.setWorkingMessage(zh.workingMessage);
109
- ctx.ui.setHiddenThinkingLabel(zh.hiddenThinkingLabel);
110
-
111
- // 中文化内置斜杠命令描述:包一层自动补全 provider,只替换命令项的
112
- // description(文件补全等其他建议原样透传)。替换是幂等的,跨 session 只包一次。
113
- if (ctx.mode === "tui" && !autocompleteTranslated) {
114
- autocompleteTranslated = true;
115
- ctx.ui.addAutocompleteProvider((current) => ({
116
- ...(current.triggerCharacters ? { triggerCharacters: current.triggerCharacters } : {}),
117
- getSuggestions: async (lines, cursorLine, cursorCol, options) => {
118
- const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
119
- if (!suggestions) return null;
120
- return {
121
- ...suggestions,
122
- items: suggestions.items.map((item) => {
123
- const key = item.value.replace(/^\//, "") as keyof typeof zh.builtinCommands;
124
- const zhDesc = zh.builtinCommands[key];
125
- return zhDesc ? { ...item, description: zhDesc } : item;
126
- }),
127
- };
128
- },
129
- applyCompletion: (lines, cursorLine, cursorCol, item, prefix) =>
130
- current.applyCompletion(lines, cursorLine, cursorCol, item, prefix),
131
- }));
132
- }
133
-
134
- // 中文 header:复刻内置结构(标题 + 折叠提示行 + 引导语),快捷键跟随用户键位配置。
135
- if (ctx.mode === "tui") {
136
- ctx.ui.setHeader((_tui, theme) => ({
137
- render(_width: number): string[] {
138
- const logo =
139
- theme.bold(theme.fg("accent", "pi")) +
140
- theme.fg("dim", ` v${VERSION}`);
141
- const compact = [
142
- keyHint("app.interrupt", zh.header.interrupt),
143
- rawKeyHint(
144
- `${keyText("app.clear")}/${keyText("app.exit")}`,
145
- zh.header.clearExit,
146
- ),
147
- rawKeyHint("/", zh.header.commands),
148
- rawKeyHint("!", "bash"),
149
- keyHint("app.tools.expand", zh.header.more),
150
- ].join(theme.fg("muted", " · "));
151
- return [
152
- logo,
153
- compact,
154
- theme.fg(
155
- "dim",
156
- `${zh.header.expandHelp} (${keyText("app.tools.expand")})`,
157
- ),
158
- "",
159
- theme.fg("dim", zh.header.onboarding),
160
- ];
161
- },
162
- invalidate() {},
163
- }));
164
- }
165
-
166
- // 中文 footer:逐行复刻内置信息(pwd 行 / 统计行 / 扩展状态行),仅描述文案用中文。
167
- if (ctx.mode === "tui") {
168
- let usingZhFooter = true;
169
-
170
- const applyZhFooter = () => {
171
- usingZhFooter = true;
172
- // 自动压缩标记:全局设置为基准,受信任项目的本地设置优先。
173
- // 一秒缓存避免流式渲染时频繁读文件,同时能跟随 /settings 的运行时修改。
174
- const settingsPaths = [join(getAgentDir(), "settings.json")];
175
- if (ctx.isProjectTrusted())
176
- settingsPaths.push(join(ctx.cwd, CONFIG_DIR_NAME, "settings.json"));
177
- let autoCompactEnabled = true;
178
- let autoCompactCheckedAt = 0;
179
- const getAutoCompactSuffix = () => {
180
- const now = Date.now();
181
- if (now - autoCompactCheckedAt >= 1000) {
182
- autoCompactEnabled = true;
183
- for (const settingsPath of settingsPaths) {
184
- try {
185
- const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
186
- if (typeof settings?.compaction?.enabled === "boolean") {
187
- autoCompactEnabled = settings.compaction.enabled;
188
- }
189
- } catch {
190
- // 设置文件不存在或内容无效时忽略该层。
191
- }
192
- }
193
- autoCompactCheckedAt = now;
194
- }
195
- return autoCompactEnabled ? zh.footer.autoCompactSuffix : "";
196
- };
197
-
198
- let unsubscribeBranch: (() => void) | undefined;
199
-
200
- ctx.ui.setFooter((tui, theme, footerData) => {
201
- unsubscribeBranch?.();
202
- unsubscribeBranch = footerData.onBranchChange(() =>
203
- tui.requestRender(),
204
- );
205
-
206
- const home = homedir();
207
- return {
208
- dispose() {
209
- unsubscribeBranch?.();
210
- unsubscribeBranch = undefined;
211
- },
212
- invalidate() {},
213
- render(width: number): string[] {
214
- let input = 0,
215
- output = 0,
216
- cacheRead = 0,
217
- cacheWrite = 0,
218
- cost = 0;
219
- const addUsage = (t: {
220
- input: number;
221
- output: number;
222
- cacheRead: number;
223
- cacheWrite: number;
224
- cost: { total: number };
225
- }) => {
226
- input += t.input;
227
- output += t.output;
228
- cacheRead += t.cacheRead;
229
- cacheWrite += t.cacheWrite;
230
- cost += t.cost.total;
231
- };
232
- let latestRate: number | undefined;
233
-
234
- for (const e of ctx.sessionManager.getEntries()) {
235
- if (e.type === "message" && e.message.role === "assistant") {
236
- const u = (e.message as AssistantMessage).usage;
237
- addUsage(u);
238
- const promptTokens = u.input + u.cacheRead + u.cacheWrite;
239
- latestRate =
240
- promptTokens > 0
241
- ? (u.cacheRead / promptTokens) * 100
242
- : undefined;
243
- } else if (
244
- e.type === "message" &&
245
- e.message.role === "toolResult" &&
246
- e.message.usage
247
- ) {
248
- addUsage(e.message.usage as any);
249
- } else if (
250
- (e.type === "branch_summary" || e.type === "compaction") &&
251
- (e as any).usage
252
- ) {
253
- addUsage((e as any).usage);
254
- }
255
- }
256
-
257
- const fmt = (n: number) =>
258
- n < 1e3
259
- ? `${n}`
260
- : n < 1e4
261
- ? `${(n / 1e3).toFixed(1)}k`
262
- : n < 1e6
263
- ? `${Math.round(n / 1e3)}k`
264
- : n < 1e7
265
- ? `${(n / 1e6).toFixed(1)}M`
266
- : `${Math.round(n / 1e6)}M`;
49
+ applyUiCopy(ctx);
50
+ setChineseHeader(ctx);
51
+ const toggle = setupChineseFooter(ctx);
52
+ if (toggle) toggleFooter = toggle;
267
53
 
268
- // 第 1 行:当前目录:~路径(分支)• 会话名
269
- let pwd = ctx.sessionManager.getCwd();
270
- const homeRelative = relative(home, pwd);
271
- if (homeRelative === "") pwd = "~";
272
- else if (
273
- homeRelative &&
274
- !homeRelative.startsWith("..") &&
275
- !homeRelative.startsWith("/")
276
- )
277
- pwd = `~/${homeRelative}`;
278
- const branch = footerData.getGitBranch();
279
- if (branch) pwd += ` ${zh.footer.branchPrefix}(${branch})`;
280
- const sessionName = ctx.sessionManager.getSessionName();
281
- if (sessionName) pwd += ` • ${sessionName}`;
282
- pwd = `${zh.footer.cwdPrefix}${pwd}`;
283
-
284
- // 第 2 行左:严格按内置顺序:输入、输出、缓存、费用、上下文占比。
285
- const clauses: string[] = [];
286
- if (input > 0 || output > 0) {
287
- const io: string[] = [];
288
- if (input > 0) io.push(`${zh.footer.inputLabel}↑${fmt(input)}`);
289
- if (output > 0)
290
- io.push(`${zh.footer.outputLabel}↓${fmt(output)}`);
291
- clauses.push(io.join("/"));
292
- }
293
-
294
- const cacheParts: string[] = [];
295
- if (cacheRead > 0)
296
- cacheParts.push(`${zh.footer.cacheReadLabel}${fmt(cacheRead)}`);
297
- if (cacheWrite > 0)
298
- cacheParts.push(
299
- `${zh.footer.cacheWriteLabel}${fmt(cacheWrite)}`,
300
- );
301
- if (
302
- (cacheRead > 0 || cacheWrite > 0) &&
303
- latestRate !== undefined
304
- ) {
305
- cacheParts.push(
306
- `${zh.footer.cacheHitLabel}${latestRate.toFixed(1)}%`,
307
- );
308
- }
309
- if (cacheParts.length > 0) clauses.push(cacheParts.join(" "));
310
-
311
- const provider = ctx.model
312
- ? ctx.modelRegistry.getProvider(ctx.model.provider)
313
- : undefined;
314
- const usingSubscription =
315
- ctx.model?.provider === "kimi-coding" ||
316
- (!!ctx.model &&
317
- ctx.modelRegistry.isUsingOAuth(ctx.model) &&
318
- provider?.auth.oauth?.isSubscription === true);
319
- if (cost > 0 || usingSubscription) {
320
- clauses.push(
321
- `${zh.footer.costLabel}$${cost.toFixed(3)}${usingSubscription ? zh.footer.subSuffix : ""}`,
322
- );
323
- }
324
-
325
- // built-in 顺序的最后一项:上下文占比 / 上下文窗口(自动压缩)。
326
- const cu = ctx.getContextUsage();
327
- const windowSize =
328
- cu?.contextWindow ?? ctx.model?.contextWindow ?? 0;
329
- const pctValue = cu?.percent ?? 0;
330
- // 与 built-in 一致:明确为 null 时显示未知;尚无 usage 时按 0.0% 显示。
331
- const pctText =
332
- cu?.percent === null ? "?" : `${pctValue.toFixed(1)}%`;
333
- const contextClause = `${zh.footer.ratioLabel}${pctText}/${fmt(windowSize)}${getAutoCompactSuffix()}`;
334
- const contextIndex = clauses.length;
335
- clauses.push(contextClause);
336
-
337
- // 占比按阈值变色(>90% 红、>70% 黄);实验标记沿用 built-in 样式。
338
- const statsLeft = clauses
339
- .map((clause, index) => {
340
- if (index === contextIndex) {
341
- return pctValue > 90
342
- ? theme.fg("error", clause)
343
- : pctValue > 70
344
- ? theme.fg("warning", clause)
345
- : theme.fg("dim", clause);
346
- }
347
- return theme.fg("dim", clause);
348
- })
349
- .join(" ");
350
-
351
- // 第 2 行右:provider • 模型 • 思考等级(等级映射为中文)
352
- let rightSideWithoutProvider = ctx.model?.id ?? zh.footer.noModel;
353
- if (ctx.model?.reasoning) {
354
- const tl = ctx.thinkingLevel || "off";
355
- const tlLabel =
356
- tl === "off"
357
- ? zh.footer.thinkingOff
358
- : ((zh.footer.thinkingLevels as Record<string, string>)[
359
- tl
360
- ] ?? tl);
361
- rightSideWithoutProvider = `${rightSideWithoutProvider} • ${tlLabel}`;
362
- }
363
- let rightSide = rightSideWithoutProvider;
364
- if (footerData.getAvailableProviderCount() > 1 && ctx.model) {
365
- rightSide = `(${ctx.model.provider}) ${rightSideWithoutProvider}`;
366
- if (
367
- visibleWidth(statsLeft) + 2 + visibleWidth(rightSide) >
368
- width
369
- ) {
370
- rightSide = rightSideWithoutProvider;
371
- }
372
- }
373
-
374
- let statsDisplay = statsLeft;
375
- let statsWidth = visibleWidth(statsDisplay);
376
- if (statsWidth > width) {
377
- statsDisplay = truncateToWidth(statsDisplay, width, "...");
378
- statsWidth = visibleWidth(statsDisplay);
379
- }
380
- const availableForRight = width - statsWidth - 2;
381
- let statsLine = statsDisplay;
382
- if (availableForRight > 0) {
383
- const truncatedRight = truncateToWidth(
384
- rightSide,
385
- availableForRight,
386
- "",
387
- );
388
- const padding = " ".repeat(
389
- Math.max(
390
- 0,
391
- width - statsWidth - visibleWidth(truncatedRight),
392
- ),
393
- );
394
- statsLine = `${statsDisplay}${padding}${theme.fg("dim", truncatedRight)}`;
395
- }
396
-
397
- const lines = [
398
- truncateToWidth(
399
- theme.fg("dim", pwd),
400
- width,
401
- theme.fg("dim", "..."),
402
- ),
403
- ];
404
- lines.push(statsLine);
405
-
406
- // 第 3 行:各扩展的 setStatus 状态
407
- const statuses = footerData.getExtensionStatuses();
408
- if (statuses.size > 0) {
409
- const statusLine = Array.from(statuses.entries())
410
- .sort(([a], [b]) => a.localeCompare(b))
411
- .map(([, text]) =>
412
- text
413
- .replace(/[\r\n\t]/g, " ")
414
- .replace(/ +/g, " ")
415
- .trim(),
416
- )
417
- .join(" ");
418
- lines.push(
419
- truncateToWidth(statusLine, width, theme.fg("dim", "...")),
420
- );
421
- }
422
- return lines;
423
- },
424
- };
425
- });
426
- };
427
-
428
- applyZhFooter();
429
- toggleFooter = () => {
430
- if (usingZhFooter) {
431
- usingZhFooter = false;
432
- ctx.ui.setFooter(undefined);
433
- } else {
434
- applyZhFooter();
435
- }
436
- };
437
- }
438
-
439
- registerLocalizedTools(ctx.cwd);
54
+ registerLocalizedTools(pi, ctx.cwd);
440
55
  });
441
-
442
- // ---------- 工具渲染通用小工具 ----------
443
- const textOf = (result: any): string =>
444
- result.content?.[0]?.type === "text" ? result.content[0].text : "";
445
-
446
- const countNonEmpty = (s: string): number =>
447
- s.split("\n").filter((l) => l.trim()).length;
448
-
449
- const notePartial = (msg: string, theme: any): Text =>
450
- new Text(theme.fg("warning", msg), 0, 0);
451
-
452
- // 错误结果:内容以 "Error" 开头时只展示首行(edit/write 共用)。
453
- const errorFirstLine = (result: any, theme: any): Text | undefined => {
454
- const out = textOf(result);
455
- return out.startsWith("Error")
456
- ? new Text(theme.fg("error", out.split("\n")[0]), 0, 0)
457
- : undefined;
458
- };
459
-
460
- // 展开态:在摘要后追加逐行弱化的原始输出。
461
- const withExpanded = (raw: string, summary: string, theme: any): string =>
462
- raw
463
- ? `${summary}\n${raw
464
- .split("\n")
465
- .map((l) => theme.fg("dim", l))
466
- .join("\n")}`
467
- : summary;
468
-
469
- // 失败判定(bash/powershell 共用):解析退出码;powershell 通过 extraRe
470
- // 追加 Windows 平台提示的识别。
471
- function failureState(output: string, extraRe: RegExp[] = []) {
472
- const m = output.match(
473
- /(?:exit code:\s*|command exited with code\s+)(\d+)/i,
474
- );
475
- const exitCode = m ? parseInt(m[1], 10) : null;
476
- return {
477
- exitCode,
478
- failed:
479
- exitCode !== null ||
480
- /^error\b/i.test(output) ||
481
- /\n\ncommand (?:timed out|was aborted|failed)/i.test(output) ||
482
- extraRe.some((re) => re.test(output)),
483
- };
484
- }
485
-
486
- // shell 类工具(bash/powershell 文案同构)的渲染对。
487
- type ShellCopy =
488
- | typeof zh.tools.bash
489
- | typeof zh.tools.powershell;
490
- const shellRenders = (w: ShellCopy, extraRe: RegExp[] = []) => ({
491
- renderCall(args: any, theme: any): Text {
492
- let text = theme.fg("toolTitle", theme.bold(w.title));
493
- const cmd: string =
494
- args.command.length > 80
495
- ? `${args.command.slice(0, 77)}...`
496
- : args.command;
497
- text += theme.fg("accent", cmd);
498
- if (args.timeout) text += theme.fg("dim", w.timeout(args.timeout));
499
- return new Text(text, 0, 0);
500
- },
501
-
502
- renderResult(
503
- result: any,
504
- { expanded, isPartial }: { expanded: boolean; isPartial: boolean },
505
- theme: any,
506
- ): Text {
507
- if (isPartial) return notePartial(w.running, theme);
508
- const output = textOf(result);
509
- const { exitCode, failed } = failureState(output, extraRe);
510
- let text = failed
511
- ? theme.fg(
512
- "error",
513
- exitCode !== null ? w.exit(exitCode) : w.failed,
514
- )
515
- : theme.fg("success", w.done);
516
- text += theme.fg("dim", ` (${w.lines(countNonEmpty(output))})`);
517
- if (
518
- (result.details as BashToolDetails | undefined)?.truncation?.truncated
519
- ) {
520
- text += theme.fg("warning", ` ${w.truncated}`);
521
- }
522
- return new Text(withExpanded(expanded ? output : "", text, theme), 0, 0);
523
- },
524
- });
525
-
526
- // 计数类工具(grep/find/ls 文案同构)的结果渲染:无匹配哨兵、条数统计、
527
- // 上限提示与截断警告四处逻辑一致,仅文案不同。
528
- function countResultRender(
529
- labels: {
530
- searching: string;
531
- noMatch: string;
532
- found: (n: number) => string;
533
- limit?: (n: number) => string;
534
- truncated: string;
535
- },
536
- sentinel: string,
537
- limitField: string,
538
- ) {
539
- return (
540
- result: any,
541
- { expanded, isPartial }: { expanded: boolean; isPartial: boolean },
542
- theme: any,
543
- ): Text => {
544
- if (isPartial) return notePartial(labels.searching, theme);
545
- const details = result.details ?? {};
546
- const output = textOf(result);
547
- const isEmpty = output === sentinel;
548
- let text = theme.fg(
549
- isEmpty ? "muted" : "success",
550
- isEmpty ? labels.noMatch : labels.found(countNonEmpty(output)),
551
- );
552
- if (labels.limit && typeof details[limitField] === "number") {
553
- text += theme.fg("warning", labels.limit(details[limitField]));
554
- }
555
- // 与旧实现的行为差异:grep 原本同时检查 linesTruncated,find/ls 只查
556
- // truncation.truncated;这里统一为两者都检查,Find/LsToolDetails 目前无
557
- // linesTruncated 字段,运行时行为等价。若上游新增该字段需复核。
558
- if (details.linesTruncated || details.truncation?.truncated) {
559
- text += theme.fg("warning", labels.truncated);
560
- }
561
- return new Text(
562
- withExpanded(
563
- expanded ? (isEmpty ? labels.noMatch : output) : "",
564
- text,
565
- theme,
566
- ),
567
- 0,
568
- 0,
569
- );
570
- };
571
- }
572
-
573
- function registerLocalizedTools(cwd: string) {
574
- const activeTools = pi.getActiveTools();
575
- const builtinTools = new Set(
576
- pi
577
- .getAllTools()
578
- .filter((tool) => tool.sourceInfo.source === "builtin")
579
- .map((tool) => tool.name),
580
- );
581
-
582
- // ---------- 内置工具汉化渲染 ----------
583
- // read
584
- pi.registerTool({
585
- ...createReadTool(cwd),
586
- label: zh.tools.read.label,
587
-
588
- renderCall(args: any, theme: any) {
589
- let text = theme.fg("toolTitle", theme.bold(`${zh.tools.read.title} `));
590
- text += theme.fg("accent", args.path);
591
- if (args.offset || args.limit) {
592
- const parts: string[] = [];
593
- if (args.offset) parts.push(`offset=${args.offset}`);
594
- if (args.limit) parts.push(`limit=${args.limit}`);
595
- text += theme.fg("dim", ` (${parts.join(", ")})`);
596
- }
597
- return new Text(text, 0, 0);
598
- },
599
-
600
- renderResult(
601
- result: any,
602
- { expanded, isPartial }: { expanded: boolean; isPartial: boolean },
603
- theme: any,
604
- ) {
605
- if (isPartial) return notePartial(zh.tools.read.reading, theme);
606
-
607
- const content = result.content[0];
608
- if (content?.type === "image") {
609
- return new Text(theme.fg("success", zh.tools.read.imageLoaded), 0, 0);
610
- }
611
- if (content?.type !== "text") {
612
- return new Text(theme.fg("error", zh.tools.read.noContent), 0, 0);
613
- }
614
-
615
- let text = theme.fg(
616
- "success",
617
- zh.tools.read.lines(content.text.split("\n").length),
618
- );
619
- const truncation = (result.details as ReadToolDetails | undefined)
620
- ?.truncation;
621
- if (truncation?.truncated) {
622
- text += theme.fg(
623
- "warning",
624
- zh.tools.read.truncatedFrom(truncation.totalLines),
625
- );
626
- }
627
- return new Text(
628
- withExpanded(expanded ? content.text : "", text, theme),
629
- 0,
630
- 0,
631
- );
632
- },
633
- });
634
-
635
- // bash / powershell(渲染逻辑同构,仅文案与失败判定集合不同)
636
- pi.registerTool({
637
- ...createBashTool(cwd),
638
- label: zh.tools.bash.label,
639
- ...shellRenders(zh.tools.bash),
640
- });
641
-
642
- // edit
643
- pi.registerTool({
644
- ...createEditTool(cwd),
645
- label: zh.tools.edit.label,
646
- renderShell: "self",
647
-
648
- renderCall(args: any, theme: any) {
649
- let text = theme.fg("toolTitle", theme.bold(zh.tools.edit.title));
650
- text += theme.fg("accent", args.path);
651
- return new Text(text, 0, 0);
652
- },
653
-
654
- renderResult(
655
- result: any,
656
- { expanded, isPartial }: { expanded: boolean; isPartial: boolean },
657
- theme: any,
658
- ) {
659
- if (isPartial) return notePartial(zh.tools.edit.editing, theme);
660
-
661
- const err = errorFirstLine(result, theme);
662
- if (err) return err;
663
-
664
- const details = result.details as EditToolDetails | undefined;
665
- if (!details?.diff) {
666
- return new Text(theme.fg("success", zh.tools.edit.applied), 0, 0);
667
- }
668
-
669
- let additions = 0,
670
- removals = 0;
671
- for (const line of details.diff.split("\n")) {
672
- if (line.startsWith("+") && !line.startsWith("+++")) additions++;
673
- if (line.startsWith("-") && !line.startsWith("---")) removals++;
674
- }
675
-
676
- let text = theme.fg("success", zh.tools.edit.additions(additions));
677
- text += theme.fg("dim", " / ");
678
- text += theme.fg("error", zh.tools.edit.removals(removals));
679
-
680
- return new Text(
681
- withExpanded(expanded ? details.diff : "", text, theme),
682
- 0,
683
- 0,
684
- );
685
- },
686
- });
687
-
688
- // write
689
- pi.registerTool({
690
- ...createWriteTool(cwd),
691
- label: zh.tools.write.label,
692
-
693
- renderCall(args: any, theme: any) {
694
- let text = theme.fg("toolTitle", theme.bold(zh.tools.write.title));
695
- text += theme.fg("accent", args.path);
696
- const lineCount = String(args.content).split("\n").length;
697
- text += theme.fg("dim", ` (${zh.tools.write.lines(lineCount)})`);
698
- return new Text(text, 0, 0);
699
- },
700
-
701
- renderResult(result: any, { isPartial }: { isPartial: boolean }, theme: any) {
702
- if (isPartial) return notePartial(zh.tools.write.writing, theme);
703
- const err = errorFirstLine(result, theme);
704
- if (err) return err;
705
- return new Text(theme.fg("success", zh.tools.write.written), 0, 0);
706
- },
707
- });
708
-
709
- // grep / find / ls:调用渲染各自略有差异,结果渲染走同一个计数模板。
710
- if (builtinTools.has("grep")) {
711
- pi.registerTool({
712
- ...createGrepTool(cwd),
713
- label: zh.tools.grep.label,
714
-
715
- renderCall(args: any, theme: any) {
716
- let text = theme.fg("toolTitle", theme.bold(zh.tools.grep.title));
717
- text += theme.fg("accent", args.pattern);
718
- if (args.path) text += theme.fg("dim", ` · ${args.path}`);
719
- if (args.glob) text += theme.fg("dim", ` (${args.glob})`);
720
- return new Text(text, 0, 0);
721
- },
722
-
723
- renderResult: countResultRender(
724
- {
725
- searching: zh.tools.grep.searching,
726
- noMatch: zh.tools.grep.noMatches,
727
- found: zh.tools.grep.matches,
728
- limit: zh.tools.grep.matchLimit,
729
- truncated: zh.tools.grep.truncated,
730
- },
731
- "No matches found",
732
- "matchLimitReached",
733
- ),
734
- });
735
- }
736
-
737
- if (builtinTools.has("find")) {
738
- pi.registerTool({
739
- ...createFindTool(cwd),
740
- label: zh.tools.find.label,
741
-
742
- renderCall(args: any, theme: any) {
743
- let text = theme.fg("toolTitle", theme.bold(zh.tools.find.title));
744
- text += theme.fg("accent", args.pattern);
745
- if (args.path) text += theme.fg("dim", ` · ${args.path}`);
746
- return new Text(text, 0, 0);
747
- },
748
-
749
- renderResult: countResultRender(
750
- {
751
- searching: zh.tools.find.searching,
752
- noMatch: zh.tools.find.noFiles,
753
- found: zh.tools.find.results,
754
- limit: zh.tools.find.resultLimit,
755
- truncated: zh.tools.grep.truncated,
756
- },
757
- "No files found matching pattern",
758
- "resultLimitReached",
759
- ),
760
- });
761
- }
762
-
763
- if (builtinTools.has("ls")) {
764
- pi.registerTool({
765
- ...createLsTool(cwd),
766
- label: zh.tools.ls.label,
767
-
768
- renderCall(args: any, theme: any) {
769
- let text = theme.fg("toolTitle", theme.bold(zh.tools.ls.title));
770
- text += theme.fg("accent", args.path || ".");
771
- return new Text(text, 0, 0);
772
- },
773
-
774
- renderResult: countResultRender(
775
- {
776
- searching: zh.tools.ls.listing,
777
- noMatch: zh.tools.ls.empty,
778
- found: zh.tools.ls.entries,
779
- limit: zh.tools.ls.entryLimit,
780
- truncated: zh.tools.grep.truncated,
781
- },
782
- "(empty directory)",
783
- "entryLimitReached",
784
- ),
785
- });
786
- }
787
-
788
- if (builtinTools.has("powershell")) {
789
- pi.registerTool({
790
- ...createPowerShellTool(cwd),
791
- label: zh.tools.powershell.label,
792
- ...shellRenders(zh.tools.powershell, [/only available on windows/i]),
793
- });
794
- }
795
-
796
- // 同名注册可能改变 active 集合;恢复注册前的工具启用状态。
797
- pi.setActiveTools(activeTools);
798
- }
799
56
  }