pi-fabric 0.104.2 → 0.104.3
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/dist/chunks/chunk-2Y24ML7H.js +115 -0
- package/dist/chunks/chunk-2Y24ML7H.js.map +7 -0
- package/dist/chunks/chunk-B2Y4364O.js +78 -0
- package/dist/chunks/chunk-B2Y4364O.js.map +7 -0
- package/dist/index.js +7 -6
- package/dist/index.js.map +2 -2
- package/dist/ui/conversation-render.d.ts +2 -1
- package/dist/ui/conversation-render.d.ts.map +1 -1
- package/dist/ui/conversation-scrollbar.d.ts +1 -1
- package/dist/ui/conversation-scrollbar.d.ts.map +1 -1
- package/dist/ui/conversation-selection.d.ts +2 -2
- package/dist/ui/conversation-selection.d.ts.map +1 -1
- package/dist/ui/conversation.d.ts +1 -0
- package/dist/ui/conversation.d.ts.map +1 -1
- package/dist/ui/conversation.js +28 -14
- package/dist/ui/conversation.js.map +2 -2
- package/dist/ui/image-overlays.d.ts +16 -0
- package/dist/ui/image-overlays.d.ts.map +1 -0
- package/dist/ui/image-overlays.js +10 -0
- package/dist/ui/image-overlays.js.map +7 -0
- package/dist/ui/kitty-viewport.d.ts +24 -0
- package/dist/ui/kitty-viewport.d.ts.map +1 -0
- package/dist/ui/settings.d.ts.map +1 -1
- package/dist/ui/settings.js +7 -4
- package/dist/ui/settings.js.map +2 -2
- package/docs/kitty-images.md +89 -0
- package/package.json +2 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/ui/kitty-viewport.ts
|
|
2
|
+
import { getPngDimensions } from "@earendil-works/pi-tui";
|
|
3
|
+
var isKittyImageLine = (line) => line.includes("\x1B_G");
|
|
4
|
+
var EMPTY_IMAGE_ROWS = /* @__PURE__ */ new Set();
|
|
5
|
+
var MAX_CROP_CACHE_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
var MAX_CACHED_CROPS = 2;
|
|
7
|
+
function header(line) {
|
|
8
|
+
const start = line.indexOf("\x1B_G");
|
|
9
|
+
if (start < 0) return void 0;
|
|
10
|
+
const end = line.indexOf(";", start);
|
|
11
|
+
if (end < 0) return void 0;
|
|
12
|
+
const controls = line.slice(start + 3, end).split(",");
|
|
13
|
+
const rows = Number(controls.find((control) => control.startsWith("r="))?.slice(2));
|
|
14
|
+
if (!Number.isSafeInteger(rows) || rows <= 0) return void 0;
|
|
15
|
+
return { start, end, controls, rows };
|
|
16
|
+
}
|
|
17
|
+
var KittyViewport = class {
|
|
18
|
+
source;
|
|
19
|
+
images = [];
|
|
20
|
+
crops = /* @__PURE__ */ new Map();
|
|
21
|
+
cropBytes = 0;
|
|
22
|
+
clear() {
|
|
23
|
+
this.source = void 0;
|
|
24
|
+
this.images = [];
|
|
25
|
+
this.crops.clear();
|
|
26
|
+
this.cropBytes = 0;
|
|
27
|
+
}
|
|
28
|
+
slice(source, start, end) {
|
|
29
|
+
if (source !== this.source) {
|
|
30
|
+
this.clear();
|
|
31
|
+
this.source = source;
|
|
32
|
+
for (let row = 0; row < source.length; row++) {
|
|
33
|
+
const image = header(source[row]);
|
|
34
|
+
if (!image) continue;
|
|
35
|
+
this.images.push({ ...image, row, line: source[row] });
|
|
36
|
+
row += image.rows - 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const lines = source.slice(start, end);
|
|
40
|
+
if (!lines.length || !this.images.length) return { lines, imageRows: EMPTY_IMAGE_ROWS };
|
|
41
|
+
let low = 0;
|
|
42
|
+
let high = this.images.length;
|
|
43
|
+
while (low < high) {
|
|
44
|
+
const middle = low + high >>> 1;
|
|
45
|
+
const image = this.images[middle];
|
|
46
|
+
if (image.row + image.rows <= start) low = middle + 1;
|
|
47
|
+
else high = middle;
|
|
48
|
+
}
|
|
49
|
+
let imageRows;
|
|
50
|
+
for (let index = low; index < this.images.length; index++) {
|
|
51
|
+
const image = this.images[index];
|
|
52
|
+
if (image.row >= start + lines.length) break;
|
|
53
|
+
const row = Math.max(0, image.row - start);
|
|
54
|
+
const hidden = Math.max(0, start - image.row);
|
|
55
|
+
const visible = Math.min(image.rows - hidden, lines.length - row);
|
|
56
|
+
const line = this.crop(image, hidden, visible);
|
|
57
|
+
lines[row] = line;
|
|
58
|
+
if (!line) continue;
|
|
59
|
+
imageRows ??= /* @__PURE__ */ new Set();
|
|
60
|
+
for (let i = row; i < row + visible; i++) imageRows.add(i);
|
|
61
|
+
}
|
|
62
|
+
return { lines, imageRows: imageRows ?? EMPTY_IMAGE_ROWS };
|
|
63
|
+
}
|
|
64
|
+
crop(image, hidden, visible) {
|
|
65
|
+
if (hidden === 0 && visible === image.rows) return image.line;
|
|
66
|
+
const cached = this.crops.get(image);
|
|
67
|
+
if (cached?.hidden === hidden && cached.visible === visible) return cached.line;
|
|
68
|
+
if (image.heightPx === void 0) image.heightPx = getPngDimensions(image.line.slice(image.end + 1, image.end + 33))?.heightPx ?? null;
|
|
69
|
+
if (!image.heightPx || !image.controls.includes("f=100") || !image.controls.includes("a=T")) return "";
|
|
70
|
+
const sourceY = Math.floor(image.heightPx * hidden / image.rows);
|
|
71
|
+
const sourceEnd = Math.ceil(image.heightPx * (hidden + visible) / image.rows);
|
|
72
|
+
const controls = image.controls.filter((control) => !/^[yhr]=/.test(control));
|
|
73
|
+
controls.push(`y=${sourceY}`, `h=${Math.max(1, sourceEnd - sourceY)}`, `r=${visible}`);
|
|
74
|
+
const line = `${image.line.slice(0, image.start)}\x1B_G${controls.join(",")};${image.line.slice(image.end + 1)}`;
|
|
75
|
+
if (cached) {
|
|
76
|
+
this.cropBytes -= cached.bytes;
|
|
77
|
+
this.crops.delete(image);
|
|
78
|
+
}
|
|
79
|
+
const bytes = line.length * 2;
|
|
80
|
+
if (bytes <= MAX_CROP_CACHE_BYTES) {
|
|
81
|
+
while (this.crops.size >= MAX_CACHED_CROPS || this.cropBytes + bytes > MAX_CROP_CACHE_BYTES) {
|
|
82
|
+
const oldest = this.crops.keys().next().value;
|
|
83
|
+
this.cropBytes -= this.crops.get(oldest).bytes;
|
|
84
|
+
this.crops.delete(oldest);
|
|
85
|
+
}
|
|
86
|
+
this.crops.set(image, { hidden, visible, line, bytes });
|
|
87
|
+
this.cropBytes += bytes;
|
|
88
|
+
}
|
|
89
|
+
return line;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function hideKittyImages(lines, viewportHeight = lines.length) {
|
|
93
|
+
const start = Math.max(0, lines.length - viewportHeight);
|
|
94
|
+
let result;
|
|
95
|
+
const hide = (row) => {
|
|
96
|
+
result ??= lines.slice();
|
|
97
|
+
result[row] = "";
|
|
98
|
+
};
|
|
99
|
+
for (let row = start; row < lines.length; row++) if (isKittyImageLine(lines[row])) hide(row);
|
|
100
|
+
for (let row = start - 1; row >= 0; row--) {
|
|
101
|
+
const line = lines[row];
|
|
102
|
+
if (line === "") continue;
|
|
103
|
+
const image = header(line);
|
|
104
|
+
if (image && row + image.rows > start) hide(row);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
return result ?? lines;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
isKittyImageLine,
|
|
112
|
+
KittyViewport,
|
|
113
|
+
hideKittyImages
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=chunk-2Y24ML7H.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/kitty-viewport.ts"],
|
|
4
|
+
"sourcesContent": ["import { getPngDimensions } from \"@earendil-works/pi-tui\";\n\nexport const isKittyImageLine = (line: string): boolean => line.includes(\"\\x1b_G\");\nconst EMPTY_IMAGE_ROWS: ReadonlySet<number> = new Set<number>();\nconst MAX_CROP_CACHE_BYTES = 16 * 1024 * 1024;\nconst MAX_CACHED_CROPS = 2; // Only the top and bottom image can need cropping.\n\nfunction header(line: string) {\n const start = line.indexOf(\"\\x1b_G\");\n if (start < 0) return undefined;\n const end = line.indexOf(\";\", start);\n if (end < 0) return undefined;\n const controls = line.slice(start + 3, end).split(\",\");\n const rows = Number(controls.find((control) => control.startsWith(\"r=\"))?.slice(2));\n if (!Number.isSafeInteger(rows) || rows <= 0) return undefined;\n return { start, end, controls, rows };\n}\n\ntype ImageBlock = NonNullable<ReturnType<typeof header>> & { row: number; line: string; heightPx?: number | null };\ntype CachedCrop = { hidden: number; visible: number; line: string; bytes: number };\nexport interface KittyViewportFrame {\n lines: string[];\n /** Reserved rows must stay empty so Pi pre-clears them before placing pixels. */\n imageRows: ReadonlySet<number>;\n}\n\n/** Index immutable retained frames once. Scrolling then touches only the viewport\n * and its intersecting images, never a long run of offscreen blank reservations.\n * The two-entry, byte-bounded crop cache avoids rebuilding megabyte transmissions\n * on unchanged frames. No image data is decoded, uploaded or deleted here. */\nexport class KittyViewport {\n private source: readonly string[] | undefined;\n private images: ImageBlock[] = [];\n private readonly crops = new Map<ImageBlock, CachedCrop>();\n private cropBytes = 0;\n\n clear(): void {\n this.source = undefined;\n this.images = [];\n this.crops.clear();\n this.cropBytes = 0;\n }\n\n slice(source: readonly string[], start: number, end: number): KittyViewportFrame {\n if (source !== this.source) {\n this.clear();\n this.source = source;\n for (let row = 0; row < source.length; row++) {\n const image = header(source[row]!);\n if (!image) continue;\n this.images.push({ ...image, row, line: source[row]! });\n row += image.rows - 1;\n }\n }\n const lines = source.slice(start, end);\n if (!lines.length || !this.images.length) return { lines, imageRows: EMPTY_IMAGE_ROWS };\n let low = 0;\n let high = this.images.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n const image = this.images[middle]!;\n if (image.row + image.rows <= start) low = middle + 1;\n else high = middle;\n }\n let imageRows: Set<number> | undefined;\n for (let index = low; index < this.images.length; index++) {\n const image = this.images[index]!;\n if (image.row >= start + lines.length) break;\n const row = Math.max(0, image.row - start);\n const hidden = Math.max(0, start - image.row);\n const visible = Math.min(image.rows - hidden, lines.length - row);\n const line = this.crop(image, hidden, visible);\n lines[row] = line;\n if (!line) continue;\n imageRows ??= new Set<number>();\n for (let i = row; i < row + visible; i++) imageRows.add(i);\n }\n return { lines, imageRows: imageRows ?? EMPTY_IMAGE_ROWS };\n }\n\n private crop(image: ImageBlock, hidden: number, visible: number): string {\n if (hidden === 0 && visible === image.rows) return image.line;\n const cached = this.crops.get(image);\n if (cached?.hidden === hidden && cached.visible === visible) return cached.line;\n // Pi's f=100 transmission starts with a PNG header. Read only its first 32\n // base64 characters, not a second Pi installation's image metadata registry.\n if (image.heightPx === undefined) image.heightPx = getPngDimensions(image.line.slice(image.end + 1, image.end + 33))?.heightPx ?? null;\n if (!image.heightPx || !image.controls.includes(\"f=100\") || !image.controls.includes(\"a=T\")) return \"\";\n const sourceY = Math.floor(image.heightPx * hidden / image.rows);\n const sourceEnd = Math.ceil(image.heightPx * (hidden + visible) / image.rows);\n const controls = image.controls.filter((control) => !/^[yhr]=/.test(control));\n controls.push(`y=${sourceY}`, `h=${Math.max(1, sourceEnd - sourceY)}`, `r=${visible}`);\n const line = `${image.line.slice(0, image.start)}\\x1b_G${controls.join(\",\")};${image.line.slice(image.end + 1)}`;\n if (cached) {\n this.cropBytes -= cached.bytes;\n this.crops.delete(image);\n }\n const bytes = line.length * 2; // conservative UTF-16 storage budget\n if (bytes <= MAX_CROP_CACHE_BYTES) {\n while (this.crops.size >= MAX_CACHED_CROPS || this.cropBytes + bytes > MAX_CROP_CACHE_BYTES) {\n const oldest = this.crops.keys().next().value!;\n this.cropBytes -= this.crops.get(oldest)!.bytes;\n this.crops.delete(oldest);\n }\n this.crops.set(image, { hidden, visible, line, bytes });\n this.cropBytes += bytes;\n }\n return line;\n }\n}\n\n/** Hide only background placements that can reach the visible terminal. Leave\n * offscreen scrollback intact; removing it would force unnecessary reuploads on\n * close. Return text-only frames unchanged instead of copying retained history. */\nexport function hideKittyImages(lines: string[], viewportHeight = lines.length): string[] {\n const start = Math.max(0, lines.length - viewportHeight);\n let result: string[] | undefined;\n const hide = (row: number): void => { result ??= lines.slice(); result[row] = \"\"; };\n for (let row = start; row < lines.length; row++) if (isKittyImageLine(lines[row]!)) hide(row);\n // An image anchored above the viewport may extend into it through empty rows.\n for (let row = start - 1; row >= 0; row--) {\n const line = lines[row]!;\n if (line === \"\") continue;\n const image = header(line);\n if (image && row + image.rows > start) hide(row);\n break;\n }\n return result ?? lines;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hideKittyImages
|
|
3
|
+
} from "./chunk-2Y24ML7H.js";
|
|
4
|
+
|
|
5
|
+
// src/ui/image-overlays.ts
|
|
6
|
+
var leases = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function retainImageSafeOverlays(tui) {
|
|
8
|
+
let prototype = Object.getPrototypeOf(tui);
|
|
9
|
+
while (prototype && !Object.hasOwn(prototype, "compositeOverlays")) prototype = Object.getPrototypeOf(prototype);
|
|
10
|
+
if (!prototype) return () => {
|
|
11
|
+
};
|
|
12
|
+
const host = prototype;
|
|
13
|
+
if (typeof host.compositeOverlays !== "function" || typeof host.compositeLineAt !== "function" || typeof host.isOverlayVisible !== "function") return () => {
|
|
14
|
+
};
|
|
15
|
+
let lease = leases.get(prototype);
|
|
16
|
+
if (!lease) {
|
|
17
|
+
const original = host.compositeOverlays;
|
|
18
|
+
const originalLine = host.compositeLineAt;
|
|
19
|
+
const composite = function(lines, width, height) {
|
|
20
|
+
const visible = this.overlayStack.filter((entry) => this.isOverlayVisible(entry)).sort((a, b) => a.focusOrder - b.focusOrder);
|
|
21
|
+
if (!visible.length) return original.call(this, lines, width, height);
|
|
22
|
+
const restore = [];
|
|
23
|
+
try {
|
|
24
|
+
for (const entry of visible.slice(0, -1)) {
|
|
25
|
+
const component = entry.component;
|
|
26
|
+
entry.component = {
|
|
27
|
+
render: (columns) => hideKittyImages(component.render(columns)),
|
|
28
|
+
invalidate: () => component.invalidate()
|
|
29
|
+
};
|
|
30
|
+
restore.push(() => {
|
|
31
|
+
entry.component = component;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return original.call(this, hideKittyImages(lines, height), width, height);
|
|
35
|
+
} finally {
|
|
36
|
+
for (const undo of restore.reverse()) undo();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const compositeLine = function(base, overlay, column, width, totalWidth) {
|
|
40
|
+
if (column === 0 && width === totalWidth) return overlay;
|
|
41
|
+
return originalLine.call(this, base, overlay, column, width, totalWidth);
|
|
42
|
+
};
|
|
43
|
+
host.compositeOverlays = composite;
|
|
44
|
+
host.compositeLineAt = compositeLine;
|
|
45
|
+
lease = { count: 0, restore: () => {
|
|
46
|
+
if (host.compositeOverlays === composite) host.compositeOverlays = original;
|
|
47
|
+
if (host.compositeLineAt === compositeLine) host.compositeLineAt = originalLine;
|
|
48
|
+
} };
|
|
49
|
+
leases.set(prototype, lease);
|
|
50
|
+
}
|
|
51
|
+
lease.count++;
|
|
52
|
+
let released = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (released) return;
|
|
55
|
+
released = true;
|
|
56
|
+
if (--lease.count === 0) {
|
|
57
|
+
lease.restore();
|
|
58
|
+
leases.delete(prototype);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function imageSafeCustom(ui, factory, options) {
|
|
63
|
+
let release;
|
|
64
|
+
try {
|
|
65
|
+
return await ui.custom((tui, theme, keys, done) => {
|
|
66
|
+
release = retainImageSafeOverlays(tui);
|
|
67
|
+
return factory(tui, theme, keys, done);
|
|
68
|
+
}, options);
|
|
69
|
+
} finally {
|
|
70
|
+
release?.();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
retainImageSafeOverlays,
|
|
76
|
+
imageSafeCustom
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=chunk-B2Y4364O.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/image-overlays.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ExtensionContext, KeybindingsManager, Theme } from \"@earendil-works/pi-coding-agent\";\nimport type { Component, TUI } from \"@earendil-works/pi-tui\";\nimport { hideKittyImages } from \"./kitty-viewport.js\";\n\ntype OverlayEntry = { component: Component; focusOrder: number };\ntype Compositor = {\n overlayStack: OverlayEntry[];\n isOverlayVisible(entry: OverlayEntry): boolean;\n compositeOverlays(lines: string[], width: number, height: number): string[];\n compositeLineAt(base: string, overlay: string, column: number, width: number, totalWidth: number): string;\n};\nconst leases = new WeakMap<object, { count: number; restore(): void }>();\n\n/** Pi 1.0's compositor returns a base image instead of the overlay, and pads\n * full-width image reservations into text. There is no public compositor hook.\n * Lease only these two host prototype methods while a Fabric overlay is open;\n * native layout, focus, visibility, image deletion and upload caches still own\n * the frame. Resolve through the supplied live proxy, never import another TUI.\n * Background layers deliberately hide images rather than trying to z-order\n * terminal graphics against text. The native diff restores them on close. */\nexport function retainImageSafeOverlays(tui: TUI): () => void {\n let prototype = Object.getPrototypeOf(tui) as object | null;\n while (prototype && !Object.hasOwn(prototype, \"compositeOverlays\")) prototype = Object.getPrototypeOf(prototype) as object | null;\n if (!prototype) return () => {};\n const host = prototype as Compositor;\n if (typeof host.compositeOverlays !== \"function\" || typeof host.compositeLineAt !== \"function\" ||\n typeof host.isOverlayVisible !== \"function\") return () => {};\n let lease = leases.get(prototype);\n if (!lease) {\n const original = host.compositeOverlays;\n const originalLine = host.compositeLineAt;\n const composite: Compositor[\"compositeOverlays\"] = function (this: Compositor, lines, width, height) {\n const visible = this.overlayStack.filter((entry) => this.isOverlayVisible(entry))\n .sort((a, b) => a.focusOrder - b.focusOrder);\n if (!visible.length) return original.call(this, lines, width, height);\n const restore: (() => void)[] = [];\n try {\n for (const entry of visible.slice(0, -1)) {\n const component = entry.component;\n // Wrap the stack slot, not the component: third-party components may\n // be frozen or use private fields, and must keep their render receiver.\n entry.component = {\n render: (columns) => hideKittyImages(component.render(columns)),\n invalidate: () => component.invalidate(),\n };\n restore.push(() => { entry.component = component; });\n }\n return original.call(this, hideKittyImages(lines, height), width, height);\n } finally {\n for (const undo of restore.reverse()) undo();\n }\n };\n const compositeLine: Compositor[\"compositeLineAt\"] = function (this: Compositor, base, overlay, column, width, totalWidth) {\n // Avoid padding image reservations or passing control payloads through\n // text slicing. A full-width overlay replaces the entire terminal row.\n if (column === 0 && width === totalWidth) return overlay;\n return originalLine.call(this, base, overlay, column, width, totalWidth);\n };\n host.compositeOverlays = composite;\n host.compositeLineAt = compositeLine;\n lease = { count: 0, restore: () => {\n if (host.compositeOverlays === composite) host.compositeOverlays = original;\n if (host.compositeLineAt === compositeLine) host.compositeLineAt = originalLine;\n } };\n leases.set(prototype, lease);\n }\n lease.count++;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--lease.count === 0) {\n lease.restore();\n leases.delete(prototype);\n }\n };\n}\n\ntype CustomComponent = Component & { dispose?(): void };\nexport async function imageSafeCustom<T>(\n ui: ExtensionContext[\"ui\"],\n factory: (tui: TUI, theme: Theme, keys: KeybindingsManager, done: (result: T) => void) => CustomComponent | Promise<CustomComponent>,\n options?: Parameters<ExtensionContext[\"ui\"][\"custom\"]>[1],\n): Promise<T> {\n let release: (() => void) | undefined;\n try {\n return await ui.custom<T>((tui, theme, keys, done) => {\n release = retainImageSafeOverlays(tui);\n return factory(tui, theme, keys, done);\n }, options);\n } finally {\n release?.();\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAWA,IAAM,SAAS,oBAAI,QAAoD;AAShE,SAAS,wBAAwB,KAAsB;AAC5D,MAAI,YAAY,OAAO,eAAe,GAAG;AACzC,SAAO,aAAa,CAAC,OAAO,OAAO,WAAW,mBAAmB,EAAG,aAAY,OAAO,eAAe,SAAS;AAC/G,MAAI,CAAC,UAAW,QAAO,MAAM;AAAA,EAAC;AAC9B,QAAM,OAAO;AACb,MAAI,OAAO,KAAK,sBAAsB,cAAc,OAAO,KAAK,oBAAoB,cAClF,OAAO,KAAK,qBAAqB,WAAY,QAAO,MAAM;AAAA,EAAC;AAC7D,MAAI,QAAQ,OAAO,IAAI,SAAS;AAChC,MAAI,CAAC,OAAO;AACV,UAAM,WAAW,KAAK;AACtB,UAAM,eAAe,KAAK;AAC1B,UAAM,YAA6C,SAA4B,OAAO,OAAO,QAAQ;AACnG,YAAM,UAAU,KAAK,aAAa,OAAO,CAAC,UAAU,KAAK,iBAAiB,KAAK,CAAC,EAC7E,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAC7C,UAAI,CAAC,QAAQ,OAAQ,QAAO,SAAS,KAAK,MAAM,OAAO,OAAO,MAAM;AACpE,YAAM,UAA0B,CAAC;AACjC,UAAI;AACF,mBAAW,SAAS,QAAQ,MAAM,GAAG,EAAE,GAAG;AACxC,gBAAM,YAAY,MAAM;AAGxB,gBAAM,YAAY;AAAA,YAChB,QAAQ,CAAC,YAAY,gBAAgB,UAAU,OAAO,OAAO,CAAC;AAAA,YAC9D,YAAY,MAAM,UAAU,WAAW;AAAA,UACzC;AACA,kBAAQ,KAAK,MAAM;AAAE,kBAAM,YAAY;AAAA,UAAW,CAAC;AAAA,QACrD;AACA,eAAO,SAAS,KAAK,MAAM,gBAAgB,OAAO,MAAM,GAAG,OAAO,MAAM;AAAA,MAC1E,UAAE;AACA,mBAAW,QAAQ,QAAQ,QAAQ,EAAG,MAAK;AAAA,MAC7C;AAAA,IACF;AACA,UAAM,gBAA+C,SAA4B,MAAM,SAAS,QAAQ,OAAO,YAAY;AAGzH,UAAI,WAAW,KAAK,UAAU,WAAY,QAAO;AACjD,aAAO,aAAa,KAAK,MAAM,MAAM,SAAS,QAAQ,OAAO,UAAU;AAAA,IACzE;AACA,SAAK,oBAAoB;AACzB,SAAK,kBAAkB;AACvB,YAAQ,EAAE,OAAO,GAAG,SAAS,MAAM;AACjC,UAAI,KAAK,sBAAsB,UAAW,MAAK,oBAAoB;AACnE,UAAI,KAAK,oBAAoB,cAAe,MAAK,kBAAkB;AAAA,IACrE,EAAE;AACF,WAAO,IAAI,WAAW,KAAK;AAAA,EAC7B;AACA,QAAM;AACN,MAAI,WAAW;AACf,SAAO,MAAM;AACX,QAAI,SAAU;AACd,eAAW;AACX,QAAI,EAAE,MAAM,UAAU,GAAG;AACvB,YAAM,QAAQ;AACd,aAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AACF;AAGA,eAAsB,gBACpB,IACA,SACA,SACY;AACZ,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,GAAG,OAAU,CAAC,KAAK,OAAO,MAAM,SAAS;AACpD,gBAAU,wBAAwB,GAAG;AACrC,aAAO,QAAQ,KAAK,OAAO,MAAM,IAAI;AAAA,IACvC,GAAG,OAAO;AAAA,EACZ,UAAE;AACA,cAAU;AAAA,EACZ;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5059,9 +5059,9 @@ var FabricUiController = class {
|
|
|
5059
5059
|
this.#tasksOpen = true;
|
|
5060
5060
|
const epoch = this.#epoch;
|
|
5061
5061
|
try {
|
|
5062
|
-
const { ShellTasksView } = await import("./ui/shell-tasks.js");
|
|
5062
|
+
const [{ ShellTasksView }, { imageSafeCustom }] = await Promise.all([import("./ui/shell-tasks.js"), import("./ui/image-overlays.js")]);
|
|
5063
5063
|
if (epoch !== this.#epoch) return;
|
|
5064
|
-
await context.ui
|
|
5064
|
+
await imageSafeCustom(context.ui, (tui, theme, keys, done) => {
|
|
5065
5065
|
this.#closeTasks = () => done();
|
|
5066
5066
|
const id = candidates[0]?.id;
|
|
5067
5067
|
this.#tasksView = new ShellTasksView({
|
|
@@ -5107,7 +5107,7 @@ var FabricUiController = class {
|
|
|
5107
5107
|
const { initializeConversationHost } = await import("./ui/conversation-host.js");
|
|
5108
5108
|
if (epoch !== this.#epoch) return;
|
|
5109
5109
|
initializeConversationHost(piConversationHost);
|
|
5110
|
-
const [{ FabricConversationView, FabricConversationState }, { conversationTargets, resolveConversationTarget }, { readConversationAppearance }, { NativeConversationReader }] = await Promise.all([import("./ui/conversation.js"), import("./ui/conversation-targets.js"), import("./ui/conversation-chrome.js"), import("./ui/conversation-native-reader.js")]);
|
|
5110
|
+
const [{ FabricConversationView, FabricConversationState }, { conversationTargets, resolveConversationTarget }, { readConversationAppearance }, { NativeConversationReader }, { imageSafeCustom }] = await Promise.all([import("./ui/conversation.js"), import("./ui/conversation-targets.js"), import("./ui/conversation-chrome.js"), import("./ui/conversation-native-reader.js"), import("./ui/image-overlays.js")]);
|
|
5111
5111
|
if (epoch !== this.#epoch) return;
|
|
5112
5112
|
this.#refresh();
|
|
5113
5113
|
const initialTarget = query?.trim() ? resolveConversationTarget(conversationTargets(this.#snapshot), query.trim()) : void 0;
|
|
@@ -5196,7 +5196,7 @@ var FabricUiController = class {
|
|
|
5196
5196
|
}
|
|
5197
5197
|
};
|
|
5198
5198
|
this.#schedulePoll(true);
|
|
5199
|
-
await context.ui
|
|
5199
|
+
await imageSafeCustom(context.ui, (tui, theme, keybindings, done) => {
|
|
5200
5200
|
if (epoch !== this.#epoch) {
|
|
5201
5201
|
done(void 0);
|
|
5202
5202
|
return { render: () => [], invalidate: () => {
|
|
@@ -5283,7 +5283,7 @@ var FabricUiController = class {
|
|
|
5283
5283
|
if (!this.#context) this.start(context);
|
|
5284
5284
|
this.#dashboardOpen = true;
|
|
5285
5285
|
this.#refresh();
|
|
5286
|
-
const [{ FabricDashboard }, { buildClaudeModelSource, buildModelSource: buildModelSource2 }] = await Promise.all([import("./ui/dashboard.js"), import("./ui/model-picker.js")]);
|
|
5286
|
+
const [{ FabricDashboard }, { buildClaudeModelSource, buildModelSource: buildModelSource2 }, { imageSafeCustom }] = await Promise.all([import("./ui/dashboard.js"), import("./ui/model-picker.js"), import("./ui/image-overlays.js")]);
|
|
5287
5287
|
const modelSource = buildModelSource2(context.modelRegistry, resolveAgentDir());
|
|
5288
5288
|
let claudeModelSource;
|
|
5289
5289
|
if (this.#snapshot.actors.some((actor) => actor.runner === "claude")) {
|
|
@@ -5398,7 +5398,8 @@ var FabricUiController = class {
|
|
|
5398
5398
|
let conversationTarget;
|
|
5399
5399
|
const epoch = this.#epoch;
|
|
5400
5400
|
try {
|
|
5401
|
-
await
|
|
5401
|
+
await imageSafeCustom(
|
|
5402
|
+
context.ui,
|
|
5402
5403
|
(tui, theme, keybindings, done) => {
|
|
5403
5404
|
this.#dashboardTui = tui;
|
|
5404
5405
|
return new FabricDashboard(tui, theme, () => this.#snapshot, () => done(void 0), {
|