pi-image-tools 1.2.1 → 1.3.1
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 +23 -0
- package/README.md +10 -7
- package/package.json +69 -69
- package/src/clipboard.ts +54 -291
- package/src/config.ts +24 -15
- package/src/image-size.ts +62 -63
- package/src/image-transcode.ts +198 -0
- package/src/index.ts +115 -32
- package/src/inline-user-preview.ts +225 -69
- package/src/keybindings.ts +61 -60
- 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/recent-images.ts +1 -0
- package/src/shell-environment.ts +75 -0
|
@@ -12,17 +12,11 @@ import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./i
|
|
|
12
12
|
import { buildSixelRenderLines, isInlineImageProtocolLine } from "./sixel-protocol.js";
|
|
13
13
|
import { setActiveTerminalImageSettingsCwd } from "./terminal-image-width.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type UserMessagePrototype = {
|
|
18
|
-
render: UserMessageRenderFn;
|
|
19
|
-
__piImageToolsInlineOriginalRender?: UserMessageRenderFn;
|
|
20
|
-
__piImageToolsInlinePatched?: boolean;
|
|
21
|
-
};
|
|
15
|
+
const INLINE_PREVIEW_PATCH_VERSION = "pi-image-tools-inline-preview-chat-component-v4";
|
|
16
|
+
const PREPARE_SUBMITTED_PREVIEW_TIMEOUT_MS = 2_500;
|
|
22
17
|
|
|
23
18
|
type UserMessageInstance = {
|
|
24
|
-
|
|
25
|
-
__piImageToolsInlineItems?: ImagePreviewItem[];
|
|
19
|
+
render?: (width: number) => string[];
|
|
26
20
|
};
|
|
27
21
|
|
|
28
22
|
type InteractiveModePrototype = {
|
|
@@ -31,6 +25,7 @@ type InteractiveModePrototype = {
|
|
|
31
25
|
__piImageToolsOriginalAddMessageToChat?: (message: unknown, options?: unknown) => void;
|
|
32
26
|
__piImageToolsOriginalGetUserMessageText?: (message: unknown) => string;
|
|
33
27
|
__piImageToolsPreviewPatched?: boolean;
|
|
28
|
+
__piImageToolsPreviewPatchVersion?: string;
|
|
34
29
|
};
|
|
35
30
|
|
|
36
31
|
interface UserImageContent {
|
|
@@ -46,8 +41,22 @@ interface UserMessageLike {
|
|
|
46
41
|
|
|
47
42
|
interface InteractiveModeLike {
|
|
48
43
|
chatContainer?: {
|
|
44
|
+
addChild?: (component: unknown) => void;
|
|
49
45
|
children?: unknown[];
|
|
50
46
|
};
|
|
47
|
+
ui?: {
|
|
48
|
+
requestRender?: () => void;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ImagePreviewChatComponent {
|
|
53
|
+
constructor(private readonly items: readonly ImagePreviewItem[]) {}
|
|
54
|
+
|
|
55
|
+
invalidate(): void {}
|
|
56
|
+
|
|
57
|
+
render(width: number): string[] {
|
|
58
|
+
return renderPreviewLines(this.items, width);
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
|
|
@@ -137,18 +146,13 @@ function toImageContent(value: unknown): UserImageContent | null {
|
|
|
137
146
|
};
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
if (userMessage.role !== "user") {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!Array.isArray(userMessage.content)) {
|
|
149
|
+
function extractImagePayloadsFromContent(content: unknown): ImagePayload[] {
|
|
150
|
+
if (!Array.isArray(content)) {
|
|
147
151
|
return [];
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
const payloads: ImagePayload[] = [];
|
|
151
|
-
for (const part of
|
|
155
|
+
for (const part of content) {
|
|
152
156
|
const image = toImageContent(part);
|
|
153
157
|
if (!image) {
|
|
154
158
|
continue;
|
|
@@ -164,76 +168,171 @@ function extractImagePayloads(message: unknown): ImagePayload[] {
|
|
|
164
168
|
return payloads;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
function extractImagePayloads(message: unknown): ImagePayload[] {
|
|
172
|
+
const userMessage = toUserMessage(message);
|
|
173
|
+
if (userMessage.role !== "user") {
|
|
174
|
+
return [];
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
return
|
|
177
|
+
return extractImagePayloadsFromContent(userMessage.content);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getImagePayloadSignature(images: readonly ImagePayload[]): string {
|
|
181
|
+
return images
|
|
182
|
+
.map((image) => `${image.mimeType}:${image.data.length}:${image.data.slice(0, 32)}:${image.data.slice(-32)}`)
|
|
183
|
+
.join("|");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface PreparedPreviewItems {
|
|
187
|
+
signature: string;
|
|
188
|
+
items: ImagePreviewItem[];
|
|
173
189
|
}
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
const preparedPreviewItemsQueue: PreparedPreviewItems[] = [];
|
|
192
|
+
const inFlightPreviewItems = new Map<string, Promise<ImagePreviewItem[]>>();
|
|
193
|
+
|
|
194
|
+
function queuePreparedPreviewItems(images: readonly ImagePayload[], items: ImagePreviewItem[]): void {
|
|
195
|
+
if (images.length === 0 || items.length === 0) {
|
|
178
196
|
return;
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
preparedPreviewItemsQueue.push({
|
|
200
|
+
signature: getImagePayloadSignature(images),
|
|
201
|
+
items,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (preparedPreviewItemsQueue.length > 8) {
|
|
205
|
+
preparedPreviewItemsQueue.splice(0, preparedPreviewItemsQueue.length - 8);
|
|
183
206
|
}
|
|
207
|
+
}
|
|
184
208
|
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
function consumePreparedPreviewItems(images: readonly ImagePayload[]): ImagePreviewItem[] | undefined {
|
|
210
|
+
if (images.length === 0 || preparedPreviewItemsQueue.length === 0) {
|
|
211
|
+
return undefined;
|
|
187
212
|
}
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
const signature = getImagePayloadSignature(images);
|
|
215
|
+
const index = preparedPreviewItemsQueue.findIndex((entry) => entry.signature === signature);
|
|
216
|
+
if (index === -1) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!Array.isArray(instance.__piImageToolsInlineItems)) {
|
|
199
|
-
instance.__piImageToolsInlineItems = [];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
220
|
+
const [entry] = preparedPreviewItemsQueue.splice(index, 1);
|
|
221
|
+
return entry?.items;
|
|
222
|
+
}
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
function getInFlightPreviewItems(images: readonly ImagePayload[]): Promise<ImagePreviewItem[]> | undefined {
|
|
225
|
+
if (images.length === 0) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return inFlightPreviewItems.get(getImagePayloadSignature(images));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildPreviewItemsOnce(
|
|
233
|
+
images: readonly ImagePayload[],
|
|
234
|
+
options: { cwd?: string; logger?: DebugLogger } = {},
|
|
235
|
+
): Promise<ImagePreviewItem[]> {
|
|
236
|
+
const signature = getImagePayloadSignature(images);
|
|
237
|
+
const existing = inFlightPreviewItems.get(signature);
|
|
238
|
+
if (existing) {
|
|
239
|
+
return existing;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const promise = buildPreviewItems(images, options);
|
|
243
|
+
promise.then(
|
|
244
|
+
() => inFlightPreviewItems.delete(signature),
|
|
245
|
+
() => inFlightPreviewItems.delete(signature),
|
|
246
|
+
);
|
|
247
|
+
promise.catch(() => {
|
|
248
|
+
// Consumers log contextual errors; this prevents unhandled rejections when prebuild times out.
|
|
249
|
+
});
|
|
250
|
+
inFlightPreviewItems.set(signature, promise);
|
|
251
|
+
return promise;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function waitForPreviewItemsDeadline(
|
|
255
|
+
previewItemsPromise: Promise<ImagePreviewItem[]>,
|
|
256
|
+
): Promise<ImagePreviewItem[] | undefined> {
|
|
257
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
return await Promise.race([
|
|
261
|
+
previewItemsPromise,
|
|
262
|
+
new Promise<undefined>((resolve) => {
|
|
263
|
+
timeout = setTimeout(() => resolve(undefined), PREPARE_SUBMITTED_PREVIEW_TIMEOUT_MS);
|
|
264
|
+
timeout.unref?.();
|
|
265
|
+
}),
|
|
266
|
+
]);
|
|
267
|
+
} finally {
|
|
268
|
+
if (timeout) {
|
|
269
|
+
clearTimeout(timeout);
|
|
207
270
|
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
208
273
|
|
|
209
|
-
|
|
210
|
-
|
|
274
|
+
function imagePlaceholderText(count: number): string {
|
|
275
|
+
if (count <= 1) {
|
|
276
|
+
return "[ 1 image attached]";
|
|
277
|
+
}
|
|
211
278
|
|
|
212
|
-
|
|
279
|
+
return `[ ${count} images attached]`;
|
|
213
280
|
}
|
|
214
281
|
|
|
215
|
-
function
|
|
282
|
+
function isUserMessageComponentLike(value: unknown): value is UserMessageInstance {
|
|
283
|
+
if (value instanceof UserMessageComponent) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isRecord(value) || typeof value.render !== "function") {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const constructorName =
|
|
292
|
+
typeof value.constructor === "function" && typeof value.constructor.name === "string"
|
|
293
|
+
? value.constructor.name
|
|
294
|
+
: undefined;
|
|
295
|
+
|
|
296
|
+
return constructorName === "UserMessageComponent";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function requestInteractiveModeRender(mode: InteractiveModeLike): void {
|
|
300
|
+
try {
|
|
301
|
+
mode.ui?.requestRender?.();
|
|
302
|
+
} catch {
|
|
303
|
+
// Rendering will be retried by the next TUI update.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function addPreviewItemsAfterLatestUserMessage(
|
|
216
308
|
mode: InteractiveModeLike,
|
|
217
309
|
fromChildIndex: number,
|
|
218
310
|
previewItems: ImagePreviewItem[],
|
|
219
|
-
):
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
311
|
+
): boolean {
|
|
312
|
+
const chatContainer = mode.chatContainer;
|
|
313
|
+
const children = chatContainer?.children;
|
|
314
|
+
if (!Array.isArray(children) || children.length === 0 || previewItems.length === 0) {
|
|
315
|
+
return false;
|
|
223
316
|
}
|
|
224
317
|
|
|
225
318
|
const start = Math.max(0, fromChildIndex);
|
|
226
319
|
for (let index = children.length - 1; index >= start; index -= 1) {
|
|
227
320
|
const child = children[index];
|
|
228
|
-
if (!(child
|
|
321
|
+
if (!isUserMessageComponentLike(child)) {
|
|
229
322
|
continue;
|
|
230
323
|
}
|
|
231
324
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
325
|
+
const previewComponent = new ImagePreviewChatComponent(previewItems);
|
|
326
|
+
const insertIndex = index + 1;
|
|
327
|
+
if (insertIndex >= children.length && typeof chatContainer?.addChild === "function") {
|
|
328
|
+
chatContainer.addChild(previewComponent);
|
|
329
|
+
} else {
|
|
330
|
+
children.splice(insertIndex, 0, previewComponent);
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
236
333
|
}
|
|
334
|
+
|
|
335
|
+
return false;
|
|
237
336
|
}
|
|
238
337
|
|
|
239
338
|
function logInlinePreviewError(
|
|
@@ -262,7 +361,10 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
262
361
|
prototype.__piImageToolsOriginalAddMessageToChat = prototype.addMessageToChat;
|
|
263
362
|
}
|
|
264
363
|
|
|
265
|
-
if (
|
|
364
|
+
if (
|
|
365
|
+
prototype.__piImageToolsPreviewPatched &&
|
|
366
|
+
prototype.__piImageToolsPreviewPatchVersion === INLINE_PREVIEW_PATCH_VERSION
|
|
367
|
+
) {
|
|
266
368
|
return;
|
|
267
369
|
}
|
|
268
370
|
|
|
@@ -299,13 +401,26 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
299
401
|
return;
|
|
300
402
|
}
|
|
301
403
|
|
|
302
|
-
|
|
404
|
+
const preparedPreviewItems = consumePreparedPreviewItems(imagePayloads);
|
|
405
|
+
if (preparedPreviewItems) {
|
|
406
|
+
if (addPreviewItemsAfterLatestUserMessage(mode, beforeCount, preparedPreviewItems)) {
|
|
407
|
+
requestInteractiveModeRender(mode);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const previewItemsPromise = getInFlightPreviewItems(imagePayloads)
|
|
413
|
+
?? buildPreviewItemsOnce(imagePayloads, { logger });
|
|
414
|
+
|
|
415
|
+
void previewItemsPromise
|
|
303
416
|
.then((previewItems) => {
|
|
304
417
|
if (previewItems.length === 0) {
|
|
305
418
|
return;
|
|
306
419
|
}
|
|
307
420
|
|
|
308
|
-
|
|
421
|
+
if (addPreviewItemsAfterLatestUserMessage(mode, beforeCount, previewItems)) {
|
|
422
|
+
requestInteractiveModeRender(mode);
|
|
423
|
+
}
|
|
309
424
|
})
|
|
310
425
|
.catch((error: unknown) => {
|
|
311
426
|
logInlinePreviewError(logger, "inline-user-preview.build_preview_failed", error);
|
|
@@ -313,6 +428,7 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
313
428
|
};
|
|
314
429
|
|
|
315
430
|
prototype.__piImageToolsPreviewPatched = true;
|
|
431
|
+
prototype.__piImageToolsPreviewPatchVersion = INLINE_PREVIEW_PATCH_VERSION;
|
|
316
432
|
}
|
|
317
433
|
|
|
318
434
|
export interface RegisterInlineUserImagePreviewOptions {
|
|
@@ -323,15 +439,16 @@ export function registerInlineUserImagePreview(
|
|
|
323
439
|
pi: ExtensionAPI,
|
|
324
440
|
options: RegisterInlineUserImagePreviewOptions = {},
|
|
325
441
|
): void {
|
|
442
|
+
const applyPatch = (): void => {
|
|
443
|
+
try {
|
|
444
|
+
patchInteractiveMode(options.logger);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
logInlinePreviewError(options.logger, "inline-user-preview.patch_failed", error);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
326
450
|
const runPatch = (delayMs: number): void => {
|
|
327
|
-
setTimeout(
|
|
328
|
-
try {
|
|
329
|
-
patchInteractiveMode(options.logger);
|
|
330
|
-
patchUserMessageRender();
|
|
331
|
-
} catch (error) {
|
|
332
|
-
logInlinePreviewError(options.logger, "inline-user-preview.patch_failed", error);
|
|
333
|
-
}
|
|
334
|
-
}, delayMs);
|
|
451
|
+
setTimeout(applyPatch, delayMs);
|
|
335
452
|
};
|
|
336
453
|
|
|
337
454
|
const schedulePatch = (): void => {
|
|
@@ -342,18 +459,57 @@ export function registerInlineUserImagePreview(
|
|
|
342
459
|
const handleSessionEvent = (eventName: string, cwd: string | undefined): void => {
|
|
343
460
|
try {
|
|
344
461
|
setActiveTerminalImageSettingsCwd(cwd);
|
|
462
|
+
applyPatch();
|
|
345
463
|
schedulePatch();
|
|
346
464
|
} catch (error) {
|
|
347
465
|
logInlinePreviewError(options.logger, `inline-user-preview.${eventName}_failed`, error);
|
|
348
466
|
}
|
|
349
467
|
};
|
|
350
468
|
|
|
469
|
+
const prepareSubmittedImagePreview = async (
|
|
470
|
+
event: { images?: unknown },
|
|
471
|
+
cwd: string | undefined,
|
|
472
|
+
): Promise<{ imagePayloads: ImagePayload[]; previewItems: ImagePreviewItem[] } | undefined> => {
|
|
473
|
+
try {
|
|
474
|
+
setActiveTerminalImageSettingsCwd(cwd);
|
|
475
|
+
applyPatch();
|
|
476
|
+
const imagePayloads = extractImagePayloadsFromContent(event.images);
|
|
477
|
+
if (imagePayloads.length === 0) {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const previewItemsPromise = buildPreviewItemsOnce(imagePayloads, { cwd, logger: options.logger });
|
|
482
|
+
const previewItems = await waitForPreviewItemsDeadline(previewItemsPromise);
|
|
483
|
+
if (!previewItems || previewItems.length === 0) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return { imagePayloads, previewItems };
|
|
488
|
+
} catch (error) {
|
|
489
|
+
logInlinePreviewError(options.logger, "inline-user-preview.prepare_submitted_preview_failed", error);
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
applyPatch();
|
|
495
|
+
|
|
351
496
|
pi.on("session_start", async (_event, ctx) => {
|
|
352
497
|
handleSessionEvent("session_start", ctx.cwd);
|
|
353
498
|
});
|
|
354
499
|
|
|
355
|
-
pi.on("before_agent_start", async (
|
|
500
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
356
501
|
handleSessionEvent("before_agent_start", ctx.cwd);
|
|
502
|
+
if (!ctx.hasUI) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const preparedPreview = await prepareSubmittedImagePreview(event, ctx.cwd);
|
|
507
|
+
if (!preparedPreview) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
queuePreparedPreviewItems(preparedPreview.imagePayloads, preparedPreview.previewItems);
|
|
512
|
+
return undefined;
|
|
357
513
|
});
|
|
358
514
|
|
|
359
515
|
const onSessionSwitch = pi.on as unknown as (
|
package/src/keybindings.ts
CHANGED
|
@@ -72,49 +72,51 @@ const LEGACY_KEYBINDING_NAME_MIGRATIONS: Record<string, string> = {
|
|
|
72
72
|
deleteSessionNoninvasive: "app.session.deleteNoninvasive",
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
function getAppKeybindingDefaults(platform: NodeJS.Platform): KeybindingDefaults {
|
|
76
|
+
return {
|
|
77
|
+
"app.interrupt": "escape",
|
|
78
|
+
"app.clear": "ctrl+c",
|
|
79
|
+
"app.exit": "ctrl+d",
|
|
80
|
+
"app.suspend": platform === "win32" ? [] : "ctrl+z",
|
|
81
|
+
"app.thinking.cycle": "shift+tab",
|
|
82
|
+
"app.model.cycleForward": "ctrl+p",
|
|
83
|
+
"app.model.cycleBackward": "shift+ctrl+p",
|
|
84
|
+
"app.model.select": "ctrl+l",
|
|
85
|
+
"app.tools.expand": "ctrl+o",
|
|
86
|
+
"app.thinking.toggle": "ctrl+t",
|
|
87
|
+
"app.session.toggleNamedFilter": "ctrl+n",
|
|
88
|
+
"app.editor.external": "ctrl+g",
|
|
89
|
+
"app.message.followUp": "alt+enter",
|
|
90
|
+
"app.message.dequeue": "alt+up",
|
|
91
|
+
"app.clipboard.pasteImage": platform === "win32" ? "alt+v" : "ctrl+v",
|
|
92
|
+
"app.session.new": [],
|
|
93
|
+
"app.session.tree": [],
|
|
94
|
+
"app.session.fork": [],
|
|
95
|
+
"app.session.resume": [],
|
|
96
|
+
"app.tree.foldOrUp": ["ctrl+left", "alt+left"],
|
|
97
|
+
"app.tree.unfoldOrDown": ["ctrl+right", "alt+right"],
|
|
98
|
+
"app.tree.editLabel": "shift+l",
|
|
99
|
+
"app.tree.toggleLabelTimestamp": "shift+t",
|
|
100
|
+
"app.session.togglePath": "ctrl+p",
|
|
101
|
+
"app.session.toggleSort": "ctrl+s",
|
|
102
|
+
"app.session.rename": "ctrl+r",
|
|
103
|
+
"app.session.delete": "ctrl+d",
|
|
104
|
+
"app.session.deleteNoninvasive": "ctrl+backspace",
|
|
105
|
+
"app.models.save": "ctrl+s",
|
|
106
|
+
"app.models.enableAll": "ctrl+a",
|
|
107
|
+
"app.models.clearAll": "ctrl+x",
|
|
108
|
+
"app.models.toggleProvider": "ctrl+p",
|
|
109
|
+
"app.models.reorderUp": "alt+up",
|
|
110
|
+
"app.models.reorderDown": "alt+down",
|
|
111
|
+
"app.tree.filter.default": "ctrl+d",
|
|
112
|
+
"app.tree.filter.noTools": "ctrl+t",
|
|
113
|
+
"app.tree.filter.userOnly": "ctrl+u",
|
|
114
|
+
"app.tree.filter.labeledOnly": "ctrl+l",
|
|
115
|
+
"app.tree.filter.all": "ctrl+a",
|
|
116
|
+
"app.tree.filter.cycleForward": "ctrl+o",
|
|
117
|
+
"app.tree.filter.cycleBackward": "shift+ctrl+o",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
118
120
|
|
|
119
121
|
export interface RegisterImagePasteKeybindingsOptions {
|
|
120
122
|
config: ImageToolsConfig;
|
|
@@ -186,7 +188,7 @@ function normalizeUserKeybindings(rawConfig: Record<string, unknown> | undefined
|
|
|
186
188
|
return userKeybindings;
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
function getBuiltinKeybindingDefaults(): KeybindingDefaults {
|
|
191
|
+
function getBuiltinKeybindingDefaults(platform: NodeJS.Platform): KeybindingDefaults {
|
|
190
192
|
const tuiDefaults: KeybindingDefaults = {};
|
|
191
193
|
for (const [keybinding, definition] of Object.entries(TUI_KEYBINDINGS)) {
|
|
192
194
|
tuiDefaults[keybinding] = definition.defaultKeys;
|
|
@@ -194,12 +196,12 @@ function getBuiltinKeybindingDefaults(): KeybindingDefaults {
|
|
|
194
196
|
|
|
195
197
|
return {
|
|
196
198
|
...tuiDefaults,
|
|
197
|
-
...
|
|
199
|
+
...getAppKeybindingDefaults(platform),
|
|
198
200
|
};
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
function getConfiguredBuiltinShortcuts(): Set<string> {
|
|
202
|
-
const defaults = getBuiltinKeybindingDefaults();
|
|
203
|
+
function getConfiguredBuiltinShortcuts(platform: NodeJS.Platform): Set<string> {
|
|
204
|
+
const defaults = getBuiltinKeybindingDefaults(platform);
|
|
203
205
|
const userKeybindings = normalizeUserKeybindings(readKeybindingsConfig());
|
|
204
206
|
const builtinShortcuts = new Set<string>();
|
|
205
207
|
|
|
@@ -224,26 +226,21 @@ function getImagePasteShortcutCandidates(platform: NodeJS.Platform): KeyId[] {
|
|
|
224
226
|
return ["ctrl+v", "alt+v", "ctrl+alt+v"];
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
function removeBuiltinConflicts(shortcuts: readonly KeyId[]): KeyId[] {
|
|
228
|
-
const builtinShortcuts = getConfiguredBuiltinShortcuts();
|
|
229
|
+
function removeBuiltinConflicts(shortcuts: readonly KeyId[], platform: NodeJS.Platform): KeyId[] {
|
|
230
|
+
const builtinShortcuts = getConfiguredBuiltinShortcuts(platform);
|
|
229
231
|
|
|
230
232
|
return shortcuts.filter((shortcut) => !builtinShortcuts.has(normalizeShortcutKey(shortcut)));
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
function getImagePasteShortcuts(
|
|
235
|
+
export function getImagePasteShortcuts(
|
|
234
236
|
config: ImageToolsConfig,
|
|
235
237
|
platform: NodeJS.Platform = process.platform,
|
|
236
238
|
): KeyId[] {
|
|
237
239
|
const shouldAvoidBuiltinConflicts =
|
|
238
240
|
config.shortcuts.avoidBuiltinConflicts || config.shortcuts.suppressBuiltinConflictWarnings;
|
|
241
|
+
const candidates = config.shortcuts.pasteImage ?? getImagePasteShortcutCandidates(platform);
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
return shouldAvoidBuiltinConflicts
|
|
242
|
-
? removeBuiltinConflicts(config.shortcuts.pasteImage)
|
|
243
|
-
: config.shortcuts.pasteImage;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return removeBuiltinConflicts(getImagePasteShortcutCandidates(platform));
|
|
243
|
+
return shouldAvoidBuiltinConflicts ? removeBuiltinConflicts(candidates, platform) : [...candidates];
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
export function registerImagePasteKeybindings(
|
|
@@ -251,16 +248,20 @@ export function registerImagePasteKeybindings(
|
|
|
251
248
|
handler: PasteImageHandler,
|
|
252
249
|
options: RegisterImagePasteKeybindingsOptions,
|
|
253
250
|
): void {
|
|
251
|
+
const platform = process.platform;
|
|
254
252
|
const configuredShortcuts = options.config.shortcuts.pasteImage;
|
|
255
|
-
const
|
|
256
|
-
const
|
|
253
|
+
const requestedShortcuts = configuredShortcuts ?? getImagePasteShortcutCandidates(platform);
|
|
254
|
+
const shortcuts = getImagePasteShortcuts(options.config, platform);
|
|
255
|
+
const skippedShortcuts = requestedShortcuts.filter(
|
|
257
256
|
(shortcut) => !shortcuts.some((registeredShortcut) => normalizeShortcutKey(registeredShortcut) === normalizeShortcutKey(shortcut)),
|
|
258
|
-
)
|
|
257
|
+
);
|
|
259
258
|
|
|
260
259
|
options.logger.log("keybindings.register", {
|
|
261
260
|
avoidBuiltinConflicts: options.config.shortcuts.avoidBuiltinConflicts,
|
|
262
261
|
suppressBuiltinConflictWarnings: options.config.shortcuts.suppressBuiltinConflictWarnings,
|
|
263
262
|
configured: configuredShortcuts !== undefined,
|
|
263
|
+
platform,
|
|
264
|
+
requestedShortcuts,
|
|
264
265
|
registeredShortcuts: shortcuts,
|
|
265
266
|
skippedShortcuts,
|
|
266
267
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { isErrnoException } from "../errors.js";
|
|
4
|
+
|
|
5
|
+
export const LIST_TYPES_TIMEOUT_MS = 1000;
|
|
6
|
+
export const READ_TIMEOUT_MS = 5000;
|
|
7
|
+
export const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export interface CommandResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
stdout: Buffer;
|
|
12
|
+
stderr: Buffer;
|
|
13
|
+
missingCommand: boolean;
|
|
14
|
+
status: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CommandRunOptions {
|
|
18
|
+
environment?: NodeJS.ProcessEnv;
|
|
19
|
+
maxBuffer?: number;
|
|
20
|
+
timeout: number;
|
|
21
|
+
windowsHide?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CommandRunner = (
|
|
25
|
+
command: string,
|
|
26
|
+
args: readonly string[],
|
|
27
|
+
options: CommandRunOptions,
|
|
28
|
+
) => CommandResult;
|
|
29
|
+
|
|
30
|
+
function toBuffer(value: unknown): Buffer {
|
|
31
|
+
if (Buffer.isBuffer(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value instanceof Uint8Array) {
|
|
36
|
+
return Buffer.from(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Buffer.from(value === undefined || value === null ? "" : String(value), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const defaultCommandRunner: CommandRunner = (command, args, options) => {
|
|
43
|
+
const result = spawnSync(command, [...args], {
|
|
44
|
+
env: options.environment,
|
|
45
|
+
maxBuffer: options.maxBuffer ?? MAX_BUFFER_BYTES,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
timeout: options.timeout,
|
|
48
|
+
windowsHide: options.windowsHide,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
stdout: toBuffer(result.stdout),
|
|
55
|
+
stderr: toBuffer(result.stderr),
|
|
56
|
+
missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
|
|
57
|
+
status: result.status ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: result.status === 0,
|
|
63
|
+
stdout: toBuffer(result.stdout),
|
|
64
|
+
stderr: toBuffer(result.stderr),
|
|
65
|
+
missingCommand: false,
|
|
66
|
+
status: result.status ?? null,
|
|
67
|
+
};
|
|
68
|
+
};
|