pi-cc-extensions 0.8.48 → 0.8.49

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.
Files changed (38) hide show
  1. package/README.en.md +17 -36
  2. package/README.md +13 -12
  3. package/assets/readme/welcome.webp +0 -0
  4. package/extensions/config/config.ts +245 -0
  5. package/extensions/config/panel.ts +469 -0
  6. package/extensions/{compact-thinking.ts → feature/compact-thinking.ts} +43 -6
  7. package/extensions/{markdown-enhance.ts → feature/markdown-enhance.ts} +88 -62
  8. package/extensions/{pi-startup-header.ts → feature/pi-startup-header.ts} +129 -56
  9. package/extensions/index.ts +9 -9
  10. package/extensions/renderer/index.ts +890 -0
  11. package/extensions/renderer/mouse-interaction.ts +1344 -0
  12. package/extensions/renderer/show-more-hint.ts +40 -0
  13. package/extensions/{tool-diff → renderer/tool-diff}/diff-renderer.ts +3 -2
  14. package/extensions/{tool-diff → renderer/tool-diff}/line-width-safety.ts +2 -1
  15. package/extensions/{tool-grouping.ts → renderer/tool-grouping.ts} +71 -10
  16. package/extensions/renderer/tool-result.ts +717 -0
  17. package/package.json +1 -1
  18. package/themes/cc-dark.json +2 -2
  19. package/themes/cc-light.json +2 -2
  20. package/extensions/claude-code-style.ts +0 -3348
  21. /package/extensions/{agent-autocomplete.ts → feature/agent-autocomplete.ts} +0 -0
  22. /package/extensions/{context.ts → feature/context.ts} +0 -0
  23. /package/extensions/{pi-aliases.ts → feature/pi-aliases.ts} +0 -0
  24. /package/extensions/{session-reference-core.ts → feature/session-reference-core.ts} +0 -0
  25. /package/extensions/{session-reference.ts → feature/session-reference.ts} +0 -0
  26. /package/extensions/{working-message.ts → feature/working-message.ts} +0 -0
  27. /package/extensions/{tool-diff → renderer/tool-diff}/ATTRIBUTION.md +0 -0
  28. /package/extensions/{tool-diff → renderer/tool-diff}/ansi-utils.ts +0 -0
  29. /package/extensions/{tool-diff → renderer/tool-diff}/diff-presentation.ts +0 -0
  30. /package/extensions/{tool-diff → renderer/tool-diff}/index.ts +0 -0
  31. /package/extensions/{tool-diff → renderer/tool-diff}/render-utils.ts +0 -0
  32. /package/extensions/{tool-diff → renderer/tool-diff}/shiki-highlight.ts +0 -0
  33. /package/extensions/{tool-diff → renderer/tool-diff}/types.ts +0 -0
  34. /package/extensions/{tool-diff → renderer/tool-diff}/write-display-utils.ts +0 -0
  35. /package/extensions/{tool-diff → renderer/tool-diff}/write-execution.ts +0 -0
  36. /package/extensions/{fullscreen-detect.ts → utils/fullscreen-detect.ts} +0 -0
  37. /package/extensions/{tool-loading-icon.ts → utils/tool-loading-icon.ts} +0 -0
  38. /package/extensions/{tool-result-sanitize.ts → utils/tool-result-sanitize.ts} +0 -0
@@ -134,75 +134,101 @@ function linkifyUrls(markdown: string): string {
134
134
  // ============================================================================
135
135
 
136
136
  export default function (pi: ExtensionAPI): void {
137
- // 1. Mermaid 方言渲染(流式跳过,最终渲染时执行)
137
+ // 注意:pi 每个扩展只有一个 markdownTransformer 槽位,多次注册会互相覆盖,
138
+ // 所以三个转换合并为一次注册,内部按序链式执行。
138
139
  pi.registerMarkdownTransformer((markdown, context) => {
139
- const { isStreaming = false, availableWidth } = context ?? {};
140
- if (isStreaming) return markdown;
141
- const lines = markdown.split("\n");
142
- const out: string[] = [];
143
- let i = 0;
144
- while (i < lines.length) {
145
- const line = lines[i];
146
- const fence = line.match(DIAGRAM_FENCE);
147
- if (fence) {
148
- const collected = collectFence(lines, i);
149
- if (collected) {
150
- const { diagram, next } = collected;
151
- // grok-mermaid 需要源码自带类型头,方言头在 fence 标签里时补回去
152
- const label = fence[2];
153
- const src = label.toLowerCase() === "mermaid" ? diagram : `${label}\n${diagram}`;
154
- let art = null;
155
- try {
156
- art = renderMermaid(src);
157
- } catch {
158
- art = null;
159
- }
160
- const width = availableWidth ?? 80;
161
- if (art && art.width <= width) {
162
- // 图行用硬换行(行尾两空格)连接,防止 Markdown 软换行合并行
163
- out.push(art.plain.map(codeSpan).join(" \n"));
164
- } else {
165
- out.push(framedSource(src, width));
166
- }
167
- i = next;
168
- continue;
140
+ const { messageType, isStreaming = false } = context ?? {};
141
+ // 与官方推荐一致:thinking 块与流式中间态不转换
142
+ if (messageType === "assistant-thinking" || isStreaming) return markdown;
143
+ // 1. Mermaid 方言渲染
144
+ markdown = renderDiagrams(markdown, context);
145
+ // 2. GitHub 风格提示框
146
+ markdown = renderAdmonitions(markdown);
147
+ // 3. 裸 URL 转超链接
148
+ return linkifyUrls(markdown);
149
+ });
150
+ }
151
+
152
+ // grok-mermaid 渲染结果缓存:resize/restored 重绘会重复转换,按源码缓存 art。
153
+ // ponytail: 固定上限 50 条,防长期会话膨胀;超出后清空重来。
154
+ const ART_CACHE = new Map<string, ReturnType<typeof renderMermaid>>();
155
+ const ART_CACHE_MAX = 50;
156
+
157
+ /** 获取/计算 mermaid art(带缓存)。 */
158
+ function getArt(src: string): ReturnType<typeof renderMermaid> | null {
159
+ const cached = ART_CACHE.get(src);
160
+ if (cached) return cached;
161
+ let art = null;
162
+ try {
163
+ art = renderMermaid(src);
164
+ } catch {
165
+ art = null;
166
+ }
167
+ if (ART_CACHE.size >= ART_CACHE_MAX) ART_CACHE.clear();
168
+ if (art) ART_CACHE.set(src, art);
169
+ return art;
170
+ }
171
+
172
+ /** Mermaid 方言代码块 → ASCII 图(流式跳过)。 */
173
+ function renderDiagrams(markdown: string, context?: { isStreaming?: boolean; availableWidth?: number }): string {
174
+ const { isStreaming = false, availableWidth } = context ?? {};
175
+ if (isStreaming) return markdown;
176
+ const lines = markdown.split("\n");
177
+ const out: string[] = [];
178
+ let i = 0;
179
+ while (i < lines.length) {
180
+ const line = lines[i];
181
+ const fence = line.match(DIAGRAM_FENCE);
182
+ if (fence) {
183
+ const collected = collectFence(lines, i);
184
+ if (collected) {
185
+ const { diagram, next } = collected;
186
+ // grok-mermaid 需要源码自带类型头,方言头在 fence 标签里时补回去
187
+ const label = fence[2];
188
+ const src = label.toLowerCase() === "mermaid" ? diagram : `${label}\n${diagram}`;
189
+ const art = getArt(src);
190
+ const width = availableWidth ?? 80;
191
+ if (art && art.width <= width) {
192
+ // 图行用硬换行(行尾两空格)连接,防止 Markdown 软换行合并行
193
+ out.push(art.plain.map(codeSpan).join(" \n"));
194
+ } else {
195
+ out.push(framedSource(src, width));
169
196
  }
170
- // 未闭合:保持原文
197
+ i = next;
198
+ continue;
171
199
  }
200
+ // 未闭合:保持原文
201
+ }
202
+ out.push(line);
203
+ i++;
204
+ }
205
+ return out.join("\n");
206
+ }
207
+
208
+ /** GitHub 风格提示框 → 两列表格(跳过代码块)。 */
209
+ function renderAdmonitions(markdown: string): string {
210
+ const lines = markdown.split("\n");
211
+ const out: string[] = [];
212
+ let inFence = false;
213
+ let i = 0;
214
+ while (i < lines.length) {
215
+ const line = lines[i];
216
+ if (/^```/.test(line)) {
217
+ inFence = !inFence;
172
218
  out.push(line);
173
219
  i++;
220
+ continue;
174
221
  }
175
- return out.join("\n");
176
- });
177
-
178
- // 2. GitHub 风格提示框(跳过代码块)
179
- pi.registerMarkdownTransformer((markdown) => {
180
- const lines = markdown.split("\n");
181
- const out: string[] = [];
182
- let inFence = false;
183
- let i = 0;
184
- while (i < lines.length) {
185
- const line = lines[i];
186
- if (/^```/.test(line)) {
187
- inFence = !inFence;
188
- out.push(line);
189
- i++;
222
+ if (!inFence && /^>\s*\[\s*!/i.test(line)) {
223
+ const result = renderAdmonition(lines, i);
224
+ if (result) {
225
+ out.push(...result.output);
226
+ i = result.next;
190
227
  continue;
191
228
  }
192
- if (!inFence && /^>\s*\[\s*!/i.test(line)) {
193
- const result = renderAdmonition(lines, i);
194
- if (result) {
195
- out.push(...result.output);
196
- i = result.next;
197
- continue;
198
- }
199
- }
200
- out.push(line);
201
- i++;
202
229
  }
203
- return out.join("\n");
204
- });
205
-
206
- // 3. 裸 URL 转超链接
207
- pi.registerMarkdownTransformer((markdown) => linkifyUrls(markdown));
230
+ out.push(line);
231
+ i++;
232
+ }
233
+ return out.join("\n");
208
234
  }
@@ -1,4 +1,7 @@
1
+ import { VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
2
+ import { getKeybindings } from "@earendil-works/pi-tui";
1
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { config } from "../config/config.ts";
2
5
 
3
6
  type Rgb = [number, number, number];
4
7
  type StyledPart = {
@@ -10,30 +13,38 @@ const ANSI_RESET = "\x1b[0m";
10
13
  const ANSI_PATTERN =
11
14
  /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
12
15
 
16
+ // 官方 install.sh 静态 logo(4 行原样,短行补尾随空格统一到 8 列)+ 底部空行补到 5 行,
17
+ // 与右侧 tips 行数等高;着色保持现状(accent 渐变)
13
18
  const LOGO_LINES = [
14
- "████████████╗",
15
- "████████████║",
16
- "████╔═══████║",
17
- "████║ ████║",
18
- "████████╬═══████╗",
19
- "████████║ ████║ ",
20
- "████╔═══╝ ████║",
21
- "████║ ████║",
22
- "╚═══╝ ╚═══╝",
19
+ "██████ ",
20
+ "██ ██ ",
21
+ "████ ██",
22
+ "██ ██",
23
+ " ",
23
24
  ];
24
25
 
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 = ".";
26
+ // hero 文案(单行,替换原生 header 的 "Pi can explain..." 默认位置)
27
+ const HERO_PREFIX = "There are many agent harnesses, but this one is ";
28
+ const HERO_HIGHLIGHT = "yours";
29
+ const HERO_SUFFIX = ".";
30
+
31
+ // 左右双栏布局:左侧 logo(5 行),右侧原生提示(5 行)
32
+ const TWO_COL_GAP = 2;
33
+ // 窄于该宽度回退为垂直堆叠(logo + hero)
34
+ const TWO_COL_MIN_WIDTH = 48;
29
35
 
30
36
  const FALLBACK_ACCENT_RGB: Rgb = [80, 160, 255];
31
37
  const LOGO_BLOCK_WIDTH = Math.max(...LOGO_LINES.map((line) => [...line].length));
38
+ // 左栏宽度 = logo 宽,右侧栏从该宽度后开始
39
+ const LEFT_COLUMN_WIDTH = LOGO_BLOCK_WIDTH;
32
40
 
33
41
  const PALETTE_STEPS = 24;
34
42
  const PALETTE_MAX_DARKEN = 0.18;
35
43
  const PALETTE_MAX_LIGHTEN = 0.18;
36
- const LOGO_ROW_PHASE_STEP = 0.12;
44
+ // 行内渐变波长:1/4 全波长(暗→亮单调,避免小字符数行内来回跳变)
45
+ const PALETTE_SPAN = 0.25;
46
+ // 行间相位偏移:小步累加 → 整体左上暗→右下亮的对角渐变
47
+ const LOGO_ROW_PHASE_STEP = 0.08;
37
48
 
38
49
  const ANSI_16_RGB_TABLE: Rgb[] = [
39
50
  [0, 0, 0],
@@ -186,7 +197,8 @@ function renderGradientText(text: string, palette: Rgb[], phase: number): string
186
197
  return characters
187
198
  .map((character, index) => {
188
199
  if (character === " ") return character;
189
- const color = sampleGradientColor(palette, index / span + phase);
200
+ // 行内单调暗→亮(1/4 波长),行间相位偏移 → 对角渐变
201
+ const color = sampleGradientColor(palette, (index / span) * PALETTE_SPAN + phase);
190
202
  return applyTruecolor(color, character);
191
203
  })
192
204
  .join("");
@@ -219,40 +231,71 @@ function renderLogoLines(width: number, palette: Rgb[]): string[] {
219
231
  });
220
232
  }
221
233
 
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
- );
234
+ // ---- 右侧:原生默认 header 文本(对齐 pi 内置 startup header,按键文本随用户 keybindings 动态渲染) ----
233
235
 
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
- );
236
+ function formatKeyPart(part: string): string {
237
+ // 与 pi 内置 keybinding-hints 一致:macOS 上 alt 显示为 option
238
+ return process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
239
+ }
240
+
241
+ function formatKeyText(key: string): string {
242
+ return key
243
+ .split("/")
244
+ .map((part) => part.split("+").map(formatKeyPart).join("+"))
245
+ .join("/");
246
+ }
247
+
248
+ function keyText(keybinding: AppKeybinding): string {
249
+ const keys = getKeybindings().getKeys(keybinding);
250
+ return keys.length === 0 ? "" : formatKeyText(keys.join("/"));
251
+ }
252
+
253
+ function renderNativeLines(theme: {
254
+ fg(name: string, text: string): string;
255
+ bold(text: string): string;
256
+ }): string[] {
257
+ // 颜色方案照抄 pi 内置 header:按键 dim、描述 muted、分隔符 muted、版本 dim
258
+ const hint = (keybinding: AppKeybinding, description: string) =>
259
+ theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`);
260
+ const rawHint = (key: string, description: string) =>
261
+ theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`);
262
+
263
+ const logo = theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${VERSION}`);
264
+ const compact = [
265
+ hint("app.interrupt", "interrupt"),
266
+ rawHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
267
+ rawHint("/", "commands"),
268
+ rawHint("!", "bash"),
269
+ hint("app.tools.expand", "more"),
270
+ ].join(theme.fg("muted", " · "));
271
+
272
+ return [
273
+ logo,
274
+ compact,
275
+ theme.fg(
276
+ "dim",
277
+ `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`,
278
+ ),
279
+ "",
280
+ // hero 文案替换原生 "Pi can explain..." 行位置
281
+ renderHeroParts(theme)
282
+ .map((part) => part.styled)
283
+ .join(""),
284
+ ];
285
+ }
251
286
 
252
- return [line1, line2];
287
+ function renderHeroParts(theme: {
288
+ fg(name: string, text: string): string;
289
+ bold(text: string): string;
290
+ }): StyledPart[] {
291
+ return [
292
+ { raw: HERO_PREFIX, styled: theme.fg("accent", HERO_PREFIX) },
293
+ { raw: HERO_HIGHLIGHT, styled: theme.bold(theme.fg("mdLink", HERO_HIGHLIGHT)) },
294
+ { raw: HERO_SUFFIX, styled: theme.fg("accent", HERO_SUFFIX) },
295
+ ];
253
296
  }
254
297
 
255
- function renderHeaderLines(
298
+ export function renderHeaderLines(
256
299
  width: number,
257
300
  theme: {
258
301
  getFgAnsi(name: string): string;
@@ -262,22 +305,52 @@ function renderHeaderLines(
262
305
  ): string[] {
263
306
  const accentRgb = resolveAccentRgb(theme);
264
307
  const palette = buildAccentPalette(accentRgb);
265
- const logoLines = renderLogoLines(width, palette);
266
- const taglineLines = renderTaglineLines(width, theme);
267
308
 
268
- return ["", ...logoLines, "", ...taglineLines, ""].map((line) => fitLineToWidth(line, width));
309
+ if (width < TWO_COL_MIN_WIDTH) {
310
+ // 窄屏回退:logo + hero 单行垂直堆叠居中
311
+ const logoLines = renderLogoLines(width, palette);
312
+ const heroLine = createCenteredStyledLine(renderHeroParts(theme), width);
313
+ return ["", ...logoLines, "", heroLine, ""].map((line) => fitLineToWidth(line, width));
314
+ }
315
+
316
+ // 双栏:左官方 logo(5 行) 右原生提示(5 行),同高并排,gap 分隔不交叉
317
+ const leftLines = renderLogoLines(LEFT_COLUMN_WIDTH, palette);
318
+ const rightLines = renderNativeLines(theme);
319
+ const rightWidth = width - LEFT_COLUMN_WIDTH - TWO_COL_GAP;
320
+ const padTop = Math.floor((rightLines.length - leftLines.length) / 2);
321
+ const paddedLeft = [
322
+ ...Array.from({ length: padTop }, () => ""),
323
+ ...leftLines,
324
+ ...Array.from({ length: rightLines.length - leftLines.length - padTop }, () => ""),
325
+ ];
326
+
327
+ return paddedLeft.map(
328
+ (line, index) => `${line}${" ".repeat(TWO_COL_GAP)}${fitLineToWidth(rightLines[index] ?? "", rightWidth)}`,
329
+ );
330
+ }
331
+
332
+ /**
333
+ * 按配置应用启动头:on → 自定义 header;off → 恢复官方默认 header。
334
+ * 导出供 /ccstyle 面板在切换开关时实时重应用。
335
+ */
336
+ export function applyStartupHeader(ctx: any): void {
337
+ if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function") return;
338
+ if (!config.showStartupHeader) {
339
+ // 恢复官方内置 header(logo + 快捷键提示 + onboarding)。
340
+ ctx.ui.setHeader(undefined);
341
+ return;
342
+ }
343
+ ctx.ui.setHeader((_tui, theme) => ({
344
+ render(width: number): string[] {
345
+ return renderHeaderLines(width, theme);
346
+ },
347
+ invalidate() {},
348
+ }));
269
349
  }
270
350
 
271
351
  export default function piStartupHeader(pi: ExtensionAPI) {
272
352
  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
- }));
353
+ applyStartupHeader(ctx);
281
354
  });
282
355
 
283
356
  pi.on("session_shutdown", async (_event, ctx) => {
@@ -1,13 +1,13 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import agentAutocomplete from "./agent-autocomplete.ts";
3
- import claudeCodeStyle, { getCompactThinkingConfig } from "./claude-code-style.ts";
4
- import { installCompactThinking, type CompactThinkingController } from "./compact-thinking.ts";
5
- import context from "./context.ts";
6
- import markdownEnhance from "./markdown-enhance.ts";
7
- import piAliases from "./pi-aliases.ts";
8
- import piStartupHeader from "./pi-startup-header.ts";
9
- import sessionReference from "./session-reference.ts";
10
- import workingMessage from "./working-message.ts";
2
+ import agentAutocomplete from "./feature/agent-autocomplete.ts";
3
+ import claudeCodeStyle, { getCompactThinkingConfig } from "./renderer/index.ts";
4
+ import { installCompactThinking, type CompactThinkingController } from "./feature/compact-thinking.ts";
5
+ import context from "./feature/context.ts";
6
+ import markdownEnhance from "./feature/markdown-enhance.ts";
7
+ import piAliases from "./feature/pi-aliases.ts";
8
+ import piStartupHeader from "./feature/pi-startup-header.ts";
9
+ import sessionReference from "./feature/session-reference.ts";
10
+ import workingMessage from "./feature/working-message.ts";
11
11
 
12
12
  export default function (pi: ExtensionAPI): void {
13
13
  piAliases(pi);