@yinan_chen/pi-claude-code-ui 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +44 -2
- package/package.json +34 -5
- package/src/claude-tools.ts +274 -0
- package/src/claude-view.ts +775 -0
- package/src/index.ts +566 -0
- package/src/pi-runtime.ts +232 -0
- package/src/preview.ts +147 -0
- package/src/running-effect.ts +29 -0
- package/src/skill-block-recollapse.ts +362 -0
- package/src/sticky-first-message.ts +1114 -0
- package/src/subagent-panel.ts +144 -0
- package/src/vendor/ATTRIBUTION.md +26 -0
- package/src/vendor/ansi-color.ts +44 -0
- package/src/vendor/ansi-text.ts +47 -0
- package/src/vendor/diff/ATTRIBUTION.md +28 -0
- package/src/vendor/diff/ansi-utils.ts +113 -0
- package/src/vendor/diff/diff-component.ts +125 -0
- package/src/vendor/diff/diff-edit-render.ts +237 -0
- package/src/vendor/diff/diff-header.ts +137 -0
- package/src/vendor/diff/diff-highlight.ts +115 -0
- package/src/vendor/diff/diff-inline.ts +324 -0
- package/src/vendor/diff/diff-layout.ts +762 -0
- package/src/vendor/diff/diff-limits.ts +229 -0
- package/src/vendor/diff/diff-palette.ts +410 -0
- package/src/vendor/diff/diff-parse.ts +535 -0
- package/src/vendor/diff/diff-presentation.ts +70 -0
- package/src/vendor/diff/diff-renderer.ts +23 -0
- package/src/vendor/diff/diff-text.ts +45 -0
- package/src/vendor/diff/diff-write-render.ts +572 -0
- package/src/vendor/diff/index.ts +126 -0
- package/src/vendor/diff/line-width-safety.ts +92 -0
- package/src/vendor/diff/shiki-highlight.ts +92 -0
- package/src/vendor/diff/write-execution.ts +113 -0
- package/src/vendor/diff-config.ts +30 -0
- package/src/vendor/show-more-hint.ts +38 -0
- package/src/vendor/tool-result-sanitize.ts +24 -0
- package/src/working-message.ts +140 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lean-tidy — Claude transcript adapter with a Pi-native fallback.
|
|
3
|
+
*
|
|
4
|
+
* Rendering lives in claude-view.ts; this entry point owns configuration,
|
|
5
|
+
* built-in tool execution/renderers, reload-safe prototype trampolines and F5.
|
|
6
|
+
* A trampoline reads __leanTidyImpl on every invocation, so /reload replaces
|
|
7
|
+
* behavior without wrapping the same Pi class twice. The original FLAG names
|
|
8
|
+
* are intentionally stable for an already-running Pi process.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
ToolExecutionComponent,
|
|
13
|
+
createReadToolDefinition,
|
|
14
|
+
createBashToolDefinition,
|
|
15
|
+
createEditToolDefinition,
|
|
16
|
+
createWriteToolDefinition,
|
|
17
|
+
createGrepToolDefinition,
|
|
18
|
+
createFindToolDefinition,
|
|
19
|
+
createLsToolDefinition,
|
|
20
|
+
generateDiffString,
|
|
21
|
+
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { mkdir, readFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
23
|
+
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
27
|
+
import { getPiRuntime, VERIFIED_PI_VERSION } from "./pi-runtime.ts";
|
|
28
|
+
import { createClaudeView } from "./claude-view.ts";
|
|
29
|
+
import registerSubagentPanels from "./subagent-panel.ts";
|
|
30
|
+
import stickyFirstMessage from "./sticky-first-message.ts";
|
|
31
|
+
import registerSkillRecollapse from "./skill-block-recollapse.ts";
|
|
32
|
+
import registerWorkingMessage from "./working-message.ts";
|
|
33
|
+
// Vendored rich diff (MIT; see vendor/diff/ATTRIBUTION.md).
|
|
34
|
+
import { renderRichToolResult, WriteExecutionMetadataStore } from "./vendor/diff/index.ts";
|
|
35
|
+
import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "./vendor/diff-config.ts";
|
|
36
|
+
|
|
37
|
+
const LOG = "/tmp/lean-tidy.log";
|
|
38
|
+
const DEBUG = (process.env.PI_LEAN_TIDY_DEBUG || "").trim() === "1";
|
|
39
|
+
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
40
|
+
const CONFIG_PATH = join(AGENT_DIR, "lean-tidy.json");
|
|
41
|
+
const TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
42
|
+
const IMPL_KEY = "__leanTidyImpl";
|
|
43
|
+
const writeMeta = new WriteExecutionMetadataStore();
|
|
44
|
+
let diffDisplayConfig: ToolDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG };
|
|
45
|
+
let THEME: any = (globalThis as any).__leanTidyTheme;
|
|
46
|
+
let LAST_MODE: any;
|
|
47
|
+
const loadCounter = ((globalThis as any).__leanTidyLoadCounter ?? 0) + 1;
|
|
48
|
+
(globalThis as any).__leanTidyLoadCounter = loadCounter;
|
|
49
|
+
|
|
50
|
+
function diag(...parts: unknown[]): void {
|
|
51
|
+
if (!DEBUG) return;
|
|
52
|
+
try {
|
|
53
|
+
appendFileSync(LOG, `${parts.map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(" ")}\n`);
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captureTheme(theme: any): void {
|
|
58
|
+
if (theme && typeof theme.fg === "function" && THEME !== theme) {
|
|
59
|
+
THEME = theme;
|
|
60
|
+
(globalThis as any).__leanTidyTheme = theme;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Only options used by the Claude adapter are read; obsolete settings are ignored. */
|
|
65
|
+
interface Config {
|
|
66
|
+
profile: "claude" | "native";
|
|
67
|
+
/** Skill 调用块样式:native = pi 原生 [skill] name 折叠行(默认);claude = ● Skill(name) 两行风格。 */
|
|
68
|
+
skillRendering: "native" | "claude";
|
|
69
|
+
/** Subagent 完成通知(@tintinweb/pi-subagents 的 subagent-notification)样式:
|
|
70
|
+
* claude = ● Subagent(desc) + ⎿ 分支风格(默认);native = 保留包内面板。 */
|
|
71
|
+
subagentRendering: "native" | "claude";
|
|
72
|
+
detailsBackground: string;
|
|
73
|
+
bashPreviewLines: number;
|
|
74
|
+
codePreviewLines: number;
|
|
75
|
+
errorPreviewLines: number;
|
|
76
|
+
liveThinking: boolean;
|
|
77
|
+
thinkingTailLines: number;
|
|
78
|
+
foldAnimMs: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadConfig(): Config {
|
|
82
|
+
const config: Config = {
|
|
83
|
+
profile: "claude",
|
|
84
|
+
skillRendering: "native",
|
|
85
|
+
subagentRendering: "claude",
|
|
86
|
+
detailsBackground: "selectedBg",
|
|
87
|
+
bashPreviewLines: 1,
|
|
88
|
+
codePreviewLines: 3,
|
|
89
|
+
errorPreviewLines: 1,
|
|
90
|
+
liveThinking: true,
|
|
91
|
+
thinkingTailLines: 3,
|
|
92
|
+
foldAnimMs: 240,
|
|
93
|
+
};
|
|
94
|
+
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG };
|
|
95
|
+
try {
|
|
96
|
+
const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
97
|
+
if (data?.profile === "claude" || data?.profile === "native") config.profile = data.profile;
|
|
98
|
+
if (data?.skillRendering === "claude" || data?.skillRendering === "native") config.skillRendering = data.skillRendering;
|
|
99
|
+
if (data?.subagentRendering === "claude" || data?.subagentRendering === "native") config.subagentRendering = data.subagentRendering;
|
|
100
|
+
if (["selectedBg", "customMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg", ""].includes(data?.detailsBackground)) {
|
|
101
|
+
config.detailsBackground = data.detailsBackground;
|
|
102
|
+
}
|
|
103
|
+
for (const key of ["bashPreviewLines", "codePreviewLines", "errorPreviewLines"] as const) {
|
|
104
|
+
if (Number.isInteger(data?.[key]) && data[key] >= 0 && data[key] <= 100) config[key] = data[key];
|
|
105
|
+
}
|
|
106
|
+
if (typeof data?.liveThinking === "boolean") config.liveThinking = data.liveThinking;
|
|
107
|
+
if (Number.isFinite(data?.thinkingTailLines) && data.thinkingTailLines >= 0) {
|
|
108
|
+
config.thinkingTailLines = Math.floor(data.thinkingTailLines);
|
|
109
|
+
}
|
|
110
|
+
if (Number.isFinite(data?.foldAnimMs) && data.foldAnimMs >= 0) config.foldAnimMs = Math.floor(data.foldAnimMs);
|
|
111
|
+
if (data?.diff && typeof data.diff === "object" && !Array.isArray(data.diff)) {
|
|
112
|
+
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG, ...data.diff };
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
const profile = (process.env.PI_LEAN_TIDY_PROFILE || "").trim();
|
|
116
|
+
if (profile === "claude" || profile === "native") config.profile = profile;
|
|
117
|
+
const skill = (process.env.PI_LEAN_TIDY_SKILL || "").trim();
|
|
118
|
+
if (skill === "claude" || skill === "native") config.skillRendering = skill;
|
|
119
|
+
const subagent = (process.env.PI_LEAN_TIDY_SUBAGENT || "").trim();
|
|
120
|
+
if (subagent === "claude" || subagent === "native") config.subagentRendering = subagent;
|
|
121
|
+
const live = (process.env.PI_LEAN_TIDY_LIVE_THINKING || "").trim().toLowerCase();
|
|
122
|
+
if (live === "on" || live === "off") config.liveThinking = live === "on";
|
|
123
|
+
if ((process.env.PI_TIDY_STATIC_THINKING || "").trim() === "1") config.liveThinking = false;
|
|
124
|
+
const tail = (process.env.PI_LEAN_TIDY_THINKING_TAIL || "").trim();
|
|
125
|
+
if (/^\d+$/.test(tail)) config.thinkingTailLines = Number(tail);
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Global settings are safe to read; project settings require explicit project trust. */
|
|
130
|
+
function readBuiltinToolOptions(cwd?: string, trusted = false): { read: any; bash: any; key: string } {
|
|
131
|
+
const readJson = (path: string): any => {
|
|
132
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return undefined; }
|
|
133
|
+
};
|
|
134
|
+
const globalSettings = readJson(join(AGENT_DIR, "settings.json")) ?? {};
|
|
135
|
+
const projectSettings = cwd && trusted ? (readJson(join(cwd, ".pi", "settings.json")) ?? {}) : {};
|
|
136
|
+
const pick = <T>(get: (settings: any) => T): T | undefined => {
|
|
137
|
+
const projectValue = get(projectSettings);
|
|
138
|
+
return projectValue !== undefined ? projectValue : get(globalSettings);
|
|
139
|
+
};
|
|
140
|
+
const opts = {
|
|
141
|
+
read: { autoResizeImages: pick((s) => s?.images?.autoResize) ?? true },
|
|
142
|
+
bash: { commandPrefix: pick((s) => s?.shellCommandPrefix), shellPath: pick((s) => s?.shellPath) },
|
|
143
|
+
};
|
|
144
|
+
return { ...opts, key: JSON.stringify(opts) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Leave the native renderer intact: Claude owns transcript presentation, not tool execution. */
|
|
148
|
+
function richDiffComponent(tool: any, expanded: boolean, theme: any, invalidate: () => void): any | undefined {
|
|
149
|
+
const name = String(tool.toolName ?? "");
|
|
150
|
+
if ((name !== "edit" && name !== "write") || !theme || tool.result?.isError) return undefined;
|
|
151
|
+
const result = tool.result;
|
|
152
|
+
const hasDiff = typeof result?.details?.diff === "string" && result.details.diff.trim().length > 0;
|
|
153
|
+
if (name === "edit" && !hasDiff) return undefined;
|
|
154
|
+
const toolCallId = String(tool.toolCallId ?? "");
|
|
155
|
+
if (name === "write" && !writeMeta.get(toolCallId)) return undefined;
|
|
156
|
+
try {
|
|
157
|
+
return renderRichToolResult(
|
|
158
|
+
name,
|
|
159
|
+
result,
|
|
160
|
+
{ isPartial: false, isError: false, expanded, isHovered: () => false, invalidate },
|
|
161
|
+
theme,
|
|
162
|
+
{ args: tool.args ?? {}, toolCallId, invalidate, isError: false, expanded },
|
|
163
|
+
writeMeta,
|
|
164
|
+
diffDisplayConfig,
|
|
165
|
+
);
|
|
166
|
+
} catch { return undefined; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const IMPL_METHODS = [
|
|
170
|
+
"toolExecRender", "toolExecHandleMouse", "assistantRender", "assistantUpdateContent",
|
|
171
|
+
"genericRender", "modeRenderInitial", "modeAddMessage", "modeSetToolsExpanded",
|
|
172
|
+
"modeUpdateThinkingVisibility", "modeHandleEvent",
|
|
173
|
+
] as const;
|
|
174
|
+
type ImplMethod = typeof IMPL_METHODS[number];
|
|
175
|
+
type RuntimeImpl = {
|
|
176
|
+
loadId: number;
|
|
177
|
+
config: Config;
|
|
178
|
+
hookMode(mode: any): void;
|
|
179
|
+
containerRender(container: any, width: number, chat: boolean): string[];
|
|
180
|
+
} & Record<ImplMethod, (self: any, orig: (...args: any[]) => any, ...args: any[]) => any>;
|
|
181
|
+
|
|
182
|
+
function getImpl(): RuntimeImpl | undefined {
|
|
183
|
+
return (globalThis as any)[IMPL_KEY];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Unwind instance-only hooks left by an in-process /reload before native rendering. */
|
|
187
|
+
function restoreNativeMode(mode: any): void {
|
|
188
|
+
const chat = mode?.chatContainer;
|
|
189
|
+
if (!chat) return;
|
|
190
|
+
if (chat.__tidyNoSpacers) {
|
|
191
|
+
if (chat.addChild?.name === "leanTidyAddChild") chat.addChild = Container.prototype.addChild;
|
|
192
|
+
for (const child of chat.children ?? []) {
|
|
193
|
+
if (typeof child?.setLines === "function" && child.lines === 0) child.setLines(1);
|
|
194
|
+
}
|
|
195
|
+
delete chat.__tidyNoSpacers;
|
|
196
|
+
}
|
|
197
|
+
if (chat.__tidyRenderWrapped && chat.render?.name === "leanTidyChatRender") {
|
|
198
|
+
chat.render = Container.prototype.render;
|
|
199
|
+
delete chat.__tidyRenderWrapped;
|
|
200
|
+
}
|
|
201
|
+
// Claude's instance wrapper also survives /reload; do not leave a stale
|
|
202
|
+
// adapter dispatch in the native profile even though its fallback works.
|
|
203
|
+
if (chat.__claudeChatWrapped) {
|
|
204
|
+
const original = Symbol.for("lean-tidy/claude-original-render");
|
|
205
|
+
chat.render = chat[original] ?? Container.prototype.render;
|
|
206
|
+
delete chat[original];
|
|
207
|
+
delete chat.__claudeChatWrapped;
|
|
208
|
+
}
|
|
209
|
+
for (const child of chat.children ?? []) {
|
|
210
|
+
const content = child?.contentContainer;
|
|
211
|
+
if (content?.__leanTidyFilteredContainer) {
|
|
212
|
+
content.render = Container.prototype.render;
|
|
213
|
+
delete content.__leanTidyFilteredContainer;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Stable FLAG names are essential: old trampolines also call getImpl() after /reload.
|
|
219
|
+
const FLAG_TOOLEXEC = "__tidyAnytoolPatched";
|
|
220
|
+
const FLAG_ASSISTANT = "__tidyAnytoolAssistantPatched";
|
|
221
|
+
const FLAG_LIVE = "__tidyAnytoolLiveThinkingPatched";
|
|
222
|
+
const FLAG_USERMSG = "__tidyAnytoolUserMsgPatched";
|
|
223
|
+
const FLAG_SKILL = "__tidyAnytoolSkillPatched";
|
|
224
|
+
const FLAG_INTERACTIVE = "__tidyAnytoolInteractivePatched";
|
|
225
|
+
const FILTER_KEYS: [string, string][] = [
|
|
226
|
+
["BashExecutionComponent", "__tidyAnytoolBashExecPatched"],
|
|
227
|
+
["CustomMessageComponent", "__tidyAnytoolCustomMsgPatched"],
|
|
228
|
+
["CompactionSummaryMessageComponent", "__tidyAnytoolCompactionPatched"],
|
|
229
|
+
["BranchSummaryMessageComponent", "__tidyAnytoolBranchPatched"],
|
|
230
|
+
["CustomEntryComponent", "__tidyAnytoolCustomEntryPatched"],
|
|
231
|
+
];
|
|
232
|
+
const patchedProtos = new Set<any>();
|
|
233
|
+
|
|
234
|
+
function trampoline(proto: any, method: string, implMethod: ImplMethod): boolean {
|
|
235
|
+
const orig = proto?.[method];
|
|
236
|
+
if (typeof orig !== "function") return false;
|
|
237
|
+
const wrapper = function (this: any, ...args: any[]) {
|
|
238
|
+
const impl = getImpl();
|
|
239
|
+
if (!impl) return orig.apply(this, args);
|
|
240
|
+
try { return impl[implMethod](this, orig, ...args); }
|
|
241
|
+
catch (error: any) {
|
|
242
|
+
diag("impl error:", implMethod, error?.message);
|
|
243
|
+
return orig.apply(this, args);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
try { Object.defineProperty(wrapper, "name", { value: `leanTidy_${implMethod}` }); } catch {}
|
|
247
|
+
proto[method] = wrapper;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function once(proto: any, flag: string): boolean {
|
|
252
|
+
if (!proto || proto[flag]) return false;
|
|
253
|
+
proto[flag] = true;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function patchToolExec(cls: any): boolean {
|
|
258
|
+
const proto = cls?.prototype;
|
|
259
|
+
if (!proto || typeof proto.render !== "function" || typeof proto.updateDisplay !== "function" || typeof proto.setExpanded !== "function") return false;
|
|
260
|
+
if (!once(proto, FLAG_TOOLEXEC)) return false;
|
|
261
|
+
trampoline(proto, "render", "toolExecRender");
|
|
262
|
+
trampoline(proto, "handleMouse", "toolExecHandleMouse");
|
|
263
|
+
patchedProtos.add(proto);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function patchAssistant(cls: any): boolean {
|
|
268
|
+
const proto = cls?.prototype;
|
|
269
|
+
if (!proto || typeof proto.render !== "function" || typeof proto.updateContent !== "function" || typeof proto.setOutputPad !== "function") return false;
|
|
270
|
+
let did = false;
|
|
271
|
+
if (once(proto, FLAG_ASSISTANT)) did = trampoline(proto, "render", "assistantRender") || did;
|
|
272
|
+
if (once(proto, FLAG_LIVE)) did = trampoline(proto, "updateContent", "assistantUpdateContent") || did;
|
|
273
|
+
if (did) patchedProtos.add(proto);
|
|
274
|
+
return did;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function patchUserMessage(cls: any): boolean {
|
|
278
|
+
const proto = cls?.prototype;
|
|
279
|
+
if (!proto || typeof proto.render !== "function" || typeof proto.rebuild !== "function") return false;
|
|
280
|
+
if (!once(proto, FLAG_USERMSG)) return false;
|
|
281
|
+
trampoline(proto, "render", "genericRender");
|
|
282
|
+
patchedProtos.add(proto);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function patchSkill(cls: any): boolean {
|
|
287
|
+
const proto = cls?.prototype;
|
|
288
|
+
if (!proto || typeof proto.render !== "function" || typeof proto.updateDisplay !== "function" || typeof proto.setExpanded !== "function") return false;
|
|
289
|
+
if (!once(proto, FLAG_SKILL)) return false;
|
|
290
|
+
trampoline(proto, "render", "genericRender");
|
|
291
|
+
patchedProtos.add(proto);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function patchFilterOnly(cls: any, flag: string): boolean {
|
|
296
|
+
const proto = cls?.prototype;
|
|
297
|
+
if (!proto || typeof proto.render !== "function") return false;
|
|
298
|
+
if (!once(proto, flag)) return false;
|
|
299
|
+
trampoline(proto, "render", "genericRender");
|
|
300
|
+
patchedProtos.add(proto);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function patchInteractive(cls: any): boolean {
|
|
305
|
+
const proto = cls?.prototype;
|
|
306
|
+
if (!proto || typeof proto.renderInitialMessages !== "function" || typeof proto.addMessageToChat !== "function") return false;
|
|
307
|
+
let did = false;
|
|
308
|
+
if (once(proto, FLAG_INTERACTIVE)) {
|
|
309
|
+
trampoline(proto, "renderInitialMessages", "modeRenderInitial");
|
|
310
|
+
trampoline(proto, "addMessageToChat", "modeAddMessage");
|
|
311
|
+
trampoline(proto, "handleEvent", "modeHandleEvent");
|
|
312
|
+
did = true;
|
|
313
|
+
}
|
|
314
|
+
// A live /reload can add these two entry points to an already-patched class.
|
|
315
|
+
if (once(proto, "__leanTidyToolsExpansionPatched")) {
|
|
316
|
+
did = trampoline(proto, "setToolsExpanded", "modeSetToolsExpanded") || did;
|
|
317
|
+
}
|
|
318
|
+
if (once(proto, "__leanTidyThinkingVisibilityPatched")) {
|
|
319
|
+
did = trampoline(proto, "updateThinkingBlockVisibility", "modeUpdateThinkingVisibility") || did;
|
|
320
|
+
}
|
|
321
|
+
if (did) patchedProtos.add(proto);
|
|
322
|
+
return did;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function patchModule(mod: any, where: string): number {
|
|
326
|
+
let n = 0;
|
|
327
|
+
for (const key of Object.keys(mod)) {
|
|
328
|
+
let cls: any;
|
|
329
|
+
try { cls = mod[key]; } catch { continue; }
|
|
330
|
+
if (typeof cls !== "function") continue;
|
|
331
|
+
let did = false;
|
|
332
|
+
if (key.includes("ToolExecution")) did = patchToolExec(cls);
|
|
333
|
+
else if (key.includes("AssistantMessage")) did = patchAssistant(cls);
|
|
334
|
+
else if (key.includes("UserMessage")) did = patchUserMessage(cls);
|
|
335
|
+
else if (key.includes("SkillInvocation")) did = patchSkill(cls);
|
|
336
|
+
else if (key.includes("InteractiveMode")) did = patchInteractive(cls);
|
|
337
|
+
else {
|
|
338
|
+
for (const [name, flag] of FILTER_KEYS) {
|
|
339
|
+
if (key.includes(name)) { did = patchFilterOnly(cls, flag); break; }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (did) { n++; diag("patched:", where, key); }
|
|
343
|
+
}
|
|
344
|
+
return n;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface ScanInfo {
|
|
348
|
+
pkgRoot: string;
|
|
349
|
+
piVersion: string;
|
|
350
|
+
bundleRuntime: boolean;
|
|
351
|
+
files: string[];
|
|
352
|
+
patched: number;
|
|
353
|
+
cacheHit: boolean;
|
|
354
|
+
ms: number;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function patchAllCopies(): Promise<ScanInfo> {
|
|
358
|
+
const start = Date.now();
|
|
359
|
+
const rt = await getPiRuntime();
|
|
360
|
+
let patched = 0;
|
|
361
|
+
for (const { where, mod } of rt.modules) patched += patchModule(mod, where);
|
|
362
|
+
return {
|
|
363
|
+
pkgRoot: rt.pkgRoot, piVersion: rt.piVersion, bundleRuntime: rt.bundleRuntime,
|
|
364
|
+
files: rt.files, patched, cacheHit: rt.cacheHit, ms: Date.now() - start,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
369
|
+
registerWorkingMessage(pi);
|
|
370
|
+
await stickyFirstMessage(pi);
|
|
371
|
+
registerSkillRecollapse(pi);
|
|
372
|
+
const config = loadConfig();
|
|
373
|
+
// Subagent 通知接管必须发生在加载期(registerMessageRenderer 仅 factory 内合法):
|
|
374
|
+
// lean-tidy 先于 npm 包加载,首个命中 customType 的渲染器胜出(一南 2026-02-12)。
|
|
375
|
+
// redraw 闭包延迟读 LAST_MODE(hookMode 后才看得到 TUI)。
|
|
376
|
+
if (config.subagentRendering === "claude") {
|
|
377
|
+
registerSubagentPanels(pi, { redraw: () => { LAST_MODE?.ui?.requestRender?.(); } });
|
|
378
|
+
}
|
|
379
|
+
if (DEBUG) {
|
|
380
|
+
try { writeFileSync(LOG, `=== lean-tidy load #${loadCounter} ${new Date().toISOString()} ===\n`); } catch {}
|
|
381
|
+
}
|
|
382
|
+
let noticeContext: any;
|
|
383
|
+
let lastWarning: string | undefined;
|
|
384
|
+
(globalThis as any).__leanTidyClaude?.dispose?.();
|
|
385
|
+
const claude = config.profile === "claude" ? createClaudeView(
|
|
386
|
+
// Keep the same object: runtime foldAnimMs changes must reach the adapter.
|
|
387
|
+
config,
|
|
388
|
+
() => THEME,
|
|
389
|
+
(message) => {
|
|
390
|
+
lastWarning = message;
|
|
391
|
+
diag(message);
|
|
392
|
+
noticeContext?.ui?.notify?.(message, "warning");
|
|
393
|
+
},
|
|
394
|
+
richDiffComponent,
|
|
395
|
+
) : undefined;
|
|
396
|
+
(globalThis as any).__leanTidyClaude = claude;
|
|
397
|
+
const hookMode = (mode: any) => {
|
|
398
|
+
LAST_MODE = mode;
|
|
399
|
+
if (claude) claude.hookMode(mode);
|
|
400
|
+
else restoreNativeMode(mode);
|
|
401
|
+
};
|
|
402
|
+
const impl = {
|
|
403
|
+
loadId: loadCounter,
|
|
404
|
+
config,
|
|
405
|
+
hookMode,
|
|
406
|
+
containerRender: (container: any, width: number, chat: boolean): string[] =>
|
|
407
|
+
claude && chat ? claude.renderChat(container, width) : Container.prototype.render.call(container, width),
|
|
408
|
+
} as RuntimeImpl;
|
|
409
|
+
for (const method of IMPL_METHODS) {
|
|
410
|
+
impl[method] = (self, orig, ...args) => {
|
|
411
|
+
if (!claude) {
|
|
412
|
+
if (method === "modeRenderInitial" || method === "modeAddMessage" || method === "modeHandleEvent") hookMode(self);
|
|
413
|
+
return orig.apply(self, args);
|
|
414
|
+
}
|
|
415
|
+
try { return (claude[method] as (...parts: any[]) => any)(self, orig, ...args); }
|
|
416
|
+
catch (error) { claude.fail(error); return orig.apply(self, args); }
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
(globalThis as any)[IMPL_KEY] = impl;
|
|
420
|
+
|
|
421
|
+
let scan: ScanInfo | undefined;
|
|
422
|
+
try {
|
|
423
|
+
scan = await patchAllCopies();
|
|
424
|
+
diag("scan:", scan);
|
|
425
|
+
} catch (error: any) { diag("scan error:", error?.message); }
|
|
426
|
+
|
|
427
|
+
let registeredOptsKey = "";
|
|
428
|
+
let probeDef: ToolDefinition | undefined;
|
|
429
|
+
/** Construct a detached tool to capture Pi's theme before transcript rendering. */
|
|
430
|
+
function probeTheme(cwd: string): void {
|
|
431
|
+
if (THEME || !probeDef) return;
|
|
432
|
+
try {
|
|
433
|
+
new ToolExecutionComponent("read", "__lean_tidy_theme_probe__", { path: "" }, {}, probeDef, { requestRender() {} } as any, cwd);
|
|
434
|
+
} catch (error: any) { diag("theme probe failed:", error?.message); }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Register before session_start: the first historical render may happen earlier. */
|
|
438
|
+
function registerTools(cwd: string, trusted = false): void {
|
|
439
|
+
const opts = readBuiltinToolOptions(cwd, trusted);
|
|
440
|
+
if (registeredOptsKey === opts.key) return;
|
|
441
|
+
registeredOptsKey = opts.key;
|
|
442
|
+
const factories: Record<string, (cwd: string, options?: any) => any> = {
|
|
443
|
+
read: (dir) => createReadToolDefinition(dir, opts.read),
|
|
444
|
+
bash: (dir) => createBashToolDefinition(dir, opts.bash),
|
|
445
|
+
edit: (dir) => createEditToolDefinition(dir),
|
|
446
|
+
write: (dir, options) => createWriteToolDefinition(dir, options),
|
|
447
|
+
grep: (dir) => createGrepToolDefinition(dir),
|
|
448
|
+
find: (dir) => createFindToolDefinition(dir),
|
|
449
|
+
ls: (dir) => createLsToolDefinition(dir),
|
|
450
|
+
};
|
|
451
|
+
for (const name of TOOLS) {
|
|
452
|
+
let orig: any;
|
|
453
|
+
try { orig = factories[name](cwd); }
|
|
454
|
+
catch (error: any) { diag("factory failed:", name, error?.message); continue; }
|
|
455
|
+
// Pi's write tool owns the file-mutation queue; operations execute inside it.
|
|
456
|
+
const execute = name === "write"
|
|
457
|
+
? async (id: string, params: any, signal: any, onUpdate: any, ctx: any) => {
|
|
458
|
+
let diff = "";
|
|
459
|
+
let created = false;
|
|
460
|
+
let previousContent = "";
|
|
461
|
+
const writeTool = factories.write(cwd, {
|
|
462
|
+
operations: {
|
|
463
|
+
mkdir: async (directory: string) => { await mkdir(directory, { recursive: true }); },
|
|
464
|
+
writeFile: async (path: string, content: string) => {
|
|
465
|
+
let previous = "";
|
|
466
|
+
try { previous = await readFile(path, "utf8"); }
|
|
467
|
+
catch (error: any) {
|
|
468
|
+
if (error?.code !== "ENOENT") throw error;
|
|
469
|
+
created = true;
|
|
470
|
+
}
|
|
471
|
+
previousContent = previous;
|
|
472
|
+
await mkdir(dirname(path), { recursive: true });
|
|
473
|
+
await fsWriteFile(path, content, "utf8");
|
|
474
|
+
diff = generateDiffString(previous, content).diff;
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
const result = await (writeTool.execute as (...args: any[]) => any).call(writeTool, id, params, signal, onUpdate, ctx);
|
|
479
|
+
const oversized = previousContent.length > 512_000 ||
|
|
480
|
+
(typeof params?.content === "string" && params.content.length > 512_000);
|
|
481
|
+
writeMeta.set(id, {
|
|
482
|
+
fileExistedBeforeWrite: !created,
|
|
483
|
+
previousContent,
|
|
484
|
+
diffUnavailableReason: oversized ? "file exceeds the 512,000-byte diff limit" : undefined,
|
|
485
|
+
});
|
|
486
|
+
return { ...result, details: { ...(result.details ?? {}), diff, created } };
|
|
487
|
+
}
|
|
488
|
+
: (id: string, params: any, signal: any, onUpdate: any, ctx: any) => orig.execute(id, params, signal, onUpdate, ctx);
|
|
489
|
+
// Spread preserves promptSnippet, promptGuidelines, constrainedSampling,
|
|
490
|
+
// prepareArguments and executionMode. Renderers remain Pi-native for fallback.
|
|
491
|
+
const tool: ToolDefinition = {
|
|
492
|
+
...orig,
|
|
493
|
+
execute: execute as ToolDefinition["execute"],
|
|
494
|
+
renderCall: (args: any, theme: any, context: any) => {
|
|
495
|
+
captureTheme(theme);
|
|
496
|
+
const slot = context?.state ?? {};
|
|
497
|
+
const rendered = orig.renderCall?.(args, theme, { ...context, lastComponent: slot.__claudeNativeCall }) ?? new Container();
|
|
498
|
+
slot.__claudeNativeCall = rendered;
|
|
499
|
+
return rendered;
|
|
500
|
+
},
|
|
501
|
+
renderResult: (result: any, options: any, theme: any, context: any) => {
|
|
502
|
+
captureTheme(theme);
|
|
503
|
+
const slot = context?.state ?? {};
|
|
504
|
+
const rendered = orig.renderResult?.(result, options, theme, { ...context, lastComponent: slot.__claudeNativeResult }) ?? new Container();
|
|
505
|
+
slot.__claudeNativeResult = rendered;
|
|
506
|
+
return rendered;
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
pi.registerTool(tool);
|
|
510
|
+
if (name === "read") probeDef = tool;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
pi.on("agent_start", async () => { claude?.onAgentStart(); });
|
|
515
|
+
pi.on("message_start", async (event: any) => { claude?.onEvent({ ...event, type: "message_start" }); });
|
|
516
|
+
pi.on("message_update", async (event: any) => { claude?.onEvent({ ...event, type: "message_update" }); });
|
|
517
|
+
pi.on("message_end", async (event: any) => { claude?.onEvent({ ...event, type: "message_end" }); });
|
|
518
|
+
pi.on("agent_settled", async () => { claude?.onSettled(); });
|
|
519
|
+
pi.on("session_shutdown", async () => { claude?.dispose(); });
|
|
520
|
+
|
|
521
|
+
/** Reset Pi's diff renderer without changing the current session or transcript. */
|
|
522
|
+
function forceRedraw(): boolean {
|
|
523
|
+
const tui: any = LAST_MODE?.ui;
|
|
524
|
+
if (!tui) return false;
|
|
525
|
+
try {
|
|
526
|
+
if ("previousScreen" in tui) tui.previousScreen = [];
|
|
527
|
+
else tui.previousWidth = 1;
|
|
528
|
+
tui.requestRender?.();
|
|
529
|
+
return true;
|
|
530
|
+
} catch { return false; }
|
|
531
|
+
}
|
|
532
|
+
pi.registerCommand("tidy-redraw", {
|
|
533
|
+
description: "lean-tidy: 强制整屏重绘(清掉终端/pane 层留下的残影行)",
|
|
534
|
+
handler: async (_args: string, ctx: any) => {
|
|
535
|
+
if (!forceRedraw()) ctx.ui?.notify?.("lean-tidy: 拿不到 TUI,无法重绘", "warning");
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
pi.registerShortcut("f5" as any, {
|
|
539
|
+
description: "lean-tidy: 强制整屏重绘",
|
|
540
|
+
handler: () => { forceRedraw(); },
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
544
|
+
noticeContext = ctx;
|
|
545
|
+
claude?.onSession();
|
|
546
|
+
captureTheme(ctx.ui?.theme);
|
|
547
|
+
registerTools(ctx.cwd ?? process.cwd(), ctx.isProjectTrusted?.() === true);
|
|
548
|
+
probeTheme(ctx.cwd ?? process.cwd());
|
|
549
|
+
if (lastWarning) ctx.ui?.notify?.(lastWarning, "warning");
|
|
550
|
+
const key = `__leanTidyHealthNotified:${scan?.piVersion ?? "?"}`;
|
|
551
|
+
const global = globalThis as any;
|
|
552
|
+
if (scan && !global[key]) {
|
|
553
|
+
global[key] = true;
|
|
554
|
+
if (scan.patched === 0 && patchedProtos.size === 0 && !(ToolExecutionComponent.prototype as any)[FLAG_TOOLEXEC]) {
|
|
555
|
+
ctx.ui?.notify?.("lean-tidy: 0 classes patched — rendering falls back to pi native", "warning");
|
|
556
|
+
} else if ((process.env.PI_PATCH_VERSION_WARN || "").trim() !== "" && scan.piVersion && scan.piVersion !== VERIFIED_PI_VERSION) {
|
|
557
|
+
// 版本比对警告默认静默(一南 2026-10-04);上一分支的「0 classes patched」真问题警告不受影响。
|
|
558
|
+
// VERIFIED_PI_VERSION 与逐版核验记录仍维护在 _shared/pi-runtime.ts,需要时 PI_PATCH_VERSION_WARN=1 查看。
|
|
559
|
+
ctx.ui?.notify?.(`lean-tidy: pi ${scan.piVersion} ≠ verified ${VERIFIED_PI_VERSION}; run with PI_LEAN_TIDY_DEBUG=1 if rendering looks off`, "warning");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
try { registerTools(process.cwd()); }
|
|
565
|
+
catch (error: any) { diag("early register failed:", error?.message); }
|
|
566
|
+
}
|