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.
@@ -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.custom((tui, theme, keys, done) => {
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.custom((tui, theme, keybindings, done) => {
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 context.ui.custom(
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), {