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.
- package/README.md +1 -0
- package/extensions/footer.ts +309 -0
- package/extensions/header.ts +52 -0
- package/extensions/index.ts +31 -774
- package/extensions/localized-tools.ts +265 -0
- package/extensions/tool-renders.ts +184 -0
- package/extensions/trust.ts +144 -0
- package/extensions/ui-copy.ts +53 -0
- package/extensions/zh.ts +12 -2
- package/package.json +5 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置 read/bash/edit/write/grep/find/ls/powershell 工具的中文渲染。
|
|
3
|
+
* 同名重注册,执行委托原实现(createXxxTool);
|
|
4
|
+
* 不改动发给模型的内容(工具 description 保持英文)。
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
createBashTool,
|
|
9
|
+
createEditTool,
|
|
10
|
+
createFindTool,
|
|
11
|
+
createGrepTool,
|
|
12
|
+
createLsTool,
|
|
13
|
+
createPowerShellTool,
|
|
14
|
+
createReadTool,
|
|
15
|
+
createWriteTool,
|
|
16
|
+
type BashToolDetails,
|
|
17
|
+
type EditToolDetails,
|
|
18
|
+
type ReadToolDetails,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
21
|
+
import { zh } from "./zh.ts";
|
|
22
|
+
import {
|
|
23
|
+
countResultRender,
|
|
24
|
+
errorFirstLine,
|
|
25
|
+
notePartial,
|
|
26
|
+
shellRenders,
|
|
27
|
+
withExpanded,
|
|
28
|
+
} from "./tool-renders.ts";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 重注册所有内置工具的中文渲染。
|
|
32
|
+
*
|
|
33
|
+
* 逐个用 `createXxxTool(cwd)` 重新生成内置实现并展开同名注册:执行逻辑、
|
|
34
|
+
* 参数与 description(发给模型的)保持原样,仅覆盖 `label` 与
|
|
35
|
+
* `renderCall`/`renderResult` 为中文渲染;各工具在 `zh.tools.*` 下有自己的
|
|
36
|
+
* 文案对象。grep / find / ls / powershell 仅在内置工具集存在时注册。
|
|
37
|
+
*
|
|
38
|
+
* @param pi 扩展 API 实例
|
|
39
|
+
* @param cwd 当前项目目录(透传给 createXxxTool)
|
|
40
|
+
*/
|
|
41
|
+
export function registerLocalizedTools(pi: ExtensionAPI, cwd: string) {
|
|
42
|
+
const activeTools = pi.getActiveTools();
|
|
43
|
+
const builtinTools = new Set(
|
|
44
|
+
pi
|
|
45
|
+
.getAllTools()
|
|
46
|
+
.filter((tool) => tool.sourceInfo.source === "builtin")
|
|
47
|
+
.map((tool) => tool.name),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// read
|
|
51
|
+
pi.registerTool({
|
|
52
|
+
...createReadTool(cwd),
|
|
53
|
+
label: zh.tools.read.label,
|
|
54
|
+
|
|
55
|
+
renderCall(args: any, theme: any) {
|
|
56
|
+
let text = theme.fg("toolTitle", theme.bold(`${zh.tools.read.title} `));
|
|
57
|
+
text += theme.fg("accent", args.path);
|
|
58
|
+
if (args.offset || args.limit) {
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
if (args.offset) parts.push(`offset=${args.offset}`);
|
|
61
|
+
if (args.limit) parts.push(`limit=${args.limit}`);
|
|
62
|
+
text += theme.fg("dim", ` (${parts.join(", ")})`);
|
|
63
|
+
}
|
|
64
|
+
return new Text(text, 0, 0);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
renderResult(
|
|
68
|
+
result: any,
|
|
69
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
70
|
+
theme: any,
|
|
71
|
+
) {
|
|
72
|
+
if (isPartial) return notePartial(zh.tools.read.reading, theme);
|
|
73
|
+
|
|
74
|
+
const content = result.content[0];
|
|
75
|
+
if (content?.type === "image") {
|
|
76
|
+
return new Text(theme.fg("success", zh.tools.read.imageLoaded), 0, 0);
|
|
77
|
+
}
|
|
78
|
+
if (content?.type !== "text") {
|
|
79
|
+
return new Text(theme.fg("error", zh.tools.read.noContent), 0, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let text = theme.fg(
|
|
83
|
+
"success",
|
|
84
|
+
zh.tools.read.lines(content.text.split("\n").length),
|
|
85
|
+
);
|
|
86
|
+
const truncation = (result.details as ReadToolDetails | undefined)
|
|
87
|
+
?.truncation;
|
|
88
|
+
if (truncation?.truncated) {
|
|
89
|
+
text += theme.fg(
|
|
90
|
+
"warning",
|
|
91
|
+
zh.tools.read.truncatedFrom(truncation.totalLines),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return new Text(
|
|
95
|
+
withExpanded(expanded ? content.text : "", text, theme),
|
|
96
|
+
0,
|
|
97
|
+
0,
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// bash / powershell(渲染逻辑同构,仅文案与失败判定集合不同)
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
...createBashTool(cwd),
|
|
105
|
+
label: zh.tools.bash.label,
|
|
106
|
+
...shellRenders(zh.tools.bash),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// edit
|
|
110
|
+
pi.registerTool({
|
|
111
|
+
...createEditTool(cwd),
|
|
112
|
+
label: zh.tools.edit.label,
|
|
113
|
+
renderShell: "self",
|
|
114
|
+
|
|
115
|
+
renderCall(args: any, theme: any) {
|
|
116
|
+
let text = theme.fg("toolTitle", theme.bold(zh.tools.edit.title));
|
|
117
|
+
text += theme.fg("accent", args.path);
|
|
118
|
+
return new Text(text, 0, 0);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
renderResult(
|
|
122
|
+
result: any,
|
|
123
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
124
|
+
theme: any,
|
|
125
|
+
) {
|
|
126
|
+
if (isPartial) return notePartial(zh.tools.edit.editing, theme);
|
|
127
|
+
|
|
128
|
+
const err = errorFirstLine(result, theme);
|
|
129
|
+
if (err) return err;
|
|
130
|
+
|
|
131
|
+
const details = result.details as EditToolDetails | undefined;
|
|
132
|
+
if (!details?.diff) {
|
|
133
|
+
return new Text(theme.fg("success", zh.tools.edit.applied), 0, 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let additions = 0,
|
|
137
|
+
removals = 0;
|
|
138
|
+
for (const line of details.diff.split("\n")) {
|
|
139
|
+
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
140
|
+
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let text = theme.fg("success", zh.tools.edit.additions(additions));
|
|
144
|
+
text += theme.fg("dim", " / ");
|
|
145
|
+
text += theme.fg("error", zh.tools.edit.removals(removals));
|
|
146
|
+
|
|
147
|
+
return new Text(
|
|
148
|
+
withExpanded(expanded ? details.diff : "", text, theme),
|
|
149
|
+
0,
|
|
150
|
+
0,
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// write
|
|
156
|
+
pi.registerTool({
|
|
157
|
+
...createWriteTool(cwd),
|
|
158
|
+
label: zh.tools.write.label,
|
|
159
|
+
|
|
160
|
+
renderCall(args: any, theme: any) {
|
|
161
|
+
let text = theme.fg("toolTitle", theme.bold(zh.tools.write.title));
|
|
162
|
+
text += theme.fg("accent", args.path);
|
|
163
|
+
const lineCount = String(args.content).split("\n").length;
|
|
164
|
+
text += theme.fg("dim", ` (${zh.tools.write.lines(lineCount)})`);
|
|
165
|
+
return new Text(text, 0, 0);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
renderResult(result: any, { isPartial }: { isPartial: boolean }, theme: any) {
|
|
169
|
+
if (isPartial) return notePartial(zh.tools.write.writing, theme);
|
|
170
|
+
const err = errorFirstLine(result, theme);
|
|
171
|
+
if (err) return err;
|
|
172
|
+
return new Text(theme.fg("success", zh.tools.write.written), 0, 0);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// grep / find / ls:调用渲染各自略有差异,结果渲染走同一个计数模板。
|
|
177
|
+
if (builtinTools.has("grep")) {
|
|
178
|
+
pi.registerTool({
|
|
179
|
+
...createGrepTool(cwd),
|
|
180
|
+
label: zh.tools.grep.label,
|
|
181
|
+
|
|
182
|
+
renderCall(args: any, theme: any) {
|
|
183
|
+
let text = theme.fg("toolTitle", theme.bold(zh.tools.grep.title));
|
|
184
|
+
text += theme.fg("accent", args.pattern);
|
|
185
|
+
if (args.path) text += theme.fg("dim", ` · ${args.path}`);
|
|
186
|
+
if (args.glob) text += theme.fg("dim", ` (${args.glob})`);
|
|
187
|
+
return new Text(text, 0, 0);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
renderResult: countResultRender(
|
|
191
|
+
{
|
|
192
|
+
searching: zh.tools.grep.searching,
|
|
193
|
+
noMatch: zh.tools.grep.noMatches,
|
|
194
|
+
found: zh.tools.grep.matches,
|
|
195
|
+
limit: zh.tools.grep.matchLimit,
|
|
196
|
+
truncated: zh.tools.grep.truncated,
|
|
197
|
+
},
|
|
198
|
+
"No matches found",
|
|
199
|
+
"matchLimitReached",
|
|
200
|
+
),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (builtinTools.has("find")) {
|
|
205
|
+
pi.registerTool({
|
|
206
|
+
...createFindTool(cwd),
|
|
207
|
+
label: zh.tools.find.label,
|
|
208
|
+
|
|
209
|
+
renderCall(args: any, theme: any) {
|
|
210
|
+
let text = theme.fg("toolTitle", theme.bold(zh.tools.find.title));
|
|
211
|
+
text += theme.fg("accent", args.pattern);
|
|
212
|
+
if (args.path) text += theme.fg("dim", ` · ${args.path}`);
|
|
213
|
+
return new Text(text, 0, 0);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
renderResult: countResultRender(
|
|
217
|
+
{
|
|
218
|
+
searching: zh.tools.find.searching,
|
|
219
|
+
noMatch: zh.tools.find.noFiles,
|
|
220
|
+
found: zh.tools.find.results,
|
|
221
|
+
limit: zh.tools.find.resultLimit,
|
|
222
|
+
truncated: zh.tools.grep.truncated,
|
|
223
|
+
},
|
|
224
|
+
"No files found matching pattern",
|
|
225
|
+
"resultLimitReached",
|
|
226
|
+
),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (builtinTools.has("ls")) {
|
|
231
|
+
pi.registerTool({
|
|
232
|
+
...createLsTool(cwd),
|
|
233
|
+
label: zh.tools.ls.label,
|
|
234
|
+
|
|
235
|
+
renderCall(args: any, theme: any) {
|
|
236
|
+
let text = theme.fg("toolTitle", theme.bold(zh.tools.ls.title));
|
|
237
|
+
text += theme.fg("accent", args.path || ".");
|
|
238
|
+
return new Text(text, 0, 0);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
renderResult: countResultRender(
|
|
242
|
+
{
|
|
243
|
+
searching: zh.tools.ls.listing,
|
|
244
|
+
noMatch: zh.tools.ls.empty,
|
|
245
|
+
found: zh.tools.ls.entries,
|
|
246
|
+
limit: zh.tools.ls.entryLimit,
|
|
247
|
+
truncated: zh.tools.grep.truncated,
|
|
248
|
+
},
|
|
249
|
+
"(empty directory)",
|
|
250
|
+
"entryLimitReached",
|
|
251
|
+
),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (builtinTools.has("powershell")) {
|
|
256
|
+
pi.registerTool({
|
|
257
|
+
...createPowerShellTool(cwd),
|
|
258
|
+
label: zh.tools.powershell.label,
|
|
259
|
+
...shellRenders(zh.tools.powershell, [/only available on windows/i]),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 同名注册可能改变 active 集合;恢复注册前的工具启用状态。
|
|
264
|
+
pi.setActiveTools(activeTools);
|
|
265
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置工具渲染的共享文案模板:read/bash/edit/write/grep/find/ls
|
|
3
|
+
* 的 renderResult 共用这些小工具。
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
BashToolDetails,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
9
|
+
import { zh } from "./zh.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 取工具结果中的首个 text 块内容;结果不含文本块(如图片)时返回空串。
|
|
13
|
+
*/
|
|
14
|
+
export const textOf = (result: any): string =>
|
|
15
|
+
result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
16
|
+
|
|
17
|
+
/** 统计非空行数,用于「N 行输出」类摘要。 */
|
|
18
|
+
export const countNonEmpty = (s: string): number =>
|
|
19
|
+
s.split("\n").filter((l) => l.trim()).length;
|
|
20
|
+
|
|
21
|
+
/** 流式部分结果的占位文案(黄色提示行)。 */
|
|
22
|
+
export const notePartial = (msg: string, theme: any): Text =>
|
|
23
|
+
new Text(theme.fg("warning", msg), 0, 0);
|
|
24
|
+
|
|
25
|
+
// 错误结果:内容以 "Error" 开头时只展示首行(edit/write 共用)。
|
|
26
|
+
/** 展示错误首行(红色);结果不是错误时返回 undefined。 */
|
|
27
|
+
export const errorFirstLine = (result: any, theme: any): Text | undefined => {
|
|
28
|
+
const out = textOf(result);
|
|
29
|
+
return out.startsWith("Error")
|
|
30
|
+
? new Text(theme.fg("error", out.split("\n")[0]), 0, 0)
|
|
31
|
+
: undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 展开态:在摘要后追加逐行弱化的原始输出。
|
|
35
|
+
/**
|
|
36
|
+
* 组装展开态文本:raw 为空时返回 summary,否则在 summary 之后
|
|
37
|
+
* 追加逐行弱化(dim 色)的原始输出。
|
|
38
|
+
*/
|
|
39
|
+
export const withExpanded = (raw: string, summary: string, theme: any): string =>
|
|
40
|
+
raw
|
|
41
|
+
? `${summary}\n${raw
|
|
42
|
+
.split("\n")
|
|
43
|
+
.map((l) => theme.fg("dim", l))
|
|
44
|
+
.join("\n")}`
|
|
45
|
+
: summary;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 判定 shell 命令是否失败。
|
|
49
|
+
*
|
|
50
|
+
* 内置 bash 工具不返回结构化退出码,只能从输出文本推断:匹配
|
|
51
|
+
* 「exit code: N」/「command exited with code N」即失败;以 Error 开头、
|
|
52
|
+
* 或含超时/中止提示也视为失败。powershell 在 Windows 上额外追加平台
|
|
53
|
+
* 提示,通过 extraRe 补充识别。
|
|
54
|
+
*
|
|
55
|
+
* @param output 工具输出的完整文本
|
|
56
|
+
* @param extraRe 额外的失败特征正则(默认无)
|
|
57
|
+
* @returns exitCode 解析到的退出码(无法解析为 null);failed 是否视为失败
|
|
58
|
+
*/
|
|
59
|
+
function failureState(output: string, extraRe: RegExp[] = []) {
|
|
60
|
+
const m = output.match(
|
|
61
|
+
/(?:exit code:\s*|command exited with code\s+)(\d+)/i,
|
|
62
|
+
);
|
|
63
|
+
const exitCode = m ? parseInt(m[1], 10) : null;
|
|
64
|
+
return {
|
|
65
|
+
exitCode,
|
|
66
|
+
failed:
|
|
67
|
+
exitCode !== null ||
|
|
68
|
+
/^error\b/i.test(output) ||
|
|
69
|
+
/\n\ncommand (?:timed out|was aborted|failed)/i.test(output) ||
|
|
70
|
+
extraRe.some((re) => re.test(output)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// shell 类工具(bash/powershell 文案同构)的渲染对。
|
|
75
|
+
type ShellCopy =
|
|
76
|
+
| typeof zh.tools.bash
|
|
77
|
+
| typeof zh.tools.powershell;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 生成 shell 类工具(bash / powershell)共用的 renderCall + renderResult。
|
|
81
|
+
*
|
|
82
|
+
* 调用渲染显示命令(超 80 字符截断)与可选的超时参数;结果渲染根据
|
|
83
|
+
* {@link failureState} 显示完成/失败状态、非空行数与截断警告,展开态追加
|
|
84
|
+
* 逐行弱化的原始输出。
|
|
85
|
+
*
|
|
86
|
+
* @param w 该工具的文案对象(zh.tools.bash 或 zh.tools.powershell)
|
|
87
|
+
* @param extraRe 传给 {@link failureState} 的额外失败特征正则
|
|
88
|
+
*/
|
|
89
|
+
export function shellRenders(w: ShellCopy, extraRe: RegExp[] = []) {
|
|
90
|
+
return {
|
|
91
|
+
renderCall(args: any, theme: any): Text {
|
|
92
|
+
let text = theme.fg("toolTitle", theme.bold(w.title));
|
|
93
|
+
const cmd: string =
|
|
94
|
+
args.command.length > 80
|
|
95
|
+
? `${args.command.slice(0, 77)}...`
|
|
96
|
+
: args.command;
|
|
97
|
+
text += theme.fg("accent", cmd);
|
|
98
|
+
if (args.timeout) text += theme.fg("dim", w.timeout(args.timeout));
|
|
99
|
+
return new Text(text, 0, 0);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
renderResult(
|
|
103
|
+
result: any,
|
|
104
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
105
|
+
theme: any,
|
|
106
|
+
): Text {
|
|
107
|
+
if (isPartial) return notePartial(w.running, theme);
|
|
108
|
+
const output = textOf(result);
|
|
109
|
+
const { exitCode, failed } = failureState(output, extraRe);
|
|
110
|
+
let text = failed
|
|
111
|
+
? theme.fg(
|
|
112
|
+
"error",
|
|
113
|
+
exitCode !== null ? w.exit(exitCode) : w.failed,
|
|
114
|
+
)
|
|
115
|
+
: theme.fg("success", w.done);
|
|
116
|
+
text += theme.fg("dim", ` (${w.lines(countNonEmpty(output))})`);
|
|
117
|
+
if (
|
|
118
|
+
(result.details as BashToolDetails | undefined)?.truncation?.truncated
|
|
119
|
+
) {
|
|
120
|
+
text += theme.fg("warning", ` ${w.truncated}`);
|
|
121
|
+
}
|
|
122
|
+
return new Text(withExpanded(expanded ? output : "", text, theme), 0, 0);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 生成计数类工具(grep / find / ls)共用的 renderResult 渲染器。
|
|
129
|
+
*
|
|
130
|
+
* 结果形态四处一致:流式时显示「搜索中」占位;输出等于哨兵值时显示
|
|
131
|
+
* 「无匹配」(muted 色);否则显示「N 行/个/项」(success 色);达到上限
|
|
132
|
+
* (由 details 中 limitField 字段标记)或发生截断时追加警告。展开态在
|
|
133
|
+
* 摘要后追加原始输出(无匹配时用 noMatch 文案替代)。
|
|
134
|
+
*
|
|
135
|
+
* @param labels 各工具对应的中文文案对象(searching 流式占位、noMatch 无
|
|
136
|
+
* 匹配文案、found 条数摘要、limit 可选的上限提示、truncated 截断警告)
|
|
137
|
+
* @param sentinel 无匹配时内置工具输出的哨兵字符串
|
|
138
|
+
* @param limitField details 中标记「已达到上限」的字段名
|
|
139
|
+
* (matchLimitReached / resultLimitReached / entryLimitReached)
|
|
140
|
+
*/
|
|
141
|
+
export function countResultRender(
|
|
142
|
+
labels: {
|
|
143
|
+
searching: string;
|
|
144
|
+
noMatch: string;
|
|
145
|
+
found: (n: number) => string;
|
|
146
|
+
limit?: (n: number) => string;
|
|
147
|
+
truncated: string;
|
|
148
|
+
},
|
|
149
|
+
sentinel: string,
|
|
150
|
+
limitField: string,
|
|
151
|
+
) {
|
|
152
|
+
return (
|
|
153
|
+
result: any,
|
|
154
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
155
|
+
theme: any,
|
|
156
|
+
): Text => {
|
|
157
|
+
if (isPartial) return notePartial(labels.searching, theme);
|
|
158
|
+
const details = result.details ?? {};
|
|
159
|
+
const output = textOf(result);
|
|
160
|
+
const isEmpty = output === sentinel;
|
|
161
|
+
let text = theme.fg(
|
|
162
|
+
isEmpty ? "muted" : "success",
|
|
163
|
+
isEmpty ? labels.noMatch : labels.found(countNonEmpty(output)),
|
|
164
|
+
);
|
|
165
|
+
if (labels.limit && typeof details[limitField] === "number") {
|
|
166
|
+
text += theme.fg("warning", labels.limit(details[limitField]));
|
|
167
|
+
}
|
|
168
|
+
// 与旧实现的行为差异:grep 原本同时检查 linesTruncated,find/ls 只查
|
|
169
|
+
// truncation.truncated;这里统一为两者都检查,Find/LsToolDetails 目前无
|
|
170
|
+
// linesTruncated 字段,运行时行为等价。若上游新增该字段需复核。
|
|
171
|
+
if (details.linesTruncated || details.truncation?.truncated) {
|
|
172
|
+
text += theme.fg("warning", labels.truncated);
|
|
173
|
+
}
|
|
174
|
+
return new Text(
|
|
175
|
+
withExpanded(
|
|
176
|
+
expanded ? (isEmpty ? labels.noMatch : output) : "",
|
|
177
|
+
text,
|
|
178
|
+
theme,
|
|
179
|
+
),
|
|
180
|
+
0,
|
|
181
|
+
0,
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目信任弹窗:中文选择器代替内置英文界面。
|
|
3
|
+
*
|
|
4
|
+
* 复刻 ~/「agent dir」/trust.json 的读写逻辑(与内置 ProjectTrustStore
|
|
5
|
+
* 同格式:键为 realpath 归一化路径,null 表示删除该键);已有决定的目录
|
|
6
|
+
* 不再弹窗。选项与内置一一对应,含「信任父文件夹」变体——扩展 API 表达
|
|
7
|
+
* 不了父目录决定,这里按内置 setMany 语义直接写 trust.json 实现。
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
getAgentDir,
|
|
19
|
+
type ExtensionAPI,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { zh } from "./zh.ts";
|
|
22
|
+
|
|
23
|
+
/** realpath 归一化(与 core 的 normalizeCwd 等价);目录不存在时原样返回。 */
|
|
24
|
+
const normalizeDir = (dir: string): string => {
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(dir);
|
|
27
|
+
} catch {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** trust.json 路径。 */
|
|
33
|
+
const trustFile = (): string => join(getAgentDir(), "trust.json");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查询某个目录已保存的项目信任决定。
|
|
37
|
+
*
|
|
38
|
+
* 查找规则与内置逻辑一致:先对目录做 realpath 归一化,然后沿祖先链在
|
|
39
|
+
* `~/<agentDir>/trust.json` 中查找该目录(或其任一祖先)的记录——子目录
|
|
40
|
+
* 继承最近的祖先决定;到达根目录仍无记录则视为未决定。
|
|
41
|
+
*
|
|
42
|
+
* @param dir 要查询的项目目录(绝对路径)
|
|
43
|
+
* @returns 已保存的信任(`true`)/ 不信任(`false`)决定;
|
|
44
|
+
* 目录未被记录、trust.json 缺失或内容无效时返回 `null`
|
|
45
|
+
*/
|
|
46
|
+
const savedTrustDecision = (dir: string): boolean | null => {
|
|
47
|
+
let current = normalizeDir(dir);
|
|
48
|
+
try {
|
|
49
|
+
const store = JSON.parse(readFileSync(trustFile(), "utf8"));
|
|
50
|
+
for (;;) {
|
|
51
|
+
const decision = store[current];
|
|
52
|
+
if (typeof decision === "boolean") return decision;
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current) return null;
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return null; // 无 trust.json 或内容无效 → 视作没有已存决定
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 写入信任决定,语义与内置 ProjectTrustStore.setMany / writeTrustFile
|
|
64
|
+
* 一致:`decision` 为 `null` 删除该键;输出按键排序、2 空格缩进、结尾
|
|
65
|
+
* 换行。文件缺失时从空表开始;内容无效时抛出(与内置一致,此时事件
|
|
66
|
+
* 处理器报错,core 回退到内置英文弹窗)。
|
|
67
|
+
*
|
|
68
|
+
* # ponytail: 无文件锁(内置用 withTrustFileLock);只在启动信任弹窗
|
|
69
|
+
* 时写入,并发窗口极小,出现实害再补锁文件。
|
|
70
|
+
*
|
|
71
|
+
* @param decisions 要写入的决定列表
|
|
72
|
+
* @param file trust.json 路径(默认 agentDir 下;测试可覆盖)
|
|
73
|
+
*/
|
|
74
|
+
export const saveTrustDecisions = (
|
|
75
|
+
decisions: Array<{ path: string; decision: boolean | null }>,
|
|
76
|
+
file: string = trustFile(),
|
|
77
|
+
): void => {
|
|
78
|
+
let store: Record<string, boolean | null> = {};
|
|
79
|
+
if (existsSync(file)) {
|
|
80
|
+
store = JSON.parse(readFileSync(file, "utf8"));
|
|
81
|
+
}
|
|
82
|
+
for (const { path, decision } of decisions) {
|
|
83
|
+
const key = normalizeDir(path);
|
|
84
|
+
if (decision === null) delete store[key];
|
|
85
|
+
else store[key] = decision;
|
|
86
|
+
}
|
|
87
|
+
const sorted: Record<string, boolean | null> = {};
|
|
88
|
+
for (const key of Object.keys(store).sort()) {
|
|
89
|
+
const value = store[key];
|
|
90
|
+
if (value === true || value === false || value === null) sorted[key] = value;
|
|
91
|
+
}
|
|
92
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
93
|
+
writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`, "utf8");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 注册 `project_trust` 事件处理器:用中文选择弹窗代替内置英文信任界面。
|
|
98
|
+
*
|
|
99
|
+
* 选「信任父文件夹」时按内置语义写 trust.json(父目录 = 信任、清除本目
|
|
100
|
+
* 录记录),并返回不带 `remember` 的结果——core 只在 `remember` 时写
|
|
101
|
+
* cwd,这样文件里只留下与内置选项完全相同的两条记录。
|
|
102
|
+
*
|
|
103
|
+
* 无 UI 的运行模式(rpc/print 等)或目录已有保存决定时不弹窗,返回
|
|
104
|
+
* `undecided` 让 core 走默认流程。
|
|
105
|
+
*
|
|
106
|
+
* @param pi 扩展 API 实例
|
|
107
|
+
*/
|
|
108
|
+
export function registerProjectTrustDialog(pi: ExtensionAPI) {
|
|
109
|
+
pi.on("project_trust", async (event, ctx) => {
|
|
110
|
+
// 无 UI(rpc/print 等模式)或已有保存的决定时返回 undecided,让 core 走默认流程。
|
|
111
|
+
if (!ctx.hasUI || savedTrustDecision(event.cwd) !== null) return { trusted: "undecided" };
|
|
112
|
+
|
|
113
|
+
const trustPath = normalizeDir(event.cwd);
|
|
114
|
+
const parentPath = dirname(trustPath);
|
|
115
|
+
const parentOption =
|
|
116
|
+
parentPath === trustPath ? undefined : zh.trust.trustParent(parentPath);
|
|
117
|
+
|
|
118
|
+
const options = [
|
|
119
|
+
zh.trust.options.trustAndRemember,
|
|
120
|
+
...(parentOption ? [parentOption] : []),
|
|
121
|
+
zh.trust.options.trustSessionOnly,
|
|
122
|
+
zh.trust.options.distrustAndRemember,
|
|
123
|
+
zh.trust.options.distrustSessionOnly,
|
|
124
|
+
];
|
|
125
|
+
const selected = await ctx.ui.select(zh.trust.title(event.cwd), options);
|
|
126
|
+
if (parentOption !== undefined && selected === parentOption) {
|
|
127
|
+
saveTrustDecisions([
|
|
128
|
+
{ path: parentPath, decision: true },
|
|
129
|
+
{ path: trustPath, decision: null },
|
|
130
|
+
]);
|
|
131
|
+
return { trusted: "yes" }; // 不带 remember:core 不写本目录,只留上面两条记录
|
|
132
|
+
}
|
|
133
|
+
switch (selected ?? zh.trust.options.distrustSessionOnly) {
|
|
134
|
+
case zh.trust.options.trustAndRemember:
|
|
135
|
+
return { trusted: "yes", remember: true };
|
|
136
|
+
case zh.trust.options.trustSessionOnly:
|
|
137
|
+
return { trusted: "yes" };
|
|
138
|
+
case zh.trust.options.distrustAndRemember:
|
|
139
|
+
return { trusted: "no", remember: true };
|
|
140
|
+
default:
|
|
141
|
+
return { trusted: "no" }; // 取消 = 本次不信任(与内置行为一致)
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI 文案模块:流式加载提示、thinking 折叠块标签,
|
|
3
|
+
* 以及 TUI 模式下中文化内置斜杠命令描述。
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { zh } from "./zh.ts";
|
|
7
|
+
|
|
8
|
+
// 替换是幂等的,跨 session 只包一次。
|
|
9
|
+
let autocompleteTranslated = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 中文化内置斜杠命令的补全描述(仅 TUI 模式)。
|
|
13
|
+
*
|
|
14
|
+
* 做法:在现有 autocomplete provider 外再包一层——`getSuggestions` 只把
|
|
15
|
+
* 命令项的 `description` 换成中文(zh.builtinCommands 按命令名查找,无对应
|
|
16
|
+
* 条目的项原样透传),文件路径等其他建议不改动;`applyCompletion` 直接
|
|
17
|
+
* 委托原实现。
|
|
18
|
+
*
|
|
19
|
+
* 整个进程只包装一次(跨 session 幂等),避免重复叠加翻译层。
|
|
20
|
+
*/
|
|
21
|
+
function translateBuiltinCommands(ctx: ExtensionContext) {
|
|
22
|
+
if (ctx.mode !== "tui" || autocompleteTranslated) return;
|
|
23
|
+
autocompleteTranslated = true;
|
|
24
|
+
ctx.ui.addAutocompleteProvider((current) => ({
|
|
25
|
+
...(current.triggerCharacters ? { triggerCharacters: current.triggerCharacters } : {}),
|
|
26
|
+
getSuggestions: async (lines, cursorLine, cursorCol, options) => {
|
|
27
|
+
const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
28
|
+
if (!suggestions) return null;
|
|
29
|
+
return {
|
|
30
|
+
...suggestions,
|
|
31
|
+
items: suggestions.items.map((item) => {
|
|
32
|
+
const key = item.value.replace(/^\//, "") as keyof typeof zh.builtinCommands;
|
|
33
|
+
const zhDesc = zh.builtinCommands[key];
|
|
34
|
+
return zhDesc ? { ...item, description: zhDesc } : item;
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
applyCompletion: (lines, cursorLine, cursorCol, item, prefix) =>
|
|
39
|
+
current.applyCompletion(lines, cursorLine, cursorCol, item, prefix),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 应用所有会话启动时的 UI 文案:流式加载提示、折叠 thinking 标签,
|
|
45
|
+
* 以及(TUI 模式下)内置斜杠命令的中文描述。
|
|
46
|
+
*
|
|
47
|
+
* @param ctx 当前会话上下文(要求 hasUI 为 true,由调用方保证)
|
|
48
|
+
*/
|
|
49
|
+
export function applyUiCopy(ctx: ExtensionContext) {
|
|
50
|
+
ctx.ui.setWorkingMessage(zh.workingMessage);
|
|
51
|
+
ctx.ui.setHiddenThinkingLabel(zh.hiddenThinkingLabel);
|
|
52
|
+
translateBuiltinCommands(ctx);
|
|
53
|
+
}
|
package/extensions/zh.ts
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* 工具 description(发给模型的)保持英文原文,避免影响模型行为。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 汉化文案表(全部界面文案的单一来源)。
|
|
10
|
+
*
|
|
11
|
+
* 只包含通过扩展 API 能覆盖的界面文案;工具 description(发给模型的)
|
|
12
|
+
* 保持英文原文,避免影响模型行为。各文件按需取用:
|
|
13
|
+
* - `footer.*` → footer.ts `header.*` → header.ts
|
|
14
|
+
* - `tools.*` → localized-tools.ts / tool-renders.ts
|
|
15
|
+
* - `trust.*` → trust.ts `builtinCommands` → ui-copy.ts
|
|
16
|
+
*/
|
|
8
17
|
export const zh = {
|
|
9
18
|
// 流式加载提示(默认 "Working...",工具执行时也会显示)
|
|
10
19
|
workingMessage: "正在处理…",
|
|
@@ -125,10 +134,11 @@ export const zh = {
|
|
|
125
134
|
},
|
|
126
135
|
},
|
|
127
136
|
|
|
128
|
-
//
|
|
137
|
+
// 项目信任弹窗(复刻内置全部选项;「信任父文件夹」由扩展直接写 trust.json)
|
|
129
138
|
trust: {
|
|
130
139
|
title: (cwd: string) =>
|
|
131
|
-
`是否信任项目目录:${cwd}
|
|
140
|
+
`是否信任项目目录:${cwd}?\n将允许 pi 加载 .pi 配置与资源、安装项目缺失的包、执行项目扩展`,
|
|
141
|
+
trustParent: (parent: string) => `信任父文件夹(${parent})`,
|
|
132
142
|
options: {
|
|
133
143
|
trustAndRemember: "信任并记住此决定",
|
|
134
144
|
trustSessionOnly: "仅本次会话信任",
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zh-cn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "pi 编码代理的简体中文界面汉化插件",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/GoetheDady/pi-zh-cn.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"keywords": [
|
|
7
11
|
"pi-package",
|