@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoose/pi-beautify",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Small visual polish extensions for pi coding agent",
5
5
  "type": "module",
6
6
  "files": [
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;