@smoose/pi-beautify 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +22 -40
package/README.md CHANGED
@@ -4,7 +4,7 @@ A small pi extension for visual polish.
4
4
 
5
5
  ## Clipboard image chips
6
6
 
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 `[image:1]`, then turns those chips back into attached image content before the prompt is sent.
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 `[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
8
 
9
9
  ## Installation
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoose/pi-beautify",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Small visual polish extensions for pi coding agent",
5
5
  "type": "module",
6
6
  "files": [
package/src/index.ts CHANGED
@@ -1,27 +1,15 @@
1
1
  import { CustomEditor, type AppKeybinding, type ExtensionAPI, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
- import type { ImageContent } from "@earendil-works/pi-ai";
3
2
  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";
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;
16
12
 
17
- function mimeTypeForPath(path: string): string {
18
- const ext = extname(path).toLowerCase();
19
- if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
20
- if (ext === ".webp") return "image/webp";
21
- if (ext === ".gif") return "image/gif";
22
- return "image/png";
23
- }
24
-
25
13
  function imageChip(id: number): string {
26
14
  return `[image${id}]`;
27
15
  }
@@ -39,8 +27,6 @@ interface EditorInternals {
39
27
  }
40
28
 
41
29
  class ImageTokenController {
42
- private nextId = 1;
43
-
44
30
  constructor(private readonly attachments: Map<string, Attachment>) {}
45
31
 
46
32
  renderChips(lines: string[], theme: Theme, width: number): string[] {
@@ -51,16 +37,18 @@ class ImageTokenController {
51
37
  return rendered.map((line) => truncateToWidth(line, width, ""));
52
38
  }
53
39
 
54
- replaceClipboardPathsInText(text: string): string {
55
- return text.replace(CLIPBOARD_PATH_RE, (path) => this.createImageToken(path));
40
+ replaceClipboardPathsInText(text: string, existingText = ""): string {
41
+ const usedIds = this.collectUsedIds(`${existingText}\n${text}`);
42
+ return text.replace(CLIPBOARD_PATH_RE, (path) => this.createImageToken(path, usedIds));
56
43
  }
57
44
 
58
45
  replaceClipboardPathsInEditor(editor: EditorComponent, tui: TUI): void {
59
46
  const current = editor.getText();
47
+ const usedIds = this.collectUsedIds(current);
60
48
  let changed = false;
61
49
  const next = current.replace(CLIPBOARD_PATH_RE, (path) => {
62
50
  changed = true;
63
- return this.createImageToken(path);
51
+ return this.createImageToken(path, usedIds);
64
52
  });
65
53
  if (!changed) return;
66
54
  editor.setText(next);
@@ -107,9 +95,18 @@ class ImageTokenController {
107
95
  return undefined;
108
96
  }
109
97
 
110
- private createImageToken(path: string): string {
111
- const token = imageChip(this.nextId++);
112
- this.attachments.set(token, { token, path, mimeType: mimeTypeForPath(path) });
98
+ private collectUsedIds(text: string): Set<number> {
99
+ const usedIds = new Set<number>();
100
+ for (const match of text.matchAll(TOKEN_RE)) usedIds.add(Number(match[1]));
101
+ return usedIds;
102
+ }
103
+
104
+ private createImageToken(path: string, usedIds: Set<number>): string {
105
+ let id = 1;
106
+ while (usedIds.has(id)) id++;
107
+ usedIds.add(id);
108
+ const token = imageChip(id);
109
+ this.attachments.set(token, { token, path });
113
110
  return `${token} `;
114
111
  }
115
112
  }
@@ -135,7 +132,7 @@ class BeautifyEditor extends CustomEditor {
135
132
  }
136
133
 
137
134
  insertTextAtCursor(text: string): void {
138
- super.insertTextAtCursor(this.imageTokens.replaceClipboardPathsInText(text));
135
+ super.insertTextAtCursor(this.imageTokens.replaceClipboardPathsInText(text, this.getText()));
139
136
  }
140
137
 
141
138
  render(width: number): string[] {
@@ -221,7 +218,7 @@ class BeautifyEditorWrapper implements EditorComponent {
221
218
  }
222
219
 
223
220
  insertTextAtCursor(text: string): void {
224
- const next = this.imageTokens.replaceClipboardPathsInText(text);
221
+ const next = this.imageTokens.replaceClipboardPathsInText(text, this.inner.getText());
225
222
  if (this.inner.insertTextAtCursor) {
226
223
  this.inner.insertTextAtCursor(next);
227
224
  return;
@@ -328,15 +325,6 @@ function collectImageAttachments(text: string, attachments: Map<string, Attachme
328
325
  return selected;
329
326
  }
330
327
 
331
- function toImageContent(attachment: Attachment): ImageContent | undefined {
332
- if (!existsSync(attachment.path)) return undefined;
333
- return {
334
- type: "image",
335
- data: readFileSync(attachment.path).toString("base64"),
336
- mimeType: attachment.mimeType,
337
- };
338
- }
339
-
340
328
  export default function piBeautify(pi: ExtensionAPI) {
341
329
  const attachments = new Map<string, Attachment>();
342
330
 
@@ -359,23 +347,17 @@ export default function piBeautify(pi: ExtensionAPI) {
359
347
  if (ctx.hasUI) ctx.ui.setStatus("pi-beautify", undefined);
360
348
  });
361
349
 
362
- pi.on("input", async (event, ctx) => {
350
+ pi.on("input", async (event) => {
363
351
  const selected = collectImageAttachments(event.text, attachments);
364
352
  if (selected.length === 0) return { action: "continue" };
365
353
 
366
- const converted = selected.map(toImageContent).filter((image): image is ImageContent => image !== undefined);
367
- if (converted.length === 0) {
368
- if (ctx.hasUI) ctx.ui.notify("pi-beautify: image file disappeared before submit", "warning");
369
- return { action: "continue" };
370
- }
371
-
354
+ const text = event.text.replace(TOKEN_RE, (full, id) => attachments.get(imageChip(Number(id)))?.path ?? full);
372
355
  for (const attachment of selected) attachments.delete(attachment.token);
373
356
 
374
- const text = event.text.replace(TOKEN_RE, (_full, id) => `[attached image ${id}]`);
375
357
  return {
376
358
  action: "transform",
377
359
  text,
378
- images: [...(event.images ?? []), ...converted],
360
+ images: event.images,
379
361
  };
380
362
  });
381
363
  }