kcode-pi 0.1.27 → 0.1.31
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 +3 -0
- package/docs/CHANGELOG.md +46 -0
- package/docs/COMMAND_REFERENCE.md +55 -0
- package/docs/HARNESS_WORKFLOW.md +33 -0
- package/extensions/kingdee-harness.ts +96 -1
- package/extensions/kingdee-header.ts +93 -9
- package/extensions/kingdee-subagents.ts +430 -0
- package/package.json +2 -1
- package/prompts/kd-verify.md +1 -1
- package/skills/kd-verify/SKILL.md +2 -2
- package/src/harness/delegation.ts +297 -0
- package/src/harness/prompt.ts +13 -1
- package/src/harness/repair.ts +224 -0
- package/src/harness/state.ts +15 -0
- package/src/harness/types.ts +10 -0
package/README.md
CHANGED
|
@@ -108,6 +108,8 @@ enterprise 金蝶企业版 / C#
|
|
|
108
108
|
/kd-advance [阶段]
|
|
109
109
|
/kd-artifact [阶段] [内容] [--replace]
|
|
110
110
|
/kd-answer Q-001 <答案>
|
|
111
|
+
/kd-review [审查重点]
|
|
112
|
+
/kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
完整说明见 [命令参考](docs/COMMAND_REFERENCE.md)。
|
|
@@ -129,6 +131,7 @@ kd_sdk_signature 从当前项目实际 SDK jar/dll 中读取类和方法签
|
|
|
129
131
|
kd_ksql_lint 运行 KSQL/SQL lint
|
|
130
132
|
kd_build 按产品画像执行或 dry-run 构建
|
|
131
133
|
kd_debug 分析金蝶日志和堆栈
|
|
134
|
+
kd_subagent 将调研、文档、代码、验证或交叉审查委派给隔离子 agent
|
|
132
135
|
```
|
|
133
136
|
|
|
134
137
|
工具细节和使用顺序见 [Harness 工作流](docs/HARNESS_WORKFLOW.md)。
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,52 @@
|
|
|
6
6
|
|
|
7
7
|
- 暂无。
|
|
8
8
|
|
|
9
|
+
## 0.1.30 - 2026-06-07
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- 新增 `kd_subagent` 工具,用隔离 Pi 子进程执行调研、文档、代码、验证和交叉审查任务。
|
|
14
|
+
- 新增 `/kd-review` 命令,用只读子 agent 执行交叉自查。
|
|
15
|
+
- 新增 `/kd-delegate` 命令,支持按 `research/doc/code/review/verify` 角色委派任务,并可用 `--dry-run` 预览上下文包。
|
|
16
|
+
- `kd_subagent` 支持单任务、只读角色并行 `tasks` 和链式 `chain` 三种模式。
|
|
17
|
+
|
|
18
|
+
### 改进
|
|
19
|
+
|
|
20
|
+
- 子 agent 上下文由 `src/harness/delegation.ts` 集中生成,明确写入边界、主状态机边界和禁止递归委派。
|
|
21
|
+
- 工作流 prompt 增加自动委派策略:复杂调研、交叉审查和可并行拆分任务可主动调用 `kd_subagent`,但不自动推进阶段。
|
|
22
|
+
- 子 agent 进程使用角色环境标记和工具白名单;child 环境不注册 `kd_subagent`,避免递归委派。
|
|
23
|
+
- `research/review/verify` 为只读角色;`doc` 只能写 README、docs/ 和当前 run 阶段文档;`code` 只能在 `execute` 阶段写产品源码并继续受 PLAN/TDD/SDK 门禁约束。
|
|
24
|
+
|
|
25
|
+
### 验证
|
|
26
|
+
|
|
27
|
+
- `npm run smoke:harness` 覆盖委派参数解析、上下文包关键约束和写入边界预览。
|
|
28
|
+
|
|
29
|
+
## 0.1.29 - 2026-06-07
|
|
30
|
+
|
|
31
|
+
### 修复
|
|
32
|
+
|
|
33
|
+
- 修复自动修复循环达到上限后,即使后续验证通过也会被遗留阻断问题继续卡住的问题。
|
|
34
|
+
- 强化 `kd_verify_result` 输入容错,避免坏 payload 触发类似 `undefined.replace/trim` 的崩溃。
|
|
35
|
+
- 限制验证结果只能在 `verify` 阶段或自动修复中的 `execute` 阶段记录,避免非验证阶段污染 `VERIFY.md` 和 evidence。
|
|
36
|
+
- 收敛 `/kd-verify` 与 `kd-verify` skill 的验证结果入口,要求通过 `kd_verify_result` 进入同一修复闭环。
|
|
37
|
+
|
|
38
|
+
### 验证
|
|
39
|
+
|
|
40
|
+
- `npm run smoke:harness` 覆盖 repair 阻断问题关闭、坏验证 payload 容错和非法阶段拒绝。
|
|
41
|
+
|
|
42
|
+
## 0.1.28 - 2026-06-07
|
|
43
|
+
|
|
44
|
+
### 新增
|
|
45
|
+
|
|
46
|
+
- 新增 `kd_verify_result` 工具和 `/kd-verify-result` 命令,用于记录验证命令结果。
|
|
47
|
+
- 验证失败时自动写入 `evidence/verify-failure-###.md`,切回 `execute` 阶段并注入修复上下文。
|
|
48
|
+
- 验证通过时写入 `evidence/verify-pass.md`,重置修复状态并继续尝试推进。
|
|
49
|
+
- 自动修复循环默认最多 3 轮;达到上限后创建阻断问题,避免无限修复。
|
|
50
|
+
|
|
51
|
+
### 验证
|
|
52
|
+
|
|
53
|
+
- `npm run smoke:harness` 通过,覆盖验证失败回到 execute、三轮失败阻塞、验证通过重置修复状态。
|
|
54
|
+
|
|
9
55
|
## 0.1.27 - 2026-06-07
|
|
10
56
|
|
|
11
57
|
### 修复
|
|
@@ -255,6 +255,37 @@ discuss -> spec -> plan -> execute -> verify -> ship
|
|
|
255
255
|
/kd-finish
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
+
### /kd-review
|
|
259
|
+
|
|
260
|
+
启动只读交叉自查子 agent:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
/kd-review [审查重点]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
用于检查状态机漏洞、门禁绕过、证据缺口、提示词分散和测试缺口。子 agent 不修改文件,主 agent 负责采纳结论和后续修复。
|
|
267
|
+
|
|
268
|
+
### /kd-delegate
|
|
269
|
+
|
|
270
|
+
把局部任务委派给隔离子 agent:
|
|
271
|
+
|
|
272
|
+
```text
|
|
273
|
+
/kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
角色:
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
research 只读调研,输出压缩结论和证据位置
|
|
280
|
+
doc 写指定文档或阶段产物
|
|
281
|
+
code 只在 execute 阶段修改 PLAN.md 批准文件
|
|
282
|
+
review 只读交叉自查,输出 findings 和是否阻止发布
|
|
283
|
+
verify 只读分析验证命令和失败证据,实际验证由主 agent 执行并用 kd_verify_result 记录
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`--dry-run` 只预览上下文包,不启动子进程。
|
|
287
|
+
复杂任务也可能由主 agent 自动调用 `kd_subagent` 委派;自动委派不会改变 Harness 阶段。
|
|
288
|
+
|
|
258
289
|
## 内置工具
|
|
259
290
|
|
|
260
291
|
这些工具多数情况下会由 KCode 自动使用;当需要明确证据或排障时,也可以按下面参数手动调用。
|
|
@@ -279,6 +310,30 @@ kd_question action=list
|
|
|
279
310
|
|
|
280
311
|
一次只能登记一个当前最阻塞的问题,最多 3 个简短选项。
|
|
281
312
|
|
|
313
|
+
### kd_subagent
|
|
314
|
+
|
|
315
|
+
将局部任务委派给隔离 Pi 子进程:
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
kd_subagent role=review task="审查当前 run 的门禁和证据缺口"
|
|
319
|
+
kd_subagent role=research task="查找采购订单保存插件相关代码" dryRun=true
|
|
320
|
+
kd_subagent tasks=[{"role":"research","task":"查找模型层"},{"role":"review","task":"审查状态机"}]
|
|
321
|
+
kd_subagent chain=[{"role":"research","task":"找相关代码"},{"role":"review","task":"基于上一输出审查风险"}]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
参数:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
role 必填,research/doc/code/review/verify
|
|
328
|
+
task 必填,具体委派任务
|
|
329
|
+
tasks 可选,并行任务数组,只允许 research/review/verify,和 role/task/chain 三选一
|
|
330
|
+
chain 可选,链式任务数组,和 role/task/tasks 三选一
|
|
331
|
+
dryRun 可选,只预览上下文包
|
|
332
|
+
maxOutputChars 可选,限制返回给主 agent 的输出长度
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
主 Harness 仍负责阶段推进、证据和门禁;子 agent 返回结果后由主 agent 决策下一步。
|
|
336
|
+
|
|
282
337
|
### kd_search
|
|
283
338
|
|
|
284
339
|
搜索随包金蝶知识库:
|
package/docs/HARNESS_WORKFLOW.md
CHANGED
|
@@ -145,3 +145,36 @@ KCode 会阻止过早写入 Java/XML/SQL/C# 等产品代码:
|
|
|
145
145
|
- 必须先理解当前业务项目已有目录、模块、包名、基类和本地封装。
|
|
146
146
|
|
|
147
147
|
证据和门禁细节见 [证据和门禁](EVIDENCE_AND_GATES.md)。
|
|
148
|
+
|
|
149
|
+
## 子 agent 委派
|
|
150
|
+
|
|
151
|
+
KCode 支持把局部任务委派给隔离子 agent,用来降低长上下文带来的注意力漂移。主 Harness 仍是唯一状态机,负责阶段推进、门禁、证据和风险记录。
|
|
152
|
+
|
|
153
|
+
触发方式有两种:
|
|
154
|
+
|
|
155
|
+
- 自动:主 agent 在大量调研、独立交叉审查、长上下文复盘或可并行拆分时,可以主动调用 `kd_subagent`。
|
|
156
|
+
- 显式:用户用 `/kd-review` 或 `/kd-delegate` 指定委派任务。
|
|
157
|
+
|
|
158
|
+
常用入口:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
/kd-review 审查当前实现是否有门禁绕过和测试缺口
|
|
162
|
+
/kd-delegate research 调研采购订单保存插件相关代码
|
|
163
|
+
/kd-delegate doc 更新当前阶段文档 --dry-run
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
角色边界:
|
|
167
|
+
|
|
168
|
+
- `research`、`review` 默认只读。
|
|
169
|
+
- `doc` 只写明确指定的文档或阶段产物。
|
|
170
|
+
- `code` 只能在 `execute` 阶段运行,并且只能修改 `PLAN.md` 批准文件。
|
|
171
|
+
- `verify` 只读分析验证命令、失败证据和风险;实际命令和结果记录仍由主 agent 执行。
|
|
172
|
+
|
|
173
|
+
`--dry-run` 会预览发送给子 agent 的上下文包,用来检查上下文是否过长、是否包含不该交给子 agent 的信息。
|
|
174
|
+
|
|
175
|
+
工具层支持并行和链式委派。并行只允许 `research`、`review`、`verify` 这类只读角色;`doc` 和 `code` 必须串行执行:
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
kd_subagent tasks=[{"role":"research","task":"查找模型层"},{"role":"review","task":"审查门禁"}]
|
|
179
|
+
kd_subagent chain=[{"role":"research","task":"找相关代码"},{"role":"review","task":"基于上一输出审查风险"}]
|
|
180
|
+
```
|
|
@@ -24,7 +24,9 @@ import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from
|
|
|
24
24
|
import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
|
|
25
25
|
import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
|
|
26
26
|
import { windowsPathHint } from "../src/platform/path.ts";
|
|
27
|
-
import { workflowPromptForRun } from "../src/harness/prompt.ts";
|
|
27
|
+
import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts";
|
|
28
|
+
import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts";
|
|
29
|
+
import { isSubagentChild, subagentRoleFromEnv, subagentToolCallBlockReason } from "../src/harness/delegation.ts";
|
|
28
30
|
|
|
29
31
|
function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
|
|
30
32
|
return readActiveRun(cwd);
|
|
@@ -275,9 +277,47 @@ const kdQuestionTool = defineTool({
|
|
|
275
277
|
},
|
|
276
278
|
});
|
|
277
279
|
|
|
280
|
+
function createKdVerifyResultTool(pi: ExtensionAPI) {
|
|
281
|
+
return defineTool({
|
|
282
|
+
name: "kd_verify_result",
|
|
283
|
+
label: "KD 验证结果",
|
|
284
|
+
description: "记录当前 verify 命令结果。失败时自动写失败证据并回到 execute 修复;成功时记录通过证据并尝试推进。",
|
|
285
|
+
parameters: Type.Object({
|
|
286
|
+
command: Type.String({ description: "实际执行的验证命令。" }),
|
|
287
|
+
exitCode: Type.Number({ description: "验证命令退出码。" }),
|
|
288
|
+
stdout: Type.Optional(Type.String({ description: "验证命令 STDOUT 摘要或完整输出。" })),
|
|
289
|
+
stderr: Type.Optional(Type.String({ description: "验证命令 STDERR 摘要或完整输出。" })),
|
|
290
|
+
summary: Type.Optional(Type.String({ description: "失败原因或通过结论摘要。" })),
|
|
291
|
+
}),
|
|
292
|
+
|
|
293
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
294
|
+
const run = readActiveRun(ctx.cwd);
|
|
295
|
+
if (!run) {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
|
|
298
|
+
details: { error: "no-active-run" },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const outcome = recordVerifyResult(ctx.cwd, run, {
|
|
302
|
+
command: params.command,
|
|
303
|
+
exitCode: params.exitCode,
|
|
304
|
+
stdout: params.stdout,
|
|
305
|
+
stderr: params.stderr,
|
|
306
|
+
summary: params.summary,
|
|
307
|
+
});
|
|
308
|
+
handleVerifyOutcome(pi, ctx, outcome);
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text: outcome.message }],
|
|
311
|
+
details: { outcome },
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
278
317
|
export default function (pi: ExtensionAPI) {
|
|
279
318
|
pi.registerTool(kdPlanStatusTool);
|
|
280
319
|
pi.registerTool(kdQuestionTool);
|
|
320
|
+
pi.registerTool(createKdVerifyResultTool(pi));
|
|
281
321
|
|
|
282
322
|
pi.on("session_start", async (_event, ctx) => {
|
|
283
323
|
const run = readActiveRun(ctx.cwd);
|
|
@@ -294,6 +334,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
294
334
|
|
|
295
335
|
pi.on("input", async (event, ctx) => {
|
|
296
336
|
if (event.source === "extension") return { action: "continue" };
|
|
337
|
+
if (isSubagentChild()) return { action: "continue" };
|
|
297
338
|
|
|
298
339
|
let run = readActiveRun(ctx.cwd);
|
|
299
340
|
if (!run && shouldStartHarnessFromInput(event.text)) {
|
|
@@ -324,6 +365,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
324
365
|
return { block: true, reason };
|
|
325
366
|
}
|
|
326
367
|
|
|
368
|
+
const subagentRole = isSubagentChild() ? subagentRoleFromEnv() : undefined;
|
|
369
|
+
if (subagentRole) {
|
|
370
|
+
const run = readActiveRun(ctx.cwd);
|
|
371
|
+
const sourceWriteBlock =
|
|
372
|
+
sdkSignatureProductionWriteBlockReason(ctx.cwd, run, path) ??
|
|
373
|
+
tddProductionWriteBlockReason(ctx.cwd, run, path) ??
|
|
374
|
+
planWriteBlockReason(ctx.cwd, run, path, run ? (readArtifact(ctx.cwd, run, "plan") ?? "") : "") ??
|
|
375
|
+
flagshipWriteBlockReason(run, path, ctx.cwd);
|
|
376
|
+
const reason = subagentToolCallBlockReason({
|
|
377
|
+
role: subagentRole,
|
|
378
|
+
toolName: event.toolName,
|
|
379
|
+
path,
|
|
380
|
+
cwd: ctx.cwd,
|
|
381
|
+
run,
|
|
382
|
+
sourceLike: path ? isSourceLikePath(path) : false,
|
|
383
|
+
sourceWriteBlockReason: sourceWriteBlock,
|
|
384
|
+
});
|
|
385
|
+
if (reason) {
|
|
386
|
+
if (ctx.hasUI) ctx.ui.notify(reason, "warning");
|
|
387
|
+
return { block: true, reason };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
327
391
|
if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
|
|
328
392
|
|
|
329
393
|
const reason = codeWriteBlockReason(ctx.cwd, path) ?? flagshipWriteBlockReason(readActiveRun(ctx.cwd), path, ctx.cwd);
|
|
@@ -551,6 +615,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
551
615
|
autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`);
|
|
552
616
|
},
|
|
553
617
|
});
|
|
618
|
+
|
|
619
|
+
pi.registerCommand("kd-verify-result", {
|
|
620
|
+
description: "记录验证命令结果:/kd-verify-result <exitCode> <command>",
|
|
621
|
+
handler: async (args, ctx) => {
|
|
622
|
+
const run = requireRun(ctx.cwd);
|
|
623
|
+
if (!run) {
|
|
624
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const [exitCodeText, ...commandParts] = args.trim().split(/\s+/);
|
|
628
|
+
const exitCode = Number(exitCodeText);
|
|
629
|
+
const command = commandParts.join(" ").trim();
|
|
630
|
+
if (!Number.isFinite(exitCode) || !command) {
|
|
631
|
+
ctx.ui.notify("用法:/kd-verify-result <exitCode> <command>", "error");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const outcome = recordVerifyResult(ctx.cwd, run, { command, exitCode });
|
|
635
|
+
ctx.ui.notify(outcome.message, outcome.status === "passed" ? "info" : "warning");
|
|
636
|
+
handleVerifyOutcome(pi, ctx, outcome);
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function handleVerifyOutcome(pi: ExtensionAPI, ctx: ExtensionContext, outcome: VerifyResultOutcome): void {
|
|
642
|
+
if (outcome.status === "passed") {
|
|
643
|
+
autoAdvanceCommand(pi, ctx, outcome.run, "验证结果已通过。");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (outcome.status === "repairing") {
|
|
647
|
+
sendWorkflowPrompt(pi, ctx, outcome.run, repairPromptForRun(outcome.run));
|
|
648
|
+
}
|
|
554
649
|
}
|
|
555
650
|
|
|
556
651
|
function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
|
|
@@ -4,6 +4,96 @@ import { readActiveRun } from "../src/harness/state.ts";
|
|
|
4
4
|
import type { ActiveRun, GateResult } from "../src/harness/types.ts";
|
|
5
5
|
import { formatProductProfile } from "../src/product/profile.ts";
|
|
6
6
|
|
|
7
|
+
/** ANSI escape sequence pattern: CSI, OSC, APC. */
|
|
8
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b_[^\x1b]*\x1b\\/g;
|
|
9
|
+
|
|
10
|
+
/** Visible width of a string (strips ANSI codes; CJK/wide chars = 2 columns). */
|
|
11
|
+
function visibleWidth(str: string): number {
|
|
12
|
+
let width = 0;
|
|
13
|
+
const clean = str.replace(ANSI_RE, "");
|
|
14
|
+
for (const ch of clean) {
|
|
15
|
+
const code = ch.codePointAt(0)!;
|
|
16
|
+
width +=
|
|
17
|
+
code >= 0x1100 &&
|
|
18
|
+
!(code >= 0x00a0 && code <= 0x00ff) &&
|
|
19
|
+
((code >= 0x1100 && code <= 0x115f) ||
|
|
20
|
+
(code >= 0x2329 && code <= 0x232a) ||
|
|
21
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
22
|
+
(code >= 0x3040 && code <= 0x3247) ||
|
|
23
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
24
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
25
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
26
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
27
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
28
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
29
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
30
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
31
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
32
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
33
|
+
(code >= 0x20000 && code <= 0x2fffd))
|
|
34
|
+
? 2
|
|
35
|
+
: 1;
|
|
36
|
+
}
|
|
37
|
+
return width;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* If the line's visible width exceeds `maxWidth`, truncate visible characters
|
|
42
|
+
* and append `>` so the result fits. Preserves ANSI codes in the kept portion
|
|
43
|
+
* and appends SGR reset before the `>`. No padding — pi-tui only requires
|
|
44
|
+
* visibleWidth <= width.
|
|
45
|
+
*/
|
|
46
|
+
function clipLine(text: string, maxWidth: number): string {
|
|
47
|
+
if (maxWidth <= 0) return "";
|
|
48
|
+
const vw = visibleWidth(text);
|
|
49
|
+
if (vw <= maxWidth) return text;
|
|
50
|
+
|
|
51
|
+
const targetW = maxWidth - 1; // reserve 1 col for ">"
|
|
52
|
+
let result = "";
|
|
53
|
+
let visibleSoFar = 0;
|
|
54
|
+
let i = 0;
|
|
55
|
+
|
|
56
|
+
while (i < text.length && visibleSoFar < targetW) {
|
|
57
|
+
// Preserve ANSI escape sequences
|
|
58
|
+
if (text[i] === "\x1b") {
|
|
59
|
+
const m = text.slice(i).match(/^(?:\[[0-9;]*[A-Za-z]|\][^\x07]*\x07|_[^\x1b]*\x1b\\)/);
|
|
60
|
+
if (m) {
|
|
61
|
+
result += m[0];
|
|
62
|
+
i += m[0].length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ch = text[i];
|
|
68
|
+
const code = ch.codePointAt(0)!;
|
|
69
|
+
const wide =
|
|
70
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
71
|
+
(code >= 0x2329 && code <= 0x232a) ||
|
|
72
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
73
|
+
(code >= 0x3040 && code <= 0x3247) ||
|
|
74
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
75
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
76
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
77
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
78
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
79
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
80
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
81
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
82
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
83
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
84
|
+
(code >= 0x20000 && code <= 0x2fffd);
|
|
85
|
+
const charW = wide ? 2 : 1;
|
|
86
|
+
|
|
87
|
+
if (visibleSoFar + charW > targetW) break;
|
|
88
|
+
|
|
89
|
+
result += ch;
|
|
90
|
+
visibleSoFar += charW;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result + "\x1b[0m>";
|
|
95
|
+
}
|
|
96
|
+
|
|
7
97
|
function formatProduct(run: ActiveRun | undefined): string {
|
|
8
98
|
if (!run) return "未选择";
|
|
9
99
|
if (run.profile?.product === "unknown") return "未确认";
|
|
@@ -43,12 +133,6 @@ function riskColor(risk: string): "error" | "warning" | "muted" | "success" {
|
|
|
43
133
|
return "success";
|
|
44
134
|
}
|
|
45
135
|
|
|
46
|
-
function padOrTrim(text: string, width: number): string {
|
|
47
|
-
if (width <= 0) return "";
|
|
48
|
-
if (text.length > width) return text.slice(0, Math.max(0, width - 1)) + ">";
|
|
49
|
-
return text + " ".repeat(width - text.length);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
136
|
function logoLines(theme: Theme): string[] {
|
|
53
137
|
const accent = (text: string) => theme.fg("accent", text);
|
|
54
138
|
const muted = (text: string) => theme.fg("muted", text);
|
|
@@ -87,9 +171,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
87
171
|
|
|
88
172
|
return [
|
|
89
173
|
"",
|
|
90
|
-
...logoLines(theme).map((line) =>
|
|
91
|
-
|
|
92
|
-
|
|
174
|
+
...logoLines(theme).map((line) => clipLine(line, width)),
|
|
175
|
+
clipLine(status, width),
|
|
176
|
+
clipLine(theme.fg("dim", `run:${runId}`), width),
|
|
93
177
|
"",
|
|
94
178
|
];
|
|
95
179
|
},
|