@timurproko/a1 0.1.8-dev.282 → 0.1.8-dev.289

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.
Files changed (45) hide show
  1. package/bin/ui.js +14 -2
  2. package/dist/contracts/owned-ui/image-attachments.d.ts +29 -0
  3. package/dist/contracts/owned-ui/image-attachments.js +60 -0
  4. package/dist/contracts/owned-ui/index.d.ts +1 -0
  5. package/dist/contracts/owned-ui/index.js +1 -0
  6. package/dist/contracts/owned-ui/validation.js +2 -9
  7. package/dist/features/owned-ui/run.js +16 -1
  8. package/dist/foundation/release/bootstrap.js +2 -1
  9. package/dist/foundation/terminal-cleanup/fatal-exit.d.ts +12 -0
  10. package/dist/foundation/terminal-cleanup/fatal-exit.js +85 -0
  11. package/dist/foundation/terminal-cleanup/index.d.ts +2 -0
  12. package/dist/foundation/terminal-cleanup/index.js +2 -0
  13. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +4 -0
  14. package/dist/foundation/terminal-cleanup/terminal-reset.js +40 -0
  15. package/dist/integrations/pi/components/owned-editor-ux.d.ts +6 -0
  16. package/dist/integrations/pi/components/owned-editor-ux.js +71 -0
  17. package/dist/integrations/pi/components/shell-editor-autocomplete.js +2 -0
  18. package/dist/integrations/pi/components/shell-shared-facade.d.ts +6 -1
  19. package/dist/integrations/pi/session-ui/clipboard-image.d.ts +2 -2
  20. package/dist/integrations/pi/session-ui/clipboard-image.js +13 -4
  21. package/dist/integrations/pi/session-ui/image-preparation-client.d.ts +20 -0
  22. package/dist/integrations/pi/session-ui/image-preparation-client.js +145 -0
  23. package/dist/integrations/pi/session-ui/image-preparation.d.ts +18 -0
  24. package/dist/integrations/pi/session-ui/image-preparation.js +126 -0
  25. package/dist/integrations/pi/session-ui/image-source.d.ts +13 -0
  26. package/dist/integrations/pi/session-ui/image-source.js +171 -0
  27. package/dist/integrations/pi/session-ui/image-worker.d.ts +17 -0
  28. package/dist/integrations/pi/session-ui/image-worker.js +23 -0
  29. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +11 -2
  30. package/dist/integrations/pi/session-ui/prompt-chips.js +114 -4
  31. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +8 -3
  32. package/dist/integrations/pi/session-ui/session-shell-root.js +27 -2
  33. package/dist/integrations/pi/session-ui/session-shell.js +179 -52
  34. package/dist/integrations/pi/session-ui/session-viewport-controller.js +17 -7
  35. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +2 -2
  36. package/dist/integrations/pi/session-ui/system-clipboard.js +25 -14
  37. package/dist/integrations/pi/tui-runtime/adapter.js +13 -2
  38. package/dist/native/darwin-arm64/manifest.json +1 -1
  39. package/dist/native/linux-x64/manifest.json +3 -3
  40. package/dist/native/linux-x64/process-guardian +0 -0
  41. package/dist/native/win32-x64/manifest.json +2 -2
  42. package/dist/native/win32-x64/process-guardian.exe +0 -0
  43. package/dist/runtime-payload-inventory.json +22 -18
  44. package/docs/architecture/boundaries.md +2 -1
  45. package/package.json +2 -1
@@ -0,0 +1,145 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
3
+ import { IMAGE_PREPARATION_MS, MAX_SOURCE_IMAGE_BYTES } from "./image-source.js";
4
+ /** Owns bounded paste acquisition and one off-thread conversion, canceled on session disposal. */
5
+ export class ImagePreparationClient {
6
+ #active = new Set();
7
+ #queue = [];
8
+ #stopping = new Set();
9
+ #converting = false;
10
+ #disposed = false;
11
+ start(read, limits = {}) {
12
+ const controller = new AbortController();
13
+ if (this.#disposed || this.#active.size >= 8) {
14
+ const result = Promise.reject(new ImageAttachmentError(this.#disposed ? "image-canceled" : "image-busy"));
15
+ void result.catch(() => { });
16
+ return { result, cancel: () => { } };
17
+ }
18
+ this.#active.add(controller);
19
+ const deadline = setTimeout(() => controller.abort(new ImageAttachmentError("image-timeout")), IMAGE_PREPARATION_MS);
20
+ const result = new Promise((resolve, reject) => {
21
+ controller.signal.addEventListener("abort", () => reject(abortError(controller.signal)), { once: true });
22
+ // Concurrency: the pending marker gets a render opportunity before any acquisition starts.
23
+ setImmediate(() => {
24
+ if (controller.signal.aborted)
25
+ return;
26
+ const acquire = read === undefined
27
+ ? runImageWorker({ kind: "clipboard" }, controller.signal)
28
+ : Promise.resolve().then(() => read(controller.signal));
29
+ void acquire.then(content => {
30
+ if (controller.signal.aborted)
31
+ return;
32
+ if (content?.kind !== "image") {
33
+ resolve(content);
34
+ return;
35
+ }
36
+ if (Math.ceil(content.data.length / 4) * 3 > MAX_SOURCE_IMAGE_BYTES + 2)
37
+ throw new ImageAttachmentError("image-source-size");
38
+ this.#queue.push({ signal: controller.signal, run: async () => {
39
+ try {
40
+ const image = await runImageWorker({ kind: "prepare", source: content, limits }, controller.signal);
41
+ resolve({ kind: "image", ...image });
42
+ }
43
+ catch (error) {
44
+ reject(error);
45
+ }
46
+ } });
47
+ this.#drain();
48
+ }).catch(reject);
49
+ });
50
+ }).finally(() => {
51
+ clearTimeout(deadline);
52
+ this.#active.delete(controller);
53
+ const stopped = waitForImageWorkers(controller.signal);
54
+ this.#stopping.add(stopped);
55
+ void stopped.finally(() => this.#stopping.delete(stopped));
56
+ const index = this.#queue.findIndex(item => item.signal === controller.signal);
57
+ if (index >= 0)
58
+ this.#queue.splice(index, 1);
59
+ });
60
+ void result.catch(() => { });
61
+ return { result, cancel: () => controller.abort(new ImageAttachmentError("image-canceled")) };
62
+ }
63
+ async dispose() {
64
+ this.#disposed = true;
65
+ for (const controller of this.#active)
66
+ controller.abort(new ImageAttachmentError("image-canceled"));
67
+ this.#queue.length = 0;
68
+ await Promise.resolve();
69
+ await Promise.all(this.#stopping);
70
+ }
71
+ #drain() {
72
+ if (this.#converting)
73
+ return;
74
+ const next = this.#queue.shift();
75
+ if (next === undefined)
76
+ return;
77
+ if (next.signal.aborted) {
78
+ this.#drain();
79
+ return;
80
+ }
81
+ this.#converting = true;
82
+ void next.run().finally(() => { this.#converting = false; this.#drain(); });
83
+ }
84
+ }
85
+ const ACTIVE_IMAGE_WORKERS = new Map();
86
+ async function waitForImageWorkers(signal) {
87
+ await Promise.all([...ACTIVE_IMAGE_WORKERS].filter(([, owner]) => owner === signal)
88
+ .map(([worker]) => new Promise(resolve => { worker.once("exit", () => resolve()); })));
89
+ }
90
+ export function runImageWorker(request, signal) {
91
+ return new Promise((resolve, reject) => {
92
+ if (signal.aborted) {
93
+ reject(abortError(signal));
94
+ return;
95
+ }
96
+ if (ACTIVE_IMAGE_WORKERS.size >= 8) {
97
+ reject(new ImageAttachmentError("image-busy"));
98
+ return;
99
+ }
100
+ // Compatibility: production uses the emitted sibling; source tests load TypeScript through tsx.
101
+ const source = import.meta.url.endsWith(".ts");
102
+ const entry = new URL(source ? "./image-worker.ts" : "./image-worker.js", import.meta.url);
103
+ const worker = source
104
+ ? new Worker(`import('tsx/esm/api').then(({ tsImport }) => tsImport(${JSON.stringify(entry.href)}, ${JSON.stringify(import.meta.url)}))`, { eval: true, workerData: request, stdout: true, stderr: true })
105
+ : new Worker(entry, { workerData: request, stdout: true, stderr: true });
106
+ // Security: native/codec output is not trusted diagnostics and must never reach the terminal or logs.
107
+ worker.stdout.resume();
108
+ worker.stderr.resume();
109
+ ACTIVE_IMAGE_WORKERS.set(worker, signal);
110
+ let outcome;
111
+ let stop;
112
+ const requestStop = () => {
113
+ if (stop !== undefined)
114
+ return;
115
+ worker.postMessage("cancel");
116
+ // Security: allow clipboard subprocess abort first; a busy codec cannot handle messages.
117
+ stop = setTimeout(() => { void worker.terminate().catch(() => { }); }, 50);
118
+ stop.unref();
119
+ };
120
+ const abort = () => { outcome = { error: abortError(signal) }; requestStop(); };
121
+ signal.addEventListener("abort", abort, { once: true });
122
+ worker.once("message", (message) => {
123
+ if (!signal.aborted)
124
+ outcome = message?.ok === true
125
+ ? { value: message.value }
126
+ : { error: new ImageAttachmentError(message?.code ?? "image-codec") };
127
+ requestStop();
128
+ });
129
+ worker.once("error", () => { outcome = { error: new ImageAttachmentError("image-codec") }; requestStop(); });
130
+ worker.once("exit", () => {
131
+ ACTIVE_IMAGE_WORKERS.delete(worker);
132
+ if (stop !== undefined)
133
+ clearTimeout(stop);
134
+ signal.removeEventListener("abort", abort);
135
+ // Concurrency: release the conversion slot only after the old executor has actually stopped.
136
+ if (outcome === undefined || outcome.error !== undefined)
137
+ reject(outcome?.error ?? new ImageAttachmentError("image-codec"));
138
+ else
139
+ resolve(outcome.value);
140
+ });
141
+ });
142
+ }
143
+ function abortError(signal) {
144
+ return signal.reason instanceof ImageAttachmentError ? signal.reason : new ImageAttachmentError("image-canceled");
145
+ }
@@ -0,0 +1,18 @@
1
+ export interface ImagePreparationLimits {
2
+ readonly encodedBytes?: number;
3
+ readonly decodedBytes?: number;
4
+ readonly maxDimension?: number;
5
+ }
6
+ export interface PreparedImage {
7
+ readonly data: string;
8
+ readonly mimeType: string;
9
+ readonly width: number;
10
+ readonly height: number;
11
+ readonly transformed: boolean;
12
+ }
13
+ export declare const IMAGE_TARGET_BASE64_BYTES: number;
14
+ export declare const IMAGE_TARGET_DECODED_BYTES: number;
15
+ export declare function prepareImage(source: {
16
+ readonly data: string;
17
+ readonly mimeType: string;
18
+ }, limits?: ImagePreparationLimits): Promise<PreparedImage>;
@@ -0,0 +1,126 @@
1
+ import { isMainThread } from "node:worker_threads";
2
+ import { ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
3
+ import { canonicalizeClipboardImage } from "./clipboard-image.js";
4
+ import { sourceImageInfo } from "./image-source.js";
5
+ export const IMAGE_TARGET_BASE64_BYTES = 4.5 * 1024 * 1024;
6
+ export const IMAGE_TARGET_DECODED_BYTES = 5 * 1024 * 1024;
7
+ export async function prepareImage(source, limits = {}) {
8
+ // Performance: an async signature does not move synchronous WASM work off the interactive thread.
9
+ if (isMainThread)
10
+ throw new Error("Image preparation requires a worker");
11
+ const canonical = canonicalizeClipboardImage(source, true);
12
+ if (canonical === null)
13
+ throw new ImageAttachmentError("image-data");
14
+ const bytes = Buffer.from(canonical.data, "base64");
15
+ const metadata = sourceImageInfo(bytes, canonical.mimeType);
16
+ const encodedLimit = Math.min(IMAGE_TARGET_BASE64_BYTES - 1, positive(limits.encodedBytes, Infinity));
17
+ const decodedLimit = Math.min(IMAGE_TARGET_DECODED_BYTES, positive(limits.decodedBytes, Infinity));
18
+ const maxDimension = positive(limits.maxDimension, Infinity);
19
+ const fits = (length) => length <= decodedLimit && 4 * Math.ceil(length / 3) <= encodedLimit;
20
+ if (fits(bytes.length) && Math.max(metadata.width, metadata.height) <= maxDimension) {
21
+ return { ...canonical, width: metadata.width, height: metadata.height, transformed: false };
22
+ }
23
+ if (metadata.conversionUnsafe)
24
+ throw new ImageAttachmentError("image-conversion");
25
+ const photon = await import("@silvia-odwyer/photon-node").catch(() => { throw new ImageAttachmentError("image-codec"); });
26
+ let image;
27
+ try {
28
+ image = photon.PhotonImage.new_from_byteslice(bytes);
29
+ if (image.get_width() !== metadata.width || image.get_height() !== metadata.height)
30
+ throw new ImageAttachmentError("image-data");
31
+ if (metadata.orientation !== 1) {
32
+ const raw = image.get_raw_pixels();
33
+ const oriented = orientPixels(raw, metadata.width, metadata.height, metadata.orientation);
34
+ image.free();
35
+ image = undefined;
36
+ image = new photon.PhotonImage(oriented.pixels, oriented.width, oriented.height);
37
+ }
38
+ const width = image.get_width(), height = image.get_height();
39
+ let scale = Math.min(1, 2000 / Math.max(width, height), maxDimension / Math.max(width, height));
40
+ const floor = Math.min(Math.max(width, height), 1024, maxDimension);
41
+ for (let level = 0; level < 4; level++, scale *= 0.75) {
42
+ const w = Math.max(1, Math.round(width * scale)), h = Math.max(1, Math.round(height * scale));
43
+ if (Math.max(w, h) < floor)
44
+ break;
45
+ const resized = photon.resize(image, w, h, photon.SamplingFilter.Lanczos3);
46
+ let opaque;
47
+ try {
48
+ const png = resized.get_bytes();
49
+ if (fits(png.length))
50
+ return result(png, "image/png", w, h);
51
+ const pixels = resized.get_raw_pixels();
52
+ for (let p = 0; p < pixels.length; p += 4) {
53
+ const alpha = (pixels[p + 3] ?? 255) / 255;
54
+ for (let c = 0; c < 3; c++)
55
+ pixels[p + c] = Math.round((pixels[p + c] ?? 0) * alpha + 255 * (1 - alpha));
56
+ pixels[p + 3] = 255;
57
+ }
58
+ opaque = new photon.PhotonImage(pixels, w, h);
59
+ for (const quality of [85, 70, 55, 40]) {
60
+ const jpeg = opaque.get_bytes_jpeg(quality);
61
+ if (fits(jpeg.length))
62
+ return result(jpeg, "image/jpeg", w, h);
63
+ }
64
+ }
65
+ finally {
66
+ opaque?.free();
67
+ resized.free();
68
+ }
69
+ }
70
+ throw new ImageAttachmentError("image-output");
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ImageAttachmentError)
74
+ throw error;
75
+ throw new ImageAttachmentError("image-conversion");
76
+ }
77
+ finally {
78
+ image?.free();
79
+ }
80
+ }
81
+ function positive(value, fallback) {
82
+ if (value === undefined)
83
+ return fallback;
84
+ if (!Number.isFinite(value) || value < 1)
85
+ throw new ImageAttachmentError("image-output");
86
+ return Math.floor(value);
87
+ }
88
+ function result(bytes, mimeType, width, height) {
89
+ return { data: Buffer.from(bytes).toString("base64"), mimeType, width, height, transformed: true };
90
+ }
91
+ function orientPixels(pixels, width, height, orientation) {
92
+ const swapped = orientation >= 5;
93
+ const outputWidth = swapped ? height : width, outputHeight = swapped ? width : height;
94
+ const output = new Uint8Array(pixels.length);
95
+ for (let y = 0; y < height; y++)
96
+ for (let x = 0; x < width; x++) {
97
+ let dx = x, dy = y;
98
+ if (orientation === 2)
99
+ dx = width - 1 - x;
100
+ if (orientation === 3) {
101
+ dx = width - 1 - x;
102
+ dy = height - 1 - y;
103
+ }
104
+ if (orientation === 4)
105
+ dy = height - 1 - y;
106
+ if (orientation === 5) {
107
+ dx = y;
108
+ dy = x;
109
+ }
110
+ if (orientation === 6) {
111
+ dx = height - 1 - y;
112
+ dy = x;
113
+ }
114
+ if (orientation === 7) {
115
+ dx = height - 1 - y;
116
+ dy = width - 1 - x;
117
+ }
118
+ if (orientation === 8) {
119
+ dx = y;
120
+ dy = width - 1 - x;
121
+ }
122
+ const from = (y * width + x) * 4, to = (dy * outputWidth + dx) * 4;
123
+ output.set(pixels.subarray(from, from + 4), to);
124
+ }
125
+ return { pixels: output, width: outputWidth, height: outputHeight };
126
+ }
@@ -0,0 +1,13 @@
1
+ export declare const MAX_SOURCE_IMAGE_BYTES: number;
2
+ export declare const MAX_SOURCE_PIXELS = 40000000;
3
+ export declare const MAX_SOURCE_DIMENSION = 32768;
4
+ export declare const IMAGE_PREPARATION_MS = 15000;
5
+ export interface ImageSourceInfo {
6
+ readonly width: number;
7
+ readonly height: number;
8
+ readonly mimeType: string;
9
+ readonly orientation: number;
10
+ readonly conversionUnsafe: boolean;
11
+ }
12
+ export declare function assertSourceBytes(length: number): void;
13
+ export declare function sourceImageInfo(bytes: Uint8Array, mimeType: string): ImageSourceInfo;
@@ -0,0 +1,171 @@
1
+ import { ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
2
+ export const MAX_SOURCE_IMAGE_BYTES = 20 * 1024 * 1024;
3
+ export const MAX_SOURCE_PIXELS = 40_000_000;
4
+ export const MAX_SOURCE_DIMENSION = 32_768;
5
+ export const IMAGE_PREPARATION_MS = 15_000;
6
+ export function assertSourceBytes(length) {
7
+ if (length > MAX_SOURCE_IMAGE_BYTES)
8
+ throw new ImageAttachmentError("image-source-size");
9
+ if (!Number.isSafeInteger(length) || length <= 0)
10
+ throw new ImageAttachmentError("image-data");
11
+ }
12
+ export function sourceImageInfo(bytes, mimeType) {
13
+ assertSourceBytes(bytes.length);
14
+ try {
15
+ const info = readHeader(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
16
+ if (info.mimeType !== mimeType.toLowerCase())
17
+ throw new ImageAttachmentError("image-mime");
18
+ if (info.width <= 0 || info.height <= 0)
19
+ throw new ImageAttachmentError("image-data");
20
+ if (info.width > MAX_SOURCE_DIMENSION || info.height > MAX_SOURCE_DIMENSION || info.width * info.height > MAX_SOURCE_PIXELS) {
21
+ throw new ImageAttachmentError("image-pixels");
22
+ }
23
+ return info;
24
+ }
25
+ catch (error) {
26
+ if (error instanceof ImageAttachmentError)
27
+ throw error;
28
+ throw new ImageAttachmentError("image-data");
29
+ }
30
+ }
31
+ function readHeader(b) {
32
+ let orientation = 1;
33
+ let conversionUnsafe = false;
34
+ const info = (width, height, mimeType) => ({ width, height, mimeType, orientation, conversionUnsafe });
35
+ if (b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
36
+ if (b.toString("ascii", 12, 16) !== "IHDR" || b.readUInt32BE(8) !== 13)
37
+ throw new ImageAttachmentError("image-data");
38
+ for (let p = 8; p + 12 <= b.length;) {
39
+ const n = b.readUInt32BE(p);
40
+ if (p + n + 12 > b.length)
41
+ throw new ImageAttachmentError("image-data");
42
+ const type = b.toString("ascii", p + 4, p + 8);
43
+ if (type === "eXIf")
44
+ orientation = exifOrientation(b.subarray(p + 8, p + 8 + n));
45
+ if (type === "acTL" || type === "iCCP" || (type === "IHDR" && b[p + 16] === 16))
46
+ conversionUnsafe = true;
47
+ p += n + 12;
48
+ }
49
+ return info(b.readUInt32BE(16), b.readUInt32BE(20), "image/png");
50
+ }
51
+ if (b[0] === 255 && b[1] === 216) {
52
+ let width = 0, height = 0;
53
+ for (let p = 2; p < b.length;) {
54
+ if (b[p++] !== 255)
55
+ throw new ImageAttachmentError("image-data");
56
+ while (b[p] === 255)
57
+ p++;
58
+ const marker = b[p++];
59
+ if (marker === 218 || marker === 217)
60
+ break;
61
+ if (marker === 1 || (marker !== undefined && marker >= 208 && marker <= 215))
62
+ continue;
63
+ const n = b.readUInt16BE(p);
64
+ if (n < 2 || p + n > b.length)
65
+ throw new ImageAttachmentError("image-data");
66
+ if (marker === 225 && b.toString("ascii", p + 2, p + 8) === "Exif\0\0")
67
+ orientation = exifOrientation(b.subarray(p + 8, p + n));
68
+ if (marker === 226 && b.toString("ascii", p + 2, p + 13) === "ICC_PROFILE")
69
+ conversionUnsafe = true;
70
+ if (marker !== undefined && [192, 193, 194].includes(marker)) {
71
+ height = b.readUInt16BE(p + 3);
72
+ width = b.readUInt16BE(p + 5);
73
+ if (b[p + 7] === 4)
74
+ conversionUnsafe = true;
75
+ }
76
+ p += n;
77
+ }
78
+ return info(width, height, "image/jpeg");
79
+ }
80
+ if (/^GIF8[79]a$/.test(b.toString("ascii", 0, 6))) {
81
+ conversionUnsafe = gifFrameCount(b) > 1;
82
+ return info(b.readUInt16LE(6), b.readUInt16LE(8), "image/gif");
83
+ }
84
+ if (b.toString("ascii", 0, 2) === "BM") {
85
+ const dib = b.readUInt32LE(14);
86
+ if (dib === 12)
87
+ return info(b.readUInt16LE(18), b.readUInt16LE(20), "image/bmp");
88
+ if (dib < 40)
89
+ throw new ImageAttachmentError("image-data");
90
+ return info(b.readInt32LE(18), Math.abs(b.readInt32LE(22)), "image/bmp");
91
+ }
92
+ if (b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") {
93
+ const kind = b.toString("ascii", 12, 16);
94
+ if (kind === "VP8X") {
95
+ conversionUnsafe = ((b[20] ?? 0) & 0x22) !== 0;
96
+ for (let p = 12; p + 8 <= b.length;) {
97
+ const n = b.readUInt32LE(p + 4);
98
+ if (p + n + 8 > b.length)
99
+ throw new ImageAttachmentError("image-data");
100
+ if (b.toString("ascii", p, p + 4) === "EXIF")
101
+ orientation = exifOrientation(b.subarray(p + 8, p + 8 + n));
102
+ p += 8 + n + (n % 2);
103
+ }
104
+ return info(1 + b.readUIntLE(24, 3), 1 + b.readUIntLE(27, 3), "image/webp");
105
+ }
106
+ if (kind === "VP8 " && b.toString("hex", 23, 26) === "9d012a")
107
+ return info(b.readUInt16LE(26) & 0x3fff, b.readUInt16LE(28) & 0x3fff, "image/webp");
108
+ if (kind === "VP8L" && b[20] === 47) {
109
+ const bits = b.readUInt32LE(21);
110
+ return info((bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1, "image/webp");
111
+ }
112
+ }
113
+ throw new ImageAttachmentError("image-conversion");
114
+ }
115
+ function gifFrameCount(b) {
116
+ let p = 13 + (((b[10] ?? 0) & 128) ? 3 * 2 ** (((b[10] ?? 0) & 7) + 1) : 0);
117
+ let frames = 0;
118
+ const skipBlocks = () => {
119
+ while (p < b.length) {
120
+ const length = b[p++] ?? 0;
121
+ if (length === 0)
122
+ return;
123
+ p += length;
124
+ if (p > b.length)
125
+ throw new ImageAttachmentError("image-data");
126
+ }
127
+ throw new ImageAttachmentError("image-data");
128
+ };
129
+ while (p < b.length) {
130
+ const marker = b[p++];
131
+ if (marker === 59)
132
+ return frames;
133
+ if (marker === 33) {
134
+ p++;
135
+ skipBlocks();
136
+ continue;
137
+ }
138
+ if (marker !== 44 || p + 9 > b.length)
139
+ throw new ImageAttachmentError("image-data");
140
+ frames++;
141
+ const packed = b[p + 8] ?? 0;
142
+ p += 9 + ((packed & 128) ? 3 * 2 ** ((packed & 7) + 1) : 0);
143
+ p++;
144
+ skipBlocks();
145
+ }
146
+ throw new ImageAttachmentError("image-data");
147
+ }
148
+ function exifOrientation(input) {
149
+ const b = input.toString("ascii", 0, 6) === "Exif\0\0" ? input.subarray(6) : input;
150
+ const little = b.toString("ascii", 0, 2) === "II";
151
+ if (!little && b.toString("ascii", 0, 2) !== "MM")
152
+ throw new ImageAttachmentError("image-conversion");
153
+ const u16 = (p) => little ? b.readUInt16LE(p) : b.readUInt16BE(p);
154
+ const u32 = (p) => little ? b.readUInt32LE(p) : b.readUInt32BE(p);
155
+ if (u16(2) !== 42)
156
+ throw new ImageAttachmentError("image-conversion");
157
+ const directory = u32(4);
158
+ const count = u16(directory);
159
+ if (directory + 2 + count * 12 > b.length)
160
+ throw new ImageAttachmentError("image-conversion");
161
+ for (let i = 0; i < count; i++) {
162
+ const p = directory + 2 + i * 12;
163
+ if (u16(p) !== 274)
164
+ continue;
165
+ const value = u16(p + 8);
166
+ if (u16(p + 2) !== 3 || u32(p + 4) !== 1 || value < 1 || value > 8)
167
+ throw new ImageAttachmentError("image-conversion");
168
+ return value;
169
+ }
170
+ return 1;
171
+ }
@@ -0,0 +1,17 @@
1
+ import { type ImagePreparationLimits } from "./image-preparation.js";
2
+ export type ImageWorkerRequest = {
3
+ readonly kind: "clipboard";
4
+ } | {
5
+ readonly kind: "canonicalize";
6
+ readonly source: {
7
+ readonly data: string;
8
+ readonly mimeType: string;
9
+ };
10
+ } | {
11
+ readonly kind: "prepare";
12
+ readonly source: {
13
+ readonly data: string;
14
+ readonly mimeType: string;
15
+ };
16
+ readonly limits?: ImagePreparationLimits;
17
+ };
@@ -0,0 +1,23 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
3
+ import { prepareImage } from "./image-preparation.js";
4
+ import { canonicalizeClipboardImage } from "./clipboard-image.js";
5
+ import { readSystemClipboardContent } from "./system-clipboard.js";
6
+ const controller = new AbortController();
7
+ parentPort?.on("message", message => { if (message === "cancel")
8
+ controller.abort(); });
9
+ const request = workerData;
10
+ try {
11
+ const value = request.kind === "clipboard"
12
+ ? await readSystemClipboardContent(controller.signal)
13
+ : request.kind === "canonicalize" ? canonicalizeClipboardImage(request.source, true)
14
+ : await prepareImage(request.source, request.limits);
15
+ parentPort?.postMessage({ ok: true, value });
16
+ }
17
+ catch (error) {
18
+ // Security: never forward native/codec exceptions which may contain input data.
19
+ parentPort?.postMessage({ ok: false, code: error instanceof ImageAttachmentError ? error.code : "image-codec" });
20
+ }
21
+ finally {
22
+ parentPort?.close();
23
+ }
@@ -8,10 +8,19 @@ export interface PreparedPrompt {
8
8
  readonly text: string;
9
9
  readonly images: readonly PromptImageAttachment[];
10
10
  }
11
- /** Owns semantic clipboard records while the prompt displays compact chips. */
11
+ /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
12
12
  export declare class PromptChipStore {
13
13
  #private;
14
- transformPastedContent(content: PiShellClipboardContent): string;
14
+ beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void): {
15
+ marker: string;
16
+ result: Promise<string>;
17
+ };
18
+ hasPending(text: string): boolean;
19
+ waitForPastes(text: string, signal: AbortSignal, readDraft?: () => string): Promise<PreparedPrompt>;
20
+ reconcileDraft(text: string): void;
21
+ resetPastes(text: string): string;
22
+ dispose(): Promise<void>;
23
+ transformPastedContent(content: PiShellClipboardContent, currentText?: string): string;
15
24
  atomicRanges(line: string): readonly PiShellEditorTextRange[];
16
25
  hyperlinkRanges(text: string): readonly {
17
26
  start: number;