pi-cc-extensions 0.8.14 → 0.8.16

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 CHANGED
@@ -81,7 +81,6 @@ The `ask_user_question` tool is registered automatically.
81
81
  | Structured questionnaires | Single choice, multiple choice, custom answers, Markdown previews, and RPC / ACP fallback | `ask_user_question` |
82
82
  | Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
83
83
  | Session references | Search and inject effective context from previous Sessions or existing SubAgents | `@session:` |
84
- | Agent completion | Complete and delegate custom Agents from`~/.pi/agent/agents/*.md` | `@agent-name` |
85
84
  | Theme | Includes the GitHub Dark Default theme | `/theme` |
86
85
 
87
86
  ### Output modes
@@ -165,10 +164,11 @@ Run `/reload` after changing extensions. Changes under `extensions/ask-user-ques
165
164
  | `npm:pi-theme-picker` | Theme search and live preview |
166
165
  | `npm:@ayulab/pi-rewind` | Checkpoint-based code or conversation rollback |
167
166
  | `npm:pi-compact-thinking` | Compact rendering for hidden thinking blocks |
168
- | `npm:pi-startup-header` | Theme-aware startup header |
169
167
 
170
168
  ## Credits
171
169
 
172
170
  - Compact transcript behavior is based on [`avhagedorn/pi-compact-transcript`](https://github.com/avhagedorn/pi-compact-transcript) v0.6.2 (MIT).
173
171
  - Structured questionnaires are based on [`@juicesharp/rpiv-ask-user-question`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-ask-user-question) v2.1.0 (MIT).
174
172
  - Rich diffs are adapted from [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display) (MIT). See [`extensions/tool-diff/ATTRIBUTION.md`](./extensions/tool-diff/ATTRIBUTION.md).
173
+ - Startup header based on [`EnderLiquid/pi-startup-header`](https://github.com/EnderLiquid/pi-startup-header) (MIT).
174
+ - Command aliases based on [`xRyul/pi-aliases`](https://github.com/xRyul/pi-aliases) (MIT).
package/README.md CHANGED
@@ -59,17 +59,12 @@
59
59
 
60
60
  ```bash
61
61
  pi install npm:pi-cc-extensions
62
- ```
63
-
64
- 安装后执行 `/reload`,然后可以使用:
65
62
 
66
- ```text
67
- /ccstyle # 配置输出与 fixed editor 交互
68
- /context # 查看上下文占用
69
- @ # 补全 Agent、Session 与文件
63
+ # GitHub
64
+ pi install git:github.com/minuque/pi-cc-extensions
70
65
  ```
71
66
 
72
- `ask_user_question` 会自动注册为模型工具。
67
+ 安装后执行 `/reload`
73
68
 
74
69
  ## 功能
75
70
 
@@ -81,62 +76,7 @@ pi install npm:pi-cc-extensions
81
76
  | Ask User Question 结构化问卷 | 单选、多选、自定义回答、Markdown 预览及 RPC / ACP 降级 | `ask_user_question` |
82
77
  | 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
83
78
  | Session 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@session:` |
84
- | Agent 补全 | 从`~/.pi/agent/agents/*.md` 补全并委托自定义 Agent | `@agent-name` |
85
- | 主题 | 随包提供 GitHub Dark Default 主题 | `/theme` |
86
-
87
- ### 输出模式
88
-
89
-
90
- | 模式 | 行为 |
91
- | ----------- | ----------------------------------------------------------- |
92
- | `on` | Claude Code 风格工具输出;`edit` / `write` 使用 rich diff |
93
- | `off` | 使用 Pi 原生 renderer |
94
- | `compact` | 单行工具预览、同名调用合并、运行耗时与总结 |
95
-
96
- ```text
97
- /ccstyle on
98
- /ccstyle off
99
- /ccstyle compact
100
- /ccstyle status
101
- ```
102
-
103
- 配置保存在 `~/.pi/agent/claude-code-style.json`:
104
-
105
- ```json
106
- {
107
- "mode": "on",
108
- "excludeRenderers": [],
109
- "fixedEditorFeatures": true
110
- }
111
- ```
112
-
113
- - `excludeRenderers` 使用精确工具名;`Agent` 始终保留专用 renderer。
114
- - `fixedEditorFeatures: true` 启用 fixed editor 鼠标滚动、点击、viewport 映射和回到底部提示。
115
- - 设为 `false` 会关闭终端鼠标上报,恢复终端原生滚轮;`Ctrl+End` 仍可用。
116
- - 手动修改配置后执行 `/reload`。
117
-
118
- ### 结构化问卷
119
-
120
- 支持 1–4 个问题、每题 2–4 个选项、单选、多选、自定义回答和可选 Markdown 预览。TUI 会保留部分 transcript 空间;RPC / ACP 环境自动降级为原生对话框。
121
-
122
- 问卷配置位于 `~/.config/rpiv-ask-user-question/config.json`。快捷键与完整行为见 [`extensions/ask-user-question/README.md`](./extensions/ask-user-question/README.md)。
123
-
124
- ### `@` 引用
125
-
126
- - 输入 `@session:` 引用历史 Session。
127
- - 输入 `@agent-name` 委托自定义 Agent。
128
- - 可与 `@bacnh85/pi-fff` 和 `@tintinweb/pi-subagents` 共存。
129
- - Session 内容会去重并限制注入规模。
130
-
131
- ## 其他安装方式
132
-
133
- ```bash
134
- # GitHub
135
- pi install git:github.com/minuque/pi-cc-extensions
136
-
137
- # 本地仓库
138
- pi install /absolute/path/to/pi-cc-extensions
139
- ```
79
+ | 主题 | 随包提供内置 GitHub Dark Default 主题 | `/theme` |
140
80
 
141
81
  ## 本地开发
142
82
 
@@ -165,10 +105,11 @@ pi -e .
165
105
  | `npm:pi-theme-picker` | 主题搜索和实时预览 |
166
106
  | `npm:@ayulab/pi-rewind` | 基于 checkpoint 回退代码或对话 |
167
107
  | `npm:pi-compact-thinking` | 紧凑显示隐藏的 thinking 块 |
168
- | `npm:pi-startup-header` | 主题自适应启动标题 |
169
108
 
170
109
  ## 致谢
171
110
 
172
111
  - Compact transcript 基于 [`avhagedorn/pi-compact-transcript`](https://github.com/avhagedorn/pi-compact-transcript) v0.6.2(MIT)。
173
112
  - 结构化问卷基于 [`@juicesharp/rpiv-ask-user-question`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-ask-user-question) v2.1.0(MIT)。
174
113
  - Rich diff 改编自 [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display)(MIT);详见 [`extensions/tool-diff/ATTRIBUTION.md`](./extensions/tool-diff/ATTRIBUTION.md)。
114
+ - 启动头基于 [`EnderLiquid/pi-startup-header`](https://github.com/EnderLiquid/pi-startup-header)(MIT)。
115
+ - 命令别名基于 [`xRyul/pi-aliases`](https://github.com/xRyul/pi-aliases)(MIT)。
@@ -2138,6 +2138,8 @@ function createCcstyleTool(
2138
2138
  setToolVisualState(context, isError ? "error" : "success");
2139
2139
  const expanded = isToolExpanded(options, context);
2140
2140
  if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
2141
+ // Pass getter so Diff indicator / wrap / limits update on the next paint
2142
+ // without recreating the tool result component.
2141
2143
  const richResult = renderRichToolResult(
2142
2144
  toolName,
2143
2145
  result,
@@ -2145,7 +2147,7 @@ function createCcstyleTool(
2145
2147
  theme,
2146
2148
  context,
2147
2149
  writeExecutionMetadata,
2148
- getToolDisplayConfig(),
2150
+ getToolDisplayConfig,
2149
2151
  );
2150
2152
  if (richResult) return richResult;
2151
2153
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pi Aliases Extension
3
+ *
4
+ * Registers command aliases that show in autocomplete:
5
+ * /clear → /new (start a new session)
6
+ * /exit → /quit (exit pi)
7
+ *
8
+ * Install:
9
+ * pi install npm:pi-aliases
10
+ * pi install git:github.com/xRyul/pi-aliases
11
+ */
12
+
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+
15
+ export default function (pi: ExtensionAPI) {
16
+ // /clear → /new
17
+ pi.registerCommand("clear", {
18
+ description: "→ /new · Start a new session",
19
+ handler: async (_args, ctx) => {
20
+ await ctx.newSession();
21
+ },
22
+ });
23
+
24
+ // /exit → /quit
25
+ pi.registerCommand("exit", {
26
+ description: "→ /quit · Exit pi",
27
+ handler: async (_args, ctx) => {
28
+ ctx.shutdown();
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,288 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ type Rgb = [number, number, number];
4
+ type StyledPart = {
5
+ raw: string;
6
+ styled: string;
7
+ };
8
+
9
+ const ANSI_RESET = "\x1b[0m";
10
+ const ANSI_PATTERN =
11
+ /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
12
+
13
+ const LOGO_LINES = [
14
+ "████████████╗",
15
+ "████████████║",
16
+ "████╔═══████║",
17
+ "████║ ████║",
18
+ "████████╬═══████╗",
19
+ "████████║ ████║ ",
20
+ "████╔═══╝ ████║",
21
+ "████║ ████║",
22
+ "╚═══╝ ╚═══╝",
23
+ ];
24
+
25
+ const TAGLINE_LINE_1 = "There are many agent harnesses,";
26
+ const TAGLINE_LINE_2_PREFIX = "but this one is ";
27
+ const TAGLINE_LINE_2_HIGHLIGHT = "yours";
28
+ const TAGLINE_LINE_2_SUFFIX = ".";
29
+
30
+ const FALLBACK_ACCENT_RGB: Rgb = [80, 160, 255];
31
+ const LOGO_BLOCK_WIDTH = Math.max(...LOGO_LINES.map((line) => [...line].length));
32
+
33
+ const PALETTE_STEPS = 24;
34
+ const PALETTE_MAX_DARKEN = 0.18;
35
+ const PALETTE_MAX_LIGHTEN = 0.18;
36
+ const LOGO_ROW_PHASE_STEP = 0.12;
37
+
38
+ const ANSI_16_RGB_TABLE: Rgb[] = [
39
+ [0, 0, 0],
40
+ [128, 0, 0],
41
+ [0, 128, 0],
42
+ [128, 128, 0],
43
+ [0, 0, 128],
44
+ [128, 0, 128],
45
+ [0, 128, 128],
46
+ [192, 192, 192],
47
+ [128, 128, 128],
48
+ [255, 0, 0],
49
+ [0, 255, 0],
50
+ [255, 255, 0],
51
+ [0, 0, 255],
52
+ [255, 0, 255],
53
+ [0, 255, 255],
54
+ [255, 255, 255],
55
+ ];
56
+
57
+ function stripAnsi(text: string): string {
58
+ return text.replace(ANSI_PATTERN, "");
59
+ }
60
+
61
+ function getVisibleLength(text: string): number {
62
+ return [...stripAnsi(text)].length;
63
+ }
64
+
65
+ function clampByte(value: number): number {
66
+ return Math.max(0, Math.min(255, Math.round(value)));
67
+ }
68
+
69
+ function interpolateChannel(start: number, end: number, factor: number): number {
70
+ return Math.round(start + (end - start) * factor);
71
+ }
72
+
73
+ function interpolateRgb(start: Rgb, end: Rgb, factor: number): Rgb {
74
+ return [
75
+ interpolateChannel(start[0], end[0], factor),
76
+ interpolateChannel(start[1], end[1], factor),
77
+ interpolateChannel(start[2], end[2], factor),
78
+ ];
79
+ }
80
+
81
+ function darkenRgb(rgb: Rgb, amount: number): Rgb {
82
+ return [
83
+ clampByte(rgb[0] * (1 - amount)),
84
+ clampByte(rgb[1] * (1 - amount)),
85
+ clampByte(rgb[2] * (1 - amount)),
86
+ ];
87
+ }
88
+
89
+ function lightenRgb(rgb: Rgb, amount: number): Rgb {
90
+ return [
91
+ clampByte(rgb[0] + (255 - rgb[0]) * amount),
92
+ clampByte(rgb[1] + (255 - rgb[1]) * amount),
93
+ clampByte(rgb[2] + (255 - rgb[2]) * amount),
94
+ ];
95
+ }
96
+
97
+ function applyTruecolor(rgb: Rgb, text: string): string {
98
+ const [red, green, blue] = rgb;
99
+ return `\x1b[38;2;${red};${green};${blue}m${text}${ANSI_RESET}`;
100
+ }
101
+
102
+ function ansi16ToRgb(index: number): Rgb {
103
+ return ANSI_16_RGB_TABLE[index] ?? [255, 255, 255];
104
+ }
105
+
106
+ function ansi256ToRgb(index: number): Rgb {
107
+ if (index < 16) return ansi16ToRgb(index);
108
+
109
+ if (index >= 232) {
110
+ const gray = 8 + (index - 232) * 10;
111
+ return [gray, gray, gray];
112
+ }
113
+
114
+ const cubeIndex = index - 16;
115
+ const redIndex = Math.floor(cubeIndex / 36);
116
+ const greenIndex = Math.floor((cubeIndex % 36) / 6);
117
+ const blueIndex = cubeIndex % 6;
118
+ const values = [0, 95, 135, 175, 215, 255];
119
+
120
+ return [values[redIndex]!, values[greenIndex]!, values[blueIndex]!];
121
+ }
122
+
123
+ function parseTruecolorAnsi(ansi: string): Rgb | undefined {
124
+ const match = ansi.match(/38;2;(\d+);(\d+);(\d+)/);
125
+ if (!match) return undefined;
126
+
127
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
128
+ }
129
+
130
+ function parseAnsi256Foreground(ansi: string): Rgb | undefined {
131
+ const match = ansi.match(/38;5;(\d+)/);
132
+ if (!match) return undefined;
133
+
134
+ return ansi256ToRgb(Number(match[1]));
135
+ }
136
+
137
+ function parseAnsi16Foreground(ansi: string): Rgb | undefined {
138
+ const normalMatch = ansi.match(/(?:\[|;)(3[0-7])(?:;|m)/);
139
+ if (normalMatch) {
140
+ return ansi16ToRgb(Number(normalMatch[1]) - 30);
141
+ }
142
+
143
+ const brightMatch = ansi.match(/(?:\[|;)(9[0-7])(?:;|m)/);
144
+ if (brightMatch) {
145
+ return ansi16ToRgb(Number(brightMatch[1]) - 90 + 8);
146
+ }
147
+
148
+ return undefined;
149
+ }
150
+
151
+ function parseForegroundRgbFromAnsi(ansi: string): Rgb | undefined {
152
+ return parseTruecolorAnsi(ansi) ?? parseAnsi256Foreground(ansi) ?? parseAnsi16Foreground(ansi);
153
+ }
154
+
155
+ function resolveAccentRgb(theme: { getFgAnsi(name: string): string }): Rgb {
156
+ return parseForegroundRgbFromAnsi(theme.getFgAnsi("accent")) ?? FALLBACK_ACCENT_RGB;
157
+ }
158
+
159
+ function buildAccentPalette(accent: Rgb): Rgb[] {
160
+ return Array.from({ length: PALETTE_STEPS }, (_, index) => {
161
+ const progress = index / PALETTE_STEPS;
162
+ const wave = -Math.cos(progress * Math.PI * 2);
163
+
164
+ if (wave < 0) {
165
+ return darkenRgb(accent, PALETTE_MAX_DARKEN * -wave);
166
+ }
167
+
168
+ return lightenRgb(accent, PALETTE_MAX_LIGHTEN * wave);
169
+ });
170
+ }
171
+
172
+ function sampleGradientColor(palette: Rgb[], position: number): Rgb {
173
+ const wrappedPosition = ((position % 1) + 1) % 1;
174
+ const scaledPosition = wrappedPosition * palette.length;
175
+ const baseIndex = Math.floor(scaledPosition) % palette.length;
176
+ const nextIndex = (baseIndex + 1) % palette.length;
177
+ const factor = scaledPosition - Math.floor(scaledPosition);
178
+
179
+ return interpolateRgb(palette[baseIndex]!, palette[nextIndex]!, factor);
180
+ }
181
+
182
+ function renderGradientText(text: string, palette: Rgb[], phase: number): string {
183
+ const characters = [...text];
184
+ const span = Math.max(characters.length - 1, 1);
185
+
186
+ return characters
187
+ .map((character, index) => {
188
+ if (character === " ") return character;
189
+ const color = sampleGradientColor(palette, index / span + phase);
190
+ return applyTruecolor(color, character);
191
+ })
192
+ .join("");
193
+ }
194
+
195
+ function createCenteredBlockLine(text: string, width: number, blockWidth: number): string {
196
+ const leftPadding = Math.max(0, Math.floor((width - blockWidth) / 2));
197
+ return `${" ".repeat(leftPadding)}${text}`;
198
+ }
199
+
200
+ function createCenteredStyledLine(parts: StyledPart[], width: number): string {
201
+ const rawText = parts.map((part) => part.raw).join("");
202
+ const leftPadding = Math.max(0, Math.floor((width - [...rawText].length) / 2));
203
+ const styledText = parts.map((part) => part.styled).join("");
204
+ return `${" ".repeat(leftPadding)}${styledText}`;
205
+ }
206
+
207
+ function fitLineToWidth(line: string, width: number): string {
208
+ if (getVisibleLength(line) <= width) {
209
+ return line;
210
+ }
211
+
212
+ return stripAnsi(line).slice(0, width);
213
+ }
214
+
215
+ function renderLogoLines(width: number, palette: Rgb[]): string[] {
216
+ return LOGO_LINES.map((line, rowIndex) => {
217
+ const phasedLine = renderGradientText(line, palette, rowIndex * LOGO_ROW_PHASE_STEP);
218
+ return createCenteredBlockLine(phasedLine, width, LOGO_BLOCK_WIDTH);
219
+ });
220
+ }
221
+
222
+ function renderTaglineLines(
223
+ width: number,
224
+ theme: {
225
+ fg(name: string, text: string): string;
226
+ bold(text: string): string;
227
+ },
228
+ ): string[] {
229
+ const line1 = createCenteredStyledLine(
230
+ [{ raw: TAGLINE_LINE_1, styled: theme.fg("accent", TAGLINE_LINE_1) }],
231
+ width,
232
+ );
233
+
234
+ const line2 = createCenteredStyledLine(
235
+ [
236
+ {
237
+ raw: TAGLINE_LINE_2_PREFIX,
238
+ styled: theme.fg("accent", TAGLINE_LINE_2_PREFIX),
239
+ },
240
+ {
241
+ raw: TAGLINE_LINE_2_HIGHLIGHT,
242
+ styled: theme.bold(theme.fg("mdLink", TAGLINE_LINE_2_HIGHLIGHT)),
243
+ },
244
+ {
245
+ raw: TAGLINE_LINE_2_SUFFIX,
246
+ styled: theme.fg("accent", TAGLINE_LINE_2_SUFFIX),
247
+ },
248
+ ],
249
+ width,
250
+ );
251
+
252
+ return [line1, line2];
253
+ }
254
+
255
+ function renderHeaderLines(
256
+ width: number,
257
+ theme: {
258
+ getFgAnsi(name: string): string;
259
+ fg(name: string, text: string): string;
260
+ bold(text: string): string;
261
+ },
262
+ ): string[] {
263
+ const accentRgb = resolveAccentRgb(theme);
264
+ const palette = buildAccentPalette(accentRgb);
265
+ const logoLines = renderLogoLines(width, palette);
266
+ const taglineLines = renderTaglineLines(width, theme);
267
+
268
+ return ["", ...logoLines, "", ...taglineLines, ""].map((line) => fitLineToWidth(line, width));
269
+ }
270
+
271
+ export default function piStartupHeader(pi: ExtensionAPI) {
272
+ pi.on("session_start", async (_event, ctx) => {
273
+ if (!ctx.hasUI) return;
274
+
275
+ ctx.ui.setHeader((_tui, theme) => ({
276
+ render(width: number): string[] {
277
+ return renderHeaderLines(width, theme);
278
+ },
279
+ invalidate() {},
280
+ }));
281
+ });
282
+
283
+ pi.on("session_shutdown", async (_event, ctx) => {
284
+ if (!ctx.hasUI) return;
285
+
286
+ ctx.ui.setHeader(undefined);
287
+ });
288
+ }
@@ -1312,6 +1312,25 @@ function resolveDiffIndicatorMode(
1312
1312
  return config.diffIndicatorMode ?? DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode;
1313
1313
  }
1314
1314
 
1315
+ /** Snapshot or live getter — panel changes must apply on the next paint. */
1316
+ export type DisplayConfigInput = ToolDisplayConfig | (() => ToolDisplayConfig);
1317
+
1318
+ function resolveLiveDisplayConfig(input: DisplayConfigInput): ToolDisplayConfig {
1319
+ return typeof input === "function" ? input() : input;
1320
+ }
1321
+
1322
+ /** Cache key fragment so indicator/wrap/limits invalidate without host recreate. */
1323
+ function displayConfigCacheKey(config: ToolDisplayConfig): string {
1324
+ return [
1325
+ config.diffViewMode,
1326
+ config.diffIndicatorMode,
1327
+ String(config.diffSplitMinWidth),
1328
+ String(config.diffCollapsedLines),
1329
+ config.diffWordWrap ? "1" : "0",
1330
+ String(config.expandedPreviewMaxLines),
1331
+ ].join(":");
1332
+ }
1333
+
1315
1334
  function resolveIndicatorGlyph(
1316
1335
  kind: DiffLineKind,
1317
1336
  indicatorMode: DiffIndicatorMode,
@@ -2350,24 +2369,38 @@ function createDiffRenderCache() {
2350
2369
  let cachedWidth: number | undefined;
2351
2370
  let cachedExpanded: boolean | undefined;
2352
2371
  let cachedMode: DiffPresentationMode | undefined;
2372
+ let cachedConfigKey: string | undefined;
2353
2373
  let cachedLines: string[] | undefined;
2354
2374
 
2355
2375
  return {
2356
- get(width: number, expanded: boolean, mode: DiffPresentationMode): string[] | undefined {
2376
+ get(
2377
+ width: number,
2378
+ expanded: boolean,
2379
+ mode: DiffPresentationMode,
2380
+ configKey: string,
2381
+ ): string[] | undefined {
2357
2382
  if (
2358
2383
  cachedLines &&
2359
2384
  cachedWidth === width &&
2360
2385
  cachedExpanded === expanded &&
2361
- cachedMode === mode
2386
+ cachedMode === mode &&
2387
+ cachedConfigKey === configKey
2362
2388
  ) {
2363
2389
  return cachedLines;
2364
2390
  }
2365
2391
  return undefined;
2366
2392
  },
2367
- set(width: number, expanded: boolean, mode: DiffPresentationMode, lines: string[]): string[] {
2393
+ set(
2394
+ width: number,
2395
+ expanded: boolean,
2396
+ mode: DiffPresentationMode,
2397
+ configKey: string,
2398
+ lines: string[],
2399
+ ): string[] {
2368
2400
  cachedWidth = width;
2369
2401
  cachedExpanded = expanded;
2370
2402
  cachedMode = mode;
2403
+ cachedConfigKey = configKey;
2371
2404
  cachedLines = lines;
2372
2405
  return lines;
2373
2406
  },
@@ -2375,6 +2408,7 @@ function createDiffRenderCache() {
2375
2408
  cachedWidth = undefined;
2376
2409
  cachedExpanded = undefined;
2377
2410
  cachedMode = undefined;
2411
+ cachedConfigKey = undefined;
2378
2412
  cachedLines = undefined;
2379
2413
  },
2380
2414
  };
@@ -2383,7 +2417,7 @@ function createDiffRenderCache() {
2383
2417
  export function renderEditDiffResult(
2384
2418
  details: unknown,
2385
2419
  options: DiffRenderOptions,
2386
- config: ToolDisplayConfig,
2420
+ config: DisplayConfigInput,
2387
2421
  theme: DiffTheme,
2388
2422
  fallbackText: string,
2389
2423
  ): Component {
@@ -2418,16 +2452,19 @@ export function renderEditDiffResult(
2418
2452
  const containerBgAnsi = undefined;
2419
2453
  const language = resolveLanguageFromPath(options.filePath);
2420
2454
  const highlightLine = createCodeLineHighlighter(language);
2421
- const wordWrap = config.diffWordWrap;
2422
- const indicatorMode = resolveDiffIndicatorMode(config);
2423
2455
 
2424
2456
  const cache = createDiffRenderCache();
2425
2457
 
2426
2458
  return {
2427
2459
  render(width: number): string[] {
2460
+ // Live config: panel can change indicator/wrap/limits after this component is created.
2461
+ const live = resolveLiveDisplayConfig(config);
2462
+ const wordWrap = live.diffWordWrap;
2463
+ const indicatorMode = resolveDiffIndicatorMode(live);
2464
+ const configKey = displayConfigCacheKey(live);
2428
2465
  const safeWidth = normalizeDiffRenderWidth(width);
2429
- const mode = resolveDiffPresentationMode(config, safeWidth, canRenderSplitLayout(safeWidth));
2430
- const cached = cache.get(safeWidth, options.expanded, mode);
2466
+ const mode = resolveDiffPresentationMode(live, safeWidth, canRenderSplitLayout(safeWidth));
2467
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2431
2468
  if (cached) {
2432
2469
  return cached;
2433
2470
  }
@@ -2437,6 +2474,7 @@ export function renderEditDiffResult(
2437
2474
  safeWidth,
2438
2475
  options.expanded,
2439
2476
  mode,
2477
+ configKey,
2440
2478
  clampDiffLinesToWidth(
2441
2479
  renderSingleDiffRow(
2442
2480
  buildDiffSummaryText(parsed.stats, safeWidth),
@@ -2452,8 +2490,8 @@ export function renderEditDiffResult(
2452
2490
  const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
2453
2491
  const displayLimit = resolveDiffDisplayLimit(
2454
2492
  options.expanded,
2455
- config.diffCollapsedLines,
2456
- config.expandedPreviewMaxLines,
2493
+ live.diffCollapsedLines,
2494
+ live.expandedPreviewMaxLines,
2457
2495
  );
2458
2496
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2459
2497
  // Only highlight/render a prefix that can fill the display limit; full-diff
@@ -2486,8 +2524,8 @@ export function renderEditDiffResult(
2486
2524
  bodyRows,
2487
2525
  safeWidth,
2488
2526
  options.expanded,
2489
- config.diffCollapsedLines,
2490
- config.expandedPreviewMaxLines,
2527
+ live.diffCollapsedLines,
2528
+ live.expandedPreviewMaxLines,
2491
2529
  parsed.stats.hunks,
2492
2530
  theme,
2493
2531
  unprocessedLogicalRows,
@@ -2499,7 +2537,7 @@ export function renderEditDiffResult(
2499
2537
  : [...headerRows.map((row) => row.text), ...bodyWithLimit];
2500
2538
 
2501
2539
  const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2502
- return cache.set(safeWidth, options.expanded, mode, clampedLines);
2540
+ return cache.set(safeWidth, options.expanded, mode, configKey, clampedLines);
2503
2541
  },
2504
2542
  invalidate: cache.invalidate,
2505
2543
  };
@@ -2753,7 +2791,7 @@ function renderWriteOverwriteGuardRows(
2753
2791
  export function renderWriteDiffResult(
2754
2792
  content: string | undefined,
2755
2793
  options: DiffRenderOptions,
2756
- config: ToolDisplayConfig,
2794
+ config: DisplayConfigInput,
2757
2795
  theme: DiffTheme,
2758
2796
  fallbackText: string,
2759
2797
  ): Component {
@@ -2782,8 +2820,6 @@ export function renderWriteDiffResult(
2782
2820
  const containerBgAnsi = undefined;
2783
2821
  const language = resolveLanguageFromPath(filePath);
2784
2822
  const highlightLine = createCodeLineHighlighter(language);
2785
- const wordWrap = config.diffWordWrap;
2786
- const indicatorMode = resolveDiffIndicatorMode(config);
2787
2823
 
2788
2824
  let detailedData: WriteDiffData | undefined;
2789
2825
  const cache = createDiffRenderCache();
@@ -2801,9 +2837,14 @@ export function renderWriteDiffResult(
2801
2837
 
2802
2838
  return {
2803
2839
  render(width: number): string[] {
2840
+ // Live config: panel can change indicator/wrap/limits after this component is created.
2841
+ const live = resolveLiveDisplayConfig(config);
2842
+ const wordWrap = live.diffWordWrap;
2843
+ const indicatorMode = resolveDiffIndicatorMode(live);
2844
+ const configKey = displayConfigCacheKey(live);
2804
2845
  const safeWidth = normalizeDiffRenderWidth(width);
2805
2846
  const resolvedMode = resolveDiffPresentationMode(
2806
- config,
2847
+ live,
2807
2848
  safeWidth,
2808
2849
  canRenderSplitLayout(safeWidth),
2809
2850
  );
@@ -2812,7 +2853,7 @@ export function renderWriteDiffResult(
2812
2853
  : resolvedMode === "split"
2813
2854
  ? "unified"
2814
2855
  : resolvedMode;
2815
- const cached = cache.get(safeWidth, options.expanded, mode);
2856
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2816
2857
  if (cached) {
2817
2858
  return cached;
2818
2859
  }
@@ -2832,6 +2873,7 @@ export function renderWriteDiffResult(
2832
2873
  safeWidth,
2833
2874
  options.expanded,
2834
2875
  mode,
2876
+ configKey,
2835
2877
  clampDiffLinesToWidth(
2836
2878
  [header, ...renderWriteOverwriteGuardRows(overwriteGuard, safeWidth, theme)],
2837
2879
  safeWidth,
@@ -2856,6 +2898,7 @@ export function renderWriteDiffResult(
2856
2898
  safeWidth,
2857
2899
  options.expanded,
2858
2900
  mode,
2901
+ configKey,
2859
2902
  clampDiffLinesToWidth(summaryRows, safeWidth),
2860
2903
  );
2861
2904
  }
@@ -2863,8 +2906,8 @@ export function renderWriteDiffResult(
2863
2906
  const data = getDetailedData();
2864
2907
  const displayLimit = resolveDiffDisplayLimit(
2865
2908
  options.expanded,
2866
- config.diffCollapsedLines,
2867
- config.expandedPreviewMaxLines,
2909
+ live.diffCollapsedLines,
2910
+ live.expandedPreviewMaxLines,
2868
2911
  );
2869
2912
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2870
2913
  const entryBudget = takeEntriesForLineBudget(data.entries, processBudget);
@@ -2901,8 +2944,8 @@ export function renderWriteDiffResult(
2901
2944
  bodyRows,
2902
2945
  safeWidth,
2903
2946
  options.expanded,
2904
- config.diffCollapsedLines,
2905
- config.expandedPreviewMaxLines,
2947
+ live.diffCollapsedLines,
2948
+ live.expandedPreviewMaxLines,
2906
2949
  data.hunkCount,
2907
2950
  theme,
2908
2951
  unprocessedLogicalRows,
@@ -2911,7 +2954,7 @@ export function renderWriteDiffResult(
2911
2954
  const renderedLines =
2912
2955
  mode === "unified" ? [header, frame, ...bodyWithLimit, frame] : [header, ...bodyWithLimit];
2913
2956
  const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2914
- return cache.set(safeWidth, options.expanded, mode, finalLines);
2957
+ return cache.set(safeWidth, options.expanded, mode, configKey, finalLines);
2915
2958
  },
2916
2959
  invalidate: cache.invalidate,
2917
2960
  };
@@ -1,7 +1,11 @@
1
1
  import { createWriteToolDefinition, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import { renderEditDiffResult, renderWriteDiffResult } from "./diff-renderer.ts";
4
- import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "./types.ts";
3
+ import {
4
+ renderEditDiffResult,
5
+ renderWriteDiffResult,
6
+ type DisplayConfigInput,
7
+ } from "./diff-renderer.ts";
8
+ import { DEFAULT_TOOL_DISPLAY_CONFIG } from "./types.ts";
5
9
  import { executeWriteWithMetadata, WriteExecutionMetadataStore } from "./write-execution.ts";
6
10
 
7
11
  function resultText(result: any): string {
@@ -34,7 +38,8 @@ export function renderRichToolResult(
34
38
  theme: any,
35
39
  context: any,
36
40
  writeMetadata: WriteExecutionMetadataStore,
37
- displayConfig: ToolDisplayConfig = DEFAULT_TOOL_DISPLAY_CONFIG,
41
+ /** Plain snapshot or live getter — getter lets /ccstyle panel changes repaint existing diffs. */
42
+ displayConfig: DisplayConfigInput = DEFAULT_TOOL_DISPLAY_CONFIG,
38
43
  ): any | undefined {
39
44
  if (options?.isPartial || options?.isError || context?.isError) return undefined;
40
45
  const expanded = options?.expanded === true || context?.expanded === true;
@@ -110,4 +115,5 @@ export {
110
115
  type DiffViewMode,
111
116
  type DiffIndicatorMode,
112
117
  } from "./types.ts";
118
+ export type { DisplayConfigInput } from "./diff-renderer.ts";
113
119
  export { WriteExecutionMetadataStore } from "./write-execution.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -27,6 +27,8 @@
27
27
  ],
28
28
  "pi": {
29
29
  "extensions": [
30
+ "./extensions/pi-aliases.ts",
31
+ "./extensions/pi-startup-header.ts",
30
32
  "./extensions/claude-code-style.ts",
31
33
  "./extensions/context.ts",
32
34
  "./extensions/session-reference.ts",