pi-image-tools 1.0.11 → 1.2.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/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import { readClipboardImage } from "./clipboard.js";
4
4
  import { registerPasteImageCommand } from "./commands.js";
5
5
  import { loadImageToolsConfig } from "./config.js";
6
6
  import { DebugLogger } from "./debug-logger.js";
7
+ import { getErrorMessage } from "./errors.js";
8
+ import { assertImageWithinByteLimit } from "./image-size.js";
7
9
  import {
8
10
  IMAGE_PREVIEW_CUSTOM_TYPE,
9
11
  buildPreviewItems,
@@ -27,15 +29,8 @@ const IMAGE_ATTACHMENT_INDICATOR = "[󰈟 Image Attached]";
27
29
 
28
30
  interface PendingImage extends ImagePayload {}
29
31
 
30
- function getErrorMessage(error: unknown): string {
31
- if (error instanceof Error && error.message.trim().length > 0) {
32
- return error.message;
33
- }
34
-
35
- return "Unknown error";
36
- }
37
-
38
32
  function imageToBase64(image: ClipboardImage): string {
33
+ assertImageWithinByteLimit(image.bytes.length, "Image attachment");
39
34
  return Buffer.from(image.bytes).toString("base64");
40
35
  }
41
36
 
@@ -113,12 +108,13 @@ function buildRecentImageEmptyStateMessage(searchedDirectories: readonly string[
113
108
  ].join(" ");
114
109
  }
115
110
 
116
- function showRecentSelectionPreview(
111
+ async function showRecentSelectionPreview(
117
112
  pi: ExtensionAPI,
118
113
  image: ClipboardImage,
119
114
  cwd: string,
120
- ): void {
121
- const previewItems = buildPreviewItems(
115
+ logger: DebugLogger,
116
+ ): Promise<void> {
117
+ const previewItems = await buildPreviewItems(
122
118
  [
123
119
  {
124
120
  type: "image",
@@ -126,7 +122,7 @@ function showRecentSelectionPreview(
126
122
  mimeType: image.mimeType,
127
123
  },
128
124
  ],
129
- { cwd },
125
+ { cwd, logger },
130
126
  );
131
127
 
132
128
  if (previewItems.length === 0) {
@@ -155,8 +151,8 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
155
151
  pasteImageShortcutsConfigured: config.shortcuts.pasteImage !== undefined,
156
152
  });
157
153
 
158
- registerInlineUserImagePreview(pi);
159
- registerImagePreviewDisplay(pi);
154
+ registerInlineUserImagePreview(pi, { logger });
155
+ registerImagePreviewDisplay(pi, { logger });
160
156
 
161
157
  const pasteImageFromClipboard = async (ctx: PasteContext): Promise<void> => {
162
158
  if (!ctx.hasUI) {
@@ -214,11 +210,9 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
214
210
  const selectedCandidate = discovery.candidates[selectedIndex];
215
211
  const selectedImage = loadRecentImage(selectedCandidate);
216
212
 
217
- try {
218
- showRecentSelectionPreview(pi, selectedImage, ctx.cwd);
219
- } catch (error) {
213
+ void showRecentSelectionPreview(pi, selectedImage, ctx.cwd, logger).catch((error: unknown) => {
220
214
  ctx.ui.notify(`Could not render recent image preview: ${getErrorMessage(error)}`, "warning");
221
- }
215
+ });
222
216
 
223
217
  queueImageAttachment(
224
218
  ctx,
@@ -2,9 +2,12 @@ import {
2
2
  type ExtensionAPI,
3
3
  InteractiveMode,
4
4
  UserMessageComponent,
5
- } from "@mariozechner/pi-coding-agent";
6
- import { Image, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { Image, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
7
 
8
+ import { isRecord } from "./config.js";
9
+ import type { DebugLogger } from "./debug-logger.js";
10
+ import { getErrorMessage } from "./errors.js";
8
11
  import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./image-preview.js";
9
12
  import { buildSixelRenderLines, isInlineImageProtocolLine } from "./sixel-protocol.js";
10
13
  import { setActiveTerminalImageSettingsCwd } from "./terminal-image-width.js";
@@ -108,19 +111,15 @@ function renderPreviewLines(items: readonly ImagePreviewItem[], width: number):
108
111
  }
109
112
 
110
113
  function toUserMessage(value: unknown): UserMessageLike {
111
- if (!value || typeof value !== "object" || Array.isArray(value)) {
112
- return {};
113
- }
114
-
115
- return value as UserMessageLike;
114
+ return isRecord(value) ? value : {};
116
115
  }
117
116
 
118
117
  function toImageContent(value: unknown): UserImageContent | null {
119
- if (!value || typeof value !== "object" || Array.isArray(value)) {
118
+ if (!isRecord(value)) {
120
119
  return null;
121
120
  }
122
121
 
123
- const record = value as Record<string, unknown>;
122
+ const record = value;
124
123
  if (record.type !== "image") {
125
124
  return null;
126
125
  }
@@ -237,7 +236,19 @@ function assignPreviewItemsToLatestUserMessage(
237
236
  }
238
237
  }
239
238
 
240
- function patchInteractiveMode(): void {
239
+ function logInlinePreviewError(
240
+ logger: DebugLogger | undefined,
241
+ event: string,
242
+ error: unknown,
243
+ ): void {
244
+ try {
245
+ logger?.log(event, { error: getErrorMessage(error) });
246
+ } catch {
247
+ // Debug logging is best-effort inside Pi event handlers.
248
+ }
249
+ }
250
+
251
+ function patchInteractiveMode(logger?: DebugLogger): void {
241
252
  const prototype = (InteractiveMode as unknown as { prototype: InteractiveModePrototype }).prototype;
242
253
  if (!prototype) {
243
254
  return;
@@ -277,15 +288,6 @@ function patchInteractiveMode(): void {
277
288
  : 0;
278
289
 
279
290
  const imagePayloads = extractImagePayloads(message);
280
- let previewItems: ImagePreviewItem[] = [];
281
- if (imagePayloads.length > 0) {
282
- try {
283
- previewItems = buildPreviewItems(imagePayloads);
284
- } catch {
285
- previewItems = [];
286
- }
287
- }
288
-
289
291
  const original = prototype.__piImageToolsOriginalAddMessageToChat;
290
292
  if (!original) {
291
293
  return;
@@ -293,41 +295,73 @@ function patchInteractiveMode(): void {
293
295
 
294
296
  original.call(this, message, options);
295
297
 
296
- if (previewItems.length === 0) {
298
+ if (imagePayloads.length === 0) {
297
299
  return;
298
300
  }
299
301
 
300
- assignPreviewItemsToLatestUserMessage(mode, beforeCount, previewItems);
302
+ void buildPreviewItems(imagePayloads, { logger })
303
+ .then((previewItems) => {
304
+ if (previewItems.length === 0) {
305
+ return;
306
+ }
307
+
308
+ assignPreviewItemsToLatestUserMessage(mode, beforeCount, previewItems);
309
+ })
310
+ .catch((error: unknown) => {
311
+ logInlinePreviewError(logger, "inline-user-preview.build_preview_failed", error);
312
+ });
301
313
  };
302
314
 
303
315
  prototype.__piImageToolsPreviewPatched = true;
304
316
  }
305
317
 
306
- export function registerInlineUserImagePreview(pi: ExtensionAPI): void {
307
- const schedulePatch = (): void => {
308
- setTimeout(() => {
309
- patchInteractiveMode();
310
- patchUserMessageRender();
311
- }, 0);
318
+ export interface RegisterInlineUserImagePreviewOptions {
319
+ logger?: DebugLogger;
320
+ }
312
321
 
322
+ export function registerInlineUserImagePreview(
323
+ pi: ExtensionAPI,
324
+ options: RegisterInlineUserImagePreviewOptions = {},
325
+ ): void {
326
+ const runPatch = (delayMs: number): void => {
313
327
  setTimeout(() => {
314
- patchInteractiveMode();
315
- patchUserMessageRender();
316
- }, 25);
328
+ try {
329
+ patchInteractiveMode(options.logger);
330
+ patchUserMessageRender();
331
+ } catch (error) {
332
+ logInlinePreviewError(options.logger, "inline-user-preview.patch_failed", error);
333
+ }
334
+ }, delayMs);
335
+ };
336
+
337
+ const schedulePatch = (): void => {
338
+ runPatch(0);
339
+ runPatch(25);
340
+ };
341
+
342
+ const handleSessionEvent = (eventName: string, cwd: string | undefined): void => {
343
+ try {
344
+ setActiveTerminalImageSettingsCwd(cwd);
345
+ schedulePatch();
346
+ } catch (error) {
347
+ logInlinePreviewError(options.logger, `inline-user-preview.${eventName}_failed`, error);
348
+ }
317
349
  };
318
350
 
319
351
  pi.on("session_start", async (_event, ctx) => {
320
- setActiveTerminalImageSettingsCwd(ctx.cwd);
321
- schedulePatch();
352
+ handleSessionEvent("session_start", ctx.cwd);
322
353
  });
323
354
 
324
355
  pi.on("before_agent_start", async (_event, ctx) => {
325
- setActiveTerminalImageSettingsCwd(ctx.cwd);
326
- schedulePatch();
356
+ handleSessionEvent("before_agent_start", ctx.cwd);
327
357
  });
328
358
 
329
- pi.on("session_switch", async (_event, ctx) => {
330
- setActiveTerminalImageSettingsCwd(ctx.cwd);
331
- schedulePatch();
359
+ const onSessionSwitch = pi.on as unknown as (
360
+ event: "session_switch",
361
+ handler: (_event: unknown, ctx: { cwd?: string }) => Promise<void>,
362
+ ) => void;
363
+
364
+ onSessionSwitch("session_switch", async (_event, ctx) => {
365
+ handleSessionEvent("session_switch", ctx.cwd);
332
366
  });
333
367
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
- import { getAgentDir, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
- import { TUI_KEYBINDINGS, type KeyId, type KeybindingsConfig } from "@mariozechner/pi-tui";
4
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { TUI_KEYBINDINGS, type KeyId, type KeybindingsConfig } from "@earendil-works/pi-tui";
6
6
 
7
7
  import { isRecord, type ImageToolsConfig } from "./config.js";
8
8
  import type { DebugLogger } from "./debug-logger.js";
@@ -0,0 +1,222 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+
3
+ import { getErrorMessage, isErrnoException } from "./errors.js";
4
+
5
+ export interface PowerShellCommandResult {
6
+ ok: boolean;
7
+ stdout: string;
8
+ stderr: string;
9
+ missingCommand: boolean;
10
+ reason?: string;
11
+ }
12
+
13
+ export interface BufferedCommandResult {
14
+ status: number | null;
15
+ stdout: Buffer;
16
+ stderr: Buffer;
17
+ error?: Error;
18
+ }
19
+
20
+ export interface RunBufferedCommandOptions {
21
+ maxBuffer: number;
22
+ timeout: number;
23
+ windowsHide?: boolean;
24
+ }
25
+
26
+ export interface RunPowerShellCommandOptions {
27
+ args?: string[];
28
+ encoded?: boolean;
29
+ maxBuffer: number;
30
+ sta?: boolean;
31
+ timeout: number;
32
+ }
33
+
34
+ function encodePowerShell(script: string): string {
35
+ return Buffer.from(script, "utf16le").toString("base64");
36
+ }
37
+
38
+ export function runBufferedCommand(
39
+ command: string,
40
+ args: readonly string[],
41
+ options: RunBufferedCommandOptions,
42
+ ): Promise<BufferedCommandResult> {
43
+ return new Promise((resolve) => {
44
+ let child: ReturnType<typeof spawn>;
45
+ try {
46
+ child = spawn(command, [...args], {
47
+ windowsHide: options.windowsHide,
48
+ });
49
+ } catch (error) {
50
+ resolve({
51
+ status: null,
52
+ stdout: Buffer.alloc(0),
53
+ stderr: Buffer.alloc(0),
54
+ error: error instanceof Error ? error : new Error(String(error)),
55
+ });
56
+ return;
57
+ }
58
+
59
+ const stdoutChunks: Buffer[] = [];
60
+ const stderrChunks: Buffer[] = [];
61
+ let stdoutBytes = 0;
62
+ let stderrBytes = 0;
63
+ let settled = false;
64
+ let processError: Error | undefined;
65
+
66
+ const finish = (result: BufferedCommandResult): void => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+
71
+ settled = true;
72
+ clearTimeout(timeout);
73
+ resolve(result);
74
+ };
75
+
76
+ const failAndKill = (error: Error): void => {
77
+ processError = error;
78
+ child.kill();
79
+ };
80
+
81
+ const appendChunk = (chunks: Buffer[], chunk: unknown, currentBytes: number): number => {
82
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
83
+ const nextBytes = currentBytes + buffer.length;
84
+ if (nextBytes > options.maxBuffer && !processError) {
85
+ failAndKill(new Error(`Command output exceeded maxBuffer (${options.maxBuffer} bytes).`));
86
+ return nextBytes;
87
+ }
88
+
89
+ chunks.push(buffer);
90
+ return nextBytes;
91
+ };
92
+
93
+ const timeout = setTimeout(() => {
94
+ failAndKill(new Error(`Command timed out after ${options.timeout}ms.`));
95
+ }, options.timeout);
96
+ timeout.unref?.();
97
+
98
+ child.stdout?.on("data", (chunk: unknown) => {
99
+ stdoutBytes = appendChunk(stdoutChunks, chunk, stdoutBytes);
100
+ });
101
+
102
+ child.stderr?.on("data", (chunk: unknown) => {
103
+ stderrBytes = appendChunk(stderrChunks, chunk, stderrBytes);
104
+ });
105
+
106
+ child.on("error", (error: Error) => {
107
+ processError = error;
108
+ finish({
109
+ status: null,
110
+ stdout: Buffer.concat(stdoutChunks),
111
+ stderr: Buffer.concat(stderrChunks),
112
+ error: processError,
113
+ });
114
+ });
115
+
116
+ child.on("close", (status: number | null) => {
117
+ finish({
118
+ status,
119
+ stdout: Buffer.concat(stdoutChunks),
120
+ stderr: Buffer.concat(stderrChunks),
121
+ error: processError,
122
+ });
123
+ });
124
+ });
125
+ }
126
+
127
+ export function runPowerShellCommand(
128
+ script: string,
129
+ options: RunPowerShellCommandOptions,
130
+ ): PowerShellCommandResult {
131
+ if (process.platform !== "win32") {
132
+ return {
133
+ ok: false,
134
+ stdout: "",
135
+ stderr: "",
136
+ missingCommand: false,
137
+ reason: "PowerShell is only available through pi-image-tools on Windows.",
138
+ };
139
+ }
140
+
141
+ const commandArgs = [
142
+ "-NoProfile",
143
+ "-NonInteractive",
144
+ ...(options.sta ? ["-STA"] : []),
145
+ ...(options.encoded ? ["-EncodedCommand", encodePowerShell(script)] : ["-Command", script]),
146
+ ...(options.args ?? []),
147
+ ];
148
+
149
+ const result = spawnSync("powershell.exe", commandArgs, {
150
+ encoding: "utf8",
151
+ timeout: options.timeout,
152
+ maxBuffer: options.maxBuffer,
153
+ windowsHide: true,
154
+ });
155
+
156
+ if (result.error) {
157
+ return {
158
+ ok: false,
159
+ stdout: result.stdout ?? "",
160
+ stderr: result.stderr ?? "",
161
+ missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
162
+ reason: getErrorMessage(result.error),
163
+ };
164
+ }
165
+
166
+ return {
167
+ ok: result.status === 0,
168
+ stdout: result.stdout ?? "",
169
+ stderr: result.stderr ?? "",
170
+ missingCommand: false,
171
+ reason: result.status === 0 ? undefined : `PowerShell exited with code ${result.status}`,
172
+ };
173
+ }
174
+
175
+ export async function runPowerShellCommandAsync(
176
+ script: string,
177
+ options: RunPowerShellCommandOptions,
178
+ ): Promise<PowerShellCommandResult> {
179
+ if (process.platform !== "win32") {
180
+ return {
181
+ ok: false,
182
+ stdout: "",
183
+ stderr: "",
184
+ missingCommand: false,
185
+ reason: "PowerShell is only available through pi-image-tools on Windows.",
186
+ };
187
+ }
188
+
189
+ const commandArgs = [
190
+ "-NoProfile",
191
+ "-NonInteractive",
192
+ ...(options.sta ? ["-STA"] : []),
193
+ ...(options.encoded ? ["-EncodedCommand", encodePowerShell(script)] : ["-Command", script]),
194
+ ...(options.args ?? []),
195
+ ];
196
+
197
+ const result = await runBufferedCommand("powershell.exe", commandArgs, {
198
+ timeout: options.timeout,
199
+ maxBuffer: options.maxBuffer,
200
+ windowsHide: true,
201
+ });
202
+ const stdout = result.stdout.toString("utf8");
203
+ const stderr = result.stderr.toString("utf8");
204
+
205
+ if (result.error) {
206
+ return {
207
+ ok: false,
208
+ stdout,
209
+ stderr,
210
+ missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
211
+ reason: getErrorMessage(result.error),
212
+ };
213
+ }
214
+
215
+ return {
216
+ ok: result.status === 0,
217
+ stdout,
218
+ stderr,
219
+ missingCommand: false,
220
+ reason: result.status === 0 ? undefined : `PowerShell exited with code ${result.status}`,
221
+ };
222
+ }
@@ -11,6 +11,8 @@ import {
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { basename, extname, join, resolve } from "node:path";
13
13
 
14
+ import { extensionToMimeType, mimeTypeToExtension } from "./image-mime.js";
15
+ import { assertImageWithinByteLimit, formatByteLimit } from "./image-size.js";
14
16
  import type { ClipboardImage } from "./types.js";
15
17
 
16
18
  export const RECENT_IMAGE_ENV_VAR = "PI_IMAGE_TOOLS_RECENT_DIRS";
@@ -30,15 +32,6 @@ const SCREENSHOT_NAME_PATTERNS: readonly RegExp[] = [
30
32
  /^スクリーンショット/i,
31
33
  ];
32
34
 
33
- const EXTENSION_TO_MIME = new Map<string, string>([
34
- [".png", "image/png"],
35
- [".jpg", "image/jpeg"],
36
- [".jpeg", "image/jpeg"],
37
- [".webp", "image/webp"],
38
- [".gif", "image/gif"],
39
- [".bmp", "image/bmp"],
40
- ]);
41
-
42
35
  interface RecentImageSource {
43
36
  path: string;
44
37
  filterScreenshotNames: boolean;
@@ -193,27 +186,11 @@ function isLikelyScreenshotName(name: string): boolean {
193
186
  }
194
187
 
195
188
  function toMimeType(fileName: string): string | null {
196
- const extension = extname(fileName).toLowerCase();
197
- return EXTENSION_TO_MIME.get(extension) ?? null;
189
+ return extensionToMimeType(extname(fileName));
198
190
  }
199
191
 
200
- function extensionForMimeType(mimeType: string): string {
201
- const normalized = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
202
-
203
- switch (normalized) {
204
- case "image/png":
205
- return "png";
206
- case "image/jpeg":
207
- return "jpg";
208
- case "image/webp":
209
- return "webp";
210
- case "image/gif":
211
- return "gif";
212
- case "image/bmp":
213
- return "bmp";
214
- default:
215
- return "png";
216
- }
192
+ function isExtensionOwnedCacheFileName(name: string): boolean {
193
+ return /^pi-recent-\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i.test(name);
217
194
  }
218
195
 
219
196
  function listRecentImagesFromSource(source: RecentImageSource): RecentImageCandidate[] {
@@ -372,7 +349,7 @@ function pruneCacheDirectory(cacheDirectory: string, maxCacheFiles: number): voi
372
349
  return null;
373
350
  }
374
351
 
375
- if (!toMimeType(name)) {
352
+ if (!isExtensionOwnedCacheFileName(name) || !toMimeType(name)) {
376
353
  return null;
377
354
  }
378
355
 
@@ -406,8 +383,9 @@ export function persistImageToRecentCache(
406
383
  }
407
384
 
408
385
  const environment = options.environment ?? process.env;
386
+ assertImageWithinByteLimit(image.bytes.length, "Cached image", environment);
409
387
  const cacheDirectory = getRecentImageCacheDirectory(environment);
410
- const extension = extensionForMimeType(image.mimeType);
388
+ const extension = mimeTypeToExtension(image.mimeType);
411
389
 
412
390
  mkdirSync(cacheDirectory, { recursive: true });
413
391
 
@@ -451,23 +429,6 @@ function formatRelativeAge(modifiedAtMs: number, nowMs: number): string {
451
429
  return `${deltaYears}y ago`;
452
430
  }
453
431
 
454
- function formatSize(sizeBytes: number): string {
455
- if (sizeBytes < 1024) {
456
- return `${sizeBytes} B`;
457
- }
458
-
459
- const units = ["KB", "MB", "GB"] as const;
460
- let value = sizeBytes / 1024;
461
- let unitIndex = 0;
462
-
463
- while (value >= 1024 && unitIndex < units.length - 1) {
464
- value /= 1024;
465
- unitIndex += 1;
466
- }
467
-
468
- return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
469
- }
470
-
471
432
  function detectPathSeparator(pathValue: string): string {
472
433
  return pathValue.includes("\\") ? "\\" : "/";
473
434
  }
@@ -489,13 +450,17 @@ function abbreviatePath(pathValue: string, maxChars: number): string {
489
450
 
490
451
  export function formatRecentImageLabel(candidate: RecentImageCandidate, nowMs = Date.now()): string {
491
452
  const age = formatRelativeAge(candidate.modifiedAtMs, nowMs);
492
- const size = formatSize(candidate.sizeBytes);
453
+ const size = formatByteLimit(candidate.sizeBytes);
493
454
  const shortPath = abbreviatePath(candidate.path, 64);
494
455
 
495
456
  return `${candidate.name} • ${age} • ${size} • ${shortPath}`;
496
457
  }
497
458
 
498
- export function loadRecentImage(candidate: RecentImageCandidate): ClipboardImage {
459
+ export function loadRecentImage(
460
+ candidate: RecentImageCandidate,
461
+ environment: NodeJS.ProcessEnv = process.env,
462
+ ): ClipboardImage {
463
+ assertImageWithinByteLimit(candidate.sizeBytes, `Recent image ${candidate.name}`, environment);
499
464
  const raw = readFileSync(candidate.path);
500
465
  if (raw.length === 0) {
501
466
  throw new Error(`File is empty: ${candidate.path}`);
@@ -1,6 +1,8 @@
1
- import { SettingsManager, getAgentDir } from "@mariozechner/pi-coding-agent";
1
+ import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { resolve } from "node:path";
3
3
 
4
+ import { isRecord } from "./config.js";
5
+
4
6
  export const DEFAULT_TERMINAL_IMAGE_WIDTH_CELLS = 60;
5
7
 
6
8
  export interface TerminalImageWidthOptions {
@@ -42,7 +44,7 @@ function normalizeImageWidthCells(value: unknown): number {
42
44
  }
43
45
 
44
46
  function readRawImageWidthCells(settings: unknown): unknown {
45
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
47
+ if (!isRecord(settings)) {
46
48
  return undefined;
47
49
  }
48
50
 
package/src/types.ts CHANGED
@@ -1,20 +1,20 @@
1
- import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
-
3
- export type PasteContext = ExtensionContext | ExtensionCommandContext;
4
-
5
- export interface ClipboardImage {
6
- bytes: Uint8Array;
7
- mimeType: string;
8
- }
9
-
10
- export interface ClipboardModule {
11
- hasImage: () => boolean;
12
- getImageBinary: () => Promise<Array<number> | Uint8Array>;
13
- }
14
-
15
- export type PasteImageHandler = (ctx: PasteContext) => Promise<void>;
16
-
17
- export interface PasteImageCommandHandlers {
18
- fromClipboard: PasteImageHandler;
19
- fromRecent: PasteImageHandler;
20
- }
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type PasteContext = ExtensionContext | ExtensionCommandContext;
4
+
5
+ export interface ClipboardImage {
6
+ bytes: Uint8Array;
7
+ mimeType: string;
8
+ }
9
+
10
+ export interface ClipboardModule {
11
+ hasImage: () => boolean;
12
+ getImageBinary: () => Promise<Array<number> | Uint8Array>;
13
+ }
14
+
15
+ export type PasteImageHandler = (ctx: PasteContext) => Promise<void>;
16
+
17
+ export interface PasteImageCommandHandlers {
18
+ fromClipboard: PasteImageHandler;
19
+ fromRecent: PasteImageHandler;
20
+ }