pi-cc-extensions 0.8.45 → 0.8.47

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.
@@ -43,124 +43,214 @@ export function escCloseHitbox(bounds: DialogBounds): {
43
43
  };
44
44
  }
45
45
 
46
+ let activeTextPreviews = 0;
47
+
48
+ /** fullscreen 输入包装用于把鼠标事件继续传给当前文本预览 overlay。 */
49
+ export function hasActiveTextPreview(): boolean {
50
+ return activeTextPreviews > 0;
51
+ }
52
+
46
53
  export async function showTextPreview(
47
54
  ctx: Pick<ExtensionCommandContext, "ui">,
48
55
  title: string,
49
56
  rawContent: string,
50
57
  ): Promise<void> {
51
58
  const content = normalizePreviewText(rawContent);
52
- await ctx.ui.custom(
53
- (tui, theme, _keybindings, done) => {
54
- let scrollOffset = 0;
55
- let pageSize = 1;
56
- let totalLines = 1;
57
- let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
58
- const markdownView = new Markdown(content, 0, 0, getMarkdownTheme());
59
-
60
- const scrollTo = (nextOffset: number): void => {
61
- scrollOffset = Math.max(0, Math.min(nextOffset, Math.max(0, totalLines - pageSize)));
62
- tui.requestRender();
63
- };
64
-
65
- return {
66
- invalidate() {
67
- markdownView.invalidate();
68
- },
69
- handleInput(data: string) {
70
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
71
- done(undefined);
72
- return;
73
- }
74
- if (matchesKey(data, Key.up)) scrollTo(scrollOffset - 1);
75
- else if (matchesKey(data, Key.down)) scrollTo(scrollOffset + 1);
76
- else if (matchesKey(data, "pageUp")) scrollTo(scrollOffset - pageSize);
77
- else if (matchesKey(data, "pageDown")) scrollTo(scrollOffset + pageSize);
78
- else if (matchesKey(data, Key.home)) scrollTo(0);
79
- else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
80
- else {
81
- const mouse = parseSgrMousePacket(data);
82
- if (mouse?.final !== "M") return;
83
- const button = mouseBaseButton(mouse.code);
84
- if (button === 0 && (mouse.code & 32) === 0) {
85
- if (
59
+ activeTextPreviews++;
60
+ try {
61
+ await ctx.ui.custom(
62
+ (tui, theme, _keybindings, done) => {
63
+ let scrollOffset = 0;
64
+ let pageSize = 1;
65
+ let totalLines = 1;
66
+ let escHovered = false;
67
+ let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
68
+ let scrollbarHitbox:
69
+ | {
70
+ col: number;
71
+ startRow: number;
72
+ endRow: number;
73
+ thumbStart: number;
74
+ thumbSize: number;
75
+ maxOffset: number;
76
+ }
77
+ | undefined;
78
+ let scrollbarDragOffset: number | null = null;
79
+ const markdownView = new Markdown(content, 0, 0, getMarkdownTheme());
80
+
81
+ const scrollTo = (nextOffset: number): void => {
82
+ const next = Math.max(0, Math.min(nextOffset, Math.max(0, totalLines - pageSize)));
83
+ if (next === scrollOffset) return;
84
+ scrollOffset = next;
85
+ tui.requestRender();
86
+ };
87
+
88
+ const setEscHovered = (hovered: boolean): void => {
89
+ if (hovered === escHovered) return;
90
+ escHovered = hovered;
91
+ tui.requestRender();
92
+ };
93
+
94
+ const dragScrollbarTo = (mouseRow: number): void => {
95
+ if (!scrollbarHitbox || scrollbarDragOffset === null) return;
96
+ const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
97
+ const maxThumbStart = Math.max(0, trackSize - scrollbarHitbox.thumbSize);
98
+ const thumbStart = Math.max(
99
+ 0,
100
+ Math.min(mouseRow - scrollbarHitbox.startRow - scrollbarDragOffset, maxThumbStart),
101
+ );
102
+ const nextOffset =
103
+ maxThumbStart > 0
104
+ ? Math.round((thumbStart / maxThumbStart) * scrollbarHitbox.maxOffset)
105
+ : 0;
106
+ scrollTo(nextOffset);
107
+ };
108
+
109
+ return {
110
+ invalidate() {
111
+ markdownView.invalidate();
112
+ },
113
+ handleInput(data: string) {
114
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
115
+ done(undefined);
116
+ return;
117
+ }
118
+ if (matchesKey(data, Key.up)) scrollTo(scrollOffset - 1);
119
+ else if (matchesKey(data, Key.down)) scrollTo(scrollOffset + 1);
120
+ else if (matchesKey(data, "pageUp")) scrollTo(scrollOffset - pageSize);
121
+ else if (matchesKey(data, "pageDown")) scrollTo(scrollOffset + pageSize);
122
+ else if (matchesKey(data, Key.home)) scrollTo(0);
123
+ else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
124
+ else {
125
+ const mouse = parseSgrMousePacket(data);
126
+ if (!mouse) return;
127
+ if (mouse.final === "m") {
128
+ scrollbarDragOffset = null;
129
+ return;
130
+ }
131
+ const overEsc = Boolean(
86
132
  escHitbox &&
87
- mouse.row === escHitbox.row &&
88
- mouse.col >= escHitbox.startCol &&
89
- mouse.col <= escHitbox.endCol
90
- ) {
91
- done(undefined);
133
+ mouse.row === escHitbox.row &&
134
+ mouse.col >= escHitbox.startCol &&
135
+ mouse.col <= escHitbox.endCol,
136
+ );
137
+ setEscHovered(overEsc);
138
+ const button = mouseBaseButton(mouse.code);
139
+ const motion = (mouse.code & 32) !== 0;
140
+ if (button === 0 && !motion) {
141
+ if (overEsc) {
142
+ done(undefined);
143
+ return;
144
+ }
145
+ if (
146
+ scrollbarHitbox &&
147
+ mouse.col === scrollbarHitbox.col &&
148
+ mouse.row >= scrollbarHitbox.startRow &&
149
+ mouse.row <= scrollbarHitbox.endRow
150
+ ) {
151
+ const trackRow = mouse.row - scrollbarHitbox.startRow;
152
+ const inThumb =
153
+ trackRow >= scrollbarHitbox.thumbStart &&
154
+ trackRow < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
155
+ scrollbarDragOffset = inThumb
156
+ ? trackRow - scrollbarHitbox.thumbStart
157
+ : Math.floor(scrollbarHitbox.thumbSize / 2);
158
+ dragScrollbarTo(mouse.row);
159
+ return;
160
+ }
161
+ }
162
+ if (motion && scrollbarDragOffset !== null) {
163
+ dragScrollbarTo(mouse.row);
92
164
  return;
93
165
  }
166
+ if (button === 64) scrollTo(scrollOffset - 3);
167
+ else if (button === 65) scrollTo(scrollOffset + 3);
94
168
  }
95
- if (button === 64) scrollTo(scrollOffset - 3);
96
- else if (button === 65) scrollTo(scrollOffset + 3);
97
- }
98
- },
99
- render(width: number) {
100
- const inner = Math.max(1, width - 2);
101
- const escWidth = visibleWidth("[esc]");
102
- const bodyInner = Math.max(1, inner - 1);
103
- const bodyWidth = Math.max(1, bodyInner - 1);
104
- const terminalHeight = Math.max(1, tui.terminal.rows);
105
- const availableHeight = Math.max(1, terminalHeight - 4);
106
- const viewportHeight = Math.min(
107
- 30,
108
- Math.max(1, Math.floor(terminalHeight * 0.8)),
109
- availableHeight,
110
- );
111
- pageSize = Math.max(1, viewportHeight - 6);
112
- const wrapped = markdownView.render(bodyWidth);
113
- totalLines = wrapped.length;
114
- scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
115
- // Centered overlay with margin 2: mirror TUI resolveOverlayLayout for anchor "center".
116
- const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
117
- const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
118
- escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
119
- const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
120
- const border = (text: string) => theme.fg("border", text);
121
- const padLine = (text: string, lineWidth = inner): string => {
122
- const truncated = truncateToWidth(text, lineWidth, "…");
123
- return truncated + " ".repeat(Math.max(0, lineWidth - visibleWidth(truncated)));
124
- };
125
- const scrollable = totalLines > pageSize;
126
- const thumbSize = scrollable
127
- ? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
128
- : 0;
129
- const maxScrollOffset = Math.max(0, totalLines - pageSize);
130
- const thumbStart =
131
- scrollable && maxScrollOffset > 0
132
- ? Math.round((scrollOffset / maxScrollOffset) * (pageSize - thumbSize))
169
+ },
170
+ render(width: number) {
171
+ const inner = Math.max(1, width - 2);
172
+ const escWidth = visibleWidth("[esc]");
173
+ const bodyInner = Math.max(1, inner - 1);
174
+ const bodyWidth = Math.max(1, bodyInner - 1);
175
+ const terminalHeight = Math.max(1, tui.terminal.rows);
176
+ const availableHeight = Math.max(1, terminalHeight - 4);
177
+ const viewportHeight = Math.min(
178
+ 30,
179
+ Math.max(1, Math.floor(terminalHeight * 0.8)),
180
+ availableHeight,
181
+ );
182
+ pageSize = Math.max(1, viewportHeight - 6);
183
+ const wrapped = markdownView.render(bodyWidth);
184
+ totalLines = wrapped.length;
185
+ scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
186
+ // Centered overlay with margin 2: mirror TUI resolveOverlayLayout for anchor "center".
187
+ const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
188
+ const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
189
+ escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
190
+ const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
191
+ const border = (text: string) => theme.fg("border", text);
192
+ const padLine = (text: string, lineWidth = inner): string => {
193
+ const truncated = truncateToWidth(text, lineWidth, "…");
194
+ return truncated + " ".repeat(Math.max(0, lineWidth - visibleWidth(truncated)));
195
+ };
196
+ const scrollable = totalLines > pageSize;
197
+ const thumbSize = scrollable
198
+ ? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
133
199
  : 0;
134
- const scrollbar = (row: number): string => {
135
- if (!scrollable) return " ";
136
- const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
137
- return theme.fg(inThumb ? "accent" : "borderMuted", inThumb ? "█" : "│");
138
- };
139
- const bodyRows = Array.from({ length: pageSize }, (_, row) => {
140
- const line = visible[row] ?? "";
141
- return `${border("│")}${padLine(` ${line}`, bodyInner)}${scrollbar(row)}${border("│")}`;
142
- });
143
- const start = totalLines === 0 ? 0 : scrollOffset + 1;
144
- const end = Math.min(totalLines, scrollOffset + pageSize);
145
- const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
146
-
147
- return [
148
- border(`╭${"─".repeat(inner)}╮`),
149
- `${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - escWidth)}${theme.fg("muted", "[esc]")}${border("│")}`,
150
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
151
- ...bodyRows,
152
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
153
- `${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
154
- border(`╰${"─".repeat(inner)}╯`),
155
- ];
200
+ const maxScrollOffset = Math.max(0, totalLines - pageSize);
201
+ const thumbStart =
202
+ scrollable && maxScrollOffset > 0
203
+ ? Math.round((scrollOffset / maxScrollOffset) * (pageSize - thumbSize))
204
+ : 0;
205
+ scrollbarHitbox = scrollable
206
+ ? {
207
+ col: overlayLeft + width - 1,
208
+ startRow: overlayTop + 4,
209
+ endRow: overlayTop + 3 + pageSize,
210
+ thumbStart,
211
+ thumbSize,
212
+ maxOffset: maxScrollOffset,
213
+ }
214
+ : undefined;
215
+ const scrollbar = (row: number): string => {
216
+ if (!scrollable) return " ";
217
+ const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
218
+ return theme.fg(inThumb ? "accent" : "borderMuted", inThumb ? "█" : "│");
219
+ };
220
+ const bodyRows = Array.from({ length: pageSize }, (_, row) => {
221
+ const line = visible[row] ?? "";
222
+ return `${border("│")}${padLine(` ${line}`, bodyInner)}${scrollbar(row)}${border("│")}`;
223
+ });
224
+ const start = totalLines === 0 ? 0 : scrollOffset + 1;
225
+ const end = Math.min(totalLines, scrollOffset + pageSize);
226
+ const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
227
+
228
+ return [
229
+ border(`╭${"─".repeat(inner)}╮`),
230
+ `${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
231
+ `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
232
+ ...bodyRows,
233
+ `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
234
+ `${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
235
+ border(`╰${"─".repeat(inner)}╯`),
236
+ ];
237
+ },
238
+ };
239
+ },
240
+ {
241
+ overlay: true,
242
+ overlayOptions: {
243
+ anchor: "center",
244
+ width: "85%",
245
+ minWidth: 50,
246
+ maxHeight: "80%",
247
+ margin: 2,
156
248
  },
157
- };
158
- },
159
- {
160
- overlay: true,
161
- overlayOptions: { anchor: "center", width: "85%", minWidth: 50, maxHeight: "80%", margin: 2 },
162
- },
163
- );
249
+ },
250
+ );
251
+ } finally {
252
+ activeTextPreviews--;
253
+ }
164
254
  }
165
255
 
166
256
  type SgrMousePacket = {
@@ -3,15 +3,16 @@ import agentAutocomplete from "./agent-autocomplete.ts";
3
3
  import claudeCodeStyle, { getCompactThinkingConfig } from "./claude-code-style.ts";
4
4
  import { installCompactThinking, type CompactThinkingController } from "./compact-thinking.ts";
5
5
  import context from "./context.ts";
6
+ import markdownEnhance from "./markdown-enhance.ts";
6
7
  import piAliases from "./pi-aliases.ts";
7
8
  import piStartupHeader from "./pi-startup-header.ts";
8
9
  import sessionReference from "./session-reference.ts";
9
- import subagentNotification from "./subagent-notification.ts";
10
10
  import workingMessage from "./working-message.ts";
11
11
 
12
12
  export default function (pi: ExtensionAPI): void {
13
13
  piAliases(pi);
14
14
  piStartupHeader(pi);
15
+ markdownEnhance(pi);
15
16
  let compactThinking: CompactThinkingController | undefined;
16
17
  const compactThinkingBridge: CompactThinkingController = {
17
18
  updateConfig(next) {
@@ -20,7 +21,6 @@ export default function (pi: ExtensionAPI): void {
20
21
  };
21
22
  claudeCodeStyle(pi, undefined, compactThinkingBridge);
22
23
  compactThinking = installCompactThinking(pi, getCompactThinkingConfig());
23
- subagentNotification(pi);
24
24
  workingMessage(pi);
25
25
  context(pi);
26
26
  sessionReference(pi);
@@ -0,0 +1,208 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { render as renderMermaid, sourceBox } from "grok-mermaid";
3
+
4
+ // ============================================================================
5
+ // Mermaid 方言渲染
6
+ // ============================================================================
7
+
8
+ // 内置 mermaid transformer 只认 ```mermaid,这里补上 grok-mermaid 支持的其他方言
9
+ const DIAGRAM_FENCE =
10
+ /^(`{3,})\s*(mermaid|statediagram|statediagram-v2|classdiagram|classdiagram-v2|erdiagram|sequencediagram)\s*$/i;
11
+ const FENCE_OPEN = /^(`{3,})/;
12
+ const FENCE_CLOSE = /^`{3,}\s*$/;
13
+
14
+ /** 把一行图内容包成行内代码(保持空格与框线字符对齐)。 */
15
+ function codeSpan(line: string): string {
16
+ const content = line || "\u00a0";
17
+ const longestRun = Math.max(0, ...Array.from(content.matchAll(/`+/g), (m) => m[0].length));
18
+ const fence = "`".repeat(longestRun + 1);
19
+ const padding = content.startsWith("`") || content.endsWith("`") ? " " : "";
20
+ return `${fence}${padding}${content}${padding}${fence}`;
21
+ }
22
+
23
+ /** 渲染失败或图宽超出终端宽度时,把源码包进带标题的框里显示。 */
24
+ function framedSource(src: string, width: number): string {
25
+ const box = sourceBox(src, Math.max(8, width - 2));
26
+ return `\`\`\`\n${box.plain.join("\n")}\n\`\`\``;
27
+ }
28
+
29
+ /** 解析 ```diagram 代码块,返回 { 源码, 块内行数 };未闭合返回 null。 */
30
+ function collectFence(lines: string[], i: number): { diagram: string; next: number } | null {
31
+ const open = lines[i].match(FENCE_OPEN)?.[0] ?? "```";
32
+ const src: string[] = [];
33
+ let j = i + 1;
34
+ while (j < lines.length) {
35
+ if (lines[j].match(FENCE_CLOSE) && lines[j].startsWith(open)) {
36
+ return { diagram: src.join("\n"), next: j + 1 };
37
+ }
38
+ src.push(lines[j]);
39
+ j++;
40
+ }
41
+ return null; // 未闭合:放弃转换,保持原文
42
+ }
43
+
44
+ // ============================================================================
45
+ // GitHub 风格提示框(admonition)
46
+ // ============================================================================
47
+
48
+ const ADMONITION = /^>\s*\[\s*!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\s*\]\s*(.*)$/i;
49
+ const ADMONITION_STYLE: Record<string, { icon: string; label: string }> = {
50
+ NOTE: { icon: "💡", label: "NOTE" },
51
+ TIP: { icon: "✅", label: "TIP" },
52
+ IMPORTANT: { icon: "❗", label: "IMPORTANT" },
53
+ WARNING: { icon: "⚠️", label: "WARNING" },
54
+ CAUTION: { icon: "⚠️", label: "CAUTION" },
55
+ };
56
+
57
+ /**
58
+ * 把 > [!TYPE] 及其后续 > 行转成两列表格(图标标签列 + 内容列),视觉上成提示框。
59
+ * 表格 cell 内不支持换行(<br> 显示为字面量),多行内容用空格合并。
60
+ * 返回 null 表示该行不是提示框。
61
+ */
62
+ function renderAdmonition(lines: string[], i: number): { output: string[]; next: number } | null {
63
+ const m = lines[i].match(ADMONITION);
64
+ if (!m) return null;
65
+ const style = ADMONITION_STYLE[m[1].toUpperCase()];
66
+ const body: string[] = [m[2]];
67
+ let j = i + 1;
68
+ while (j < lines.length && /^>\s?/.test(lines[j]) && !ADMONITION.test(lines[j])) {
69
+ body.push(lines[j].replace(/^>\s?/, ""));
70
+ j++;
71
+ }
72
+ while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
73
+ // 内容里的 | 转义,避免拆出额外表格列
74
+ const content = body.join(" ").trim().replace(/\|/g, "\\|");
75
+ // 表格后补空行,防止紧接的段落被解析为表格数据行
76
+ return {
77
+ output: [`| ${style.icon} ${style.label} | ${content} |`, "|---|---|", ""],
78
+ next: j,
79
+ };
80
+ }
81
+
82
+ // ============================================================================
83
+ // 裸 URL 转可点击超链接
84
+ // ============================================================================
85
+
86
+ const URL_RE = /(?<!<)(?<!\]\()https?:\/\/[^\s<>'"|,。;:!?、」』】]+/g;
87
+ const TRIM_URL_RE = /[.,;:!?】」』"'》]+$/;
88
+
89
+ /** 去掉 URL 尾部标点;括号按平衡保留(如 Wikipedia 链接含括号)。 */
90
+ function trimUrl(url: string): string {
91
+ let t = url.replace(TRIM_URL_RE, "");
92
+ while (t.endsWith(")") && (t.match(/\(/g)?.length ?? 0) < (t.match(/\)/g)?.length ?? 0)) {
93
+ t = t.slice(0, -1);
94
+ }
95
+ return t;
96
+ }
97
+
98
+ /**
99
+ * 把代码块和行内代码外的裸 URL 转成 markdown 链接。
100
+ * 已有的 [text](url) / ![alt](url) / <url> 自动链接用 lookbehind 排除。
101
+ */
102
+ function linkifyUrls(markdown: string): string {
103
+ const lines = markdown.split("\n");
104
+ const out: string[] = [];
105
+ let inFence = false;
106
+ for (const line of lines) {
107
+ if (/^```/.test(line)) {
108
+ inFence = !inFence;
109
+ out.push(line);
110
+ continue;
111
+ }
112
+ if (inFence) {
113
+ out.push(line);
114
+ continue;
115
+ }
116
+ // 行内代码(`...`)按反引号分片,只处理偶数片(代码外)
117
+ const parts = line.split("`");
118
+ for (let p = 0; p < parts.length; p++) {
119
+ if (p % 2 === 0) {
120
+ parts[p] = parts[p].replace(URL_RE, (url) => {
121
+ const trimmed = trimUrl(url);
122
+ if (trimmed.length === 0) return url;
123
+ return `[${trimmed}](${trimmed})`;
124
+ });
125
+ }
126
+ }
127
+ out.push(parts.join("`"));
128
+ }
129
+ return out.join("\n");
130
+ }
131
+
132
+ // ============================================================================
133
+ // 注册
134
+ // ============================================================================
135
+
136
+ export default function (pi: ExtensionAPI): void {
137
+ // 1. Mermaid 方言渲染(流式跳过,最终渲染时执行)
138
+ 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;
169
+ }
170
+ // 未闭合:保持原文
171
+ }
172
+ out.push(line);
173
+ i++;
174
+ }
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++;
190
+ continue;
191
+ }
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
+ }
203
+ return out.join("\n");
204
+ });
205
+
206
+ // 3. 裸 URL 转超链接
207
+ pi.registerMarkdownTransformer((markdown) => linkifyUrls(markdown));
208
+ }
@@ -5,9 +5,6 @@
5
5
  * /clear → /new (start a new session)
6
6
  * /exit → /quit (exit pi)
7
7
  *
8
- * Install:
9
- * pi install npm:pi-aliases
10
- * pi install git:github.com/xRyul/pi-aliases
11
8
  */
12
9
 
13
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -2319,13 +2319,18 @@ function applyLineLimit(
2319
2319
  DIFF_WIDTH_OPS,
2320
2320
  );
2321
2321
 
2322
+ const clickLabel = "click to show more";
2323
+ const clickIndex = hintText.lastIndexOf(clickLabel);
2324
+ const styledHint =
2325
+ hovered && !expanded && clickIndex >= 0
2326
+ ? theme.fg("muted", hintText.slice(0, clickIndex)) +
2327
+ theme.fg("text", clickLabel) +
2328
+ theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
2329
+ : theme.fg(expanded ? "warning" : "muted", hintText);
2322
2330
  return [
2323
2331
  ...shown.map((row) => clampDiffLineToWidth(row.text, width)),
2324
2332
  renderDiffSpacerLine(width),
2325
- clampDiffLineToWidth(
2326
- theme.fg(expanded ? "warning" : hovered ? "text" : "muted", hintText),
2327
- width,
2328
- ),
2333
+ clampDiffLineToWidth(styledHint, width),
2329
2334
  ];
2330
2335
  }
2331
2336
 
@@ -311,7 +311,8 @@ export class ToolGroupComponent extends Container {
311
311
  if (overall === "pending") scheduleGroupAnimation(this.patch);
312
312
  const overallColor = overall === "pending" ? "accent" : overall;
313
313
  const nameList = names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
314
- const hint = fg(this.hintHovered ? "text" : "dim", "• click to show more");
314
+ // 圆点保持 dim;hover 只高亮可点击文字。
315
+ const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", "click to show more")}`;
315
316
  const lines = [
316
317
  "",
317
318
  truncateToWidth(
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.45",
3
+ "version": "0.8.47",
4
4
  "license": "MIT",
5
- "description": "A Pi productivity suite with Claude Code-style UI, fixed editor, context inspection, and agent/session references",
5
+ "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "pi-package",
9
9
  "pi-extension",
10
10
  "pi-coding-agent",
11
- "claude-code",
12
- "fixed-editor"
11
+ "claude-code"
13
12
  ],
14
13
  "homepage": "https://github.com/minuque/pi-cc-extensions#readme",
15
14
  "repository": {
@@ -32,9 +31,11 @@
32
31
  ],
33
32
  "themes": [
34
33
  "./themes/github-dark-default.json",
35
- "./themes/cc-dark.json"
34
+ "./themes/cc-dark.json",
35
+ "./themes/cc-light.json"
36
36
  ],
37
- "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp"
37
+ "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp",
38
+ "video": "https://github.com/user-attachments/assets/debf76ea-a9dc-4f34-977f-aae344a9a7f8"
38
39
  },
39
40
  "scripts": {
40
41
  "test": "node --test tests/**/*.test.ts",
@@ -48,12 +49,10 @@
48
49
  },
49
50
  "dependencies": {
50
51
  "@shikijs/cli": "^4.0.2",
51
- "@tifan/pi-fixed-editor": "^0.2.5",
52
+ "grok-mermaid": "^0.2.2",
52
53
  "jiti": "^2.7.0"
53
54
  },
54
- "bundledDependencies": [
55
- "@tifan/pi-fixed-editor"
56
- ],
55
+ "bundledDependencies": [],
57
56
  "peerDependencies": {
58
57
  "@earendil-works/pi-ai": "^0.84.0",
59
58
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -65,8 +64,5 @@
65
64
  "devDependencies": {
66
65
  "@biomejs/biome": "2.5.5",
67
66
  "@types/node": "^26.1.1"
68
- },
69
- "bundleDependencies": [
70
- "@tifan/pi-fixed-editor"
71
- ]
67
+ }
72
68
  }