@oh-my-pi/pi-tui 18.8.5 → 18.8.7
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 +30 -16
- package/README.md +7 -5
- package/dist/types/chat/assistant-message.d.ts +1 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/user-message.d.ts +2 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/image.d.ts +1 -0
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +1 -0
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/native/blobs.d.ts +3 -1
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/terminal.d.ts +10 -2
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +11 -0
- package/package.json +13 -9
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.ts +8 -2
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/table-chart.ts +17 -20
- package/src/chat/tool-execution.ts +18 -0
- package/src/chat/user-message.ts +14 -0
- package/src/chrome/transcript-container.ts +32 -1
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/image.ts +7 -5
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.ts +190 -8
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.ts +5 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.ts +7 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/native/backend.ts +16 -15
- package/src/native/blobs.ts +43 -10
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/model-hub.ts +39 -4
- package/src/overlays/session-selector.ts +40 -22
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/prompt/attachment-chips.ts +13 -10
- package/src/prompt/tips.txt +2 -1
- package/src/prompt/welcome.ts +46 -9
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/terminal.ts +24 -3
- package/src/theme/theme.ts +1 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/edit.ts +142 -120
- package/src/tools/fetch.ts +5 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/mcp.ts +15 -2
- package/src/tools/read.ts +7 -5
- package/src/tools/write.ts +70 -56
- package/src/tools/yield.ts +225 -0
- package/src/tui.ts +25 -0
package/src/chat/table-chart.ts
CHANGED
|
@@ -16,8 +16,8 @@ import { chartAlt, renderChartSvg } from "../charts/chart-svg";
|
|
|
16
16
|
import { analyzeTable, type TableAnalysis } from "../charts/table-data";
|
|
17
17
|
import type { ImageBudget } from "../components/image";
|
|
18
18
|
import { lexDocument } from "../components/markdown";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { keyed } from "../native/describe";
|
|
20
|
+
import { nativeImageNode } from "../native/blobs";
|
|
21
21
|
import type { NativeNode } from "../native/node";
|
|
22
22
|
import { getThemeEpoch } from "../theme";
|
|
23
23
|
import type { Component } from "../tui";
|
|
@@ -85,11 +85,11 @@ interface ChartEntry {
|
|
|
85
85
|
let chartMode: TableChartMode = "off";
|
|
86
86
|
let chartPlanner: TableChartPlanner | undefined;
|
|
87
87
|
const charts = new Map<string, ChartEntry>();
|
|
88
|
-
/** The theme-resolved
|
|
89
|
-
const
|
|
88
|
+
/** The theme-resolved image node owning a chart's native bytes. */
|
|
89
|
+
const kNativeImage = Symbol("tableChart.nativeImage");
|
|
90
90
|
|
|
91
91
|
interface NativeTagged extends TableChart {
|
|
92
|
-
[
|
|
92
|
+
[kNativeImage]?: { epoch: number; image: NativeNode };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Set how tables become charts (the host's `tui.autoGraph`) and its `smart` planner; the host rebuilds the transcript after. */
|
|
@@ -205,24 +205,21 @@ function drawChart(analysis: TableAnalysis, plan: ChartPlan, guessed = false): T
|
|
|
205
205
|
export function describeTableChart(chart: TableChart, key: string): NativeNode {
|
|
206
206
|
const epoch = getThemeEpoch();
|
|
207
207
|
const tagged: NativeTagged = chart;
|
|
208
|
-
let entry = tagged[
|
|
208
|
+
let entry = tagged[kNativeImage];
|
|
209
209
|
if (entry?.epoch !== epoch) {
|
|
210
210
|
const svg = prepareSvg(chart.svg, svgFigurePalette());
|
|
211
|
-
entry = {
|
|
212
|
-
|
|
211
|
+
entry = {
|
|
212
|
+
epoch,
|
|
213
|
+
image: nativeImageNode(new TextEncoder().encode(svg), "image/svg+xml", {
|
|
214
|
+
alt: chart.alt,
|
|
215
|
+
w: chart.width,
|
|
216
|
+
h: chart.height,
|
|
217
|
+
max: { w: `${Math.round(chart.width / UNITS_PER_COLUMN)}ch` },
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
tagged[kNativeImage] = entry;
|
|
213
221
|
}
|
|
214
|
-
return
|
|
215
|
-
"image",
|
|
216
|
-
{
|
|
217
|
-
blob: entry.blob,
|
|
218
|
-
alt: chart.alt,
|
|
219
|
-
w: chart.width,
|
|
220
|
-
h: chart.height,
|
|
221
|
-
max: { w: `${Math.round(chart.width / UNITS_PER_COLUMN)}ch` },
|
|
222
|
-
},
|
|
223
|
-
undefined,
|
|
224
|
-
key,
|
|
225
|
-
);
|
|
222
|
+
return keyed(entry.image, key);
|
|
226
223
|
}
|
|
227
224
|
|
|
228
225
|
export interface TableChartFigureOptions {
|
|
@@ -170,6 +170,24 @@ class SafeToolRendererComponent implements Component {
|
|
|
170
170
|
invalidate.call(this.#component);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
releaseRenderCaches(): void {
|
|
174
|
+
const release = this.#component.releaseRenderCaches;
|
|
175
|
+
if (release === undefined) return;
|
|
176
|
+
try {
|
|
177
|
+
release.call(this.#component);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (!this.#warned) {
|
|
180
|
+
this.#warned = true;
|
|
181
|
+
logger.warn("Tool renderer failed", {
|
|
182
|
+
tool: this.#toolName,
|
|
183
|
+
stage: this.#stage,
|
|
184
|
+
phase: "releaseRenderCaches",
|
|
185
|
+
error: String(err),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
173
191
|
setIgnoreTight(ignore: boolean): void {
|
|
174
192
|
const setIgnoreTight = this.#component.setIgnoreTight;
|
|
175
193
|
if (setIgnoreTight === undefined) return;
|
package/src/chat/user-message.ts
CHANGED
|
@@ -177,6 +177,12 @@ export class UserMessageComponent extends Container implements ReactionTarget {
|
|
|
177
177
|
this.addChild(markdown);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
override releaseRenderCaches(): void {
|
|
181
|
+
this.#zoneSource = undefined;
|
|
182
|
+
this.#zoneLines = undefined;
|
|
183
|
+
super.releaseRenderCaches();
|
|
184
|
+
}
|
|
185
|
+
|
|
180
186
|
setReaction(emoji: string): void {
|
|
181
187
|
if (this.#reaction === emoji) return;
|
|
182
188
|
this.#reaction = emoji;
|
|
@@ -337,6 +343,10 @@ class SyntheticSummary implements Component {
|
|
|
337
343
|
this.#summary = summary;
|
|
338
344
|
}
|
|
339
345
|
|
|
346
|
+
releaseRenderCaches(): void {
|
|
347
|
+
this.#cache = undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
340
350
|
invalidate(): void {
|
|
341
351
|
this.#cache = undefined;
|
|
342
352
|
}
|
|
@@ -425,6 +435,10 @@ export class CollapsedSyntheticMessageComponent implements Component {
|
|
|
425
435
|
this.#disclosure.invalidate();
|
|
426
436
|
}
|
|
427
437
|
|
|
438
|
+
releaseRenderCaches(): void {
|
|
439
|
+
this.#disclosure.releaseRenderCaches();
|
|
440
|
+
}
|
|
441
|
+
|
|
428
442
|
dispose(): void {
|
|
429
443
|
this.#disclosure.dispose();
|
|
430
444
|
}
|
|
@@ -206,6 +206,7 @@ export class TranscriptContainer extends Container {
|
|
|
206
206
|
*/
|
|
207
207
|
#frameRows = new Map<TranscriptEntry, readonly string[]>();
|
|
208
208
|
#frameRowsWidth = 0;
|
|
209
|
+
#releaseFailures = new WeakSet<Component>();
|
|
209
210
|
/** The `children` array `#entries` last mirrored; see {@link #syncEntries}. */
|
|
210
211
|
#syncedChildren: Component[] | undefined;
|
|
211
212
|
/** Forces the next {@link #syncEntries} to compare every entry, not just the live tail. */
|
|
@@ -583,7 +584,10 @@ export class TranscriptContainer extends Container {
|
|
|
583
584
|
popLoopPhase();
|
|
584
585
|
}
|
|
585
586
|
this.#replayPending = false;
|
|
586
|
-
if (rows.length === 0)
|
|
587
|
+
if (rows.length === 0) {
|
|
588
|
+
this.#releaseCommittedRenderCaches();
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
587
591
|
const batch: HistoryBatch = { id: this.#nextBatchId++, rows, kind: "replay" };
|
|
588
592
|
this.#offered = { batch, kind: "replay" };
|
|
589
593
|
return batch;
|
|
@@ -747,6 +751,8 @@ export class TranscriptContainer extends Container {
|
|
|
747
751
|
this.#retireEntry(this.#entries[index]!);
|
|
748
752
|
}
|
|
749
753
|
this.#frontier = offered.end;
|
|
754
|
+
} else {
|
|
755
|
+
this.#releaseCommittedRenderCaches();
|
|
750
756
|
}
|
|
751
757
|
this.#offered = undefined;
|
|
752
758
|
if (this.#replayRequested) this.#startReplay();
|
|
@@ -766,6 +772,7 @@ export class TranscriptContainer extends Container {
|
|
|
766
772
|
const entry = this.#entries[index]!;
|
|
767
773
|
this.#setAllocation(entry.component, Number.MAX_SAFE_INTEGER, this.#lastFrame);
|
|
768
774
|
const block = trimBlankEdges(entry.component.render(width));
|
|
775
|
+
if (entry.state === "committed") this.#releaseRenderCaches(entry);
|
|
769
776
|
if (block.length === 0) continue;
|
|
770
777
|
if (rows.length > 0) rows.unshift("");
|
|
771
778
|
rows.unshift(...block);
|
|
@@ -813,6 +820,7 @@ export class TranscriptContainer extends Container {
|
|
|
813
820
|
for (const entry of this.#entries) {
|
|
814
821
|
this.#setAllocation(entry.component, Number.MAX_SAFE_INTEGER, this.#lastFrame);
|
|
815
822
|
const block = this.#renderEntry(entry, width);
|
|
823
|
+
if (entry.state === "committed") this.#releaseRenderCaches(entry);
|
|
816
824
|
if (block.length === 0) continue;
|
|
817
825
|
if (rows.length > 0) rows.push("");
|
|
818
826
|
this.#childStartRows.set(entry.component, rows.length);
|
|
@@ -1054,6 +1062,29 @@ export class TranscriptContainer extends Container {
|
|
|
1054
1062
|
entry.stableRows = EMPTY_STABLE_ROWS;
|
|
1055
1063
|
entry.renderedStableByWidth = new Map();
|
|
1056
1064
|
entry.stableRowCountByWidth = new Map();
|
|
1065
|
+
this.#frameRows.delete(entry);
|
|
1066
|
+
this.#releaseRenderCaches(entry);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#releaseCommittedRenderCaches(): void {
|
|
1070
|
+
for (let index = 0; index < this.#frontier; index++) {
|
|
1071
|
+
this.#releaseRenderCaches(this.#entries[index]!);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
#releaseRenderCaches(entry: TranscriptEntry): void {
|
|
1076
|
+
const release = entry.component.releaseRenderCaches;
|
|
1077
|
+
if (release === undefined) return;
|
|
1078
|
+
try {
|
|
1079
|
+
release.call(entry.component);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
if (this.#releaseFailures.has(entry.component)) return;
|
|
1082
|
+
this.#releaseFailures.add(entry.component);
|
|
1083
|
+
logger.warn("Transcript block failed to release render caches", {
|
|
1084
|
+
component: entry.component.constructor.name,
|
|
1085
|
+
error: String(err),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1057
1088
|
}
|
|
1058
1089
|
|
|
1059
1090
|
#startReplay(): void {
|
package/src/components/box.ts
CHANGED
|
@@ -135,6 +135,11 @@ export class Box implements Component {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
releaseRenderCaches(): void {
|
|
139
|
+
this.#invalidateCache();
|
|
140
|
+
for (const child of this.children) child.releaseRenderCaches?.();
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
/**
|
|
139
144
|
* A `card` when the box has a border or background: the background token
|
|
140
145
|
* picks tone/role (user message, tool state, selection), a border colour
|
|
@@ -100,6 +100,14 @@ export class Disclosure implements Component {
|
|
|
100
100
|
this.#body?.invalidate?.();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
releaseRenderCaches(): void {
|
|
104
|
+
this.#cache = undefined;
|
|
105
|
+
this.#boundedCache = undefined;
|
|
106
|
+
this.#summary?.releaseRenderCaches?.();
|
|
107
|
+
this.#collapsedBody?.releaseRenderCaches?.();
|
|
108
|
+
this.#body?.releaseRenderCaches?.();
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
dispose(): void {
|
|
104
112
|
if (this.#disposed) return;
|
|
105
113
|
this.#disposed = true;
|
package/src/components/image.ts
CHANGED
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
renderImage,
|
|
11
11
|
TERMINAL,
|
|
12
12
|
} from "../terminal-capabilities";
|
|
13
|
-
import {
|
|
14
|
-
import { node } from "../native/describe";
|
|
13
|
+
import { nativeImageNode } from "../native/blobs";
|
|
15
14
|
import type { DescribeContext, NativeNode } from "../native/node";
|
|
16
15
|
import type { Component } from "../tui";
|
|
17
16
|
|
|
@@ -826,6 +825,11 @@ export class Image implements Component {
|
|
|
826
825
|
this.#cachedWidth = undefined;
|
|
827
826
|
}
|
|
828
827
|
|
|
828
|
+
releaseRenderCaches(): void {
|
|
829
|
+
this.#cachedLines = undefined;
|
|
830
|
+
this.#cachedWidth = undefined;
|
|
831
|
+
}
|
|
832
|
+
|
|
829
833
|
/**
|
|
830
834
|
* SIXEL sequence for a target size. A new size starts the encode off the JS
|
|
831
835
|
* thread and answers `undefined` until it settles; the settled encode
|
|
@@ -865,11 +869,9 @@ export class Image implements Component {
|
|
|
865
869
|
*/
|
|
866
870
|
describe(_cx: DescribeContext): NativeNode {
|
|
867
871
|
if (this.#native) return this.#native;
|
|
868
|
-
const blob = registerNativeBlob(Buffer.from(this.#base64Data, "base64"), this.#mimeType);
|
|
869
872
|
const maxW = this.#options.maxWidthCells;
|
|
870
873
|
const maxH = this.#options.maxHeightCells;
|
|
871
|
-
this.#native =
|
|
872
|
-
blob,
|
|
874
|
+
this.#native = nativeImageNode(Buffer.from(this.#base64Data, "base64"), this.#mimeType, {
|
|
873
875
|
alt: imageFallback(this.#mimeType, this.#dimensions, this.#options.filename),
|
|
874
876
|
w: this.#dimensions.widthPx,
|
|
875
877
|
h: this.#dimensions.heightPx,
|
|
@@ -54,6 +54,10 @@ export class KeyValueList implements Component {
|
|
|
54
54
|
this.#native = undefined;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
releaseRenderCaches(): void {
|
|
58
|
+
this.#cache = undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
/** An aligned `kv` grid; label width and value wrapping are the terminal's. */
|
|
58
62
|
describe(_cx: DescribeContext): NativeNode {
|
|
59
63
|
this.#native ??= node("kv", {
|
|
@@ -168,6 +168,13 @@ export class Row implements Component, MouseRoutable {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
releaseRenderCaches(): void {
|
|
172
|
+
this.#memo = undefined;
|
|
173
|
+
for (const child of uniqueLayoutComponents(this.#children.map(item => item.content))) {
|
|
174
|
+
child.releaseRenderCaches?.();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
171
178
|
dispose(): void {
|
|
172
179
|
for (const child of uniqueLayoutComponents(this.#children.map(item => item.content))) {
|
|
173
180
|
child.dispose?.();
|
|
@@ -140,6 +140,13 @@ export class Stack implements Component, MouseRoutable {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
releaseRenderCaches(): void {
|
|
144
|
+
this.#memo = undefined;
|
|
145
|
+
for (const child of uniqueLayoutComponents(this.#children.map(item => item.content))) {
|
|
146
|
+
child.releaseRenderCaches?.();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
dispose(): void {
|
|
144
151
|
for (const child of uniqueLayoutComponents(this.#children.map(item => item.content))) {
|
|
145
152
|
child.dispose?.();
|
|
@@ -25,7 +25,8 @@ import { md } from "../native/describe";
|
|
|
25
25
|
import type { DescribeContext, NativeNode } from "../native/node";
|
|
26
26
|
import type { SymbolTheme } from "../symbols";
|
|
27
27
|
import { TERMINAL } from "../terminal-capabilities";
|
|
28
|
-
import { getSymbolTheme } from "../theme/theme";
|
|
28
|
+
import { getSymbolTheme, getThemeEpoch } from "../theme/theme";
|
|
29
|
+
import { canCacheMarkdownListItems } from "../theme/tui-adapters";
|
|
29
30
|
import type { Component } from "../tui";
|
|
30
31
|
import {
|
|
31
32
|
applyBackgroundToLine,
|
|
@@ -1390,10 +1391,27 @@ interface InlineStyleContext {
|
|
|
1390
1391
|
stylePrefix: string;
|
|
1391
1392
|
}
|
|
1392
1393
|
|
|
1393
|
-
type ListToken = Token & {
|
|
1394
|
+
type ListToken = Token & {
|
|
1395
|
+
items: Array<{ raw: string; tokens?: Token[] }>;
|
|
1396
|
+
ordered: boolean;
|
|
1397
|
+
start?: number;
|
|
1398
|
+
loose: boolean;
|
|
1399
|
+
};
|
|
1394
1400
|
type TableCellToken = { tokens?: Token[] };
|
|
1395
1401
|
type TableToken = Token & { header: TableCellToken[]; rows: TableCellToken[][]; raw?: string };
|
|
1396
1402
|
|
|
1403
|
+
/** `item` as marked lexes it inside a loose list: tight `text` blocks become paragraphs. */
|
|
1404
|
+
function loosenListItem(item: Tokens.ListItem): Tokens.ListItem {
|
|
1405
|
+
if (item.loose) return item;
|
|
1406
|
+
return {
|
|
1407
|
+
...item,
|
|
1408
|
+
loose: true,
|
|
1409
|
+
tokens: item.tokens.map(token =>
|
|
1410
|
+
token.type === "text" ? ({ ...token, type: "paragraph" } as Tokens.Paragraph) : token,
|
|
1411
|
+
),
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1397
1415
|
function formatHyperlink(text: string, target: string): string {
|
|
1398
1416
|
if (!TERMINAL.hyperlinks || !target) {
|
|
1399
1417
|
return text;
|
|
@@ -1743,6 +1761,25 @@ interface TailRowCache extends RenderSignature {
|
|
|
1743
1761
|
// type of token[i+1] when the rows were produced (blank/spacing gate).
|
|
1744
1762
|
nextTypes: (string | undefined)[];
|
|
1745
1763
|
}
|
|
1764
|
+
interface ListItemRowsCache {
|
|
1765
|
+
signature: RenderSignature;
|
|
1766
|
+
width: number;
|
|
1767
|
+
themeEpoch: number;
|
|
1768
|
+
ordered: boolean;
|
|
1769
|
+
start: number | undefined;
|
|
1770
|
+
loose: boolean;
|
|
1771
|
+
raws: string[];
|
|
1772
|
+
rows: RenderedLine[][];
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
interface StreamingListCache {
|
|
1776
|
+
text: string;
|
|
1777
|
+
before: Token[];
|
|
1778
|
+
start: number;
|
|
1779
|
+
tailStart: number;
|
|
1780
|
+
stableCount: number;
|
|
1781
|
+
list: Tokens.List;
|
|
1782
|
+
}
|
|
1746
1783
|
/**
|
|
1747
1784
|
* Mutable per-token record collector passed to #renderContentLines while
|
|
1748
1785
|
* rendering the streaming tail. The render loop fills `raws`/`nextTypes`
|
|
@@ -1856,6 +1893,8 @@ export class Markdown implements Component {
|
|
|
1856
1893
|
// the blank-replacement branch of setText and the fallback branch of
|
|
1857
1894
|
// #lexTokens.
|
|
1858
1895
|
#tailRowCache?: TailRowCache;
|
|
1896
|
+
#listItemRowsCache?: ListItemRowsCache;
|
|
1897
|
+
#streamingListCache?: StreamingListCache;
|
|
1859
1898
|
// True while #renderStreamingContentLines renders the frozen token range:
|
|
1860
1899
|
// frozen code blocks highlight even in transient mode so their bytes match
|
|
1861
1900
|
// the finalized render (they render once into the prefix line cache, so
|
|
@@ -1962,6 +2001,7 @@ export class Markdown implements Component {
|
|
|
1962
2001
|
// the non-append edit, so drop the frozen stream state here or it
|
|
1963
2002
|
// outlives the content it indexed.
|
|
1964
2003
|
this.#dropStreamPrefix();
|
|
2004
|
+
this.#listItemRowsCache = undefined;
|
|
1965
2005
|
// B+: the captured fast-path rows index the replaced content — drop
|
|
1966
2006
|
// the recipe so a fresh stream cannot splice onto stale rows.
|
|
1967
2007
|
this.#fastTail = undefined;
|
|
@@ -1976,6 +2016,14 @@ export class Markdown implements Component {
|
|
|
1976
2016
|
this.#cachedLines = undefined;
|
|
1977
2017
|
}
|
|
1978
2018
|
|
|
2019
|
+
releaseRenderCaches(): void {
|
|
2020
|
+
this.invalidate();
|
|
2021
|
+
this.#dropStreamPrefix();
|
|
2022
|
+
this.#streamingHighlightCache = undefined;
|
|
2023
|
+
this.#fastTail = undefined;
|
|
2024
|
+
this.#lastTailCapture = undefined;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1979
2027
|
/**
|
|
1980
2028
|
* The unrendered source as an `md` node, `stream` while the streaming
|
|
1981
2029
|
* (transient) cache is on. Append-only growth yields a fresh node of the
|
|
@@ -2031,7 +2079,9 @@ export class Markdown implements Component {
|
|
|
2031
2079
|
// render consumes it (see #lexTokens / the L2 hit path).
|
|
2032
2080
|
this.#streamPrefixLineCache = undefined;
|
|
2033
2081
|
this.#tailRowCache = undefined;
|
|
2082
|
+
this.#listItemRowsCache = undefined;
|
|
2034
2083
|
this.#streamingHighlightCache = undefined;
|
|
2084
|
+
this.#streamingListCache = undefined;
|
|
2035
2085
|
this.#streamRewound = undefined;
|
|
2036
2086
|
}
|
|
2037
2087
|
this.invalidate();
|
|
@@ -2108,32 +2158,98 @@ export class Markdown implements Component {
|
|
|
2108
2158
|
}
|
|
2109
2159
|
this.#lastScanLength = text.length;
|
|
2110
2160
|
this.#lastScanCanStream = canStream;
|
|
2161
|
+
if (!canStream) this.#listItemRowsCache = undefined;
|
|
2111
2162
|
this.#lastScanValid = true;
|
|
2112
2163
|
this.#appendOnlySinceLastScan = true;
|
|
2164
|
+
if (canStream && retainPrefix) {
|
|
2165
|
+
const tokens = this.#resumeStreamingList(text);
|
|
2166
|
+
if (tokens !== undefined) {
|
|
2167
|
+
this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2168
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2169
|
+
return tokens;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2113
2172
|
if (canStream && hasPrefix) {
|
|
2114
|
-
const tailTokens =
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
// can resolve a reference in the frozen prefix, which was lexed
|
|
2173
|
+
const tailTokens = lexDocument(refDefText);
|
|
2174
|
+
// A definition at any nesting depth registers for the whole document
|
|
2175
|
+
// and can resolve a reference in the frozen prefix, which was lexed
|
|
2118
2176
|
// without it, so any definition in the tail sends the text to a full lex.
|
|
2119
2177
|
if (Object.keys(tailTokens.links).length === 0) {
|
|
2120
2178
|
const tokens = [...prefixTokens, ...tailTokens];
|
|
2121
2179
|
if (retainPrefix) this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2122
2180
|
else this.#dropStreamPrefix();
|
|
2181
|
+
if (retainPrefix) this.#checkpointStreamingList(text, tokens);
|
|
2123
2182
|
return tokens;
|
|
2124
2183
|
}
|
|
2125
2184
|
}
|
|
2126
|
-
const tokens =
|
|
2185
|
+
const tokens = lexDocument(text);
|
|
2186
|
+
const hasReferenceDefinitions = Object.keys(tokens.links).length > 0;
|
|
2187
|
+
if (hasReferenceDefinitions) this.#lastScanCanStream = false;
|
|
2127
2188
|
// A definition frozen into the prefix would be missing from every later
|
|
2128
2189
|
// tail lex, so a full lex that registered any definition freezes nothing.
|
|
2129
|
-
if (canStream && retainPrefix &&
|
|
2190
|
+
if (canStream && retainPrefix && !hasReferenceDefinitions) {
|
|
2130
2191
|
this.#freezeStablePrefix(text, tokens, { preserveExisting: false });
|
|
2192
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2131
2193
|
} else {
|
|
2132
2194
|
this.#dropStreamPrefix();
|
|
2133
2195
|
}
|
|
2134
2196
|
return tokens;
|
|
2135
2197
|
}
|
|
2136
2198
|
|
|
2199
|
+
#resumeStreamingList(text: string): Token[] | undefined {
|
|
2200
|
+
const cached = this.#streamingListCache;
|
|
2201
|
+
if (cached === undefined || text.length <= cached.text.length || !text.startsWith(cached.text)) return undefined;
|
|
2202
|
+
const tail = lexDocument(text.slice(cached.tailStart));
|
|
2203
|
+
if (Object.keys(tail.links).length > 0) return undefined;
|
|
2204
|
+
const first = tail[0];
|
|
2205
|
+
if (first?.type !== "list") return undefined;
|
|
2206
|
+
const list = first as Tokens.List;
|
|
2207
|
+
if (!listMayContinueAt(list.raw, 0, cached.list.raw)) return undefined;
|
|
2208
|
+
const loose = cached.list.loose || list.loose;
|
|
2209
|
+
const completed = cached.list.items.slice(0, cached.stableCount);
|
|
2210
|
+
const items = [...completed, ...list.items];
|
|
2211
|
+
const merged: Tokens.List = {
|
|
2212
|
+
...cached.list,
|
|
2213
|
+
raw: text.slice(cached.start, cached.tailStart + list.raw.length),
|
|
2214
|
+
loose,
|
|
2215
|
+
items: loose ? items.map(loosenListItem) : items,
|
|
2216
|
+
};
|
|
2217
|
+
return [...cached.before, merged, ...tail.slice(1)];
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
#checkpointStreamingList(text: string, tokens: Token[]): void {
|
|
2221
|
+
const previous = this.#streamingListCache;
|
|
2222
|
+
this.#streamingListCache = undefined;
|
|
2223
|
+
let index = tokens.length - 1;
|
|
2224
|
+
while (tokens[index]?.type === "space") index--;
|
|
2225
|
+
const token = tokens[index];
|
|
2226
|
+
if (token?.type !== "list") return;
|
|
2227
|
+
const list = token as Tokens.List;
|
|
2228
|
+
let start = 0;
|
|
2229
|
+
for (let i = 0; i < index; i++) start += tokens[i]!.raw.length;
|
|
2230
|
+
// Late block delimiters can absorb an unfrozen preceding region. Bare
|
|
2231
|
+
// environments can also absorb list items through an equation prefix.
|
|
2232
|
+
if (start !== (this.#streamPrefixText?.length ?? 0) || BARE_ENV_BEGIN.test(list.raw)) return;
|
|
2233
|
+
const canResume = previous !== undefined && previous.start === start && text.startsWith(previous.text);
|
|
2234
|
+
let cursor = canResume ? previous.tailStart : start;
|
|
2235
|
+
let tailStart = start;
|
|
2236
|
+
let stableCount = 0;
|
|
2237
|
+
const indent = /^ */.exec(list.raw)![0].length;
|
|
2238
|
+
for (let i = canResume ? previous.stableCount : 0; i < list.items.length; i++) {
|
|
2239
|
+
const item = list.items[i]!;
|
|
2240
|
+
const position = text.indexOf(item.raw, cursor);
|
|
2241
|
+
if (position < cursor || position >= start + list.raw.length) return;
|
|
2242
|
+
// List continuation uses the first item's indentation. Resume only at
|
|
2243
|
+
// an item with that same indentation so the lexer sees the same context.
|
|
2244
|
+
if (/^ */.exec(item.raw)![0].length === indent) {
|
|
2245
|
+
tailStart = position;
|
|
2246
|
+
stableCount = i;
|
|
2247
|
+
}
|
|
2248
|
+
cursor = position + item.raw.length;
|
|
2249
|
+
}
|
|
2250
|
+
this.#streamingListCache = { text, before: tokens.slice(0, index), start, tailStart, stableCount, list };
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2137
2253
|
/**
|
|
2138
2254
|
* Drop the frozen lex prefix and the transient row caches keyed on it. A
|
|
2139
2255
|
* prefix set aside for #restoreRewoundPrefix checks itself against each
|
|
@@ -2146,6 +2262,8 @@ export class Markdown implements Component {
|
|
|
2146
2262
|
this.#streamSettledText = undefined;
|
|
2147
2263
|
this.#streamPrefixLineCache = undefined;
|
|
2148
2264
|
this.#tailRowCache = undefined;
|
|
2265
|
+
this.#listItemRowsCache = undefined;
|
|
2266
|
+
this.#streamingListCache = undefined;
|
|
2149
2267
|
}
|
|
2150
2268
|
|
|
2151
2269
|
/**
|
|
@@ -2186,6 +2304,8 @@ export class Markdown implements Component {
|
|
|
2186
2304
|
#rewindStreamPrefix(end: number, count: number): void {
|
|
2187
2305
|
this.#streamPrefixLineCache = rewoundLineCache(this.#streamPrefixLineCache, count);
|
|
2188
2306
|
this.#tailRowCache = undefined;
|
|
2307
|
+
this.#listItemRowsCache = undefined;
|
|
2308
|
+
this.#streamingListCache = undefined;
|
|
2189
2309
|
if (count === 0) {
|
|
2190
2310
|
this.#streamPrefixText = undefined;
|
|
2191
2311
|
this.#streamPrefixTokens = undefined;
|
|
@@ -2223,6 +2343,8 @@ export class Markdown implements Component {
|
|
|
2223
2343
|
this.#streamPrefixOpeners = rewound.openers;
|
|
2224
2344
|
this.#streamPrefixLineCache = rewound.lineCache;
|
|
2225
2345
|
this.#tailRowCache = rewound.tailRowCache;
|
|
2346
|
+
this.#listItemRowsCache = undefined;
|
|
2347
|
+
this.#streamingListCache = undefined;
|
|
2226
2348
|
}
|
|
2227
2349
|
|
|
2228
2350
|
// Freeze the largest run of leading blocks that end on a hard "\n\n" boundary
|
|
@@ -2591,6 +2713,7 @@ export class Markdown implements Component {
|
|
|
2591
2713
|
signature: RenderSignature,
|
|
2592
2714
|
contentWidth: number,
|
|
2593
2715
|
): string[] {
|
|
2716
|
+
if (tokens[tokens.length - 1]?.type !== "list") this.#listItemRowsCache = undefined;
|
|
2594
2717
|
const stableText = this.#streamPrefixText;
|
|
2595
2718
|
const stableTokenCount = this.#streamPrefixTokens?.length ?? 0;
|
|
2596
2719
|
if (stableText === undefined || stableTokenCount === 0 || !normalizedText.startsWith(stableText)) {
|
|
@@ -3594,6 +3717,41 @@ export class Markdown implements Component {
|
|
|
3594
3717
|
const indent = " ".repeat(depth);
|
|
3595
3718
|
// Use the list's start property (defaults to 1 for ordered lists)
|
|
3596
3719
|
const startNumber = token.start ?? 1;
|
|
3720
|
+
const signature =
|
|
3721
|
+
depth === 0 &&
|
|
3722
|
+
styleContext === undefined &&
|
|
3723
|
+
this.#defaultTextStyle === undefined &&
|
|
3724
|
+
this.#transientRenderCache &&
|
|
3725
|
+
!this.#renderingStablePrefix &&
|
|
3726
|
+
this.#lastScanCanStream &&
|
|
3727
|
+
canCacheMarkdownListItems(this.#theme)
|
|
3728
|
+
? this.#activeRenderSignature
|
|
3729
|
+
: undefined;
|
|
3730
|
+
const themeEpoch = signature === undefined ? undefined : getThemeEpoch();
|
|
3731
|
+
const previous = this.#listItemRowsCache;
|
|
3732
|
+
const reusable =
|
|
3733
|
+
signature !== undefined &&
|
|
3734
|
+
themeEpoch !== undefined &&
|
|
3735
|
+
previous !== undefined &&
|
|
3736
|
+
previous.width === width &&
|
|
3737
|
+
previous.themeEpoch === themeEpoch &&
|
|
3738
|
+
previous.ordered === token.ordered &&
|
|
3739
|
+
previous.start === token.start &&
|
|
3740
|
+
previous.loose === token.loose &&
|
|
3741
|
+
this.#signatureEquals(previous.signature, signature)
|
|
3742
|
+
? previous
|
|
3743
|
+
: undefined;
|
|
3744
|
+
let reusableCount = 0;
|
|
3745
|
+
if (reusable !== undefined) {
|
|
3746
|
+
while (
|
|
3747
|
+
reusableCount < token.items.length &&
|
|
3748
|
+
token.items[reusableCount]?.raw === reusable.raws[reusableCount]
|
|
3749
|
+
) {
|
|
3750
|
+
reusableCount++;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
const raws: string[] | undefined = signature === undefined ? undefined : [];
|
|
3754
|
+
const rows: RenderedLine[][] | undefined = signature === undefined ? undefined : [];
|
|
3597
3755
|
const pushWrapped = (line: RenderedLine, firstPrefix: string, continuationPrefix: string): void => {
|
|
3598
3756
|
if (line.literalCode) {
|
|
3599
3757
|
const wrappedLiteralRows = wrapTextWithAnsi(line.text, Math.max(1, width));
|
|
@@ -3629,6 +3787,14 @@ export class Markdown implements Component {
|
|
|
3629
3787
|
|
|
3630
3788
|
for (let i = 0; i < token.items.length; i++) {
|
|
3631
3789
|
const item = token.items[i];
|
|
3790
|
+
if (i < reusableCount && reusable !== undefined && raws !== undefined && rows !== undefined) {
|
|
3791
|
+
const cached = reusable.rows[i]!;
|
|
3792
|
+
for (const row of cached) lines.push(row);
|
|
3793
|
+
raws.push(item.raw);
|
|
3794
|
+
rows.push(cached);
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const rowStart = lines.length;
|
|
3632
3798
|
const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
|
|
3633
3799
|
const firstPrefix = indent + this.#theme.listBullet(bullet);
|
|
3634
3800
|
// Continuation rows align under the item text, so the hang matches the
|
|
@@ -3658,6 +3824,22 @@ export class Markdown implements Component {
|
|
|
3658
3824
|
} else {
|
|
3659
3825
|
lines.push(renderedLine(firstPrefix));
|
|
3660
3826
|
}
|
|
3827
|
+
if (raws !== undefined && rows !== undefined) {
|
|
3828
|
+
raws.push(item.raw);
|
|
3829
|
+
rows.push(lines.slice(rowStart));
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
if (signature !== undefined && themeEpoch !== undefined && raws !== undefined && rows !== undefined) {
|
|
3833
|
+
this.#listItemRowsCache = {
|
|
3834
|
+
signature,
|
|
3835
|
+
width,
|
|
3836
|
+
themeEpoch,
|
|
3837
|
+
ordered: token.ordered,
|
|
3838
|
+
start: token.start,
|
|
3839
|
+
loose: token.loose,
|
|
3840
|
+
raws,
|
|
3841
|
+
rows,
|
|
3842
|
+
};
|
|
3661
3843
|
}
|
|
3662
3844
|
|
|
3663
3845
|
return lines;
|
|
@@ -408,6 +408,11 @@ export class ScrollView implements Component {
|
|
|
408
408
|
this.#child?.invalidate?.();
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
releaseRenderCaches(): void {
|
|
412
|
+
this.#cache = undefined;
|
|
413
|
+
this.#child?.releaseRenderCaches?.();
|
|
414
|
+
}
|
|
415
|
+
|
|
411
416
|
/** Permanently release a wrapped child's timers, subscriptions, and other resources. */
|
|
412
417
|
dispose(): void {
|
|
413
418
|
if (this.#disposed) return;
|
|
@@ -51,6 +51,11 @@ export class Section implements Component {
|
|
|
51
51
|
this.#cache = undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
releaseRenderCaches(): void {
|
|
55
|
+
this.#cache = undefined;
|
|
56
|
+
if (isComponent(this.#options.body)) this.#options.body.releaseRenderCaches?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
dispose(): void {
|
|
55
60
|
if (this.#disposed) return;
|
|
56
61
|
this.#disposed = true;
|
package/src/components/table.ts
CHANGED
|
@@ -117,6 +117,10 @@ export class Table implements Component {
|
|
|
117
117
|
this.#native = undefined;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
releaseRenderCaches(): void {
|
|
121
|
+
this.#cache = undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
/**
|
|
121
125
|
* A native `table`. Column priorities carry over unchanged (lower gives
|
|
122
126
|
* way first); truncating columns truncate at the end, the rest keep
|
package/src/components/text.ts
CHANGED
|
@@ -109,6 +109,13 @@ export class Text implements Component {
|
|
|
109
109
|
this.#cachedLines = undefined;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
releaseRenderCaches(): void {
|
|
113
|
+
this.#cachedText = undefined;
|
|
114
|
+
this.#cachedWidth = undefined;
|
|
115
|
+
this.#cachedWidthConfigEpoch = undefined;
|
|
116
|
+
this.#cachedLines = undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
/**
|
|
113
120
|
* Wrapped text; styling re-derived from the styled source so a theme swap
|
|
114
121
|
* re-resolves tokens. A background fill becomes the node's tone.
|