@smoose/pi-beautify 0.1.4 → 0.1.7
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 +6 -0
- package/package.json +1 -1
- package/src/index.ts +157 -2
package/README.md
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
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
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
|
|
|
13
|
+
On macOS, if Ctrl+V sees Finder file URLs on the clipboard, the extension inserts those original file paths first and skips pi's image-reader path. This avoids Finder-copied files being saved as PNG file icons.
|
|
14
|
+
|
|
9
15
|
## Installation
|
|
10
16
|
|
|
11
17
|
```bash
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
import { CustomEditor, type AppKeybinding, type ExtensionAPI, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getKeybindings, matchesKey, truncateToWidth, type AutocompleteProvider, type EditorComponent, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { getKeybindings, Markdown, matchesKey, truncateToWidth, type AutocompleteProvider, type EditorComponent, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
3
5
|
|
|
4
6
|
interface Attachment {
|
|
5
7
|
token: string;
|
|
@@ -9,6 +11,103 @@ interface Attachment {
|
|
|
9
11
|
const CLIPBOARD_PATH_RE = /(?:[^\s"'`<>]+[\\/])?pi-clipboard-[0-9a-f-]+\.(?:png|jpe?g|webp|gif)/gi;
|
|
10
12
|
const TOKEN_RE = /\[image(\d+)\]/g;
|
|
11
13
|
const TOKEN_LINE_RE = /\[image\d+\]/g;
|
|
14
|
+
const IMAGE_FILE_RE = /\.(?:png|jpe?g|webp|gif)$/i;
|
|
15
|
+
const MARKDOWN_PATCH_STATE = Symbol.for("smoose.pi-beautify.markdown.patch");
|
|
16
|
+
const PLAIN_CODE_LANGS = new Set(["", "text", "plain", "plaintext"]);
|
|
17
|
+
const MACOS_CLIPBOARD_FILE_PATHS_SCRIPT = `
|
|
18
|
+
ObjC.import('AppKit');
|
|
19
|
+
ObjC.import('Foundation');
|
|
20
|
+
const pb = $.NSPasteboard.generalPasteboard;
|
|
21
|
+
const classes = $.NSArray.arrayWithObject($.NSURL);
|
|
22
|
+
const options = $.NSDictionary.dictionaryWithObjectForKey($.NSNumber.numberWithBool(true), $.NSPasteboardURLReadingFileURLsOnlyKey);
|
|
23
|
+
const urls = pb.readObjectsForClassesOptions(classes, options);
|
|
24
|
+
const paths = [];
|
|
25
|
+
if (urls) {
|
|
26
|
+
for (let i = 0; i < urls.count; i++) {
|
|
27
|
+
const url = urls.objectAtIndex(i);
|
|
28
|
+
if (url.isFileURL) paths.push(ObjC.unwrap(url.path));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
JSON.stringify(paths);
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
interface MarkdownCodeToken {
|
|
35
|
+
type: "code";
|
|
36
|
+
lang?: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface BeautifyMarkdownTheme {
|
|
41
|
+
codeBlock: (text: string) => string;
|
|
42
|
+
codeBlockIndent?: string;
|
|
43
|
+
highlightCode?: (code: string, lang?: string) => string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface MarkdownRuntime {
|
|
47
|
+
theme: BeautifyMarkdownTheme;
|
|
48
|
+
applyDefaultStyle?: (text: string) => string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type MarkdownRenderToken = (this: MarkdownRuntime, token: unknown, width: number, nextTokenType?: string, styleContext?: unknown) => string[];
|
|
52
|
+
|
|
53
|
+
interface MarkdownPatchState {
|
|
54
|
+
installed: true;
|
|
55
|
+
original: MarkdownRenderToken;
|
|
56
|
+
renderCodeToken: (instance: MarkdownRuntime, token: MarkdownCodeToken, nextTokenType?: string) => string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type PatchedMarkdownPrototype = {
|
|
60
|
+
renderToken?: MarkdownRenderToken;
|
|
61
|
+
[key: symbol]: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function isMarkdownCodeToken(token: unknown): token is MarkdownCodeToken {
|
|
65
|
+
return typeof token === "object" && token !== null && (token as { type?: unknown }).type === "code";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderCodeTokenWithoutFences(instance: MarkdownRuntime, token: MarkdownCodeToken, nextTokenType?: string): string[] {
|
|
69
|
+
const raw = typeof token.text === "string" ? token.text : "";
|
|
70
|
+
const lang = typeof token.lang === "string" ? token.lang.trim().toLowerCase() : "";
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
|
|
73
|
+
if (PLAIN_CODE_LANGS.has(lang)) {
|
|
74
|
+
for (const line of raw.split("\n")) lines.push(instance.applyDefaultStyle?.(line) ?? line);
|
|
75
|
+
} else if (instance.theme.highlightCode) {
|
|
76
|
+
const indent = instance.theme.codeBlockIndent ?? " ";
|
|
77
|
+
for (const line of instance.theme.highlightCode(raw, token.lang)) lines.push(`${indent}${line}`);
|
|
78
|
+
} else {
|
|
79
|
+
const indent = instance.theme.codeBlockIndent ?? " ";
|
|
80
|
+
for (const line of raw.split("\n")) lines.push(`${indent}${instance.theme.codeBlock(line)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function installMarkdownFencePatch(): void {
|
|
88
|
+
const proto = Markdown.prototype as unknown as PatchedMarkdownPrototype;
|
|
89
|
+
const existing = proto[MARKDOWN_PATCH_STATE] as MarkdownPatchState | undefined;
|
|
90
|
+
if (existing?.installed) {
|
|
91
|
+
existing.renderCodeToken = renderCodeTokenWithoutFences;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const original = proto.renderToken;
|
|
96
|
+
if (typeof original !== "function") return;
|
|
97
|
+
|
|
98
|
+
const state: MarkdownPatchState = {
|
|
99
|
+
installed: true,
|
|
100
|
+
original,
|
|
101
|
+
renderCodeToken: renderCodeTokenWithoutFences,
|
|
102
|
+
};
|
|
103
|
+
proto[MARKDOWN_PATCH_STATE] = state;
|
|
104
|
+
|
|
105
|
+
proto.renderToken = function (this: MarkdownRuntime, token: unknown, width: number, nextTokenType?: string, styleContext?: unknown): string[] {
|
|
106
|
+
const current = proto[MARKDOWN_PATCH_STATE] as MarkdownPatchState | undefined;
|
|
107
|
+
if (current && isMarkdownCodeToken(token)) return current.renderCodeToken(this, token, nextTokenType);
|
|
108
|
+
return (current?.original ?? original).call(this, token, width, nextTokenType, styleContext);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
12
111
|
|
|
13
112
|
function imageChip(id: number): string {
|
|
14
113
|
return `[image${id}]`;
|
|
@@ -18,6 +117,47 @@ function displayChip(token: string, theme: Theme): string {
|
|
|
18
117
|
return theme.fg("toolDiffAdded", theme.inverse(token));
|
|
19
118
|
}
|
|
20
119
|
|
|
120
|
+
function readClipboardFilePaths(): string[] {
|
|
121
|
+
if (process.platform !== "darwin") return [];
|
|
122
|
+
|
|
123
|
+
const result = spawnSync("osascript", ["-l", "JavaScript", "-e", MACOS_CLIPBOARD_FILE_PATHS_SCRIPT], {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
timeout: 700,
|
|
126
|
+
maxBuffer: 1024 * 1024,
|
|
127
|
+
});
|
|
128
|
+
if (result.error || result.status !== 0) return [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const parsed: unknown = JSON.parse(result.stdout.trim() || "[]");
|
|
132
|
+
if (!Array.isArray(parsed)) return [];
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
return parsed.filter((path): path is string => {
|
|
135
|
+
if (typeof path !== "string" || path.length === 0 || seen.has(path)) return false;
|
|
136
|
+
seen.add(path);
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pasteClipboardFilePaths(editor: EditorComponent, imageTokens: ImageTokenController, tui: TUI): boolean {
|
|
145
|
+
const paths = readClipboardFilePaths();
|
|
146
|
+
if (paths.length === 0) return false;
|
|
147
|
+
|
|
148
|
+
const text = imageTokens.formatClipboardFilePaths(paths, editor.getText());
|
|
149
|
+
if (!text) return false;
|
|
150
|
+
|
|
151
|
+
if (editor.insertTextAtCursor) {
|
|
152
|
+
editor.insertTextAtCursor(text);
|
|
153
|
+
} else {
|
|
154
|
+
editor.setText(editor.getText() + text);
|
|
155
|
+
editor.onChange?.(editor.getText());
|
|
156
|
+
}
|
|
157
|
+
tui.requestRender();
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
21
161
|
interface EditorInternals {
|
|
22
162
|
state: { lines: string[]; cursorLine: number; cursorCol: number };
|
|
23
163
|
historyIndex: number;
|
|
@@ -42,6 +182,12 @@ class ImageTokenController {
|
|
|
42
182
|
return text.replace(CLIPBOARD_PATH_RE, (path) => this.createImageToken(path, usedIds));
|
|
43
183
|
}
|
|
44
184
|
|
|
185
|
+
formatClipboardFilePaths(paths: string[], existingText = ""): string {
|
|
186
|
+
const usedIds = this.collectUsedIds(existingText);
|
|
187
|
+
const pieces = paths.map((path) => (IMAGE_FILE_RE.test(path) ? this.createImageToken(path, usedIds).trimEnd() : path));
|
|
188
|
+
return pieces.length > 0 ? `${pieces.join(paths.length > 1 ? "\n" : "")} ` : "";
|
|
189
|
+
}
|
|
190
|
+
|
|
45
191
|
replaceClipboardPathsInEditor(editor: EditorComponent, tui: TUI): void {
|
|
46
192
|
const current = editor.getText();
|
|
47
193
|
const usedIds = this.collectUsedIds(current);
|
|
@@ -126,9 +272,15 @@ class BeautifyEditor extends CustomEditor {
|
|
|
126
272
|
|
|
127
273
|
handleInput(data: string): void {
|
|
128
274
|
const isImagePaste = this.appKeybindings.matches(data, "app.clipboard.pasteImage");
|
|
275
|
+
if (isImagePaste) {
|
|
276
|
+
if (this.onExtensionShortcut?.(data)) return;
|
|
277
|
+
if (pasteClipboardFilePaths(this, this.imageTokens, this.tui)) return;
|
|
278
|
+
this.onPasteImage?.();
|
|
279
|
+
this.scheduleClipboardPathScan();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
129
282
|
if (this.imageTokens.deleteImageTokenAtCursor(this, data, this.tui)) return;
|
|
130
283
|
super.handleInput(data);
|
|
131
|
-
if (isImagePaste) this.scheduleClipboardPathScan();
|
|
132
284
|
}
|
|
133
285
|
|
|
134
286
|
insertTextAtCursor(text: string): void {
|
|
@@ -256,6 +408,7 @@ class BeautifyEditorWrapper implements EditorComponent {
|
|
|
256
408
|
if (this.onExtensionShortcut?.(data)) return;
|
|
257
409
|
if (this.imageTokens.deleteImageTokenAtCursor(this.inner, data, this.tui)) return;
|
|
258
410
|
if (isImagePaste) {
|
|
411
|
+
if (pasteClipboardFilePaths(this, this.imageTokens, this.tui)) return;
|
|
259
412
|
this.onPasteImage?.();
|
|
260
413
|
this.scheduleClipboardPathScan();
|
|
261
414
|
return;
|
|
@@ -326,6 +479,8 @@ function collectImageAttachments(text: string, attachments: Map<string, Attachme
|
|
|
326
479
|
}
|
|
327
480
|
|
|
328
481
|
export default function piBeautify(pi: ExtensionAPI) {
|
|
482
|
+
installMarkdownFencePatch();
|
|
483
|
+
|
|
329
484
|
const attachments = new Map<string, Attachment>();
|
|
330
485
|
|
|
331
486
|
pi.on("session_start", (_event, ctx) => {
|