@quandev104/pi-style 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -2
- package/dist/extensions/pi-style.js +57 -41
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/command-service.ts +0 -2
- package/extension-src/pi-style/domain/config-normalization.ts +0 -3
- package/extension-src/pi-style/domain/config-types.ts +0 -2
- package/extension-src/pi-style/features/messages/image-preview.ts +67 -20
- package/extension-src/pi-style/features/messages/index.ts +0 -44
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +0 -1
- package/extension-src/pi-style/pi/compatibility-probe.ts +1 -2
- package/extension-src/pi-style/pi/index.ts +30 -15
- package/package.json +1 -1
|
@@ -48,7 +48,6 @@ const allowedPaths = new Set([
|
|
|
48
48
|
"messages.assistantPrefix",
|
|
49
49
|
"messages.specialBlocks",
|
|
50
50
|
"messages.hideThinkingLabel",
|
|
51
|
-
"messages.hideInterimText",
|
|
52
51
|
"messages.showImagePreviews",
|
|
53
52
|
"messages.clipboardImages",
|
|
54
53
|
"messages.previewMaxWidth",
|
|
@@ -84,7 +83,6 @@ function validatePathValue(path: string, value: unknown): boolean {
|
|
|
84
83
|
"messages.assistantPrefix",
|
|
85
84
|
"messages.specialBlocks",
|
|
86
85
|
"messages.hideThinkingLabel",
|
|
87
|
-
"messages.hideInterimText",
|
|
88
86
|
"messages.showImagePreviews",
|
|
89
87
|
"messages.clipboardImages",
|
|
90
88
|
"messages.previewMaxWidth",
|
|
@@ -33,7 +33,6 @@ export const DEFAULT_CONFIG: NormalizedPiStyleConfig = Object.freeze({
|
|
|
33
33
|
assistantPrefix: true,
|
|
34
34
|
specialBlocks: true,
|
|
35
35
|
hideThinkingLabel: true,
|
|
36
|
-
hideInterimText: true,
|
|
37
36
|
showImagePreviews: true,
|
|
38
37
|
clipboardImages: true,
|
|
39
38
|
previewMaxWidth: 30,
|
|
@@ -180,7 +179,6 @@ export function normalizeConfig(
|
|
|
180
179
|
assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
|
|
181
180
|
specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
|
|
182
181
|
hideThinkingLabel: bool(messages.hideThinkingLabel, defaults.messages.hideThinkingLabel),
|
|
183
|
-
hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText),
|
|
184
182
|
showImagePreviews: bool(messages.showImagePreviews, defaults.messages.showImagePreviews),
|
|
185
183
|
clipboardImages: bool(messages.clipboardImages, defaults.messages.clipboardImages),
|
|
186
184
|
previewMaxWidth: boundedInt(messages.previewMaxWidth, defaults.messages.previewMaxWidth, 8, 60),
|
|
@@ -244,7 +242,6 @@ const BOOL_PATHS = new Set([
|
|
|
244
242
|
"messages.assistantPrefix",
|
|
245
243
|
"messages.specialBlocks",
|
|
246
244
|
"messages.hideThinkingLabel",
|
|
247
|
-
"messages.hideInterimText",
|
|
248
245
|
"messages.showImagePreviews",
|
|
249
246
|
"messages.clipboardImages",
|
|
250
247
|
"tools.enabled",
|
|
@@ -37,7 +37,6 @@ export interface PiStyleConfig {
|
|
|
37
37
|
assistantPrefix?: boolean;
|
|
38
38
|
specialBlocks?: boolean;
|
|
39
39
|
hideThinkingLabel?: boolean;
|
|
40
|
-
hideInterimText?: boolean;
|
|
41
40
|
/** Inline previews for user-prompt images (ADR 0008); gates append and render. */
|
|
42
41
|
showImagePreviews?: boolean;
|
|
43
42
|
/** Clipboard image input (ADR 0009): upgrade built-in paste temp paths to attachments. */
|
|
@@ -102,7 +101,6 @@ export interface NormalizedPiStyleConfig {
|
|
|
102
101
|
assistantPrefix: boolean;
|
|
103
102
|
specialBlocks: boolean;
|
|
104
103
|
hideThinkingLabel: boolean;
|
|
105
|
-
hideInterimText: boolean;
|
|
106
104
|
/** Inline previews for user-prompt images (ADR 0008); gates append and render. */
|
|
107
105
|
showImagePreviews: boolean;
|
|
108
106
|
/** Clipboard image input (ADR 0009): upgrade built-in paste temp paths to attachments. */
|
|
@@ -21,15 +21,19 @@
|
|
|
21
21
|
// multiple images render side-by-side on kitty-capable terminals (kitty
|
|
22
22
|
// graphics sequences are zero-width, so line zipping composes them), stacked
|
|
23
23
|
// elsewhere. Width is capped by `messages.previewMaxWidth` (default 30);
|
|
24
|
-
//
|
|
24
|
+
// Kitty slots use the actual rendered image width to avoid portrait-image gaps;
|
|
25
|
+
// strongly height-mismatched images stack instead of leaving empty rows. Pi's
|
|
26
|
+
// global expansion (Ctrl+O) lifts the cap for a closer look.
|
|
25
27
|
//
|
|
26
28
|
// Fail-closed rules: unknown/malformed entry data renders zero lines (never
|
|
27
29
|
// an error box, never base64 leakage); a theme without fg disables the
|
|
28
30
|
// surface; the config leaf gates both the stage and the render side.
|
|
29
31
|
|
|
30
32
|
import {
|
|
33
|
+
type CellDimensions,
|
|
31
34
|
type Component,
|
|
32
35
|
getCapabilities,
|
|
36
|
+
getCellDimensions,
|
|
33
37
|
getImageDimensions,
|
|
34
38
|
Image,
|
|
35
39
|
type ImageDimensions,
|
|
@@ -50,6 +54,8 @@ export const IMAGE_PREVIEW_MAX_WIDTH_CELLS = 60;
|
|
|
50
54
|
/** Grid layout constants (kitty side-by-side). */
|
|
51
55
|
const GRID_GAP = 2;
|
|
52
56
|
const GRID_MIN_COLUMN = 14;
|
|
57
|
+
const GRID_MAX_HEIGHT_RATIO = 1.5;
|
|
58
|
+
const FALLBACK_IMAGE_DIMENSIONS: ImageDimensions = { widthPx: 800, heightPx: 600 };
|
|
53
59
|
|
|
54
60
|
/** Persisted entry payload: base64 data + mime type per attached image. */
|
|
55
61
|
export interface ImagePreviewImage {
|
|
@@ -132,6 +138,7 @@ function parseImageDimensions(image: ImagePreviewImage): ImageDimensions | undef
|
|
|
132
138
|
/** Per-entry render artifacts (components, labels). */
|
|
133
139
|
interface PreviewCache {
|
|
134
140
|
readonly images: readonly ImagePreviewImage[];
|
|
141
|
+
readonly dimensions: readonly (ImageDimensions | undefined)[];
|
|
135
142
|
readonly components: Image[];
|
|
136
143
|
readonly labels: string[];
|
|
137
144
|
}
|
|
@@ -172,10 +179,10 @@ export function renderImagePreviewEntry(entry: unknown, options: unknown, theme:
|
|
|
172
179
|
),
|
|
173
180
|
);
|
|
174
181
|
const labels = images.map((_image, index) => imageLabel(themed, index + 1, dims[index]));
|
|
175
|
-
cached = { images, components, labels };
|
|
182
|
+
cached = { images, dimensions: dims, components, labels };
|
|
176
183
|
previewCache.set(entry as object, cached);
|
|
177
184
|
}
|
|
178
|
-
const { components, labels } = cached;
|
|
185
|
+
const { components, dimensions, labels } = cached;
|
|
179
186
|
// Memoized output: render passes tick on every streaming update; the
|
|
180
187
|
// same width/expansion/config/capability key returns the SAME array
|
|
181
188
|
// reference, skipping the grid re-zip and the host's per-line diff.
|
|
@@ -196,7 +203,7 @@ export function renderImagePreviewEntry(entry: unknown, options: unknown, theme:
|
|
|
196
203
|
let lines: string[];
|
|
197
204
|
if (width <= 0) lines = [];
|
|
198
205
|
else if (images.length === 1 || !kitty) lines = renderStacked(width, maxWidth, labels, components);
|
|
199
|
-
else lines = renderGrid(width, maxWidth, labels, components);
|
|
206
|
+
else lines = renderGrid(width, maxWidth, labels, components, dimensions);
|
|
200
207
|
memo = { key, lines };
|
|
201
208
|
return lines;
|
|
202
209
|
},
|
|
@@ -230,9 +237,17 @@ function stripTrailingSpaces(line: string): string {
|
|
|
230
237
|
/**
|
|
231
238
|
* Side-by-side grid (kitty graphics only — sequences are zero-width so
|
|
232
239
|
* per-row line zipping composes columns). Columns shrink to fit the width;
|
|
233
|
-
*
|
|
240
|
+
* compact slots follow actual image widths; strongly height-mismatched groups
|
|
241
|
+
* degrade to stacked. When fewer than two usable columns fit, this also
|
|
242
|
+
* degrades to stacked.
|
|
234
243
|
*/
|
|
235
|
-
function renderGrid(
|
|
244
|
+
function renderGrid(
|
|
245
|
+
width: number,
|
|
246
|
+
maxWidth: number,
|
|
247
|
+
labels: string[],
|
|
248
|
+
components: Image[],
|
|
249
|
+
dimensions: readonly (ImageDimensions | undefined)[],
|
|
250
|
+
): string[] {
|
|
236
251
|
const usable = Math.max(1, width - 2);
|
|
237
252
|
const maxColumns = Math.floor((usable + GRID_GAP) / (GRID_MIN_COLUMN + GRID_GAP));
|
|
238
253
|
const columns = Math.max(1, Math.min(components.length, maxColumns, 3));
|
|
@@ -241,36 +256,48 @@ function renderGrid(width: number, maxWidth: number, labels: string[], component
|
|
|
241
256
|
if (columnWidth < GRID_MIN_COLUMN) return renderStacked(width, maxWidth, labels, components);
|
|
242
257
|
|
|
243
258
|
const lines: string[] = [];
|
|
259
|
+
const cellDimensions = getCellDimensions();
|
|
244
260
|
for (let start = 0; start < components.length; start += columns) {
|
|
245
261
|
const group = components.slice(start, start + columns);
|
|
246
262
|
const groupLabels = labels.slice(start, start + columns);
|
|
263
|
+
const groupDimensions = dimensions.slice(start, start + columns);
|
|
247
264
|
if (start > 0) lines.push("");
|
|
248
265
|
const rendered = group.map((component) => component.render(columnWidth));
|
|
249
|
-
|
|
266
|
+
const sizes = groupDimensions.map((imageDimensions) => imageCellSize(imageDimensions, columnWidth, cellDimensions));
|
|
267
|
+
|
|
268
|
+
// Avoid keeping a short neighbor's column alive for a much taller image.
|
|
269
|
+
// Similar screenshots remain side-by-side; strongly mismatched images stack.
|
|
270
|
+
const rowCounts = rendered.map((column) => column.length).filter((count) => count > 0);
|
|
271
|
+
const minRows = Math.min(...rowCounts);
|
|
272
|
+
const maxRows = Math.max(...rowCounts);
|
|
273
|
+
if (rowCounts.length > 1 && minRows > 0 && maxRows >= minRows * GRID_MAX_HEIGHT_RATIO) {
|
|
274
|
+
return renderStacked(width, maxWidth, labels, components);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Use actual image widths for the slots instead of the shared max width.
|
|
278
|
+
// This removes unused horizontal space around narrow portrait images.
|
|
279
|
+
const slotWidths = groupLabels.map((label, index) =>
|
|
280
|
+
Math.max(sizes[index]?.columns ?? 1, visibleWidth(stripFormatting(label))),
|
|
281
|
+
);
|
|
250
282
|
const labelRow = groupLabels
|
|
251
|
-
.map((label,
|
|
252
|
-
const pad = Math.max(0,
|
|
283
|
+
.map((label, index) => {
|
|
284
|
+
const pad = Math.max(0, (slotWidths[index] ?? 0) - visibleWidth(stripFormatting(label)));
|
|
253
285
|
return label + " ".repeat(pad);
|
|
254
286
|
})
|
|
255
287
|
.join(" ".repeat(GRID_GAP))
|
|
256
288
|
.trimEnd();
|
|
257
289
|
lines.push(labelRow);
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// second transmission would land ON TOP of image 1. Each subsequent
|
|
262
|
-
// column therefore starts with a CHA jump (`ESC[<col>G`, 1-based) to its
|
|
263
|
-
// start column; terminal/herdr cursor tracking follows, and both images
|
|
264
|
-
// composite side by side.
|
|
290
|
+
|
|
291
|
+
// Kitty sequences are zero-width, so each image starts with a CHA jump.
|
|
292
|
+
// The offset is based on cumulative compact slot widths, not columnWidth.
|
|
265
293
|
const rows = Math.max(...rendered.map((column) => column.length));
|
|
266
294
|
for (let row = 0; row < rows; row++) {
|
|
267
295
|
let line = "";
|
|
296
|
+
let startCol = 0;
|
|
268
297
|
for (let column = 0; column < rendered.length; column++) {
|
|
269
|
-
if (column > 0) {
|
|
270
|
-
const startCol = column * (columnWidth + GRID_GAP);
|
|
271
|
-
line += `\x1b[${startCol + 1}G`;
|
|
272
|
-
}
|
|
298
|
+
if (column > 0) line += `\x1b[${startCol + 1}G`;
|
|
273
299
|
line += rendered[column]?.[row] ?? "";
|
|
300
|
+
startCol += (slotWidths[column] ?? 0) + GRID_GAP;
|
|
274
301
|
}
|
|
275
302
|
lines.push(stripTrailingSpaces(line));
|
|
276
303
|
}
|
|
@@ -278,6 +305,26 @@ function renderGrid(width: number, maxWidth: number, labels: string[], component
|
|
|
278
305
|
return lines;
|
|
279
306
|
}
|
|
280
307
|
|
|
308
|
+
/** Match pi-tui Image.render's Kitty cell-size calculation for slot sizing. */
|
|
309
|
+
function imageCellSize(
|
|
310
|
+
dimensions: ImageDimensions | undefined,
|
|
311
|
+
requestedWidth: number,
|
|
312
|
+
cellDimensions: CellDimensions,
|
|
313
|
+
): { columns: number; rows: number } {
|
|
314
|
+
const imageDimensions = dimensions ?? FALLBACK_IMAGE_DIMENSIONS;
|
|
315
|
+
const maxWidth = Math.max(1, Math.min(requestedWidth - 2, 60));
|
|
316
|
+
const maxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));
|
|
317
|
+
const imageWidth = Math.max(1, imageDimensions.widthPx);
|
|
318
|
+
const imageHeight = Math.max(1, imageDimensions.heightPx);
|
|
319
|
+
const widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth;
|
|
320
|
+
const heightScale = (maxHeight * cellDimensions.heightPx) / imageHeight;
|
|
321
|
+
const scale = Math.min(widthScale, heightScale);
|
|
322
|
+
return {
|
|
323
|
+
columns: Math.max(1, Math.min(maxWidth, Math.ceil((imageWidth * scale) / cellDimensions.widthPx))),
|
|
324
|
+
rows: Math.max(1, Math.min(maxHeight, Math.ceil((imageHeight * scale) / cellDimensions.heightPx))),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
281
328
|
/** Label text may carry ANSI color; measure only the visible part.
|
|
282
329
|
* Reuses the shared ANSI-stripping utility (no local control-char regex). */
|
|
283
330
|
import { stripAnsi as stripFormatting } from "../../shared/ansi.js";
|
|
@@ -507,8 +507,6 @@ export type MessageDecorationSnapshot = Readonly<{
|
|
|
507
507
|
assistantEnabled: boolean;
|
|
508
508
|
/** Drop the hidden-thinking label row and its trailing spacer (zero-trace collapse). */
|
|
509
509
|
collapseHiddenThinking: boolean;
|
|
510
|
-
/** Hide the text of assistant messages that also carry tool calls (interim narration). */
|
|
511
|
-
hideInterimText: boolean;
|
|
512
510
|
}>;
|
|
513
511
|
|
|
514
512
|
export function __getMessageDecorationTestState(): Readonly<MessageDecorationTestState> {
|
|
@@ -535,7 +533,6 @@ export function decorateMessageRender(
|
|
|
535
533
|
assistantPrefix: "│ ",
|
|
536
534
|
assistantEnabled: true,
|
|
537
535
|
collapseHiddenThinking: false,
|
|
538
|
-
hideInterimText: false,
|
|
539
536
|
},
|
|
540
537
|
): unknown {
|
|
541
538
|
if (typeof original !== "function") return undefined;
|
|
@@ -565,34 +562,6 @@ function isSpacerChild(child: unknown): boolean {
|
|
|
565
562
|
return typeof (child as { setLines?: unknown } | undefined)?.setLines === "function";
|
|
566
563
|
}
|
|
567
564
|
|
|
568
|
-
/**
|
|
569
|
-
* The assistant TEXT Markdown block: `Markdown(text, pad, 0, theme, undefined,
|
|
570
|
-
* { transform })` — duck-typed by setText + options object + no defaultTextStyle.
|
|
571
|
-
* Thinking Markdown passes a `{ color, italic }` defaultTextStyle and is
|
|
572
|
-
* therefore excluded; plain Text components carry no `options`.
|
|
573
|
-
*/
|
|
574
|
-
function isInterimTextChild(child: unknown): boolean {
|
|
575
|
-
const candidate = child as { setText?: unknown; options?: unknown; defaultTextStyle?: unknown } | undefined;
|
|
576
|
-
return (
|
|
577
|
-
typeof candidate?.setText === "function" &&
|
|
578
|
-
candidate.options !== undefined &&
|
|
579
|
-
typeof candidate.options === "object" &&
|
|
580
|
-
candidate.defaultTextStyle === undefined
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/** Whether the message content includes a toolCall item (interim narration marker). */
|
|
585
|
-
function hasToolCallItems(message: unknown): boolean {
|
|
586
|
-
if (!message || typeof message !== "object") return false;
|
|
587
|
-
const content = (message as { content?: unknown }).content;
|
|
588
|
-
return (
|
|
589
|
-
Array.isArray(content) &&
|
|
590
|
-
content.some(
|
|
591
|
-
(item) => item !== null && typeof item === "object" && (item as { type?: unknown }).type === "toolCall",
|
|
592
|
-
)
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
565
|
/**
|
|
597
566
|
* The hidden-thinking placeholder: a Text (setCustomBgFn) whose ANSI-stripped
|
|
598
567
|
* rendered content is empty. Duck-typed on the public shape because
|
|
@@ -652,7 +621,6 @@ export function decorateMessageUpdate(
|
|
|
652
621
|
assistantPrefix: "│ ",
|
|
653
622
|
assistantEnabled: true,
|
|
654
623
|
collapseHiddenThinking: false,
|
|
655
|
-
hideInterimText: false,
|
|
656
624
|
},
|
|
657
625
|
): unknown {
|
|
658
626
|
if (typeof original !== "function") return undefined;
|
|
@@ -675,18 +643,6 @@ export function decorateMessageUpdate(
|
|
|
675
643
|
if (isSpacerChild(children[index])) children.splice(index, 1);
|
|
676
644
|
}
|
|
677
645
|
}
|
|
678
|
-
// Interim narration: assistant messages that carry tool calls use their text
|
|
679
|
-
// only to narrate while working; the tool blocks tell the story. Hide the text
|
|
680
|
-
// so the feed shows the run's summary and the final answer. Deterministic per
|
|
681
|
-
// content — streaming, scroll-back, and resume behave identically. Errors and
|
|
682
|
-
// truncation notices are Text children and stay; if only spacers remain the
|
|
683
|
-
// message becomes zero-trace.
|
|
684
|
-
if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
|
|
685
|
-
for (let index = children.length - 1; index >= 0; index--) {
|
|
686
|
-
if (isInterimTextChild(children[index])) children.splice(index, 1);
|
|
687
|
-
}
|
|
688
|
-
if (children.every((child) => isSpacerChild(child))) children.length = 0;
|
|
689
|
-
}
|
|
690
646
|
markChildrenScanned(instance, children);
|
|
691
647
|
}
|
|
692
648
|
return result;
|
|
@@ -168,7 +168,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
168
168
|
assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
|
|
169
169
|
assistantEnabled,
|
|
170
170
|
collapseHiddenThinking: thinkingCollapseEnabled,
|
|
171
|
-
hideInterimText: config.messages.hideInterimText && messagesEnabled,
|
|
172
171
|
},
|
|
173
172
|
toolSnapshot: {
|
|
174
173
|
callMarker: authorization.ascii ? "[tool] " : "[tool] ",
|
|
@@ -374,7 +374,6 @@ export interface CompatibilityProbeOptions {
|
|
|
374
374
|
assistantPrefix: boolean;
|
|
375
375
|
specialBlocks: boolean;
|
|
376
376
|
hideThinkingLabel: boolean;
|
|
377
|
-
hideInterimText: boolean;
|
|
378
377
|
};
|
|
379
378
|
tools: { enabled: boolean; style: string; maxCollapsedLines: number; maxExpandedLines: number; dimOutput: boolean };
|
|
380
379
|
preset: string;
|
|
@@ -423,7 +422,7 @@ function surfaceDisabled(spec: TargetSpec, config: CompatibilityProbeOptions["co
|
|
|
423
422
|
if (!config.messages.enabled) return true;
|
|
424
423
|
if (spec.subtype === "native-assistant-message" && spec.method === "render") return !config.messages.assistantPrefix;
|
|
425
424
|
if (spec.subtype === "native-assistant-message" && spec.method === "updateContent")
|
|
426
|
-
return !config.messages.hideThinkingLabel
|
|
425
|
+
return !config.messages.hideThinkingLabel;
|
|
427
426
|
if (isSpecialBlock(spec)) return !config.messages.specialBlocks;
|
|
428
427
|
return true;
|
|
429
428
|
}
|
|
@@ -76,21 +76,20 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
|
76
76
|
// entries whose customType has no renderer — and once at extension load is
|
|
77
77
|
// enough (persisted entries from previous sessions render on resume).
|
|
78
78
|
registerImagePreviewSurface(pi);
|
|
79
|
-
// User-prompt image previews (ADR 0008):
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// the session file records user → preview → assistant for identical resume.
|
|
88
|
-
// Edge: a steered prompt arriving before the first flush overwrites the
|
|
89
|
-
// staged slot (last write wins) — steer-with-image is a rare corner.
|
|
79
|
+
// User-prompt image previews (ADR 0008): the prompt's `images` are only
|
|
80
|
+
// available at `before_agent_start`, but appending there would place the entry
|
|
81
|
+
// above the user message because extension handlers run before the UI handles
|
|
82
|
+
// `message_start(user)`. Capture there, then flush on the next event-loop turn
|
|
83
|
+
// after `message_start(user)` so the preview appears immediately below it.
|
|
84
|
+
// The assistant-start handler is a synchronous fallback for runtimes that begin
|
|
85
|
+
// the assistant before the deferred flush runs. A steered prompt arriving before
|
|
86
|
+
// flush replaces the staged slot (last write wins).
|
|
90
87
|
let stagedImagePreview: ImagePreviewEntryData | undefined;
|
|
88
|
+
let previewFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
91
89
|
pi.on("before_agent_start", (event) => {
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Always replace the slot: a subsequent image-less prompt must not
|
|
91
|
+
// accidentally flush the previous prompt's preview.
|
|
92
|
+
stagedImagePreview = stageImagePreviewData(event.images ?? []);
|
|
94
93
|
});
|
|
95
94
|
pi.on("session_start", async (event, ctx) => {
|
|
96
95
|
resetUsageFromSessionCache(ctx.sessionManager);
|
|
@@ -163,9 +162,25 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
|
163
162
|
// A new message is a batch boundary: quiet-tool (read/ls/find) calls of the
|
|
164
163
|
// new message start a fresh batch instead of joining the previous one.
|
|
165
164
|
closeActiveBatch();
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
if (event.message?.role === "user" && stagedImagePreview && !previewFlushTimer) {
|
|
166
|
+
// Extension handlers run before Pi's interactive listener adds the user
|
|
167
|
+
// message to the chat. Defer one turn so the entry is appended below the
|
|
168
|
+
// visible user message, but do not wait for assistant content to arrive.
|
|
169
|
+
const pending = stagedImagePreview;
|
|
170
|
+
previewFlushTimer = setTimeout(() => {
|
|
171
|
+
previewFlushTimer = undefined;
|
|
172
|
+
if (stagedImagePreview !== pending) return;
|
|
173
|
+
flushImagePreviewEntry(pi, pending);
|
|
174
|
+
stagedImagePreview = undefined;
|
|
175
|
+
}, 0);
|
|
176
|
+
}
|
|
177
|
+
// Fallback for runtimes that start the assistant before the deferred turn
|
|
178
|
+
// runs. This preserves the ordering below the user and never duplicates.
|
|
168
179
|
if (stagedImagePreview && event.message?.role === "assistant") {
|
|
180
|
+
if (previewFlushTimer) {
|
|
181
|
+
clearTimeout(previewFlushTimer);
|
|
182
|
+
previewFlushTimer = undefined;
|
|
183
|
+
}
|
|
169
184
|
flushImagePreviewEntry(pi, stagedImagePreview);
|
|
170
185
|
stagedImagePreview = undefined;
|
|
171
186
|
}
|