@oh-my-pi/pi-tui 18.11.2 → 18.12.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 +37 -100
- package/README.md +36 -0
- package/dist/types/components/markdown.d.ts +4 -3
- package/dist/types/overlays/approval-dialog.d.ts +25 -15
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +2 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/render/hanging-wrap.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/terminal-capabilities.d.ts +8 -1
- package/dist/types/terminal-multiplexer.d.ts +6 -0
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/read.d.ts +13 -5
- package/dist/types/tools/task.d.ts +8 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +3 -1
- package/src/chat/read-tool-group.ts +17 -9
- package/src/chat/tool-execution.ts +23 -6
- package/src/components/image.ts +31 -14
- package/src/components/markdown.ts +74 -5
- package/src/overlays/approval-dialog.ts +104 -36
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/session-selector.ts +14 -9
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/render/hanging-wrap.ts +28 -1
- package/src/render/render-utils.ts +16 -2
- package/src/status-line/component.ts +1 -0
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/types.ts +1 -0
- package/src/terminal-capabilities.ts +35 -21
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.ts +11 -2
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.ts +46 -14
- package/src/tmux.ts +7 -1
- package/src/tools/ast-edit.ts +28 -4
- package/src/tools/ast-grep.ts +24 -4
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.ts +77 -16
- package/src/tools/find.ts +65 -15
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.ts +94 -11
- package/src/tools/lsp.ts +23 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/read.ts +52 -22
- package/src/tools/task.ts +17 -7
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.ts +17 -5
- package/src/tools/yield.ts +2 -2
- package/src/tui.ts +36 -0
|
@@ -102,6 +102,10 @@ export interface TaskItem {
|
|
|
102
102
|
name?: string;
|
|
103
103
|
/** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
|
|
104
104
|
agent?: string;
|
|
105
|
+
/** Historical source session selected by the bundled consult agent. */
|
|
106
|
+
sourceSession?: string;
|
|
107
|
+
/** Optional model selector override for the historical source session. */
|
|
108
|
+
model?: string | string[];
|
|
105
109
|
/** The work; required by the schema. */
|
|
106
110
|
task?: string;
|
|
107
111
|
/** How open-ended the work is; required by the schema and the child's sole `auto` thinking classification input. */
|
|
@@ -128,6 +132,10 @@ export interface TaskParams {
|
|
|
128
132
|
name?: string;
|
|
129
133
|
/** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
|
|
130
134
|
agent?: string;
|
|
135
|
+
/** Historical source session selected by the bundled consult agent. */
|
|
136
|
+
sourceSession?: string;
|
|
137
|
+
/** Optional model selector override for the historical source session. */
|
|
138
|
+
model?: string | string[];
|
|
131
139
|
/** The work (flat form). */
|
|
132
140
|
task?: string;
|
|
133
141
|
/** How open-ended the work is (flat form); see {@link TaskItem.solutionSpace}. */
|
|
@@ -73,6 +73,8 @@ interface WriteStreamingPreviewStateCarrier {
|
|
|
73
73
|
/** Render context for the write tool: resolves an `xd://`-mounted tool so its live renderer drives device dispatch previews. */
|
|
74
74
|
export interface WriteRenderContext {
|
|
75
75
|
resolveXdevMounted?: (name: string) => XdevMountedRenderer | undefined;
|
|
76
|
+
/** Original result metadata used only for filesystem link targets. */
|
|
77
|
+
rawDetails?: unknown;
|
|
76
78
|
}
|
|
77
79
|
/** Render file writes and delegated tool-device calls. */
|
|
78
80
|
export declare const writeToolRenderer: {
|
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
|
+
"version": "18.12.0",
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.
|
|
44
|
+
"@oh-my-pi/omptype": "18.12.0",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.12.0",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.12.0",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.12.0",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.12.0",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.12.0",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.12.0",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.12.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { presentNarrationAsAnswer } from "@oh-my-pi/pi-ai/utils/narration";
|
|
2
3
|
import { type Component, Container } from "../tui";
|
|
3
4
|
import { Image, type ImageBudget } from "../components/image";
|
|
4
5
|
import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
@@ -1803,7 +1804,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
1803
1804
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
1804
1805
|
// Everything below renders the display form; #lastMessage keeps the
|
|
1805
1806
|
// verbatim message so re-renders re-derive the reaction deterministically.
|
|
1806
|
-
|
|
1807
|
+
// Narration the provider returned as thinking renders as answer text.
|
|
1808
|
+
message = this.#displayMessage(presentNarrationAsAnswer(message), this.#lastUpdateTransient);
|
|
1807
1809
|
this.#displayedMessage = message;
|
|
1808
1810
|
|
|
1809
1811
|
// Streaming-speed gauge: only a live, in-flight render of the single
|
|
@@ -22,7 +22,7 @@ import { canonicalizeMessage } from "./thinking-display";
|
|
|
22
22
|
import { internalUrlSchemeSpec, splitUrlScheme } from "../tools/url-scheme-host";
|
|
23
23
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
24
24
|
import { formatUsageRow } from "../overlays/usage-row";
|
|
25
|
-
import { formatCount } from "@oh-my-pi/pi-utils";
|
|
25
|
+
import { formatCount, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
|
|
26
26
|
import type { TspCardStatus, TspSpan, TspText } from "@oh-my-pi/pi-wire";
|
|
27
27
|
import type { NativeToolHead } from "../tools/renderer";
|
|
28
28
|
import { card, code, keyed, md, node, span, text, withHidden } from "../native/describe";
|
|
@@ -188,9 +188,12 @@ const READ_STATUS_RANK: Record<ReadEntry["status"], number> = {
|
|
|
188
188
|
error: 3,
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
function getDisplayReadTargets(
|
|
191
|
+
function getDisplayReadTargets(
|
|
192
|
+
details: ReadToolDetails | undefined,
|
|
193
|
+
rawDetails: ReadToolDetails | undefined,
|
|
194
|
+
): ReadDisplayPathSpec[] | undefined {
|
|
192
195
|
if (!Array.isArray(details?.displayReadTargets)) return undefined;
|
|
193
|
-
const links = Array.isArray(
|
|
196
|
+
const links = Array.isArray(rawDetails?.displayReadTargetLinks) ? rawDetails.displayReadTargetLinks : undefined;
|
|
194
197
|
const targets: ReadDisplayPathSpec[] = [];
|
|
195
198
|
details.displayReadTargets.forEach((raw, index) => {
|
|
196
199
|
if (typeof raw !== "string") return;
|
|
@@ -437,12 +440,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
437
440
|
if (!toolCallId) return;
|
|
438
441
|
const rawPath =
|
|
439
442
|
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
443
|
+
const displayPath = sanitizeText(rawPath);
|
|
440
444
|
const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
|
|
441
445
|
toolCallId,
|
|
442
|
-
path:
|
|
446
|
+
path: displayPath,
|
|
443
447
|
status: "pending",
|
|
444
448
|
};
|
|
445
|
-
entry.path =
|
|
449
|
+
entry.path = displayPath;
|
|
450
|
+
entry.linkPath = path.isAbsolute(rawPath) ? rawPath : undefined;
|
|
446
451
|
entry.raw = args.raw === true;
|
|
447
452
|
this.#entries.set(toolCallId, entry);
|
|
448
453
|
this.#updateDisplay();
|
|
@@ -484,12 +489,15 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
484
489
|
if (!entry) return;
|
|
485
490
|
if (isPartial) return;
|
|
486
491
|
this.#blockVersion++;
|
|
487
|
-
|
|
492
|
+
// `details` can contain raw file text and filesystem paths. Keep the
|
|
493
|
+
// latter for links while only the sanitized copy supplies display labels.
|
|
494
|
+
const rawDetails = result.details as ReadToolDetails | undefined;
|
|
495
|
+
const details = sanitizeTextDeep(rawDetails) as ReadToolDetails | undefined;
|
|
488
496
|
const suffixResolution = getSuffixResolution(details);
|
|
489
|
-
const displayPaths = getDisplayReadTargets(details);
|
|
490
|
-
entry.linkPath = readResultLinkPath(
|
|
497
|
+
const displayPaths = getDisplayReadTargets(details, rawDetails);
|
|
498
|
+
entry.linkPath = readResultLinkPath(rawDetails) ?? entry.linkPath;
|
|
491
499
|
if (suffixResolution) {
|
|
492
|
-
entry.path = displayPathWithSuffixResolution(entry.path, suffixResolution);
|
|
500
|
+
entry.path = sanitizeText(displayPathWithSuffixResolution(entry.path, suffixResolution));
|
|
493
501
|
entry.correctedFrom = suffixResolution.from;
|
|
494
502
|
entry.displayPaths = undefined;
|
|
495
503
|
} else {
|
|
@@ -8,7 +8,7 @@ import { Text } from "../components/text";
|
|
|
8
8
|
import { getImageDimensions, ImageProtocol, imageFallback, TERMINAL } from "../terminal-capabilities";
|
|
9
9
|
import { type Component, Container, type TUI } from "../tui";
|
|
10
10
|
import { truncateToWidth } from "../utils";
|
|
11
|
-
import { getProjectDir, isRecord, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { getProjectDir, isRecord, logger, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Theme } from "../theme/theme";
|
|
13
13
|
import { ensureThemeSync, getThemeEpoch, theme } from "../theme/theme";
|
|
14
14
|
import {
|
|
@@ -420,6 +420,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
420
420
|
isError?: boolean;
|
|
421
421
|
details?: unknown;
|
|
422
422
|
};
|
|
423
|
+
// Machine paths remain verbatim; only #result.details is handed to display renderers.
|
|
424
|
+
#rawDetails: unknown;
|
|
423
425
|
// Edit preview state
|
|
424
426
|
#editMode?: EditMode;
|
|
425
427
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
@@ -632,7 +634,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
632
634
|
const partialResultPainted = this.#partialResultShapePainted;
|
|
633
635
|
this.#firstResultViewportRepaintShapePainted = false;
|
|
634
636
|
this.#partialResultShapePainted = false;
|
|
635
|
-
this.#
|
|
637
|
+
this.#rawDetails = result.details;
|
|
638
|
+
// Renderers receive sanitized details for display; raw paths are available
|
|
639
|
+
// separately for links and media through the built-in render context.
|
|
640
|
+
const details = sanitizeTextDeep(result.details);
|
|
641
|
+
this.#result = details === result.details ? result : { ...result, details };
|
|
636
642
|
this.#resultVersion++;
|
|
637
643
|
this.#blockVersion++;
|
|
638
644
|
this.#isPartial = isPartial;
|
|
@@ -1251,7 +1257,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
1251
1257
|
#nativeResultImages(body: readonly NativeChild[] | undefined): NativeNode[] {
|
|
1252
1258
|
if (!this.#result || !this.#showImages) return [];
|
|
1253
1259
|
const shown = shownMedia(body);
|
|
1254
|
-
const mediaPath = readMediaPath(this.#
|
|
1260
|
+
const mediaPath = readMediaPath(this.#rawDetails);
|
|
1255
1261
|
if (mediaPath && shown.paths.has(mediaPath)) return [];
|
|
1256
1262
|
const images: NativeNode[] = [];
|
|
1257
1263
|
this.#getAllImageBlocks().forEach((image, index) => {
|
|
@@ -1558,6 +1564,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
1558
1564
|
this.#contentBox.clear();
|
|
1559
1565
|
|
|
1560
1566
|
const renderContext = this.#buildRenderContext();
|
|
1567
|
+
const rawPerFileResults =
|
|
1568
|
+
isRecord(this.#rawDetails) && Array.isArray(this.#rawDetails.perFileResults)
|
|
1569
|
+
? this.#rawDetails.perFileResults
|
|
1570
|
+
: undefined;
|
|
1561
1571
|
this.#renderState.renderContext = renderContext;
|
|
1562
1572
|
|
|
1563
1573
|
for (let i = 0; i < perFileResults.length; i++) {
|
|
@@ -1569,9 +1579,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
1569
1579
|
}
|
|
1570
1580
|
const fileBox = new Box(0, 0);
|
|
1571
1581
|
try {
|
|
1582
|
+
const fileOptions = {
|
|
1583
|
+
...this.#renderState,
|
|
1584
|
+
renderContext: { ...renderContext, rawDetails: rawPerFileResults?.[i] },
|
|
1585
|
+
};
|
|
1572
1586
|
const resultComponent = renderer.renderResult(
|
|
1573
1587
|
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
1574
|
-
|
|
1588
|
+
fileOptions,
|
|
1575
1589
|
theme,
|
|
1576
1590
|
);
|
|
1577
1591
|
if (resultComponent) {
|
|
@@ -1695,7 +1709,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
1695
1709
|
|
|
1696
1710
|
if (this.#result) {
|
|
1697
1711
|
const imageBlocks = this.#getAllImageBlocks();
|
|
1698
|
-
const sourcePath = readMediaPath(this.#
|
|
1712
|
+
const sourcePath = readMediaPath(this.#rawDetails);
|
|
1699
1713
|
|
|
1700
1714
|
for (let i = 0; i < imageBlocks.length; i++) {
|
|
1701
1715
|
const img = imageBlocks[i];
|
|
@@ -1802,6 +1816,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
1802
1816
|
return Math.max(1, Math.min(maxSeconds, value));
|
|
1803
1817
|
};
|
|
1804
1818
|
|
|
1819
|
+
if (this.#renderer && !this.#tool?.renderResult && this.#rawDetails !== undefined) {
|
|
1820
|
+
context.rawDetails = this.#rawDetails;
|
|
1821
|
+
}
|
|
1805
1822
|
if (this.#toolName === "bash") {
|
|
1806
1823
|
// Bash needs render context even before a result exists. The renderer uses the pending-call args
|
|
1807
1824
|
// plus this context to keep the inline command preview visible while tool-call JSON is still streaming.
|
|
@@ -1882,7 +1899,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
1882
1899
|
const dims = img.data ? (getImageDimensions(img.data, mimeType) ?? undefined) : undefined;
|
|
1883
1900
|
const placeholder = imageFallback(mimeType, dims);
|
|
1884
1901
|
return img.data
|
|
1885
|
-
? linkedImageText(placeholder, { data: img.data, mimeType }, readMediaPath(this.#
|
|
1902
|
+
? linkedImageText(placeholder, { data: img.data, mimeType }, readMediaPath(this.#rawDetails))
|
|
1886
1903
|
: placeholder;
|
|
1887
1904
|
})
|
|
1888
1905
|
.join("\n");
|
package/src/components/image.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
encodeSixelNow,
|
|
5
5
|
getCellDimensions,
|
|
6
6
|
getImageDimensions,
|
|
7
|
+
getTerminalBackgroundColor,
|
|
7
8
|
type ImageDimensions,
|
|
8
9
|
ImageProtocol,
|
|
9
10
|
imageFallback,
|
|
@@ -41,6 +42,13 @@ export interface ImageOptions {
|
|
|
41
42
|
requestRender?: () => void;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
interface SixelEncode {
|
|
46
|
+
widthPx: number;
|
|
47
|
+
heightPx: number;
|
|
48
|
+
backgroundRgb: number;
|
|
49
|
+
sequence?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
/** Renders in progress whose rows are bound for native scrollback. */
|
|
45
53
|
let scrollbackRenderDepth = 0;
|
|
46
54
|
|
|
@@ -655,6 +663,7 @@ export class Image implements Component {
|
|
|
655
663
|
#cachedImageProtocol: typeof TERMINAL.imageProtocol = null;
|
|
656
664
|
#cachedCellWidthPx = 0;
|
|
657
665
|
#cachedCellHeightPx = 0;
|
|
666
|
+
#cachedSixelBackground: number | undefined;
|
|
658
667
|
#cachedKittyUnicodePlaceholders = false;
|
|
659
668
|
#cachedLinked = false;
|
|
660
669
|
// Tallest graphic placement this image has rendered. The text fallback
|
|
@@ -663,8 +672,8 @@ export class Image implements Component {
|
|
|
663
672
|
#renderedGraphicRows = 0;
|
|
664
673
|
/** Described node and the link policy it was built under (an `off` switch drops its `href`). */
|
|
665
674
|
#native?: { linked: boolean; node: NativeNode };
|
|
666
|
-
/** Newest SIXEL encode:
|
|
667
|
-
#sixel?:
|
|
675
|
+
/** Newest SIXEL encode: target size, compositing background, and settled sequence. */
|
|
676
|
+
#sixel?: SixelEncode;
|
|
668
677
|
|
|
669
678
|
constructor(
|
|
670
679
|
base64Data: string,
|
|
@@ -704,33 +713,38 @@ export class Image implements Component {
|
|
|
704
713
|
}
|
|
705
714
|
|
|
706
715
|
/**
|
|
707
|
-
* SIXEL sequence for a target size. A
|
|
716
|
+
* SIXEL sequence for a target size and background. A change starts the encode off the JS
|
|
708
717
|
* thread and answers `undefined` until it settles; the settled encode
|
|
709
718
|
* invalidates the cached lines and requests a repaint. A render bound for
|
|
710
719
|
* scrollback cannot wait, so it encodes synchronously.
|
|
711
720
|
*/
|
|
712
|
-
#sixelSequence(widthPx: number, heightPx: number): string | null | undefined {
|
|
721
|
+
#sixelSequence(widthPx: number, heightPx: number, backgroundRgb: number): string | null | undefined {
|
|
713
722
|
const current = this.#sixel;
|
|
714
|
-
const
|
|
715
|
-
|
|
723
|
+
const sameInput =
|
|
724
|
+
current !== undefined &&
|
|
725
|
+
current.widthPx === widthPx &&
|
|
726
|
+
current.heightPx === heightPx &&
|
|
727
|
+
current.backgroundRgb === backgroundRgb;
|
|
728
|
+
if (sameInput && current.sequence !== undefined) return current.sequence;
|
|
716
729
|
if (scrollbackRenderDepth > 0) {
|
|
717
730
|
// Replacing the entry makes a still-pending async encode settle as stale.
|
|
718
|
-
const sequence = encodeSixelNow(this.#base64Data, widthPx, heightPx);
|
|
719
|
-
this.#sixel = { widthPx, heightPx, sequence };
|
|
731
|
+
const sequence = encodeSixelNow(this.#base64Data, widthPx, heightPx, backgroundRgb);
|
|
732
|
+
this.#sixel = { widthPx, heightPx, backgroundRgb, sequence };
|
|
720
733
|
return sequence;
|
|
721
734
|
}
|
|
722
|
-
if (
|
|
723
|
-
const request:
|
|
735
|
+
if (sameInput) return undefined;
|
|
736
|
+
const request: SixelEncode = { widthPx, heightPx, backgroundRgb };
|
|
724
737
|
this.#sixel = request;
|
|
725
738
|
const settle = (sequence: string | null): void => {
|
|
726
739
|
request.sequence = sequence;
|
|
727
|
-
if (this.#sixel !== request) return;
|
|
740
|
+
if (this.#sixel !== request || getTerminalBackgroundColor() !== backgroundRgb) return;
|
|
728
741
|
this.invalidate();
|
|
729
742
|
if (this.#options.requestRender) this.#options.requestRender();
|
|
730
743
|
else this.#budget?.requestRender();
|
|
731
744
|
};
|
|
732
|
-
encodeSixelAsync(new Uint8Array(Buffer.from(this.#base64Data, "base64")), widthPx, heightPx).then(
|
|
733
|
-
settle
|
|
745
|
+
encodeSixelAsync(new Uint8Array(Buffer.from(this.#base64Data, "base64")), widthPx, heightPx, backgroundRgb).then(
|
|
746
|
+
settle,
|
|
747
|
+
() => settle(null),
|
|
734
748
|
);
|
|
735
749
|
return undefined;
|
|
736
750
|
}
|
|
@@ -768,6 +782,7 @@ export class Image implements Component {
|
|
|
768
782
|
const hasProtocol = imageProtocol != null;
|
|
769
783
|
const cellDimensions = getCellDimensions();
|
|
770
784
|
const kittyUnicodePlaceholders = getKittyGraphics().unicodePlaceholders;
|
|
785
|
+
const sixelBackground = imageProtocol === ImageProtocol.Sixel ? getTerminalBackgroundColor() : undefined;
|
|
771
786
|
// observe() must run on every pass — even a cache hit — so the image keeps
|
|
772
787
|
// its display-order slot in the budget. Only graphics-capable frames count
|
|
773
788
|
// toward (and are demoted by) the budget; without a protocol every image is
|
|
@@ -789,6 +804,7 @@ export class Image implements Component {
|
|
|
789
804
|
this.#cachedImageProtocol === imageProtocol &&
|
|
790
805
|
this.#cachedCellWidthPx === cellDimensions.widthPx &&
|
|
791
806
|
this.#cachedCellHeightPx === cellDimensions.heightPx &&
|
|
807
|
+
this.#cachedSixelBackground === sixelBackground &&
|
|
792
808
|
this.#cachedKittyUnicodePlaceholders === kittyUnicodePlaceholders &&
|
|
793
809
|
// The text fallback embeds a link only under an enabled hyperlink policy.
|
|
794
810
|
this.#cachedLinked === linked &&
|
|
@@ -813,7 +829,7 @@ export class Image implements Component {
|
|
|
813
829
|
maxHeightCells: this.#options.maxHeightCells,
|
|
814
830
|
imageId: this.#imageId,
|
|
815
831
|
includeTransmit: needsTransmit,
|
|
816
|
-
sixel: (widthPx, heightPx) => this.#sixelSequence(widthPx, heightPx),
|
|
832
|
+
sixel: (widthPx, heightPx) => this.#sixelSequence(widthPx, heightPx, sixelBackground!),
|
|
817
833
|
});
|
|
818
834
|
|
|
819
835
|
if (result?.transmit && this.#imageId != null && this.#budget !== undefined) {
|
|
@@ -861,6 +877,7 @@ export class Image implements Component {
|
|
|
861
877
|
this.#cachedImageProtocol = imageProtocol;
|
|
862
878
|
this.#cachedCellWidthPx = cellDimensions.widthPx;
|
|
863
879
|
this.#cachedCellHeightPx = cellDimensions.heightPx;
|
|
880
|
+
this.#cachedSixelBackground = sixelBackground;
|
|
864
881
|
this.#cachedKittyUnicodePlaceholders = kittyUnicodePlaceholders;
|
|
865
882
|
this.#cachedLinked = linked;
|
|
866
883
|
|
|
@@ -1779,6 +1779,33 @@ function loosenListItem(item: Tokens.ListItem): Tokens.ListItem {
|
|
|
1779
1779
|
};
|
|
1780
1780
|
}
|
|
1781
1781
|
|
|
1782
|
+
const REFERENCE_LABEL_RE = /\[[^[\]]*[^\s[\]][^[\]]*\]/;
|
|
1783
|
+
const LABEL_FREE_TOKENS = new Set(["checkbox", "code", "codespan", "escape", "html", "image", "math"]);
|
|
1784
|
+
|
|
1785
|
+
function labelSource(tokens: readonly Token[]): string {
|
|
1786
|
+
let source = "";
|
|
1787
|
+
for (const token of tokens) {
|
|
1788
|
+
if (token.type === "list") {
|
|
1789
|
+
for (const item of (token as Tokens.List).items) source += `${labelSource(item.tokens)}\n\n`;
|
|
1790
|
+
} else if (token.type === "table") {
|
|
1791
|
+
const table = token as TableToken;
|
|
1792
|
+
for (const cell of [...table.header, ...table.rows.flat()]) source += `${labelSource(cell.tokens ?? [])}\n\n`;
|
|
1793
|
+
} else if ("tokens" in token && token.tokens) {
|
|
1794
|
+
source += labelSource(token.tokens);
|
|
1795
|
+
} else {
|
|
1796
|
+
source += LABEL_FREE_TOKENS.has(token.type) ? " " : token.raw;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return source;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** Whether a reference definition arriving later could still turn part of `item` into a link. */
|
|
1803
|
+
function hasUnresolvedReferenceLabel(item: Tokens.ListItem): boolean {
|
|
1804
|
+
return labelSource(item.tokens)
|
|
1805
|
+
.split(BLANK_LINE_RE)
|
|
1806
|
+
.some(block => REFERENCE_LABEL_RE.test(block));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1782
1809
|
function formatHyperlink(text: string, target: string): string {
|
|
1783
1810
|
if (!TERMINAL.hyperlinks || !target) {
|
|
1784
1811
|
return text;
|
|
@@ -2145,6 +2172,7 @@ interface StreamingListCache {
|
|
|
2145
2172
|
start: number;
|
|
2146
2173
|
tailStart: number;
|
|
2147
2174
|
stableCount: number;
|
|
2175
|
+
referenceStop?: number;
|
|
2148
2176
|
list: Tokens.List;
|
|
2149
2177
|
}
|
|
2150
2178
|
/**
|
|
@@ -2232,6 +2260,13 @@ export class Markdown implements Component {
|
|
|
2232
2260
|
// The frozen prefix's leading run in front of the first of those openers:
|
|
2233
2261
|
// no append can change its tokens, so getLastRenderStableText publishes it.
|
|
2234
2262
|
#streamSettledText?: string;
|
|
2263
|
+
// The text in front of the newest top-level item of a list that is still
|
|
2264
|
+
// streaming as the last block. Once an item's marker line has ended, no
|
|
2265
|
+
// append can add to the items in front of it, and #renderList draws each
|
|
2266
|
+
// item from its own tokens alone, so their rows are final. Kept while later
|
|
2267
|
+
// texts extend it, so the published prefix never shrinks when the list
|
|
2268
|
+
// stops being the last block.
|
|
2269
|
+
#streamListSettledText?: string;
|
|
2235
2270
|
// The prefix before a rewind that only a closer on the still-growing last
|
|
2236
2271
|
// line caused. Once a later text holds no closer line after it, that
|
|
2237
2272
|
// prefix is right again, so #lexTokens puts it back.
|
|
@@ -2411,14 +2446,18 @@ export class Markdown implements Component {
|
|
|
2411
2446
|
}
|
|
2412
2447
|
|
|
2413
2448
|
/**
|
|
2414
|
-
* Width-independent source prefix of the last render
|
|
2415
|
-
*
|
|
2416
|
-
* display-math opener whose block an append could still close
|
|
2449
|
+
* Width-independent source prefix of the last render that no append can
|
|
2450
|
+
* change: up to a frozen Markdown block boundary, stopping in front of a
|
|
2451
|
+
* display-math opener whose block an append could still close, or, while a
|
|
2452
|
+
* list streams as the last block, up to its newest top-level item. Only
|
|
2417
2453
|
* meaningful while streaming (transient render cache on); grows
|
|
2418
2454
|
* monotonically under append-only `setText`.
|
|
2419
2455
|
*/
|
|
2420
2456
|
getLastRenderStableText(): string {
|
|
2421
|
-
|
|
2457
|
+
if (!this.#transientRenderCache) return "";
|
|
2458
|
+
const settled = this.#streamSettledText ?? "";
|
|
2459
|
+
const list = this.#streamListSettledText;
|
|
2460
|
+
return list !== undefined && list.length > settled.length ? list : settled;
|
|
2422
2461
|
}
|
|
2423
2462
|
|
|
2424
2463
|
get transientRenderCache(): boolean {
|
|
@@ -2589,6 +2628,9 @@ export class Markdown implements Component {
|
|
|
2589
2628
|
#checkpointStreamingList(text: string, tokens: Token[]): void {
|
|
2590
2629
|
const previous = this.#streamingListCache;
|
|
2591
2630
|
this.#streamingListCache = undefined;
|
|
2631
|
+
if (this.#streamListSettledText !== undefined && !text.startsWith(this.#streamListSettledText)) {
|
|
2632
|
+
this.#streamListSettledText = undefined;
|
|
2633
|
+
}
|
|
2592
2634
|
let index = tokens.length - 1;
|
|
2593
2635
|
while (tokens[index]?.type === "space") index--;
|
|
2594
2636
|
const token = tokens[index];
|
|
@@ -2603,6 +2645,7 @@ export class Markdown implements Component {
|
|
|
2603
2645
|
let cursor = canResume ? previous.tailStart : start;
|
|
2604
2646
|
let tailStart = start;
|
|
2605
2647
|
let stableCount = 0;
|
|
2648
|
+
let referenceStop = canResume ? previous.referenceStop : undefined;
|
|
2606
2649
|
const indent = /^ */.exec(list.raw)![0].length;
|
|
2607
2650
|
for (let i = canResume ? previous.stableCount : 0; i < list.items.length; i++) {
|
|
2608
2651
|
const item = list.items[i]!;
|
|
@@ -2614,9 +2657,34 @@ export class Markdown implements Component {
|
|
|
2614
2657
|
tailStart = position;
|
|
2615
2658
|
stableCount = i;
|
|
2616
2659
|
}
|
|
2660
|
+
if (referenceStop === undefined && hasUnresolvedReferenceLabel(item)) referenceStop = tailStart;
|
|
2617
2661
|
cursor = position + item.raw.length;
|
|
2618
2662
|
}
|
|
2619
|
-
|
|
2663
|
+
// Items from stableCount on are scanned again next time and may still
|
|
2664
|
+
// resolve their own labels, so only a finished item's label is kept.
|
|
2665
|
+
if (referenceStop !== undefined && referenceStop >= tailStart) referenceStop = undefined;
|
|
2666
|
+
this.#streamingListCache = {
|
|
2667
|
+
text,
|
|
2668
|
+
before: tokens.slice(0, index),
|
|
2669
|
+
start,
|
|
2670
|
+
tailStart,
|
|
2671
|
+
stableCount,
|
|
2672
|
+
referenceStop,
|
|
2673
|
+
list,
|
|
2674
|
+
};
|
|
2675
|
+
// No append can add to an item once a later one has begun, but a later
|
|
2676
|
+
// reference definition can still restyle a bracketed label in it. The
|
|
2677
|
+
// list must also start right at the settled boundary: in front of it may
|
|
2678
|
+
// sit a display-math opener whose closer, arriving after the list, would
|
|
2679
|
+
// turn the whole run, list included, into one math block.
|
|
2680
|
+
const settledEnd = referenceStop ?? tailStart;
|
|
2681
|
+
if (
|
|
2682
|
+
settledEnd > start &&
|
|
2683
|
+
(this.#streamSettledText?.length ?? 0) === start &&
|
|
2684
|
+
settledEnd > (this.#streamListSettledText?.length ?? 0)
|
|
2685
|
+
) {
|
|
2686
|
+
this.#streamListSettledText = text.slice(0, settledEnd);
|
|
2687
|
+
}
|
|
2620
2688
|
}
|
|
2621
2689
|
|
|
2622
2690
|
/**
|
|
@@ -2629,6 +2697,7 @@ export class Markdown implements Component {
|
|
|
2629
2697
|
this.#streamPrefixTokens = undefined;
|
|
2630
2698
|
this.#streamPrefixOpeners = NO_OPENERS;
|
|
2631
2699
|
this.#streamSettledText = undefined;
|
|
2700
|
+
this.#streamListSettledText = undefined;
|
|
2632
2701
|
this.#streamPrefixLineCache = undefined;
|
|
2633
2702
|
this.#tailRowCache = undefined;
|
|
2634
2703
|
this.#listItemRowsCache = undefined;
|