@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.
- package/README.md +1 -1
- package/package.json +1 -1
- 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 `[
|
|
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
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
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
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
|
|
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
|
|
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:
|
|
360
|
+
images: event.images,
|
|
379
361
|
};
|
|
380
362
|
});
|
|
381
363
|
}
|