@oh-my-pi/pi-tui 18.4.0 → 18.4.2
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 +42 -0
- package/dist/types/chat/image-loading.d.ts +24 -0
- package/dist/types/chat/transcript-entry.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +24 -3
- package/dist/types/overlays/agents-hub.d.ts +7 -4
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +4 -3
- package/dist/types/overlays/usage-dashboard.d.ts +9 -2
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/composer-cache.d.ts +34 -16
- package/dist/types/prompt/composer.d.ts +15 -18
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/prompt/welcome.d.ts +5 -2
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/status-line/component.d.ts +13 -4
- package/dist/types/status-line/metrics.d.ts +0 -1
- package/dist/types/status-line/startup.d.ts +38 -0
- package/dist/types/status-line/types.d.ts +0 -2
- package/dist/types/terminal-capabilities.d.ts +10 -6
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tools/output-meta.d.ts +2 -1
- package/dist/types/tools/vibe.d.ts +4 -0
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +27 -21
- package/src/chat/image-loading.ts +81 -0
- package/src/chat/tool-execution.ts +53 -17
- package/src/chat/transcript-entry.ts +16 -0
- package/src/chrome/transcript-container.ts +282 -86
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +25 -9
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/agents-hub.ts +7 -18
- package/src/overlays/copy-selector.ts +8 -31
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/model-hub.ts +49 -16
- package/src/overlays/rewind-selector.ts +60 -13
- package/src/overlays/usage-dashboard.ts +47 -6
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer-cache.ts +217 -196
- package/src/prompt/composer.ts +45 -72
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/prompt/welcome.ts +31 -28
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +286 -182
- package/src/status-line/metrics.ts +3 -21
- package/src/status-line/segments.ts +68 -79
- package/src/status-line/startup.ts +181 -0
- package/src/status-line/types.ts +0 -2
- package/src/terminal-capabilities.ts +12 -6
- package/src/terminal.ts +75 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tools/output-meta.ts +11 -3
- package/src/tools/vibe.ts +15 -2
- package/src/tui.ts +208 -154
- package/src/utils.ts +15 -0
|
@@ -49,7 +49,7 @@ export declare class Theme {
|
|
|
49
49
|
* Theme-derived inputs for `getSessionAccentHex`: the accent hex whose
|
|
50
50
|
* OKLCH weight session accents adopt, the major colors they must not
|
|
51
51
|
* hue-collide with, and the light-theme surface luminance to contrast
|
|
52
|
-
* against.
|
|
52
|
+
* against. Built once per instance and frozen; callers share the object.
|
|
53
53
|
*/
|
|
54
54
|
get sessionAccentInputs(): SessionAccentTheme;
|
|
55
55
|
fg(color: ThemeColor, text: string): string;
|
|
@@ -73,9 +73,10 @@ export interface LimitsMeta {
|
|
|
73
73
|
reached: number;
|
|
74
74
|
suggestion: number;
|
|
75
75
|
};
|
|
76
|
+
/** `suggestion` is omitted when the tool is already at its hard cap, so no larger usable limit exists to advise. */
|
|
76
77
|
resultLimit?: {
|
|
77
78
|
reached: number;
|
|
78
|
-
suggestion
|
|
79
|
+
suggestion?: number;
|
|
79
80
|
};
|
|
80
81
|
headLimit?: {
|
|
81
82
|
reached: number;
|
|
@@ -8,6 +8,8 @@ export interface VibeToolDetails {
|
|
|
8
8
|
op: VibeOp;
|
|
9
9
|
/** Live TV-wall snapshot of the owner's worker sessions at (or during) the call. */
|
|
10
10
|
screens: VibeScreenSnapshot[];
|
|
11
|
+
/** Sessions the director killed, left off `screens`; shown as a count. */
|
|
12
|
+
hiddenKilled?: string[];
|
|
11
13
|
spawned?: {
|
|
12
14
|
id: string;
|
|
13
15
|
cli: VibeCli;
|
|
@@ -38,6 +40,8 @@ export interface VibeScreenSnapshot {
|
|
|
38
40
|
id: string;
|
|
39
41
|
cli: VibeCli;
|
|
40
42
|
state: VibeSessionState;
|
|
43
|
+
/** Terminated by the director (`vibe_kill`) or mode teardown; unset for workers that died on their own. */
|
|
44
|
+
killed?: boolean;
|
|
41
45
|
model?: string;
|
|
42
46
|
turns: number;
|
|
43
47
|
queued: number;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -273,8 +273,9 @@ export declare class Container implements Component {
|
|
|
273
273
|
/**
|
|
274
274
|
* Merge runs of byte-adjacent SGR sequences (`CSI [0-9;:]* m`) into one. Only
|
|
275
275
|
* CSI-SGR sequences are touched; text, cursor moves, OSC, hyperlinks and image
|
|
276
|
-
* payloads pass through verbatim.
|
|
277
|
-
*
|
|
276
|
+
* payloads pass through verbatim. Isolated sequences require no parameter
|
|
277
|
+
* slices or arrays; output is built only at merged boundaries or empty resets
|
|
278
|
+
* that need normalization within an adjacent run.
|
|
278
279
|
*/
|
|
279
280
|
export declare function coalesceAdjacentSgr(line: string): string;
|
|
280
281
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.4.
|
|
4
|
+
"version": "18.4.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.4.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.4.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.4.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.4.
|
|
44
|
+
"@oh-my-pi/omptype": "18.4.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.4.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.4.2",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.4.2",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.4.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.4.2",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.4.2",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.4.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -12,7 +12,7 @@ import type { AssistantThinkingRenderer } from "./extension-types";
|
|
|
12
12
|
import { ensureThemeSync, getMarkdownTheme, theme } from "../theme";
|
|
13
13
|
import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
|
|
14
14
|
import { WidthAwareText } from "../render";
|
|
15
|
-
import {
|
|
15
|
+
import { cachedPngConversion, convertImageToPngShared, imagePayloadKey } from "./image-loading";
|
|
16
16
|
import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "./thinking-display";
|
|
17
17
|
import { resolveAssistantErrorPresentation } from "./transcript-render-helpers";
|
|
18
18
|
import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
|
|
@@ -186,10 +186,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
186
186
|
#lastMessage?: AssistantMessage;
|
|
187
187
|
#emergencyText?: Markdown;
|
|
188
188
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
189
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Payload keys ({@link imagePayloadKey}) whose Kitty PNG conversion this
|
|
191
|
+
* component already awaits, so a re-delivered image neither re-encodes nor
|
|
192
|
+
* schedules a second {@link updateContent} cascade. The conversions
|
|
193
|
+
* themselves are shared process-wide by {@link convertImageToPngShared}.
|
|
194
|
+
*/
|
|
195
|
+
#kittyConversionsAwaited = new Set<string>();
|
|
196
|
+
/**
|
|
197
|
+
* Conversions this component displays, held so a later re-render (theme
|
|
198
|
+
* invalidation) still finds them after the bounded shared cache evicts them.
|
|
199
|
+
*/
|
|
200
|
+
#kittyConverted = new Map<string, ImageContent>();
|
|
190
201
|
#showImages = true;
|
|
191
202
|
#showToolResultImages = true;
|
|
192
|
-
#kittyConversionsInFlight = new Set<string>();
|
|
193
203
|
#transcriptBlockFinalized: boolean;
|
|
194
204
|
/** See {@link setMidStreamPublication}; the wire's `stream-revision` axis decides it. */
|
|
195
205
|
#midStreamPublication = true;
|
|
@@ -832,16 +842,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
832
842
|
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
833
843
|
if (!toolCallId) return;
|
|
834
844
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
835
|
-
for (const key of Array.from(this.#convertedKittyImages.keys())) {
|
|
836
|
-
if (key.startsWith(`${toolCallId}:`)) {
|
|
837
|
-
this.#convertedKittyImages.delete(key);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
for (const key of Array.from(this.#kittyConversionsInFlight)) {
|
|
841
|
-
if (key.startsWith(`${toolCallId}:`)) {
|
|
842
|
-
this.#kittyConversionsInFlight.delete(key);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
845
|
if (validImages.length === 0) {
|
|
846
846
|
this.#toolImagesByCallId.delete(toolCallId);
|
|
847
847
|
} else {
|
|
@@ -855,21 +855,27 @@ export class AssistantMessageComponent extends Container {
|
|
|
855
855
|
|
|
856
856
|
#convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
|
|
857
857
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
858
|
-
for (const { image
|
|
858
|
+
for (const { image } of entries) {
|
|
859
859
|
if (image.mimeType === "image/png") continue;
|
|
860
|
-
|
|
861
|
-
this.#
|
|
862
|
-
|
|
860
|
+
const key = imagePayloadKey(image);
|
|
861
|
+
if (this.#kittyConverted.has(key)) continue;
|
|
862
|
+
const cached = cachedPngConversion(image);
|
|
863
|
+
if (cached) {
|
|
864
|
+
this.#kittyConverted.set(key, cached);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (this.#kittyConversionsAwaited.has(key)) continue;
|
|
868
|
+
this.#kittyConversionsAwaited.add(key);
|
|
869
|
+
convertImageToPngShared(image)
|
|
863
870
|
.then(converted => {
|
|
864
|
-
this.#
|
|
865
|
-
this.#convertedKittyImages.set(key, converted);
|
|
871
|
+
this.#kittyConverted.set(key, converted);
|
|
866
872
|
if (this.#lastMessage) {
|
|
867
873
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
868
874
|
}
|
|
869
875
|
this.#onImageUpdate?.();
|
|
870
876
|
})
|
|
871
877
|
.catch(() => {
|
|
872
|
-
this.#
|
|
878
|
+
this.#kittyConversionsAwaited.delete(key);
|
|
873
879
|
});
|
|
874
880
|
}
|
|
875
881
|
}
|
|
@@ -882,7 +888,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
882
888
|
for (const { image, key } of entries) {
|
|
883
889
|
const displayImage =
|
|
884
890
|
TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
|
|
885
|
-
? this.#
|
|
891
|
+
? this.#kittyConverted.get(imagePayloadKey(image))
|
|
886
892
|
: image;
|
|
887
893
|
if (TERMINAL.imageProtocol && displayImage) {
|
|
888
894
|
this.#contentContainer.addChild(
|
|
@@ -110,6 +110,87 @@ export async function convertImageToPng(image: ImageContent): Promise<ImageConte
|
|
|
110
110
|
return { ...image, data, mimeType: "image/png" };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Byte ceiling for {@link convertImageToPngShared}'s resident conversions.
|
|
115
|
+
* Converted PNGs are larger than the webp/jpeg they came from, so the cache is
|
|
116
|
+
* bounded and evicts least-recently-used entries rather than growing with the
|
|
117
|
+
* session's image history.
|
|
118
|
+
*/
|
|
119
|
+
const PNG_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
120
|
+
|
|
121
|
+
/** Least-recently-used first; {@link touchPngCache} re-inserts on hit. */
|
|
122
|
+
const pngCache = new Map<string, ImageContent>();
|
|
123
|
+
let pngCacheBytes = 0;
|
|
124
|
+
const pngConversionsInFlight = new Map<string, Promise<ImageContent>>();
|
|
125
|
+
|
|
126
|
+
/** Cache lookup that also marks `key` as most recently used. */
|
|
127
|
+
function touchPngCache(key: string): ImageContent | undefined {
|
|
128
|
+
const hit = pngCache.get(key);
|
|
129
|
+
if (!hit) return undefined;
|
|
130
|
+
pngCache.delete(key);
|
|
131
|
+
pngCache.set(key, hit);
|
|
132
|
+
return hit;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Content-addressed identity of an image payload, stable across components and
|
|
137
|
+
* transcript rebuilds. Callers key their own per-image state by this instead of
|
|
138
|
+
* positional ids like `${toolCallId}:${index}`, which go stale when the images
|
|
139
|
+
* behind a position are replaced.
|
|
140
|
+
*/
|
|
141
|
+
export function imagePayloadKey(image: ImageContent): string {
|
|
142
|
+
return `${image.mimeType}:${image.data.length}:${Bun.hash(image.data)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The PNG conversion of `image` when one is still resident, else `undefined`.
|
|
147
|
+
* Synchronous so renderers can use an already-converted image on the spot
|
|
148
|
+
* instead of scheduling another async re-render.
|
|
149
|
+
*/
|
|
150
|
+
export function cachedPngConversion(image: ImageContent): ImageContent | undefined {
|
|
151
|
+
return touchPngCache(imagePayloadKey(image));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Converts `image` to PNG at most once per distinct payload: concurrent callers
|
|
156
|
+
* share one in-flight conversion and later callers hit {@link pngCache}.
|
|
157
|
+
* Kitty-graphics renderers use this because `convertImageToPng` is a full
|
|
158
|
+
* decode plus re-encode, and the same image is delivered repeatedly (read-result
|
|
159
|
+
* replay) and rebuilt from scratch on resume, rewind, and `/tree` navigation.
|
|
160
|
+
*
|
|
161
|
+
* Rejections are not cached — a payload that failed to decode is retried by the
|
|
162
|
+
* next caller, matching {@link convertImageToPng}'s contract.
|
|
163
|
+
*/
|
|
164
|
+
export function convertImageToPngShared(image: ImageContent): Promise<ImageContent> {
|
|
165
|
+
const key = imagePayloadKey(image);
|
|
166
|
+
const cached = touchPngCache(key);
|
|
167
|
+
if (cached) return Promise.resolve(cached);
|
|
168
|
+
const running = pngConversionsInFlight.get(key);
|
|
169
|
+
if (running) return running;
|
|
170
|
+
const conversion = convertImageToPng(image)
|
|
171
|
+
.then(({ data }) => {
|
|
172
|
+
pngConversionsInFlight.delete(key);
|
|
173
|
+
// Only the payload is shared; caller-specific fields (`url`,
|
|
174
|
+
// `providerFile`, `detail`) describe the source image, not this PNG.
|
|
175
|
+
const converted: ImageContent = { type: "image", data, mimeType: "image/png" };
|
|
176
|
+
pngCache.set(key, converted);
|
|
177
|
+
pngCacheBytes += data.length;
|
|
178
|
+
for (const [oldest, entry] of pngCache) {
|
|
179
|
+
if (pngCacheBytes <= PNG_CACHE_MAX_BYTES) break;
|
|
180
|
+
if (oldest === key) continue;
|
|
181
|
+
pngCache.delete(oldest);
|
|
182
|
+
pngCacheBytes -= entry.data.length;
|
|
183
|
+
}
|
|
184
|
+
return converted;
|
|
185
|
+
})
|
|
186
|
+
.catch(error => {
|
|
187
|
+
pngConversionsInFlight.delete(key);
|
|
188
|
+
throw error;
|
|
189
|
+
});
|
|
190
|
+
pngConversionsInFlight.set(key, conversion);
|
|
191
|
+
return conversion;
|
|
192
|
+
}
|
|
193
|
+
|
|
113
194
|
export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
|
|
114
195
|
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
|
115
196
|
return image;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
1
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
3
|
import { Box } from "../components/box";
|
|
3
4
|
import { SPINNER_ADVANCE_MS } from "../components/loader";
|
|
@@ -26,7 +27,7 @@ import { isWaitingPollDetails } from "../tools/wait";
|
|
|
26
27
|
import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../render/render-utils";
|
|
27
28
|
import type { XdevMountedState } from "../tools/xdev";
|
|
28
29
|
import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../render/index";
|
|
29
|
-
import {
|
|
30
|
+
import { cachedPngConversion, convertImageToPngShared, imagePayloadKey } from "./image-loading";
|
|
30
31
|
import { sanitizeWithOptionalSixelPassthrough } from "../render/sixel";
|
|
31
32
|
import { renderDiff } from "../chrome/diff";
|
|
32
33
|
import { type AnimationFrame, trimBlankEdges } from "../chrome/transcript-container";
|
|
@@ -284,6 +285,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
284
285
|
// so a terminal resize re-shapes image-bearing results to rescale them without
|
|
285
286
|
// forcing the common image-free result to re-shape on every resize tick.
|
|
286
287
|
#renderedImageCount = 0;
|
|
288
|
+
// `stateBgKey|themeEpoch` of the tint last handed to #contentText. Re-tinting
|
|
289
|
+
// drops its wrap cache, so a rebuild whose tint is unchanged skips it and the
|
|
290
|
+
// inner Text re-wraps only when the reformatted card text actually differs.
|
|
291
|
+
#contentTextBgKey: string | undefined;
|
|
292
|
+
// Memoized #getTextOutput(), keyed by every input it reads: the result
|
|
293
|
+
// (versioned by #resultVersion), #showImages, and the image protocol.
|
|
294
|
+
#textOutput = "";
|
|
295
|
+
#textOutputKey: string | undefined;
|
|
287
296
|
#tool?: AgentTool;
|
|
288
297
|
#renderer?: ToolRenderer;
|
|
289
298
|
#ui: ToolExecutionUi;
|
|
@@ -296,8 +305,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
296
305
|
#editMode?: EditMode;
|
|
297
306
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
298
307
|
#previewReady?: PromiseWithResolvers<void>;
|
|
299
|
-
//
|
|
300
|
-
|
|
308
|
+
// Payload keys whose Kitty PNG conversion is already awaited; the converted
|
|
309
|
+
// images themselves live in the process-wide cache behind
|
|
310
|
+
// `convertImageToPngShared`, so rebuilt components reuse them.
|
|
311
|
+
#kittyConversionsAwaited = new Set<string>();
|
|
312
|
+
// Conversions this component displays, held so a later re-render still finds
|
|
313
|
+
// them after the bounded shared cache evicts them.
|
|
314
|
+
#kittyConverted = new Map<string, ImageContent>();
|
|
301
315
|
// Spinner animation for partial task results
|
|
302
316
|
#spinnerFrame?: number;
|
|
303
317
|
#spinnerActive = false;
|
|
@@ -545,26 +559,32 @@ export class ToolExecutionComponent extends Container {
|
|
|
545
559
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
546
560
|
if (!this.#result) return;
|
|
547
561
|
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
for (let i = 0; i < imageBlocks.length; i++) {
|
|
551
|
-
const img = imageBlocks[i];
|
|
562
|
+
for (const img of this.#getAllImageBlocks()) {
|
|
552
563
|
if (!img.data || !img.mimeType) continue;
|
|
553
|
-
// Skip if already PNG or already converted
|
|
564
|
+
// Skip if already PNG or already converted anywhere in this process
|
|
554
565
|
if (img.mimeType === "image/png") continue;
|
|
555
|
-
|
|
566
|
+
const image: ImageContent = { type: "image", data: img.data, mimeType: img.mimeType };
|
|
567
|
+
const key = imagePayloadKey(image);
|
|
568
|
+
if (this.#kittyConverted.has(key)) continue;
|
|
569
|
+
const cached = cachedPngConversion(image);
|
|
570
|
+
if (cached) {
|
|
571
|
+
this.#kittyConverted.set(key, cached);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (this.#kittyConversionsAwaited.has(key)) continue;
|
|
575
|
+
this.#kittyConversionsAwaited.add(key);
|
|
556
576
|
|
|
557
577
|
// Convert async - catch errors from processing
|
|
558
|
-
|
|
559
|
-
convertImageToPng({ type: "image", data: img.data, mimeType: img.mimeType })
|
|
578
|
+
convertImageToPngShared(image)
|
|
560
579
|
.then(converted => {
|
|
561
|
-
this.#
|
|
580
|
+
this.#kittyConverted.set(key, converted);
|
|
562
581
|
this.#displayInputVersion++;
|
|
563
582
|
this.#updateDisplay();
|
|
564
583
|
this.#ui.requestRender();
|
|
565
584
|
})
|
|
566
585
|
.catch(() => {
|
|
567
586
|
// Ignore conversion failures - display will use original image format
|
|
587
|
+
this.#kittyConversionsAwaited.delete(key);
|
|
568
588
|
});
|
|
569
589
|
}
|
|
570
590
|
}
|
|
@@ -1188,8 +1208,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
1188
1208
|
// Generic fallback (no custom/built-in renderer). WidthAwareText
|
|
1189
1209
|
// reformats at render time so output fills the actual terminal width
|
|
1190
1210
|
// instead of a fixed column cap.
|
|
1191
|
-
this.#
|
|
1192
|
-
this.#contentText.invalidate();
|
|
1211
|
+
this.#refreshContentText(stateBgKey, stateBgFn);
|
|
1193
1212
|
}
|
|
1194
1213
|
|
|
1195
1214
|
// Handle images (same for both custom and built-in)
|
|
@@ -1209,7 +1228,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
1209
1228
|
const img = imageBlocks[i];
|
|
1210
1229
|
if (TERMINAL.imageProtocol && this.#showImages && img.data && img.mimeType) {
|
|
1211
1230
|
// Use converted PNG for Kitty protocol if available
|
|
1212
|
-
const
|
|
1231
|
+
const source: ImageContent = { type: "image", data: img.data, mimeType: img.mimeType };
|
|
1232
|
+
const converted =
|
|
1233
|
+
TERMINAL.imageProtocol === ImageProtocol.Kitty && img.mimeType !== "image/png"
|
|
1234
|
+
? (this.#kittyConverted.get(imagePayloadKey(source)) ?? cachedPngConversion(source))
|
|
1235
|
+
: undefined;
|
|
1213
1236
|
const imageData = converted?.data ?? img.data;
|
|
1214
1237
|
const imageMimeType = converted?.mimeType ?? img.mimeType;
|
|
1215
1238
|
|
|
@@ -1323,6 +1346,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
1323
1346
|
|
|
1324
1347
|
#getTextOutput(): string {
|
|
1325
1348
|
if (!this.#result) return "";
|
|
1349
|
+
const key = `${this.#resultVersion}|${this.#showImages}|${TERMINAL.imageProtocol ?? "-"}`;
|
|
1350
|
+
if (key === this.#textOutputKey) return this.#textOutput;
|
|
1326
1351
|
|
|
1327
1352
|
const textBlocks = this.#result.content.filter(c => c.type === "text");
|
|
1328
1353
|
const imageBlocks = this.#getAllImageBlocks();
|
|
@@ -1344,9 +1369,21 @@ export class ToolExecutionComponent extends Container {
|
|
|
1344
1369
|
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
1345
1370
|
}
|
|
1346
1371
|
|
|
1372
|
+
this.#textOutputKey = key;
|
|
1373
|
+
this.#textOutput = output;
|
|
1347
1374
|
return output;
|
|
1348
1375
|
}
|
|
1349
1376
|
|
|
1377
|
+
/** Re-tint (only when the tint changed) and reformat the generic #contentText card. */
|
|
1378
|
+
#refreshContentText(stateBgKey: string, stateBgFn: (text: string) => string): void {
|
|
1379
|
+
const bgKey = `${stateBgKey}|${getThemeEpoch()}`;
|
|
1380
|
+
if (bgKey !== this.#contentTextBgKey) {
|
|
1381
|
+
this.#contentTextBgKey = bgKey;
|
|
1382
|
+
this.#contentText.setCustomBgFn(stateBgFn);
|
|
1383
|
+
}
|
|
1384
|
+
this.#contentText.reformat();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1350
1387
|
/**
|
|
1351
1388
|
* Format the generic call/result card at `contentWidth`. Shared by the
|
|
1352
1389
|
* #contentText fallback and the benign-skip path so both render identically.
|
|
@@ -1390,8 +1427,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
1390
1427
|
*/
|
|
1391
1428
|
#renderBenignSkipCard(stateBgFn: (text: string) => string): void {
|
|
1392
1429
|
if (!this.#usesContentBox) {
|
|
1393
|
-
this.#
|
|
1394
|
-
this.#contentText.invalidate();
|
|
1430
|
+
this.#refreshContentText("toolPendingBg", stateBgFn);
|
|
1395
1431
|
return;
|
|
1396
1432
|
}
|
|
1397
1433
|
for (const box of this.#multiFileBoxes) {
|
|
@@ -68,6 +68,22 @@ export function isUserRequestEntry(entry: TranscriptEntryLike | { type: string }
|
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Recent transcript tail starting at a user-request boundary, so tool calls
|
|
73
|
+
* and results stay together. ChatTranscriptBuilder drops a tool result whose
|
|
74
|
+
* initiating call was sliced away, so a tail of orphaned results can leave
|
|
75
|
+
* the picker without any target.
|
|
76
|
+
*
|
|
77
|
+
* A whole user turn may exceed `limit`; paginate rendering if one turn grows too large.
|
|
78
|
+
*/
|
|
79
|
+
export function recentTranscriptEntries(entries: TranscriptEntryLike[], limit = 600): TranscriptEntryLike[] {
|
|
80
|
+
if (entries.length <= limit) return entries;
|
|
81
|
+
for (let index = entries.length - limit; index > 0; index--) {
|
|
82
|
+
if (isUserRequestEntry(entries[index]!)) return entries.slice(index);
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
/** Editable user request text, preserving skill invocation syntax. */
|
|
72
88
|
export function userTurnDraft(entry: TranscriptEntryLike): string | undefined {
|
|
73
89
|
const message = transcriptEntryMessage(entry);
|