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/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
- type: "image",
121
- data: imageToBase64(image),
122
- mimeType: image.mimeType,
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
- pi.sendMessage(
131
- {
132
- customType: IMAGE_PREVIEW_CUSTOM_TYPE,
133
- content: "",
134
- display: true,
135
- details: { items: previewItems },
136
- },
137
- { triggerTurn: false },
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
- 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=";
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 (isInlineImageLine(line)) {
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(...buildSixelLines(item.sixelSequence, item.rows));
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
+ }