pi-image-tools 1.0.4 → 1.0.6

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.
@@ -1,354 +1,354 @@
1
- import {
2
- type ExtensionAPI,
3
- InteractiveMode,
4
- UserMessageComponent,
5
- } from "@mariozechner/pi-coding-agent";
6
- import { Image, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
-
8
- import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./image-preview.js";
9
-
10
- const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
11
- const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
12
- const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
13
-
14
- type UserMessageRenderFn = (width: number) => string[];
15
-
16
- type UserMessagePrototype = {
17
- render: UserMessageRenderFn;
18
- __piImageToolsInlineOriginalRender?: UserMessageRenderFn;
19
- __piImageToolsInlinePatched?: boolean;
20
- };
21
-
22
- type UserMessageInstance = {
23
- __piImageToolsInlineAssigned?: boolean;
24
- __piImageToolsInlineItems?: ImagePreviewItem[];
25
- };
26
-
27
- type InteractiveModePrototype = {
28
- addMessageToChat: (message: unknown, options?: unknown) => void;
29
- getUserMessageText: (message: unknown) => string;
30
- __piImageToolsOriginalAddMessageToChat?: (message: unknown, options?: unknown) => void;
31
- __piImageToolsOriginalGetUserMessageText?: (message: unknown) => string;
32
- __piImageToolsPreviewPatched?: boolean;
33
- };
34
-
35
- interface UserImageContent {
36
- type: "image";
37
- data: string;
38
- mimeType: string;
39
- }
40
-
41
- interface UserMessageLike {
42
- role?: unknown;
43
- content?: unknown;
44
- }
45
-
46
- interface InteractiveModeLike {
47
- chatContainer?: {
48
- children?: unknown[];
49
- };
50
- }
51
-
52
- function sanitizeRows(rows: number): number {
53
- return Math.max(1, Math.min(Math.trunc(rows), 80));
54
- }
55
-
56
- function buildSixelLines(sequence: string, rows: number): string[] {
57
- const safeRows = sanitizeRows(rows);
58
- const lines = Array.from({ length: Math.max(0, safeRows - 1) }, () => "");
59
- const moveUp = safeRows > 1 ? `\x1b[${safeRows - 1}A` : "";
60
- return [...lines, `${SIXEL_IMAGE_LINE_MARKER}${moveUp}${sequence}`];
61
- }
62
-
63
- function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
64
- if (!item.data) {
65
- return [];
66
- }
67
-
68
- const image = new Image(
69
- item.data,
70
- item.mimeType,
71
- {
72
- fallbackColor: (text: string) => text,
73
- },
74
- {
75
- maxWidthCells: item.maxWidthCells,
76
- },
77
- );
78
-
79
- return image.render(Math.max(8, width));
80
- }
81
-
82
- function isInlineImageLine(line: string): boolean {
83
- return (
84
- line.startsWith(SIXEL_IMAGE_LINE_MARKER) ||
85
- line.includes(SIXEL_IMAGE_LINE_MARKER) ||
86
- line.startsWith(KITTY_IMAGE_LINE_MARKER) ||
87
- line.includes(KITTY_IMAGE_LINE_MARKER) ||
88
- line.startsWith(ITERM_IMAGE_LINE_MARKER) ||
89
- line.includes(ITERM_IMAGE_LINE_MARKER)
90
- );
91
- }
92
-
93
- function fitLineToWidth(line: string, width: number): string {
94
- const safeWidth = Math.max(1, Math.floor(width));
95
- if (isInlineImageLine(line)) {
96
- return line;
97
- }
98
-
99
- if (visibleWidth(line) <= safeWidth) {
100
- return line;
101
- }
102
-
103
- return truncateToWidth(line, safeWidth, "", true);
104
- }
105
-
106
- function fitLinesToWidth(lines: readonly string[], width: number): string[] {
107
- return lines.map((line) => fitLineToWidth(line, width));
108
- }
109
-
110
- function renderPreviewLines(items: readonly ImagePreviewItem[], width: number): string[] {
111
- if (items.length === 0) {
112
- return [];
113
- }
114
-
115
- const lines: string[] = ["", "↳ pasted image preview"];
116
-
117
- for (const item of items) {
118
- lines.push("");
119
-
120
- if (item.protocol === "sixel" && item.sixelSequence) {
121
- lines.push(...buildSixelLines(item.sixelSequence, item.rows));
122
- } else {
123
- lines.push(...buildNativeLines(item, width));
124
- }
125
-
126
- if (item.warning) {
127
- lines.push(...item.warning.split(/\r?\n/).filter((line) => line.length > 0));
128
- }
129
- }
130
-
131
- return fitLinesToWidth(lines, width);
132
- }
133
-
134
- function toUserMessage(value: unknown): UserMessageLike {
135
- if (!value || typeof value !== "object" || Array.isArray(value)) {
136
- return {};
137
- }
138
-
139
- return value as UserMessageLike;
140
- }
141
-
142
- function toImageContent(value: unknown): UserImageContent | null {
143
- if (!value || typeof value !== "object" || Array.isArray(value)) {
144
- return null;
145
- }
146
-
147
- const record = value as Record<string, unknown>;
148
- if (record.type !== "image") {
149
- return null;
150
- }
151
-
152
- if (typeof record.data !== "string" || record.data.length === 0) {
153
- return null;
154
- }
155
-
156
- return {
157
- type: "image",
158
- data: record.data,
159
- mimeType: typeof record.mimeType === "string" && record.mimeType.length > 0
160
- ? record.mimeType
161
- : "image/png",
162
- };
163
- }
164
-
165
- function extractImagePayloads(message: unknown): ImagePayload[] {
166
- const userMessage = toUserMessage(message);
167
- if (userMessage.role !== "user") {
168
- return [];
169
- }
170
-
171
- if (!Array.isArray(userMessage.content)) {
172
- return [];
173
- }
174
-
175
- const payloads: ImagePayload[] = [];
176
- for (const part of userMessage.content) {
177
- const image = toImageContent(part);
178
- if (!image) {
179
- continue;
180
- }
181
-
182
- payloads.push({
183
- type: "image",
184
- data: image.data,
185
- mimeType: image.mimeType,
186
- });
187
- }
188
-
189
- return payloads;
190
- }
191
-
192
- function imagePlaceholderText(count: number): string {
193
- if (count <= 1) {
194
- return "[󰈟 1 image attached]";
195
- }
196
-
197
- return `[󰈟 ${count} images attached]`;
198
- }
199
-
200
- function patchUserMessageRender(): void {
201
- const prototype = (UserMessageComponent as unknown as { prototype: UserMessagePrototype }).prototype;
202
- if (typeof prototype.render !== "function") {
203
- return;
204
- }
205
-
206
- if (!prototype.__piImageToolsInlineOriginalRender) {
207
- prototype.__piImageToolsInlineOriginalRender = prototype.render;
208
- }
209
-
210
- if (prototype.__piImageToolsInlinePatched) {
211
- return;
212
- }
213
-
214
- prototype.render = function renderWithInlineImagePreview(width: number): string[] {
215
- const originalRender = prototype.__piImageToolsInlineOriginalRender;
216
- if (!originalRender) {
217
- return [];
218
- }
219
-
220
- const instance = this as unknown as UserMessageInstance;
221
- if (!instance.__piImageToolsInlineAssigned) {
222
- instance.__piImageToolsInlineAssigned = true;
223
- if (!Array.isArray(instance.__piImageToolsInlineItems)) {
224
- instance.__piImageToolsInlineItems = [];
225
- }
226
- }
227
-
228
- const baseLines = originalRender.call(this, width);
229
- const previewLines = renderPreviewLines(instance.__piImageToolsInlineItems ?? [], width);
230
- if (previewLines.length === 0) {
231
- return baseLines;
232
- }
233
-
234
- return [...baseLines, ...previewLines];
235
- };
236
-
237
- prototype.__piImageToolsInlinePatched = true;
238
- }
239
-
240
- function assignPreviewItemsToLatestUserMessage(
241
- mode: InteractiveModeLike,
242
- fromChildIndex: number,
243
- previewItems: ImagePreviewItem[],
244
- ): void {
245
- const children = mode.chatContainer?.children;
246
- if (!Array.isArray(children) || children.length === 0) {
247
- return;
248
- }
249
-
250
- const start = Math.max(0, fromChildIndex);
251
- for (let index = children.length - 1; index >= start; index -= 1) {
252
- const child = children[index];
253
- if (!(child instanceof UserMessageComponent)) {
254
- continue;
255
- }
256
-
257
- const instance = child as unknown as UserMessageInstance;
258
- instance.__piImageToolsInlineItems = previewItems;
259
- instance.__piImageToolsInlineAssigned = true;
260
- return;
261
- }
262
- }
263
-
264
- function patchInteractiveMode(): void {
265
- const prototype = (InteractiveMode as unknown as { prototype: InteractiveModePrototype }).prototype;
266
- if (!prototype) {
267
- return;
268
- }
269
-
270
- if (!prototype.__piImageToolsOriginalGetUserMessageText) {
271
- prototype.__piImageToolsOriginalGetUserMessageText = prototype.getUserMessageText;
272
- }
273
-
274
- if (!prototype.__piImageToolsOriginalAddMessageToChat) {
275
- prototype.__piImageToolsOriginalAddMessageToChat = prototype.addMessageToChat;
276
- }
277
-
278
- if (prototype.__piImageToolsPreviewPatched) {
279
- return;
280
- }
281
-
282
- prototype.getUserMessageText = function getUserMessageTextWithImagePlaceholder(message: unknown): string {
283
- const original = prototype.__piImageToolsOriginalGetUserMessageText;
284
- const text = original ? original.call(this, message) : "";
285
- if (text.trim().length > 0) {
286
- return text;
287
- }
288
-
289
- const images = extractImagePayloads(message);
290
- if (images.length === 0) {
291
- return text;
292
- }
293
-
294
- return imagePlaceholderText(images.length);
295
- };
296
-
297
- prototype.addMessageToChat = function addMessageToChatWithImagePreview(message: unknown, options?: unknown): void {
298
- const mode = this as unknown as InteractiveModeLike;
299
- const beforeCount = Array.isArray(mode.chatContainer?.children)
300
- ? mode.chatContainer?.children.length ?? 0
301
- : 0;
302
-
303
- const imagePayloads = extractImagePayloads(message);
304
- let previewItems: ImagePreviewItem[] = [];
305
- if (imagePayloads.length > 0) {
306
- try {
307
- previewItems = buildPreviewItems(imagePayloads);
308
- } catch {
309
- previewItems = [];
310
- }
311
- }
312
-
313
- const original = prototype.__piImageToolsOriginalAddMessageToChat;
314
- if (!original) {
315
- return;
316
- }
317
-
318
- original.call(this, message, options);
319
-
320
- if (previewItems.length === 0) {
321
- return;
322
- }
323
-
324
- assignPreviewItemsToLatestUserMessage(mode, beforeCount, previewItems);
325
- };
326
-
327
- prototype.__piImageToolsPreviewPatched = true;
328
- }
329
-
330
- export function registerInlineUserImagePreview(pi: ExtensionAPI): void {
331
- const schedulePatch = (): void => {
332
- setTimeout(() => {
333
- patchInteractiveMode();
334
- patchUserMessageRender();
335
- }, 0);
336
-
337
- setTimeout(() => {
338
- patchInteractiveMode();
339
- patchUserMessageRender();
340
- }, 25);
341
- };
342
-
343
- pi.on("session_start", async () => {
344
- schedulePatch();
345
- });
346
-
347
- pi.on("before_agent_start", async () => {
348
- schedulePatch();
349
- });
350
-
351
- pi.on("session_switch", async () => {
352
- schedulePatch();
353
- });
354
- }
1
+ import {
2
+ type ExtensionAPI,
3
+ InteractiveMode,
4
+ UserMessageComponent,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import { Image, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
+
8
+ import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./image-preview.js";
9
+
10
+ const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
11
+ const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
12
+ const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
13
+
14
+ type UserMessageRenderFn = (width: number) => string[];
15
+
16
+ type UserMessagePrototype = {
17
+ render: UserMessageRenderFn;
18
+ __piImageToolsInlineOriginalRender?: UserMessageRenderFn;
19
+ __piImageToolsInlinePatched?: boolean;
20
+ };
21
+
22
+ type UserMessageInstance = {
23
+ __piImageToolsInlineAssigned?: boolean;
24
+ __piImageToolsInlineItems?: ImagePreviewItem[];
25
+ };
26
+
27
+ type InteractiveModePrototype = {
28
+ addMessageToChat: (message: unknown, options?: unknown) => void;
29
+ getUserMessageText: (message: unknown) => string;
30
+ __piImageToolsOriginalAddMessageToChat?: (message: unknown, options?: unknown) => void;
31
+ __piImageToolsOriginalGetUserMessageText?: (message: unknown) => string;
32
+ __piImageToolsPreviewPatched?: boolean;
33
+ };
34
+
35
+ interface UserImageContent {
36
+ type: "image";
37
+ data: string;
38
+ mimeType: string;
39
+ }
40
+
41
+ interface UserMessageLike {
42
+ role?: unknown;
43
+ content?: unknown;
44
+ }
45
+
46
+ interface InteractiveModeLike {
47
+ chatContainer?: {
48
+ children?: unknown[];
49
+ };
50
+ }
51
+
52
+ function sanitizeRows(rows: number): number {
53
+ return Math.max(1, Math.min(Math.trunc(rows), 80));
54
+ }
55
+
56
+ function buildSixelLines(sequence: string, rows: number): string[] {
57
+ const safeRows = sanitizeRows(rows);
58
+ const lines = Array.from({ length: Math.max(0, safeRows - 1) }, () => "");
59
+ const moveUp = safeRows > 1 ? `\x1b[${safeRows - 1}A` : "";
60
+ return [...lines, `${SIXEL_IMAGE_LINE_MARKER}${moveUp}${sequence}`];
61
+ }
62
+
63
+ function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
64
+ if (!item.data) {
65
+ return [];
66
+ }
67
+
68
+ const image = new Image(
69
+ item.data,
70
+ item.mimeType,
71
+ {
72
+ fallbackColor: (text: string) => text,
73
+ },
74
+ {
75
+ maxWidthCells: item.maxWidthCells,
76
+ },
77
+ );
78
+
79
+ return image.render(Math.max(8, width));
80
+ }
81
+
82
+ function isInlineImageLine(line: string): boolean {
83
+ return (
84
+ line.startsWith(SIXEL_IMAGE_LINE_MARKER) ||
85
+ line.includes(SIXEL_IMAGE_LINE_MARKER) ||
86
+ line.startsWith(KITTY_IMAGE_LINE_MARKER) ||
87
+ line.includes(KITTY_IMAGE_LINE_MARKER) ||
88
+ line.startsWith(ITERM_IMAGE_LINE_MARKER) ||
89
+ line.includes(ITERM_IMAGE_LINE_MARKER)
90
+ );
91
+ }
92
+
93
+ function fitLineToWidth(line: string, width: number): string {
94
+ const safeWidth = Math.max(1, Math.floor(width));
95
+ if (isInlineImageLine(line)) {
96
+ return line;
97
+ }
98
+
99
+ if (visibleWidth(line) <= safeWidth) {
100
+ return line;
101
+ }
102
+
103
+ return truncateToWidth(line, safeWidth, "", true);
104
+ }
105
+
106
+ function fitLinesToWidth(lines: readonly string[], width: number): string[] {
107
+ return lines.map((line) => fitLineToWidth(line, width));
108
+ }
109
+
110
+ function renderPreviewLines(items: readonly ImagePreviewItem[], width: number): string[] {
111
+ if (items.length === 0) {
112
+ return [];
113
+ }
114
+
115
+ const lines: string[] = ["", "↳ pasted image preview"];
116
+
117
+ for (const item of items) {
118
+ lines.push("");
119
+
120
+ if (item.protocol === "sixel" && item.sixelSequence) {
121
+ lines.push(...buildSixelLines(item.sixelSequence, item.rows));
122
+ } else {
123
+ lines.push(...buildNativeLines(item, width));
124
+ }
125
+
126
+ if (item.warning) {
127
+ lines.push(...item.warning.split(/\r?\n/).filter((line) => line.length > 0));
128
+ }
129
+ }
130
+
131
+ return fitLinesToWidth(lines, width);
132
+ }
133
+
134
+ function toUserMessage(value: unknown): UserMessageLike {
135
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
136
+ return {};
137
+ }
138
+
139
+ return value as UserMessageLike;
140
+ }
141
+
142
+ function toImageContent(value: unknown): UserImageContent | null {
143
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
144
+ return null;
145
+ }
146
+
147
+ const record = value as Record<string, unknown>;
148
+ if (record.type !== "image") {
149
+ return null;
150
+ }
151
+
152
+ if (typeof record.data !== "string" || record.data.length === 0) {
153
+ return null;
154
+ }
155
+
156
+ return {
157
+ type: "image",
158
+ data: record.data,
159
+ mimeType: typeof record.mimeType === "string" && record.mimeType.length > 0
160
+ ? record.mimeType
161
+ : "image/png",
162
+ };
163
+ }
164
+
165
+ function extractImagePayloads(message: unknown): ImagePayload[] {
166
+ const userMessage = toUserMessage(message);
167
+ if (userMessage.role !== "user") {
168
+ return [];
169
+ }
170
+
171
+ if (!Array.isArray(userMessage.content)) {
172
+ return [];
173
+ }
174
+
175
+ const payloads: ImagePayload[] = [];
176
+ for (const part of userMessage.content) {
177
+ const image = toImageContent(part);
178
+ if (!image) {
179
+ continue;
180
+ }
181
+
182
+ payloads.push({
183
+ type: "image",
184
+ data: image.data,
185
+ mimeType: image.mimeType,
186
+ });
187
+ }
188
+
189
+ return payloads;
190
+ }
191
+
192
+ function imagePlaceholderText(count: number): string {
193
+ if (count <= 1) {
194
+ return "[󰈟 1 image attached]";
195
+ }
196
+
197
+ return `[󰈟 ${count} images attached]`;
198
+ }
199
+
200
+ function patchUserMessageRender(): void {
201
+ const prototype = (UserMessageComponent as unknown as { prototype: UserMessagePrototype }).prototype;
202
+ if (typeof prototype.render !== "function") {
203
+ return;
204
+ }
205
+
206
+ if (!prototype.__piImageToolsInlineOriginalRender) {
207
+ prototype.__piImageToolsInlineOriginalRender = prototype.render;
208
+ }
209
+
210
+ if (prototype.__piImageToolsInlinePatched) {
211
+ return;
212
+ }
213
+
214
+ prototype.render = function renderWithInlineImagePreview(width: number): string[] {
215
+ const originalRender = prototype.__piImageToolsInlineOriginalRender;
216
+ if (!originalRender) {
217
+ return [];
218
+ }
219
+
220
+ const instance = this as unknown as UserMessageInstance;
221
+ if (!instance.__piImageToolsInlineAssigned) {
222
+ instance.__piImageToolsInlineAssigned = true;
223
+ if (!Array.isArray(instance.__piImageToolsInlineItems)) {
224
+ instance.__piImageToolsInlineItems = [];
225
+ }
226
+ }
227
+
228
+ const baseLines = originalRender.call(this, width);
229
+ const previewLines = renderPreviewLines(instance.__piImageToolsInlineItems ?? [], width);
230
+ if (previewLines.length === 0) {
231
+ return baseLines;
232
+ }
233
+
234
+ return [...baseLines, ...previewLines];
235
+ };
236
+
237
+ prototype.__piImageToolsInlinePatched = true;
238
+ }
239
+
240
+ function assignPreviewItemsToLatestUserMessage(
241
+ mode: InteractiveModeLike,
242
+ fromChildIndex: number,
243
+ previewItems: ImagePreviewItem[],
244
+ ): void {
245
+ const children = mode.chatContainer?.children;
246
+ if (!Array.isArray(children) || children.length === 0) {
247
+ return;
248
+ }
249
+
250
+ const start = Math.max(0, fromChildIndex);
251
+ for (let index = children.length - 1; index >= start; index -= 1) {
252
+ const child = children[index];
253
+ if (!(child instanceof UserMessageComponent)) {
254
+ continue;
255
+ }
256
+
257
+ const instance = child as unknown as UserMessageInstance;
258
+ instance.__piImageToolsInlineItems = previewItems;
259
+ instance.__piImageToolsInlineAssigned = true;
260
+ return;
261
+ }
262
+ }
263
+
264
+ function patchInteractiveMode(): void {
265
+ const prototype = (InteractiveMode as unknown as { prototype: InteractiveModePrototype }).prototype;
266
+ if (!prototype) {
267
+ return;
268
+ }
269
+
270
+ if (!prototype.__piImageToolsOriginalGetUserMessageText) {
271
+ prototype.__piImageToolsOriginalGetUserMessageText = prototype.getUserMessageText;
272
+ }
273
+
274
+ if (!prototype.__piImageToolsOriginalAddMessageToChat) {
275
+ prototype.__piImageToolsOriginalAddMessageToChat = prototype.addMessageToChat;
276
+ }
277
+
278
+ if (prototype.__piImageToolsPreviewPatched) {
279
+ return;
280
+ }
281
+
282
+ prototype.getUserMessageText = function getUserMessageTextWithImagePlaceholder(message: unknown): string {
283
+ const original = prototype.__piImageToolsOriginalGetUserMessageText;
284
+ const text = original ? original.call(this, message) : "";
285
+ if (text.trim().length > 0) {
286
+ return text;
287
+ }
288
+
289
+ const images = extractImagePayloads(message);
290
+ if (images.length === 0) {
291
+ return text;
292
+ }
293
+
294
+ return imagePlaceholderText(images.length);
295
+ };
296
+
297
+ prototype.addMessageToChat = function addMessageToChatWithImagePreview(message: unknown, options?: unknown): void {
298
+ const mode = this as unknown as InteractiveModeLike;
299
+ const beforeCount = Array.isArray(mode.chatContainer?.children)
300
+ ? mode.chatContainer?.children.length ?? 0
301
+ : 0;
302
+
303
+ const imagePayloads = extractImagePayloads(message);
304
+ let previewItems: ImagePreviewItem[] = [];
305
+ if (imagePayloads.length > 0) {
306
+ try {
307
+ previewItems = buildPreviewItems(imagePayloads);
308
+ } catch {
309
+ previewItems = [];
310
+ }
311
+ }
312
+
313
+ const original = prototype.__piImageToolsOriginalAddMessageToChat;
314
+ if (!original) {
315
+ return;
316
+ }
317
+
318
+ original.call(this, message, options);
319
+
320
+ if (previewItems.length === 0) {
321
+ return;
322
+ }
323
+
324
+ assignPreviewItemsToLatestUserMessage(mode, beforeCount, previewItems);
325
+ };
326
+
327
+ prototype.__piImageToolsPreviewPatched = true;
328
+ }
329
+
330
+ export function registerInlineUserImagePreview(pi: ExtensionAPI): void {
331
+ const schedulePatch = (): void => {
332
+ setTimeout(() => {
333
+ patchInteractiveMode();
334
+ patchUserMessageRender();
335
+ }, 0);
336
+
337
+ setTimeout(() => {
338
+ patchInteractiveMode();
339
+ patchUserMessageRender();
340
+ }, 25);
341
+ };
342
+
343
+ pi.on("session_start", async () => {
344
+ schedulePatch();
345
+ });
346
+
347
+ pi.on("before_agent_start", async () => {
348
+ schedulePatch();
349
+ });
350
+
351
+ pi.on("session_switch", async () => {
352
+ schedulePatch();
353
+ });
354
+ }