@quandev104/pi-style 0.2.1 → 0.2.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +8 -3
  3. package/dist/extensions/pi-style.js +1328 -333
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/command-service.ts +8 -0
  6. package/extension-src/pi-style/app/runtime.ts +33 -4
  7. package/extension-src/pi-style/domain/config-normalization.ts +11 -1
  8. package/extension-src/pi-style/domain/config-types.ts +12 -0
  9. package/extension-src/pi-style/domain/status-renderer.ts +41 -9
  10. package/extension-src/pi-style/domain/status.ts +28 -12
  11. package/extension-src/pi-style/domain/theme.ts +32 -1
  12. package/extension-src/pi-style/features/editor/index.ts +144 -5
  13. package/extension-src/pi-style/features/messages/image-input.ts +205 -0
  14. package/extension-src/pi-style/features/messages/image-preview.ts +288 -0
  15. package/extension-src/pi-style/features/messages/index.ts +302 -61
  16. package/extension-src/pi-style/features/messages/render-config.ts +36 -0
  17. package/extension-src/pi-style/features/status-line/index.ts +41 -11
  18. package/extension-src/pi-style/features/tools/boxed/bash.ts +101 -40
  19. package/extension-src/pi-style/features/tools/boxed/batch.ts +17 -1
  20. package/extension-src/pi-style/features/tools/boxed/edit.ts +20 -15
  21. package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
  22. package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
  23. package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
  24. package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
  25. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +18 -11
  26. package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
  27. package/extension-src/pi-style/features/tools/boxed/shared.ts +27 -1
  28. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
  29. package/extension-src/pi-style/pi/index.ts +55 -2
  30. package/extension-src/pi-style/pi/session-coordinator.ts +13 -0
  31. package/extension-src/pi-style/shared/ansi.ts +17 -5
  32. package/extension-src/pi-style/shared/box.ts +70 -4
  33. package/extension-src/pi-style/shared/clipboard-path.ts +98 -0
  34. package/extension-src/pi-style/shared/clipboard-presence.ts +112 -0
  35. package/extension-src/pi-style/shared/pending-images.ts +199 -0
  36. package/extension-src/pi-style/shared/split-diff.ts +8 -5
  37. package/package.json +1 -1
  38. package/extension-src/pi-style/features/.gitkeep +0 -0
@@ -1,4 +1,354 @@
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
+ getImageDimensions,
347
+ Image
348
+ } from "@earendil-works/pi-tui";
349
+
1
350
  // extension-src/pi-style/shared/ansi.ts
351
+ import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
2
352
  function isFinal(byte) {
3
353
  return byte >= "@" && byte <= "~";
4
354
  }
@@ -82,7 +432,7 @@ function stripAnsi(value) {
82
432
  return output;
83
433
  }
84
434
  function visibleWidth(value) {
85
- return [...stripAnsi(value)].length;
435
+ return tuiVisibleWidth(value);
86
436
  }
87
437
  function resetAnsi(value) {
88
438
  return `${value}\x1B[0m`;
@@ -93,9 +443,10 @@ function fitAnsiWidth(value, width, ellipsis = "\u2026") {
93
443
  function truncateAnsi(value, width, ellipsis = "\u2026") {
94
444
  if (width <= 0) return "";
95
445
  if (visibleWidth(value) <= width) return resetAnsi(value);
446
+ const ellipsisWidth = visibleWidth(ellipsis);
96
447
  let output = "";
97
448
  let visible = 0;
98
- for (let i = 0; i < value.length && visible < width - visibleWidth(ellipsis); i++) {
449
+ for (let i = 0; i < value.length && visible < width - ellipsisWidth; i++) {
99
450
  if (value.charCodeAt(i) === 27) {
100
451
  const start = i;
101
452
  i++;
@@ -212,6 +563,147 @@ function fgHex(theme, hex, text) {
212
563
  return `${escapes.prefix}${text}${escapes.suffix}`;
213
564
  }
214
565
 
566
+ // extension-src/pi-style/features/messages/image-preview.ts
567
+ var IMAGE_PREVIEW_ENTRY_TYPE = "pi-style-image-preview";
568
+ var IMAGE_PREVIEW_EXPANDED_MAX_WIDTH_CELLS = 60;
569
+ var IMAGE_PREVIEW_MAX_WIDTH_CELLS = 60;
570
+ var GRID_GAP = 2;
571
+ var GRID_MIN_COLUMN = 14;
572
+ function isPreviewableImage(value) {
573
+ if (!value || typeof value !== "object") return false;
574
+ const data = value.data;
575
+ const mimeType = value.mimeType;
576
+ return typeof data === "string" && data.length > 0 && typeof mimeType === "string" && mimeType.length > 0;
577
+ }
578
+ function stageImagePreviewData(images) {
579
+ if (!getMessagesRenderConfig().showImagePreviews) return void 0;
580
+ const valid = images.filter(isPreviewableImage).map((image) => ({ data: image.data, mimeType: image.mimeType }));
581
+ return valid.length > 0 ? { images: valid } : void 0;
582
+ }
583
+ function flushImagePreviewEntry(pi, data) {
584
+ if (!getMessagesRenderConfig().showImagePreviews) return;
585
+ pi.appendEntry(IMAGE_PREVIEW_ENTRY_TYPE, data);
586
+ }
587
+ function previewImagesFromEntryData(data) {
588
+ if (!data || typeof data !== "object") return void 0;
589
+ const images = data.images;
590
+ if (!Array.isArray(images) || images.length === 0) return void 0;
591
+ if (!images.every(isPreviewableImage)) return void 0;
592
+ return images;
593
+ }
594
+ function imageLabel(theme, index, dims) {
595
+ const dimsPart = dims ? ` \xB7 ${dims.widthPx}\xD7${dims.heightPx}` : "";
596
+ return theme.fg("dim", `#${index}${dimsPart}`);
597
+ }
598
+ var __dimensionParsesForTest = { count: 0 };
599
+ function parseImageDimensions(image) {
600
+ __dimensionParsesForTest.count++;
601
+ const fromPrefix = getImageDimensions(image.data.slice(0, 256), image.mimeType);
602
+ return fromPrefix ?? getImageDimensions(image.data, image.mimeType) ?? void 0;
603
+ }
604
+ var previewCache = /* @__PURE__ */ new WeakMap();
605
+ function renderImagePreviewEntry(entry, options, theme) {
606
+ if (!getMessagesRenderConfig().showImagePreviews) return void 0;
607
+ const images = previewImagesFromEntryData(entry?.data);
608
+ if (!images) return void 0;
609
+ const fg = theme?.fg;
610
+ if (typeof fg !== "function") return void 0;
611
+ const expanded = options?.expanded === true;
612
+ try {
613
+ const themed = { fg: fg.bind(theme) };
614
+ let cached = previewCache.get(entry);
615
+ if (!cached) {
616
+ const dims = images.map(parseImageDimensions);
617
+ const components2 = images.map(
618
+ (image, index) => new Image(
619
+ image.data,
620
+ image.mimeType,
621
+ { fallbackColor: (text) => themed.fg("toolOutput", text) },
622
+ {},
623
+ dims[index]
624
+ )
625
+ );
626
+ const labels2 = images.map((_image, index) => imageLabel(themed, index + 1, dims[index]));
627
+ cached = { images, components: components2, labels: labels2 };
628
+ previewCache.set(entry, cached);
629
+ }
630
+ const { components, labels } = cached;
631
+ let memo;
632
+ return {
633
+ invalidate() {
634
+ memo = void 0;
635
+ for (const component of components) component.invalidate();
636
+ },
637
+ render(width) {
638
+ const config = getMessagesRenderConfig();
639
+ const kitty = getCapabilities().images === "kitty";
640
+ const key = `${width}|${expanded}|${config.previewMaxWidth}|${config.showImagePreviews}|${kitty}`;
641
+ if (memo && memo.key === key) return memo.lines;
642
+ const maxWidth = expanded ? IMAGE_PREVIEW_EXPANDED_MAX_WIDTH_CELLS : Math.min(config.previewMaxWidth, IMAGE_PREVIEW_MAX_WIDTH_CELLS);
643
+ let lines;
644
+ if (width <= 0) lines = [];
645
+ else if (images.length === 1 || !kitty) lines = renderStacked(width, maxWidth, labels, components);
646
+ else lines = renderGrid(width, maxWidth, labels, components);
647
+ memo = { key, lines };
648
+ return lines;
649
+ }
650
+ };
651
+ } catch {
652
+ return void 0;
653
+ }
654
+ }
655
+ function renderStacked(width, maxWidth, labels, components) {
656
+ const lines = [];
657
+ for (const [index, component] of components.entries()) {
658
+ if (index > 0) lines.push("");
659
+ lines.push(labels[index] ?? "");
660
+ lines.push(...component.render(Math.min(width, Math.max(1, maxWidth + 2))));
661
+ }
662
+ return lines;
663
+ }
664
+ function stripTrailingSpaces(line) {
665
+ if (!line.endsWith(" ")) return line;
666
+ let end = line.length;
667
+ while (end > 0 && line.charCodeAt(end - 1) === 32) end--;
668
+ return line.slice(0, end);
669
+ }
670
+ function renderGrid(width, maxWidth, labels, components) {
671
+ const usable = Math.max(1, width - 2);
672
+ const maxColumns = Math.floor((usable + GRID_GAP) / (GRID_MIN_COLUMN + GRID_GAP));
673
+ const columns = Math.max(1, Math.min(components.length, maxColumns, 3));
674
+ if (columns < 2) return renderStacked(width, maxWidth, labels, components);
675
+ const columnWidth = Math.min(maxWidth, Math.floor((usable - (columns - 1) * GRID_GAP) / columns));
676
+ if (columnWidth < GRID_MIN_COLUMN) return renderStacked(width, maxWidth, labels, components);
677
+ const lines = [];
678
+ for (let start = 0; start < components.length; start += columns) {
679
+ const group = components.slice(start, start + columns);
680
+ const groupLabels = labels.slice(start, start + columns);
681
+ if (start > 0) lines.push("");
682
+ const rendered = group.map((component) => component.render(columnWidth));
683
+ const labelRow = groupLabels.map((label, _index) => {
684
+ const pad = Math.max(0, columnWidth - visibleWidth(stripAnsi(label)));
685
+ return label + " ".repeat(pad);
686
+ }).join(" ".repeat(GRID_GAP)).trimEnd();
687
+ lines.push(labelRow);
688
+ const rows = Math.max(...rendered.map((column) => column.length));
689
+ for (let row = 0; row < rows; row++) {
690
+ let line = "";
691
+ for (let column = 0; column < rendered.length; column++) {
692
+ if (column > 0) {
693
+ const startCol = column * (columnWidth + GRID_GAP);
694
+ line += `\x1B[${startCol + 1}G`;
695
+ }
696
+ line += rendered[column]?.[row] ?? "";
697
+ }
698
+ lines.push(stripTrailingSpaces(line));
699
+ }
700
+ }
701
+ return lines;
702
+ }
703
+ function registerImagePreviewSurface(pi) {
704
+ pi.registerEntryRenderer(IMAGE_PREVIEW_ENTRY_TYPE, renderImagePreviewEntry);
705
+ }
706
+
215
707
  // extension-src/pi-style/shared/box.ts
216
708
  import { homedir as homedir2 } from "os";
217
709
  import { relative, resolve as resolve2 } from "path";
@@ -233,7 +725,7 @@ function getElapsedMs(result) {
233
725
  // extension-src/pi-style/shared/render-budget.ts
234
726
  import {
235
727
  truncateToWidth as tuiTruncateToWidth,
236
- visibleWidth as tuiVisibleWidth,
728
+ visibleWidth as tuiVisibleWidth2,
237
729
  wrapTextWithAnsi
238
730
  } from "@earendil-works/pi-tui";
239
731
  var MAX_RENDER_LINE_CHARS = 2e3;
@@ -455,7 +947,7 @@ function safeVisibleWidth(text) {
455
947
  const fastWidth = knownVisibleWidth(text);
456
948
  if (fastWidth) return fastWidth.visibleWidth;
457
949
  if (text === TRUNCATE_ELLIPSIS) return 1;
458
- return tuiVisibleWidth(text);
950
+ return tuiVisibleWidth2(text);
459
951
  }
460
952
  function safeTruncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
461
953
  const width = Math.floor(maxWidth);
@@ -509,9 +1001,9 @@ function boxedResultRenderBudget(rawLineBudget = DEFAULT_COLLAPSED_RENDER_LINES)
509
1001
  // extension-src/pi-style/shared/theme-extras.ts
510
1002
  import { existsSync, readdirSync, readFileSync } from "fs";
511
1003
  import { homedir } from "os";
512
- import { dirname, join, resolve } from "path";
1004
+ import { dirname as dirname2, join, resolve } from "path";
513
1005
  import { fileURLToPath } from "url";
514
- var extensionDir = dirname(fileURLToPath(import.meta.url));
1006
+ var extensionDir = dirname2(fileURLToPath(import.meta.url));
515
1007
  var THEME_EXTRA_DEFAULTS = Object.freeze({
516
1008
  assistantPrefix: "\u2022",
517
1009
  assistantPrefixColor: "",
@@ -743,12 +1235,48 @@ function getTextOutput(result) {
743
1235
  });
744
1236
  return textBlocks.map((contentBlock) => String(contentBlock.text ?? "")).join("\n").replace(/\r/g, "");
745
1237
  }
1238
+ var WORD_CP_UNKNOWN = 255;
1239
+ var WORD_CP_SURROGATE = 254;
1240
+ var WORD_CP_CLASS = new Uint8Array(65536).fill(WORD_CP_UNKNOWN);
1241
+ for (let code = 48; code <= 57; code++) WORD_CP_CLASS[code] = 1;
1242
+ for (let code = 65; code <= 90; code++) WORD_CP_CLASS[code] = 1;
1243
+ for (let code = 97; code <= 122; code++) WORD_CP_CLASS[code] = 1;
1244
+ WORD_CP_CLASS[39] = 1;
1245
+ WORD_CP_CLASS[45] = 1;
1246
+ WORD_CP_CLASS[95] = 1;
1247
+ WORD_CP_CLASS.fill(WORD_CP_SURROGATE, 55296, 57344);
1248
+ var NON_ASCII_WORD_RE = /[\p{L}\p{N}]/u;
1249
+ var ASTRAL_WORD_CP = /* @__PURE__ */ new Map();
1250
+ function astralWordMembership(codePoint) {
1251
+ const cached = ASTRAL_WORD_CP.get(codePoint);
1252
+ if (cached !== void 0) return cached;
1253
+ const membership = NON_ASCII_WORD_RE.test(String.fromCodePoint(codePoint)) ? 1 : 0;
1254
+ ASTRAL_WORD_CP.set(codePoint, membership);
1255
+ return membership;
1256
+ }
746
1257
  function countWords(text) {
1258
+ const len = text.length;
747
1259
  let count = 0;
748
- let inWord = false;
749
- for (const char of text) {
750
- const isWord = /[\p{L}\p{N}_'-]/u.test(char);
751
- if (isWord && !inWord) count++;
1260
+ let inWord = 0;
1261
+ for (let i = 0; i < len; i++) {
1262
+ const code = text.charCodeAt(i);
1263
+ const membership = WORD_CP_CLASS[code] ?? 0;
1264
+ let isWord;
1265
+ if (membership <= 1) {
1266
+ isWord = membership;
1267
+ } else if (membership === WORD_CP_SURROGATE) {
1268
+ const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
1269
+ if (code <= 56319 && next >= 56320 && next <= 57343) {
1270
+ isWord = astralWordMembership(65536 + (code - 55296 << 10) + (next - 56320));
1271
+ i++;
1272
+ } else {
1273
+ isWord = 0;
1274
+ }
1275
+ } else {
1276
+ isWord = NON_ASCII_WORD_RE.test(String.fromCharCode(code)) ? 1 : 0;
1277
+ WORD_CP_CLASS[code] = isWord;
1278
+ }
1279
+ count += isWord & (inWord ^ 1);
752
1280
  inWord = isWord;
753
1281
  }
754
1282
  return count;
@@ -1615,7 +2143,7 @@ function registerBatchResult(meta, data, context) {
1615
2143
  if (member) {
1616
2144
  const nextStatus = data.isPartial ? "running" : "done";
1617
2145
  const nextIsError = !data.isPartial && data.isError;
1618
- const changed = member.status !== nextStatus || member.isError !== nextIsError || member.errorText !== (nextIsError ? data.errorText : void 0) || member.outputEntries !== data.entries;
2146
+ const changed = member.status !== nextStatus || member.isError !== nextIsError || member.errorText !== (nextIsError ? data.errorText : void 0) || data.entries !== void 0 && member.outputEntries !== data.entries;
1619
2147
  member.status = nextStatus;
1620
2148
  member.isError = nextIsError;
1621
2149
  if (member.isError && data.errorText !== void 0) member.errorText = data.errorText;
@@ -1629,6 +2157,12 @@ function registerBatchResult(meta, data, context) {
1629
2157
  }
1630
2158
  return { batch, isLeader: batch.leaderId === context.toolCallId };
1631
2159
  }
2160
+ function hasFinalBatchOutput(toolCallId) {
2161
+ const batch = batchByCallId.get(toolCallId);
2162
+ if (!batch) return false;
2163
+ const member = batch.members.find((entry) => entry.toolCallId === toolCallId);
2164
+ return member !== void 0 && member.outputEntries !== void 0 && member.status === "done" && !member.isError;
2165
+ }
1632
2166
  function batchStatus(batch) {
1633
2167
  let done = 0;
1634
2168
  let failed = 0;
@@ -1988,6 +2522,9 @@ function invalidateTurnMembers(turn) {
1988
2522
  }
1989
2523
  }
1990
2524
  }
2525
+ function releaseTurnInvalidators(turn) {
2526
+ for (const member of turn.members) invalidateByCallId.delete(member.toolCallId);
2527
+ }
1991
2528
  function noteTurnMemberElapsed(toolCallId, elapsedMs) {
1992
2529
  if (elapsedMs === void 0) return;
1993
2530
  const entry = memberByCallId.get(toolCallId);
@@ -2879,7 +3416,9 @@ import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
2879
3416
  import { highlightCode } from "@earendil-works/pi-coding-agent";
2880
3417
  var ESC2 = "\x1B";
2881
3418
  var BG_ANSI_PATTERN = new RegExp(`${ESC2}\\[(?:4\\d|10\\d|48;5;\\d{1,3}|48;2;\\d{1,3};\\d{1,3};\\d{1,3}|49)m`, "g");
3419
+ var ANSI_SGR_PATTERN = new RegExp(`${ESC2}\\[([0-9;]*)m`, "g");
2882
3420
  var CONTROL_CHARS = "\0-\b\v\f-\x7F";
3421
+ var CONTROL_CHARS_PATTERN = new RegExp(`[${CONTROL_CHARS}]`, "g");
2883
3422
  var ADD_ROW_BACKGROUND_MIX_RATIO = 0.24;
2884
3423
  var REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
2885
3424
  var ADD_INLINE_EMPHASIS_MIX_RATIO = 0.44;
@@ -2971,7 +3510,7 @@ function resolveDiffPalette(theme) {
2971
3510
  }
2972
3511
  function keepBackgroundAcrossResets(text, rowBgAnsi) {
2973
3512
  if (!text) return text;
2974
- return text.replace(new RegExp(`${ESC2}\\[([0-9;]*)m`, "g"), (sequence, rawCodes) => {
3513
+ return text.replace(ANSI_SGR_PATTERN, (sequence, rawCodes) => {
2975
3514
  const split = String(rawCodes ?? "").split(";").filter(Boolean);
2976
3515
  const codes = split.length > 0 ? split : ["0"];
2977
3516
  const hasGlobalReset = codes.includes("0");
@@ -3013,7 +3552,7 @@ function applyBackgroundToVisibleRange(ansiText, start, end, backgroundAnsi, res
3013
3552
  return output;
3014
3553
  }
3015
3554
  function sanitizeSingleLineText(value) {
3016
- return value.replace(/\r/g, "").replace(/\n/g, "").replace(new RegExp(`[${CONTROL_CHARS}]`, "g"), "");
3555
+ return value.replace(/\r/g, "").replace(/\n/g, "").replace(CONTROL_CHARS_PATTERN, "");
3017
3556
  }
3018
3557
  function stripInlineBreaksPreserveAnsi(value) {
3019
3558
  return value.replace(/\r/g, "").replace(/\n/g, "");
@@ -3610,6 +4149,101 @@ var AdaptiveDiffComponent = class {
3610
4149
  }
3611
4150
  };
3612
4151
 
4152
+ // extension-src/pi-style/features/tools/boxed/shared.ts
4153
+ function pendingFlag(context) {
4154
+ return Boolean(context.isPartial);
4155
+ }
4156
+ function displayPath(rawPath, context) {
4157
+ const path = String(rawPath ?? "");
4158
+ if (!path) return "(unknown)";
4159
+ return shortenPath(resolveRelativePath(path, context.cwd));
4160
+ }
4161
+ function pathRangeDetail(rawPath, offset, limit, context) {
4162
+ const path = displayPath(rawPath, context);
4163
+ let range = "";
4164
+ if (offset !== void 0 || limit !== void 0) {
4165
+ const start = offset ?? 1;
4166
+ const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
4167
+ range = `:${start}${end ? `-${end}` : ""}`;
4168
+ }
4169
+ return path ? `${path}${range}` : "(unknown)";
4170
+ }
4171
+ function compactCall(theme, toolName, detailLine, options) {
4172
+ return renderCompactBoxedToolCall(theme, toolName, detailLine, {
4173
+ widthKey: boxedToolWidthKey(toolName, options.detailKey),
4174
+ state: options.context.state,
4175
+ isError: Boolean(options.context.isError),
4176
+ isPartial: Boolean(options.context.isPartial),
4177
+ isPending: pendingFlag(options.context),
4178
+ running: Boolean(options.context.executionStarted)
4179
+ });
4180
+ }
4181
+ function noteExecutionStart(context) {
4182
+ recordExecutionStarted(context.state, context.executionStarted);
4183
+ }
4184
+ function noteBoxedCallState(context) {
4185
+ if (!context.executionStarted) return;
4186
+ if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
4187
+ else {
4188
+ recordExecutionEnded(context.state);
4189
+ stopElapsedTicker(context.state);
4190
+ }
4191
+ }
4192
+ function noteBoxedResultPhase(context, isPartial) {
4193
+ const firstResultPass = !isResultSeen(context.state);
4194
+ markResultSeen(context.state);
4195
+ if (isPartial) startElapsedTicker(context.state, context.invalidate);
4196
+ else {
4197
+ recordExecutionEnded(context.state);
4198
+ stopElapsedTicker(context.state);
4199
+ }
4200
+ return firstResultPass;
4201
+ }
4202
+ function stateElapsedMs(context) {
4203
+ return getStateElapsedMs(context.state);
4204
+ }
4205
+ function compactFooterWithState(theme, result, context, options = {}) {
4206
+ const elapsedMs = stateElapsedMs(context);
4207
+ return renderCompactBoxedFooter(theme, result, {
4208
+ state: context.state,
4209
+ isError: Boolean(options.isError ?? context.isError),
4210
+ isPartial: Boolean(options.isPartial ?? context.isPartial),
4211
+ ...elapsedMs === void 0 ? {} : { elapsedMs }
4212
+ });
4213
+ }
4214
+ function resultFooterLines(theme, result, context, extraParts = []) {
4215
+ return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
4216
+ }
4217
+ var CACHE_KEY_LONG_PART_THRESHOLD = 64;
4218
+ function fnv1aHex(text) {
4219
+ let hash = 2166136261;
4220
+ for (let i = 0; i < text.length; i++) {
4221
+ hash ^= text.charCodeAt(i);
4222
+ hash = Math.imul(hash, 16777619) >>> 0;
4223
+ }
4224
+ return hash.toString(16).padStart(8, "0");
4225
+ }
4226
+ function boundedCacheKeyPart(part) {
4227
+ if (typeof part !== "string" || part.length <= CACHE_KEY_LONG_PART_THRESHOLD) return part;
4228
+ return `${part.length}:${fnv1aHex(part)}`;
4229
+ }
4230
+ function getRenderCacheKey(prefix, theme, ...parts) {
4231
+ const pieces = [prefix, themeCacheKey(theme), getToolsRenderCacheSignature()];
4232
+ for (const part of parts) pieces.push(boundedCacheKeyPart(part));
4233
+ return pieces.join("|");
4234
+ }
4235
+ function memoizedStateComponent(state, slot, key, build) {
4236
+ if (!state || typeof state !== "object") return build();
4237
+ const cached = state[slot];
4238
+ if (cached && cached.key === key) return cached.component;
4239
+ const component = build();
4240
+ state[slot] = { key, component };
4241
+ return component;
4242
+ }
4243
+ function clearFooterState(context) {
4244
+ clearCompactBoxedFooter(context.state);
4245
+ }
4246
+
3613
4247
  // extension-src/pi-style/features/tools/boxed/git.ts
3614
4248
  var GIT_SHORT_STATUS_FLAGS = /* @__PURE__ */ new Set(["-s", "--short", "--porcelain"]);
3615
4249
  var GIT_DIFF_FORMAT_REJECT = /* @__PURE__ */ new Set([
@@ -4068,16 +4702,16 @@ function parseGitShowStat(text) {
4068
4702
  }
4069
4703
  break;
4070
4704
  }
4071
- const stat = parseGitDiffStat(lines.slice(i).join("\n"));
4072
- if (!stat) return null;
4705
+ const stat3 = parseGitDiffStat(lines.slice(i).join("\n"));
4706
+ if (!stat3) return null;
4073
4707
  return {
4074
4708
  kind: "show-stat",
4075
4709
  hash,
4076
4710
  subject,
4077
- files: stat.files,
4078
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4079
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4080
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
4711
+ files: stat3.files,
4712
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
4713
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
4714
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4081
4715
  };
4082
4716
  }
4083
4717
  var DIFF_GIT_HEADER = /^diff --git a\/(.*) b\/(.*)$/;
@@ -4404,17 +5038,17 @@ function parseGitPull(text) {
4404
5038
  return null;
4405
5039
  }
4406
5040
  if (!range || ffIndex < 0) return null;
4407
- const stat = parseGitDiffStat(lines.slice(ffIndex + 1).join("\n"));
4408
- if (!stat) return null;
5041
+ const stat3 = parseGitDiffStat(lines.slice(ffIndex + 1).join("\n"));
5042
+ if (!stat3) return null;
4409
5043
  return {
4410
5044
  kind: "action",
4411
5045
  command: "pull",
4412
- files: stat.files,
5046
+ files: stat3.files,
4413
5047
  status: "Fast-forward",
4414
5048
  range,
4415
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4416
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4417
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5049
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5050
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5051
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4418
5052
  };
4419
5053
  }
4420
5054
  function parseGitFetch(text) {
@@ -4556,30 +5190,30 @@ function parseGitMerge(text) {
4556
5190
  }
4557
5191
  const marker2 = body[idx] ?? "";
4558
5192
  if (range && marker2 === "Fast-forward") {
4559
- const stat = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
4560
- if (!stat) return null;
5193
+ const stat3 = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
5194
+ if (!stat3) return null;
4561
5195
  return {
4562
5196
  kind: "action",
4563
5197
  command: "merge",
4564
- files: stat.files,
5198
+ files: stat3.files,
4565
5199
  status: "Fast-forward",
4566
5200
  range,
4567
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4568
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4569
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5201
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5202
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5203
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4570
5204
  };
4571
5205
  }
4572
5206
  if (MERGE_MADE.exec(marker2)) {
4573
- const stat = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
4574
- if (!stat) return null;
5207
+ const stat3 = parseGitDiffStatTolerant(body.slice(idx + 1).join("\n"));
5208
+ if (!stat3) return null;
4575
5209
  return {
4576
5210
  kind: "action",
4577
5211
  command: "merge",
4578
- files: stat.files,
5212
+ files: stat3.files,
4579
5213
  status: marker2,
4580
- ...stat.filesChanged !== void 0 ? { filesChanged: stat.filesChanged } : {},
4581
- ...stat.insertions !== void 0 ? { insertions: stat.insertions } : {},
4582
- ...stat.deletions !== void 0 ? { deletions: stat.deletions } : {}
5214
+ ...stat3.filesChanged !== void 0 ? { filesChanged: stat3.filesChanged } : {},
5215
+ ...stat3.insertions !== void 0 ? { insertions: stat3.insertions } : {},
5216
+ ...stat3.deletions !== void 0 ? { deletions: stat3.deletions } : {}
4583
5217
  };
4584
5218
  }
4585
5219
  return null;
@@ -4916,6 +5550,34 @@ function binaryBodyLine(theme, status) {
4916
5550
  }
4917
5551
  function renderGitDiffResult(theme, parsed, options, context) {
4918
5552
  const expanded = Boolean(options.expanded);
5553
+ let totalAdditions = 0;
5554
+ let totalRemovals = 0;
5555
+ const sigParts = [];
5556
+ for (const file of parsed.files) {
5557
+ totalAdditions += file.additions;
5558
+ totalRemovals += file.removals;
5559
+ sigParts.push(
5560
+ `${file.path}:${file.body.length}:${file.additions}:${file.removals}:${file.binary ? 1 : 0}:${file.status ?? ""}`
5561
+ );
5562
+ }
5563
+ const sig = sigParts.join(";").slice(0, 2048);
5564
+ return memoizedStateComponent(
5565
+ context.state,
5566
+ "__piStyleGitDiffResult",
5567
+ getRenderCacheKey(
5568
+ "git-diff-result",
5569
+ theme,
5570
+ String(parsed.show),
5571
+ String(expanded),
5572
+ parsed.files.length,
5573
+ totalAdditions,
5574
+ totalRemovals,
5575
+ sig
5576
+ ),
5577
+ () => buildGitDiffResultComponent(theme, parsed, expanded, context)
5578
+ );
5579
+ }
5580
+ function buildGitDiffResultComponent(theme, parsed, expanded, context) {
4919
5581
  const elapsedMs = getStateElapsedMs(context.state);
4920
5582
  const fileCount = parsed.files.length;
4921
5583
  const footerParts = [];
@@ -4963,108 +5625,28 @@ function renderGitDiffResult(theme, parsed, options, context) {
4963
5625
  });
4964
5626
  }
4965
5627
  }
4966
- let cacheWidth;
4967
- let cacheLines;
4968
- return {
4969
- invalidate() {
4970
- cacheWidth = void 0;
4971
- cacheLines = void 0;
4972
- for (const box of fileBoxes) box.resultComponent.invalidate();
4973
- },
4974
- render(width) {
4975
- if (cacheWidth === width && cacheLines) return cacheLines;
4976
- const renderedWidth = boxWidth(width);
4977
- const lines = [];
4978
- for (const box of fileBoxes) {
4979
- lines.push(boxLabeledBorder(theme, "\u256D", "\u256E", box.topLabel, void 0, renderedWidth));
4980
- lines.push(boxBlankLine(theme, renderedWidth));
4981
- lines.push(...box.resultComponent.render(width));
4982
- }
4983
- cacheWidth = width;
4984
- cacheLines = lines;
4985
- return lines;
4986
- }
4987
- };
4988
- }
4989
-
4990
- // extension-src/pi-style/features/tools/boxed/shared.ts
4991
- function pendingFlag(context) {
4992
- return Boolean(context.isPartial);
4993
- }
4994
- function displayPath(rawPath, context) {
4995
- const path = String(rawPath ?? "");
4996
- if (!path) return "(unknown)";
4997
- return shortenPath(resolveRelativePath(path, context.cwd));
4998
- }
4999
- function pathRangeDetail(rawPath, offset, limit, context) {
5000
- const path = displayPath(rawPath, context);
5001
- let range = "";
5002
- if (offset !== void 0 || limit !== void 0) {
5003
- const start = offset ?? 1;
5004
- const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
5005
- range = `:${start}${end ? `-${end}` : ""}`;
5006
- }
5007
- return path ? `${path}${range}` : "(unknown)";
5008
- }
5009
- function compactCall(theme, toolName, detailLine, options) {
5010
- return renderCompactBoxedToolCall(theme, toolName, detailLine, {
5011
- widthKey: boxedToolWidthKey(toolName, options.detailKey),
5012
- state: options.context.state,
5013
- isError: Boolean(options.context.isError),
5014
- isPartial: Boolean(options.context.isPartial),
5015
- isPending: pendingFlag(options.context),
5016
- running: Boolean(options.context.executionStarted)
5017
- });
5018
- }
5019
- function noteExecutionStart(context) {
5020
- recordExecutionStarted(context.state, context.executionStarted);
5021
- }
5022
- function noteBoxedCallState(context) {
5023
- if (!context.executionStarted) return;
5024
- if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
5025
- else {
5026
- recordExecutionEnded(context.state);
5027
- stopElapsedTicker(context.state);
5028
- }
5029
- }
5030
- function noteBoxedResultPhase(context, isPartial) {
5031
- const firstResultPass = !isResultSeen(context.state);
5032
- markResultSeen(context.state);
5033
- if (isPartial) startElapsedTicker(context.state, context.invalidate);
5034
- else {
5035
- recordExecutionEnded(context.state);
5036
- stopElapsedTicker(context.state);
5037
- }
5038
- return firstResultPass;
5039
- }
5040
- function stateElapsedMs(context) {
5041
- return getStateElapsedMs(context.state);
5042
- }
5043
- function compactFooterWithState(theme, result, context, options = {}) {
5044
- const elapsedMs = stateElapsedMs(context);
5045
- return renderCompactBoxedFooter(theme, result, {
5046
- state: context.state,
5047
- isError: Boolean(options.isError ?? context.isError),
5048
- isPartial: Boolean(options.isPartial ?? context.isPartial),
5049
- ...elapsedMs === void 0 ? {} : { elapsedMs }
5050
- });
5051
- }
5052
- function resultFooterLines(theme, result, context, extraParts = []) {
5053
- return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
5054
- }
5055
- function getRenderCacheKey(prefix, theme, ...parts) {
5056
- return [prefix, themeCacheKey(theme), getToolsRenderCacheSignature(), ...parts].join("|");
5057
- }
5058
- function memoizedStateComponent(state, slot, key, build) {
5059
- if (!state || typeof state !== "object") return build();
5060
- const cached = state[slot];
5061
- if (cached && cached.key === key) return cached.component;
5062
- const component = build();
5063
- state[slot] = { key, component };
5064
- return component;
5065
- }
5066
- function clearFooterState(context) {
5067
- clearCompactBoxedFooter(context.state);
5628
+ let cacheWidth;
5629
+ let cacheLines;
5630
+ return {
5631
+ invalidate() {
5632
+ cacheWidth = void 0;
5633
+ cacheLines = void 0;
5634
+ for (const box of fileBoxes) box.resultComponent.invalidate();
5635
+ },
5636
+ render(width) {
5637
+ if (cacheWidth === width && cacheLines) return cacheLines;
5638
+ const renderedWidth = boxWidth(width);
5639
+ const lines = [];
5640
+ for (const box of fileBoxes) {
5641
+ lines.push(boxLabeledBorder(theme, "\u256D", "\u256E", box.topLabel, void 0, renderedWidth));
5642
+ lines.push(boxBlankLine(theme, renderedWidth));
5643
+ lines.push(...box.resultComponent.render(width));
5644
+ }
5645
+ cacheWidth = width;
5646
+ cacheLines = lines;
5647
+ return lines;
5648
+ }
5649
+ };
5068
5650
  }
5069
5651
 
5070
5652
  // extension-src/pi-style/features/tools/boxed/bash.ts
@@ -5167,6 +5749,25 @@ function countNewlines(text, from, to) {
5167
5749
  }
5168
5750
  return count;
5169
5751
  }
5752
+ function findBackwardLineStart(text, need, end = text.length) {
5753
+ let found = 0;
5754
+ for (let i = end - 1; i >= 0; i--) {
5755
+ if (text.charCodeAt(i) === 10 && ++found >= need) return i + 1;
5756
+ }
5757
+ return 0;
5758
+ }
5759
+ function isOutputWhitespaceCode(code) {
5760
+ if (code === 32 || code >= 9 && code <= 13) return true;
5761
+ if (code === 133 || code === 160 || code === 5760) return true;
5762
+ if (code >= 8192 && code <= 8202) return true;
5763
+ return code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
5764
+ }
5765
+ function lastVisibleEnd(text) {
5766
+ for (let i = text.length - 1; i >= 0; i--) {
5767
+ if (!isOutputWhitespaceCode(text.charCodeAt(i))) return i + 1;
5768
+ }
5769
+ return 0;
5770
+ }
5170
5771
  function stripBashToolNoticeLines(text) {
5171
5772
  const filteredLines = text.replace(/\r/g, "").split("\n").filter((line) => !BASH_TOOL_NOTICE_PATTERN.test(line.trim()));
5172
5773
  return filteredLines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -5297,8 +5898,10 @@ function bashBodyComponent(preview, emptyLines) {
5297
5898
  };
5298
5899
  }
5299
5900
  function renderBashStreamingResult(theme, raw, options, context) {
5300
- const body = stripBashToolNoticeLines(stripAnsi(raw));
5301
- const hasOutput = body.trim().length > 0;
5901
+ const contentEnd = lastVisibleEnd(raw);
5902
+ const hasOutput = contentEnd > 0;
5903
+ const tailStart = hasOutput ? findBackwardLineStart(raw, getToolsRenderConfig().maxCollapsedLines + 10, contentEnd) : 0;
5904
+ const body = stripBashToolNoticeLines(stripAnsi(raw.slice(tailStart, contentEnd)));
5302
5905
  const elapsed = getStateElapsedMs(context.state);
5303
5906
  const emptyLines = [theme.fg("dim", "No output received yet")];
5304
5907
  if (!hasOutput && isInteractiveCommand(context?.args?.command) && (elapsed ?? 0) >= 1e3) {
@@ -5329,17 +5932,7 @@ function renderBashFinalResult(theme, raw, options, context) {
5329
5932
  const referenceLines = rawCommand.split("\n").map((line, index) => `${index === 0 ? "$ " : "> "}${line}`);
5330
5933
  if (!options.expanded) {
5331
5934
  const scanLines = getToolsRenderConfig().maxCollapsedLines + 10;
5332
- let nlCount = 0;
5333
- let tailStart = 0;
5334
- for (let i = statusStripped.length - 1; i >= 0; i--) {
5335
- if (statusStripped.charCodeAt(i) === 10) {
5336
- nlCount++;
5337
- if (nlCount >= scanLines) {
5338
- tailStart = i + 1;
5339
- break;
5340
- }
5341
- }
5342
- }
5935
+ const tailStart = findBackwardLineStart(statusStripped, scanLines);
5343
5936
  const tail = stripBashToolNoticeLines(stripAnsi(statusStripped.slice(tailStart)));
5344
5937
  const totalLinesBefore = tailStart > 0 ? countNewlines(statusStripped, 0, tailStart) : 0;
5345
5938
  const preview2 = createBashResultPreview(theme, tail, options, outputColor);
@@ -5397,17 +5990,7 @@ function createBashResultPreview(theme, text, options, color) {
5397
5990
  if (cacheLines && cacheKey2 === cacheId) return cacheLines;
5398
5991
  if (!expanded) {
5399
5992
  const needed = cfg.maxCollapsedLines;
5400
- let totalNewlines = 0;
5401
- let scanFrom = 0;
5402
- for (let i = text.length - 1; i >= 0; i--) {
5403
- if (text.charCodeAt(i) === 10) {
5404
- totalNewlines++;
5405
- if (totalNewlines === needed) {
5406
- scanFrom = i + 1;
5407
- break;
5408
- }
5409
- }
5410
- }
5993
+ const scanFrom = findBackwardLineStart(text, needed);
5411
5994
  if (text.length === 0) {
5412
5995
  cacheKey2 = cacheId;
5413
5996
  cacheLines = [];
@@ -5430,26 +6013,26 @@ function createBashResultPreview(theme, text, options, color) {
5430
6013
  return cacheLines;
5431
6014
  }
5432
6015
  const normalized = replaceTabs(text);
5433
- const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
5434
- const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
6016
+ const rawLines = normalized.split("\n");
6017
+ const totalLines = rawLines.length;
6018
+ const hasOutput = !(totalLines === 1 && rawLines[0] === "");
5435
6019
  if (!hasOutput) {
5436
6020
  cacheKey2 = cacheId;
5437
6021
  cacheLines = [];
5438
6022
  return cacheLines;
5439
6023
  }
5440
- const truncatedLines = logicalLines.map((line) => safeTruncateToWidth(line, bodyWidth, "\u2026"));
5441
- const expandedLines = truncatedLines.length === 1 && truncatedLines[0] === "" ? [] : truncatedLines;
5442
6024
  const applyColor = (l) => color === "error" ? formatToolOutputLine(theme, l, "error") : cfg.dimOutput ? formatToolOutputLine(theme, l) : formatToolOutputLine(theme, l, "text");
5443
- if (cfg.maxExpandedLines > 0 && expandedLines.length > cfg.maxExpandedLines) {
5444
- const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
5445
- const remaining = expandedLines.length - cfg.maxExpandedLines;
6025
+ const renderRawLine = (line) => safeTruncateToWidth(clampLineLength(line), bodyWidth, "\u2026");
6026
+ if (cfg.maxExpandedLines > 0 && totalLines > cfg.maxExpandedLines) {
6027
+ const truncated = rawLines.slice(-cfg.maxExpandedLines).map((line) => applyColor(renderRawLine(line)));
6028
+ const remaining = totalLines - cfg.maxExpandedLines;
5446
6029
  truncated.unshift(theme.fg("dim", `\u2026 ${remaining} earlier lines`));
5447
6030
  cacheKey2 = cacheId;
5448
6031
  cacheLines = truncated;
5449
6032
  return cacheLines;
5450
6033
  }
5451
6034
  cacheKey2 = cacheId;
5452
- cacheLines = expandedLines.map(applyColor);
6035
+ cacheLines = rawLines.map((line) => applyColor(renderRawLine(line)));
5453
6036
  return cacheLines;
5454
6037
  }
5455
6038
  };
@@ -5616,6 +6199,7 @@ function isGitDiffClass(cls) {
5616
6199
  function isGitActionClass(cls) {
5617
6200
  return !isBashTreeClass(cls) && cls.kind === "action";
5618
6201
  }
6202
+ var PARTIAL_REPARSE_THRESHOLD = 4096;
5619
6203
  function parseSemanticOutput(cls, output) {
5620
6204
  if (isBashTreeClass(cls)) return parseBashTreeOutput(cls, output);
5621
6205
  if (isGhClass(cls)) return parseGhOutput(cls, output);
@@ -5708,6 +6292,7 @@ var bashTool = {
5708
6292
  existing.finished = false;
5709
6293
  existing.revision++;
5710
6294
  delete existing.renderCache;
6295
+ delete existing.lastParsedLength;
5711
6296
  }
5712
6297
  existing.command = command;
5713
6298
  existing.cls = cls;
@@ -5736,21 +6321,24 @@ var bashTool = {
5736
6321
  }
5737
6322
  if (!options.isPartial || isBashTreeClass(cls)) {
5738
6323
  const output = stripBashToolNoticeLines(stripAnsi(getTextOutput(result)));
5739
- const parsed = parseSemanticOutput(cls, output);
5740
6324
  const state = semanticStates.get(context.toolCallId);
6325
+ const shouldParse = !options.isPartial || state === void 0 || state.lastParsedLength === void 0 || output.length - state.lastParsedLength >= PARTIAL_REPARSE_THRESHOLD;
6326
+ const parsed = shouldParse ? parseSemanticOutput(cls, output) : void 0;
5741
6327
  if (parsed) {
5742
6328
  if (state) {
5743
6329
  state.parsed = parsed;
5744
6330
  state.finished = !options.isPartial;
5745
6331
  state.revision++;
5746
6332
  delete state.renderCache;
6333
+ if (options.isPartial) state.lastParsedLength = output.length;
5747
6334
  } else
5748
6335
  semanticStates.set(context.toolCallId, {
5749
6336
  cls,
5750
6337
  command: String(context?.args?.command ?? ""),
5751
6338
  parsed,
5752
6339
  finished: !options.isPartial,
5753
- revision: 0
6340
+ revision: 0,
6341
+ ...options.isPartial ? { lastParsedLength: output.length } : {}
5754
6342
  });
5755
6343
  if (isGitDiffClass(cls)) {
5756
6344
  return renderGitDiffResult(theme, parsed, options, context);
@@ -5760,11 +6348,17 @@ var bashTool = {
5760
6348
  }
5761
6349
  return EMPTY_BASH_TREE_RESULT;
5762
6350
  }
5763
- if (state) {
5764
- state.fallback = true;
5765
- state.finished = !options.isPartial;
5766
- state.revision++;
5767
- delete state.renderCache;
6351
+ if (!shouldParse) {
6352
+ if (state?.parsed !== void 0) return EMPTY_BASH_TREE_RESULT;
6353
+ }
6354
+ if (shouldParse) {
6355
+ if (state) {
6356
+ state.fallback = true;
6357
+ state.finished = !options.isPartial;
6358
+ state.revision++;
6359
+ delete state.renderCache;
6360
+ if (options.isPartial) state.lastParsedLength = output.length;
6361
+ }
5768
6362
  }
5769
6363
  }
5770
6364
  } else if (options.isPartial) {
@@ -5863,14 +6457,8 @@ var editTool = {
5863
6457
  const message = firstText(result.content);
5864
6458
  const argPath = String(context?.args?.path ?? context?.args?.file_path ?? "");
5865
6459
  const sourcePath = details?.path ?? (argPath || extractEditedPath(message));
5866
- const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
5867
- const rows = buildSplitRows(diff);
5868
6460
  const expanded = options.expanded;
5869
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
5870
6461
  const stats = countDiffStats(diff);
5871
- const maxRows = expanded ? 160 : 36;
5872
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5873
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5874
6462
  return memoizedStateComponent(
5875
6463
  context.state,
5876
6464
  "__piStyleEditDiffResult",
@@ -5882,22 +6470,30 @@ var editTool = {
5882
6470
  sourcePath ?? "",
5883
6471
  editDiffFooter(theme, result, context, stats)
5884
6472
  ),
5885
- () => renderBoxedToolResult(
5886
- theme,
5887
- {
5888
- render(width) {
5889
- return diffView.render(width);
6473
+ () => {
6474
+ const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
6475
+ const rows = buildSplitRows(diff);
6476
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
6477
+ const maxRows = expanded ? 160 : 36;
6478
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6479
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6480
+ return renderBoxedToolResult(
6481
+ theme,
6482
+ {
6483
+ render(width) {
6484
+ return diffView.render(width);
6485
+ },
6486
+ invalidate() {
6487
+ diffView.invalidate();
6488
+ }
5890
6489
  },
5891
- invalidate() {
5892
- diffView.invalidate();
6490
+ {
6491
+ dividerLabel: diffDividerLabel2(theme, stats),
6492
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6493
+ footerLines: [editDiffFooter(theme, result, context, stats)]
5893
6494
  }
5894
- },
5895
- {
5896
- dividerLabel: diffDividerLabel2(theme, stats),
5897
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5898
- footerLines: [editDiffFooter(theme, result, context, stats)]
5899
- }
5900
- )
6495
+ );
6496
+ }
5901
6497
  );
5902
6498
  }
5903
6499
  };
@@ -6002,14 +6598,16 @@ var findTool = {
6002
6598
  return renderBatchAwareCall(theme, batch);
6003
6599
  },
6004
6600
  result(result, options, _theme, context) {
6005
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6006
- const entries = context.isError ? void 0 : parseFindOutput(output);
6601
+ const isError = Boolean(context.isError);
6602
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6603
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6604
+ const entries = settled || isError ? void 0 : parseFindOutput(output);
6007
6605
  registerBatchResult(
6008
6606
  FIND_META,
6009
6607
  {
6010
6608
  isPartial: Boolean(options.isPartial),
6011
- isError: Boolean(context.isError),
6012
- errorText: context.isError ? output || void 0 : void 0,
6609
+ isError,
6610
+ errorText: isError ? output || void 0 : void 0,
6013
6611
  ...entries !== void 0 ? { entries } : {}
6014
6612
  },
6015
6613
  context
@@ -6120,14 +6718,19 @@ var grepTool = {
6120
6718
  return renderGrepPanel(theme, context.toolCallId);
6121
6719
  },
6122
6720
  result(result, options, _theme, context) {
6123
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6124
6721
  const isError = Boolean(context.isError);
6722
+ const isPartial = Boolean(options.isPartial);
6723
+ const state = grepPanels.get(context.toolCallId);
6724
+ if (!isPartial && !isError && state !== void 0 && state.matches !== void 0 && !state.isPartial) {
6725
+ return EMPTY_GREP_RESULT;
6726
+ }
6727
+ const output = stripAnsi(getTextOutput(result)).trimEnd();
6125
6728
  const matches = isError ? [] : parseGrepOutput(output);
6126
6729
  registerGrepResult(context.toolCallId, {
6127
6730
  matches,
6128
6731
  isError,
6129
6732
  errorText: isError ? output || void 0 : void 0,
6130
- isPartial: Boolean(options.isPartial)
6733
+ isPartial
6131
6734
  });
6132
6735
  return EMPTY_GREP_RESULT;
6133
6736
  }
@@ -6154,14 +6757,16 @@ var lsTool = {
6154
6757
  return renderBatchAwareCall(theme, batch);
6155
6758
  },
6156
6759
  result(result, options, _theme, context) {
6157
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6158
- const entries = context.isError ? void 0 : parseLsOutput(output);
6760
+ const isError = Boolean(context.isError);
6761
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6762
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6763
+ const entries = settled || isError ? void 0 : parseLsOutput(output);
6159
6764
  registerBatchResult(
6160
6765
  LIST_META,
6161
6766
  {
6162
6767
  isPartial: Boolean(options.isPartial),
6163
- isError: Boolean(context.isError),
6164
- errorText: context.isError ? output || void 0 : void 0,
6768
+ isError,
6769
+ errorText: isError ? output || void 0 : void 0,
6165
6770
  ...entries !== void 0 ? { entries } : {}
6166
6771
  },
6167
6772
  context
@@ -6285,15 +6890,9 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6285
6890
  footerLines: [quickEditFooter(theme, context)]
6286
6891
  });
6287
6892
  }
6288
- const rows = buildSplitRows(diff);
6289
6893
  const expanded = options.expanded;
6290
6894
  const argPath = String(context?.args?.path ?? "");
6291
- const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6292
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6293
6895
  const stats = countDiffStats(diff);
6294
- const maxRows = expanded ? 160 : 36;
6295
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6296
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6297
6896
  return memoizedStateComponent(
6298
6897
  context.state,
6299
6898
  "__piStyleQuickEditDiffResult",
@@ -6306,22 +6905,30 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6306
6905
  argPath,
6307
6906
  quickEditDiffFooter(theme, result, context, stats)
6308
6907
  ),
6309
- () => renderBoxedToolResult(
6310
- theme,
6311
- {
6312
- render(width) {
6313
- return diffView.render(width);
6908
+ () => {
6909
+ const rows = buildSplitRows(diff);
6910
+ const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6911
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6912
+ const maxRows = expanded ? 160 : 36;
6913
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6914
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6915
+ return renderBoxedToolResult(
6916
+ theme,
6917
+ {
6918
+ render(width) {
6919
+ return diffView.render(width);
6920
+ },
6921
+ invalidate() {
6922
+ diffView.invalidate();
6923
+ }
6314
6924
  },
6315
- invalidate() {
6316
- diffView.invalidate();
6925
+ {
6926
+ dividerLabel: quickEditDividerLabel(theme, stats),
6927
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6928
+ footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6317
6929
  }
6318
- },
6319
- {
6320
- dividerLabel: quickEditDividerLabel(theme, stats),
6321
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6322
- footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6323
- }
6324
- )
6930
+ );
6931
+ }
6325
6932
  );
6326
6933
  }
6327
6934
  function quickEditFooter(theme, context) {
@@ -6367,13 +6974,13 @@ var readTool = {
6367
6974
  return renderBatchAwareCall(theme, batch);
6368
6975
  },
6369
6976
  result(result, options, _theme, context) {
6370
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6977
+ const errorText = context.isError ? stripAnsi(getTextOutput(result)).trimEnd() || void 0 : void 0;
6371
6978
  registerBatchResult(
6372
6979
  READ_META,
6373
6980
  {
6374
6981
  isPartial: Boolean(options.isPartial),
6375
6982
  isError: Boolean(context.isError),
6376
- errorText: context.isError ? output || void 0 : void 0
6983
+ errorText
6377
6984
  },
6378
6985
  context
6379
6986
  );
@@ -6978,7 +7585,7 @@ var DEFAULT_CONFIG = Object.freeze({
6978
7585
  startup: Object.freeze({ mode: "compact", showResources: false, alwaysExpanded: false }),
6979
7586
  statusLine: Object.freeze({
6980
7587
  enabled: true,
6981
- separator: "powerline-thin",
7588
+ separator: "|",
6982
7589
  layout: Object.freeze({
6983
7590
  left: ["path", "git", "context_bar", "cost"],
6984
7591
  right: ["model_effort"],
@@ -6995,7 +7602,10 @@ var DEFAULT_CONFIG = Object.freeze({
6995
7602
  assistantPrefix: true,
6996
7603
  specialBlocks: true,
6997
7604
  hideThinkingLabel: true,
6998
- hideInterimText: true
7605
+ hideInterimText: true,
7606
+ showImagePreviews: true,
7607
+ clipboardImages: true,
7608
+ previewMaxWidth: 30
6999
7609
  }),
7000
7610
  tools: Object.freeze({
7001
7611
  enabled: true,
@@ -7112,7 +7722,10 @@ function normalizeConfig(input, defaults = DEFAULT_CONFIG) {
7112
7722
  assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
7113
7723
  specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
7114
7724
  hideThinkingLabel: bool(messages.hideThinkingLabel, defaults.messages.hideThinkingLabel),
7115
- hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText)
7725
+ hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText),
7726
+ showImagePreviews: bool(messages.showImagePreviews, defaults.messages.showImagePreviews),
7727
+ clipboardImages: bool(messages.clipboardImages, defaults.messages.clipboardImages),
7728
+ previewMaxWidth: boundedInt(messages.previewMaxWidth, defaults.messages.previewMaxWidth, 8, 60)
7116
7729
  }),
7117
7730
  tools: Object.freeze({
7118
7731
  enabled: bool(tools.enabled, defaults.tools.enabled),
@@ -7166,6 +7779,8 @@ var BOOL_PATHS = /* @__PURE__ */ new Set([
7166
7779
  "messages.specialBlocks",
7167
7780
  "messages.hideThinkingLabel",
7168
7781
  "messages.hideInterimText",
7782
+ "messages.showImagePreviews",
7783
+ "messages.clipboardImages",
7169
7784
  "tools.enabled",
7170
7785
  "tools.showElapsed",
7171
7786
  "tools.dimOutput",
@@ -7211,6 +7826,8 @@ function validLeaf(path, value) {
7211
7826
  return typeof value === "string";
7212
7827
  if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
7213
7828
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7829
+ if (path === "messages.previewMaxWidth")
7830
+ return typeof value === "number" && Number.isFinite(value) && value >= 8 && value <= 60;
7214
7831
  if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
7215
7832
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7216
7833
  if (STRING_ARRAY_PATHS.has(path)) return Array.isArray(value) && value.every((item) => typeof item === "string");
@@ -7634,6 +8251,9 @@ var allowedPaths = /* @__PURE__ */ new Set([
7634
8251
  "messages.specialBlocks",
7635
8252
  "messages.hideThinkingLabel",
7636
8253
  "messages.hideInterimText",
8254
+ "messages.showImagePreviews",
8255
+ "messages.clipboardImages",
8256
+ "messages.previewMaxWidth",
7637
8257
  "tools.enabled",
7638
8258
  "tools.style",
7639
8259
  "tools.maxCollapsedLines",
@@ -7666,6 +8286,9 @@ function validatePathValue(path, value) {
7666
8286
  "messages.specialBlocks",
7667
8287
  "messages.hideThinkingLabel",
7668
8288
  "messages.hideInterimText",
8289
+ "messages.showImagePreviews",
8290
+ "messages.clipboardImages",
8291
+ "messages.previewMaxWidth",
7669
8292
  "tools.enabled",
7670
8293
  "tools.showElapsed",
7671
8294
  "tools.dimOutput",
@@ -7688,6 +8311,8 @@ function validatePathValue(path, value) {
7688
8311
  return typeof value === "string";
7689
8312
  if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
7690
8313
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
8314
+ if (path === "messages.previewMaxWidth")
8315
+ return typeof value === "number" && Number.isFinite(value) && value >= 8 && value <= 60;
7691
8316
  if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
7692
8317
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
7693
8318
  if (path.endsWith(".colors") || path.endsWith(".glyphs"))
@@ -7820,8 +8445,8 @@ function runCommand(args, host, app, storage) {
7820
8445
  }
7821
8446
 
7822
8447
  // extension-src/pi-style/pi/config-host.ts
7823
- import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
7824
- import { dirname as dirname2, join as join2 } from "path";
8448
+ import { mkdir, readFile as readFile3, rename, unlink, writeFile } from "fs/promises";
8449
+ import { dirname as dirname3, join as join2 } from "path";
7825
8450
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
7826
8451
  var locks = /* @__PURE__ */ new Map();
7827
8452
  function serialize(path, operation) {
@@ -7834,14 +8459,14 @@ function serialize(path, operation) {
7834
8459
  }
7835
8460
  function createPiConfigFilePort(fs = {}) {
7836
8461
  const fsMkdir = fs.mkdir ?? mkdir;
7837
- const fsReadFile = fs.readFile ?? readFile;
8462
+ const fsReadFile = fs.readFile ?? readFile3;
7838
8463
  const fsRename = fs.rename ?? rename;
7839
8464
  const fsUnlink = fs.unlink ?? unlink;
7840
8465
  const fsWriteFile = fs.writeFile ?? writeFile;
7841
8466
  return {
7842
8467
  read: (path) => fsReadFile(path, "utf8"),
7843
8468
  writeAtomic: async (path, content) => serialize(path, async () => {
7844
- await fsMkdir(dirname2(path), { recursive: true });
8469
+ await fsMkdir(dirname3(path), { recursive: true });
7845
8470
  const temporary = `${path}.pi-style-${process.pid}-${Date.now()}.tmp`;
7846
8471
  try {
7847
8472
  await fsWriteFile(temporary, content, { mode: 384 });
@@ -8030,12 +8655,17 @@ function createBuiltinSegments() {
8030
8655
  const percent = contextPercent(snapshot.context ?? {});
8031
8656
  if (percent === void 0) return { visible: false, content: "" };
8032
8657
  const token = contextBarToken(percent);
8033
- const window = snapshot.context?.windowTokens;
8034
- const label = `ctx${window !== void 0 ? ` (${formatTokens(window)})` : ""}:`;
8035
8658
  const width = options.context_bar?.width ?? CONTEXT_BAR_WIDTH;
8659
+ const pipe = theme.apply("separator", "|");
8660
+ const bar = `${theme.apply("muted", "[")}${theme.apply(token, contextBar(percent, width))}${theme.apply("muted", "]")}`;
8661
+ const parts = [bar, `${theme.apply(token, `${Math.round(percent)}%`)}${theme.apply("muted", " used")}`];
8662
+ const current = snapshot.context?.currentTokens;
8663
+ const window = snapshot.context?.windowTokens;
8664
+ if (current !== void 0 && window !== void 0)
8665
+ parts.push(theme.apply("muted", `${formatTokens(current)}/${formatTokens(window)}`));
8036
8666
  return {
8037
8667
  visible: true,
8038
- content: `${theme.apply("muted", label)} ${theme.apply(token, contextBar(percent, width))} ${theme.apply(token, `${Math.round(percent)}%`)}`,
8668
+ content: parts.join(` ${pipe} `),
8039
8669
  compactContent: theme.apply(token, `${Math.round(percent)}%`)
8040
8670
  };
8041
8671
  }),
@@ -8077,11 +8707,10 @@ function createBuiltinSegments() {
8077
8707
  content: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt)),
8078
8708
  compactContent: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt))
8079
8709
  })),
8080
- segment("time", 20, ({ theme }) => ({
8081
- visible: true,
8082
- content: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })),
8083
- compactContent: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))
8084
- })),
8710
+ segment("time", 20, ({ theme }) => {
8711
+ const text = theme.apply("time", clockTime());
8712
+ return { visible: true, content: text, compactContent: text };
8713
+ }),
8085
8714
  segment("hostname", 20, ({ snapshot, theme }) => ({
8086
8715
  visible: Boolean(snapshot.hostname),
8087
8716
  content: theme.apply("muted", snapshot.hostname ?? "")
@@ -8099,6 +8728,15 @@ function createBuiltinSegments() {
8099
8728
  ];
8100
8729
  return new Map(segments.map((item) => [item.id, item]));
8101
8730
  }
8731
+ var clockCache;
8732
+ function clockTime() {
8733
+ const now = /* @__PURE__ */ new Date();
8734
+ const minuteKey = now.getHours() * 60 + now.getMinutes();
8735
+ if (clockCache?.minuteKey !== minuteKey) {
8736
+ clockCache = { minuteKey, value: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) };
8737
+ }
8738
+ return clockCache.value;
8739
+ }
8102
8740
  var CONTEXT_BAR_WIDTH = 10;
8103
8741
  function contextBar(percent, width = CONTEXT_BAR_WIDTH) {
8104
8742
  const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
@@ -8110,10 +8748,7 @@ function contextBarToken(percent) {
8110
8748
  return "success";
8111
8749
  }
8112
8750
  function formatTokens(value) {
8113
- if (value >= 1e6) {
8114
- const millions = value / 1e6;
8115
- return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`;
8116
- }
8751
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
8117
8752
  if (value >= 1e3) {
8118
8753
  const thousands = value / 1e3;
8119
8754
  return `${Number.isInteger(thousands) ? thousands : thousands.toFixed(1)}K`;
@@ -8284,7 +8919,24 @@ function colorPrefixFor(active, config, noColor, token) {
8284
8919
  }
8285
8920
  return "";
8286
8921
  }
8922
+ function envKeyFor(env) {
8923
+ return `${env.PI_STYLE_NERD_FONTS ?? ""}\0${env.GHOSTTY_RESOURCES_DIR ? "1" : "0"}\0${env.TERM_PROGRAM ?? ""}`;
8924
+ }
8925
+ var glyphModeCache = /* @__PURE__ */ new WeakMap();
8287
8926
  function detectGlyphMode(config, env = {}) {
8927
+ let byEnv = glyphModeCache.get(config);
8928
+ if (!byEnv) {
8929
+ byEnv = /* @__PURE__ */ new Map();
8930
+ glyphModeCache.set(config, byEnv);
8931
+ }
8932
+ const envKey = envKeyFor(env);
8933
+ const cached = byEnv.get(envKey);
8934
+ if (cached !== void 0) return cached;
8935
+ const mode = computeGlyphMode(config, env);
8936
+ byEnv.set(envKey, mode);
8937
+ return mode;
8938
+ }
8939
+ function computeGlyphMode(config, env) {
8288
8940
  if (env.PI_STYLE_NERD_FONTS === "1") return "nerd";
8289
8941
  if (env.PI_STYLE_NERD_FONTS === "0") return "unicode";
8290
8942
  if (config.theme.nerdFonts === "on") return "nerd";
@@ -8299,7 +8951,14 @@ function detectGlyphMode(config, env = {}) {
8299
8951
  function resolveTheme(active, config, env = {}) {
8300
8952
  const noColor = Object.hasOwn(env, "NO_COLOR") && env.NO_COLOR !== "" && config.theme.colors.colorOverride !== "on";
8301
8953
  const mode = config.preset === "ascii" ? "ascii" : detectGlyphMode(config, env);
8302
- const color = (token) => colorPrefixFor(active, config, noColor, token);
8954
+ const prefixes = /* @__PURE__ */ new Map();
8955
+ const color = (token) => {
8956
+ const cached = prefixes.get(token);
8957
+ if (cached !== void 0) return cached;
8958
+ const prefix = colorPrefixFor(active, config, noColor, token);
8959
+ prefixes.set(token, prefix);
8960
+ return prefix;
8961
+ };
8303
8962
  return {
8304
8963
  mode,
8305
8964
  noColor,
@@ -8317,9 +8976,13 @@ function resolveTheme(active, config, env = {}) {
8317
8976
  var widthOf = visibleWidth3;
8318
8977
  function widthSafe(value, width) {
8319
8978
  if (width <= 0) return "";
8320
- const fitted = widthOf(value) > width ? truncateAnsi(value, width, "") : value;
8321
- const current = widthOf(fitted);
8322
- return current < width ? fitted + " ".repeat(width - current) : fitted;
8979
+ const measured = widthOf(value);
8980
+ if (measured > width) {
8981
+ const fitted = truncateAnsi(value, width, "");
8982
+ const current = widthOf(fitted);
8983
+ return current < width ? fitted + " ".repeat(width - current) : fitted;
8984
+ }
8985
+ return measured < width ? value + " ".repeat(width - measured) : value;
8323
8986
  }
8324
8987
  function isNativeBorderLine(line) {
8325
8988
  const stripped = stripAnsi(line);
@@ -8389,8 +9052,14 @@ var StyledEditor = class extends CustomEditor {
8389
9052
  fullTheme;
8390
9053
  onSnapshot;
8391
9054
  semantic;
9055
+ /** Set when config changes; invalidate() then rebuilds the (otherwise stable) semantic theme. */
9056
+ semanticDirty = false;
8392
9057
  disposed = false;
8393
9058
  renderPlanCache;
9059
+ clipboardImagePaste;
9060
+ /** Keybinding matcher (the factory's KeybindingsManager) for keystroke-level
9061
+ * interception: the paste keybinding and backspace checks below. */
9062
+ editorKeybindings;
8394
9063
  constructor(tui, theme, keybindings, options) {
8395
9064
  super(tui, theme, keybindings);
8396
9065
  this.config = options.config;
@@ -8398,6 +9067,8 @@ var StyledEditor = class extends CustomEditor {
8398
9067
  this.piTheme = theme;
8399
9068
  this.fullTheme = options.fullTheme;
8400
9069
  this.onSnapshot = options.onSnapshot;
9070
+ this.clipboardImagePaste = options.clipboardImagePaste;
9071
+ this.editorKeybindings = keybindings;
8401
9072
  this.semantic = semanticTheme(theme, options.config);
8402
9073
  this.setPaddingX(0);
8403
9074
  }
@@ -8409,17 +9080,99 @@ var StyledEditor = class extends CustomEditor {
8409
9080
  configure(config) {
8410
9081
  if (this.disposed) return;
8411
9082
  this.config = config;
8412
- this.semantic = semanticTheme(this.piTheme, config);
9083
+ this.semanticDirty = true;
8413
9084
  this.invalidate();
8414
9085
  }
8415
9086
  handleInput(data) {
8416
9087
  if (this.disposed) return;
9088
+ if (this.handleClipboardImagePasteKeystroke(data)) return;
9089
+ if (this.handleAtomicMarkerBackspace(data)) return;
8417
9090
  super.handleInput(data);
8418
9091
  this.onSnapshot({ ...this.snapshot });
8419
9092
  }
9093
+ /**
9094
+ * Instant marker (ADR 0009): when the built-in paste keybinding fires, the
9095
+ * clipboard holds an image (sync native probe), and the surface is enabled,
9096
+ * the keystroke is owned here — an `[Image #N] ` marker is inserted
9097
+ * immediately (zero-latency feedback) and the bytes fill the registry
9098
+ * entry asynchronously. Pi's own paste handler never runs for this
9099
+ * keystroke, so no temp artifact is written. Text pastes (no image) and
9100
+ * probe-unavailable hosts fall through to the native path untouched.
9101
+ */
9102
+ handleClipboardImagePasteKeystroke(data) {
9103
+ const surface = this.clipboardImagePaste;
9104
+ if (!surface?.enabled()) return false;
9105
+ if (!this.editorKeybindings.matches(data, "app.clipboard.pasteImage")) return false;
9106
+ if (surface.clipboardHasImage() !== true) return false;
9107
+ super.insertTextAtCursor(surface.markerFromClipboard());
9108
+ this.onSnapshot({ ...this.snapshot });
9109
+ return true;
9110
+ }
9111
+ /**
9112
+ * Atomic marker backspace (ADR 0009): backspacing directly after a
9113
+ * registered `[Image #N] ` marker deletes the whole marker as one unit —
9114
+ * the image is discarded (image-paste semantics). Direct state surgery
9115
+ * (O(1)) mirroring pi-tui's handleBackspace mutation protocol: no setText
9116
+ * round-trip (it clears the pastes registry, and undo snapshots store that
9117
+ * Map by reference — silently breaking `[paste #N]` atomicity on undo) and
9118
+ * no per-column left-arrow inputs (O(K·L) through the keybinding chain and
9119
+ * visual line maps). No undo snapshot is pushed: the discarded image
9120
+ * cannot be restored, so the deletion is intentionally non-undoable.
9121
+ */
9122
+ handleAtomicMarkerBackspace(data) {
9123
+ const surface = this.clipboardImagePaste;
9124
+ if (!surface?.enabled()) return false;
9125
+ const isBackspace = this.editorKeybindings.matches(data, "tui.editor.deleteCharBackward") || data === "\x7F";
9126
+ if (!isBackspace) return false;
9127
+ const lines = this.getLines();
9128
+ const cursor = this.getCursor();
9129
+ const current = lines[cursor.line];
9130
+ if (current === void 0) return false;
9131
+ const before = current.slice(0, cursor.col);
9132
+ const hit = clipboardMarkerAtEnd(before);
9133
+ if (!hit || !surface.isMarkerIndexRegistered(hit.index)) return false;
9134
+ const editor = this;
9135
+ const targetCol = cursor.col - hit.length;
9136
+ editor.exitHistoryBrowsing();
9137
+ editor.lastAction = null;
9138
+ editor.cancelAutocomplete();
9139
+ editor.state.lines[cursor.line] = current.slice(0, targetCol) + current.slice(cursor.col);
9140
+ editor.setCursorCol(targetCol);
9141
+ editor.onChange?.(this.getText());
9142
+ surface.discardMarkerIndex(hit.index);
9143
+ this.invalidate();
9144
+ this.onSnapshot({ ...this.snapshot });
9145
+ return true;
9146
+ }
9147
+ /**
9148
+ * Artifact-path fallback (ADR 0009): when the keystroke was not owned
9149
+ * (probe unavailable, native editor path, config toggled) Pi's built-in
9150
+ * paste still inserts a `<tmpdir>/pi-clipboard-<uuid>.<ext>` path through
9151
+ * this method. Convert it to a marker when the artifact reads successfully;
9152
+ * every failure inserts the original text verbatim (exact native behavior).
9153
+ */
9154
+ insertTextAtCursor(text) {
9155
+ const surface = this.clipboardImagePaste;
9156
+ if (!surface?.enabled() || !isSingleClipboardImagePath(text)) {
9157
+ super.insertTextAtCursor(text);
9158
+ return;
9159
+ }
9160
+ void (async () => {
9161
+ let replacement = text;
9162
+ try {
9163
+ replacement = await surface.markerFromArtifact(text);
9164
+ } catch {
9165
+ }
9166
+ super.insertTextAtCursor(replacement);
9167
+ this.tui.requestRender();
9168
+ })();
9169
+ }
8420
9170
  invalidate() {
8421
9171
  super.invalidate();
8422
- this.semantic = semanticTheme(this.piTheme, this.config);
9172
+ if (this.semanticDirty) {
9173
+ this.semantic = semanticTheme(this.piTheme, this.config);
9174
+ this.semanticDirty = false;
9175
+ }
8423
9176
  this.renderPlanCache = void 0;
8424
9177
  this.tui.requestRender();
8425
9178
  }
@@ -8629,6 +9382,7 @@ function installEditor(options) {
8629
9382
  snapshot,
8630
9383
  theme,
8631
9384
  ...options.host.theme ? { fullTheme: options.host.theme } : {},
9385
+ ...options.clipboardImagePaste ? { clipboardImagePaste: options.clipboardImagePaste } : {},
8632
9386
  onSnapshot: (next) => {
8633
9387
  if (!disposed && options.isCurrent?.() !== false) snapshot = next;
8634
9388
  }
@@ -9203,12 +9957,9 @@ function uniqueLayout(layout) {
9203
9957
  function renderGroup(items, separator, padding) {
9204
9958
  return items.map((item) => item.content).filter(Boolean).join(`${padding}${separator}${padding}`);
9205
9959
  }
9206
- function widthOf2(items, separator, padding) {
9207
- return visibleWidth(renderGroup(items, separator, padding));
9208
- }
9209
9960
  function renderStatus(layout, snapshot, width, options) {
9210
9961
  if (width <= 0) return { primary: "", lines: [], visibleSegments: [] };
9211
- const separator = options.separator ?? "\u2502";
9962
+ const separator = options.separator ?? "|";
9212
9963
  const padding = options.padding ?? " ";
9213
9964
  const normalized = uniqueLayout(layout);
9214
9965
  const context = { snapshot, theme: options.theme, options: options.options ?? {}, width };
@@ -9220,7 +9971,15 @@ function renderStatus(layout, snapshot, width, options) {
9220
9971
  try {
9221
9972
  const result = segment2.render(context);
9222
9973
  if (!result.visible || !result.content) continue;
9223
- candidates.set(id, { id, segment: segment2, result, content: result.content, compact: false, moved: false });
9974
+ candidates.set(id, {
9975
+ id,
9976
+ segment: segment2,
9977
+ result,
9978
+ content: result.content,
9979
+ contentWidth: visibleWidth(result.content),
9980
+ compact: false,
9981
+ moved: false
9982
+ });
9224
9983
  } catch {
9225
9984
  }
9226
9985
  }
@@ -9230,13 +9989,28 @@ function renderStatus(layout, snapshot, width, options) {
9230
9989
  const secondary = normalized.secondary.map((id) => candidates.get(id)).filter((candidate) => candidate !== void 0);
9231
9990
  const visible = [];
9232
9991
  const overflow = [];
9992
+ const gapWidth = visibleWidth(`${padding}${separator}${padding}`);
9993
+ let groupWidth = 0;
9994
+ let groupCount = 0;
9995
+ const widthAfter = (baseWidth, count, candidate) => count === 0 ? candidate.contentWidth : baseWidth + gapWidth + candidate.contentWidth;
9233
9996
  for (const candidate of primary) {
9234
9997
  visible.push(candidate);
9235
- if (widthOf2(visible, separator, padding) <= width) continue;
9998
+ const pushedWidth = widthAfter(groupWidth, groupCount, candidate);
9999
+ if (pushedWidth <= width) {
10000
+ groupWidth = pushedWidth;
10001
+ groupCount++;
10002
+ continue;
10003
+ }
9236
10004
  if (candidate.result.compactContent && !candidate.compact) {
9237
10005
  candidate.content = candidate.result.compactContent;
9238
10006
  candidate.compact = true;
9239
- if (widthOf2(visible, separator, padding) <= width) continue;
10007
+ candidate.contentWidth = visibleWidth(candidate.content);
10008
+ const compactedWidth = widthAfter(groupWidth, groupCount, candidate);
10009
+ if (compactedWidth <= width) {
10010
+ groupWidth = compactedWidth;
10011
+ groupCount++;
10012
+ continue;
10013
+ }
9240
10014
  }
9241
10015
  visible.pop();
9242
10016
  if (candidate.segment.overflow !== "drop" && candidate.segment.overflow !== "primary") {
@@ -9262,9 +10036,14 @@ function renderStatus(layout, snapshot, width, options) {
9262
10036
  }
9263
10037
  if (visibleWidth(primaryText) > width) primaryText = truncateAnsi(primaryText, width);
9264
10038
  const secondaryVisible = [];
10039
+ let secondaryWidth = 0;
10040
+ let secondaryCount = 0;
9265
10041
  for (const candidate of [...secondary].sort((a, b) => b.segment.defaultPriority - a.segment.defaultPriority)) {
10042
+ const pushedWidth = widthAfter(secondaryWidth, secondaryCount, candidate);
10043
+ if (pushedWidth > width) continue;
9266
10044
  secondaryVisible.push(candidate);
9267
- if (widthOf2(secondaryVisible, separator, padding) > width) secondaryVisible.pop();
10045
+ secondaryWidth = pushedWidth;
10046
+ secondaryCount++;
9268
10047
  }
9269
10048
  const secondaryText = renderGroup(secondaryVisible, separator, padding);
9270
10049
  const lines = secondaryText ? [primaryText, secondaryText] : primaryText ? [primaryText] : [];
@@ -9317,6 +10096,28 @@ function separatorsFor(config, theme) {
9317
10096
  if (style === "none") return " ";
9318
10097
  return theme.apply("separator", style);
9319
10098
  }
10099
+ var themeAssetsCache = /* @__PURE__ */ new WeakMap();
10100
+ function themeAssetsFor(activeTheme, config) {
10101
+ let byConfig = themeAssetsCache.get(activeTheme);
10102
+ if (!byConfig) {
10103
+ byConfig = /* @__PURE__ */ new WeakMap();
10104
+ themeAssetsCache.set(activeTheme, byConfig);
10105
+ }
10106
+ let assets = byConfig.get(config);
10107
+ if (!assets) {
10108
+ const resolved = resolveTheme(
10109
+ activeTheme.colors || activeTheme.fg ? {
10110
+ ...activeTheme.colors ? { colors: activeTheme.colors } : {},
10111
+ // Call through the theme instance so `this` binds correctly inside Pi's fg().
10112
+ ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
10113
+ } : void 0,
10114
+ config
10115
+ );
10116
+ assets = { resolved, separator: separatorsFor(config, resolved) };
10117
+ byConfig.set(config, assets);
10118
+ }
10119
+ return assets;
10120
+ }
9320
10121
  function installStatusLine(options) {
9321
10122
  const existing = installationMap(options.host).get(options.generation);
9322
10123
  if (existing) return existing;
@@ -9349,16 +10150,9 @@ function installStatusLine(options) {
9349
10150
  }
9350
10151
  const render = (activeTheme, width, secondary) => {
9351
10152
  if (width <= 0 || !config.enabled || !config.statusLine.enabled) return [];
9352
- const resolved = resolveTheme(
9353
- activeTheme.colors || activeTheme.fg ? {
9354
- ...activeTheme.colors ? { colors: activeTheme.colors } : {},
9355
- // Call through the theme instance so `this` binds correctly inside Pi's fg().
9356
- ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
9357
- } : void 0,
9358
- config
9359
- );
10153
+ const { resolved, separator } = themeAssetsFor(activeTheme, config);
9360
10154
  const result = renderStatus(config.statusLine.layout, effectiveSnapshot(snapshot), width, {
9361
- separator: separatorsFor(config, resolved),
10155
+ separator,
9362
10156
  segments,
9363
10157
  theme: resolved,
9364
10158
  options: {
@@ -9439,12 +10233,16 @@ function installStatusLine(options) {
9439
10233
  };
9440
10234
  const factory = (secondary) => (tui, theme) => {
9441
10235
  const currentTheme = theme;
10236
+ let renderCache;
9442
10237
  const component = {
9443
10238
  render(width) {
10239
+ if (renderCache?.width === width) return renderCache.lines;
9444
10240
  const lines = render(currentTheme, width, secondary);
10241
+ renderCache = { width, lines };
9445
10242
  return lines;
9446
10243
  },
9447
10244
  invalidate() {
10245
+ renderCache = void 0;
9448
10246
  primaryComponent = secondary ? primaryComponent : component;
9449
10247
  secondaryComponent = secondary ? component : secondaryComponent;
9450
10248
  if (tui.requestRender) tui.requestRender();
@@ -9862,6 +10660,7 @@ function isPlainRecord(value) {
9862
10660
  }
9863
10661
 
9864
10662
  // extension-src/pi-style/app/runtime.ts
10663
+ var GIT_INVALIDATE_DEBOUNCE_MS = 250;
9865
10664
  function createPiStyleRuntime(host, generation2, requestRender = host.requestRender ?? (() => {
9866
10665
  })) {
9867
10666
  let disposed = false;
@@ -9887,6 +10686,7 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
9887
10686
  const disposables = new DisposableStore();
9888
10687
  const scheduler = new RenderScheduler({ requestRender }, generation2, () => !disposed);
9889
10688
  const git = new CachedGitProvider(host.gitRunner);
10689
+ let pendingGitRefresh;
9890
10690
  const contextProvider = new InMemoryContextProvider();
9891
10691
  const usageProvider = new InMemoryUsageProvider();
9892
10692
  const initialContext = contextSnapshot();
@@ -9976,7 +10776,10 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
9976
10776
  config: currentConfig,
9977
10777
  generation: generation2,
9978
10778
  initialSnapshot: currentSnapshot,
9979
- isCurrent: () => !disposed
10779
+ isCurrent: () => !disposed,
10780
+ // Clipboard image paste surface (ADR 0009): instant `[Image #N] `
10781
+ // markers at keystroke time, artifact fallback, atomic backspace.
10782
+ clipboardImagePaste: createClipboardImagePasteSurface()
9980
10783
  });
9981
10784
  if (editor) {
9982
10785
  disposables.add(editor);
@@ -10119,11 +10922,18 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
10119
10922
  },
10120
10923
  invalidateGit() {
10121
10924
  if (disposed || !host.cwd || !currentConfig.enabled || !currentConfig.statusLine.enabled) return;
10122
- git.invalidate(host.cwd);
10123
- void git.get(host.cwd).then((value) => {
10925
+ const cwd = host.cwd;
10926
+ git.invalidate(cwd);
10927
+ if (pendingGitRefresh !== void 0) return;
10928
+ pendingGitRefresh = setTimeout(() => {
10929
+ pendingGitRefresh = void 0;
10124
10930
  if (disposed) return;
10125
- if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
10126
- });
10931
+ void git.get(cwd).then((value) => {
10932
+ if (disposed) return;
10933
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
10934
+ });
10935
+ }, GIT_INVALIDATE_DEBOUNCE_MS);
10936
+ pendingGitRefresh.unref?.();
10127
10937
  },
10128
10938
  get disposed() {
10129
10939
  return disposed;
@@ -10131,6 +10941,10 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
10131
10941
  dispose() {
10132
10942
  if (disposed) return;
10133
10943
  disposed = true;
10944
+ if (pendingGitRefresh !== void 0) {
10945
+ clearTimeout(pendingGitRefresh);
10946
+ pendingGitRefresh = void 0;
10947
+ }
10134
10948
  scheduler.cancel();
10135
10949
  disposeStatus();
10136
10950
  disposeEditor();
@@ -10663,7 +11477,7 @@ function isTierCAuthorized(input) {
10663
11477
 
10664
11478
  // extension-src/pi-style/pi/compatibility-probe.ts
10665
11479
  import { readFileSync as readFileSync2 } from "fs";
10666
- import { dirname as dirname3, join as join3 } from "path";
11480
+ import { dirname as dirname4, join as join3 } from "path";
10667
11481
  import { fileURLToPath as fileURLToPath2 } from "url";
10668
11482
  import {
10669
11483
  AssistantMessageComponent,
@@ -10684,6 +11498,8 @@ var MAX_RENDER_CACHE_KEYS_PER_INSTANCE = 8;
10684
11498
  var MAX_LINE_ANALYSIS_ENTRIES = 4096;
10685
11499
  var renderCacheByInstance = /* @__PURE__ */ new WeakMap();
10686
11500
  var lineAnalysisCache = /* @__PURE__ */ new Map();
11501
+ var lineCacheEvictionCursor;
11502
+ var childrenScanByInstance = /* @__PURE__ */ new WeakMap();
10687
11503
  var messageDecorationTestState = {
10688
11504
  decoratePasses: 0,
10689
11505
  cacheHits: 0,
@@ -10706,7 +11522,7 @@ function splitLeadingMarkers(line) {
10706
11522
  if (end === -1) break;
10707
11523
  index = end + 1;
10708
11524
  }
10709
- return { head: line.slice(0, index), rest: line.slice(index) };
11525
+ return index === 0 ? { head: "", rest: line } : { head: line.slice(0, index), rest: line.slice(index) };
10710
11526
  }
10711
11527
  function leadingSgr(line) {
10712
11528
  if (!line.startsWith("\x1B[")) return "";
@@ -10720,41 +11536,153 @@ function leadingSgr(line) {
10720
11536
  }
10721
11537
  function isBackgroundSgr(sequence) {
10722
11538
  if (!sequence.startsWith("\x1B[") || !sequence.endsWith("m")) return false;
10723
- for (const code of sequence.slice(2, -1).split(";")) {
10724
- const value = Number(code);
10725
- if (value === 48 || value === 49) return true;
10726
- if (value >= 40 && value <= 47) return true;
10727
- if (value >= 100 && value <= 107) return true;
11539
+ let value = 0;
11540
+ let empty = true;
11541
+ let valid = true;
11542
+ for (let index = 2; index < sequence.length - 1; index++) {
11543
+ const code = sequence.charCodeAt(index);
11544
+ if (code === 59) {
11545
+ if (valid && matchesBackgroundCode(empty ? 0 : value)) return true;
11546
+ value = 0;
11547
+ empty = true;
11548
+ valid = true;
11549
+ continue;
11550
+ }
11551
+ if (code < 48 || code > 57) {
11552
+ valid = false;
11553
+ continue;
11554
+ }
11555
+ value = value * 10 + (code - 48);
11556
+ empty = false;
10728
11557
  }
10729
- return false;
11558
+ return valid && matchesBackgroundCode(empty ? 0 : value);
11559
+ }
11560
+ function matchesBackgroundCode(value) {
11561
+ return value === 48 || value === 49 || value >= 40 && value <= 47 || value >= 100 && value <= 107;
11562
+ }
11563
+ function certifiedAsciiWidth(value) {
11564
+ let width = 0;
11565
+ let index = 0;
11566
+ const length = value.length;
11567
+ while (index < length) {
11568
+ const code = value.charCodeAt(index);
11569
+ if (code === 27) {
11570
+ const next = index + 1 < length ? value.charCodeAt(index + 1) : -1;
11571
+ if (next === 91) {
11572
+ let scan = index + 2;
11573
+ while (scan < length) {
11574
+ const terminator = value.charCodeAt(scan);
11575
+ if (terminator === 109 || // m
11576
+ terminator === 71 || // G
11577
+ terminator === 75 || // K
11578
+ terminator === 72 || // H
11579
+ terminator === 74) {
11580
+ index = scan + 1;
11581
+ break;
11582
+ }
11583
+ scan++;
11584
+ }
11585
+ if (scan >= length) return void 0;
11586
+ continue;
11587
+ }
11588
+ if (next === 93 || next === 95) {
11589
+ let scan = index + 2;
11590
+ let end = -1;
11591
+ while (scan < length) {
11592
+ const terminator = value.charCodeAt(scan);
11593
+ if (terminator === 7) {
11594
+ end = scan + 1;
11595
+ break;
11596
+ }
11597
+ if (terminator === 27 && scan + 1 < length && value.charCodeAt(scan + 1) === 92) {
11598
+ end = scan + 2;
11599
+ break;
11600
+ }
11601
+ scan++;
11602
+ }
11603
+ if (end < 0) return void 0;
11604
+ index = end;
11605
+ continue;
11606
+ }
11607
+ return void 0;
11608
+ }
11609
+ if (code < 32 || code > 126) return void 0;
11610
+ width++;
11611
+ index++;
11612
+ }
11613
+ return width;
11614
+ }
11615
+ function certifiedVisibleWidth(value) {
11616
+ return certifiedAsciiWidth(value) ?? visibleWidth(value);
11617
+ }
11618
+ var prefixWidthMemo;
11619
+ function prefixWidthOf(prefix) {
11620
+ if (prefixWidthMemo?.prefix === prefix) return prefixWidthMemo.width;
11621
+ const width = certifiedVisibleWidth(prefix);
11622
+ prefixWidthMemo = { prefix, width };
11623
+ return width;
10730
11624
  }
10731
11625
  function contentText(line) {
11626
+ if (!line.includes("\x1B")) return line;
10732
11627
  let output = "";
11628
+ let sliceStart = 0;
10733
11629
  for (let index = 0; index < line.length; index++) {
10734
- if (line.charCodeAt(index) !== 27) {
10735
- output += line[index];
10736
- continue;
10737
- }
11630
+ if (line.charCodeAt(index) !== 27) continue;
11631
+ output += line.slice(sliceStart, index);
10738
11632
  const next = line[index + 1];
10739
11633
  if (next === "]") {
10740
11634
  index += 2;
10741
11635
  while (index < line.length && line.charCodeAt(index) !== 7) index++;
10742
- continue;
10743
- }
10744
- if (next === "[") {
11636
+ } else if (next === "[") {
10745
11637
  index += 2;
10746
11638
  while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
10747
11639
  }
11640
+ sliceStart = index + 1;
11641
+ }
11642
+ output += line.slice(sliceStart);
11643
+ if (output.includes("\x1B]133;")) {
11644
+ return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
10748
11645
  }
10749
- return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
11646
+ return output;
11647
+ }
11648
+ function isWhitespaceCode(code) {
11649
+ if (code === 32 || code >= 9 && code <= 13) return true;
11650
+ if (code < 128) return false;
11651
+ return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
10750
11652
  }
10751
11653
  function hasContent(line) {
10752
- return [...contentText(line)].some((character) => !/\s/u.test(character));
11654
+ const length = line.length;
11655
+ let index = 0;
11656
+ while (index < length) {
11657
+ const code = line.charCodeAt(index);
11658
+ if (code === 27) {
11659
+ const next = index + 1 < length ? line.charCodeAt(index + 1) : -1;
11660
+ if (next === 93) {
11661
+ index += 2;
11662
+ while (index < length && line.charCodeAt(index) !== 7) index++;
11663
+ } else if (next === 91) {
11664
+ index += 2;
11665
+ while (index < length) {
11666
+ const inner = line.charCodeAt(index);
11667
+ if (inner >= 64 && inner <= 126) break;
11668
+ index++;
11669
+ }
11670
+ }
11671
+ index++;
11672
+ continue;
11673
+ }
11674
+ if (code >= 55296 && code <= 57343) return true;
11675
+ if (!isWhitespaceCode(code)) return true;
11676
+ index++;
11677
+ }
11678
+ return false;
10753
11679
  }
10754
11680
  function getLineAnalysis(line) {
10755
11681
  const cached = lineAnalysisCache.get(line);
10756
11682
  if (cached) {
10757
11683
  messageDecorationTestState.lineCacheHits++;
11684
+ lineAnalysisCache.delete(line);
11685
+ lineAnalysisCache.set(line, cached);
10758
11686
  return cached;
10759
11687
  }
10760
11688
  messageDecorationTestState.lineCacheMisses++;
@@ -10762,21 +11690,29 @@ function getLineAnalysis(line) {
10762
11690
  const backgroundAnsi = leadingSgr(leadingMarkers.rest);
10763
11691
  const isBackgroundWrapped = backgroundAnsi !== "" && isBackgroundSgr(backgroundAnsi) && leadingMarkers.rest.endsWith(BG_RESET);
10764
11692
  const backgroundBody = isBackgroundWrapped ? leadingMarkers.rest.slice(backgroundAnsi.length, leadingMarkers.rest.length - BG_RESET.length) : void 0;
11693
+ const hasOscStart = line.startsWith(OSC133_ZONE_START);
10765
11694
  const analysis = {
10766
- visibleWidth: visibleWidth(line),
11695
+ visibleWidth: certifiedVisibleWidth(line),
10767
11696
  hasContent: hasContent(line),
10768
- oscEnvelope: extractOscEnvelope(line),
10769
- hasOscStart: line.startsWith(OSC133_ZONE_START),
11697
+ oscEnvelope: hasOscStart ? extractOscEnvelope(line) : void 0,
11698
+ hasOscStart,
10770
11699
  leadingMarkers,
10771
11700
  isBackgroundWrapped,
10772
11701
  backgroundAnsi,
10773
11702
  backgroundBody,
10774
- backgroundBodyWidth: backgroundBody === void 0 ? void 0 : visibleWidth(backgroundBody)
11703
+ backgroundBodyWidth: backgroundBody === void 0 ? void 0 : certifiedVisibleWidth(backgroundBody)
10775
11704
  };
10776
11705
  lineAnalysisCache.set(line, analysis);
10777
11706
  if (lineAnalysisCache.size > MAX_LINE_ANALYSIS_ENTRIES) {
10778
- const oldestKey = lineAnalysisCache.keys().next().value;
10779
- if (oldestKey !== void 0) lineAnalysisCache.delete(oldestKey);
11707
+ let cursor = lineCacheEvictionCursor;
11708
+ if (cursor === void 0) cursor = lineAnalysisCache.keys();
11709
+ let oldest = cursor.next();
11710
+ if (oldest.done) {
11711
+ cursor = lineAnalysisCache.keys();
11712
+ oldest = cursor.next();
11713
+ }
11714
+ lineCacheEvictionCursor = cursor;
11715
+ if (!oldest.done && oldest.value !== void 0) lineAnalysisCache.delete(oldest.value);
10780
11716
  }
10781
11717
  return analysis;
10782
11718
  }
@@ -10789,8 +11725,8 @@ function rebuildAtWidth(line, width, lead, leadWidth, analysis = getLineAnalysis
10789
11725
  return `${lead}${line}${pad}`;
10790
11726
  }
10791
11727
  function decorateMessageLine(line, index, lastIndex, contentIndex, width, options, analysis = getLineAnalysis(line)) {
10792
- const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth } = options;
10793
- const lead = index === contentIndex ? prefix : index > contentIndex ? " ".repeat(prefixWidth) : "";
11728
+ const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth, continuationLead } = options;
11729
+ const lead = index === contentIndex ? prefix : index > contentIndex ? continuationLead : "";
10794
11730
  const leadWidth = index < contentIndex ? 0 : prefixWidth;
10795
11731
  if (index === contentIndex && firstEnvelope)
10796
11732
  return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix, prefixWidth)}${firstEnvelope.end}`;
@@ -10846,32 +11782,58 @@ function prefixNative(lines, width, prefix) {
10846
11782
  if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return void 0;
10847
11783
  messageDecorationTestState.decoratePasses++;
10848
11784
  const nativeLines = lines;
10849
- const prefixWidth = visibleWidth(prefix);
11785
+ const prefixWidth = prefixWidthOf(prefix);
10850
11786
  if (width <= prefixWidth) return void 0;
10851
11787
  const bodyWidth = width - prefixWidth;
10852
- const first = nativeLines[0] ?? "";
11788
+ const analyses = nativeLines.map((line) => getLineAnalysis(line));
10853
11789
  const last = nativeLines.at(-1) ?? "";
10854
- const lastAnalysis = getLineAnalysis(last);
11790
+ const lastAnalysis = analyses[analyses.length - 1] ?? getLineAnalysis(last);
10855
11791
  const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
10856
- const firstContentIndex = nativeLines.findIndex((line, index) => {
10857
- if (index !== nativeLines.length - 1 || !multilineEnvelope) return getLineAnalysis(line).hasContent;
10858
- return !nativeLines.slice(0, index).some((earlier) => getLineAnalysis(earlier).hasContent) && lastAnalysis.hasContent;
10859
- });
11792
+ let firstContentIndex = -1;
11793
+ for (let index = 0; index < nativeLines.length; index++) {
11794
+ const analysis = analyses[index] ?? getLineAnalysis(nativeLines[index] ?? "");
11795
+ if (index !== nativeLines.length - 1 || !multilineEnvelope) {
11796
+ if (analysis.hasContent) {
11797
+ firstContentIndex = index;
11798
+ break;
11799
+ }
11800
+ continue;
11801
+ }
11802
+ let earlierHasContent = false;
11803
+ for (let earlier = 0; earlier < index; earlier++) {
11804
+ if ((analyses[earlier] ?? getLineAnalysis(nativeLines[earlier] ?? "")).hasContent) {
11805
+ earlierHasContent = true;
11806
+ break;
11807
+ }
11808
+ }
11809
+ if (!earlierHasContent && lastAnalysis.hasContent) firstContentIndex = index;
11810
+ break;
11811
+ }
10860
11812
  if (firstContentIndex < 0) return nativeLines;
10861
- const firstAnalysis = getLineAnalysis(first);
11813
+ const firstAnalysis = analyses[0] ?? getLineAnalysis(nativeLines[0] ?? "");
10862
11814
  const firstEnvelope = firstContentIndex === 0 ? firstAnalysis.oscEnvelope : void 0;
10863
11815
  const firstHasStart = firstContentIndex === 0 && firstAnalysis.hasOscStart;
11816
+ const continuationLead = " ".repeat(prefixWidth);
10864
11817
  const decorated = nativeLines.map(
10865
- (line, index) => decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
10866
- firstEnvelope,
10867
- firstHasStart,
10868
- multilineEnvelope,
10869
- prefix,
10870
- prefixWidth
10871
- })
11818
+ (line, index) => decorateMessageLine(
11819
+ line,
11820
+ index,
11821
+ nativeLines.length - 1,
11822
+ firstContentIndex,
11823
+ width,
11824
+ {
11825
+ firstEnvelope,
11826
+ firstHasStart,
11827
+ multilineEnvelope,
11828
+ prefix,
11829
+ prefixWidth,
11830
+ continuationLead
11831
+ },
11832
+ analyses[index]
11833
+ )
10872
11834
  );
10873
- if (!decorated.every((line) => visibleWidth(line) <= width)) return void 0;
10874
- if (!nativeLines.every((line) => getLineAnalysis(line).visibleWidth <= bodyWidth)) return void 0;
11835
+ if (!decorated.every((line) => certifiedVisibleWidth(line) <= width)) return void 0;
11836
+ if (!analyses.every((analysis) => analysis.visibleWidth <= bodyWidth)) return void 0;
10875
11837
  return decorated;
10876
11838
  }
10877
11839
  function decorateMessageRender(original, instance, args, snapshot = {
@@ -10884,7 +11846,7 @@ function decorateMessageRender(original, instance, args, snapshot = {
10884
11846
  const width = typeof args[0] === "number" ? args[0] : 0;
10885
11847
  const prefix = snapshot.assistantPrefix;
10886
11848
  if (!snapshot.assistantEnabled) return Reflect.apply(original, instance, args);
10887
- const prefixWidth = visibleWidth(prefix);
11849
+ const prefixWidth = prefixWidthOf(prefix);
10888
11850
  if (width <= prefixWidth) return Reflect.apply(original, instance, args);
10889
11851
  const reducedWidth = width - prefixWidth;
10890
11852
  const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
@@ -10916,8 +11878,16 @@ function hasToolCallItems(message) {
10916
11878
  function isBlankTextChild(child) {
10917
11879
  const candidate = child;
10918
11880
  if (typeof candidate?.setCustomBgFn !== "function" || typeof candidate.render !== "function") return false;
11881
+ if (typeof candidate.text === "string") return contentText(candidate.text).trim() === "";
10919
11882
  return contentText(candidate.render(0).join("\n")).trim() === "";
10920
11883
  }
11884
+ function childrenUnchangedSinceScan(instance, children) {
11885
+ const state = childrenScanByInstance.get(instance);
11886
+ return state !== void 0 && state.childrenRef === children && state.length === children.length;
11887
+ }
11888
+ function markChildrenScanned(instance, children) {
11889
+ childrenScanByInstance.set(instance, { childrenRef: children, length: children.length });
11890
+ }
10921
11891
  function decorateMessageUpdate(original, instance, args, snapshot = {
10922
11892
  assistantPrefix: "\u2502 ",
10923
11893
  assistantEnabled: true,
@@ -10927,24 +11897,22 @@ function decorateMessageUpdate(original, instance, args, snapshot = {
10927
11897
  if (typeof original !== "function") return void 0;
10928
11898
  const result = Reflect.apply(original, instance, args);
10929
11899
  const target = instance;
10930
- if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10931
- const children = target.contentContainer?.children;
10932
- if (children) {
11900
+ const children = target.contentContainer?.children;
11901
+ if (children && !childrenUnchangedSinceScan(instance, children)) {
11902
+ if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10933
11903
  for (let index = children.length - 1; index >= 0; index--) {
10934
11904
  if (!isBlankTextChild(children[index])) continue;
10935
11905
  children.splice(index, 1);
10936
11906
  if (isSpacerChild(children[index])) children.splice(index, 1);
10937
11907
  }
10938
11908
  }
10939
- }
10940
- if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10941
- const children = target.contentContainer?.children;
10942
- if (children) {
11909
+ if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10943
11910
  for (let index = children.length - 1; index >= 0; index--) {
10944
11911
  if (isInterimTextChild(children[index])) children.splice(index, 1);
10945
11912
  }
10946
11913
  if (children.every((child) => isSpacerChild(child))) children.length = 0;
10947
11914
  }
11915
+ markChildrenScanned(instance, children);
10948
11916
  }
10949
11917
  return result;
10950
11918
  }
@@ -11306,19 +12274,19 @@ function detectPiVersion(resolution = {}) {
11306
12274
  );
11307
12275
  }
11308
12276
  });
11309
- const readFile2 = resolution.readFile ?? ((path) => readFileSync2(path, "utf8"));
12277
+ const readFile4 = resolution.readFile ?? ((path) => readFileSync2(path, "utf8"));
11310
12278
  try {
11311
12279
  const entry = resolvePackageEntry("@earendil-works/pi-coding-agent");
11312
- let directory = dirname3(entry);
12280
+ let directory = dirname4(entry);
11313
12281
  for (; ; ) {
11314
12282
  const packagePath = join3(directory, "package.json");
11315
12283
  try {
11316
- const packageJson = JSON.parse(readFile2(packagePath));
12284
+ const packageJson = JSON.parse(readFile4(packagePath));
11317
12285
  if (packageJson.name === "@earendil-works/pi-coding-agent" && typeof packageJson.version === "string")
11318
12286
  return { version: packageJson.version };
11319
12287
  } catch {
11320
12288
  }
11321
- const parent = dirname3(directory);
12289
+ const parent = dirname4(directory);
11322
12290
  if (parent === directory) break;
11323
12291
  directory = parent;
11324
12292
  }
@@ -11981,6 +12949,11 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
11981
12949
  };
11982
12950
  const applyMessagesConfig = (config) => {
11983
12951
  sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : void 0);
12952
+ setMessagesRenderConfig({
12953
+ showImagePreviews: config.messages.showImagePreviews,
12954
+ clipboardImages: config.messages.clipboardImages,
12955
+ previewMaxWidth: config.messages.previewMaxWidth
12956
+ });
11984
12957
  };
11985
12958
  const applyAutoTheme = (config, ctx) => {
11986
12959
  const target = config.theme.autoApply;
@@ -12040,6 +13013,7 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
12040
13013
  resetBatchRegistry();
12041
13014
  resetGrepRegistry();
12042
13015
  resetBashTreeRegistry();
13016
+ resetPendingImageRegistry();
12043
13017
  resetTurnRegistry();
12044
13018
  rebuildTurnRegistryFromEntries(ctx.sessionManager.getEntries());
12045
13019
  stopAllElapsedTickers();
@@ -12105,6 +13079,7 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
12105
13079
  resetBatchRegistry();
12106
13080
  resetGrepRegistry();
12107
13081
  resetBashTreeRegistry();
13082
+ resetPendingImageRegistry();
12108
13083
  resetTurnRegistry();
12109
13084
  stopAllElapsedTickers();
12110
13085
  app.sessionShutdown();
@@ -12308,6 +13283,12 @@ function piStyleExtension(pi) {
12308
13283
  pi.registerFlag("pi-style-ascii", { type: "boolean", description: "Use ASCII pi-style markers" });
12309
13284
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
12310
13285
  registerPiStyleCommand(pi, coordinator.app);
13286
+ registerImagePreviewSurface(pi);
13287
+ let stagedImagePreview;
13288
+ pi.on("before_agent_start", (event) => {
13289
+ const staged = stageImagePreviewData(event.images ?? []);
13290
+ if (staged) stagedImagePreview = staged;
13291
+ });
12311
13292
  pi.on("session_start", async (event, ctx) => {
12312
13293
  resetUsageFromSessionCache(ctx.sessionManager);
12313
13294
  if (pi.getFlag("pi-style-readonly-tools") === true) {
@@ -12319,7 +13300,7 @@ function piStyleExtension(pi) {
12319
13300
  coordinator.app.runtime.current?.dismissStartup();
12320
13301
  beginAgentRun();
12321
13302
  });
12322
- pi.on("input", (event, _ctx) => {
13303
+ pi.on("input", async (event, _ctx) => {
12323
13304
  coordinator.app.runtime.current?.dismissStartup();
12324
13305
  if (event.source === "interactive") {
12325
13306
  const trimmed = event.text.trimStart();
@@ -12328,6 +13309,15 @@ function piStyleExtension(pi) {
12328
13309
  if (trimmed.slice(bangLength).trim() === "") return { action: "handled" };
12329
13310
  }
12330
13311
  }
13312
+ const markerResult = await resolvePendingImageMarkers(event.text);
13313
+ const pathResult = event.source === "interactive" ? await transformClipboardImages(event.text) : void 0;
13314
+ if (markerResult || pathResult) {
13315
+ return {
13316
+ action: "transform",
13317
+ text: pathResult?.text ?? event.text,
13318
+ images: [...markerResult?.images ?? [], ...pathResult?.images ?? []]
13319
+ };
13320
+ }
12331
13321
  return void 0;
12332
13322
  });
12333
13323
  pi.on("tool_execution_start", () => coordinator.app.runtime.current?.dismissStartup());
@@ -12347,8 +13337,12 @@ function piStyleExtension(pi) {
12347
13337
  "session_info_changed",
12348
13338
  (event) => coordinator.app.update({ sessionName: event.name }, "coalesced", { refreshExtensionStatuses: true })
12349
13339
  );
12350
- pi.on("message_start", () => {
13340
+ pi.on("message_start", (event) => {
12351
13341
  closeActiveBatch();
13342
+ if (stagedImagePreview && event.message?.role === "assistant") {
13343
+ flushImagePreviewEntry(pi, stagedImagePreview);
13344
+ stagedImagePreview = void 0;
13345
+ }
12352
13346
  });
12353
13347
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
12354
13348
  pi.on(
@@ -12363,6 +13357,7 @@ function piStyleExtension(pi) {
12363
13357
  const run = finishAgentRun();
12364
13358
  if (run) {
12365
13359
  invalidateTurnMembers(run);
13360
+ releaseTurnInvalidators(run);
12366
13361
  requestToolPresentationRender();
12367
13362
  }
12368
13363
  });