@yinan_chen/pi-claude-code-ui 0.3.0 → 0.6.0
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 +6 -0
- package/package.json +3 -2
- package/src/claude-tools.ts +10 -4
- package/src/claude-view.ts +8 -8
- package/src/foreign-normalizer.ts +78 -0
- package/src/index.ts +33 -3
- package/src/subagent-panel.ts +53 -10
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
|
21
21
|
- **Claude 式视图** — 对话转录适配器,Pi 原生回退
|
|
22
22
|
- **吸附消息泡(sticky)** — 滚动时顶部吸附显示当前正在回答的用户消息原文,点击回位(v0.2.0 内置,原 sticky-first-message 扩展)
|
|
23
23
|
- **Skill 折叠(recollapse)** — 展开的 skill 全文自动折回 `/skill:名 参数`,覆盖 /tree、/fork 回填等四处盲区(v0.2.0 内置,原 skill-block-recollapse 扩展)
|
|
24
|
+
- **第三方渲染规范化** — 任何扩展的卡片(如 pi-subagents 的 workflow 卡)行首 `⎿` 自动统一为 `resultPrefix`;subagent / workflow 卡片整体接管为 Claude 风格,分发层拦截不受加载顺序影响(v0.6.0)
|
|
24
25
|
- **富 diff 渲染** — edit/write 工具的语法高亮 diff(vendored, MIT)
|
|
25
26
|
- **子代理面板** — 后台 agent 状态一目了然
|
|
26
27
|
- **工作状态提示** — working message / running effect
|
|
@@ -45,6 +46,8 @@ pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
|
45
46
|
"stickyIncludeCustom": false,
|
|
46
47
|
"skillResumePreview": true,
|
|
47
48
|
"resultPrefix": "└",
|
|
49
|
+
"resultPrefixGap": 1,
|
|
50
|
+
"normalizeForeignRenderers": true,
|
|
48
51
|
"diff": {
|
|
49
52
|
"diffViewMode": "auto",
|
|
50
53
|
"diffIndicatorMode": "bars",
|
|
@@ -70,6 +73,9 @@ pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
|
70
73
|
| `stickyIncludeCustom` | `false` | 吸附泡是否包含扩展注入/跨会话转发的旁路消息 |
|
|
71
74
|
| `skillResumePreview` | `true` | `/resume` 列表预览是否折回 `[skill] 名` 单行 |
|
|
72
75
|
| `resultPrefix` | `"└"` | 工具结果行拐角前缀(制表符族,等宽字体最稳;可换 `↳` 或 Claude Code 官方的 `⎿`,后两者在部分字体下会变宽) |
|
|
76
|
+
| `resultPrefixGap` | `1` | 前缀符号与内容之间的空格数(0–4) |
|
|
77
|
+
| `normalizeForeignRenderers` | `true` | 第三方扩展卡片输出里行首 `⎿` 自动规范化为 `resultPrefix`(任何未适配扩展的渲染都能统一风格;关闭可看第三方原版) |
|
|
78
|
+
| `workflowRendering` | `"claude"` | workflow 完成卡片样式:`● Workflow(name)` 风格 / `native` 保留包内卡片。分发层拦截实现,不受包加载顺序影响 |
|
|
73
79
|
| `diff.diffViewMode` | `"auto"` | diff 视图:`auto`/`split`/`unified` |
|
|
74
80
|
| `diff.diffIndicatorMode` | `"bars"` | 变更指示条:`bars`/`classic`/`none` |
|
|
75
81
|
| `diff.diffSplitMinWidth` | `120` | 宽于此才用 split 视图 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yinan_chen/pi-claude-code-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Claude Code-style UI for Pi: transcript view, rich diffs, subagent panels and working-message status.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"extensions": [
|
|
27
27
|
"./src/index.ts"
|
|
28
28
|
],
|
|
29
|
-
"image": "https://github.com/chenyn273/pi-claude-code-ui/raw/main/assets/preview.png"
|
|
29
|
+
"image": "https://github.com/chenyn273/pi-claude-code-ui/raw/main/assets/preview.png",
|
|
30
|
+
"video": "https://github.com/chenyn273/pi-claude-code-ui/raw/main/assets/demo.mp4"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/claude-tools.ts
CHANGED
|
@@ -4,7 +4,13 @@ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
/** UI 符号集(lean-tidy.json 可覆盖;见 index.ts loadConfig 的 resultPrefix)。 */
|
|
7
|
-
export const SYMBOLS = { resultPrefix: "└" };
|
|
7
|
+
export const SYMBOLS = { resultPrefix: "└", resultPrefixGap: 1 };
|
|
8
|
+
|
|
9
|
+
/** 结果行缩进对:first = 3 空格 + 符号 + gap 空格;cont = 4 + gap 空格。运行时读 SYMBOLS。 */
|
|
10
|
+
export function resultIndent(theme: any): { first: string; cont: string } {
|
|
11
|
+
return { first: ` ${fg(theme, "dim", SYMBOLS.resultPrefix)}${" ".repeat(SYMBOLS.resultPrefixGap)}`, cont: RESULT_CONT() };
|
|
12
|
+
}
|
|
13
|
+
export const RESULT_CONT = (): string => " ".repeat(4 + SYMBOLS.resultPrefixGap);
|
|
8
14
|
|
|
9
15
|
export interface ToolView {
|
|
10
16
|
name: string;
|
|
@@ -197,13 +203,13 @@ export function summaryOf(v: ToolView, theme: any): string | undefined {
|
|
|
197
203
|
export function paintTool(v: ToolView, theme: any, width: number, config: ToolAppearance): PaintedTool {
|
|
198
204
|
const lines = headerOf(v, theme, width), headerHeight = lines.length;
|
|
199
205
|
let moreRow: number | undefined;
|
|
200
|
-
const result = (s: string) => lines.push(...prefixed(s, width,
|
|
206
|
+
const result = (s: string) => lines.push(...prefixed(s, width, resultIndent(theme).first, RESULT_CONT()));
|
|
201
207
|
const body = (source: string[], limit: number, full = v.expanded, firstResult = false) => {
|
|
202
208
|
const shown = full ? source : source.slice(0, limit);
|
|
203
209
|
let clipped = false;
|
|
204
210
|
shown.forEach((line, i) => {
|
|
205
|
-
const first = firstResult && i === 0 ?
|
|
206
|
-
if (full) lines.push(...prefixed(line, width, first,
|
|
211
|
+
const first = firstResult && i === 0 ? resultIndent(theme).first : RESULT_CONT();
|
|
212
|
+
if (full) lines.push(...prefixed(line, width, first, RESULT_CONT()));
|
|
207
213
|
else {
|
|
208
214
|
const pw = Math.min(6, Math.max(0, width - 1)), bw = Math.max(1, width - pw);
|
|
209
215
|
clipped ||= visibleWidth(line) > bw;
|
package/src/claude-view.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Pi-specific transcript adapter. All private component fields are isolated here. */
|
|
2
2
|
import { Box, Container, Markdown, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { BUILTIN_NAMES, PROCESS_TOOL_NAMES, bold, canExpandTool, expandHint, failedTool, fg, fit, headerOf, isBlank, isImageLine, oneLine, paintBackground, paintTool, prefixed, prefixZones,
|
|
4
|
+
import { BUILTIN_NAMES, PROCESS_TOOL_NAMES, bold, canExpandTool, expandHint, failedTool, fg, fit, headerOf, isBlank, isImageLine, oneLine, paintBackground, paintTool, prefixed, prefixZones, resultIndent, RESULT_CONT, targetOf, type ToolAppearance, type ToolView } from "./claude-tools.ts";
|
|
5
5
|
import { animateRunningSummary } from "./running-effect.ts";
|
|
6
6
|
|
|
7
7
|
export interface ClaudeConfig extends ToolAppearance {
|
|
@@ -313,7 +313,7 @@ export function createClaudeView(
|
|
|
313
313
|
if (raw.length > rowsLeft) { hidden += raw.length - rowsLeft; raw = raw.slice(0, Math.max(0, rowsLeft)); }
|
|
314
314
|
rowsLeft -= raw.length;
|
|
315
315
|
const start = lines.length;
|
|
316
|
-
for (let i = 0; i < raw.length; i++) lines.push(isImageLine(raw[i]) ? raw[i] : fit(truncateToWidth(start + i === 0 ?
|
|
316
|
+
for (let i = 0; i < raw.length; i++) lines.push(isImageLine(raw[i]) ? raw[i] : fit(truncateToWidth(start + i === 0 ? resultIndent(theme()).first : RESULT_CONT(), indent, "") + raw[i], width));
|
|
317
317
|
const proxy = {
|
|
318
318
|
handleMouse(e: any) {
|
|
319
319
|
if (e.x < indent) return undefined;
|
|
@@ -346,7 +346,7 @@ export function createClaudeView(
|
|
|
346
346
|
const last = entries.at(-1);
|
|
347
347
|
if (last && --last.height <= 0) entries.pop();
|
|
348
348
|
}
|
|
349
|
-
const more = prefixed(fg(theme(), "dim", `… +${hidden} lines`) + expandHint(theme()), width,
|
|
349
|
+
const more = prefixed(fg(theme(), "dim", `… +${hidden} lines`) + expandHint(theme()), width, resultIndent(theme()).first, RESULT_CONT());
|
|
350
350
|
lines.push(...more);
|
|
351
351
|
entries.push({ component: toggleTool(self), height: more.length });
|
|
352
352
|
}
|
|
@@ -368,7 +368,7 @@ export function createClaudeView(
|
|
|
368
368
|
lines = [...head, ...(body?.lines ?? [])];
|
|
369
369
|
entries = [{ component: toggleTool(self), height: head.length }, ...(body?.entries ?? [])];
|
|
370
370
|
if (v.partial && !body?.lines.length) {
|
|
371
|
-
const pending = prefixed(fg(t, "dim", v.started ? "Running…" : "Waiting…"), width,
|
|
371
|
+
const pending = prefixed(fg(t, "dim", v.started ? "Running…" : "Waiting…"), width, resultIndent(t).first, RESULT_CONT());
|
|
372
372
|
lines.push(...pending); entries.push({ component: {}, height: pending.length });
|
|
373
373
|
}
|
|
374
374
|
} else {
|
|
@@ -390,12 +390,12 @@ export function createClaudeView(
|
|
|
390
390
|
// The vendored renderer limits body rows and supplies its own remainder hint.
|
|
391
391
|
// Do not also slice by codePreviewLines: that counts the diff header/frame
|
|
392
392
|
// against the body and hides almost every changed row.
|
|
393
|
-
const prefix =
|
|
393
|
+
const prefix = resultIndent(t).first;
|
|
394
394
|
// The rich edit header already reports +N/-M; do not repeat
|
|
395
395
|
// paintTool's "Added N lines, removed M lines" summary above it.
|
|
396
396
|
const introRows = painted.headerHeight + (v.name === "edit" ? 0 : 1);
|
|
397
397
|
lines = [...painted.lines.slice(0, introRows),
|
|
398
|
-
...detail.map((line, i) => fit((i === 0 ? prefix :
|
|
398
|
+
...detail.map((line, i) => fit((i === 0 ? prefix : RESULT_CONT()) + line, width))];
|
|
399
399
|
} catch {
|
|
400
400
|
// Keep the existing edit/write renderer as a fail-open fallback.
|
|
401
401
|
}
|
|
@@ -610,7 +610,7 @@ export function createClaudeView(
|
|
|
610
610
|
if (active && tools.length) {
|
|
611
611
|
const focus = [...tools].reverse().find((p) => p.owner.isPartial) ?? tools.at(-1)!;
|
|
612
612
|
const v = toolView(focus.owner), target = targetOf(v.name, v.args, v.cwd).text;
|
|
613
|
-
if (target) rows.push(...prefixed(fg(theme(), "muted", (v.name === "bash" || v.name === "powershell" ? "$ " : "") + target), width,
|
|
613
|
+
if (target) rows.push(...prefixed(fg(theme(), "muted", (v.name === "bash" || v.name === "powershell" ? "$ " : "") + target), width, resultIndent(theme()).first, RESULT_CONT()).slice(0, 2));
|
|
614
614
|
}
|
|
615
615
|
append(rows, toggle);
|
|
616
616
|
// Failures stay folded like the rest: the summary counts them ("N failed")
|
|
@@ -700,7 +700,7 @@ export function createClaudeView(
|
|
|
700
700
|
return trimmed;
|
|
701
701
|
}
|
|
702
702
|
const name = oneLine(self.skillBlock.name), lines = [` ${fg(theme(), "success", "●")} ${bold(theme(), "Skill")}(${name})`];
|
|
703
|
-
lines.push(...prefixed(fg(theme(), "muted", "Loaded skill") + (self.expanded ? "" : ` (${keyText("app.tools.expand")} to expand)`), width,
|
|
703
|
+
lines.push(...prefixed(fg(theme(), "muted", "Loaded skill") + (self.expanded ? "" : ` (${keyText("app.tools.expand")} to expand)`), width, resultIndent(theme()).first, RESULT_CONT()));
|
|
704
704
|
if (self.expanded) {
|
|
705
705
|
const md = new Markdown(self.skillBlock.content ?? "", 0, 0, self.markdownTheme ?? getMarkdownTheme());
|
|
706
706
|
for (const l of md.render(Math.max(1, width - 3))) lines.push(fit(" " + l, width));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foreign-normalizer — 第三方渲染输出的符号规范化层(v0.5.0)。
|
|
3
|
+
*
|
|
4
|
+
* 动机:任何扩展都能 registerMessageRenderer / registerEntryRenderer 输出
|
|
5
|
+
* 自带风格的卡片(如 @tintinweb/pi-subagents 的 workflow 卡片硬编码 ⎿)。
|
|
6
|
+
* lean-tidy 无法逐个接管——类型无限多,且"先加载者胜"的分发顺序不受本包
|
|
7
|
+
* 控制(依赖包加载序)。一劳永逸的做法:在 pi 的两个渲染宿主组件
|
|
8
|
+
* (CustomMessageComponent = custom 消息宿主;CustomEntryComponent =
|
|
9
|
+
* custom 会话条目宿主,如 workflow 卡片)的 render 输出上做行级后处理:
|
|
10
|
+
*
|
|
11
|
+
* 行首(仅空白/ANSI 转义之后)的 ⎿ 等宽替换为 SYMBOLS.resultPrefix。
|
|
12
|
+
*
|
|
13
|
+
* 性质:
|
|
14
|
+
* - 宽度守恒(⎿ 与 └ 均 1 列),布局与测量零影响;正文里的 ⎿ 不动。
|
|
15
|
+
* - 幂等:lean-tidy 自身渲染已是 resultPrefix,不匹配 ⎿ 模式,无叠加。
|
|
16
|
+
* - trampoline:wrapper 每次调用读 globalThis.__leanTidyImpl.normalizeForeign
|
|
17
|
+
* (由 index.ts 组装,闭包捕获当次 loadConfig 的开关与 SYMBOLS),
|
|
18
|
+
* /reload 后新配置立即生效,无需摘除原型补丁。
|
|
19
|
+
* - 关闭(normalizeForeignRenderers=false)时 wrapper 空转透传。
|
|
20
|
+
*/
|
|
21
|
+
import { SYMBOLS } from "./claude-tools.ts";
|
|
22
|
+
|
|
23
|
+
const FLAG = Symbol.for("lean-tidy.foreign-normalizer");
|
|
24
|
+
/** 行首到 ⎿ 之间只允许空白与 ANSI SGR 转义序列。 */
|
|
25
|
+
const LEADING = /^((?:\s|\x1b\[[0-9;]*m)*)⎿/;
|
|
26
|
+
|
|
27
|
+
/** 行级符号规范化:仅替换行首 ⎿(等宽),其余原样;无变化时返回原数组。 */
|
|
28
|
+
export function normalizeForeignLines(lines: readonly string[]): string[] {
|
|
29
|
+
const sym = SYMBOLS.resultPrefix;
|
|
30
|
+
let changed = false;
|
|
31
|
+
const out = lines.map((line) => {
|
|
32
|
+
if (typeof line !== "string" || !line.includes("⎿")) return line;
|
|
33
|
+
const replaced = line.replace(LEADING, `$1${sym}`);
|
|
34
|
+
if (replaced !== line) changed = true;
|
|
35
|
+
return replaced;
|
|
36
|
+
});
|
|
37
|
+
return changed ? out : (lines as string[]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* patch 渲染分发器(ExtensionRunner:同时有 getMessageRenderer + getEntryRenderer 原型方法)。
|
|
42
|
+
* 分发层拦截:不受包加载顺序影响,__leanTidyImpl.rendererOverride(customType) 命中即改道。
|
|
43
|
+
*/
|
|
44
|
+
export function patchRendererDispatch(cls: any): boolean {
|
|
45
|
+
const proto = cls?.prototype;
|
|
46
|
+
if (!proto || typeof proto.getMessageRenderer !== "function" || typeof proto.getEntryRenderer !== "function" || proto[FLAG]) return false;
|
|
47
|
+
proto[FLAG] = true;
|
|
48
|
+
for (const method of ["getMessageRenderer", "getEntryRenderer"]) {
|
|
49
|
+
const orig = proto[method];
|
|
50
|
+
proto[method] = function (customType: string) {
|
|
51
|
+
const impl = (globalThis as any).__leanTidyImpl;
|
|
52
|
+
try {
|
|
53
|
+
const override = impl?.rendererOverride?.(customType);
|
|
54
|
+
if (override) return override;
|
|
55
|
+
} catch {}
|
|
56
|
+
return orig.call(this, customType);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** patch 渲染宿主组件类的 prototype.render(幂等,FLAG 防重复包裹);wrapper 全部经 __leanTidyImpl trampoline 转发。 */
|
|
63
|
+
export function patchForeignHost(cls: any): boolean {
|
|
64
|
+
const proto = cls?.prototype;
|
|
65
|
+
if (!proto || typeof proto.render !== "function" || proto[FLAG]) return false;
|
|
66
|
+
const orig = proto.render;
|
|
67
|
+
proto[FLAG] = true;
|
|
68
|
+
proto.render = function (width: number) {
|
|
69
|
+
const lines = orig.call(this, width);
|
|
70
|
+
const impl = (globalThis as any).__leanTidyImpl;
|
|
71
|
+
try {
|
|
72
|
+
return typeof impl?.normalizeForeign === "function" ? impl.normalizeForeign(lines) : lines;
|
|
73
|
+
} catch {
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return true;
|
|
78
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,10 +26,11 @@ import { homedir } from "node:os";
|
|
|
26
26
|
import { Container } from "@earendil-works/pi-tui";
|
|
27
27
|
import { getPiRuntime, VERIFIED_PI_VERSION } from "./pi-runtime.ts";
|
|
28
28
|
import { createClaudeView } from "./claude-view.ts";
|
|
29
|
-
import registerSubagentPanels from "./subagent-panel.ts";
|
|
29
|
+
import registerSubagentPanels, { createSubagentRenderer, createWorkflowRenderer, registerWorkflowPanels } from "./subagent-panel.ts";
|
|
30
30
|
import stickyFirstMessage from "./sticky-first-message.ts";
|
|
31
31
|
import registerSkillRecollapse from "./skill-block-recollapse.ts";
|
|
32
32
|
import { SYMBOLS } from "./claude-tools.ts";
|
|
33
|
+
import { normalizeForeignLines, patchForeignHost, patchRendererDispatch } from "./foreign-normalizer.ts";
|
|
33
34
|
import registerWorkingMessage from "./working-message.ts";
|
|
34
35
|
// Vendored rich diff (MIT; see vendor/diff/ATTRIBUTION.md).
|
|
35
36
|
import { renderRichToolResult, WriteExecutionMetadataStore } from "./vendor/diff/index.ts";
|
|
@@ -83,6 +84,12 @@ interface Config {
|
|
|
83
84
|
skillResumePreview: boolean;
|
|
84
85
|
/** 工具结果行的拐角前缀符号(默认 └;可换 ↳ 或 Claude Code 官方的 ⎿)。 */
|
|
85
86
|
resultPrefix: string;
|
|
87
|
+
/** 前缀符号与内容之间的空格数(0–4,默认 0)。 */
|
|
88
|
+
resultPrefixGap: number;
|
|
89
|
+
/** 第三方扩展渲染输出的行首 ⎿ 是否规范化为 resultPrefix(默认 true)。 */
|
|
90
|
+
normalizeForeignRenderers: boolean;
|
|
91
|
+
/** workflow 完成卡片(subagents:workflow entry)样式:claude = ● Workflow(name) 风格(默认);native = 保留包内卡片。 */
|
|
92
|
+
workflowRendering: "claude" | "native";
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
function loadConfig(): Config {
|
|
@@ -100,6 +107,9 @@ function loadConfig(): Config {
|
|
|
100
107
|
stickyIncludeCustom: false,
|
|
101
108
|
skillResumePreview: true,
|
|
102
109
|
resultPrefix: "└",
|
|
110
|
+
resultPrefixGap: 1,
|
|
111
|
+
normalizeForeignRenderers: true,
|
|
112
|
+
workflowRendering: "claude",
|
|
103
113
|
};
|
|
104
114
|
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG };
|
|
105
115
|
try {
|
|
@@ -121,6 +131,9 @@ function loadConfig(): Config {
|
|
|
121
131
|
if (typeof data?.stickyIncludeCustom === "boolean") config.stickyIncludeCustom = data.stickyIncludeCustom;
|
|
122
132
|
if (typeof data?.skillResumePreview === "boolean") config.skillResumePreview = data.skillResumePreview;
|
|
123
133
|
if (typeof data?.resultPrefix === "string" && data.resultPrefix.trim()) config.resultPrefix = data.resultPrefix;
|
|
134
|
+
if (Number.isInteger(data?.resultPrefixGap) && data.resultPrefixGap >= 0 && data.resultPrefixGap <= 4) config.resultPrefixGap = data.resultPrefixGap;
|
|
135
|
+
if (typeof data?.normalizeForeignRenderers === "boolean") config.normalizeForeignRenderers = data.normalizeForeignRenderers;
|
|
136
|
+
if (data?.workflowRendering === "claude" || data?.workflowRendering === "native") config.workflowRendering = data.workflowRendering;
|
|
124
137
|
if (data?.diff && typeof data.diff === "object" && !Array.isArray(data.diff)) {
|
|
125
138
|
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG, ...data.diff };
|
|
126
139
|
}
|
|
@@ -342,7 +355,9 @@ function patchModule(mod: any, where: string): number {
|
|
|
342
355
|
try { cls = mod[key]; } catch { continue; }
|
|
343
356
|
if (typeof cls !== "function") continue;
|
|
344
357
|
let did = false;
|
|
345
|
-
if (key
|
|
358
|
+
if (key === "CustomMessageComponent" || key === "CustomEntryComponent") did = patchForeignHost(cls);
|
|
359
|
+
else if (typeof (cls.prototype as any)?.getMessageRenderer === "function" && typeof (cls.prototype as any)?.getEntryRenderer === "function") did = patchRendererDispatch(cls);
|
|
360
|
+
else if (key.includes("ToolExecution")) did = patchToolExec(cls);
|
|
346
361
|
else if (key.includes("AssistantMessage")) did = patchAssistant(cls);
|
|
347
362
|
else if (key.includes("UserMessage")) did = patchUserMessage(cls);
|
|
348
363
|
else if (key.includes("SkillInvocation")) did = patchSkill(cls);
|
|
@@ -382,13 +397,20 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
382
397
|
registerWorkingMessage(pi);
|
|
383
398
|
const config = loadConfig();
|
|
384
399
|
SYMBOLS.resultPrefix = config.resultPrefix;
|
|
400
|
+
SYMBOLS.resultPrefixGap = config.resultPrefixGap;
|
|
385
401
|
await stickyFirstMessage(pi, { includeCustom: config.stickyIncludeCustom });
|
|
386
402
|
registerSkillRecollapse(pi, { resumePreview: config.skillResumePreview });
|
|
387
403
|
// Subagent 通知接管必须发生在加载期(registerMessageRenderer 仅 factory 内合法):
|
|
388
404
|
// lean-tidy 先于 npm 包加载,首个命中 customType 的渲染器胜出(一南 2026-02-12)。
|
|
389
405
|
// redraw 闭包延迟读 LAST_MODE(hookMode 后才看得到 TUI)。
|
|
406
|
+
const hooks = { redraw: () => { LAST_MODE?.ui?.requestRender?.(); } };
|
|
407
|
+
const subagentRenderer = createSubagentRenderer(hooks);
|
|
408
|
+
const workflowRenderer = createWorkflowRenderer(hooks);
|
|
390
409
|
if (config.subagentRendering === "claude") {
|
|
391
|
-
registerSubagentPanels(pi,
|
|
410
|
+
registerSubagentPanels(pi, hooks);
|
|
411
|
+
}
|
|
412
|
+
if (config.workflowRendering === "claude") {
|
|
413
|
+
registerWorkflowPanels(pi, hooks);
|
|
392
414
|
}
|
|
393
415
|
if (DEBUG) {
|
|
394
416
|
try { writeFileSync(LOG, `=== lean-tidy load #${loadCounter} ${new Date().toISOString()} ===\n`); } catch {}
|
|
@@ -419,6 +441,14 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
419
441
|
hookMode,
|
|
420
442
|
containerRender: (container: any, width: number, chat: boolean): string[] =>
|
|
421
443
|
claude && chat ? claude.renderChat(container, width) : Container.prototype.render.call(container, width),
|
|
444
|
+
normalizeForeign: (lines: readonly string[]): string[] =>
|
|
445
|
+
config.normalizeForeignRenderers ? normalizeForeignLines(lines) : (lines as string[]),
|
|
446
|
+
/** 分发层拦截:命中已知类型则改道为我们自己的渲染器,不受包加载顺序影响。 */
|
|
447
|
+
rendererOverride: (customType: string): any => {
|
|
448
|
+
if (customType === "subagent-notification") return config.subagentRendering === "claude" ? subagentRenderer : undefined;
|
|
449
|
+
if (customType === "subagents:workflow") return config.workflowRendering === "claude" ? workflowRenderer : undefined;
|
|
450
|
+
return undefined;
|
|
451
|
+
},
|
|
422
452
|
} as RuntimeImpl;
|
|
423
453
|
for (const method of IMPL_METHODS) {
|
|
424
454
|
impl[method] = (self, orig, ...args) => {
|
package/src/subagent-panel.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* host 驱动,两者 OR 叠加。
|
|
23
23
|
*/
|
|
24
24
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import { bold, expandHint, fg, fit, linesOf, oneLine, prefixed,
|
|
25
|
+
import { bold, expandHint, fg, fit, linesOf, oneLine, prefixed, resultIndent, RESULT_CONT } from "./claude-tools.ts";
|
|
26
26
|
|
|
27
27
|
/** 只用到 pi 传入 options 的 expanded;outputPad 由内置 Spacer(1) 承担,忽略。 */
|
|
28
28
|
interface PanelOptions {
|
|
@@ -35,8 +35,8 @@ export interface SubagentPanelHooks {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** ⎿ 分支前缀,与 claude-view.ts 工具结果/技能块同款(3 空格 + ⎿ + 2 空格,续行 6 空格)。 */
|
|
38
|
-
const branch = (theme: any): string =>
|
|
39
|
-
const BRANCH_REST =
|
|
38
|
+
const branch = (theme: any): string => resultIndent(theme).first;
|
|
39
|
+
const BRANCH_REST = () => RESULT_CONT();
|
|
40
40
|
|
|
41
41
|
/** 与该包 ui/agent-widget.ts 的 formatTokens 对齐:"33.8k token" / "1.2M token"。 */
|
|
42
42
|
function tokensText(count: unknown): string {
|
|
@@ -85,27 +85,28 @@ function panelLines(d: any, expanded: boolean, theme: any, width: number): strin
|
|
|
85
85
|
if (stats.length) tail += fg(theme, "dim", ` · ${stats.join(" · ")}`);
|
|
86
86
|
|
|
87
87
|
const out = [fit(` ${icon} ${bold(theme, "Subagent")}(${description})`, width)];
|
|
88
|
-
out.push(...prefixed(tail, width, first, BRANCH_REST));
|
|
88
|
+
out.push(...prefixed(tail, width, first, BRANCH_REST()));
|
|
89
89
|
|
|
90
90
|
// 展开态:resultPreview 全文(其本身已在包内截到 resultMaxLen)+ transcript 路径
|
|
91
91
|
if (expanded) {
|
|
92
92
|
const body = linesOf(typeof d?.resultPreview === "string" ? d.resultPreview : "");
|
|
93
|
-
for (const line of body) out.push(...prefixed(fg(theme, "dim", line), width, first, BRANCH_REST));
|
|
93
|
+
for (const line of body) out.push(...prefixed(fg(theme, "dim", line), width, first, BRANCH_REST()));
|
|
94
94
|
if (typeof d?.outputFile === "string" && d.outputFile) {
|
|
95
|
-
out.push(...prefixed(fg(theme, "dim", `transcript: ${d.outputFile}`), width, first, BRANCH_REST));
|
|
95
|
+
out.push(...prefixed(fg(theme, "dim", `transcript: ${d.outputFile}`), width, first, BRANCH_REST()));
|
|
96
96
|
}
|
|
97
97
|
} else if (typeof d?.resultPreview === "string" && d.resultPreview.trim()) {
|
|
98
98
|
// 折叠态不显示正文,但保留可展开提示(有正文才提示)
|
|
99
|
-
out.push(...prefixed(expandHint(theme), width, first, BRANCH_REST));
|
|
99
|
+
out.push(...prefixed(expandHint(theme), width, first, BRANCH_REST()));
|
|
100
100
|
}
|
|
101
101
|
return out;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/** Claude 风格的 subagent-notification 渲染器(与注册解耦,供分发层拦截复用)。 */
|
|
105
|
+
export function createSubagentRenderer(hooks: SubagentPanelHooks) {
|
|
105
106
|
// 每个 agent 条目(details 对象,含 others 内的条目)独立的点击展开状态。
|
|
106
107
|
const clickExpanded = new WeakMap<object, boolean>();
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
return (message: any, options: PanelOptions, theme: any) => {
|
|
109
110
|
const details = message?.details;
|
|
110
111
|
if (!details || typeof details !== "object") return undefined;
|
|
111
112
|
|
|
@@ -140,5 +141,47 @@ export default function registerSubagentPanels(pi: ExtensionAPI, hooks: Subagent
|
|
|
140
141
|
return { handled: true };
|
|
141
142
|
},
|
|
142
143
|
};
|
|
143
|
-
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default function registerSubagentPanels(pi: ExtensionAPI, hooks: SubagentPanelHooks): void {
|
|
148
|
+
pi.registerMessageRenderer("subagent-notification", createSubagentRenderer(hooks));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Claude 风格的 subagents:workflow entry 渲染器(v0.6.0;data 契约见 pi-subagents dist/workflow/entry.js)。 */
|
|
152
|
+
export function createWorkflowRenderer(hooks: SubagentPanelHooks) {
|
|
153
|
+
return (entry: any, options: PanelOptions, theme: any) => {
|
|
154
|
+
const d = entry?.data;
|
|
155
|
+
if (!d || typeof d !== "object") return undefined;
|
|
156
|
+
const failed = d?.status !== "completed";
|
|
157
|
+
const icon = fg(theme, failed ? "error" : "success", "●");
|
|
158
|
+
const first = branch(theme);
|
|
159
|
+
const name = oneLine(d?.name) || "workflow";
|
|
160
|
+
const ms = typeof d?.endTime === "number" && typeof d?.startTime === "number" ? d.endTime - d.startTime : NaN;
|
|
161
|
+
const status = failed ? `Failed (${String(d?.status ?? "unknown")})` : `Completed in ${msText(ms)}`;
|
|
162
|
+
const stats: string[] = [];
|
|
163
|
+
if (typeof d?.totalTokens === "number" && d.totalTokens > 0) stats.push(tokensText(d.totalTokens));
|
|
164
|
+
if (typeof d?.agentCount === "number" && d.agentCount > 0) stats.push(`${d.agentCount} agent${d.agentCount === 1 ? "" : "s"}`);
|
|
165
|
+
let tail = fg(theme, "muted", status);
|
|
166
|
+
if (stats.length) tail += fg(theme, "dim", ` · ${stats.join(" · ")}`);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
render(width: number): string[] {
|
|
170
|
+
const lines = [fit(` ${icon} ${bold(theme, "Workflow")}(${name})`, width)];
|
|
171
|
+
lines.push(...prefixed(tail, width, first, BRANCH_REST()));
|
|
172
|
+
if (options.expanded && d?.meta && typeof d.meta === "object") {
|
|
173
|
+
for (const line of linesOf(JSON.stringify(d.meta, null, 2))) {
|
|
174
|
+
lines.push(...prefixed(fg(theme, "dim", line), width, first, BRANCH_REST()));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return lines;
|
|
178
|
+
},
|
|
179
|
+
invalidate(): void {},
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 注册(顺序可能被其他包抢先;分发层拦截保证最终生效,见 index.ts rendererOverride)。 */
|
|
185
|
+
export function registerWorkflowPanels(pi: ExtensionAPI, hooks: SubagentPanelHooks): void {
|
|
186
|
+
pi.registerEntryRenderer("subagents:workflow", createWorkflowRenderer(hooks));
|
|
144
187
|
}
|