pi-paster 0.1.0
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 +135 -0
- package/dist/index.d.mts +177 -0
- package/dist/index.mjs +634 -0
- package/docs/preview.png +0 -0
- package/package.json +53 -0
- package/spec.md +349 -0
- package/src/clipboard.ts +77 -0
- package/src/config.ts +40 -0
- package/src/editor.ts +214 -0
- package/src/image-utils.ts +236 -0
- package/src/index.ts +143 -0
- package/src/preview.ts +102 -0
- package/src/store.ts +40 -0
- package/src/terminal-input.ts +58 -0
- package/src/types.ts +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# paster
|
|
2
|
+
|
|
3
|
+
`paster` is a pi extension that turns pasted, drag-dropped, or clipboard-provided images into first-class image attachments.
|
|
4
|
+
|
|
5
|
+
Instead of leaving raw local image paths in your prompt, paster replaces them with readable placeholders such as `[#image 1]` and attaches the matching image content to the same user turn.
|
|
6
|
+
|
|
7
|
+
## Preview
|
|
8
|
+
|
|
9
|
+
<!-- Replace this with the project screenshot/demo image. -->
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
Terminal image workflows are awkward: dragging a screenshot into a terminal usually inserts a local file path, and pasting an image from the clipboard may require special handling. For multimodal models, that path is not enough—the image bytes need to be attached to the message.
|
|
16
|
+
|
|
17
|
+
`paster` bridges that gap for pi interactive mode:
|
|
18
|
+
|
|
19
|
+
1. Paste or drag/drop an image path into the editor.
|
|
20
|
+
2. The path is replaced with a placeholder like `[#image 1]`.
|
|
21
|
+
3. The image is stored in memory immediately.
|
|
22
|
+
4. When you submit, pi sends your text with the placeholder plus the actual image attachment.
|
|
23
|
+
5. The submitted image is rendered back in the conversation so you can confirm what was attached.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Converts pasted or drag-dropped image paths into placeholders.
|
|
28
|
+
- Supports PNG, JPEG, WebP, and GIF by magic-byte detection.
|
|
29
|
+
- Supports absolute, relative, home-relative, quoted, and shell-escaped paths.
|
|
30
|
+
- Attaches only placeholders still present in the submitted prompt.
|
|
31
|
+
- Preserves attachment order by first placeholder occurrence.
|
|
32
|
+
- Shows submitted image previews in chat history.
|
|
33
|
+
- Optional custom editor integration:
|
|
34
|
+
- cursor-based image preview above the input
|
|
35
|
+
- atomic deletion of whole image placeholders
|
|
36
|
+
- macOS clipboard image paste via pi's image paste keybinding
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Once published to npm, install the package with pi:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pi install npm:pi-paster
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or try it without installing:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pi -e npm:pi-paster
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For local development/testing:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pi -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
Start pi interactive mode with the extension enabled.
|
|
61
|
+
|
|
62
|
+
Then paste or drag/drop an image path:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/Users/me/Desktop/screenshot.png
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The editor will insert:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
[#image 1]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
You can also write normal text around it:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
What is wrong in this screenshot? [#image 1]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
On submit, the text and matching image attachment are sent together.
|
|
81
|
+
|
|
82
|
+
## Clipboard image paste
|
|
83
|
+
|
|
84
|
+
On macOS, pi exposes an image paste action through its keybinding system. In the default pi keybindings this is `Ctrl+V`.
|
|
85
|
+
|
|
86
|
+
`Cmd+V` is handled by the terminal emulator itself. In Ghostty, if the clipboard contains text, Ghostty pastes the text into pi; if the clipboard contains only image data, pi may receive no input. Use pi's image paste keybinding for direct clipboard-image paste.
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
By default all editor integrations are enabled.
|
|
91
|
+
|
|
92
|
+
To customize behavior, load a small wrapper extension:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createPaster } from "pi-paster";
|
|
96
|
+
|
|
97
|
+
export default createPaster({
|
|
98
|
+
customEditor: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
showImagePreview: true,
|
|
101
|
+
deletePlaceholderAsBlock: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Options
|
|
107
|
+
|
|
108
|
+
| Option | Default | Description |
|
|
109
|
+
| --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `customEditor.enabled` | `true` | Replaces pi's input editor with paster's editor integration. Disable this to keep pi's default editor. |
|
|
111
|
+
| `customEditor.showImagePreview` | `true` | Shows an image preview above the input when the cursor is inside an image placeholder. Requires `customEditor.enabled`. |
|
|
112
|
+
| `customEditor.deletePlaceholderAsBlock` | `true` | Makes backspace/delete remove the whole placeholder when editing inside or adjacent to it. Requires `customEditor.enabled`. |
|
|
113
|
+
|
|
114
|
+
When `customEditor.enabled` is `false`, paster still handles bracketed terminal paste/drop image paths, but cursor previews, atomic placeholder deletion, and paster's clipboard-image handler are disabled.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
This repo uses Vite+ via `vp` with pnpm.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
vp install
|
|
122
|
+
vp check
|
|
123
|
+
vp test run
|
|
124
|
+
vp run build
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The package manifest exposes the extension through:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"pi": {
|
|
132
|
+
"extensions": ["./src/index.ts"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as _$_earendil_works_pi_tui0 from "@earendil-works/pi-tui";
|
|
2
|
+
import { Component, EditorTheme, ImageDimensions, ImageTheme, TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { CustomEditor, ExtensionAPI, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
//#region src/config.d.ts
|
|
6
|
+
interface PasterConfig {
|
|
7
|
+
customEditor?: {
|
|
8
|
+
/** Replace pi's input editor to enable inline image UX features. */enabled?: boolean; /** Show an image preview above the input while the cursor is inside an image placeholder. */
|
|
9
|
+
showImagePreview?: boolean; /** Treat image placeholders as atomic blocks for backspace/delete. */
|
|
10
|
+
deletePlaceholderAsBlock?: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface ResolvedPasterConfig {
|
|
14
|
+
customEditor: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
showImagePreview: boolean;
|
|
17
|
+
deletePlaceholderAsBlock: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
declare const DEFAULT_PASTER_CONFIG: ResolvedPasterConfig;
|
|
21
|
+
declare function resolvePasterConfig(config?: PasterConfig): ResolvedPasterConfig;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/types.d.ts
|
|
24
|
+
declare const EXTENSION_NAME = "paster";
|
|
25
|
+
declare const MAX_IMAGE_BYTES: number;
|
|
26
|
+
type SupportedImageMimeType = "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
27
|
+
interface ImageAttachment {
|
|
28
|
+
id: number;
|
|
29
|
+
placeholder: string;
|
|
30
|
+
originalPath: string;
|
|
31
|
+
mimeType: SupportedImageMimeType;
|
|
32
|
+
data: string;
|
|
33
|
+
dimensions?: ImageDimensions;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
interface LoadedImage {
|
|
37
|
+
originalPath: string;
|
|
38
|
+
mimeType: SupportedImageMimeType;
|
|
39
|
+
data: string;
|
|
40
|
+
dimensions?: ImageDimensions;
|
|
41
|
+
}
|
|
42
|
+
interface PasterImageContent {
|
|
43
|
+
type: "image";
|
|
44
|
+
mimeType: string;
|
|
45
|
+
data: string;
|
|
46
|
+
}
|
|
47
|
+
type LoadImageResult = {
|
|
48
|
+
ok: true;
|
|
49
|
+
image: LoadedImage;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: "missing" | "not-file" | "too-large" | "unsupported" | "read-error";
|
|
53
|
+
path: string;
|
|
54
|
+
};
|
|
55
|
+
interface PasterPreviewDetails {
|
|
56
|
+
placeholders: string[];
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/clipboard.d.ts
|
|
60
|
+
type ClipboardImageResult = {
|
|
61
|
+
ok: true;
|
|
62
|
+
image: LoadedImage;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
reason: "empty" | "unsupported-platform" | "too-large" | "unsupported" | "read-error";
|
|
66
|
+
};
|
|
67
|
+
declare function readClipboardImage(maxBytes?: number): ClipboardImageResult;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/store.d.ts
|
|
70
|
+
declare class AttachmentStore {
|
|
71
|
+
private nextId;
|
|
72
|
+
private readonly attachments;
|
|
73
|
+
clear(): void;
|
|
74
|
+
list(): ImageAttachment[];
|
|
75
|
+
add(input: Omit<ImageAttachment, "id" | "placeholder" | "createdAt">): ImageAttachment;
|
|
76
|
+
get(placeholder: string): ImageAttachment | undefined;
|
|
77
|
+
matchingPlaceholders(text: string): ImageAttachment[];
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/editor.d.ts
|
|
81
|
+
declare const PASTE_START = "\u001B[200~";
|
|
82
|
+
declare const PASTE_END = "\u001B[201~";
|
|
83
|
+
declare class PasterEditor extends CustomEditor {
|
|
84
|
+
private readonly pasterKeybindings;
|
|
85
|
+
private readonly pasterOptions;
|
|
86
|
+
private pasterPasteBuffer;
|
|
87
|
+
private activePreviewPlaceholder;
|
|
88
|
+
constructor(tui: TUI, theme: EditorTheme, pasterKeybindings: KeybindingsManager, pasterOptions: {
|
|
89
|
+
cwd: string;
|
|
90
|
+
store: AttachmentStore;
|
|
91
|
+
notify: (message: string) => void;
|
|
92
|
+
deletePlaceholderAsBlock: boolean;
|
|
93
|
+
setCursorPreview: (attachment: ImageAttachment | undefined) => void;
|
|
94
|
+
pasteClipboardImage?: () => Promise<ImageAttachment | undefined> | ImageAttachment | undefined;
|
|
95
|
+
});
|
|
96
|
+
insertTextAtCursor(text: string): void;
|
|
97
|
+
handleInput(data: string): void;
|
|
98
|
+
clearCursorPreview(): void;
|
|
99
|
+
private handlePasteClipboardImage;
|
|
100
|
+
private handleBracketedPaste;
|
|
101
|
+
private handleAtomicPlaceholderDelete;
|
|
102
|
+
private deleteLineRange;
|
|
103
|
+
private transform;
|
|
104
|
+
private updateCursorPreview;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/image-utils.d.ts
|
|
108
|
+
interface PathToken {
|
|
109
|
+
raw: string;
|
|
110
|
+
value: string;
|
|
111
|
+
start: number;
|
|
112
|
+
end: number;
|
|
113
|
+
}
|
|
114
|
+
declare function detectImageMimeType(bytes: Uint8Array): SupportedImageMimeType | undefined;
|
|
115
|
+
declare function resolveImagePath(input: string, cwd: string): string;
|
|
116
|
+
declare function shellUnescape(input: string): string;
|
|
117
|
+
declare function tokenizePathLikeText(text: string): PathToken[];
|
|
118
|
+
declare function dimensionsForImage(data: string, mimeType: SupportedImageMimeType): _$_earendil_works_pi_tui0.ImageDimensions | undefined;
|
|
119
|
+
declare function loadImageFromPath(inputPath: string, cwd: string, maxBytes?: number): LoadImageResult;
|
|
120
|
+
declare function replaceImagePathsInText(text: string, options: {
|
|
121
|
+
cwd: string;
|
|
122
|
+
store: AttachmentStore;
|
|
123
|
+
loadImage?: (path: string, cwd: string) => LoadImageResult;
|
|
124
|
+
onReject?: (result: Exclude<LoadImageResult, {
|
|
125
|
+
ok: true;
|
|
126
|
+
}>) => void;
|
|
127
|
+
}): {
|
|
128
|
+
text: string;
|
|
129
|
+
replaced: number;
|
|
130
|
+
accepted: ImageAttachment[];
|
|
131
|
+
};
|
|
132
|
+
declare function imagesForText(store: AttachmentStore, text: string, existing?: PasterImageContent[]): PasterImageContent[];
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/preview.d.ts
|
|
135
|
+
declare class ImagePreviewMessage implements Component {
|
|
136
|
+
private readonly attachments;
|
|
137
|
+
private readonly theme;
|
|
138
|
+
private readonly images;
|
|
139
|
+
constructor(attachments: ImageAttachment[], theme: ImageTheme);
|
|
140
|
+
render(width: number): string[];
|
|
141
|
+
invalidate(): void;
|
|
142
|
+
}
|
|
143
|
+
interface CursorPreviewTheme {
|
|
144
|
+
title: (text: string) => string;
|
|
145
|
+
muted: (text: string) => string;
|
|
146
|
+
accent: (text: string) => string;
|
|
147
|
+
}
|
|
148
|
+
declare class CursorImagePreviewWidget implements Component {
|
|
149
|
+
private attachment;
|
|
150
|
+
private readonly theme;
|
|
151
|
+
private image;
|
|
152
|
+
constructor(attachment: ImageAttachment, theme: CursorPreviewTheme);
|
|
153
|
+
render(width: number): string[];
|
|
154
|
+
invalidate(): void;
|
|
155
|
+
private headerLine;
|
|
156
|
+
private createImage;
|
|
157
|
+
private constrainedImageWidth;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/terminal-input.d.ts
|
|
161
|
+
type TerminalInputResult = {
|
|
162
|
+
consume?: boolean;
|
|
163
|
+
data?: string;
|
|
164
|
+
} | undefined;
|
|
165
|
+
declare function createImagePasteTerminalInputHandler(options: {
|
|
166
|
+
cwd: string;
|
|
167
|
+
store: AttachmentStore;
|
|
168
|
+
notify?: (message: string) => void;
|
|
169
|
+
onAccept?: (attachments: ImageAttachment[]) => void;
|
|
170
|
+
loadImage?: (path: string, cwd: string) => LoadImageResult;
|
|
171
|
+
}): (data: string) => TerminalInputResult;
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/index.d.ts
|
|
174
|
+
declare function createPaster(config?: PasterConfig): (pi: ExtensionAPI) => void;
|
|
175
|
+
declare function paster(pi: ExtensionAPI, config?: PasterConfig): void;
|
|
176
|
+
//#endregion
|
|
177
|
+
export { AttachmentStore, ClipboardImageResult, CursorImagePreviewWidget, DEFAULT_PASTER_CONFIG, EXTENSION_NAME, ImageAttachment, ImagePreviewMessage, LoadImageResult, LoadedImage, MAX_IMAGE_BYTES, PASTE_END, PASTE_START, PasterConfig, PasterEditor, PasterImageContent, PasterPreviewDetails, ResolvedPasterConfig, SupportedImageMimeType, TerminalInputResult, createImagePasteTerminalInputHandler, createPaster, paster as default, detectImageMimeType, dimensionsForImage, imagesForText, loadImageFromPath, readClipboardImage, replaceImagePathsInText, resolveImagePath, resolvePasterConfig, shellUnescape, tokenizePathLikeText };
|