pi-image-tools 1.0.8 → 1.0.10
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/CHANGELOG.md +30 -2
- package/README.md +12 -3
- package/package.json +67 -61
- package/src/image-preview.ts +497 -469
- package/src/index.ts +22 -17
- package/src/inline-user-preview.ts +10 -31
- package/src/sixel-protocol.ts +57 -0
- package/src/terminal-image-width.ts +90 -0
package/src/index.ts
CHANGED
|
@@ -114,28 +114,33 @@ function buildRecentImageEmptyStateMessage(searchedDirectories: readonly string[
|
|
|
114
114
|
function showRecentSelectionPreview(
|
|
115
115
|
pi: ExtensionAPI,
|
|
116
116
|
image: ClipboardImage,
|
|
117
|
+
cwd: string,
|
|
117
118
|
): void {
|
|
118
|
-
const previewItems = buildPreviewItems(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
const previewItems = buildPreviewItems(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
type: "image",
|
|
123
|
+
data: imageToBase64(image),
|
|
124
|
+
mimeType: image.mimeType,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
{ cwd },
|
|
128
|
+
);
|
|
125
129
|
|
|
126
130
|
if (previewItems.length === 0) {
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
const previewMessage = {
|
|
135
|
+
customType: IMAGE_PREVIEW_CUSTOM_TYPE,
|
|
136
|
+
content: "",
|
|
137
|
+
display: true,
|
|
138
|
+
details: { items: previewItems },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
pi.sendMessage(previewMessage, { triggerTurn: false });
|
|
143
|
+
}, 0);
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
@@ -201,7 +206,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
201
206
|
const selectedImage = loadRecentImage(selectedCandidate);
|
|
202
207
|
|
|
203
208
|
try {
|
|
204
|
-
showRecentSelectionPreview(pi, selectedImage);
|
|
209
|
+
showRecentSelectionPreview(pi, selectedImage, ctx.cwd);
|
|
205
210
|
} catch (error) {
|
|
206
211
|
ctx.ui.notify(`Could not render recent image preview: ${getErrorMessage(error)}`, "warning");
|
|
207
212
|
}
|
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
import { Image, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
7
7
|
|
|
8
8
|
import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./image-preview.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
|
|
12
|
-
const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
|
|
9
|
+
import { buildSixelRenderLines, isInlineImageProtocolLine } from "./sixel-protocol.js";
|
|
10
|
+
import { setActiveTerminalImageSettingsCwd } from "./terminal-image-width.js";
|
|
13
11
|
|
|
14
12
|
type UserMessageRenderFn = (width: number) => string[];
|
|
15
13
|
|
|
@@ -49,17 +47,6 @@ interface InteractiveModeLike {
|
|
|
49
47
|
};
|
|
50
48
|
}
|
|
51
49
|
|
|
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
50
|
function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
|
|
64
51
|
if (!item.data) {
|
|
65
52
|
return [];
|
|
@@ -79,20 +66,9 @@ function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
|
|
|
79
66
|
return image.render(Math.max(8, width));
|
|
80
67
|
}
|
|
81
68
|
|
|
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
69
|
function fitLineToWidth(line: string, width: number): string {
|
|
94
70
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
95
|
-
if (
|
|
71
|
+
if (isInlineImageProtocolLine(line)) {
|
|
96
72
|
return line;
|
|
97
73
|
}
|
|
98
74
|
|
|
@@ -118,7 +94,7 @@ function renderPreviewLines(items: readonly ImagePreviewItem[], width: number):
|
|
|
118
94
|
lines.push("");
|
|
119
95
|
|
|
120
96
|
if (item.protocol === "sixel" && item.sixelSequence) {
|
|
121
|
-
lines.push(...
|
|
97
|
+
lines.push(...buildSixelRenderLines(item.sixelSequence, item.rows));
|
|
122
98
|
} else {
|
|
123
99
|
lines.push(...buildNativeLines(item, width));
|
|
124
100
|
}
|
|
@@ -340,15 +316,18 @@ export function registerInlineUserImagePreview(pi: ExtensionAPI): void {
|
|
|
340
316
|
}, 25);
|
|
341
317
|
};
|
|
342
318
|
|
|
343
|
-
pi.on("session_start", async () => {
|
|
319
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
320
|
+
setActiveTerminalImageSettingsCwd(ctx.cwd);
|
|
344
321
|
schedulePatch();
|
|
345
322
|
});
|
|
346
323
|
|
|
347
|
-
pi.on("before_agent_start", async () => {
|
|
324
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
325
|
+
setActiveTerminalImageSettingsCwd(ctx.cwd);
|
|
348
326
|
schedulePatch();
|
|
349
327
|
});
|
|
350
328
|
|
|
351
|
-
pi.on("session_switch", async () => {
|
|
329
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
330
|
+
setActiveTerminalImageSettingsCwd(ctx.cwd);
|
|
352
331
|
schedulePatch();
|
|
353
332
|
});
|
|
354
333
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
|
|
2
|
+
const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
|
|
3
|
+
const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
|
|
4
|
+
const SIXEL_DCS_PREFIX = "\x1bP";
|
|
5
|
+
const STRING_TERMINATOR = "\x1b\\";
|
|
6
|
+
const MAX_IMAGE_ROWS = 80;
|
|
7
|
+
|
|
8
|
+
function sanitizeRows(rows: number): number {
|
|
9
|
+
return Math.max(1, Math.min(Math.trunc(rows), MAX_IMAGE_ROWS));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeSixelOutput(value: string): string {
|
|
13
|
+
return value.replace(/\r?\n/g, "").replace(/\s+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure the PowerShell Sixel output is emitted as a complete DCS sequence.
|
|
18
|
+
* Some converters return only the sixel payload body; terminals need the
|
|
19
|
+
* enclosing ESC P ... ESC \\ wrapper to render it as an image.
|
|
20
|
+
*/
|
|
21
|
+
export function ensureCompleteSixelSequence(sequence: string): string {
|
|
22
|
+
let normalized = normalizeSixelOutput(sequence);
|
|
23
|
+
if (normalized.length === 0) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!normalized.startsWith(SIXEL_DCS_PREFIX)) {
|
|
28
|
+
normalized = `${SIXEL_DCS_PREFIX}${normalized.startsWith("q") ? normalized : `q${normalized}`}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!normalized.endsWith(STRING_TERMINATOR)) {
|
|
32
|
+
normalized = `${normalized}${STRING_TERMINATOR}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildSixelRenderLines(sequence: string, rows: number): string[] {
|
|
39
|
+
const safeRows = sanitizeRows(rows);
|
|
40
|
+
const completeSequence = ensureCompleteSixelSequence(sequence);
|
|
41
|
+
if (completeSequence.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lines = Array.from({ length: Math.max(0, safeRows - 1) }, () => "");
|
|
46
|
+
const moveUp = safeRows > 1 ? `\x1b[${safeRows - 1}A` : "";
|
|
47
|
+
return [...lines, `${SIXEL_IMAGE_LINE_MARKER}${moveUp}${completeSequence}`];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isInlineImageProtocolLine(line: string): boolean {
|
|
51
|
+
return (
|
|
52
|
+
line.includes(SIXEL_IMAGE_LINE_MARKER) ||
|
|
53
|
+
line.includes(KITTY_IMAGE_LINE_MARKER) ||
|
|
54
|
+
line.includes(ITERM_IMAGE_LINE_MARKER) ||
|
|
55
|
+
line.includes(SIXEL_DCS_PREFIX)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SettingsManager, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS = 60;
|
|
5
|
+
|
|
6
|
+
export interface TerminalImageWidthOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
agentDir?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SettingsWithOptionalTerminalWidth {
|
|
12
|
+
terminal?: {
|
|
13
|
+
imageWidthCells?: unknown;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type SettingsManagerWithOptionalWidthGetter = SettingsManager & {
|
|
18
|
+
getImageWidthCells?: () => number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let activeTerminalSettingsCwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
function normalizeDirectoryPath(value: string | undefined): string | undefined {
|
|
24
|
+
if (typeof value !== "string") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return resolve(trimmed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeImageWidthCells(value: unknown): number {
|
|
37
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
38
|
+
return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Math.max(1, Math.floor(value));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readRawImageWidthCells(settings: unknown): unknown {
|
|
45
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (settings as SettingsWithOptionalTerminalWidth).terminal?.imageWidthCells;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveSettingsManagerImageWidthCells(settingsManager: SettingsManager): unknown {
|
|
53
|
+
const settingsManagerWithOptionalWidthGetter =
|
|
54
|
+
settingsManager as SettingsManagerWithOptionalWidthGetter;
|
|
55
|
+
|
|
56
|
+
if (typeof settingsManagerWithOptionalWidthGetter.getImageWidthCells === "function") {
|
|
57
|
+
return settingsManagerWithOptionalWidthGetter.getImageWidthCells();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const projectWidth = readRawImageWidthCells(settingsManager.getProjectSettings());
|
|
61
|
+
if (projectWidth !== undefined) {
|
|
62
|
+
return projectWidth;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return readRawImageWidthCells(settingsManager.getGlobalSettings());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function setActiveTerminalImageSettingsCwd(cwd: string | undefined): void {
|
|
69
|
+
const normalized = normalizeDirectoryPath(cwd);
|
|
70
|
+
if (normalized) {
|
|
71
|
+
activeTerminalSettingsCwd = normalized;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveTerminalImageWidthCells(
|
|
76
|
+
options: TerminalImageWidthOptions = {},
|
|
77
|
+
): number {
|
|
78
|
+
const cwd = normalizeDirectoryPath(options.cwd) ?? activeTerminalSettingsCwd;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const settingsManager = SettingsManager.create(
|
|
82
|
+
cwd,
|
|
83
|
+
normalizeDirectoryPath(options.agentDir) ?? getAgentDir(),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return normalizeImageWidthCells(resolveSettingsManagerImageWidthCells(settingsManager));
|
|
87
|
+
} catch {
|
|
88
|
+
return DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS;
|
|
89
|
+
}
|
|
90
|
+
}
|