pi-orca-images 0.2.3 → 0.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 +4 -0
- package/README.md +14 -2
- package/package.json +1 -1
- package/src/commands.ts +1 -1
- package/src/index.ts +12 -1
- package/src/preview.ts +96 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Open image previews: each inline image gets an **Open image** link, and `/orca-images open` opens the latest image in macOS Preview. Previews are written to temporary files. Thanks to [@Ky1ezer0](https://github.com/Ky1ezer0) (#1).
|
|
6
|
+
|
|
3
7
|
## 0.2.3
|
|
4
8
|
|
|
5
9
|
- Fix: Pi crashed with "This extension ctx is stale after session replacement or reload" when an `[Image N]` label was in the prompt after `/resume`, `/new`, `/fork` or `/reload`. Pi hands extensions a Proxy over the TUI that ignores `delete`, so the old session's render hook was never removed and kept styling labels with its stale ctx.
|
package/README.md
CHANGED
|
@@ -43,21 +43,32 @@ Paste an image (or drop an image file) into Pi's prompt and the path is shown as
|
|
|
43
43
|
|
|
44
44
|
It recognizes pastes made only of existing image files (`png`, `jpg`, `gif`, `webp`, `bmp`, `tiff`, `heic`, `avif`): plain paths, `~/` paths, quoted or backslash-escaped paths, and `file://` URLs. Text that mixes a path with other words is left alone. This works in every terminal, not only Orca, including Pi's own clipboard paste. Set `PI_IMAGE_LABELS=0` to turn it off.
|
|
45
45
|
|
|
46
|
+
## Open image previews
|
|
47
|
+
|
|
48
|
+
Each rendered image gets an **Open image** file link beneath it. **Cmd-click** the link on macOS to open the preview through your terminal. In our Orca test, this opens an Orca image tab with zoom controls. Direct image clicks and hover-to-zoom are not supported.
|
|
49
|
+
|
|
50
|
+
On macOS, `/orca-images open` opens the last image rendered by Pi in **Preview**, using `open -a Preview`.
|
|
51
|
+
|
|
52
|
+
Preview copies are saved in a private `pi-orca-images-*` directory under the OS temporary directory, never in the project. Identical images reuse a file within each loaded extension instance. Files survive `/reload` and are deleted on normal Pi process exit; links expire when their files are removed. Crashes or forced termination can leave files behind, and long-running sessions can still accumulate previews. Older `.pi/image-previews/` folders are not automatically removed.
|
|
53
|
+
|
|
54
|
+
The saved bytes are those available to Pi and may already have been resized by a tool. Previews require automatic Orca detection and a writable temporary directory. They are inactive when `PI_ORCA_IMAGES=0` or an explicit `PI_IMAGE_PROTOCOL` overrides detection. The link and Preview command were tested with Pi 1.5.2 on macOS; opening through the command requires macOS Preview.
|
|
55
|
+
|
|
46
56
|
## Commands
|
|
47
57
|
|
|
48
58
|
| Command | Effect |
|
|
49
59
|
|---|---|
|
|
50
60
|
| `/orca-images` or `/orca-images status` | Shows the detection decision, Pi's active image protocol, and whether the repaint fix and image labels are on |
|
|
51
61
|
| `/orca-images test` | Draws a test gradient (it is not sent to the model) |
|
|
62
|
+
| `/orca-images open` | Opens the last rendered image in macOS Preview |
|
|
52
63
|
| `/orca-images redraw` | Re-sends every visible image right now |
|
|
53
64
|
|
|
54
65
|
## Configuration
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
The rendering fixes and preview links activate only in Orca; pasted image labels can work in any terminal. Your explicit choices always win:
|
|
57
68
|
|
|
58
69
|
| Setting | Effect |
|
|
59
70
|
|---|---|
|
|
60
|
-
| `PI_ORCA_IMAGES=0` | Disables the Orca image fixes |
|
|
71
|
+
| `PI_ORCA_IMAGES=0` | Disables the Orca image fixes and preview feature |
|
|
61
72
|
| `PI_IMAGE_LABELS=0` | Disables pasted image labels |
|
|
62
73
|
| `PI_IMAGE_PROTOCOL=…` | Used as is; the package does not override it |
|
|
63
74
|
| `terminal.images` in Pi settings | Applied by Pi on top of the package's choice |
|
|
@@ -69,6 +80,7 @@ It does nothing outside Orca. Your explicit choices always win:
|
|
|
69
80
|
- Pi 1.4 in its default `fullscreen` TUI mode. In `regular` mode detection still works, but the repaint fix and tab recovery do not apply.
|
|
70
81
|
- Not inside tmux or screen: image escapes do not reach Orca through them.
|
|
71
82
|
- Image labels wrap internal methods of Pi's editor (`handlePaste`, `segment`, `expandPasteMarkers`, `render`). If a Pi release changes them, pasted paths simply stay as paths.
|
|
83
|
+
- Preview links read internal fields of Pi's image component. If those fields change or preview copies cannot be saved, links are omitted.
|
|
72
84
|
- The repaint fix wraps two internal methods of Pi's TUI. If a Pi release renames them, `/orca-images status` reports the fix as `unavailable` and images fall back to the first-row behavior; detection keeps working.
|
|
73
85
|
|
|
74
86
|
## Development
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { completeSubcommand, SUBCOMMANDS } from "./commands.ts";
|
|
|
4
4
|
import { decide, type Decision, labelsEnabled, OWNED_ENV } from "./detect.ts";
|
|
5
5
|
import { type EditorWatcher, watchEditors } from "./labels.ts";
|
|
6
6
|
import { installShim, type Shim, type ShimTarget } from "./shim.ts";
|
|
7
|
+
import { installPreviews } from "./preview.ts";
|
|
7
8
|
import { formatStatus } from "./status.ts";
|
|
8
9
|
import { testImagePng } from "./test-image.ts";
|
|
9
10
|
|
|
@@ -32,8 +33,13 @@ export default function piOrcaImages(pi: ExtensionAPI) {
|
|
|
32
33
|
const labels = labelsEnabled(process.env);
|
|
33
34
|
let shim: Shim | undefined;
|
|
34
35
|
let watcher: EditorWatcher | undefined;
|
|
36
|
+
let previews: ReturnType<typeof installPreviews> | undefined;
|
|
35
37
|
|
|
36
38
|
pi.on("session_start", async (_event, ctx) => {
|
|
39
|
+
if (ctx.hasUI && decision.action === "apply") {
|
|
40
|
+
previews?.uninstall();
|
|
41
|
+
previews = installPreviews({ onError: (message) => ctx.ui.notify(message, "error") });
|
|
42
|
+
}
|
|
37
43
|
const repaint = decision.action === "apply" && decision.protocol === "kitty";
|
|
38
44
|
if (!ctx.hasUI || (!repaint && !labels)) return;
|
|
39
45
|
// The widget factory is the only public hook that hands an extension the TUI instance.
|
|
@@ -57,6 +63,8 @@ export default function piOrcaImages(pi: ExtensionAPI) {
|
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
pi.on("session_shutdown", async (event) => {
|
|
66
|
+
previews?.uninstall();
|
|
67
|
+
previews = undefined;
|
|
60
68
|
// The watcher styles labels through this session's ctx, which goes stale on replacement.
|
|
61
69
|
watcher?.uninstall();
|
|
62
70
|
watcher = undefined;
|
|
@@ -80,7 +88,7 @@ export default function piOrcaImages(pi: ExtensionAPI) {
|
|
|
80
88
|
});
|
|
81
89
|
|
|
82
90
|
pi.registerCommand(COMMAND, {
|
|
83
|
-
description: "Inline images in Orca: status, test, or
|
|
91
|
+
description: "Inline images in Orca: status, test, redraw, or open",
|
|
84
92
|
getArgumentCompletions: completeSubcommand,
|
|
85
93
|
handler: async (args, ctx) => {
|
|
86
94
|
const sub = (args ?? "").trim() || "status";
|
|
@@ -97,6 +105,9 @@ export default function piOrcaImages(pi: ExtensionAPI) {
|
|
|
97
105
|
);
|
|
98
106
|
} else if (sub === "test") {
|
|
99
107
|
pi.appendEntry(TEST_ENTRY, { width: 240, height: 120 });
|
|
108
|
+
} else if (sub === "open") {
|
|
109
|
+
if (previews) await previews.openLatest();
|
|
110
|
+
else ctx.ui.notify("Image previews are not active in this terminal.", "warning");
|
|
100
111
|
} else if (sub === "redraw") {
|
|
101
112
|
if (shim) shim.recover();
|
|
102
113
|
else ctx.ui.notify("pi-orca-images is not active in this terminal; nothing to redraw.", "warning");
|
package/src/preview.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { Image, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
const run = promisify(execFile);
|
|
11
|
+
const EXTENSIONS: Record<string, string> = {
|
|
12
|
+
"image/png": "png", "image/jpeg": "jpg", "image/gif": "gif",
|
|
13
|
+
"image/webp": "webp", "image/bmp": "bmp", "image/tiff": "tiff",
|
|
14
|
+
"image/avif": "avif", "image/heic": "heic",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let previewDirectory: string | undefined;
|
|
18
|
+
|
|
19
|
+
/** Retain temporary previews until this module's process exits, including across /reload. */
|
|
20
|
+
export function savePreview(data: string, mimeType: string): string {
|
|
21
|
+
const extension = EXTENSIONS[mimeType];
|
|
22
|
+
if (!extension) throw new Error(`Unsupported preview format: ${mimeType}`);
|
|
23
|
+
if (!previewDirectory) {
|
|
24
|
+
previewDirectory = mkdtempSync(join(tmpdir(), "pi-orca-images-"));
|
|
25
|
+
const directory = previewDirectory;
|
|
26
|
+
process.once("exit", () => {
|
|
27
|
+
try { rmSync(directory, { recursive: true, force: true }); } catch { /* OS temp cleanup is the fallback. */ }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const directory = previewDirectory;
|
|
31
|
+
const bytes = Buffer.from(data, "base64");
|
|
32
|
+
const path = join(directory, `${createHash("sha256").update(bytes).digest("hex")}.${extension}`);
|
|
33
|
+
writeFileSync(path, bytes, { mode: 0o600 });
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function openInPreview(path: string): Promise<void> {
|
|
38
|
+
if (process.platform !== "darwin") throw new Error("Opening previews requires macOS Preview.");
|
|
39
|
+
await run("/usr/bin/open", ["-a", "Preview", path], { timeout: 10_000 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PREVIEW = Symbol.for("pi-orca-images.preview");
|
|
43
|
+
type PreviewHost = typeof Image.prototype & { [PREVIEW]?: { uninstall(): void } };
|
|
44
|
+
|
|
45
|
+
export function installPreviews(options: {
|
|
46
|
+
open?: (path: string) => Promise<void>;
|
|
47
|
+
save?: (data: string, mimeType: string) => string;
|
|
48
|
+
onError: (message: string) => void;
|
|
49
|
+
}) {
|
|
50
|
+
const prototype = Image.prototype as PreviewHost;
|
|
51
|
+
prototype[PREVIEW]?.uninstall();
|
|
52
|
+
const originalRender = prototype.render;
|
|
53
|
+
const paths = new WeakMap<Image, string | null>();
|
|
54
|
+
let latest: string | undefined;
|
|
55
|
+
let active = true;
|
|
56
|
+
const open = async (path: string) => {
|
|
57
|
+
try {
|
|
58
|
+
await (options.open ?? openInPreview)(path);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (active) options.onError(`Could not open image in Preview: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
prototype.render = function (width) {
|
|
64
|
+
const lines = originalRender.call(this, width);
|
|
65
|
+
if (!active) return lines;
|
|
66
|
+
if (!paths.has(this)) {
|
|
67
|
+
// Pi exposes no image-data accessor; fail closed if its internal fields change.
|
|
68
|
+
const source = this as unknown as { base64Data?: string; mimeType?: string };
|
|
69
|
+
try {
|
|
70
|
+
paths.set(this, typeof source.base64Data === "string" && typeof source.mimeType === "string"
|
|
71
|
+
? (options.save ?? savePreview)(source.base64Data, source.mimeType) : null);
|
|
72
|
+
} catch {
|
|
73
|
+
paths.set(this, null);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const path = paths.get(this);
|
|
77
|
+
if (!path) return lines;
|
|
78
|
+
latest = path;
|
|
79
|
+
const label = truncateToWidth("Open image", width);
|
|
80
|
+
return [...lines, `\x1b]8;;${pathToFileURL(path).href}\x07\x1b[4m${label}\x1b[24m\x1b]8;;\x07`];
|
|
81
|
+
};
|
|
82
|
+
const preview = {
|
|
83
|
+
async openLatest() {
|
|
84
|
+
if (latest) await open(latest);
|
|
85
|
+
else options.onError("No image has been rendered yet. Try /orca-images test first.");
|
|
86
|
+
},
|
|
87
|
+
uninstall() {
|
|
88
|
+
if (!active) return;
|
|
89
|
+
active = false;
|
|
90
|
+
prototype.render = originalRender;
|
|
91
|
+
if (prototype[PREVIEW] === preview) delete prototype[PREVIEW];
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
prototype[PREVIEW] = preview;
|
|
95
|
+
return preview;
|
|
96
|
+
}
|