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.
- package/README.en.md +11 -26
- package/README.md +40 -23
- package/assets/readme/welcome.webp +0 -0
- package/extensions/claude-code-style.ts +554 -519
- package/extensions/context.ts +196 -106
- package/extensions/index.ts +2 -2
- package/extensions/markdown-enhance.ts +208 -0
- package/extensions/pi-aliases.ts +0 -3
- package/extensions/tool-diff/diff-renderer.ts +9 -4
- package/extensions/tool-grouping.ts +2 -1
- package/package.json +10 -14
- package/themes/cc-dark.json +1 -7
- package/themes/cc-light.json +91 -0
- package/extensions/compact-style.ts +0 -1268
- package/extensions/fixed-editor.ts +0 -458
- package/extensions/subagent-notification.ts +0 -121
- package/node_modules/@tifan/pi-fixed-editor/README.md +0 -36
- package/node_modules/@tifan/pi-fixed-editor/package.json +0 -46
- package/node_modules/@tifan/pi-fixed-editor/src/cluster.ts +0 -113
- package/node_modules/@tifan/pi-fixed-editor/src/index.ts +0 -259
- package/node_modules/@tifan/pi-fixed-editor/src/terminal-split.ts +0 -1337
- package/themes/github-dark-default.json +0 -91
package/extensions/context.ts
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Math.max(1,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
163
|
-
);
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
activeTextPreviews--;
|
|
253
|
+
}
|
|
164
254
|
}
|
|
165
255
|
|
|
166
256
|
type SgrMousePacket = {
|
package/extensions/index.ts
CHANGED
|
@@ -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) /  / <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
|
+
}
|
package/extensions/pi-aliases.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.47",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "A Pi productivity suite with Claude Code-style UI,
|
|
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
|
-
"
|
|
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
|
}
|