pi-image-tools 1.3.1 → 1.4.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 +135 -123
- package/README.md +10 -5
- package/package.json +13 -4
- package/src/config.ts +3 -0
- package/src/debug-logger.ts +4 -2
- package/src/image-preview.ts +25 -32
- package/src/image-transcode.ts +9 -1
- package/src/index.ts +23 -27
- package/src/inline-user-preview.ts +512 -523
- package/src/powershell.ts +82 -56
- package/src/preview-logging.ts +28 -0
- package/src/providers/command-runner.ts +19 -1
- package/src/providers/mac-osascript-pngf.ts +62 -101
- package/src/providers/mac-osascript-publicpng.ts +38 -77
- package/src/providers/mac-pngpaste.ts +30 -69
- package/src/providers/powershell-forms.ts +8 -16
- package/src/providers/provider-helpers.ts +196 -0
- package/src/providers/wl-paste.ts +46 -81
- package/src/providers/xclip.ts +51 -87
- package/src/recent-images.ts +54 -83
package/CHANGELOG.md
CHANGED
|
@@ -1,123 +1,135 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [Unreleased]
|
|
4
|
-
|
|
5
|
-
## [1.
|
|
6
|
-
|
|
7
|
-
###
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- Added
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
## [1.0.
|
|
79
|
-
|
|
80
|
-
### Changed
|
|
81
|
-
- Updated
|
|
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
|
-
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.4.0] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added an `enabled` config toggle to disable the extension without uninstalling. ([7c176b9](https://github.com/MasuRii/pi-image-tools/commit/7c176b9402e0681f23e91258fde4b5f8139aa14d))
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Widened Pi peer dependency ranges to 0.80 and added security dependency overrides. ([04f6847](https://github.com/MasuRii/pi-image-tools/commit/04f6847f3d29f50c63d5b069293b2dbff2f7bc64))
|
|
12
|
+
- Extracted a shared provider base class and preview logging to reduce duplication. ([7c176b9](https://github.com/MasuRii/pi-image-tools/commit/7c176b9402e0681f23e91258fde4b5f8139aa14d))
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Restricted clipboard and transcode commands to an allowlist to prevent command injection. ([27c9a0b](https://github.com/MasuRii/pi-image-tools/commit/27c9a0b7f549bb248ff2544243c5f0dc7299b562))
|
|
16
|
+
|
|
17
|
+
## [1.3.1] - 2026-06-16
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Default paste-image shortcuts now avoid Pi built-in shortcut conflicts on all platforms, including macOS. Previously macOS defaulted to overriding Pi's built-in `ctrl+v` image paste binding and printed a startup conflict warning. (reported by @gaodes in [#8](https://github.com/MasuRii/pi-image-tools/issues/8))
|
|
21
|
+
- Transcode clipboard images that arrive in MIME types model providers reject (most notably `image/bmp` from WSLg's Wayland clipboard bridge for Windows clipboard images, and `image/tiff` from some macOS sources) into PNG via ImageMagick before attaching them. Previously, pasting a Windows screenshot inside WSL produced an `Unknown image type: image/bmp` error from the model provider that blocked the message from being sent. (contributed by @codeviking428 in [#9](https://github.com/MasuRii/pi-image-tools/pull/9))
|
|
22
|
+
|
|
23
|
+
## [1.3.0] - 2026-06-01
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Added a provider registry for platform-specific clipboard readers.
|
|
27
|
+
- Added macOS `pngpaste` and `osascript` clipboard fallbacks.
|
|
28
|
+
- Added an inline preview queue and chat component for user image previews.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Deferred preview, clipboard, and recent-image module loading from the extension entrypoint.
|
|
32
|
+
- Made shortcut conflict defaults platform-aware.
|
|
33
|
+
- Updated Pi peer dependency ranges and the optional clipboard dependency for 0.78-compatible runtimes.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Improved shell and session handling for clipboard command providers.
|
|
37
|
+
|
|
38
|
+
## [1.2.1] - 2026-05-26
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Widened `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` peer dependency ranges to `^0.74.0 || ^0.75.0`.
|
|
42
|
+
|
|
43
|
+
## [1.2.0] - 2026-05-22
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Added Linux Sixel previews through `img2sixel` detection and conversion when the converter is available on `PATH`.
|
|
47
|
+
- Added asynchronous image preview conversion so user-message previews can fall back without blocking the TUI.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Updated Pi peer dependencies and runtime imports to the `@earendil-works` scope.
|
|
51
|
+
- Improved preview setup diagnostics so missing Sixel converters report actionable fallback warnings.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Hardened debug logging with redaction and asynchronous file writes while keeping logging disabled by default.
|
|
55
|
+
|
|
56
|
+
## [1.1.0] - 2026-05-03
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Added centralized image MIME, image-size, PowerShell, and error utilities used by clipboard, preview, attachment, and recent-image flows.
|
|
60
|
+
- Added size-limit enforcement for image attachments, recent-cache writes, recent-image loads, and Sixel preview conversion through `PI_IMAGE_TOOLS_MAX_IMAGE_BYTES`.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- Reworked recent-image caching to use an extension-owned cache directory with safe pruning that preserves user files.
|
|
64
|
+
- Reworked Sixel preview rendering to use an existing PowerShell `Sixel` module only, normalize converter output into complete terminal sequences, and fall back to native previews with actionable warnings.
|
|
65
|
+
- Reworked terminal image width resolution to honor Pi project/global `terminal.imageWidthCells` settings with a documented default fallback.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Prevented extension debug logging from writing terminal output; debug events now remain file-based and disabled by default.
|
|
69
|
+
- Preserved Sixel, Kitty, and iTerm inline image protocol rows during preview width fitting.
|
|
70
|
+
|
|
71
|
+
## [1.0.11] - 2026-04-25
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- Avoid Pi's built-in image paste shortcut by default while preserving the previous primary shortcut when users disable or rebind `app.clipboard.pasteImage` (thanks to @danielcherubini for reporting this in PR #3).
|
|
75
|
+
- Replaced the placeholder `enabled` config with validated `debug`, explicit `shortcuts.pasteImage`, configurable built-in conflict avoidance, and built-in warning suppression settings.
|
|
76
|
+
- Removed obsolete packaged README image assets now that the README uses a GitHub-hosted image.
|
|
77
|
+
|
|
78
|
+
## [1.0.9] - 2026-04-01
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- Updated README image to use HTML tag for better npm display compatibility
|
|
82
|
+
- Added npm keywords for improved package discoverability
|
|
83
|
+
- Added Related Pi Extensions cross-linking section to README
|
|
84
|
+
|
|
85
|
+
## [1.0.8] - 2026-04-01
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
- Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.64.0
|
|
89
|
+
|
|
90
|
+
## [1.0.7] - 2026-03-23
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
|
|
94
|
+
|
|
95
|
+
## [1.0.6] - 2026-03-12
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- Updated AWS SDK client-bedrock-runtime to 3.1005.0
|
|
99
|
+
|
|
100
|
+
## [1.0.5] - 2026-03-12
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
- Updated AWS SDK client-bedrock-runtime to 3.1005.0
|
|
104
|
+
|
|
105
|
+
## [1.0.4] - 2026-03-07
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
- Added Linux clipboard image support via `wl-paste` and `xclip` fallback readers.
|
|
109
|
+
- Added Linux and macOS default recent-image discovery locations so the recent picker works beyond Windows.
|
|
110
|
+
- Added non-Windows image paste shortcuts including `Ctrl+V` in addition to the existing alternate bindings.
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
- Updated README documentation to reflect current cross-platform support, clipboard backends, recent-image discovery behavior, and inline preview details.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
- Removed the Windows-only extension gate so supported non-Windows platforms can register commands and shortcuts.
|
|
117
|
+
- Preserved Kitty and iTerm inline image protocol rows during preview width fitting, alongside the existing Sixel-safe handling.
|
|
118
|
+
- Improved clipboard reader error handling so unsupported environments report missing backends more clearly.
|
|
119
|
+
|
|
120
|
+
## [1.0.3] - 2026-03-04
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
- Use absolute GitHub raw URL for README image to fix npm display
|
|
124
|
+
|
|
125
|
+
## [1.0.2] - 2026-03-04
|
|
126
|
+
|
|
127
|
+
### Changed
|
|
128
|
+
- Rewrote README.md with professional documentation standards
|
|
129
|
+
- Added comprehensive feature documentation, configuration reference, and usage examples
|
|
130
|
+
|
|
131
|
+
## 1.0.0
|
|
132
|
+
|
|
133
|
+
- Standardized repository layout to `src/` + root shim entrypoint.
|
|
134
|
+
- Added TypeScript/Bundler project config, package metadata, and publish whitelist.
|
|
135
|
+
- Added standard docs, license, and initial runtime config files.
|
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# pi-image-tools
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/pi-image-tools)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[]()
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
[](https://ko-fi.com/Y8Y01PSSVR)
|
|
8
10
|
|
|
11
|
+
Image attachment and preview extension for the **Pi coding agent**.
|
|
12
|
+
`pi-image-tools` lets you attach clipboard images or recent screenshots to your next user message, then preview them inline in the TUI before the message is sent.
|
|
9
13
|
<img width="1360" height="752" alt="image" src="https://github.com/user-attachments/assets/c7b462c4-6316-495e-a3fa-5a7e22edd91f" />
|
|
10
14
|
|
|
15
|
+
</div>
|
|
11
16
|
|
|
12
17
|
## Features
|
|
13
18
|
|
|
@@ -349,4 +354,4 @@ npm run check
|
|
|
349
354
|
|
|
350
355
|
## License
|
|
351
356
|
|
|
352
|
-
MIT
|
|
357
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-image-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Image attachment and rendering extension for Pi TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"build": "npm run typecheck",
|
|
21
21
|
"lint": "npm run typecheck",
|
|
22
22
|
"test": "node --test test/*.test.js test/providers/*.test.js",
|
|
23
|
-
"check": "npm run build && npm run test"
|
|
23
|
+
"check": "npm run build && npm run test",
|
|
24
|
+
"postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"pi-package",
|
|
@@ -60,10 +61,18 @@
|
|
|
60
61
|
]
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0",
|
|
64
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0"
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0"
|
|
65
66
|
},
|
|
66
67
|
"optionalDependencies": {
|
|
67
68
|
"@mariozechner/clipboard": "^0.3.9"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"protobufjs": "7.6.3",
|
|
72
|
+
"ws": "8.21.0"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
76
|
+
"@earendil-works/pi-tui": "^0.80.3"
|
|
68
77
|
}
|
|
69
78
|
}
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ImageToolsShortcutConfig {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface ImageToolsConfig {
|
|
17
|
+
enabled: boolean;
|
|
17
18
|
debug: boolean;
|
|
18
19
|
shortcuts: ImageToolsShortcutConfig;
|
|
19
20
|
}
|
|
@@ -129,6 +130,7 @@ function parseConfig(rawConfig: unknown, path: string, platform: NodeJS.Platform
|
|
|
129
130
|
|
|
130
131
|
return {
|
|
131
132
|
debug: parseBoolean(rawConfig.debug, "debug", path),
|
|
133
|
+
enabled: parseBoolean(rawConfig.enabled, "enabled", path, true),
|
|
132
134
|
shortcuts: parseShortcutConfig(rawConfig.shortcuts, path, platform),
|
|
133
135
|
};
|
|
134
136
|
}
|
|
@@ -137,6 +139,7 @@ export function loadImageToolsConfig(path = getConfigPath(), options: LoadImageT
|
|
|
137
139
|
const platform = options.platform ?? process.platform;
|
|
138
140
|
if (!existsSync(path)) {
|
|
139
141
|
return {
|
|
142
|
+
enabled: true,
|
|
140
143
|
debug: false,
|
|
141
144
|
shortcuts: getDefaultShortcutConfig(platform),
|
|
142
145
|
};
|
package/src/debug-logger.ts
CHANGED
|
@@ -53,8 +53,10 @@ export class DebugLogger {
|
|
|
53
53
|
() => this.appendLine(line),
|
|
54
54
|
);
|
|
55
55
|
void this.writeQueue.catch(() => undefined);
|
|
56
|
-
} catch {
|
|
57
|
-
// Debug logging must never affect extension behavior
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Debug logging must never affect extension behavior; the write queue
|
|
58
|
+
// is left untouched so subsequent log calls continue to drain normally.
|
|
59
|
+
void error;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
package/src/image-preview.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { DebugLogger } from "./debug-logger.js";
|
|
|
19
19
|
import { getErrorMessage } from "./errors.js";
|
|
20
20
|
import { getBase64DecodedByteLength, assertImageWithinByteLimit } from "./image-size.js";
|
|
21
21
|
import { mimeTypeToExtension } from "./image-mime.js";
|
|
22
|
+
import { logPreviewEvent, logPreviewHandlerError } from "./preview-logging.js";
|
|
22
23
|
import { runBufferedCommand, runPowerShellCommandAsync, type BufferedCommandResult, type PowerShellCommandResult, type RunPowerShellCommandOptions } from "./powershell.js";
|
|
23
24
|
import { buildSixelRenderLines, ensureCompleteSixelSequence } from "./sixel-protocol.js";
|
|
24
25
|
import {
|
|
@@ -130,14 +131,28 @@ const sixelAvailabilityState: SixelAvailability = {
|
|
|
130
131
|
available: false,
|
|
131
132
|
};
|
|
132
133
|
|
|
134
|
+
function resolveSixelAvailabilityState(
|
|
135
|
+
forceRefresh: boolean,
|
|
136
|
+
useCache: boolean,
|
|
137
|
+
): SixelAvailability {
|
|
138
|
+
return useCache ? sixelAvailabilityState : { checked: false, available: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function shouldShortCircuitSixelCheck(
|
|
142
|
+
state: SixelAvailability,
|
|
143
|
+
forceRefresh: boolean,
|
|
144
|
+
): boolean {
|
|
145
|
+
return state.checked && !forceRefresh;
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
async function ensureSixelModuleAvailable(
|
|
134
149
|
forceRefresh = false,
|
|
135
150
|
powerShellRunner: SixelPowerShellRunner = runPowerShellCommandAsync,
|
|
136
151
|
): Promise<SixelAvailability> {
|
|
137
152
|
const useCache = powerShellRunner === runPowerShellCommandAsync;
|
|
138
|
-
const state
|
|
153
|
+
const state = resolveSixelAvailabilityState(forceRefresh, useCache);
|
|
139
154
|
|
|
140
|
-
if (state
|
|
155
|
+
if (shouldShortCircuitSixelCheck(state, forceRefresh)) {
|
|
141
156
|
return state;
|
|
142
157
|
}
|
|
143
158
|
|
|
@@ -185,9 +200,9 @@ async function ensureLinuxSixelConverterAvailable(
|
|
|
185
200
|
processRunner: SixelProcessRunner = runBufferedCommand,
|
|
186
201
|
): Promise<SixelAvailability> {
|
|
187
202
|
const useCache = processRunner === runBufferedCommand;
|
|
188
|
-
const state
|
|
203
|
+
const state = resolveSixelAvailabilityState(forceRefresh, useCache);
|
|
189
204
|
|
|
190
|
-
if (state
|
|
205
|
+
if (shouldShortCircuitSixelCheck(state, forceRefresh)) {
|
|
191
206
|
return state;
|
|
192
207
|
}
|
|
193
208
|
|
|
@@ -335,7 +350,9 @@ Write-Output $rendered
|
|
|
335
350
|
} finally {
|
|
336
351
|
try {
|
|
337
352
|
rmSync(tempBaseDir, { recursive: true, force: true });
|
|
338
|
-
} catch {
|
|
353
|
+
} catch (error) {
|
|
354
|
+
// Temp-directory cleanup is best-effort; the OS reclaims leftover dirs.
|
|
355
|
+
void error;
|
|
339
356
|
}
|
|
340
357
|
}
|
|
341
358
|
}
|
|
@@ -415,18 +432,6 @@ export type BuildPreviewItemsOptions = TerminalImageWidthOptions & {
|
|
|
415
432
|
sixelPowerShellRunner?: SixelPowerShellRunner;
|
|
416
433
|
};
|
|
417
434
|
|
|
418
|
-
function logSixelEvent(
|
|
419
|
-
logger: DebugLogger | undefined,
|
|
420
|
-
event: string,
|
|
421
|
-
fields: Record<string, unknown> = {},
|
|
422
|
-
): void {
|
|
423
|
-
try {
|
|
424
|
-
logger?.log(event, fields);
|
|
425
|
-
} catch {
|
|
426
|
-
// Debug logging is best-effort and must never block image rendering.
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
435
|
export async function buildPreviewItems(
|
|
431
436
|
images: readonly ImagePayload[],
|
|
432
437
|
options: BuildPreviewItemsOptions = {},
|
|
@@ -445,7 +450,7 @@ export async function buildPreviewItems(
|
|
|
445
450
|
? await ensureSixelConverterAvailable(false, platform, processRunner, powerShellRunner)
|
|
446
451
|
: undefined;
|
|
447
452
|
|
|
448
|
-
|
|
453
|
+
logPreviewEvent(options.logger, "image-preview.sixel.detected", {
|
|
449
454
|
attemptSixel,
|
|
450
455
|
available: sixelState?.available ?? false,
|
|
451
456
|
converter: sixelState?.converter ?? null,
|
|
@@ -460,7 +465,7 @@ export async function buildPreviewItems(
|
|
|
460
465
|
if (attemptSixel && sixelState?.available && sixelState.converter) {
|
|
461
466
|
const conversion = await convertImageToSixelSequence(image, sixelState.converter, processRunner, powerShellRunner);
|
|
462
467
|
if (conversion.sequence) {
|
|
463
|
-
|
|
468
|
+
logPreviewEvent(options.logger, "image-preview.sixel.converted", {
|
|
464
469
|
converter: sixelState.converter,
|
|
465
470
|
mimeType: image.mimeType,
|
|
466
471
|
rows,
|
|
@@ -476,7 +481,7 @@ export async function buildPreviewItems(
|
|
|
476
481
|
continue;
|
|
477
482
|
}
|
|
478
483
|
|
|
479
|
-
|
|
484
|
+
logPreviewEvent(options.logger, "image-preview.sixel.conversion_failed", {
|
|
480
485
|
converter: sixelState.converter,
|
|
481
486
|
mimeType: image.mimeType,
|
|
482
487
|
error: conversion.error ?? "unknown",
|
|
@@ -513,18 +518,6 @@ export interface RegisterImagePreviewDisplayOptions {
|
|
|
513
518
|
logger?: DebugLogger;
|
|
514
519
|
}
|
|
515
520
|
|
|
516
|
-
function logPreviewHandlerError(
|
|
517
|
-
logger: DebugLogger | undefined,
|
|
518
|
-
event: string,
|
|
519
|
-
error: unknown,
|
|
520
|
-
): void {
|
|
521
|
-
try {
|
|
522
|
-
logger?.log(event, { error: getErrorMessage(error) });
|
|
523
|
-
} catch {
|
|
524
|
-
// Debug logging is best-effort inside Pi event handlers.
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
521
|
export function registerImagePreviewDisplay(
|
|
529
522
|
pi: ExtensionAPI,
|
|
530
523
|
options: RegisterImagePreviewDisplayOptions = {},
|
package/src/image-transcode.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ClipboardImage } from "./types.js";
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_PRIMARY_TRANSCODE_TOOL = "magick";
|
|
7
7
|
const LEGACY_UNIX_TRANSCODE_TOOL = "convert";
|
|
8
|
+
const ALLOWED_TRANSCODE_TOOLS = new Set([DEFAULT_PRIMARY_TRANSCODE_TOOL, LEGACY_UNIX_TRANSCODE_TOOL]);
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Maximum wall-clock time for a single ImageMagick invocation. Bounded so a
|
|
@@ -72,6 +73,10 @@ function getDefaultTranscodeTools(platform?: NodeJS.Platform): readonly string[]
|
|
|
72
73
|
return [DEFAULT_PRIMARY_TRANSCODE_TOOL, LEGACY_UNIX_TRANSCODE_TOOL];
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function isAllowedTranscodeTool(tool: string): boolean {
|
|
77
|
+
return ALLOWED_TRANSCODE_TOOLS.has(tool.trim().toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
function describeTranscodeTools(tools: readonly string[]): string {
|
|
76
81
|
const uniqueTools = [...new Set(tools.length > 0 ? tools : [DEFAULT_PRIMARY_TRANSCODE_TOOL])];
|
|
77
82
|
if (uniqueTools.length === 1) {
|
|
@@ -114,7 +119,7 @@ export interface TranscodeRunner {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
const defaultTranscodeRunner: TranscodeRunner = (command, args, input) =>
|
|
117
|
-
spawnSync(command, args as string[], {
|
|
122
|
+
spawnSync(command, args as string[], { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- command is validated by isAllowedTranscodeTool before the default runner is reached; args are fixed ImageMagick stdin/stdout specs and shell is disabled.
|
|
118
123
|
input: Buffer.from(input),
|
|
119
124
|
maxBuffer: TRANSCODE_MAX_BUFFER_BYTES,
|
|
120
125
|
timeout: TRANSCODE_TIMEOUT_MS,
|
|
@@ -167,6 +172,9 @@ export function transcodeToSupportedFormat(
|
|
|
167
172
|
|
|
168
173
|
const failures: string[] = [];
|
|
169
174
|
for (const tool of tools) {
|
|
175
|
+
if (!isAllowedTranscodeTool(tool)) {
|
|
176
|
+
throw new Error(`Transcode tool "${tool}" is not an allowed ImageMagick executable. Allowed tools: ${describeTranscodeTools([...ALLOWED_TRANSCODE_TOOLS])}.`);
|
|
177
|
+
}
|
|
170
178
|
const result = runner(tool, [inputSpec, "png:-"], image.bytes);
|
|
171
179
|
if (result.error) {
|
|
172
180
|
failures.push(`${tool}: ${result.error.message}`);
|
package/src/index.ts
CHANGED
|
@@ -26,39 +26,32 @@ type ImagePreviewModule = typeof import("./image-preview.js");
|
|
|
26
26
|
type InlineUserPreviewModule = typeof import("./inline-user-preview.js");
|
|
27
27
|
type RecentImagesModule = typeof import("./recent-images.js");
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
let imagePreviewModulePromise: Promise<ImagePreviewModule> | undefined;
|
|
31
|
-
let inlineUserPreviewModulePromise: Promise<InlineUserPreviewModule> | undefined;
|
|
32
|
-
let recentImagesModulePromise: Promise<RecentImagesModule> | undefined;
|
|
29
|
+
const lazyModules = new Map<string, Promise<unknown>>();
|
|
33
30
|
const imagePreviewDisplayRegistrations = new WeakSet<ExtensionAPI>();
|
|
34
31
|
const inlineUserPreviewRegistrations = new WeakSet<ExtensionAPI>();
|
|
35
32
|
const previewRegistrationPromises = new WeakMap<ExtensionAPI, Promise<void>>();
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function loadInlineUserPreviewModule(): Promise<InlineUserPreviewModule> {
|
|
48
|
-
inlineUserPreviewModulePromise ??= import("./inline-user-preview.js");
|
|
49
|
-
return inlineUserPreviewModulePromise;
|
|
50
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* Lazily import and cache a module by key. The factory uses a literal
|
|
36
|
+
* `import("./path.js")` so TypeScript keeps full module-type inference; the
|
|
37
|
+
* key retains a single in-flight promise per module across calls.
|
|
38
|
+
*/
|
|
39
|
+
function loadModule<T>(key: string, factory: () => Promise<T>): Promise<T> {
|
|
40
|
+
const cached = lazyModules.get(key);
|
|
41
|
+
if (cached !== undefined) {
|
|
42
|
+
return cached as Promise<T>;
|
|
43
|
+
}
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
45
|
+
const promise = factory();
|
|
46
|
+
lazyModules.set(key, promise);
|
|
47
|
+
return promise;
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
async function ensureImagePreviewDisplayRegistered(
|
|
58
51
|
pi: ExtensionAPI,
|
|
59
52
|
logger: DebugLogger,
|
|
60
53
|
): Promise<ImagePreviewModule> {
|
|
61
|
-
const module = await
|
|
54
|
+
const module = await loadModule<ImagePreviewModule>("image-preview", () => import("./image-preview.js"));
|
|
62
55
|
if (!imagePreviewDisplayRegistrations.has(pi)) {
|
|
63
56
|
imagePreviewDisplayRegistrations.add(pi);
|
|
64
57
|
module.registerImagePreviewDisplay(pi, { logger });
|
|
@@ -68,12 +61,12 @@ async function ensureImagePreviewDisplayRegistered(
|
|
|
68
61
|
|
|
69
62
|
async function ensurePreviewRegistrations(pi: ExtensionAPI, logger: DebugLogger): Promise<void> {
|
|
70
63
|
let registrationPromise = previewRegistrationPromises.get(pi);
|
|
71
|
-
if (
|
|
64
|
+
if (registrationPromise === undefined) {
|
|
72
65
|
registrationPromise = (async () => {
|
|
73
66
|
await ensureImagePreviewDisplayRegistered(pi, logger);
|
|
74
67
|
|
|
75
68
|
if (!inlineUserPreviewRegistrations.has(pi)) {
|
|
76
|
-
const module = await
|
|
69
|
+
const module = await loadModule<InlineUserPreviewModule>("inline-user-preview", () => import("./inline-user-preview.js"));
|
|
77
70
|
inlineUserPreviewRegistrations.add(pi);
|
|
78
71
|
module.registerInlineUserImagePreview(pi, { logger });
|
|
79
72
|
}
|
|
@@ -116,7 +109,7 @@ function removeAttachmentIndicators(text: string): string {
|
|
|
116
109
|
|
|
117
110
|
async function cacheImageForRecentPicker(ctx: PasteContext, image: ClipboardImage): Promise<void> {
|
|
118
111
|
try {
|
|
119
|
-
const { persistImageToRecentCache } = await
|
|
112
|
+
const { persistImageToRecentCache } = await loadModule<RecentImagesModule>("recent-images", () => import("./recent-images.js"));
|
|
120
113
|
persistImageToRecentCache(image);
|
|
121
114
|
} catch (error) {
|
|
122
115
|
if (ctx.hasUI) {
|
|
@@ -206,6 +199,9 @@ async function showRecentSelectionPreview(
|
|
|
206
199
|
|
|
207
200
|
export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
208
201
|
const config = loadImageToolsConfig();
|
|
202
|
+
if (!config.enabled) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
209
205
|
const logger = DebugLogger.create(config);
|
|
210
206
|
const pendingImages: PendingImage[] = [];
|
|
211
207
|
|
|
@@ -230,7 +226,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
230
226
|
}
|
|
231
227
|
|
|
232
228
|
try {
|
|
233
|
-
const { readClipboardImage } = await
|
|
229
|
+
const { readClipboardImage } = await loadModule<ClipboardModule>("clipboard", () => import("./clipboard.js"));
|
|
234
230
|
const image = await readClipboardImage();
|
|
235
231
|
if (!image) {
|
|
236
232
|
ctx.ui.notify("No image found in clipboard.", "warning");
|
|
@@ -261,7 +257,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
|
261
257
|
formatRecentImageLabel,
|
|
262
258
|
getRecentImageCacheDirectory,
|
|
263
259
|
loadRecentImage,
|
|
264
|
-
} = await
|
|
260
|
+
} = await loadModule<RecentImagesModule>("recent-images", () => import("./recent-images.js"));
|
|
265
261
|
const discovery = discoverRecentImages();
|
|
266
262
|
if (discovery.candidates.length === 0) {
|
|
267
263
|
ctx.ui.notify(
|