pi-cc-extensions 1.0.11 → 1.0.12
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.en.md +1 -1
- package/README.md +1 -1
- package/extensions/config/config.ts +37 -0
- package/extensions/config/panel.ts +13 -9
- package/extensions/renderer/default-mode.ts +2 -2
- package/extensions/renderer/tool/diff/diff-highlight.ts +41 -1
- package/extensions/renderer/tool/diff/write-execution.ts +42 -25
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -46,7 +46,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
46
46
|
{
|
|
47
47
|
// style
|
|
48
48
|
"mode": "on", // on / compact / off
|
|
49
|
-
"excludeRenderers": [], // tools keeping the native renderer; Agent always keeps its dedicated renderer
|
|
49
|
+
"excludeRenderers": [], // tools keeping the native renderer, matched case-insensitively; Agent always keeps its dedicated renderer
|
|
50
50
|
|
|
51
51
|
// features
|
|
52
52
|
"enableSessionReference": true, // @ session references
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
46
46
|
{
|
|
47
47
|
// style
|
|
48
48
|
"mode": "on", // on / compact / off
|
|
49
|
-
"excludeRenderers": [], //
|
|
49
|
+
"excludeRenderers": [], // 走原生渲染的工具名,匹配忽略大小写;Agent 始终保留专用渲染器
|
|
50
50
|
|
|
51
51
|
// features
|
|
52
52
|
"enableSessionReference": true, // @ session 引用
|
|
@@ -340,6 +340,43 @@ export function formatExcludeRenderers(names: readonly string[]): string {
|
|
|
340
340
|
return names.length === 0 ? "none" : names.join(", ");
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/** excludeRenderers 与运行时工具名按大小写不敏感匹配(如候选 "agent" 对上 pi-subagents 的 "Agent")。 */
|
|
344
|
+
export function isExcludedRenderer(
|
|
345
|
+
toolName: string,
|
|
346
|
+
excludeRenderers: readonly string[] = config.excludeRenderers,
|
|
347
|
+
): boolean {
|
|
348
|
+
const key = toolName.toLowerCase();
|
|
349
|
+
return excludeRenderers.some((name) => name.toLowerCase() === key);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 写入名单时去掉同名异大小写项,再按 id 写入,避免 agent/Agent 并存。 */
|
|
353
|
+
export function withExcludeRendererToggle(
|
|
354
|
+
excludeRenderers: readonly string[],
|
|
355
|
+
id: string,
|
|
356
|
+
exclude: boolean,
|
|
357
|
+
): string[] {
|
|
358
|
+
const key = id.toLowerCase();
|
|
359
|
+
const next = excludeRenderers.filter((name) => name.toLowerCase() !== key);
|
|
360
|
+
if (exclude) next.push(id);
|
|
361
|
+
return [...new Set(next)].sort((a, b) => a.localeCompare(b));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 面板候选:内置候选与用户名单按大小写不敏感合并,保留先出现的写法(候选在前,仍是 agent)。 */
|
|
365
|
+
export function mergeExcludeRendererCandidates(
|
|
366
|
+
names: readonly string[],
|
|
367
|
+
candidates: readonly string[] = EXCLUDE_RENDERER_CANDIDATES,
|
|
368
|
+
): string[] {
|
|
369
|
+
const seen = new Set<string>();
|
|
370
|
+
const merged: string[] = [];
|
|
371
|
+
for (const name of [...candidates, ...names]) {
|
|
372
|
+
const key = name.toLowerCase();
|
|
373
|
+
if (seen.has(key)) continue;
|
|
374
|
+
seen.add(key);
|
|
375
|
+
merged.push(name);
|
|
376
|
+
}
|
|
377
|
+
return merged.sort((a, b) => a.localeCompare(b));
|
|
378
|
+
}
|
|
379
|
+
|
|
343
380
|
export function formatConfigStatus(source: Config = config): string {
|
|
344
381
|
return [
|
|
345
382
|
`mode=${source.mode}`,
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
DIFF_INDICATOR_MODES,
|
|
39
39
|
DIFF_SPLIT_MIN_WIDTH_VALUES,
|
|
40
40
|
DIFF_VIEW_MODES,
|
|
41
|
-
EXCLUDE_RENDERER_CANDIDATES,
|
|
42
41
|
EXPANDED_INPUT_MAX_LINES_VALUES,
|
|
43
42
|
EXPANDED_OUTPUT_MAX_LINES_VALUES,
|
|
44
43
|
EXPANDED_PREVIEW_MAX_LINES_VALUES,
|
|
@@ -62,6 +61,9 @@ import {
|
|
|
62
61
|
type FooterGitStatsMode,
|
|
63
62
|
type FooterTokenRateMode,
|
|
64
63
|
type ScrollButtonMode,
|
|
64
|
+
isExcludedRenderer,
|
|
65
|
+
mergeExcludeRendererCandidates,
|
|
66
|
+
withExcludeRendererToggle,
|
|
65
67
|
} from "./config.ts";
|
|
66
68
|
|
|
67
69
|
/** renderer 注入的渲染副作用,面板自身不触碰渲染状态。 */
|
|
@@ -268,14 +270,13 @@ function buildExcludeRenderersSubmenu(
|
|
|
268
270
|
invalidate: () => void;
|
|
269
271
|
handleInput: (data: string) => void;
|
|
270
272
|
} {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
].sort((a, b) => a.localeCompare(b));
|
|
273
|
+
// 候选与名单按大小写不敏感合并:存过 Agent 时不再多出一行 agent。
|
|
274
|
+
const candidates = mergeExcludeRendererCandidates(config.excludeRenderers);
|
|
274
275
|
const items = candidates.map((name) => ({
|
|
275
276
|
id: name,
|
|
276
277
|
label: name,
|
|
277
278
|
description: `Use Pi native renderer for ${name} instead of Claude Code styling.`,
|
|
278
|
-
currentValue: config.excludeRenderers
|
|
279
|
+
currentValue: isExcludedRenderer(name, config.excludeRenderers) ? "exclude" : "style",
|
|
279
280
|
values: ["style", "exclude"],
|
|
280
281
|
}));
|
|
281
282
|
const list = new SettingsList(
|
|
@@ -283,10 +284,13 @@ function buildExcludeRenderersSubmenu(
|
|
|
283
284
|
Math.min(8, Math.max(4, items.length)),
|
|
284
285
|
getSettingsListTheme(),
|
|
285
286
|
(id: string, value: string) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
updateConfig({
|
|
288
|
+
excludeRenderers: withExcludeRendererToggle(
|
|
289
|
+
config.excludeRenderers,
|
|
290
|
+
id,
|
|
291
|
+
value === "exclude",
|
|
292
|
+
),
|
|
293
|
+
});
|
|
290
294
|
onLiveChange();
|
|
291
295
|
},
|
|
292
296
|
() => onClose(),
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 生命周期对齐 compact-mode:installDefaultMode → hooks.shutdown。
|
|
7
7
|
*/
|
|
8
8
|
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { config, type CompactStyleMode } from "../config/config.ts";
|
|
9
|
+
import { config, isExcludedRenderer, type CompactStyleMode } from "../config/config.ts";
|
|
10
10
|
import {
|
|
11
11
|
COMPONENT_TOOL_RENDER_MODE,
|
|
12
12
|
GLOBAL_TOOL_RENDER_PATCH,
|
|
@@ -27,7 +27,7 @@ export function preservesOriginalRenderer(
|
|
|
27
27
|
builtInToolDefinition?: any,
|
|
28
28
|
excludeRenderers: readonly string[] = config.excludeRenderers,
|
|
29
29
|
): boolean {
|
|
30
|
-
if (!
|
|
30
|
+
if (!isExcludedRenderer(toolName, excludeRenderers)) return false;
|
|
31
31
|
return [extensionDefinition, builtInToolDefinition].some(
|
|
32
32
|
(definition) =>
|
|
33
33
|
definition?.renderShell === "self" ||
|
|
@@ -45,11 +45,51 @@ export function createCodeLineHighlighter(
|
|
|
45
45
|
invalidate?: () => void,
|
|
46
46
|
): CodeLineHighlighter {
|
|
47
47
|
const codeEntries = entries.filter((entry): entry is DiffLineEntry => entry.kind === "line");
|
|
48
|
+
const allCode = codeEntries
|
|
49
|
+
.map((entry) => cleanCodeLine(normalizeCodeWhitespace(entry.content)))
|
|
50
|
+
.join("\n");
|
|
51
|
+
// Budget check stays on the whole visible code: over the limit nothing is highlighted.
|
|
52
|
+
// Per-segment checks would let a huge diff highlight itself one segment at a time.
|
|
53
|
+
const segmentLanguage = shouldHighlightCodeBlock(allCode) ? language : undefined;
|
|
54
|
+
const highlightByEntry = new WeakMap<DiffLineEntry, CodeLineHighlighter>();
|
|
55
|
+
let segment: DiffLineEntry[] = [];
|
|
56
|
+
|
|
57
|
+
const flushSegment = () => {
|
|
58
|
+
if (segment.length === 0) return;
|
|
59
|
+
const highlight = createContiguousCodeLineHighlighter(
|
|
60
|
+
segmentLanguage,
|
|
61
|
+
theme,
|
|
62
|
+
segment,
|
|
63
|
+
invalidate,
|
|
64
|
+
);
|
|
65
|
+
for (const entry of segment) highlightByEntry.set(entry, highlight);
|
|
66
|
+
segment = [];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Omitted source cannot carry lexer state across a diff discontinuity.
|
|
70
|
+
// Each segment is highlighted and cached on its own; the budget above still bounds one diff.
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry.kind === "line") segment.push(entry);
|
|
73
|
+
else flushSegment();
|
|
74
|
+
}
|
|
75
|
+
flushSegment();
|
|
76
|
+
|
|
77
|
+
return (line, entry) => highlightByEntry.get(entry)?.(line, entry) ?? "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createContiguousCodeLineHighlighter(
|
|
81
|
+
language: string | undefined,
|
|
82
|
+
theme: DiffTheme,
|
|
83
|
+
codeEntries: readonly DiffLineEntry[],
|
|
84
|
+
invalidate?: () => void,
|
|
85
|
+
): CodeLineHighlighter {
|
|
48
86
|
const cleanLines = codeEntries.map((entry) =>
|
|
49
87
|
cleanCodeLine(normalizeCodeWhitespace(entry.content)),
|
|
50
88
|
);
|
|
51
89
|
const code = cleanLines.join("\n");
|
|
52
|
-
|
|
90
|
+
// A segment is a subset of the visible code, so `segmentLanguage` already carries
|
|
91
|
+
// the total budget decision; only the per-segment highlight is left to decide here.
|
|
92
|
+
const shouldHighlight = language !== undefined;
|
|
53
93
|
const fallbackLines = shouldHighlight
|
|
54
94
|
? (() => {
|
|
55
95
|
try {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createWriteToolDefinition, type WriteOperations } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
3
|
|
|
5
4
|
export const MAX_COMPARABLE_WRITE_BYTES = 512_000;
|
|
6
5
|
export const MAX_WRITE_METADATA_ENTRIES = 100;
|
|
@@ -81,33 +80,51 @@ export async function executeWriteWithMetadata(
|
|
|
81
80
|
signal: AbortSignal | undefined,
|
|
82
81
|
cwd: string,
|
|
83
82
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: undefined }> {
|
|
84
|
-
const absolutePath = isAbsolute(params.path) ? params.path : resolve(cwd, params.path);
|
|
85
83
|
store.delete(toolCallId);
|
|
84
|
+
// 覆盖层只揽写前快照:路径解析(含 Windows 下 /mnt/<drive>、/cygdrive/<drive> 转换)、
|
|
85
|
+
// 目录创建、mutation queue 与错误文案全交回 pi 官方 write,参数与官方逐字一致。
|
|
86
|
+
const definition = createWriteToolDefinition(cwd, {
|
|
87
|
+
operations: writeSnapshotOperations(store, toolCallId),
|
|
88
|
+
}) as any;
|
|
86
89
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
90
|
-
};
|
|
91
|
-
throwIfAborted();
|
|
92
|
-
const metadata = await capturePreviousContent(absolutePath);
|
|
93
|
-
throwIfAborted();
|
|
94
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
95
|
-
throwIfAborted();
|
|
96
|
-
await writeFile(absolutePath, params.content, "utf8");
|
|
97
|
-
// The file is already changed. A later abort must not report failure.
|
|
98
|
-
store.set(toolCallId, metadata);
|
|
99
|
-
return {
|
|
100
|
-
content: [
|
|
101
|
-
{
|
|
102
|
-
type: "text",
|
|
103
|
-
text: `Successfully wrote ${Buffer.byteLength(params.content, "utf8")} bytes to ${params.path}`,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
details: undefined,
|
|
107
|
-
};
|
|
108
|
-
});
|
|
90
|
+
await definition.execute(toolCallId, params, signal, undefined, { cwd });
|
|
91
|
+
return writeSuccessResult(params);
|
|
109
92
|
} catch (error) {
|
|
93
|
+
// 文件已落盘,只是官方写后那次 abort 检查抦了错:保持「文件已改即成功」。
|
|
94
|
+
if (store.get(toolCallId) && signal?.aborted) return writeSuccessResult(params);
|
|
110
95
|
store.delete(toolCallId);
|
|
111
96
|
throw error;
|
|
112
97
|
}
|
|
113
98
|
}
|
|
99
|
+
|
|
100
|
+
/** 官方 write 的落盘操作:拿到的是官方解析后的绝对路径,快照才跟真实落盘一致。 */
|
|
101
|
+
function writeSnapshotOperations(
|
|
102
|
+
store: WriteExecutionMetadataStore,
|
|
103
|
+
toolCallId: string,
|
|
104
|
+
): WriteOperations {
|
|
105
|
+
return {
|
|
106
|
+
mkdir: (dir) => mkdir(dir, { recursive: true }).then(() => {}),
|
|
107
|
+
writeFile: async (absolutePath, content) => {
|
|
108
|
+
store.delete(toolCallId);
|
|
109
|
+
const metadata = await capturePreviousContent(absolutePath);
|
|
110
|
+
await writeFile(absolutePath, content, "utf8");
|
|
111
|
+
// 写已生效:后续 abort 不得把这次写报成失败,快照先落下。
|
|
112
|
+
store.set(toolCallId, metadata);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeSuccessResult(params: { path: string; content: string }): {
|
|
118
|
+
content: Array<{ type: "text"; text: string }>;
|
|
119
|
+
details: undefined;
|
|
120
|
+
} {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Successfully wrote ${Buffer.byteLength(params.content, "utf8")} bytes to ${params.path}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
details: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|