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
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- 底部状态栏:当前目录 / 分支 / 会话名、token 与缓存用量、费用、上下文占比、模型和思考等级
|
|
11
11
|
- 加载提示:「正在处理…」;thinking 折叠块的标签
|
|
12
12
|
- 八个内置工具的执行结果:读取、执行(bash)、编辑、写入、搜索内容、查找文件、列出目录、PowerShell——状态行都是中文,比如「完成」「退出码 N」「输出过长已截断」
|
|
13
|
+
- 项目信任弹窗:信任/不信任选择,含「信任父文件夹」变体
|
|
13
14
|
|
|
14
15
|
发给模型的内容不变。工具的 description 保持英文原文,避免影响模型行为。
|
|
15
16
|
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中文 footer:逐行复刻内置信息(pwd 行 / 统计行 / 扩展状态行),仅描述文案用中文。
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
8
|
+
import {
|
|
9
|
+
CONFIG_DIR_NAME,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
type ExtensionContext,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
14
|
+
import { zh } from "./zh.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 安装中文 footer(仅 TUI 模式,其他模式返回 undefined)。
|
|
18
|
+
*
|
|
19
|
+
* footer 逐行复刻内置信息结构:
|
|
20
|
+
* 1. pwd 行 —— 当前目录(~ 缩写)+ git 分支 + 会话名;
|
|
21
|
+
* 2. 统计行 —— 左侧为输入/输出 token、缓存读写与命中率、费用、上下文
|
|
22
|
+
* 占比(按阈值变色),右侧为 provider • 模型 • 思考等级(放不下时省略);
|
|
23
|
+
* 3. 扩展状态行 —— 各扩展 `setStatus` 的文本。
|
|
24
|
+
* 仅描述文案用中文,数据格式与 built-in 一致。
|
|
25
|
+
*
|
|
26
|
+
* @param ctx 当前会话上下文
|
|
27
|
+
* @returns 切换函数:第一次调用恢复内置 footer,再调用切回中文 footer;
|
|
28
|
+
* 非 TUI 模式下为 undefined
|
|
29
|
+
*/
|
|
30
|
+
export function setupChineseFooter(ctx: ExtensionContext): (() => void) | undefined {
|
|
31
|
+
if (ctx.mode !== "tui") return undefined;
|
|
32
|
+
|
|
33
|
+
let usingZhFooter = true;
|
|
34
|
+
|
|
35
|
+
const applyZhFooter = () => {
|
|
36
|
+
usingZhFooter = true;
|
|
37
|
+
// 自动压缩标记:全局设置为基准,受信任项目的本地设置优先。
|
|
38
|
+
// 一秒缓存避免流式渲染时频繁读文件,同时能跟随 /settings 的运行时修改。
|
|
39
|
+
const settingsPaths = [join(getAgentDir(), "settings.json")];
|
|
40
|
+
if (ctx.isProjectTrusted())
|
|
41
|
+
settingsPaths.push(join(ctx.cwd, CONFIG_DIR_NAME, "settings.json"));
|
|
42
|
+
let autoCompactEnabled = true;
|
|
43
|
+
let autoCompactCheckedAt = 0;
|
|
44
|
+
/**
|
|
45
|
+
* 返回自动压缩标记文案。设置读取带 1 秒缓存:避免流式渲染时每次
|
|
46
|
+
* 重绘都读文件,同时仍能跟随 /settings 的运行时修改。全局设置为基准,
|
|
47
|
+
* 受信任项目的本地设置优先覆盖。
|
|
48
|
+
*/
|
|
49
|
+
const getAutoCompactSuffix = () => {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (now - autoCompactCheckedAt >= 1000) {
|
|
52
|
+
autoCompactEnabled = true;
|
|
53
|
+
for (const settingsPath of settingsPaths) {
|
|
54
|
+
try {
|
|
55
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
56
|
+
if (typeof settings?.compaction?.enabled === "boolean") {
|
|
57
|
+
autoCompactEnabled = settings.compaction.enabled;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// 设置文件不存在或内容无效时忽略该层。
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
autoCompactCheckedAt = now;
|
|
64
|
+
}
|
|
65
|
+
return autoCompactEnabled ? zh.footer.autoCompactSuffix : "";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let unsubscribeBranch: (() => void) | undefined;
|
|
69
|
+
|
|
70
|
+
// footer 渲染器:onBranchChange 触发重绘;dispose 时取消订阅。
|
|
71
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
72
|
+
unsubscribeBranch?.();
|
|
73
|
+
unsubscribeBranch = footerData.onBranchChange(() =>
|
|
74
|
+
tui.requestRender(),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const home = homedir();
|
|
78
|
+
return {
|
|
79
|
+
dispose() {
|
|
80
|
+
unsubscribeBranch?.();
|
|
81
|
+
unsubscribeBranch = undefined;
|
|
82
|
+
},
|
|
83
|
+
invalidate() {},
|
|
84
|
+
/** 汇总会话 usage 统计并渲染三行中文 footer,见模块级说明。 */
|
|
85
|
+
render(width: number): string[] {
|
|
86
|
+
let input = 0,
|
|
87
|
+
output = 0,
|
|
88
|
+
cacheRead = 0,
|
|
89
|
+
cacheWrite = 0,
|
|
90
|
+
cost = 0;
|
|
91
|
+
const addUsage = (t: {
|
|
92
|
+
input: number;
|
|
93
|
+
output: number;
|
|
94
|
+
cacheRead: number;
|
|
95
|
+
cacheWrite: number;
|
|
96
|
+
cost: { total: number };
|
|
97
|
+
}) => {
|
|
98
|
+
input += t.input;
|
|
99
|
+
output += t.output;
|
|
100
|
+
cacheRead += t.cacheRead;
|
|
101
|
+
cacheWrite += t.cacheWrite;
|
|
102
|
+
cost += t.cost.total;
|
|
103
|
+
};
|
|
104
|
+
let latestRate: number | undefined;
|
|
105
|
+
|
|
106
|
+
for (const e of ctx.sessionManager.getEntries()) {
|
|
107
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
108
|
+
const u = (e.message as AssistantMessage).usage;
|
|
109
|
+
addUsage(u);
|
|
110
|
+
const promptTokens = u.input + u.cacheRead + u.cacheWrite;
|
|
111
|
+
latestRate =
|
|
112
|
+
promptTokens > 0
|
|
113
|
+
? (u.cacheRead / promptTokens) * 100
|
|
114
|
+
: undefined;
|
|
115
|
+
} else if (
|
|
116
|
+
e.type === "message" &&
|
|
117
|
+
e.message.role === "toolResult" &&
|
|
118
|
+
e.message.usage
|
|
119
|
+
) {
|
|
120
|
+
addUsage(e.message.usage as any);
|
|
121
|
+
} else if (
|
|
122
|
+
(e.type === "branch_summary" || e.type === "compaction") &&
|
|
123
|
+
(e as any).usage
|
|
124
|
+
) {
|
|
125
|
+
addUsage((e as any).usage);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fmt = (n: number) =>
|
|
130
|
+
n < 1e3
|
|
131
|
+
? `${n}`
|
|
132
|
+
: n < 1e4
|
|
133
|
+
? `${(n / 1e3).toFixed(1)}k`
|
|
134
|
+
: n < 1e6
|
|
135
|
+
? `${Math.round(n / 1e3)}k`
|
|
136
|
+
: n < 1e7
|
|
137
|
+
? `${(n / 1e6).toFixed(1)}M`
|
|
138
|
+
: `${Math.round(n / 1e6)}M`;
|
|
139
|
+
|
|
140
|
+
// 第 1 行:当前目录:~路径(分支)• 会话名
|
|
141
|
+
let pwd = ctx.sessionManager.getCwd();
|
|
142
|
+
const homeRelative = relative(home, pwd);
|
|
143
|
+
if (homeRelative === "") pwd = "~";
|
|
144
|
+
else if (
|
|
145
|
+
homeRelative &&
|
|
146
|
+
!homeRelative.startsWith("..") &&
|
|
147
|
+
!homeRelative.startsWith("/")
|
|
148
|
+
)
|
|
149
|
+
pwd = `~/${homeRelative}`;
|
|
150
|
+
const branch = footerData.getGitBranch();
|
|
151
|
+
if (branch) pwd += ` ${zh.footer.branchPrefix}(${branch})`;
|
|
152
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
153
|
+
if (sessionName) pwd += ` • ${sessionName}`;
|
|
154
|
+
pwd = `${zh.footer.cwdPrefix}${pwd}`;
|
|
155
|
+
|
|
156
|
+
// 第 2 行左:严格按内置顺序:输入、输出、缓存、费用、上下文占比。
|
|
157
|
+
const clauses: string[] = [];
|
|
158
|
+
if (input > 0 || output > 0) {
|
|
159
|
+
const io: string[] = [];
|
|
160
|
+
if (input > 0) io.push(`${zh.footer.inputLabel}↑${fmt(input)}`);
|
|
161
|
+
if (output > 0)
|
|
162
|
+
io.push(`${zh.footer.outputLabel}↓${fmt(output)}`);
|
|
163
|
+
clauses.push(io.join("/"));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const cacheParts: string[] = [];
|
|
167
|
+
if (cacheRead > 0)
|
|
168
|
+
cacheParts.push(`${zh.footer.cacheReadLabel}${fmt(cacheRead)}`);
|
|
169
|
+
if (cacheWrite > 0)
|
|
170
|
+
cacheParts.push(
|
|
171
|
+
`${zh.footer.cacheWriteLabel}${fmt(cacheWrite)}`,
|
|
172
|
+
);
|
|
173
|
+
if (
|
|
174
|
+
(cacheRead > 0 || cacheWrite > 0) &&
|
|
175
|
+
latestRate !== undefined
|
|
176
|
+
) {
|
|
177
|
+
cacheParts.push(
|
|
178
|
+
`${zh.footer.cacheHitLabel}${latestRate.toFixed(1)}%`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (cacheParts.length > 0) clauses.push(cacheParts.join(" "));
|
|
182
|
+
|
|
183
|
+
const provider = ctx.model
|
|
184
|
+
? ctx.modelRegistry.getProvider(ctx.model.provider)
|
|
185
|
+
: undefined;
|
|
186
|
+
const usingSubscription =
|
|
187
|
+
ctx.model?.provider === "kimi-coding" ||
|
|
188
|
+
(!!ctx.model &&
|
|
189
|
+
ctx.modelRegistry.isUsingOAuth(ctx.model) &&
|
|
190
|
+
provider?.auth.oauth?.isSubscription === true);
|
|
191
|
+
if (cost > 0 || usingSubscription) {
|
|
192
|
+
clauses.push(
|
|
193
|
+
`${zh.footer.costLabel}$${cost.toFixed(3)}${usingSubscription ? zh.footer.subSuffix : ""}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// built-in 顺序的最后一项:上下文占比 / 上下文窗口(自动压缩)。
|
|
198
|
+
const cu = ctx.getContextUsage();
|
|
199
|
+
const windowSize =
|
|
200
|
+
cu?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
201
|
+
const pctValue = cu?.percent ?? 0;
|
|
202
|
+
// 与 built-in 一致:明确为 null 时显示未知;尚无 usage 时按 0.0% 显示。
|
|
203
|
+
const pctText =
|
|
204
|
+
cu?.percent === null ? "?" : `${pctValue.toFixed(1)}%`;
|
|
205
|
+
const contextClause = `${zh.footer.ratioLabel}${pctText}/${fmt(windowSize)}${getAutoCompactSuffix()}`;
|
|
206
|
+
const contextIndex = clauses.length;
|
|
207
|
+
clauses.push(contextClause);
|
|
208
|
+
|
|
209
|
+
// 占比按阈值变色(>90% 红、>70% 黄);实验标记沿用 built-in 样式。
|
|
210
|
+
const statsLeft = clauses
|
|
211
|
+
.map((clause, index) => {
|
|
212
|
+
if (index === contextIndex) {
|
|
213
|
+
return pctValue > 90
|
|
214
|
+
? theme.fg("error", clause)
|
|
215
|
+
: pctValue > 70
|
|
216
|
+
? theme.fg("warning", clause)
|
|
217
|
+
: theme.fg("dim", clause);
|
|
218
|
+
}
|
|
219
|
+
return theme.fg("dim", clause);
|
|
220
|
+
})
|
|
221
|
+
.join(" ");
|
|
222
|
+
|
|
223
|
+
// 第 2 行右:provider • 模型 • 思考等级(等级映射为中文)
|
|
224
|
+
let rightSideWithoutProvider = ctx.model?.id ?? zh.footer.noModel;
|
|
225
|
+
if (ctx.model?.reasoning) {
|
|
226
|
+
const tl = ctx.thinkingLevel || "off";
|
|
227
|
+
const tlLabel =
|
|
228
|
+
tl === "off"
|
|
229
|
+
? zh.footer.thinkingOff
|
|
230
|
+
: ((zh.footer.thinkingLevels as Record<string, string>)[
|
|
231
|
+
tl
|
|
232
|
+
] ?? tl);
|
|
233
|
+
rightSideWithoutProvider = `${rightSideWithoutProvider} • ${tlLabel}`;
|
|
234
|
+
}
|
|
235
|
+
let rightSide = rightSideWithoutProvider;
|
|
236
|
+
if (footerData.getAvailableProviderCount() > 1 && ctx.model) {
|
|
237
|
+
rightSide = `(${ctx.model.provider}) ${rightSideWithoutProvider}`;
|
|
238
|
+
if (
|
|
239
|
+
visibleWidth(statsLeft) + 2 + visibleWidth(rightSide) >
|
|
240
|
+
width
|
|
241
|
+
) {
|
|
242
|
+
rightSide = rightSideWithoutProvider;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let statsDisplay = statsLeft;
|
|
247
|
+
let statsWidth = visibleWidth(statsDisplay);
|
|
248
|
+
if (statsWidth > width) {
|
|
249
|
+
statsDisplay = truncateToWidth(statsDisplay, width, "...");
|
|
250
|
+
statsWidth = visibleWidth(statsDisplay);
|
|
251
|
+
}
|
|
252
|
+
const availableForRight = width - statsWidth - 2;
|
|
253
|
+
let statsLine = statsDisplay;
|
|
254
|
+
if (availableForRight > 0) {
|
|
255
|
+
const truncatedRight = truncateToWidth(
|
|
256
|
+
rightSide,
|
|
257
|
+
availableForRight,
|
|
258
|
+
"",
|
|
259
|
+
);
|
|
260
|
+
const padding = " ".repeat(
|
|
261
|
+
Math.max(
|
|
262
|
+
0,
|
|
263
|
+
width - statsWidth - visibleWidth(truncatedRight),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
statsLine = `${statsDisplay}${padding}${theme.fg("dim", truncatedRight)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const lines = [
|
|
270
|
+
truncateToWidth(
|
|
271
|
+
theme.fg("dim", pwd),
|
|
272
|
+
width,
|
|
273
|
+
theme.fg("dim", "..."),
|
|
274
|
+
),
|
|
275
|
+
];
|
|
276
|
+
lines.push(statsLine);
|
|
277
|
+
|
|
278
|
+
// 第 3 行:各扩展的 setStatus 状态
|
|
279
|
+
const statuses = footerData.getExtensionStatuses();
|
|
280
|
+
if (statuses.size > 0) {
|
|
281
|
+
const statusLine = Array.from(statuses.entries())
|
|
282
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
283
|
+
.map(([, text]) =>
|
|
284
|
+
text
|
|
285
|
+
.replace(/[\r\n\t]/g, " ")
|
|
286
|
+
.replace(/ +/g, " ")
|
|
287
|
+
.trim(),
|
|
288
|
+
)
|
|
289
|
+
.join(" ");
|
|
290
|
+
lines.push(
|
|
291
|
+
truncateToWidth(statusLine, width, theme.fg("dim", "...")),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return lines;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
applyZhFooter();
|
|
301
|
+
return () => {
|
|
302
|
+
if (usingZhFooter) {
|
|
303
|
+
usingZhFooter = false;
|
|
304
|
+
ctx.ui.setFooter(undefined);
|
|
305
|
+
} else {
|
|
306
|
+
applyZhFooter();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中文 header:复刻内置结构(标题 + 折叠提示行 + 引导语),
|
|
3
|
+
* 快捷键跟随用户键位配置。
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
keyHint,
|
|
7
|
+
keyText,
|
|
8
|
+
rawKeyHint,
|
|
9
|
+
VERSION,
|
|
10
|
+
type ExtensionContext,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { zh } from "./zh.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 设置中文启动 header(仅 TUI 模式,其他模式无效果)。
|
|
16
|
+
*
|
|
17
|
+
* 复刻内置结构:logo + 折叠提示行(快捷键跟随用户键位配置)+
|
|
18
|
+
* 展开帮助语 + 引导文案。
|
|
19
|
+
*
|
|
20
|
+
* @param ctx 当前会话上下文
|
|
21
|
+
*/
|
|
22
|
+
export function setChineseHeader(ctx: ExtensionContext) {
|
|
23
|
+
if (ctx.mode !== "tui") return;
|
|
24
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
25
|
+
render(_width: number): string[] {
|
|
26
|
+
const logo =
|
|
27
|
+
theme.bold(theme.fg("accent", "pi")) +
|
|
28
|
+
theme.fg("dim", ` v${VERSION}`);
|
|
29
|
+
const compact = [
|
|
30
|
+
keyHint("app.interrupt", zh.header.interrupt),
|
|
31
|
+
rawKeyHint(
|
|
32
|
+
`${keyText("app.clear")}/${keyText("app.exit")}`,
|
|
33
|
+
zh.header.clearExit,
|
|
34
|
+
),
|
|
35
|
+
rawKeyHint("/", zh.header.commands),
|
|
36
|
+
rawKeyHint("!", "bash"),
|
|
37
|
+
keyHint("app.tools.expand", zh.header.more),
|
|
38
|
+
].join(theme.fg("muted", " · "));
|
|
39
|
+
return [
|
|
40
|
+
logo,
|
|
41
|
+
compact,
|
|
42
|
+
theme.fg(
|
|
43
|
+
"dim",
|
|
44
|
+
`${zh.header.expandHelp} (${keyText("app.tools.expand")})`,
|
|
45
|
+
),
|
|
46
|
+
"",
|
|
47
|
+
theme.fg("dim", zh.header.onboarding),
|
|
48
|
+
];
|
|
49
|
+
},
|
|
50
|
+
invalidate() {},
|
|
51
|
+
}));
|
|
52
|
+
}
|