@quandev104/pi-style 0.2.2 → 0.2.4

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.
@@ -1,3 +1,353 @@
1
+ // extension-src/pi-style/features/messages/image-input.ts
2
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
3
+ import { tmpdir as tmpdir2 } from "os";
4
+
5
+ // extension-src/pi-style/shared/clipboard-path.ts
6
+ import { tmpdir } from "os";
7
+ var UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
8
+ var CLIPBOARD_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
9
+ function clipboardImageMarker(index) {
10
+ return `[Image #${index}]`;
11
+ }
12
+ function escapeRegExp(text) {
13
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ }
15
+ var clipboardPathRegexCache = /* @__PURE__ */ new Map();
16
+ var CLIPBOARD_PATH_REGEX_CACHE_MAX = 16;
17
+ function clipboardPathRegex(tmpRoot = tmpdir()) {
18
+ const cached = clipboardPathRegexCache.get(tmpRoot);
19
+ if (cached) return cached;
20
+ const exts = CLIPBOARD_IMAGE_EXTENSIONS.join("|");
21
+ const regex = new RegExp(
22
+ `(?<![a-zA-Z0-9])(${escapeRegExp(tmpRoot)}/pi-clipboard-${UUID_PATTERN}\\.(${exts}))(?![a-zA-Z0-9])`,
23
+ "g"
24
+ );
25
+ if (clipboardPathRegexCache.size >= CLIPBOARD_PATH_REGEX_CACHE_MAX) {
26
+ const oldest = clipboardPathRegexCache.keys().next().value;
27
+ if (oldest !== void 0) clipboardPathRegexCache.delete(oldest);
28
+ }
29
+ clipboardPathRegexCache.set(tmpRoot, regex);
30
+ return regex;
31
+ }
32
+ function isSingleClipboardImagePath(text, tmpRoot) {
33
+ if (!text || /\s/.test(text)) return false;
34
+ const match = text.match(clipboardPathRegex(tmpRoot ?? tmpdir()));
35
+ return match !== null && match.length === 1 && match[0] === text;
36
+ }
37
+ function extractClipboardImageTokens(text, tmpRoot) {
38
+ const tokens = /* @__PURE__ */ new Set();
39
+ for (const match of text.matchAll(clipboardPathRegex(tmpRoot ?? tmpdir()))) {
40
+ const token = match[1];
41
+ if (token) tokens.add(token);
42
+ }
43
+ return [...tokens];
44
+ }
45
+ var MARKER_AT_END_PATTERN = /\[Image #([0-9]+)\]( ?)$/;
46
+ function clipboardMarkerAtEnd(text) {
47
+ const match = MARKER_AT_END_PATTERN.exec(text);
48
+ if (!match) return void 0;
49
+ return { index: Number(match[1]), length: match[0].length };
50
+ }
51
+
52
+ // extension-src/pi-style/shared/clipboard-presence.ts
53
+ import { realpathSync } from "fs";
54
+ import { createRequire } from "module";
55
+ import { dirname } from "path";
56
+ var cachedModule;
57
+ function clipboardResolutionRoots() {
58
+ const roots = [];
59
+ const argvMain = process.argv[1];
60
+ if (typeof argvMain === "string" && argvMain.startsWith("/")) {
61
+ try {
62
+ roots.push(dirname(realpathSync(argvMain)));
63
+ } catch {
64
+ roots.push(dirname(argvMain));
65
+ }
66
+ }
67
+ roots.push(import.meta.url);
68
+ return roots;
69
+ }
70
+ function loadClipboardModule() {
71
+ if (cachedModule !== void 0) return cachedModule;
72
+ if (process.env.TERMUX_VERSION) {
73
+ cachedModule = null;
74
+ return cachedModule;
75
+ }
76
+ for (const root of clipboardResolutionRoots()) {
77
+ try {
78
+ const require2 = createRequire(root);
79
+ const resolved = require2("@mariozechner/clipboard");
80
+ if (resolved && typeof resolved.hasImage === "function") {
81
+ cachedModule = resolved;
82
+ return cachedModule;
83
+ }
84
+ } catch {
85
+ }
86
+ }
87
+ cachedModule = null;
88
+ return cachedModule;
89
+ }
90
+ function clipboardHasImageSync() {
91
+ const clipboard = loadClipboardModule();
92
+ if (!clipboard || typeof clipboard.hasImage !== "function") return null;
93
+ try {
94
+ return clipboard.hasImage() === true;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function readClipboardImageBinary(options = {}) {
100
+ const clipboard = loadClipboardModule();
101
+ if (!clipboard || typeof clipboard.hasImage !== "function" || typeof clipboard.getImageBinary !== "function") {
102
+ return null;
103
+ }
104
+ try {
105
+ if (!options.skipPresenceProbe && !clipboard.hasImage()) return null;
106
+ const imageData = await clipboard.getImageBinary();
107
+ if (!imageData || imageData.length === 0) return null;
108
+ const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
109
+ return bytes.length > 0 ? { bytes } : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ // extension-src/pi-style/shared/pending-images.ts
116
+ import { readFile, stat } from "fs/promises";
117
+ var MAX_ATTACHABLE_BYTES = 20 * 1024 * 1024;
118
+ var MIME_BY_EXTENSION = Object.freeze({
119
+ png: "image/png",
120
+ jpg: "image/jpeg",
121
+ jpeg: "image/jpeg",
122
+ webp: "image/webp",
123
+ gif: "image/gif"
124
+ });
125
+ var pendingImages = /* @__PURE__ */ new Map();
126
+ var nextPendingIndex = 1;
127
+ function resetPendingImageRegistry() {
128
+ pendingImages.clear();
129
+ nextPendingIndex = 1;
130
+ }
131
+ function allocatePendingImage() {
132
+ const index = nextPendingIndex++;
133
+ let resolveFill = () => {
134
+ };
135
+ const entry = {
136
+ fill: new Promise((resolve3) => {
137
+ resolveFill = resolve3;
138
+ }),
139
+ settled: false
140
+ };
141
+ pendingImages.set(index, entry);
142
+ entry.resolveFill = resolveFill;
143
+ return { index, marker: `${clipboardImageMarker(index)} ` };
144
+ }
145
+ async function fillPendingImageFromPath(index, path, deps = {}) {
146
+ const entry = pendingImages.get(index);
147
+ if (!entry || entry.settled) return;
148
+ const extension = path.split(".").pop() ?? "";
149
+ const mimeType = MIME_BY_EXTENSION[extension];
150
+ if (!mimeType) {
151
+ settle(entry);
152
+ return;
153
+ }
154
+ const readFileImpl = deps.readFile ?? ((p) => readFile(p));
155
+ const statSizeImpl = deps.statSize ?? (async (p) => {
156
+ try {
157
+ return (await stat(p)).size;
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ });
162
+ try {
163
+ const size = await statSizeImpl(path);
164
+ if (size === void 0 || size <= 0 || size > MAX_ATTACHABLE_BYTES) {
165
+ settle(entry);
166
+ return;
167
+ }
168
+ const bytes = await readFileImpl(path);
169
+ if (bytes.length === 0 || bytes.length > MAX_ATTACHABLE_BYTES) {
170
+ settle(entry);
171
+ return;
172
+ }
173
+ entry.data = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
174
+ entry.mimeType = mimeType;
175
+ } catch {
176
+ }
177
+ settle(entry);
178
+ }
179
+ function fillPendingImageBytes(index, data, mimeType) {
180
+ const entry = pendingImages.get(index);
181
+ if (!entry || entry.settled) return;
182
+ if (data.length > 0) {
183
+ entry.data = data;
184
+ entry.mimeType = mimeType;
185
+ }
186
+ settle(entry);
187
+ }
188
+ function discardPendingImage(index) {
189
+ const entry = pendingImages.get(index);
190
+ if (!entry) return;
191
+ settle(entry);
192
+ pendingImages.delete(index);
193
+ }
194
+ function isPendingMarkerIndex(index) {
195
+ return pendingImages.has(index);
196
+ }
197
+ function discardPendingMarkerIndex(index) {
198
+ discardPendingImage(index);
199
+ }
200
+ function isPendingImageFilled(index) {
201
+ const entry = pendingImages.get(index);
202
+ return entry !== void 0 && entry.data !== void 0 && entry.mimeType !== void 0;
203
+ }
204
+ function settle(entry) {
205
+ entry.settled = true;
206
+ entry.resolveFill?.();
207
+ }
208
+ async function resolvePendingImagesForSubmit(text) {
209
+ if (pendingImages.size === 0) return void 0;
210
+ const markerRe = /\[Image #([0-9]+)\]/g;
211
+ const found = [];
212
+ for (const match of text.matchAll(markerRe)) {
213
+ const index = Number(match[1]);
214
+ if (Number.isInteger(index) && pendingImages.has(index)) found.push(index);
215
+ }
216
+ const matched = new Set(found);
217
+ await Promise.all(
218
+ [...pendingImages.entries()].filter(([index]) => matched.has(index)).map(([, entry]) => entry.fill)
219
+ );
220
+ const images = [];
221
+ for (const index of [...matched].sort((a, b) => a - b)) {
222
+ const entry = pendingImages.get(index);
223
+ if (entry?.data && entry.mimeType) {
224
+ images.push({ type: "image", data: entry.data, mimeType: entry.mimeType });
225
+ }
226
+ }
227
+ for (const index of [...pendingImages.keys()]) discardPendingImage(index);
228
+ return images.length > 0 ? { images } : void 0;
229
+ }
230
+
231
+ // extension-src/pi-style/features/messages/render-config.ts
232
+ var sessionMessagesConfig = {
233
+ showImagePreviews: true,
234
+ clipboardImages: true,
235
+ previewMaxWidth: 30
236
+ };
237
+ function setMessagesRenderConfig(config) {
238
+ sessionMessagesConfig = { ...sessionMessagesConfig, ...config };
239
+ }
240
+ function getMessagesRenderConfig() {
241
+ return sessionMessagesConfig;
242
+ }
243
+
244
+ // extension-src/pi-style/features/messages/image-input.ts
245
+ var IMAGE_TOKEN = "[image]";
246
+ var MAX_ATTACHABLE_BYTES2 = 20 * 1024 * 1024;
247
+ var MIME_BY_EXTENSION2 = Object.freeze({
248
+ png: "image/png",
249
+ jpg: "image/jpeg",
250
+ jpeg: "image/jpeg",
251
+ webp: "image/webp",
252
+ gif: "image/gif"
253
+ });
254
+ function createClipboardImagePasteSurface(deps = {}) {
255
+ const hasImage = deps.hasImage ?? clipboardHasImageSync;
256
+ const readBinary = deps.readBinary ?? (() => readClipboardImageBinary({ skipPresenceProbe: true }));
257
+ return {
258
+ enabled: () => getMessagesRenderConfig().clipboardImages,
259
+ clipboardHasImage: () => {
260
+ try {
261
+ return hasImage();
262
+ } catch {
263
+ return null;
264
+ }
265
+ },
266
+ markerFromClipboard() {
267
+ const { index, marker: marker2 } = allocatePendingImage();
268
+ void (async () => {
269
+ try {
270
+ const image = await readBinary();
271
+ if (image) {
272
+ fillPendingImageBytes(
273
+ index,
274
+ Buffer.from(image.bytes.buffer, image.bytes.byteOffset, image.bytes.byteLength).toString("base64"),
275
+ "image/png"
276
+ );
277
+ return;
278
+ }
279
+ } catch {
280
+ }
281
+ discardPendingMarkerIndex(index);
282
+ })();
283
+ return marker2;
284
+ },
285
+ async markerFromArtifact(path) {
286
+ const { index, marker: marker2 } = allocatePendingImage();
287
+ await fillPendingImageFromPath(index, path);
288
+ return isPendingImageFilled(index) ? marker2 : path;
289
+ },
290
+ isMarkerIndexRegistered: (index) => isPendingMarkerIndex(index),
291
+ discardMarkerIndex: (index) => discardPendingMarkerIndex(index)
292
+ };
293
+ }
294
+ async function resolvePendingImageMarkers(text) {
295
+ if (!getMessagesRenderConfig().clipboardImages) return void 0;
296
+ return resolvePendingImagesForSubmit(text);
297
+ }
298
+ async function transformClipboardImages(text, deps = {}) {
299
+ if (!getMessagesRenderConfig().clipboardImages) return void 0;
300
+ const tokens = extractClipboardImageTokens(text, deps.tmpRoot);
301
+ if (tokens.length === 0) return void 0;
302
+ const attached = [];
303
+ const rewritten = /* @__PURE__ */ new Map();
304
+ for (const token of tokens) {
305
+ const image = await readAttachable(token, deps);
306
+ if (!image) continue;
307
+ attached.push({ type: "image", data: image.data, mimeType: image.mimeType });
308
+ rewritten.set(token, IMAGE_TOKEN);
309
+ }
310
+ if (attached.length === 0) return void 0;
311
+ return { text: rewriteTokens(text, rewritten, deps.tmpRoot), images: attached };
312
+ }
313
+ async function readAttachable(path, deps) {
314
+ const extension = path.split(".").pop() ?? "";
315
+ const mimeType = MIME_BY_EXTENSION2[extension];
316
+ if (!mimeType) return void 0;
317
+ const readFileImpl = deps?.readFile ?? ((p) => readFile2(p));
318
+ const statSizeImpl = deps?.statSize ?? (async (p) => {
319
+ try {
320
+ return (await stat2(p)).size;
321
+ } catch {
322
+ return void 0;
323
+ }
324
+ });
325
+ try {
326
+ const size = await statSizeImpl(path);
327
+ if (size === void 0 || size <= 0 || size > MAX_ATTACHABLE_BYTES2) return void 0;
328
+ const bytes = await readFileImpl(path);
329
+ if (bytes.length === 0 || bytes.length > MAX_ATTACHABLE_BYTES2) return void 0;
330
+ return { data: Buffer.from(bytes).toString("base64"), mimeType };
331
+ } catch {
332
+ return void 0;
333
+ }
334
+ }
335
+ function rewriteTokens(text, rewritten, tmpRoot) {
336
+ if (rewritten.size === 0) return text;
337
+ return text.replace(
338
+ clipboardPathRegex(tmpRoot ?? tmpdir2()),
339
+ (match, token) => rewritten.has(token) ? IMAGE_TOKEN : match
340
+ );
341
+ }
342
+
343
+ // extension-src/pi-style/features/messages/image-preview.ts
344
+ import {
345
+ getCapabilities,
346
+ getCellDimensions,
347
+ getImageDimensions,
348
+ Image
349
+ } from "@earendil-works/pi-tui";
350
+
1
351
  // extension-src/pi-style/shared/ansi.ts
2
352
  import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
3
353
  function isFinal(byte) {
@@ -214,6 +564,174 @@ function fgHex(theme, hex, text) {
214
564
  return `${escapes.prefix}${text}${escapes.suffix}`;
215
565
  }
216
566
 
567
+ // extension-src/pi-style/features/messages/image-preview.ts
568
+ var IMAGE_PREVIEW_ENTRY_TYPE = "pi-style-image-preview";
569
+ var IMAGE_PREVIEW_EXPANDED_MAX_WIDTH_CELLS = 60;
570
+ var IMAGE_PREVIEW_MAX_WIDTH_CELLS = 60;
571
+ var GRID_GAP = 2;
572
+ var GRID_MIN_COLUMN = 14;
573
+ var GRID_MAX_HEIGHT_RATIO = 1.5;
574
+ var FALLBACK_IMAGE_DIMENSIONS = { widthPx: 800, heightPx: 600 };
575
+ function isPreviewableImage(value) {
576
+ if (!value || typeof value !== "object") return false;
577
+ const data = value.data;
578
+ const mimeType = value.mimeType;
579
+ return typeof data === "string" && data.length > 0 && typeof mimeType === "string" && mimeType.length > 0;
580
+ }
581
+ function stageImagePreviewData(images) {
582
+ if (!getMessagesRenderConfig().showImagePreviews) return void 0;
583
+ const valid = images.filter(isPreviewableImage).map((image) => ({ data: image.data, mimeType: image.mimeType }));
584
+ return valid.length > 0 ? { images: valid } : void 0;
585
+ }
586
+ function flushImagePreviewEntry(pi, data) {
587
+ if (!getMessagesRenderConfig().showImagePreviews) return;
588
+ pi.appendEntry(IMAGE_PREVIEW_ENTRY_TYPE, data);
589
+ }
590
+ function previewImagesFromEntryData(data) {
591
+ if (!data || typeof data !== "object") return void 0;
592
+ const images = data.images;
593
+ if (!Array.isArray(images) || images.length === 0) return void 0;
594
+ if (!images.every(isPreviewableImage)) return void 0;
595
+ return images;
596
+ }
597
+ function imageLabel(theme, index, dims) {
598
+ const dimsPart = dims ? ` \xB7 ${dims.widthPx}\xD7${dims.heightPx}` : "";
599
+ return theme.fg("dim", `#${index}${dimsPart}`);
600
+ }
601
+ var __dimensionParsesForTest = { count: 0 };
602
+ function parseImageDimensions(image) {
603
+ __dimensionParsesForTest.count++;
604
+ const fromPrefix = getImageDimensions(image.data.slice(0, 256), image.mimeType);
605
+ return fromPrefix ?? getImageDimensions(image.data, image.mimeType) ?? void 0;
606
+ }
607
+ var previewCache = /* @__PURE__ */ new WeakMap();
608
+ function renderImagePreviewEntry(entry, options, theme) {
609
+ if (!getMessagesRenderConfig().showImagePreviews) return void 0;
610
+ const images = previewImagesFromEntryData(entry?.data);
611
+ if (!images) return void 0;
612
+ const fg = theme?.fg;
613
+ if (typeof fg !== "function") return void 0;
614
+ const expanded = options?.expanded === true;
615
+ try {
616
+ const themed = { fg: fg.bind(theme) };
617
+ let cached = previewCache.get(entry);
618
+ if (!cached) {
619
+ const dims = images.map(parseImageDimensions);
620
+ const components2 = images.map(
621
+ (image, index) => new Image(
622
+ image.data,
623
+ image.mimeType,
624
+ { fallbackColor: (text) => themed.fg("toolOutput", text) },
625
+ {},
626
+ dims[index]
627
+ )
628
+ );
629
+ const labels2 = images.map((_image, index) => imageLabel(themed, index + 1, dims[index]));
630
+ cached = { images, dimensions: dims, components: components2, labels: labels2 };
631
+ previewCache.set(entry, cached);
632
+ }
633
+ const { components, dimensions, labels } = cached;
634
+ let memo;
635
+ return {
636
+ invalidate() {
637
+ memo = void 0;
638
+ for (const component of components) component.invalidate();
639
+ },
640
+ render(width) {
641
+ const config = getMessagesRenderConfig();
642
+ const kitty = getCapabilities().images === "kitty";
643
+ const key = `${width}|${expanded}|${config.previewMaxWidth}|${config.showImagePreviews}|${kitty}`;
644
+ if (memo && memo.key === key) return memo.lines;
645
+ const maxWidth = expanded ? IMAGE_PREVIEW_EXPANDED_MAX_WIDTH_CELLS : Math.min(config.previewMaxWidth, IMAGE_PREVIEW_MAX_WIDTH_CELLS);
646
+ let lines;
647
+ if (width <= 0) lines = [];
648
+ else if (images.length === 1 || !kitty) lines = renderStacked(width, maxWidth, labels, components);
649
+ else lines = renderGrid(width, maxWidth, labels, components, dimensions);
650
+ memo = { key, lines };
651
+ return lines;
652
+ }
653
+ };
654
+ } catch {
655
+ return void 0;
656
+ }
657
+ }
658
+ function renderStacked(width, maxWidth, labels, components) {
659
+ const lines = [];
660
+ for (const [index, component] of components.entries()) {
661
+ if (index > 0) lines.push("");
662
+ lines.push(labels[index] ?? "");
663
+ lines.push(...component.render(Math.min(width, Math.max(1, maxWidth + 2))));
664
+ }
665
+ return lines;
666
+ }
667
+ function stripTrailingSpaces(line) {
668
+ if (!line.endsWith(" ")) return line;
669
+ let end = line.length;
670
+ while (end > 0 && line.charCodeAt(end - 1) === 32) end--;
671
+ return line.slice(0, end);
672
+ }
673
+ function renderGrid(width, maxWidth, labels, components, dimensions) {
674
+ const usable = Math.max(1, width - 2);
675
+ const maxColumns = Math.floor((usable + GRID_GAP) / (GRID_MIN_COLUMN + GRID_GAP));
676
+ const columns = Math.max(1, Math.min(components.length, maxColumns, 3));
677
+ if (columns < 2) return renderStacked(width, maxWidth, labels, components);
678
+ const columnWidth = Math.min(maxWidth, Math.floor((usable - (columns - 1) * GRID_GAP) / columns));
679
+ if (columnWidth < GRID_MIN_COLUMN) return renderStacked(width, maxWidth, labels, components);
680
+ const lines = [];
681
+ const cellDimensions = getCellDimensions();
682
+ for (let start = 0; start < components.length; start += columns) {
683
+ const group = components.slice(start, start + columns);
684
+ const groupLabels = labels.slice(start, start + columns);
685
+ const groupDimensions = dimensions.slice(start, start + columns);
686
+ if (start > 0) lines.push("");
687
+ const rendered = group.map((component) => component.render(columnWidth));
688
+ const sizes = groupDimensions.map((imageDimensions) => imageCellSize(imageDimensions, columnWidth, cellDimensions));
689
+ const rowCounts = rendered.map((column) => column.length).filter((count) => count > 0);
690
+ const minRows = Math.min(...rowCounts);
691
+ const maxRows = Math.max(...rowCounts);
692
+ if (rowCounts.length > 1 && minRows > 0 && maxRows >= minRows * GRID_MAX_HEIGHT_RATIO) {
693
+ return renderStacked(width, maxWidth, labels, components);
694
+ }
695
+ const slotWidths = groupLabels.map(
696
+ (label, index) => Math.max(sizes[index]?.columns ?? 1, visibleWidth(stripAnsi(label)))
697
+ );
698
+ const labelRow = groupLabels.map((label, index) => {
699
+ const pad = Math.max(0, (slotWidths[index] ?? 0) - visibleWidth(stripAnsi(label)));
700
+ return label + " ".repeat(pad);
701
+ }).join(" ".repeat(GRID_GAP)).trimEnd();
702
+ lines.push(labelRow);
703
+ const rows = Math.max(...rendered.map((column) => column.length));
704
+ for (let row = 0; row < rows; row++) {
705
+ let line = "";
706
+ let startCol = 0;
707
+ for (let column = 0; column < rendered.length; column++) {
708
+ if (column > 0) line += `\x1B[${startCol + 1}G`;
709
+ line += rendered[column]?.[row] ?? "";
710
+ startCol += (slotWidths[column] ?? 0) + GRID_GAP;
711
+ }
712
+ lines.push(stripTrailingSpaces(line));
713
+ }
714
+ }
715
+ return lines;
716
+ }
717
+ function imageCellSize(dimensions, requestedWidth, cellDimensions) {
718
+ const imageDimensions = dimensions ?? FALLBACK_IMAGE_DIMENSIONS;
719
+ const maxWidth = Math.max(1, Math.min(requestedWidth - 2, 60));
720
+ const maxHeight = Math.max(1, Math.ceil(maxWidth * cellDimensions.widthPx / cellDimensions.heightPx));
721
+ const imageWidth = Math.max(1, imageDimensions.widthPx);
722
+ const imageHeight = Math.max(1, imageDimensions.heightPx);
723
+ const widthScale = maxWidth * cellDimensions.widthPx / imageWidth;
724
+ const heightScale = maxHeight * cellDimensions.heightPx / imageHeight;
725
+ const scale = Math.min(widthScale, heightScale);
726
+ return {
727
+ columns: Math.max(1, Math.min(maxWidth, Math.ceil(imageWidth * scale / cellDimensions.widthPx))),
728
+ rows: Math.max(1, Math.min(maxHeight, Math.ceil(imageHeight * scale / cellDimensions.heightPx)))
729
+ };
730
+ }
731
+ function registerImagePreviewSurface(pi) {
732
+ pi.registerEntryRenderer(IMAGE_PREVIEW_ENTRY_TYPE, renderImagePreviewEntry);
733
+ }
734
+
217
735
  // extension-src/pi-style/shared/box.ts
218
736
  import { homedir as homedir2 } from "os";
219
737
  import { relative, resolve as resolve2 } from "path";
@@ -511,9 +1029,9 @@ function boxedResultRenderBudget(rawLineBudget = DEFAULT_COLLAPSED_RENDER_LINES)
511
1029
  // extension-src/pi-style/shared/theme-extras.ts
512
1030
  import { existsSync, readdirSync, readFileSync } from "fs";
513
1031
  import { homedir } from "os";
514
- import { dirname, join, resolve } from "path";
1032
+ import { dirname as dirname2, join, resolve } from "path";
515
1033
  import { fileURLToPath } from "url";
516
- var extensionDir = dirname(fileURLToPath(import.meta.url));
1034
+ var extensionDir = dirname2(fileURLToPath(import.meta.url));
517
1035
  var THEME_EXTRA_DEFAULTS = Object.freeze({
518
1036
  assistantPrefix: "\u2022",
519
1037
  assistantPrefixColor: "",
@@ -4212,16 +4730,16 @@ function parseGitShowStat(text) {
4212
4730
  }
4213
4731
  break;
4214
4732
  }
4215
- const stat = parseGitDiffStat(lines.slice(i).join("\n"));
4216
- if (!stat) return null;
4733
+ const stat3 = parseGitDiffStat(lines.slice(i).join("\n"));
4734
+ if (!stat3) return null;
4217
4735
  return {
4218
4736
  kind: "show-stat",
4219
4737
  hash,
4220
4738
  subject,
4221
- files: stat.files,
4222
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4223
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4224
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
4739
+ files: stat3.files,
4740
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
4741
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
4742
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4225
4743
  };
4226
4744
  }
4227
4745
  var DIFF_GIT_HEADER = /^diff --git a\/(.*) b\/(.*)$/;
@@ -4548,17 +5066,17 @@ function parseGitPull(text) {
4548
5066
  return null;
4549
5067
  }
4550
5068
  if (!range || ffIndex < 0) return null;
4551
- const stat = parseGitDiffStat(lines.slice(ffIndex + 1).join("\n"));
4552
- if (!stat) return null;
5069
+ const stat3 = parseGitDiffStat(lines.slice(ffIndex + 1).join("\n"));
5070
+ if (!stat3) return null;
4553
5071
  return {
4554
5072
  kind: "action",
4555
5073
  command: "pull",
4556
- files: stat.files,
5074
+ files: stat3.files,
4557
5075
  status: "Fast-forward",
4558
5076
  range,
4559
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4560
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4561
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5077
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5078
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5079
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4562
5080
  };
4563
5081
  }
4564
5082
  function parseGitFetch(text) {
@@ -4700,30 +5218,30 @@ function parseGitMerge(text) {
4700
5218
  }
4701
5219
  const marker2 = body[idx] ?? "";
4702
5220
  if (range && marker2 === "Fast-forward") {
4703
- const stat = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
4704
- if (!stat) return null;
5221
+ const stat3 = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
5222
+ if (!stat3) return null;
4705
5223
  return {
4706
5224
  kind: "action",
4707
5225
  command: "merge",
4708
- files: stat.files,
5226
+ files: stat3.files,
4709
5227
  status: "Fast-forward",
4710
5228
  range,
4711
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4712
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4713
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5229
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5230
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5231
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4714
5232
  };
4715
5233
  }
4716
5234
  if (MERGE_MADE.exec(marker2)) {
4717
- const stat = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
4718
- if (!stat) return null;
5235
+ const stat3 = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
5236
+ if (!stat3) return null;
4719
5237
  return {
4720
5238
  kind: "action",
4721
5239
  command: "merge",
4722
- files: stat.files,
5240
+ files: stat3.files,
4723
5241
  status: marker2,
4724
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4725
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4726
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5242
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5243
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5244
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4727
5245
  };
4728
5246
  }
4729
5247
  return null;
@@ -7095,7 +7613,7 @@ var DEFAULT_CONFIG = Object.freeze({
7095
7613
  startup: Object.freeze({ mode: "compact", showResources: false, alwaysExpanded: false }),
7096
7614
  statusLine: Object.freeze({
7097
7615
  enabled: true,
7098
- separator: "powerline-thin",
7616
+ separator: "|",
7099
7617
  layout: Object.freeze({
7100
7618
  left: ["path", "git", "context_bar", "cost"],
7101
7619
  right: ["model_effort"],
@@ -7112,7 +7630,10 @@ var DEFAULT_CONFIG = Object.freeze({
7112
7630
  assistantPrefix: true,
7113
7631
  specialBlocks: true,
7114
7632
  hideThinkingLabel: true,
7115
- hideInterimText: true
7633
+ hideInterimText: true,
7634
+ showImagePreviews: true,
7635
+ clipboardImages: true,
7636
+ previewMaxWidth: 30
7116
7637
  }),
7117
7638
  tools: Object.freeze({
7118
7639
  enabled: true,
@@ -7229,7 +7750,10 @@ function normalizeConfig(input, defaults = DEFAULT_CONFIG) {
7229
7750
  assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
7230
7751
  specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
7231
7752
  hideThinkingLabel: bool(messages.hideThinkingLabel, defaults.messages.hideThinkingLabel),
7232
- hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText)
7753
+ hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText),
7754
+ showImagePreviews: bool(messages.showImagePreviews, defaults.messages.showImagePreviews),
7755
+ clipboardImages: bool(messages.clipboardImages, defaults.messages.clipboardImages),
7756
+ previewMaxWidth: boundedInt(messages.previewMaxWidth, defaults.messages.previewMaxWidth, 8, 60)
7233
7757
  }),
7234
7758
  tools: Object.freeze({
7235
7759
  enabled: bool(tools.enabled, defaults.tools.enabled),
@@ -7283,6 +7807,8 @@ var BOOL_PATHS = /* @__PURE__ */ new Set([
7283
7807
  "messages.specialBlocks",
7284
7808
  "messages.hideThinkingLabel",
7285
7809
  "messages.hideInterimText",
7810
+ "messages.showImagePreviews",
7811
+ "messages.clipboardImages",
7286
7812
  "tools.enabled",
7287
7813
  "tools.showElapsed",
7288
7814
  "tools.dimOutput",
@@ -7328,6 +7854,8 @@ function validLeaf(path, value) {
7328
7854
  return typeof value === "string";
7329
7855
  if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
7330
7856
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7857
+ if (path === "messages.previewMaxWidth")
7858
+ return typeof value === "number" && Number.isFinite(value) && value >= 8 && value <= 60;
7331
7859
  if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
7332
7860
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7333
7861
  if (STRING_ARRAY_PATHS.has(path)) return Array.isArray(value) && value.every((item) => typeof item === "string");
@@ -7751,6 +8279,9 @@ var allowedPaths = /* @__PURE__ */ new Set([
7751
8279
  "messages.specialBlocks",
7752
8280
  "messages.hideThinkingLabel",
7753
8281
  "messages.hideInterimText",
8282
+ "messages.showImagePreviews",
8283
+ "messages.clipboardImages",
8284
+ "messages.previewMaxWidth",
7754
8285
  "tools.enabled",
7755
8286
  "tools.style",
7756
8287
  "tools.maxCollapsedLines",
@@ -7783,6 +8314,9 @@ function validatePathValue(path, value) {
7783
8314
  "messages.specialBlocks",
7784
8315
  "messages.hideThinkingLabel",
7785
8316
  "messages.hideInterimText",
8317
+ "messages.showImagePreviews",
8318
+ "messages.clipboardImages",
8319
+ "messages.previewMaxWidth",
7786
8320
  "tools.enabled",
7787
8321
  "tools.showElapsed",
7788
8322
  "tools.dimOutput",
@@ -7805,6 +8339,8 @@ function validatePathValue(path, value) {
7805
8339
  return typeof value === "string";
7806
8340
  if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
7807
8341
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
8342
+ if (path === "messages.previewMaxWidth")
8343
+ return typeof value === "number" && Number.isFinite(value) && value >= 8 && value <= 60;
7808
8344
  if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
7809
8345
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7810
8346
  if (path.endsWith(".colors") || path.endsWith(".glyphs"))
@@ -7937,8 +8473,8 @@ function runCommand(args, host, app, storage) {
7937
8473
  }
7938
8474
 
7939
8475
  // extension-src/pi-style/pi/config-host.ts
7940
- import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
7941
- import { dirname as dirname2, join as join2 } from "path";
8476
+ import { mkdir, readFile as readFile3, rename, unlink, writeFile } from "fs/promises";
8477
+ import { dirname as dirname3, join as join2 } from "path";
7942
8478
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
7943
8479
  var locks = /* @__PURE__ */ new Map();
7944
8480
  function serialize(path, operation) {
@@ -7951,14 +8487,14 @@ function serialize(path, operation) {
7951
8487
  }
7952
8488
  function createPiConfigFilePort(fs = {}) {
7953
8489
  const fsMkdir = fs.mkdir ?? mkdir;
7954
- const fsReadFile = fs.readFile ?? readFile;
8490
+ const fsReadFile = fs.readFile ?? readFile3;
7955
8491
  const fsRename = fs.rename ?? rename;
7956
8492
  const fsUnlink = fs.unlink ?? unlink;
7957
8493
  const fsWriteFile = fs.writeFile ?? writeFile;
7958
8494
  return {
7959
8495
  read: (path) => fsReadFile(path, "utf8"),
7960
8496
  writeAtomic: async (path, content) => serialize(path, async () => {
7961
- await fsMkdir(dirname2(path), { recursive: true });
8497
+ await fsMkdir(dirname3(path), { recursive: true });
7962
8498
  const temporary = `${path}.pi-style-${process.pid}-${Date.now()}.tmp`;
7963
8499
  try {
7964
8500
  await fsWriteFile(temporary, content, { mode: 384 });
@@ -8147,12 +8683,17 @@ function createBuiltinSegments() {
8147
8683
  const percent = contextPercent(snapshot.context ?? {});
8148
8684
  if (percent === void 0) return { visible: false, content: "" };
8149
8685
  const token = contextBarToken(percent);
8150
- const window = snapshot.context?.windowTokens;
8151
- const label = `ctx${window !== void 0 ? ` (${formatTokens(window)})` : ""}:`;
8152
8686
  const width = options.context_bar?.width ?? CONTEXT_BAR_WIDTH;
8687
+ const pipe = theme.apply("separator", "|");
8688
+ const bar = `${theme.apply("muted", "[")}${theme.apply(token, contextBar(percent, width))}${theme.apply("muted", "]")}`;
8689
+ const parts = [bar, `${theme.apply(token, `${Math.round(percent)}%`)}${theme.apply("muted", " used")}`];
8690
+ const current = snapshot.context?.currentTokens;
8691
+ const window = snapshot.context?.windowTokens;
8692
+ if (current !== void 0 && window !== void 0)
8693
+ parts.push(theme.apply("muted", `${formatTokens(current)}/${formatTokens(window)}`));
8153
8694
  return {
8154
8695
  visible: true,
8155
- content: `${theme.apply("muted", label)} ${theme.apply(token, contextBar(percent, width))} ${theme.apply(token, `${Math.round(percent)}%`)}`,
8696
+ content: parts.join(` ${pipe} `),
8156
8697
  compactContent: theme.apply(token, `${Math.round(percent)}%`)
8157
8698
  };
8158
8699
  }),
@@ -8235,10 +8776,7 @@ function contextBarToken(percent) {
8235
8776
  return "success";
8236
8777
  }
8237
8778
  function formatTokens(value) {
8238
- if (value >= 1e6) {
8239
- const millions = value / 1e6;
8240
- return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`;
8241
- }
8779
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
8242
8780
  if (value >= 1e3) {
8243
8781
  const thousands = value / 1e3;
8244
8782
  return `${Number.isInteger(thousands) ? thousands : thousands.toFixed(1)}K`;
@@ -8546,6 +9084,10 @@ var StyledEditor = class extends CustomEditor {
8546
9084
  semanticDirty = false;
8547
9085
  disposed = false;
8548
9086
  renderPlanCache;
9087
+ clipboardImagePaste;
9088
+ /** Keybinding matcher (the factory's KeybindingsManager) for keystroke-level
9089
+ * interception: the paste keybinding and backspace checks below. */
9090
+ editorKeybindings;
8549
9091
  constructor(tui, theme, keybindings, options) {
8550
9092
  super(tui, theme, keybindings);
8551
9093
  this.config = options.config;
@@ -8553,6 +9095,8 @@ var StyledEditor = class extends CustomEditor {
8553
9095
  this.piTheme = theme;
8554
9096
  this.fullTheme = options.fullTheme;
8555
9097
  this.onSnapshot = options.onSnapshot;
9098
+ this.clipboardImagePaste = options.clipboardImagePaste;
9099
+ this.editorKeybindings = keybindings;
8556
9100
  this.semantic = semanticTheme(theme, options.config);
8557
9101
  this.setPaddingX(0);
8558
9102
  }
@@ -8569,9 +9113,88 @@ var StyledEditor = class extends CustomEditor {
8569
9113
  }
8570
9114
  handleInput(data) {
8571
9115
  if (this.disposed) return;
9116
+ if (this.handleClipboardImagePasteKeystroke(data)) return;
9117
+ if (this.handleAtomicMarkerBackspace(data)) return;
8572
9118
  super.handleInput(data);
8573
9119
  this.onSnapshot({ ...this.snapshot });
8574
9120
  }
9121
+ /**
9122
+ * Instant marker (ADR 0009): when the built-in paste keybinding fires, the
9123
+ * clipboard holds an image (sync native probe), and the surface is enabled,
9124
+ * the keystroke is owned here — an `[Image #N] ` marker is inserted
9125
+ * immediately (zero-latency feedback) and the bytes fill the registry
9126
+ * entry asynchronously. Pi's own paste handler never runs for this
9127
+ * keystroke, so no temp artifact is written. Text pastes (no image) and
9128
+ * probe-unavailable hosts fall through to the native path untouched.
9129
+ */
9130
+ handleClipboardImagePasteKeystroke(data) {
9131
+ const surface = this.clipboardImagePaste;
9132
+ if (!surface?.enabled()) return false;
9133
+ if (!this.editorKeybindings.matches(data, "app.clipboard.pasteImage")) return false;
9134
+ if (surface.clipboardHasImage() !== true) return false;
9135
+ super.insertTextAtCursor(surface.markerFromClipboard());
9136
+ this.onSnapshot({ ...this.snapshot });
9137
+ return true;
9138
+ }
9139
+ /**
9140
+ * Atomic marker backspace (ADR 0009): backspacing directly after a
9141
+ * registered `[Image #N] ` marker deletes the whole marker as one unit —
9142
+ * the image is discarded (image-paste semantics). Direct state surgery
9143
+ * (O(1)) mirroring pi-tui's handleBackspace mutation protocol: no setText
9144
+ * round-trip (it clears the pastes registry, and undo snapshots store that
9145
+ * Map by reference — silently breaking `[paste #N]` atomicity on undo) and
9146
+ * no per-column left-arrow inputs (O(K·L) through the keybinding chain and
9147
+ * visual line maps). No undo snapshot is pushed: the discarded image
9148
+ * cannot be restored, so the deletion is intentionally non-undoable.
9149
+ */
9150
+ handleAtomicMarkerBackspace(data) {
9151
+ const surface = this.clipboardImagePaste;
9152
+ if (!surface?.enabled()) return false;
9153
+ const isBackspace = this.editorKeybindings.matches(data, "tui.editor.deleteCharBackward") || data === "\x7F";
9154
+ if (!isBackspace) return false;
9155
+ const lines = this.getLines();
9156
+ const cursor = this.getCursor();
9157
+ const current = lines[cursor.line];
9158
+ if (current === void 0) return false;
9159
+ const before = current.slice(0, cursor.col);
9160
+ const hit = clipboardMarkerAtEnd(before);
9161
+ if (!hit || !surface.isMarkerIndexRegistered(hit.index)) return false;
9162
+ const editor = this;
9163
+ const targetCol = cursor.col - hit.length;
9164
+ editor.exitHistoryBrowsing();
9165
+ editor.lastAction = null;
9166
+ editor.cancelAutocomplete();
9167
+ editor.state.lines[cursor.line] = current.slice(0, targetCol) + current.slice(cursor.col);
9168
+ editor.setCursorCol(targetCol);
9169
+ editor.onChange?.(this.getText());
9170
+ surface.discardMarkerIndex(hit.index);
9171
+ this.invalidate();
9172
+ this.onSnapshot({ ...this.snapshot });
9173
+ return true;
9174
+ }
9175
+ /**
9176
+ * Artifact-path fallback (ADR 0009): when the keystroke was not owned
9177
+ * (probe unavailable, native editor path, config toggled) Pi's built-in
9178
+ * paste still inserts a `<tmpdir>/pi-clipboard-<uuid>.<ext>` path through
9179
+ * this method. Convert it to a marker when the artifact reads successfully;
9180
+ * every failure inserts the original text verbatim (exact native behavior).
9181
+ */
9182
+ insertTextAtCursor(text) {
9183
+ const surface = this.clipboardImagePaste;
9184
+ if (!surface?.enabled() || !isSingleClipboardImagePath(text)) {
9185
+ super.insertTextAtCursor(text);
9186
+ return;
9187
+ }
9188
+ void (async () => {
9189
+ let replacement = text;
9190
+ try {
9191
+ replacement = await surface.markerFromArtifact(text);
9192
+ } catch {
9193
+ }
9194
+ super.insertTextAtCursor(replacement);
9195
+ this.tui.requestRender();
9196
+ })();
9197
+ }
8575
9198
  invalidate() {
8576
9199
  super.invalidate();
8577
9200
  if (this.semanticDirty) {
@@ -8787,6 +9410,7 @@ function installEditor(options) {
8787
9410
  snapshot,
8788
9411
  theme,
8789
9412
  ...options.host.theme ? { fullTheme: options.host.theme } : {},
9413
+ ...options.clipboardImagePaste ? { clipboardImagePaste: options.clipboardImagePaste } : {},
8790
9414
  onSnapshot: (next) => {
8791
9415
  if (!disposed && options.isCurrent?.() !== false) snapshot = next;
8792
9416
  }
@@ -9363,7 +9987,7 @@ function renderGroup(items, separator, padding) {
9363
9987
  }
9364
9988
  function renderStatus(layout, snapshot, width, options) {
9365
9989
  if (width <= 0) return { primary: "", lines: [], visibleSegments: [] };
9366
- const separator = options.separator ?? "\u2502";
9990
+ const separator = options.separator ?? "|";
9367
9991
  const padding = options.padding ?? " ";
9368
9992
  const normalized = uniqueLayout(layout);
9369
9993
  const context = { snapshot, theme: options.theme, options: options.options ?? {}, width };
@@ -10180,7 +10804,10 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
10180
10804
  config: currentConfig,
10181
10805
  generation: generation2,
10182
10806
  initialSnapshot: currentSnapshot,
10183
- isCurrent: () => !disposed
10807
+ isCurrent: () => !disposed,
10808
+ // Clipboard image paste surface (ADR 0009): instant `[Image #N] `
10809
+ // markers at keystroke time, artifact fallback, atomic backspace.
10810
+ clipboardImagePaste: createClipboardImagePasteSurface()
10184
10811
  });
10185
10812
  if (editor) {
10186
10813
  disposables.add(editor);
@@ -10878,7 +11505,7 @@ function isTierCAuthorized(input) {
10878
11505
 
10879
11506
  // extension-src/pi-style/pi/compatibility-probe.ts
10880
11507
  import { readFileSync as readFileSync2 } from "fs";
10881
- import { dirname as dirname3, join as join3 } from "path";
11508
+ import { dirname as dirname4, join as join3 } from "path";
10882
11509
  import { fileURLToPath as fileURLToPath2 } from "url";
10883
11510
  import {
10884
11511
  AssistantMessageComponent,
@@ -11675,19 +12302,19 @@ function detectPiVersion(resolution = {}) {
11675
12302
  );
11676
12303
  }
11677
12304
  });
11678
- const readFile2 = resolution.readFile ?? ((path) => readFileSync2(path, "utf8"));
12305
+ const readFile4 = resolution.readFile ?? ((path) => readFileSync2(path, "utf8"));
11679
12306
  try {
11680
12307
  const entry = resolvePackageEntry("@earendil-works/pi-coding-agent");
11681
- let directory = dirname3(entry);
12308
+ let directory = dirname4(entry);
11682
12309
  for (; ; ) {
11683
12310
  const packagePath = join3(directory, "package.json");
11684
12311
  try {
11685
- const packageJson = JSON.parse(readFile2(packagePath));
12312
+ const packageJson = JSON.parse(readFile4(packagePath));
11686
12313
  if (packageJson.name === "@earendil-works/pi-coding-agent" && typeof packageJson.version === "string")
11687
12314
  return { version: packageJson.version };
11688
12315
  } catch {
11689
12316
  }
11690
- const parent = dirname3(directory);
12317
+ const parent = dirname4(directory);
11691
12318
  if (parent === directory) break;
11692
12319
  directory = parent;
11693
12320
  }
@@ -12350,6 +12977,11 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
12350
12977
  };
12351
12978
  const applyMessagesConfig = (config) => {
12352
12979
  sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : void 0);
12980
+ setMessagesRenderConfig({
12981
+ showImagePreviews: config.messages.showImagePreviews,
12982
+ clipboardImages: config.messages.clipboardImages,
12983
+ previewMaxWidth: config.messages.previewMaxWidth
12984
+ });
12353
12985
  };
12354
12986
  const applyAutoTheme = (config, ctx) => {
12355
12987
  const target = config.theme.autoApply;
@@ -12409,6 +13041,7 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
12409
13041
  resetBatchRegistry();
12410
13042
  resetGrepRegistry();
12411
13043
  resetBashTreeRegistry();
13044
+ resetPendingImageRegistry();
12412
13045
  resetTurnRegistry();
12413
13046
  rebuildTurnRegistryFromEntries(ctx.sessionManager.getEntries());
12414
13047
  stopAllElapsedTickers();
@@ -12474,6 +13107,7 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
12474
13107
  resetBatchRegistry();
12475
13108
  resetGrepRegistry();
12476
13109
  resetBashTreeRegistry();
13110
+ resetPendingImageRegistry();
12477
13111
  resetTurnRegistry();
12478
13112
  stopAllElapsedTickers();
12479
13113
  app.sessionShutdown();
@@ -12677,6 +13311,12 @@ function piStyleExtension(pi) {
12677
13311
  pi.registerFlag("pi-style-ascii", { type: "boolean", description: "Use ASCII pi-style markers" });
12678
13312
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
12679
13313
  registerPiStyleCommand(pi, coordinator.app);
13314
+ registerImagePreviewSurface(pi);
13315
+ let stagedImagePreview;
13316
+ let previewFlushTimer;
13317
+ pi.on("before_agent_start", (event) => {
13318
+ stagedImagePreview = stageImagePreviewData(event.images ?? []);
13319
+ });
12680
13320
  pi.on("session_start", async (event, ctx) => {
12681
13321
  resetUsageFromSessionCache(ctx.sessionManager);
12682
13322
  if (pi.getFlag("pi-style-readonly-tools") === true) {
@@ -12688,7 +13328,7 @@ function piStyleExtension(pi) {
12688
13328
  coordinator.app.runtime.current?.dismissStartup();
12689
13329
  beginAgentRun();
12690
13330
  });
12691
- pi.on("input", (event, _ctx) => {
13331
+ pi.on("input", async (event, _ctx) => {
12692
13332
  coordinator.app.runtime.current?.dismissStartup();
12693
13333
  if (event.source === "interactive") {
12694
13334
  const trimmed = event.text.trimStart();
@@ -12697,6 +13337,15 @@ function piStyleExtension(pi) {
12697
13337
  if (trimmed.slice(bangLength).trim() === "") return { action: "handled" };
12698
13338
  }
12699
13339
  }
13340
+ const markerResult = await resolvePendingImageMarkers(event.text);
13341
+ const pathResult = event.source === "interactive" ? await transformClipboardImages(event.text) : void 0;
13342
+ if (markerResult || pathResult) {
13343
+ return {
13344
+ action: "transform",
13345
+ text: pathResult?.text ?? event.text,
13346
+ images: [...markerResult?.images ?? [], ...pathResult?.images ?? []]
13347
+ };
13348
+ }
12700
13349
  return void 0;
12701
13350
  });
12702
13351
  pi.on("tool_execution_start", () => coordinator.app.runtime.current?.dismissStartup());
@@ -12716,8 +13365,25 @@ function piStyleExtension(pi) {
12716
13365
  "session_info_changed",
12717
13366
  (event) => coordinator.app.update({ sessionName: event.name }, "coalesced", { refreshExtensionStatuses: true })
12718
13367
  );
12719
- pi.on("message_start", () => {
13368
+ pi.on("message_start", (event) => {
12720
13369
  closeActiveBatch();
13370
+ if (event.message?.role === "user" && stagedImagePreview && !previewFlushTimer) {
13371
+ const pending = stagedImagePreview;
13372
+ previewFlushTimer = setTimeout(() => {
13373
+ previewFlushTimer = void 0;
13374
+ if (stagedImagePreview !== pending) return;
13375
+ flushImagePreviewEntry(pi, pending);
13376
+ stagedImagePreview = void 0;
13377
+ }, 0);
13378
+ }
13379
+ if (stagedImagePreview && event.message?.role === "assistant") {
13380
+ if (previewFlushTimer) {
13381
+ clearTimeout(previewFlushTimer);
13382
+ previewFlushTimer = void 0;
13383
+ }
13384
+ flushImagePreviewEntry(pi, stagedImagePreview);
13385
+ stagedImagePreview = void 0;
13386
+ }
12721
13387
  });
12722
13388
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
12723
13389
  pi.on(