@smoose/pi-beautify 0.1.6 → 0.1.8
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 +2 -0
- package/package.json +1 -1
- package/src/index.ts +74 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Pi's terminal markdown renderer shows fenced code block borders like ```text. Th
|
|
|
10
10
|
|
|
11
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.
|
|
12
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
|
+
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
15
17
|
```bash
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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
4
|
import { getKeybindings, Markdown, matchesKey, truncateToWidth, type AutocompleteProvider, type EditorComponent, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
3
5
|
|
|
@@ -9,8 +11,25 @@ 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;
|
|
12
15
|
const MARKDOWN_PATCH_STATE = Symbol.for("smoose.pi-beautify.markdown.patch");
|
|
13
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
|
+
`;
|
|
14
33
|
|
|
15
34
|
interface MarkdownCodeToken {
|
|
16
35
|
type: "code";
|
|
@@ -98,6 +117,47 @@ function displayChip(token: string, theme: Theme): string {
|
|
|
98
117
|
return theme.fg("toolDiffAdded", theme.inverse(token));
|
|
99
118
|
}
|
|
100
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
|
+
|
|
101
161
|
interface EditorInternals {
|
|
102
162
|
state: { lines: string[]; cursorLine: number; cursorCol: number };
|
|
103
163
|
historyIndex: number;
|
|
@@ -122,6 +182,12 @@ class ImageTokenController {
|
|
|
122
182
|
return text.replace(CLIPBOARD_PATH_RE, (path) => this.createImageToken(path, usedIds));
|
|
123
183
|
}
|
|
124
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
|
+
|
|
125
191
|
replaceClipboardPathsInEditor(editor: EditorComponent, tui: TUI): void {
|
|
126
192
|
const current = editor.getText();
|
|
127
193
|
const usedIds = this.collectUsedIds(current);
|
|
@@ -206,9 +272,15 @@ class BeautifyEditor extends CustomEditor {
|
|
|
206
272
|
|
|
207
273
|
handleInput(data: string): void {
|
|
208
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
|
+
}
|
|
209
282
|
if (this.imageTokens.deleteImageTokenAtCursor(this, data, this.tui)) return;
|
|
210
283
|
super.handleInput(data);
|
|
211
|
-
if (isImagePaste) this.scheduleClipboardPathScan();
|
|
212
284
|
}
|
|
213
285
|
|
|
214
286
|
insertTextAtCursor(text: string): void {
|
|
@@ -336,6 +408,7 @@ class BeautifyEditorWrapper implements EditorComponent {
|
|
|
336
408
|
if (this.onExtensionShortcut?.(data)) return;
|
|
337
409
|
if (this.imageTokens.deleteImageTokenAtCursor(this.inner, data, this.tui)) return;
|
|
338
410
|
if (isImagePaste) {
|
|
411
|
+
if (pasteClipboardFilePaths(this, this.imageTokens, this.tui)) return;
|
|
339
412
|
this.onPasteImage?.();
|
|
340
413
|
this.scheduleClipboardPathScan();
|
|
341
414
|
return;
|