@smoose/pi-beautify 0.1.3 → 0.1.6
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.md +5 -1
- package/package.json +1 -1
- package/src/index.ts +85 -30
package/README.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A small pi extension for visual polish.
|
|
4
4
|
|
|
5
|
+
## Cleaner markdown code blocks
|
|
6
|
+
|
|
7
|
+
Pi's terminal markdown renderer shows fenced code block borders like ```text. This extension hides those fence lines; plain text fences render as normal prose, while real code remains highlighted and indented.
|
|
8
|
+
|
|
5
9
|
## Clipboard image chips
|
|
6
10
|
|
|
7
|
-
Pi currently pastes clipboard images as long temporary file paths. This extension replaces newly pasted `pi-clipboard-*` paths in the editor with compact chips like `[
|
|
11
|
+
Pi currently pastes clipboard images as long temporary file paths. This extension replaces newly pasted `pi-clipboard-*` paths in the editor with compact chips like `[image1]` for display, then restores those chips to the original file paths before the prompt is sent so the request stays identical to native pi behavior.
|
|
8
12
|
|
|
9
13
|
## Installation
|
|
10
14
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,25 +1,93 @@
|
|
|
1
1
|
import { CustomEditor, type AppKeybinding, type ExtensionAPI, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
3
|
-
import { getKeybindings, matchesKey, truncateToWidth, type AutocompleteProvider, type EditorComponent, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { extname } from "node:path";
|
|
2
|
+
import { getKeybindings, Markdown, matchesKey, truncateToWidth, type AutocompleteProvider, type EditorComponent, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
6
3
|
|
|
7
4
|
interface Attachment {
|
|
8
5
|
token: string;
|
|
9
6
|
path: string;
|
|
10
|
-
mimeType: string;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
const CLIPBOARD_PATH_RE = /(?:[^\s"'`<>]+[\\/])?pi-clipboard-[0-9a-f-]+\.(?:png|jpe?g|webp|gif)/gi;
|
|
14
10
|
const TOKEN_RE = /\[image(\d+)\]/g;
|
|
15
11
|
const TOKEN_LINE_RE = /\[image\d+\]/g;
|
|
12
|
+
const MARKDOWN_PATCH_STATE = Symbol.for("smoose.pi-beautify.markdown.patch");
|
|
13
|
+
const PLAIN_CODE_LANGS = new Set(["", "text", "plain", "plaintext"]);
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
interface MarkdownCodeToken {
|
|
16
|
+
type: "code";
|
|
17
|
+
lang?: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BeautifyMarkdownTheme {
|
|
22
|
+
codeBlock: (text: string) => string;
|
|
23
|
+
codeBlockIndent?: string;
|
|
24
|
+
highlightCode?: (code: string, lang?: string) => string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface MarkdownRuntime {
|
|
28
|
+
theme: BeautifyMarkdownTheme;
|
|
29
|
+
applyDefaultStyle?: (text: string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MarkdownRenderToken = (this: MarkdownRuntime, token: unknown, width: number, nextTokenType?: string, styleContext?: unknown) => string[];
|
|
33
|
+
|
|
34
|
+
interface MarkdownPatchState {
|
|
35
|
+
installed: true;
|
|
36
|
+
original: MarkdownRenderToken;
|
|
37
|
+
renderCodeToken: (instance: MarkdownRuntime, token: MarkdownCodeToken, nextTokenType?: string) => string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PatchedMarkdownPrototype = {
|
|
41
|
+
renderToken?: MarkdownRenderToken;
|
|
42
|
+
[key: symbol]: unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function isMarkdownCodeToken(token: unknown): token is MarkdownCodeToken {
|
|
46
|
+
return typeof token === "object" && token !== null && (token as { type?: unknown }).type === "code";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderCodeTokenWithoutFences(instance: MarkdownRuntime, token: MarkdownCodeToken, nextTokenType?: string): string[] {
|
|
50
|
+
const raw = typeof token.text === "string" ? token.text : "";
|
|
51
|
+
const lang = typeof token.lang === "string" ? token.lang.trim().toLowerCase() : "";
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
|
|
54
|
+
if (PLAIN_CODE_LANGS.has(lang)) {
|
|
55
|
+
for (const line of raw.split("\n")) lines.push(instance.applyDefaultStyle?.(line) ?? line);
|
|
56
|
+
} else if (instance.theme.highlightCode) {
|
|
57
|
+
const indent = instance.theme.codeBlockIndent ?? " ";
|
|
58
|
+
for (const line of instance.theme.highlightCode(raw, token.lang)) lines.push(`${indent}${line}`);
|
|
59
|
+
} else {
|
|
60
|
+
const indent = instance.theme.codeBlockIndent ?? " ";
|
|
61
|
+
for (const line of raw.split("\n")) lines.push(`${indent}${instance.theme.codeBlock(line)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installMarkdownFencePatch(): void {
|
|
69
|
+
const proto = Markdown.prototype as unknown as PatchedMarkdownPrototype;
|
|
70
|
+
const existing = proto[MARKDOWN_PATCH_STATE] as MarkdownPatchState | undefined;
|
|
71
|
+
if (existing?.installed) {
|
|
72
|
+
existing.renderCodeToken = renderCodeTokenWithoutFences;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const original = proto.renderToken;
|
|
77
|
+
if (typeof original !== "function") return;
|
|
78
|
+
|
|
79
|
+
const state: MarkdownPatchState = {
|
|
80
|
+
installed: true,
|
|
81
|
+
original,
|
|
82
|
+
renderCodeToken: renderCodeTokenWithoutFences,
|
|
83
|
+
};
|
|
84
|
+
proto[MARKDOWN_PATCH_STATE] = state;
|
|
85
|
+
|
|
86
|
+
proto.renderToken = function (this: MarkdownRuntime, token: unknown, width: number, nextTokenType?: string, styleContext?: unknown): string[] {
|
|
87
|
+
const current = proto[MARKDOWN_PATCH_STATE] as MarkdownPatchState | undefined;
|
|
88
|
+
if (current && isMarkdownCodeToken(token)) return current.renderCodeToken(this, token, nextTokenType);
|
|
89
|
+
return (current?.original ?? original).call(this, token, width, nextTokenType, styleContext);
|
|
90
|
+
};
|
|
23
91
|
}
|
|
24
92
|
|
|
25
93
|
function imageChip(id: number): string {
|
|
@@ -118,7 +186,7 @@ class ImageTokenController {
|
|
|
118
186
|
while (usedIds.has(id)) id++;
|
|
119
187
|
usedIds.add(id);
|
|
120
188
|
const token = imageChip(id);
|
|
121
|
-
this.attachments.set(token, { token, path
|
|
189
|
+
this.attachments.set(token, { token, path });
|
|
122
190
|
return `${token} `;
|
|
123
191
|
}
|
|
124
192
|
}
|
|
@@ -337,16 +405,9 @@ function collectImageAttachments(text: string, attachments: Map<string, Attachme
|
|
|
337
405
|
return selected;
|
|
338
406
|
}
|
|
339
407
|
|
|
340
|
-
function toImageContent(attachment: Attachment): ImageContent | undefined {
|
|
341
|
-
if (!existsSync(attachment.path)) return undefined;
|
|
342
|
-
return {
|
|
343
|
-
type: "image",
|
|
344
|
-
data: readFileSync(attachment.path).toString("base64"),
|
|
345
|
-
mimeType: attachment.mimeType,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
408
|
export default function piBeautify(pi: ExtensionAPI) {
|
|
409
|
+
installMarkdownFencePatch();
|
|
410
|
+
|
|
350
411
|
const attachments = new Map<string, Attachment>();
|
|
351
412
|
|
|
352
413
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -368,23 +429,17 @@ export default function piBeautify(pi: ExtensionAPI) {
|
|
|
368
429
|
if (ctx.hasUI) ctx.ui.setStatus("pi-beautify", undefined);
|
|
369
430
|
});
|
|
370
431
|
|
|
371
|
-
pi.on("input", async (event
|
|
432
|
+
pi.on("input", async (event) => {
|
|
372
433
|
const selected = collectImageAttachments(event.text, attachments);
|
|
373
434
|
if (selected.length === 0) return { action: "continue" };
|
|
374
435
|
|
|
375
|
-
const
|
|
376
|
-
if (converted.length === 0) {
|
|
377
|
-
if (ctx.hasUI) ctx.ui.notify("pi-beautify: image file disappeared before submit", "warning");
|
|
378
|
-
return { action: "continue" };
|
|
379
|
-
}
|
|
380
|
-
|
|
436
|
+
const text = event.text.replace(TOKEN_RE, (full, id) => attachments.get(imageChip(Number(id)))?.path ?? full);
|
|
381
437
|
for (const attachment of selected) attachments.delete(attachment.token);
|
|
382
438
|
|
|
383
|
-
const text = event.text.replace(TOKEN_RE, (_full, id) => `[attached image ${id}]`);
|
|
384
439
|
return {
|
|
385
440
|
action: "transform",
|
|
386
441
|
text,
|
|
387
|
-
images:
|
|
442
|
+
images: event.images,
|
|
388
443
|
};
|
|
389
444
|
});
|
|
390
445
|
}
|