pi-paster 0.2.0 → 0.2.2
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 +41 -6
- package/dist/index.d.mts +80 -2
- package/dist/index.mjs +372 -23
- package/package.json +2 -2
- package/src/compress.ts +329 -0
- package/src/config.ts +67 -0
- package/src/editor.ts +105 -11
- package/src/image-utils.ts +8 -0
- package/src/index.ts +46 -8
- package/src/preview.ts +93 -4
- package/src/types.ts +11 -0
package/src/index.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readClipboardImage } from "./clipboard.ts";
|
|
3
|
+
import { registerImageCompressionCommand } from "./compress.ts";
|
|
3
4
|
import { type PasterConfig, resolvePasterConfig } from "./config.ts";
|
|
4
5
|
import { PasterEditor } from "./editor.ts";
|
|
5
|
-
import { imagesForTextOptimized } from "./image-utils.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { appendImagePathContext, imagesForTextOptimized } from "./image-utils.ts";
|
|
7
|
+
import {
|
|
8
|
+
CursorImagePreviewWidget,
|
|
9
|
+
ImageCompressionReportMessage,
|
|
10
|
+
ImagePreviewMessage,
|
|
11
|
+
} from "./preview.ts";
|
|
7
12
|
import { AttachmentStore } from "./store.ts";
|
|
8
13
|
import { createImagePasteTerminalInputHandler } from "./terminal-input.ts";
|
|
9
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ImageAttachment,
|
|
16
|
+
ImageCompressionReportDetails,
|
|
17
|
+
PasterPreviewDetails,
|
|
18
|
+
} from "./types.ts";
|
|
10
19
|
|
|
11
20
|
export * from "./clipboard.ts";
|
|
21
|
+
export * from "./compress.ts";
|
|
12
22
|
export * from "./optimize-image.ts";
|
|
13
23
|
export * from "./config.ts";
|
|
14
24
|
export * from "./editor.ts";
|
|
@@ -25,19 +35,44 @@ export function createPaster(config: PasterConfig = {}): (pi: ExtensionAPI) => v
|
|
|
25
35
|
export default function paster(pi: ExtensionAPI, config: PasterConfig = {}): void {
|
|
26
36
|
const resolvedConfig = resolvePasterConfig(config);
|
|
27
37
|
const store = new AttachmentStore();
|
|
38
|
+
registerImageCompressionCommand(pi, resolvedConfig.imageCompression);
|
|
28
39
|
let pendingPreview: ImageAttachment[] = [];
|
|
29
40
|
let activeEditor: PasterEditor | undefined;
|
|
30
41
|
let unsubscribeTerminalInput: (() => void) | undefined;
|
|
31
42
|
|
|
32
|
-
pi.registerMessageRenderer<
|
|
43
|
+
pi.registerMessageRenderer<ImageCompressionReportDetails>(
|
|
44
|
+
"paster-image-compress-report",
|
|
45
|
+
(message, options, theme) => {
|
|
46
|
+
const details = message.details;
|
|
47
|
+
if (!details) return undefined;
|
|
48
|
+
return new ImageCompressionReportMessage(
|
|
49
|
+
details,
|
|
50
|
+
{
|
|
51
|
+
background: (text) => theme.bg("toolSuccessBg", text),
|
|
52
|
+
title: (text) => theme.fg("toolTitle", theme.bold(text)),
|
|
53
|
+
muted: (text) => theme.fg("muted", text),
|
|
54
|
+
},
|
|
55
|
+
options.expanded,
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
pi.registerMessageRenderer<PasterPreviewDetails>("paster-preview", (message, options, theme) => {
|
|
33
61
|
const placeholders = message.details?.placeholders ?? [];
|
|
34
62
|
const attachments = store
|
|
35
63
|
.list()
|
|
36
64
|
.filter((attachment) => placeholders.includes(attachment.placeholder));
|
|
37
65
|
if (attachments.length === 0) return undefined;
|
|
38
|
-
return new ImagePreviewMessage(
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
return new ImagePreviewMessage(
|
|
67
|
+
attachments,
|
|
68
|
+
{
|
|
69
|
+
fallbackColor: (text) => theme.fg("muted", text),
|
|
70
|
+
background: (text) => theme.bg("toolSuccessBg", text),
|
|
71
|
+
title: (text) => theme.fg("toolTitle", theme.bold(text)),
|
|
72
|
+
muted: (text) => theme.fg("muted", text),
|
|
73
|
+
},
|
|
74
|
+
{ expanded: options.expanded, style: resolvedConfig.submittedPreviewStyle },
|
|
75
|
+
);
|
|
41
76
|
});
|
|
42
77
|
|
|
43
78
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -142,10 +177,13 @@ export default function paster(pi: ExtensionAPI, config: PasterConfig = {}): voi
|
|
|
142
177
|
// 32 MB/request caps. Per-attachment caching means each image is only
|
|
143
178
|
// resized/recompressed once across the whole session.
|
|
144
179
|
const images = await imagesForTextOptimized(store, event.text, event.images);
|
|
180
|
+
const text = resolvedConfig.includeImagePathsInPrompt
|
|
181
|
+
? appendImagePathContext(event.text, attachments)
|
|
182
|
+
: event.text;
|
|
145
183
|
|
|
146
184
|
return {
|
|
147
185
|
action: "transform" as const,
|
|
148
|
-
text
|
|
186
|
+
text,
|
|
149
187
|
images,
|
|
150
188
|
};
|
|
151
189
|
});
|
package/src/preview.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Box,
|
|
3
|
+
Container,
|
|
2
4
|
getCellDimensions,
|
|
3
5
|
Image,
|
|
4
6
|
type Component,
|
|
5
7
|
type ImageTheme,
|
|
8
|
+
Spacer,
|
|
9
|
+
Text,
|
|
6
10
|
truncateToWidth,
|
|
7
11
|
visibleWidth,
|
|
8
12
|
} from "@earendil-works/pi-tui";
|
|
9
|
-
import type { ImageAttachment } from "./types.ts";
|
|
13
|
+
import type { ImageAttachment, ImageCompressionReportDetails } from "./types.ts";
|
|
10
14
|
|
|
11
15
|
function formatAttachmentLine(
|
|
12
16
|
attachment: ImageAttachment,
|
|
@@ -18,12 +22,21 @@ function formatAttachmentLine(
|
|
|
18
22
|
return visibleWidth(line) > maxWidth ? truncateToWidth(line, maxWidth, "") : line;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export type ImagePreviewMessageStyle = "raw" | "collapsible";
|
|
26
|
+
|
|
27
|
+
interface ImagePreviewMessageTheme extends ImageTheme {
|
|
28
|
+
background?: (text: string) => string;
|
|
29
|
+
title?: (text: string) => string;
|
|
30
|
+
muted?: (text: string) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export class ImagePreviewMessage implements Component {
|
|
22
34
|
private readonly images: Image[];
|
|
23
35
|
|
|
24
36
|
constructor(
|
|
25
37
|
private readonly attachments: ImageAttachment[],
|
|
26
|
-
private readonly theme:
|
|
38
|
+
private readonly theme: ImagePreviewMessageTheme,
|
|
39
|
+
private readonly options: { expanded?: boolean; style?: ImagePreviewMessageStyle } = {},
|
|
27
40
|
) {
|
|
28
41
|
this.images = attachments.map(
|
|
29
42
|
(attachment) =>
|
|
@@ -35,7 +48,17 @@ export class ImagePreviewMessage implements Component {
|
|
|
35
48
|
);
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
invalidate(): void {
|
|
52
|
+
for (const image of this.images) image.invalidate();
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
render(width: number): string[] {
|
|
56
|
+
return this.options.style === "collapsible"
|
|
57
|
+
? this.renderCollapsible(width)
|
|
58
|
+
: this.renderRaw(width);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private renderRaw(width: number): string[] {
|
|
39
62
|
const lines: string[] = [];
|
|
40
63
|
const safeWidth = Math.max(1, width);
|
|
41
64
|
for (let index = 0; index < this.attachments.length; index++) {
|
|
@@ -46,9 +69,75 @@ export class ImagePreviewMessage implements Component {
|
|
|
46
69
|
return lines;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
private renderCollapsible(width: number): string[] {
|
|
73
|
+
const container = new Container();
|
|
74
|
+
container.addChild(new Spacer(1));
|
|
75
|
+
const box = new Box(1, 1, this.theme.background);
|
|
76
|
+
container.addChild(box);
|
|
77
|
+
|
|
78
|
+
const title = this.theme.title ?? this.theme.fallbackColor;
|
|
79
|
+
const muted = this.theme.muted ?? this.theme.fallbackColor;
|
|
80
|
+
const summary =
|
|
81
|
+
this.attachments.length === 1
|
|
82
|
+
? `Attached ${this.attachments[0]!.placeholder}`
|
|
83
|
+
: `Attached ${this.attachments.length} images`;
|
|
84
|
+
const suffix = this.options.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
|
|
85
|
+
box.addChild(new Text(`${title(summary)}${muted(suffix)}`, 0, 0));
|
|
86
|
+
|
|
87
|
+
for (const attachment of this.attachments) {
|
|
88
|
+
box.addChild(new Text(formatAttachmentLine(attachment, width, muted), 0, 0));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = container.render(width);
|
|
92
|
+
if (!this.options.expanded) return lines;
|
|
93
|
+
|
|
94
|
+
const safeWidth = Math.max(1, width);
|
|
95
|
+
for (let index = 0; index < this.attachments.length; index++) {
|
|
96
|
+
lines.push(...this.images[index]!.render(safeWidth));
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface CompressionReportTheme {
|
|
103
|
+
background?: (text: string) => string;
|
|
104
|
+
title: (text: string) => string;
|
|
105
|
+
muted: (text: string) => string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class ImageCompressionReportMessage implements Component {
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly details: ImageCompressionReportDetails,
|
|
111
|
+
private readonly theme: CompressionReportTheme,
|
|
112
|
+
private readonly expanded = false,
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
render(width: number): string[] {
|
|
116
|
+
const container = new Container();
|
|
117
|
+
container.addChild(new Spacer(1));
|
|
118
|
+
const box = new Box(1, 1, this.theme.background);
|
|
119
|
+
container.addChild(box);
|
|
120
|
+
|
|
121
|
+
const suffix = this.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
|
|
122
|
+
box.addChild(
|
|
123
|
+
new Text(
|
|
124
|
+
`${this.theme.title(`Compressed ${this.details.imageCount} image block(s) into ${this.details.summaryCount} summary/summaries`)}${this.theme.muted(suffix)}`,
|
|
125
|
+
0,
|
|
126
|
+
0,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (this.expanded) {
|
|
131
|
+
for (const item of this.details.items) {
|
|
132
|
+
box.addChild(new Text(this.theme.muted(`Image ${item.index}:`), 0, 0));
|
|
133
|
+
box.addChild(new Text(truncateToWidth(item.summary, Math.max(1, width - 4), "…"), 0, 0));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return container.render(width);
|
|
51
138
|
}
|
|
139
|
+
|
|
140
|
+
invalidate(): void {}
|
|
52
141
|
}
|
|
53
142
|
|
|
54
143
|
interface CursorPreviewTheme {
|
package/src/types.ts
CHANGED
|
@@ -57,3 +57,14 @@ export type LoadImageResult =
|
|
|
57
57
|
export interface PasterPreviewDetails {
|
|
58
58
|
placeholders: string[];
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
export interface ImageCompressionReportItem {
|
|
62
|
+
index: number;
|
|
63
|
+
summary: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ImageCompressionReportDetails {
|
|
67
|
+
imageCount: number;
|
|
68
|
+
summaryCount: number;
|
|
69
|
+
items: ImageCompressionReportItem[];
|
|
70
|
+
}
|