pi-image-tools 1.2.0 → 1.3.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/CHANGELOG.md +20 -0
- package/README.md +4 -4
- package/package.json +69 -69
- package/src/clipboard.ts +89 -336
- package/src/config.ts +24 -15
- package/src/index.ts +115 -32
- package/src/inline-user-preview.ts +225 -69
- package/src/keybindings.ts +10 -11
- package/src/providers/command-runner.ts +68 -0
- package/src/providers/mac-osascript-pngf.ts +101 -0
- package/src/providers/mac-osascript-publicpng.ts +77 -0
- package/src/providers/mac-pngpaste.ts +69 -0
- package/src/providers/native-module.ts +84 -0
- package/src/providers/powershell-forms.ts +95 -0
- package/src/providers/registry.ts +88 -0
- package/src/providers/types.ts +24 -0
- package/src/providers/wl-paste.ts +81 -0
- package/src/providers/xclip.ts +87 -0
- package/src/shell-environment.ts +75 -0
package/src/config.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface ImageToolsConfig {
|
|
|
18
18
|
shortcuts: ImageToolsShortcutConfig;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface LoadImageToolsConfigOptions {
|
|
22
|
+
platform?: NodeJS.Platform;
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
26
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
27
|
}
|
|
@@ -34,9 +38,9 @@ function formatConfigPath(path: string, property: string): string {
|
|
|
34
38
|
return `${path}${property.length > 0 ? ` property \"${property}\"` : ""}`;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
function parseBoolean(value: unknown, property: string, path: string): boolean {
|
|
41
|
+
function parseBoolean(value: unknown, property: string, path: string, defaultValue = false): boolean {
|
|
38
42
|
if (value === undefined) {
|
|
39
|
-
return
|
|
43
|
+
return defaultValue;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
if (typeof value !== "boolean") {
|
|
@@ -82,12 +86,17 @@ function parseShortcutList(value: unknown, property: string, path: string): KeyI
|
|
|
82
86
|
return shortcuts;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
function
|
|
89
|
+
function getDefaultShortcutConfig(platform: NodeJS.Platform): ImageToolsShortcutConfig {
|
|
90
|
+
return {
|
|
91
|
+
avoidBuiltinConflicts: platform !== "darwin",
|
|
92
|
+
suppressBuiltinConflictWarnings: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseShortcutConfig(value: unknown, path: string, platform: NodeJS.Platform): ImageToolsShortcutConfig {
|
|
97
|
+
const defaults = getDefaultShortcutConfig(platform);
|
|
86
98
|
if (value === undefined) {
|
|
87
|
-
return
|
|
88
|
-
avoidBuiltinConflicts: false,
|
|
89
|
-
suppressBuiltinConflictWarnings: false,
|
|
90
|
-
};
|
|
99
|
+
return defaults;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
if (!isRecord(value)) {
|
|
@@ -98,11 +107,13 @@ function parseShortcutConfig(value: unknown, path: string): ImageToolsShortcutCo
|
|
|
98
107
|
value.avoidBuiltinConflicts,
|
|
99
108
|
"shortcuts.avoidBuiltinConflicts",
|
|
100
109
|
path,
|
|
110
|
+
defaults.avoidBuiltinConflicts,
|
|
101
111
|
);
|
|
102
112
|
const suppressBuiltinConflictWarnings = parseBoolean(
|
|
103
113
|
value.suppressBuiltinConflictWarnings,
|
|
104
114
|
"shortcuts.suppressBuiltinConflictWarnings",
|
|
105
115
|
path,
|
|
116
|
+
defaults.suppressBuiltinConflictWarnings,
|
|
106
117
|
);
|
|
107
118
|
const pasteImage = parseShortcutList(value.pasteImage, "shortcuts.pasteImage", path);
|
|
108
119
|
|
|
@@ -111,31 +122,29 @@ function parseShortcutConfig(value: unknown, path: string): ImageToolsShortcutCo
|
|
|
111
122
|
: { avoidBuiltinConflicts, suppressBuiltinConflictWarnings, pasteImage };
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
function parseConfig(rawConfig: unknown, path: string): ImageToolsConfig {
|
|
125
|
+
function parseConfig(rawConfig: unknown, path: string, platform: NodeJS.Platform): ImageToolsConfig {
|
|
115
126
|
if (!isRecord(rawConfig)) {
|
|
116
127
|
throw new Error(`Invalid pi-image-tools config at ${path}: expected a JSON object.`);
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
return {
|
|
120
131
|
debug: parseBoolean(rawConfig.debug, "debug", path),
|
|
121
|
-
shortcuts: parseShortcutConfig(rawConfig.shortcuts, path),
|
|
132
|
+
shortcuts: parseShortcutConfig(rawConfig.shortcuts, path, platform),
|
|
122
133
|
};
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
export function loadImageToolsConfig(path = getConfigPath()): ImageToolsConfig {
|
|
136
|
+
export function loadImageToolsConfig(path = getConfigPath(), options: LoadImageToolsConfigOptions = {}): ImageToolsConfig {
|
|
137
|
+
const platform = options.platform ?? process.platform;
|
|
126
138
|
if (!existsSync(path)) {
|
|
127
139
|
return {
|
|
128
140
|
debug: false,
|
|
129
|
-
shortcuts:
|
|
130
|
-
avoidBuiltinConflicts: false,
|
|
131
|
-
suppressBuiltinConflictWarnings: false,
|
|
132
|
-
},
|
|
141
|
+
shortcuts: getDefaultShortcutConfig(platform),
|
|
133
142
|
};
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
try {
|
|
137
146
|
const rawConfig = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
138
|
-
return parseConfig(rawConfig, path);
|
|
147
|
+
return parseConfig(rawConfig, path, platform);
|
|
139
148
|
} catch (error) {
|
|
140
149
|
if (error instanceof SyntaxError) {
|
|
141
150
|
throw new Error(`Invalid pi-image-tools config at ${path}: ${error.message}`);
|
package/src/index.ts
CHANGED
|
@@ -1,34 +1,89 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import { readClipboardImage } from "./clipboard.js";
|
|
4
3
|
import { registerPasteImageCommand } from "./commands.js";
|
|
5
4
|
import { loadImageToolsConfig } from "./config.js";
|
|
6
5
|
import { DebugLogger } from "./debug-logger.js";
|
|
7
6
|
import { getErrorMessage } from "./errors.js";
|
|
8
7
|
import { assertImageWithinByteLimit } from "./image-size.js";
|
|
9
|
-
import {
|
|
10
|
-
IMAGE_PREVIEW_CUSTOM_TYPE,
|
|
11
|
-
buildPreviewItems,
|
|
12
|
-
registerImagePreviewDisplay,
|
|
13
|
-
type ImagePayload,
|
|
14
|
-
} from "./image-preview.js";
|
|
15
|
-
import { registerInlineUserImagePreview } from "./inline-user-preview.js";
|
|
16
8
|
import { registerImagePasteKeybindings } from "./keybindings.js";
|
|
17
|
-
import {
|
|
18
|
-
RECENT_IMAGE_CACHE_DIR_ENV_VAR,
|
|
19
|
-
RECENT_IMAGE_ENV_VAR,
|
|
20
|
-
discoverRecentImages,
|
|
21
|
-
formatRecentImageLabel,
|
|
22
|
-
getRecentImageCacheDirectory,
|
|
23
|
-
loadRecentImage,
|
|
24
|
-
persistImageToRecentCache,
|
|
25
|
-
} from "./recent-images.js";
|
|
26
9
|
import type { ClipboardImage, PasteContext } from "./types.js";
|
|
27
10
|
|
|
28
11
|
const IMAGE_ATTACHMENT_INDICATOR = "[ Image Attached]";
|
|
12
|
+
const IMAGE_PREVIEW_CUSTOM_TYPE = "pi-image-tools-preview";
|
|
13
|
+
const RECENT_IMAGE_ENV_VAR = "PI_IMAGE_TOOLS_RECENT_DIRS";
|
|
14
|
+
const RECENT_IMAGE_CACHE_DIR_ENV_VAR = "PI_IMAGE_TOOLS_RECENT_CACHE_DIR";
|
|
15
|
+
|
|
16
|
+
type ImagePayload = {
|
|
17
|
+
type: "image";
|
|
18
|
+
data: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
};
|
|
29
21
|
|
|
30
22
|
interface PendingImage extends ImagePayload {}
|
|
31
23
|
|
|
24
|
+
type ClipboardModule = typeof import("./clipboard.js");
|
|
25
|
+
type ImagePreviewModule = typeof import("./image-preview.js");
|
|
26
|
+
type InlineUserPreviewModule = typeof import("./inline-user-preview.js");
|
|
27
|
+
type RecentImagesModule = typeof import("./recent-images.js");
|
|
28
|
+
|
|
29
|
+
let clipboardModulePromise: Promise<ClipboardModule> | undefined;
|
|
30
|
+
let imagePreviewModulePromise: Promise<ImagePreviewModule> | undefined;
|
|
31
|
+
let inlineUserPreviewModulePromise: Promise<InlineUserPreviewModule> | undefined;
|
|
32
|
+
let recentImagesModulePromise: Promise<RecentImagesModule> | undefined;
|
|
33
|
+
const imagePreviewDisplayRegistrations = new WeakSet<ExtensionAPI>();
|
|
34
|
+
const inlineUserPreviewRegistrations = new WeakSet<ExtensionAPI>();
|
|
35
|
+
const previewRegistrationPromises = new WeakMap<ExtensionAPI, Promise<void>>();
|
|
36
|
+
|
|
37
|
+
function loadClipboardModule(): Promise<ClipboardModule> {
|
|
38
|
+
clipboardModulePromise ??= import("./clipboard.js");
|
|
39
|
+
return clipboardModulePromise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadImagePreviewModule(): Promise<ImagePreviewModule> {
|
|
43
|
+
imagePreviewModulePromise ??= import("./image-preview.js");
|
|
44
|
+
return imagePreviewModulePromise;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadInlineUserPreviewModule(): Promise<InlineUserPreviewModule> {
|
|
48
|
+
inlineUserPreviewModulePromise ??= import("./inline-user-preview.js");
|
|
49
|
+
return inlineUserPreviewModulePromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadRecentImagesModule(): Promise<RecentImagesModule> {
|
|
53
|
+
recentImagesModulePromise ??= import("./recent-images.js");
|
|
54
|
+
return recentImagesModulePromise;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function ensureImagePreviewDisplayRegistered(
|
|
58
|
+
pi: ExtensionAPI,
|
|
59
|
+
logger: DebugLogger,
|
|
60
|
+
): Promise<ImagePreviewModule> {
|
|
61
|
+
const module = await loadImagePreviewModule();
|
|
62
|
+
if (!imagePreviewDisplayRegistrations.has(pi)) {
|
|
63
|
+
imagePreviewDisplayRegistrations.add(pi);
|
|
64
|
+
module.registerImagePreviewDisplay(pi, { logger });
|
|
65
|
+
}
|
|
66
|
+
return module;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensurePreviewRegistrations(pi: ExtensionAPI, logger: DebugLogger): Promise<void> {
|
|
70
|
+
let registrationPromise = previewRegistrationPromises.get(pi);
|
|
71
|
+
if (!registrationPromise) {
|
|
72
|
+
registrationPromise = (async () => {
|
|
73
|
+
await ensureImagePreviewDisplayRegistered(pi, logger);
|
|
74
|
+
|
|
75
|
+
if (!inlineUserPreviewRegistrations.has(pi)) {
|
|
76
|
+
const module = await loadInlineUserPreviewModule();
|
|
77
|
+
inlineUserPreviewRegistrations.add(pi);
|
|
78
|
+
module.registerInlineUserImagePreview(pi, { logger });
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
previewRegistrationPromises.set(pi, registrationPromise);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await registrationPromise;
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
function imageToBase64(image: ClipboardImage): string {
|
|
33
88
|
assertImageWithinByteLimit(image.bytes.length, "Image attachment");
|
|
34
89
|
return Buffer.from(image.bytes).toString("base64");
|
|
@@ -59,6 +114,20 @@ function removeAttachmentIndicators(text: string): string {
|
|
|
59
114
|
return withoutMarkers.replace(/\n{3,}/g, "\n\n").trim();
|
|
60
115
|
}
|
|
61
116
|
|
|
117
|
+
async function cacheImageForRecentPicker(ctx: PasteContext, image: ClipboardImage): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
const { persistImageToRecentCache } = await loadRecentImagesModule();
|
|
120
|
+
persistImageToRecentCache(image);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (ctx.hasUI) {
|
|
123
|
+
ctx.ui.notify(
|
|
124
|
+
`Image attached, but failed to cache for /paste-image recent: ${getErrorMessage(error)}`,
|
|
125
|
+
"warning",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
62
131
|
function queueImageAttachment(
|
|
63
132
|
ctx: PasteContext,
|
|
64
133
|
pendingImages: PendingImage[],
|
|
@@ -73,16 +142,7 @@ function queueImageAttachment(
|
|
|
73
142
|
});
|
|
74
143
|
|
|
75
144
|
if (options.cacheForRecentPicker) {
|
|
76
|
-
|
|
77
|
-
persistImageToRecentCache(image);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (ctx.hasUI) {
|
|
80
|
-
ctx.ui.notify(
|
|
81
|
-
`Image attached, but failed to cache for /paste-image recent: ${getErrorMessage(error)}`,
|
|
82
|
-
"warning",
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
145
|
+
void cacheImageForRecentPicker(ctx, image);
|
|
86
146
|
}
|
|
87
147
|
|
|
88
148
|
if (!ctx.hasUI) {
|
|
@@ -93,12 +153,14 @@ function queueImageAttachment(
|
|
|
93
153
|
ctx.ui.notify(successMessage, "info");
|
|
94
154
|
}
|
|
95
155
|
|
|
96
|
-
function buildRecentImageEmptyStateMessage(
|
|
156
|
+
function buildRecentImageEmptyStateMessage(
|
|
157
|
+
searchedDirectories: readonly string[],
|
|
158
|
+
cacheDirectory: string,
|
|
159
|
+
): string {
|
|
97
160
|
const searched =
|
|
98
161
|
searchedDirectories.length > 0
|
|
99
162
|
? searchedDirectories.join("; ")
|
|
100
163
|
: "No directories configured";
|
|
101
|
-
const cacheDirectory = getRecentImageCacheDirectory();
|
|
102
164
|
|
|
103
165
|
return [
|
|
104
166
|
`No recent images found. Searched: ${searched}`,
|
|
@@ -114,6 +176,7 @@ async function showRecentSelectionPreview(
|
|
|
114
176
|
cwd: string,
|
|
115
177
|
logger: DebugLogger,
|
|
116
178
|
): Promise<void> {
|
|
179
|
+
const { buildPreviewItems } = await ensureImagePreviewDisplayRegistered(pi, logger);
|
|
117
180
|
const previewItems = await buildPreviewItems(
|
|
118
181
|
[
|
|
119
182
|
{
|
|
@@ -151,8 +214,15 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
151
214
|
pasteImageShortcutsConfigured: config.shortcuts.pasteImage !== undefined,
|
|
152
215
|
});
|
|
153
216
|
|
|
154
|
-
|
|
155
|
-
|
|
217
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
218
|
+
try {
|
|
219
|
+
const { setActiveTerminalImageSettingsCwd } = await import("./terminal-image-width.js");
|
|
220
|
+
setActiveTerminalImageSettingsCwd(ctx.cwd);
|
|
221
|
+
await ensurePreviewRegistrations(pi, logger);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logger.log("preview.lazy_registration_failed", { error: getErrorMessage(error) });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
156
226
|
|
|
157
227
|
const pasteImageFromClipboard = async (ctx: PasteContext): Promise<void> => {
|
|
158
228
|
if (!ctx.hasUI) {
|
|
@@ -160,6 +230,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
160
230
|
}
|
|
161
231
|
|
|
162
232
|
try {
|
|
233
|
+
const { readClipboardImage } = await loadClipboardModule();
|
|
163
234
|
const image = await readClipboardImage();
|
|
164
235
|
if (!image) {
|
|
165
236
|
ctx.ui.notify("No image found in clipboard.", "warning");
|
|
@@ -185,9 +256,21 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
185
256
|
}
|
|
186
257
|
|
|
187
258
|
try {
|
|
259
|
+
const {
|
|
260
|
+
discoverRecentImages,
|
|
261
|
+
formatRecentImageLabel,
|
|
262
|
+
getRecentImageCacheDirectory,
|
|
263
|
+
loadRecentImage,
|
|
264
|
+
} = await loadRecentImagesModule();
|
|
188
265
|
const discovery = discoverRecentImages();
|
|
189
266
|
if (discovery.candidates.length === 0) {
|
|
190
|
-
ctx.ui.notify(
|
|
267
|
+
ctx.ui.notify(
|
|
268
|
+
buildRecentImageEmptyStateMessage(
|
|
269
|
+
discovery.searchedDirectories,
|
|
270
|
+
getRecentImageCacheDirectory(),
|
|
271
|
+
),
|
|
272
|
+
"warning",
|
|
273
|
+
);
|
|
191
274
|
return;
|
|
192
275
|
}
|
|
193
276
|
|