@oh-my-pi/pi-tui 18.6.3 → 18.8.0
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 +46 -109
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +92 -0
- package/dist/types/charts/chart-svg.d.ts +30 -0
- package/dist/types/charts/table-data.d.ts +107 -0
- package/dist/types/chat/assistant-message.d.ts +4 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +88 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/components/editor.d.ts +9 -4
- package/dist/types/components/image.d.ts +9 -0
- package/dist/types/components/markdown.d.ts +16 -2
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/native/backend.d.ts +2 -1
- package/dist/types/native/blobs.d.ts +4 -2
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/overlays/agent-transcript-viewer.d.ts +8 -1
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/model-browser.d.ts +31 -3
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/prompt/custom-editor.d.ts +7 -4
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/renderer.d.ts +22 -0
- package/dist/types/tools/streaming-output.d.ts +11 -1
- package/dist/types/tools/write.d.ts +8 -1
- package/dist/types/utils.d.ts +6 -0
- package/package.json +9 -9
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.ts +367 -0
- package/src/charts/chart-svg.ts +693 -0
- package/src/charts/table-data.ts +436 -0
- package/src/chat/assistant-message.ts +169 -39
- package/src/chat/chat-transcript-builder.ts +4 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/svg-figure.ts +362 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.ts +301 -0
- package/src/chat/tool-execution.ts +53 -4
- package/src/chrome/transcript-container.ts +7 -1
- package/src/components/editor.ts +68 -33
- package/src/components/image.ts +27 -1
- package/src/components/markdown.ts +80 -304
- package/src/fuzzy.ts +115 -30
- package/src/keybindings.ts +1 -1
- package/src/native/backend.ts +170 -15
- package/src/native/blobs.ts +4 -2
- package/src/native/encode.ts +58 -31
- package/src/overlays/agent-hub-projection.ts +70 -2
- package/src/overlays/agent-hub.ts +3 -2
- package/src/overlays/agent-transcript-viewer.ts +66 -13
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +179 -80
- package/src/overlays/ask-dialog.ts +1 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/jobs-panel.ts +30 -2
- package/src/overlays/model-browser.ts +269 -90
- package/src/overlays/model-hub.ts +34 -7
- package/src/overlays/oauth-selector.ts +41 -26
- package/src/overlays/plan-review-overlay.ts +183 -68
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-selector.ts +24 -4
- package/src/overlays/settings-selector.ts +21 -6
- package/src/overlays/tree-selector.ts +20 -4
- package/src/overlays/usage-dashboard.ts +12 -15
- package/src/prompt/attachment-chips.ts +2 -1
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer.ts +2 -14
- package/src/prompt/custom-editor.ts +48 -19
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.ts +62 -0
- package/src/setup/scenes/splash.ts +109 -45
- package/src/status-line/component.ts +220 -64
- package/src/status-line/segments.ts +8 -3
- package/src/terminal.ts +3 -2
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.ts +164 -78
- package/src/tools/glob.ts +23 -6
- package/src/tools/grep.ts +81 -48
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +98 -68
- package/src/tools/renderer.ts +27 -0
- package/src/tools/streaming-output.ts +120 -48
- package/src/tools/task.ts +111 -54
- package/src/tools/todo.ts +115 -29
- package/src/tools/wait.ts +34 -24
- package/src/tools/write.ts +77 -39
- package/src/tui.ts +118 -105
- package/src/utils.ts +19 -0
|
@@ -27,6 +27,10 @@ import { type ServedModelMismatch, ServedModelMarkerComponent } from "./served-m
|
|
|
27
27
|
import { isReactionTarget, type ReactionSplit, type ReactionTarget, splitReaction } from "./reaction";
|
|
28
28
|
import { isRowPrefix, type TranscriptStableRow, trimBlankEdges } from "../chrome/transcript-container";
|
|
29
29
|
import { formatTurnUsage, type TurnUsageSummary } from "../overlays/usage-row";
|
|
30
|
+
import { FigureMarkdown } from "./figure-markdown";
|
|
31
|
+
import { svgFigureRendering } from "./svg-figure";
|
|
32
|
+
import { hasSvgFence } from "./svg-source";
|
|
33
|
+
import { describeTableChart, hasChartTable, lookupTableChart, splitTableCharts } from "./table-chart";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Max wrapped rows of a turn-ending provider error rendered inline in the
|
|
@@ -46,6 +50,15 @@ function thoughtLabel(clock: { start: number; end?: number } | undefined): strin
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
|
|
53
|
+
/** Renders one text or thinking block: Markdown, or {@link FigureMarkdown} for text holding a ```svg fence. */
|
|
54
|
+
type ProseBlock = Markdown | FigureMarkdown;
|
|
55
|
+
/** A streamed block the fast path updates in place. */
|
|
56
|
+
interface FastPathItem {
|
|
57
|
+
md: ProseBlock;
|
|
58
|
+
contentIndex: number;
|
|
59
|
+
blockType: "text" | "thinking";
|
|
60
|
+
lastText: string;
|
|
61
|
+
}
|
|
49
62
|
type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
|
|
50
63
|
type StablePartKind = "thinking" | "text";
|
|
51
64
|
type StablePart = { kind: StablePartKind; text: string } | { kind: "spacer" };
|
|
@@ -230,22 +243,30 @@ export class AssistantMessageComponent extends Container {
|
|
|
230
243
|
#cacheMarker?: CacheInvalidationMarkerComponent;
|
|
231
244
|
#servedModelMarker?: ServedModelMarkerComponent;
|
|
232
245
|
#lastMessage?: AssistantMessage;
|
|
233
|
-
#emergencyText?:
|
|
246
|
+
#emergencyText?: ProseBlock;
|
|
234
247
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
235
248
|
/**
|
|
236
249
|
* Payload keys ({@link imagePayloadKey}) whose Kitty PNG conversion this
|
|
237
|
-
* component
|
|
250
|
+
* component is awaiting, so a re-delivered image neither re-encodes nor
|
|
238
251
|
* schedules a second {@link updateContent} cascade. The conversions
|
|
239
|
-
* themselves
|
|
252
|
+
* themselves live in the bounded process-wide cache behind
|
|
253
|
+
* {@link convertImageToPngShared}; an evicted one is redone on the next
|
|
254
|
+
* render instead of being pinned here for the session.
|
|
240
255
|
*/
|
|
241
256
|
#kittyConversionsAwaited = new Set<string>();
|
|
242
257
|
/**
|
|
243
|
-
* Conversions
|
|
244
|
-
*
|
|
258
|
+
* Conversions the current image children display, by payload key. Rebuilt
|
|
259
|
+
* on every full render pass ({@link updateContent}), so a conversion is held
|
|
260
|
+
* only while it is on screen — a re-render after the shared cache evicted
|
|
261
|
+
* it still finds it — and released once hidden or replaced.
|
|
245
262
|
*/
|
|
246
|
-
#
|
|
263
|
+
#kittyDisplayed = new Map<string, ImageContent>();
|
|
264
|
+
/** The previous pass's {@link #kittyDisplayed}, readable only during a render pass. */
|
|
265
|
+
#kittyPreviouslyDisplayed: Map<string, ImageContent> | undefined;
|
|
247
266
|
#showImages = true;
|
|
248
267
|
#showToolResultImages = true;
|
|
268
|
+
/** Charts under numeric tables; off for subagent transcripts. */
|
|
269
|
+
#showTableCharts = true;
|
|
249
270
|
#transcriptBlockFinalized: boolean;
|
|
250
271
|
/** See {@link setMidStreamPublication}; the wire's `stream-revision` axis decides it. */
|
|
251
272
|
#midStreamPublication = true;
|
|
@@ -293,9 +314,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
293
314
|
#lastUpdateTransient = false;
|
|
294
315
|
// Fast-path state: reuse Markdown children when message shape is stable during streaming.
|
|
295
316
|
#fastPathKey: string | undefined;
|
|
296
|
-
#fastPathItems:
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
#fastPathItems: FastPathItem[] | undefined;
|
|
318
|
+
/**
|
|
319
|
+
* Text blocks whose ```svg fences are lifted into figures, by content index.
|
|
320
|
+
* Kept across slow-path rebuilds so a figure keeps its raster (and its
|
|
321
|
+
* in-flight one) instead of starting over on every update.
|
|
322
|
+
*/
|
|
323
|
+
#figureBlocks = new Map<number, FigureMarkdown>();
|
|
299
324
|
/** Live "thinking" pulse shown in place of a hidden thinking block while it
|
|
300
325
|
* streams; undefined when not animating. Driven by {@link #thinkingDotsTimer}. */
|
|
301
326
|
#thinkingDots: Text | undefined;
|
|
@@ -356,6 +381,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
356
381
|
/** Markdown nodes by key, reused while their text and streaming flag are unchanged. */
|
|
357
382
|
#nativeParts = new Map<string, { text: string; stream: boolean; node: NativeNode }>();
|
|
358
383
|
readonly #nativeImages = new NativeImageCache();
|
|
384
|
+
/** Smart table chart picks a TSP describe is waiting on. */
|
|
385
|
+
readonly #chartPicks = new Set<Promise<void>>();
|
|
359
386
|
|
|
360
387
|
setTextColorTransform(transform?: (text: string) => string): void {
|
|
361
388
|
this.#textColorTransform = transform;
|
|
@@ -387,6 +414,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
387
414
|
this.#markdownTheme = undefined;
|
|
388
415
|
this.#fastPathKey = undefined;
|
|
389
416
|
this.#fastPathItems = undefined;
|
|
417
|
+
for (const block of this.#figureBlocks.values()) block.restyle();
|
|
390
418
|
if (this.#lastMessage) {
|
|
391
419
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
392
420
|
}
|
|
@@ -537,6 +565,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
537
565
|
this.#markdownTheme = undefined;
|
|
538
566
|
this.#fastPathKey = undefined;
|
|
539
567
|
this.#fastPathItems = undefined;
|
|
568
|
+
for (const block of this.#figureBlocks.values()) block.restyle();
|
|
540
569
|
if (this.#lastMessage) {
|
|
541
570
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
542
571
|
}
|
|
@@ -713,6 +742,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
713
742
|
return this.#transcriptBlockFinalized;
|
|
714
743
|
}
|
|
715
744
|
|
|
745
|
+
/** Whether a figure still waits for the raster of its final source; the transcript holds retirement meanwhile. */
|
|
746
|
+
isTranscriptBlockPending(): boolean {
|
|
747
|
+
for (const block of this.#figureBlocks.values()) if (block.pending) return true;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
|
|
716
751
|
override render(width: number): readonly string[] {
|
|
717
752
|
const rows = super.render(width);
|
|
718
753
|
this.#publishStableSnapshot(rows, width);
|
|
@@ -806,7 +841,25 @@ export class AssistantMessageComponent extends Container {
|
|
|
806
841
|
const content = message.content[index]!;
|
|
807
842
|
const streaming = live && index === tailIndex;
|
|
808
843
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
809
|
-
|
|
844
|
+
const source = content.text.trim();
|
|
845
|
+
if (!this.#showImages || !this.#showTableCharts || !hasChartTable(source)) {
|
|
846
|
+
children.push(markdown(`t${index}`, `t${index}`, source, streaming));
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
// A chart follows its table as an SVG image node; the prose splits around it.
|
|
850
|
+
const segments = splitTableCharts(source, streaming);
|
|
851
|
+
segments.forEach((segment, part) => {
|
|
852
|
+
const slot = part === 0 ? `t${index}` : `t${index}.${part}`;
|
|
853
|
+
if (segment.kind === "markdown") {
|
|
854
|
+
children.push(
|
|
855
|
+
markdown(slot, slot, segment.text.trim(), streaming && part === segments.length - 1),
|
|
856
|
+
);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const chart = lookupTableChart(segment.table);
|
|
860
|
+
if (chart instanceof Promise) this.#awaitChart(chart);
|
|
861
|
+
else if (chart) children.push(describeTableChart(chart, slot));
|
|
862
|
+
});
|
|
810
863
|
} else if (content.type === "thinking") {
|
|
811
864
|
const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
|
|
812
865
|
if (!display.visible) continue;
|
|
@@ -1089,12 +1142,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
1089
1142
|
for (const child of this.#contentContainer.children) {
|
|
1090
1143
|
const item = items[itemIndex];
|
|
1091
1144
|
if (item?.md === child) {
|
|
1145
|
+
const md = item.md;
|
|
1146
|
+
if (md instanceof FigureMarkdown) {
|
|
1147
|
+
// Plain Markdown reproduces only the prose ahead of the first figure.
|
|
1148
|
+
if (md.leadingProse) parts.push({ kind: item.blockType, text: md.leadingProse });
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1092
1151
|
if (itemIndex === items.length - 1) {
|
|
1093
1152
|
// Streaming child: publish Markdown's frozen prefix, and only
|
|
1094
1153
|
// once non-blank content exists past it — the block's last
|
|
1095
1154
|
// non-blank line is still being written (thinking's prose fold
|
|
1096
1155
|
// may rewrite it) and must stay out of published bytes.
|
|
1097
|
-
const raw =
|
|
1156
|
+
const raw = md.getLastRenderStableText();
|
|
1098
1157
|
const frozen = raw.trim();
|
|
1099
1158
|
if (frozen.length > 0 && /\S/.test(item.lastText.slice(raw.length))) {
|
|
1100
1159
|
parts.push({ kind: item.blockType, text: frozen });
|
|
@@ -1122,7 +1181,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1122
1181
|
/**
|
|
1123
1182
|
* Render the first `partCount` stable parts, the final one cut to
|
|
1124
1183
|
* `lastLength`. Rows match fresh Markdown renders of each part byte for
|
|
1125
|
-
* byte — {@link #
|
|
1184
|
+
* byte — {@link #createMarkdown} builds the live children too — but a
|
|
1126
1185
|
* closed part renders once per width, and the growing final part reuses one
|
|
1127
1186
|
* Markdown instance so its already-frozen blocks are not re-lexed.
|
|
1128
1187
|
*/
|
|
@@ -1172,7 +1231,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1172
1231
|
// Trim like the live children, dropping the trailing blank line a frozen
|
|
1173
1232
|
// prefix still carries.
|
|
1174
1233
|
const trimmed = text.trim();
|
|
1175
|
-
if (this.#transcriptBlockFinalized) return this.#
|
|
1234
|
+
if (this.#transcriptBlockFinalized) return this.#createMarkdown(kind, trimmed).render(width);
|
|
1176
1235
|
const head = this.#stableHeadRenderer;
|
|
1177
1236
|
const replay = this.#stableReplayRenderer;
|
|
1178
1237
|
const renderer = role === "head" ? head : role === "replay" ? replay : head?.index === index ? head : replay;
|
|
@@ -1180,14 +1239,14 @@ export class AssistantMessageComponent extends Container {
|
|
|
1180
1239
|
renderer.md.setText(trimmed);
|
|
1181
1240
|
return renderer.md.render(width);
|
|
1182
1241
|
}
|
|
1183
|
-
const md = this.#
|
|
1242
|
+
const md = this.#createMarkdown(kind, trimmed);
|
|
1184
1243
|
if (role === "head") this.#stableHeadRenderer = { index, kind, md };
|
|
1185
1244
|
else if (role === "replay") this.#stableReplayRenderer = { index, kind, md };
|
|
1186
1245
|
return md.render(width);
|
|
1187
1246
|
}
|
|
1188
1247
|
|
|
1189
|
-
/**
|
|
1190
|
-
#
|
|
1248
|
+
/** Markdown for a text or thinking block; live children and stable-row renders share it so stable rows prefix the block render. */
|
|
1249
|
+
#createMarkdown(kind: StablePartKind, text: string): Markdown {
|
|
1191
1250
|
return kind === "text"
|
|
1192
1251
|
? new Markdown(
|
|
1193
1252
|
text,
|
|
@@ -1203,6 +1262,58 @@ export class AssistantMessageComponent extends Container {
|
|
|
1203
1262
|
});
|
|
1204
1263
|
}
|
|
1205
1264
|
|
|
1265
|
+
/**
|
|
1266
|
+
* Whether `text` holds a ```svg fence this terminal draws as a figure, or a
|
|
1267
|
+
* table it draws a chart under. Native (TSP) terminals receive the fence
|
|
1268
|
+
* verbatim in the `md` node and draw it themselves, and get charts as image
|
|
1269
|
+
* nodes from {@link describe}; image-less terminals keep fences as code and
|
|
1270
|
+
* tables as tables.
|
|
1271
|
+
*/
|
|
1272
|
+
#liftsFigures(text: string): boolean {
|
|
1273
|
+
return (
|
|
1274
|
+
this.#showImages &&
|
|
1275
|
+
TERMINAL.imageProtocol !== null &&
|
|
1276
|
+
!isNativeRendering() &&
|
|
1277
|
+
((svgFigureRendering() && hasSvgFence(text)) || (this.#showTableCharts && hasChartTable(text)))
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** Re-describe once a smart table chart pick lands. */
|
|
1282
|
+
#awaitChart(pick: Promise<void>): void {
|
|
1283
|
+
if (this.#chartPicks.has(pick)) return;
|
|
1284
|
+
this.#chartPicks.add(pick);
|
|
1285
|
+
void pick.then(() => {
|
|
1286
|
+
this.#chartPicks.delete(pick);
|
|
1287
|
+
this.#blockVersion++;
|
|
1288
|
+
this.#onImageUpdate?.();
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* The component for text block `index`: Markdown, or a {@link FigureMarkdown}
|
|
1294
|
+
* when it holds a ```svg fence to lift or a table to chart — taken over from
|
|
1295
|
+
* `previous` so its figures keep their rasters across rebuilds.
|
|
1296
|
+
*/
|
|
1297
|
+
#proseBlock(index: number, text: string, previous: ReadonlyMap<number, FigureMarkdown>): ProseBlock {
|
|
1298
|
+
if (!this.#liftsFigures(text)) return this.#createMarkdown("text", text);
|
|
1299
|
+
let block = previous.get(index);
|
|
1300
|
+
if (block) {
|
|
1301
|
+
block.setText(text);
|
|
1302
|
+
} else {
|
|
1303
|
+
block = new FigureMarkdown(text, {
|
|
1304
|
+
markdown: value => this.#createMarkdown("text", value),
|
|
1305
|
+
charts: this.#showTableCharts,
|
|
1306
|
+
budget: this.#imageBudget,
|
|
1307
|
+
onChange: () => {
|
|
1308
|
+
this.#blockVersion++;
|
|
1309
|
+
this.#onImageUpdate?.();
|
|
1310
|
+
},
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
this.#figureBlocks.set(index, block);
|
|
1314
|
+
return block;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1206
1317
|
#stableLedger(width: number): StableRowLedger {
|
|
1207
1318
|
let ledger = this.#stableLedgers.get(width);
|
|
1208
1319
|
if (ledger === undefined) {
|
|
@@ -1322,6 +1433,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
1322
1433
|
}
|
|
1323
1434
|
}
|
|
1324
1435
|
|
|
1436
|
+
/** Toggle charts under numeric tables (the main session's transcript only, never a subagent's). */
|
|
1437
|
+
setTableChartsVisible(visible: boolean): void {
|
|
1438
|
+
if (this.#showTableCharts === visible) return;
|
|
1439
|
+
this.#showTableCharts = visible;
|
|
1440
|
+
// Figure blocks bake the switch in; rebuild them rather than reuse.
|
|
1441
|
+
for (const block of this.#figureBlocks.values()) block.dispose();
|
|
1442
|
+
this.#figureBlocks.clear();
|
|
1443
|
+
if (this.#lastMessage) {
|
|
1444
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1325
1448
|
/** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
|
|
1326
1449
|
setToolResultImagesVisible(visible: boolean): void {
|
|
1327
1450
|
if (this.#showToolResultImages === visible) return;
|
|
@@ -1345,22 +1468,24 @@ export class AssistantMessageComponent extends Container {
|
|
|
1345
1468
|
}
|
|
1346
1469
|
}
|
|
1347
1470
|
|
|
1471
|
+
/** A displayable Kitty PNG for a non-PNG `image`: the shared cache, else this component's displayed copy. */
|
|
1472
|
+
#kittyConversion(image: ImageContent): ImageContent | undefined {
|
|
1473
|
+
const cached = cachedPngConversion(image);
|
|
1474
|
+
if (cached) return cached;
|
|
1475
|
+
const key = imagePayloadKey(image);
|
|
1476
|
+
return this.#kittyDisplayed.get(key) ?? this.#kittyPreviouslyDisplayed?.get(key);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1348
1479
|
#convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
|
|
1349
1480
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
1350
1481
|
for (const { image } of entries) {
|
|
1351
|
-
if (image.mimeType === "image/png") continue;
|
|
1482
|
+
if (image.mimeType === "image/png" || this.#kittyConversion(image)) continue;
|
|
1352
1483
|
const key = imagePayloadKey(image);
|
|
1353
|
-
if (this.#kittyConverted.has(key)) continue;
|
|
1354
|
-
const cached = cachedPngConversion(image);
|
|
1355
|
-
if (cached) {
|
|
1356
|
-
this.#kittyConverted.set(key, cached);
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
1484
|
if (this.#kittyConversionsAwaited.has(key)) continue;
|
|
1360
1485
|
this.#kittyConversionsAwaited.add(key);
|
|
1361
1486
|
convertImageToPngShared(image)
|
|
1362
|
-
.then(
|
|
1363
|
-
this.#
|
|
1487
|
+
.then(() => {
|
|
1488
|
+
this.#kittyConversionsAwaited.delete(key);
|
|
1364
1489
|
if (this.#lastMessage) {
|
|
1365
1490
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1366
1491
|
}
|
|
@@ -1378,10 +1503,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
1378
1503
|
|
|
1379
1504
|
if (withLeadingSpacer) this.#contentContainer.addChild(new Spacer(1));
|
|
1380
1505
|
for (const { image, key } of entries) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1506
|
+
let displayImage: ImageContent | undefined = image;
|
|
1507
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png") {
|
|
1508
|
+
displayImage = this.#kittyConversion(image);
|
|
1509
|
+
if (displayImage) this.#kittyDisplayed.set(imagePayloadKey(image), displayImage);
|
|
1510
|
+
}
|
|
1385
1511
|
if (TERMINAL.imageProtocol && displayImage) {
|
|
1386
1512
|
this.#contentContainer.addChild(
|
|
1387
1513
|
new Image(
|
|
@@ -1440,7 +1566,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
1440
1566
|
];
|
|
1441
1567
|
for (const content of message.content) {
|
|
1442
1568
|
if (content.type === "text") {
|
|
1443
|
-
parts.push(
|
|
1569
|
+
parts.push(
|
|
1570
|
+
!canonicalizeMessage(content.text) ? "T0" : this.#liftsFigures(content.text.trim()) ? "TF" : "T1",
|
|
1571
|
+
);
|
|
1444
1572
|
} else if (content.type === "thinking") {
|
|
1445
1573
|
if (this.#hideThinkingBlock) {
|
|
1446
1574
|
// Match the pulse's empty/nonempty transition without formatting hidden text.
|
|
@@ -1594,6 +1722,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
1594
1722
|
|
|
1595
1723
|
// Clear content container
|
|
1596
1724
|
this.#contentContainer.clear();
|
|
1725
|
+
this.#kittyPreviouslyDisplayed = this.#kittyDisplayed;
|
|
1726
|
+
this.#kittyDisplayed = new Map();
|
|
1597
1727
|
this.#thinkingExtensions.clear();
|
|
1598
1728
|
this.#emergencyText = undefined;
|
|
1599
1729
|
this.#thinkingDots = undefined;
|
|
@@ -1601,9 +1731,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
1601
1731
|
|
|
1602
1732
|
// Determine if we should capture Markdown instances for next fast path
|
|
1603
1733
|
const shouldCapture = this.#canFastPath(message);
|
|
1604
|
-
const captureItems:
|
|
1605
|
-
|
|
1606
|
-
|
|
1734
|
+
const captureItems: FastPathItem[] | undefined = shouldCapture ? [] : undefined;
|
|
1735
|
+
const previousFigures = this.#figureBlocks;
|
|
1736
|
+
this.#figureBlocks = new Map();
|
|
1607
1737
|
|
|
1608
1738
|
const hasVisibleContent = message.content.some(
|
|
1609
1739
|
c =>
|
|
@@ -1622,8 +1752,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1622
1752
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
1623
1753
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
1624
1754
|
const trimmed = content.text.trim();
|
|
1625
|
-
const
|
|
1626
|
-
const md = new Markdown(trimmed, 1, 0, this.#getProseTheme(), mdOptions, 0);
|
|
1755
|
+
const md = this.#proseBlock(i, trimmed, previousFigures);
|
|
1627
1756
|
this.#contentContainer.addChild(md);
|
|
1628
1757
|
this.#emergencyText = md;
|
|
1629
1758
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
@@ -1648,10 +1777,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1648
1777
|
);
|
|
1649
1778
|
|
|
1650
1779
|
// Thinking traces in thinkingText color, italic
|
|
1651
|
-
const md =
|
|
1652
|
-
color: (text: string) => theme.fg("thinkingText", text),
|
|
1653
|
-
italic: true,
|
|
1654
|
-
});
|
|
1780
|
+
const md = this.#createMarkdown("thinking", thinkingText);
|
|
1655
1781
|
md.transientRenderCache = this.#lastUpdateTransient;
|
|
1656
1782
|
this.#contentContainer.addChild(md);
|
|
1657
1783
|
captureItems?.push({ md, contentIndex: i, blockType: "thinking", lastText: thinkingText });
|
|
@@ -1666,6 +1792,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
1666
1792
|
hasRenderedContent ||= this.#showImages;
|
|
1667
1793
|
}
|
|
1668
1794
|
}
|
|
1795
|
+
for (const [index, block] of previousFigures) {
|
|
1796
|
+
if (this.#figureBlocks.get(index) !== block) block.dispose();
|
|
1797
|
+
}
|
|
1669
1798
|
|
|
1670
1799
|
if (this.#shouldAnimateThinking(message)) {
|
|
1671
1800
|
if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
|
|
@@ -1677,6 +1806,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1677
1806
|
}
|
|
1678
1807
|
|
|
1679
1808
|
this.#renderToolImages();
|
|
1809
|
+
this.#kittyPreviouslyDisplayed = undefined;
|
|
1680
1810
|
const errorPresentation = resolveAssistantErrorPresentation(message);
|
|
1681
1811
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
1682
1812
|
if (errorPresentation.kind === "compact-recovered") {
|
|
@@ -83,6 +83,8 @@ export interface ChatTranscriptBuilderDeps {
|
|
|
83
83
|
expandThinkingBlocks?: () => boolean;
|
|
84
84
|
/** Session-scoped resolved destinations for model-authored Markdown links. */
|
|
85
85
|
linkTargets?: ReadonlyMap<string, string>;
|
|
86
|
+
/** Draw charts under numeric tables; false for transcripts that are not the main session's. Default: true. */
|
|
87
|
+
tableCharts?: boolean;
|
|
86
88
|
requestRender: () => void;
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -419,6 +421,7 @@ export class ChatTranscriptBuilder {
|
|
|
419
421
|
);
|
|
420
422
|
assistantComponent.setImagesVisible(displayPreferences.showImages);
|
|
421
423
|
assistantComponent.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
|
|
424
|
+
assistantComponent.setTableChartsVisible(this.#deps.tableCharts !== false);
|
|
422
425
|
this.#trackExpandable(assistantComponent);
|
|
423
426
|
assistantComponent.pickReactionTarget(this.container.children);
|
|
424
427
|
this.container.addChild(assistantComponent);
|
|
@@ -457,6 +460,7 @@ export class ChatTranscriptBuilder {
|
|
|
457
460
|
);
|
|
458
461
|
component.setImagesVisible(displayPreferences.showImages);
|
|
459
462
|
component.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
|
|
463
|
+
component.setTableChartsVisible(this.#deps.tableCharts !== false);
|
|
460
464
|
this.#trackExpandable(component);
|
|
461
465
|
this.container.addChild(component);
|
|
462
466
|
lastAssistantComponent = component;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool call's {@link ToolFigure} drawn under its card, the way assistant
|
|
3
|
+
* Markdown draws that fence: ```svg as an inline image ({@link SvgFigure}),
|
|
4
|
+
* redrawn as the body streams; ```mermaid as ASCII art once the body is
|
|
5
|
+
* final. The card already shows the source, so unlike the assistant's fence
|
|
6
|
+
* it never falls back to code: {@link FenceFigure.draws} turns down fences
|
|
7
|
+
* this terminal shows as code, and an svg that does not rasterize stays blank.
|
|
8
|
+
*/
|
|
9
|
+
import type { ImageBudget } from "../components/image";
|
|
10
|
+
import { fencedCode, Markdown } from "../components/markdown";
|
|
11
|
+
import { TERMINAL } from "../terminal-capabilities";
|
|
12
|
+
import { getMarkdownTheme } from "../theme";
|
|
13
|
+
import type { ToolFigure } from "../tools/renderer";
|
|
14
|
+
import type { Component } from "../tui";
|
|
15
|
+
import { SvgFigure, svgFigureRendering } from "./svg-figure";
|
|
16
|
+
|
|
17
|
+
/** Fence languages a {@link FenceFigure} draws. */
|
|
18
|
+
export type FenceFigureLang = "svg" | "mermaid";
|
|
19
|
+
|
|
20
|
+
export interface FenceFigureOptions {
|
|
21
|
+
/** Shared inline-image budget for an svg's rasters. */
|
|
22
|
+
budget?: ImageBudget;
|
|
23
|
+
/** The figure's rows changed outside a render: a raster landed. */
|
|
24
|
+
onChange: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class FenceFigure implements Component {
|
|
28
|
+
/**
|
|
29
|
+
* Whether this terminal draws `figure` now: an svg where inline images show
|
|
30
|
+
* (`showImages` and a graphics protocol), a closed mermaid fence when
|
|
31
|
+
* diagram rendering is on and the source renders.
|
|
32
|
+
*/
|
|
33
|
+
static draws(
|
|
34
|
+
figure: ToolFigure,
|
|
35
|
+
options: { showImages: boolean },
|
|
36
|
+
): figure is ToolFigure & { readonly lang: FenceFigureLang } {
|
|
37
|
+
if (figure.lang === "svg") return options.showImages && TERMINAL.imageProtocol !== null && svgFigureRendering();
|
|
38
|
+
if (figure.lang !== "mermaid" || !figure.closed) return false;
|
|
39
|
+
return getMarkdownTheme().resolveMermaidAscii?.(figure.source.trimEnd()) != null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readonly lang: FenceFigureLang;
|
|
43
|
+
readonly #svg: SvgFigure | undefined;
|
|
44
|
+
#source = "";
|
|
45
|
+
/** The mermaid fence as assistant Markdown renders it; rebuilt after a theme change. */
|
|
46
|
+
#diagram: Markdown | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(lang: FenceFigureLang, options: FenceFigureOptions) {
|
|
49
|
+
this.lang = lang;
|
|
50
|
+
if (lang === "svg") this.#svg = new SvgFigure({ budget: options.budget, onChange: options.onChange });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Feed the fence as it stands; its `lang` is this figure's. */
|
|
54
|
+
update(figure: ToolFigure): void {
|
|
55
|
+
if (figure.source !== this.#source) this.#diagram = undefined;
|
|
56
|
+
this.#source = figure.source;
|
|
57
|
+
this.#svg?.update(figure.source, figure.closed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a closed svg's raster is still on its way. */
|
|
61
|
+
get pending(): boolean {
|
|
62
|
+
return this.#svg?.pending === true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The drawing; no rows while an svg draws, or when it does not. */
|
|
66
|
+
render(width: number): readonly string[] {
|
|
67
|
+
if (this.#svg) return this.#svg.render(width);
|
|
68
|
+
this.#diagram ??= new Markdown(fencedCode("mermaid", this.#source), 1, 0, getMarkdownTheme());
|
|
69
|
+
return this.#diagram.render(width);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
invalidate(): void {
|
|
73
|
+
this.#svg?.invalidate();
|
|
74
|
+
this.#diagram = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
dispose(): void {
|
|
78
|
+
this.#svg?.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Tokens } from "@oh-my-pi/pi-utils/marked";
|
|
2
|
+
import type { ImageBudget } from "../components/image";
|
|
3
|
+
import type { Markdown } from "../components/markdown";
|
|
4
|
+
import { Spacer } from "../components/spacer";
|
|
5
|
+
import { Container } from "../tui";
|
|
6
|
+
import { SvgFigure, svgFigureRendering } from "./svg-figure";
|
|
7
|
+
import { type FigureSegment, splitSvgFences } from "./svg-source";
|
|
8
|
+
import { type ChartSegment, hasChartTable, splitTableCharts, TableChartFigure } from "./table-chart";
|
|
9
|
+
|
|
10
|
+
export interface FigureMarkdownOptions {
|
|
11
|
+
/** Markdown for one prose run (and a failed figure's code fallback); FigureMarkdown drives its transience. */
|
|
12
|
+
markdown: (text: string) => Markdown;
|
|
13
|
+
/** Draw charts under numeric tables; off for transcripts that are not the main session's. */
|
|
14
|
+
charts: boolean;
|
|
15
|
+
/** Shared inline-image budget for the figures. */
|
|
16
|
+
budget?: ImageBudget;
|
|
17
|
+
/** A figure's rows changed outside a text update (raster landed, fell back to code). */
|
|
18
|
+
onChange: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Part =
|
|
22
|
+
| { readonly kind: "markdown"; readonly md: Markdown; text: string }
|
|
23
|
+
| { readonly kind: "svg"; readonly figure: SvgFigure; source: string; closed: boolean }
|
|
24
|
+
| { readonly kind: "chart"; readonly figure: TableChartFigure; table: Tokens.Table };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Assistant Markdown whose top-level ```svg fences are lifted out of the prose
|
|
28
|
+
* and drawn as images ({@link SvgFigure}), and whose numeric tables get a chart
|
|
29
|
+
* drawn under them ({@link TableChartFigure}), with ordinary {@link Markdown}
|
|
30
|
+
* for the prose between them. Stands in for Markdown on an assistant text
|
|
31
|
+
* block that holds an svg fence or a chartable table: {@link setText} streams
|
|
32
|
+
* in place, reusing every part whose kind is unchanged, and
|
|
33
|
+
* {@link transientRenderCache} marks the block's tail as still streaming — the
|
|
34
|
+
* last prose run renders transient, the last figure throttles its rasters
|
|
35
|
+
* until its fence closes, and a table at the tail waits for its last row.
|
|
36
|
+
*/
|
|
37
|
+
export class FigureMarkdown extends Container {
|
|
38
|
+
readonly #options: FigureMarkdownOptions;
|
|
39
|
+
#text = "";
|
|
40
|
+
#transient = false;
|
|
41
|
+
#parts: Part[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(text: string, options: FigureMarkdownOptions) {
|
|
44
|
+
super();
|
|
45
|
+
this.#options = options;
|
|
46
|
+
this.setText(text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Replace the source; returns whether it changed. */
|
|
50
|
+
setText(text: string): boolean {
|
|
51
|
+
if (text === this.#text) return false;
|
|
52
|
+
this.#text = text;
|
|
53
|
+
this.#reconcile();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get transientRenderCache(): boolean {
|
|
58
|
+
return this.#transient;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
set transientRenderCache(value: boolean) {
|
|
62
|
+
if (this.#transient === value) return;
|
|
63
|
+
this.#transient = value;
|
|
64
|
+
// The end of the stream completes a tail table, which may now get its chart.
|
|
65
|
+
this.#reconcile();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The trimmed prose ahead of the first figure, or "" when the block opens
|
|
70
|
+
* with one. These are the only rows plain Markdown reproduces byte for
|
|
71
|
+
* byte, so they are all a transcript may publish ahead of the figure.
|
|
72
|
+
*/
|
|
73
|
+
get leadingProse(): string {
|
|
74
|
+
const first = this.#parts[0];
|
|
75
|
+
return first?.kind === "markdown" ? first.text : "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
|
|
79
|
+
get pending(): boolean {
|
|
80
|
+
return this.#parts.some(part => part.kind !== "markdown" && part.figure.pending);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Rebuild the prose runs through the Markdown factory after its theme changed; figures keep their rasters. */
|
|
84
|
+
restyle(): void {
|
|
85
|
+
this.#parts = this.#parts.map(part =>
|
|
86
|
+
part.kind === "markdown" ? { kind: "markdown", md: this.#options.markdown(part.text), text: part.text } : part,
|
|
87
|
+
);
|
|
88
|
+
this.#mount();
|
|
89
|
+
this.#syncTails();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#reconcile(): void {
|
|
93
|
+
const previous = this.#parts;
|
|
94
|
+
const segments = this.#segments();
|
|
95
|
+
let reshaped = previous.length !== segments.length;
|
|
96
|
+
this.#parts = segments.map((segment, index): Part => {
|
|
97
|
+
const old = previous[index];
|
|
98
|
+
if (segment.kind === "chart") {
|
|
99
|
+
if (old?.kind === "chart") {
|
|
100
|
+
old.table = segment.table;
|
|
101
|
+
return old;
|
|
102
|
+
}
|
|
103
|
+
reshaped = true;
|
|
104
|
+
const { budget, onChange } = this.#options;
|
|
105
|
+
return { kind: "chart", figure: new TableChartFigure({ budget, onChange }), table: segment.table };
|
|
106
|
+
}
|
|
107
|
+
if (segment.kind === "markdown") {
|
|
108
|
+
const text = segment.text.trim();
|
|
109
|
+
if (old?.kind === "markdown") {
|
|
110
|
+
old.md.setText(text);
|
|
111
|
+
old.text = text;
|
|
112
|
+
return old;
|
|
113
|
+
}
|
|
114
|
+
reshaped = true;
|
|
115
|
+
return { kind: "markdown", md: this.#options.markdown(text), text };
|
|
116
|
+
}
|
|
117
|
+
if (old?.kind === "svg") {
|
|
118
|
+
old.source = segment.source;
|
|
119
|
+
old.closed = segment.closed;
|
|
120
|
+
return old;
|
|
121
|
+
}
|
|
122
|
+
reshaped = true;
|
|
123
|
+
const { markdown, budget, onChange } = this.#options;
|
|
124
|
+
return {
|
|
125
|
+
kind: "svg",
|
|
126
|
+
figure: new SvgFigure({ markdown, budget, onChange }),
|
|
127
|
+
source: segment.source,
|
|
128
|
+
closed: segment.closed,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
if (reshaped) {
|
|
132
|
+
for (const old of previous) {
|
|
133
|
+
if (old.kind !== "markdown" && !this.#parts.includes(old)) old.figure.dispose();
|
|
134
|
+
}
|
|
135
|
+
this.#mount();
|
|
136
|
+
}
|
|
137
|
+
this.#syncTails();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The block as prose, ```svg fences (when figures are on) and charts after
|
|
142
|
+
* complete tables (when charts are on); only the last run can still stream.
|
|
143
|
+
*/
|
|
144
|
+
#segments(): (FigureSegment | ChartSegment)[] {
|
|
145
|
+
const fences: FigureSegment[] = svgFigureRendering()
|
|
146
|
+
? splitSvgFences(this.#text)
|
|
147
|
+
: [{ kind: "markdown", text: this.#text }];
|
|
148
|
+
if (!this.#options.charts || !hasChartTable(this.#text)) return fences;
|
|
149
|
+
const last = fences.length - 1;
|
|
150
|
+
return fences.flatMap((segment, index): (FigureSegment | ChartSegment)[] =>
|
|
151
|
+
segment.kind === "markdown" ? splitTableCharts(segment.text, this.#transient && index === last) : [segment],
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Mount the parts as children, a blank row between neighbours; a chart draws its own when it has rows. */
|
|
156
|
+
#mount(): void {
|
|
157
|
+
this.clear();
|
|
158
|
+
this.#parts.forEach((part, index) => {
|
|
159
|
+
if (index > 0 && part.kind !== "chart") this.addChild(new Spacer(1));
|
|
160
|
+
this.addChild(part.kind === "markdown" ? part.md : part.figure);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Push sources and streaming state down: only the last part can still be streaming. */
|
|
165
|
+
#syncTails(): void {
|
|
166
|
+
const last = this.#parts.length - 1;
|
|
167
|
+
this.#parts.forEach((part, index) => {
|
|
168
|
+
const streaming = this.#transient && index === last;
|
|
169
|
+
if (part.kind === "markdown") part.md.transientRenderCache = streaming;
|
|
170
|
+
else if (part.kind === "svg") part.figure.update(part.source, part.closed || !streaming);
|
|
171
|
+
else part.figure.update(part.table);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|