@oh-my-pi/pi-tui 17.2.1 → 17.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/types/loop-watchdog.d.ts +6 -1
- package/dist/types/terminal.d.ts +15 -9
- package/package.json +3 -3
- package/src/components/markdown.ts +130 -69
- package/src/loop-watchdog.ts +11 -2
- package/src/terminal.ts +63 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.2] - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added request tokens to explicit OSC 11 appearance refreshes to allow consumers to correlate responses across queued and coalesced terminal probes.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed the event-loop watchdog incorrectly reporting system sleep or suspension as a synchronous ui.loop-blocked stall.
|
|
14
|
+
- Fixed terminal copies of fenced-code blocks retaining margins from components, lists, or blockquotes in assistant messages (#7055 by @GratefulDave).
|
|
15
|
+
|
|
5
16
|
## [17.2.0] - 2026-07-30
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -3,6 +3,8 @@ export interface LoopWatchdogOptions {
|
|
|
3
3
|
intervalMs?: number;
|
|
4
4
|
/** A tick later than this past its deadline counts as a block. Default 250. */
|
|
5
5
|
thresholdMs?: number;
|
|
6
|
+
/** Overshoot beyond this likely includes system sleep, so it is suppressed. Default 60_000. */
|
|
7
|
+
sleepMs?: number;
|
|
6
8
|
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
|
7
9
|
now?: () => number;
|
|
8
10
|
/** Timer source; injectable for tests. Default `setTimeout`. */
|
|
@@ -28,7 +30,10 @@ interface LoopWatchdogTimer {
|
|
|
28
30
|
* The handle is `unref`'d so the probe never keeps the process alive, and stop()
|
|
29
31
|
* cancels the armed timer when the handle exposes `cancel` (the default
|
|
30
32
|
* `setTimeout` handle does, via `clearTimeout`). The `#generation` guard remains
|
|
31
|
-
* as a fallback for injected handles that cannot cancel.
|
|
33
|
+
* as a fallback for injected handles that cannot cancel. An overshoot beyond
|
|
34
|
+
* `sleepMs` is treated as system sleep rather than a synchronous stall: the
|
|
35
|
+
* process could not have run JS during the missed interval, and one resume
|
|
36
|
+
* should not produce a multi-minute `ui.loop-blocked` record.
|
|
32
37
|
*/
|
|
33
38
|
export declare class LoopWatchdog {
|
|
34
39
|
#private;
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export declare function setAltScreenActive(active: boolean): void;
|
|
|
58
58
|
export declare function emergencyTerminalRestore(): void;
|
|
59
59
|
/** Terminal-reported appearance (dark/light mode). */
|
|
60
60
|
export type TerminalAppearance = "dark" | "light";
|
|
61
|
+
/** Identity of an accepted explicit terminal appearance refresh request. */
|
|
62
|
+
export type TerminalAppearanceRequestToken = number;
|
|
61
63
|
export interface Terminal {
|
|
62
64
|
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
63
65
|
stop(): void;
|
|
@@ -90,24 +92,28 @@ export interface Terminal {
|
|
|
90
92
|
* Subscribers registered after detection are invoked immediately with the
|
|
91
93
|
* already-detected appearance so late subscribers never miss it.
|
|
92
94
|
*/
|
|
93
|
-
onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
|
|
95
|
+
onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
|
|
94
96
|
/**
|
|
95
97
|
* Register a callback fired for every valid OSC 11 appearance report,
|
|
96
98
|
* including reports whose classification matches the current appearance.
|
|
97
99
|
* Unlike onAppearanceChange, this does not replay an earlier report.
|
|
98
100
|
* Optional so custom Terminals built against older pi-tui versions keep working.
|
|
99
101
|
*/
|
|
100
|
-
onAppearanceReport?(callback: (appearance: TerminalAppearance) => void): (() => void) | void;
|
|
102
|
+
onAppearanceReport?(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): (() => void) | void;
|
|
101
103
|
/**
|
|
102
104
|
* Issue a single OSC 11 background-color re-query, driving the appearance
|
|
103
105
|
* callbacks through the same parse/dedup pipeline used at startup and on Mode
|
|
104
106
|
* 2031 notifications. Bounded: one probe per call, no timers. Invoked on the
|
|
105
|
-
* user's explicit display-reset gesture
|
|
107
|
+
* user's explicit display-reset gesture so terminals that cannot
|
|
106
108
|
* deliver end-to-end Mode 2031 notifications still pick up a light/dark switch
|
|
107
|
-
* without a restart.
|
|
108
|
-
*
|
|
109
|
+
* without a restart.
|
|
110
|
+
*
|
|
111
|
+
* A caller-provided token must be propagated unchanged to callbacks and
|
|
112
|
+
* returned when the request is accepted. This lets callers establish ownership
|
|
113
|
+
* before implementations synchronously dispatch a cached response. Optional so
|
|
114
|
+
* custom Terminals built against older pi-tui versions keep working.
|
|
109
115
|
*/
|
|
110
|
-
refreshAppearance?(): void;
|
|
116
|
+
refreshAppearance?(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
111
117
|
/** The last detected terminal appearance, or undefined if not yet known. */
|
|
112
118
|
get appearance(): TerminalAppearance | undefined;
|
|
113
119
|
/**
|
|
@@ -137,8 +143,8 @@ export declare class ProcessTerminal implements Terminal {
|
|
|
137
143
|
get keyboardEnhancementEnterSequence(): string | null;
|
|
138
144
|
get keyboardEnhancementExitSequence(): string | null;
|
|
139
145
|
get appearance(): TerminalAppearance | undefined;
|
|
140
|
-
onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
|
|
141
|
-
onAppearanceReport(callback: (appearance: TerminalAppearance) => void): () => void;
|
|
146
|
+
onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
|
|
147
|
+
onAppearanceReport(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): () => void;
|
|
142
148
|
/**
|
|
143
149
|
* Re-query the terminal background via a single OSC 11 probe. Reuses the
|
|
144
150
|
* startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
|
|
@@ -148,7 +154,7 @@ export declare class ProcessTerminal implements Terminal {
|
|
|
148
154
|
* armed. Suppressed while inactive, headless, or after the terminal is torn
|
|
149
155
|
* down.
|
|
150
156
|
*/
|
|
151
|
-
refreshAppearance(): void;
|
|
157
|
+
refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
152
158
|
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
|
|
153
159
|
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
154
160
|
drainInput(maxMs?: number, idleMs?: number): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.2.2",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.2.2",
|
|
42
42
|
"lru-cache": "11.5.2",
|
|
43
43
|
"marked": "^18.0.6"
|
|
44
44
|
},
|
|
@@ -837,6 +837,19 @@ const RENDER_CACHE_MAX_SIZE = 4 * 1024 * 1024;
|
|
|
837
837
|
const RENDER_CACHE_MAX_ENTRY_SIZE = 256 * 1024;
|
|
838
838
|
const EMPTY_RENDER_LINES: readonly string[] = [];
|
|
839
839
|
|
|
840
|
+
interface RenderedLine {
|
|
841
|
+
text: string;
|
|
842
|
+
literalCode?: true;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
interface RenderedListItemLine extends RenderedLine {
|
|
846
|
+
nested: boolean;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function renderedLine(text: string, literalCode?: boolean): RenderedLine {
|
|
850
|
+
return literalCode ? { text, literalCode: true } : { text };
|
|
851
|
+
}
|
|
852
|
+
|
|
840
853
|
interface RenderCacheEntry {
|
|
841
854
|
lines: readonly string[];
|
|
842
855
|
tables: readonly RenderedTableLayout[];
|
|
@@ -1858,7 +1871,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1858
1871
|
rowOffset: number,
|
|
1859
1872
|
startingSourceOffset: number,
|
|
1860
1873
|
): string[] {
|
|
1861
|
-
const wrappedLines:
|
|
1874
|
+
const wrappedLines: RenderedLine[] = [];
|
|
1862
1875
|
let sourceOffset = startingSourceOffset;
|
|
1863
1876
|
for (let i = start; i < end; i++) {
|
|
1864
1877
|
const token = tokens[i];
|
|
@@ -1874,14 +1887,21 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1874
1887
|
`offset:${sourceOffset}`,
|
|
1875
1888
|
);
|
|
1876
1889
|
const tokenLineOffsets = [0];
|
|
1877
|
-
for (const
|
|
1890
|
+
for (const renderedRow of renderedTokenLines) {
|
|
1878
1891
|
// Lists wrap while their structural prefixes are still available, so
|
|
1879
1892
|
// continuation rows retain the correct hanging indent. Re-wrapping the
|
|
1880
1893
|
// flattened rows here would discard that structure.
|
|
1881
|
-
if (token.type === "list" || TERMINAL.isImageLine(
|
|
1882
|
-
wrappedLines.push(
|
|
1894
|
+
if (token.type === "list" || TERMINAL.isImageLine(renderedRow.text) || isOsc66Line(renderedRow.text)) {
|
|
1895
|
+
wrappedLines.push(renderedRow);
|
|
1883
1896
|
} else {
|
|
1884
|
-
|
|
1897
|
+
const wrappedRows = wrapTextWithAnsi(renderedRow.text, contentWidth);
|
|
1898
|
+
if (wrappedRows.length === 1 && wrappedRows[0] === renderedRow.text) {
|
|
1899
|
+
wrappedLines.push(renderedRow);
|
|
1900
|
+
} else {
|
|
1901
|
+
for (const wrappedLine of wrappedRows) {
|
|
1902
|
+
wrappedLines.push(renderedLine(wrappedLine, renderedRow.literalCode));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1885
1905
|
}
|
|
1886
1906
|
tokenLineOffsets.push(wrappedLines.length - tokenWrappedRowStart);
|
|
1887
1907
|
}
|
|
@@ -1914,8 +1934,9 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1914
1934
|
const bgFn = this.#defaultTextStyle?.bgColor;
|
|
1915
1935
|
const contentLines: string[] = [];
|
|
1916
1936
|
let previousLineWasOsc66 = false;
|
|
1917
|
-
|
|
1918
|
-
|
|
1937
|
+
for (const renderedLine of wrappedLines) {
|
|
1938
|
+
const literalCodeRow = renderedLine.literalCode === true;
|
|
1939
|
+
const line = renderedLine.text;
|
|
1919
1940
|
// The first empty row after a scale>1 OSC 66 heading is structural:
|
|
1920
1941
|
// it reserves the lower cells occupied by the multicell glyphs. Do
|
|
1921
1942
|
// not pad or background-fill it, because real spaces on that row can
|
|
@@ -1935,6 +1956,10 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1935
1956
|
}
|
|
1936
1957
|
|
|
1937
1958
|
previousLineWasOsc66 = false;
|
|
1959
|
+
if (literalCodeRow) {
|
|
1960
|
+
contentLines.push(line);
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1938
1963
|
const lineWithMargins = leftMargin + line + rightMargin;
|
|
1939
1964
|
|
|
1940
1965
|
if (bgFn) {
|
|
@@ -1965,8 +1990,9 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1965
1990
|
return layouts;
|
|
1966
1991
|
}
|
|
1967
1992
|
|
|
1968
|
-
#renderCodeBodyLines(token: Token, codeIndent: string):
|
|
1969
|
-
const
|
|
1993
|
+
#renderCodeBodyLines(token: Token, codeIndent: string): RenderedLine[] {
|
|
1994
|
+
const literalCode = this.#codeBlockIndent === 0;
|
|
1995
|
+
const bodyLines: RenderedLine[] = [];
|
|
1970
1996
|
const tokenText = "text" in token && typeof token.text === "string" ? token.text : "";
|
|
1971
1997
|
const lang = "lang" in token && typeof token.lang === "string" ? token.lang : undefined;
|
|
1972
1998
|
const normalizedLang = lang?.toLowerCase();
|
|
@@ -1975,11 +2001,14 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1975
2001
|
!this.#renderingFrozenPrefix &&
|
|
1976
2002
|
this.#theme.highlightCode &&
|
|
1977
2003
|
(normalizedLang === "diff" || normalizedLang === "patch" || normalizedLang === "udiff");
|
|
2004
|
+
const addBodyLine = (line: string): void => {
|
|
2005
|
+
bodyLines.push(renderedLine(literalCode ? line : codeIndent + line, literalCode));
|
|
2006
|
+
};
|
|
1978
2007
|
|
|
1979
2008
|
if (this.#theme.highlightCode && (!this.transientRenderCache || this.#renderingFrozenPrefix)) {
|
|
1980
2009
|
const highlightedLines = this.#theme.highlightCode(tokenText, lang);
|
|
1981
2010
|
for (const hlLine of highlightedLines) {
|
|
1982
|
-
|
|
2011
|
+
addBodyLine(hlLine);
|
|
1983
2012
|
}
|
|
1984
2013
|
return bodyLines;
|
|
1985
2014
|
}
|
|
@@ -1990,11 +2019,11 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
1990
2019
|
if (closedFence || lineEnd >= 0) {
|
|
1991
2020
|
const completedText = closedFence ? tokenText : tokenText.slice(0, lineEnd);
|
|
1992
2021
|
for (const hlLine of this.#highlightStreamingDiffLines(completedText, lang)) {
|
|
1993
|
-
|
|
2022
|
+
addBodyLine(hlLine);
|
|
1994
2023
|
}
|
|
1995
2024
|
if (!closedFence) {
|
|
1996
2025
|
for (const codeLine of tokenText.slice(lineEnd + 1).split("\n")) {
|
|
1997
|
-
|
|
2026
|
+
addBodyLine(this.#theme.codeBlock(codeLine));
|
|
1998
2027
|
}
|
|
1999
2028
|
}
|
|
2000
2029
|
return bodyLines;
|
|
@@ -2002,7 +2031,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2002
2031
|
}
|
|
2003
2032
|
|
|
2004
2033
|
for (const codeLine of tokenText.split("\n")) {
|
|
2005
|
-
|
|
2034
|
+
addBodyLine(this.#theme.codeBlock(codeLine));
|
|
2006
2035
|
}
|
|
2007
2036
|
return bodyLines;
|
|
2008
2037
|
}
|
|
@@ -2181,14 +2210,14 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2181
2210
|
nextTokenType?: string,
|
|
2182
2211
|
styleContext?: InlineStyleContext,
|
|
2183
2212
|
tokenKey = "root",
|
|
2184
|
-
):
|
|
2185
|
-
const lines:
|
|
2213
|
+
): RenderedLine[] {
|
|
2214
|
+
const lines: RenderedLine[] = [];
|
|
2186
2215
|
|
|
2187
2216
|
// Display math block (own-line `$$…$$` / `\[…\]`): stack `\frac` vertically
|
|
2188
2217
|
// and keep `\\` row breaks, so fractions and matrices span multiple lines.
|
|
2189
2218
|
if (isMathToken(token)) {
|
|
2190
|
-
for (const mathLine of latexToBlock(token.text)) lines.push(this.#applyDefaultStyle(mathLine));
|
|
2191
|
-
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
2219
|
+
for (const mathLine of latexToBlock(token.text)) lines.push(renderedLine(this.#applyDefaultStyle(mathLine)));
|
|
2220
|
+
if (nextTokenType && nextTokenType !== "space") lines.push(renderedLine(""));
|
|
2192
2221
|
return lines;
|
|
2193
2222
|
}
|
|
2194
2223
|
|
|
@@ -2203,10 +2232,10 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2203
2232
|
const plainWidth = visibleWidth(headingPlainText);
|
|
2204
2233
|
if (plainWidth > 0 && 2 * plainWidth <= width) {
|
|
2205
2234
|
const sizedHeading = encodeTextSizedHeading(headingPlainText, 2);
|
|
2206
|
-
lines.push(this.#theme.heading(this.#theme.bold(this.#theme.underline(sizedHeading))));
|
|
2207
|
-
lines.push(""); // reserve the heading's second visual row
|
|
2235
|
+
lines.push(renderedLine(this.#theme.heading(this.#theme.bold(this.#theme.underline(sizedHeading)))));
|
|
2236
|
+
lines.push(renderedLine("")); // reserve the heading's second visual row
|
|
2208
2237
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2209
|
-
lines.push(""); // Add spacing after headings (unless space token follows)
|
|
2238
|
+
lines.push(renderedLine("")); // Add spacing after headings (unless space token follows)
|
|
2210
2239
|
}
|
|
2211
2240
|
break;
|
|
2212
2241
|
}
|
|
@@ -2218,9 +2247,9 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2218
2247
|
} else {
|
|
2219
2248
|
styledHeading = this.#theme.heading(this.#theme.bold(headingPrefix + headingText));
|
|
2220
2249
|
}
|
|
2221
|
-
lines.push(styledHeading);
|
|
2250
|
+
lines.push(renderedLine(styledHeading));
|
|
2222
2251
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2223
|
-
lines.push(""); // Add spacing after headings (unless space token follows)
|
|
2252
|
+
lines.push(renderedLine("")); // Add spacing after headings (unless space token follows)
|
|
2224
2253
|
}
|
|
2225
2254
|
break;
|
|
2226
2255
|
}
|
|
@@ -2228,15 +2257,18 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2228
2257
|
case "paragraph": {
|
|
2229
2258
|
const displayMath = soleDisplayMath(token.tokens);
|
|
2230
2259
|
if (displayMath) {
|
|
2231
|
-
for (const mathLine of latexToBlock(displayMath.text))
|
|
2232
|
-
|
|
2260
|
+
for (const mathLine of latexToBlock(displayMath.text))
|
|
2261
|
+
lines.push(renderedLine(this.#applyDefaultStyle(mathLine)));
|
|
2262
|
+
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") lines.push(renderedLine(""));
|
|
2233
2263
|
break;
|
|
2234
2264
|
}
|
|
2235
2265
|
const paragraphText = this.#renderInlineTokens(token.tokens || [], styleContext);
|
|
2236
|
-
|
|
2266
|
+
for (const paragraphLine of hangWrapTreeGuideLines(paragraphText, width) ?? [paragraphText]) {
|
|
2267
|
+
lines.push(renderedLine(paragraphLine));
|
|
2268
|
+
}
|
|
2237
2269
|
// Don't add spacing if next token is space or list
|
|
2238
2270
|
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") {
|
|
2239
|
-
lines.push("");
|
|
2271
|
+
lines.push(renderedLine(""));
|
|
2240
2272
|
}
|
|
2241
2273
|
break;
|
|
2242
2274
|
}
|
|
@@ -2252,24 +2284,28 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2252
2284
|
if (ascii) {
|
|
2253
2285
|
for (const asciiLine of ascii.split("\n")) {
|
|
2254
2286
|
lines.push(
|
|
2255
|
-
|
|
2287
|
+
renderedLine(
|
|
2288
|
+
visibleWidth(asciiLine) > width
|
|
2289
|
+
? truncateToWidth(asciiLine, width, Ellipsis.Omit)
|
|
2290
|
+
: asciiLine,
|
|
2291
|
+
),
|
|
2256
2292
|
);
|
|
2257
2293
|
}
|
|
2258
2294
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2259
|
-
lines.push("");
|
|
2295
|
+
lines.push(renderedLine(""));
|
|
2260
2296
|
}
|
|
2261
2297
|
break;
|
|
2262
2298
|
}
|
|
2263
2299
|
}
|
|
2264
2300
|
|
|
2265
2301
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
2266
|
-
lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
2302
|
+
lines.push(renderedLine(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`)));
|
|
2267
2303
|
for (const bodyLine of this.#renderCodeBodyLines(token, codeIndent)) {
|
|
2268
2304
|
lines.push(bodyLine);
|
|
2269
2305
|
}
|
|
2270
|
-
lines.push(this.#theme.codeBlockBorder("```"));
|
|
2306
|
+
lines.push(renderedLine(this.#theme.codeBlockBorder("```")));
|
|
2271
2307
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2272
|
-
lines.push(""); // Add spacing after code blocks (unless space token follows)
|
|
2308
|
+
lines.push(renderedLine("")); // Add spacing after code blocks (unless space token follows)
|
|
2273
2309
|
}
|
|
2274
2310
|
break;
|
|
2275
2311
|
}
|
|
@@ -2284,7 +2320,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2284
2320
|
|
|
2285
2321
|
case "table": {
|
|
2286
2322
|
const tableLines = this.#renderTable(token as TableToken, width, nextTokenType, styleContext, tokenKey);
|
|
2287
|
-
lines.push(
|
|
2323
|
+
for (const tableLine of tableLines) lines.push(renderedLine(tableLine));
|
|
2288
2324
|
break;
|
|
2289
2325
|
}
|
|
2290
2326
|
|
|
@@ -2295,7 +2331,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2295
2331
|
};
|
|
2296
2332
|
const quoteContentWidth = Math.max(1, width - 2);
|
|
2297
2333
|
const quoteTokens = token.tokens || [];
|
|
2298
|
-
const renderedQuoteLines:
|
|
2334
|
+
const renderedQuoteLines: RenderedLine[] = [];
|
|
2299
2335
|
const blockquoteSpecStart = this.#activeTableRenderSpecs?.length ?? 0;
|
|
2300
2336
|
|
|
2301
2337
|
for (let i = 0; i < quoteTokens.length; i++) {
|
|
@@ -2331,7 +2367,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2331
2367
|
}
|
|
2332
2368
|
}
|
|
2333
2369
|
|
|
2334
|
-
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
|
|
2370
|
+
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1]!.text === "") {
|
|
2335
2371
|
renderedQuoteLines.pop();
|
|
2336
2372
|
}
|
|
2337
2373
|
|
|
@@ -2350,16 +2386,16 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2350
2386
|
}
|
|
2351
2387
|
lines.push(...borderedQuoteLines);
|
|
2352
2388
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2353
|
-
lines.push(""); // Add spacing after blockquotes (unless space token follows)
|
|
2389
|
+
lines.push(renderedLine("")); // Add spacing after blockquotes (unless space token follows)
|
|
2354
2390
|
}
|
|
2355
2391
|
break;
|
|
2356
2392
|
}
|
|
2357
2393
|
|
|
2358
2394
|
case "hr": {
|
|
2359
2395
|
const raw = "raw" in token && typeof token.raw === "string" ? token.raw.trim() : "";
|
|
2360
|
-
lines.push(this.#renderHrLine(width, raw[0] || ""));
|
|
2396
|
+
lines.push(renderedLine(this.#renderHrLine(width, raw[0] || "")));
|
|
2361
2397
|
if (nextTokenType && nextTokenType !== "space") {
|
|
2362
|
-
lines.push(""); // Add spacing after horizontal rules (unless space token follows)
|
|
2398
|
+
lines.push(renderedLine("")); // Add spacing after horizontal rules (unless space token follows)
|
|
2363
2399
|
}
|
|
2364
2400
|
break;
|
|
2365
2401
|
}
|
|
@@ -2372,13 +2408,13 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2372
2408
|
|
|
2373
2409
|
case "space":
|
|
2374
2410
|
// Space tokens represent blank lines in markdown
|
|
2375
|
-
lines.push("");
|
|
2411
|
+
lines.push(renderedLine(""));
|
|
2376
2412
|
break;
|
|
2377
2413
|
|
|
2378
2414
|
default:
|
|
2379
2415
|
// Handle any other token types as plain text
|
|
2380
2416
|
if ("text" in token && typeof token.text === "string") {
|
|
2381
|
-
lines.push(token.text);
|
|
2417
|
+
lines.push(renderedLine(token.text));
|
|
2382
2418
|
}
|
|
2383
2419
|
}
|
|
2384
2420
|
|
|
@@ -2395,7 +2431,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2395
2431
|
* Wrap already-rendered lines in the blockquote border and quote styling.
|
|
2396
2432
|
* `width` is the full content width; the border reserves two cells.
|
|
2397
2433
|
*/
|
|
2398
|
-
#applyQuoteBorder(renderedLines:
|
|
2434
|
+
#applyQuoteBorder(renderedLines: RenderedLine[], width: number, sourceRowOffsets?: number[]): RenderedLine[] {
|
|
2399
2435
|
const quoteStyle = (text: string) => this.#theme.quote(this.#theme.italic(text));
|
|
2400
2436
|
const quoteStylePrefix = this.#getStylePrefix(quoteStyle);
|
|
2401
2437
|
const applyQuoteStyle = (line: string): string => {
|
|
@@ -2406,12 +2442,23 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2406
2442
|
return quoteStyle(lineWithReappliedStyle);
|
|
2407
2443
|
};
|
|
2408
2444
|
const quoteContentWidth = Math.max(1, width - 2);
|
|
2409
|
-
const lines:
|
|
2445
|
+
const lines: RenderedLine[] = [];
|
|
2410
2446
|
sourceRowOffsets?.push(0);
|
|
2411
2447
|
for (const quoteLine of renderedLines) {
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2448
|
+
if (quoteLine.literalCode) {
|
|
2449
|
+
const wrappedLiteralRows = wrapTextWithAnsi(quoteLine.text, quoteContentWidth);
|
|
2450
|
+
if (wrappedLiteralRows.length === 0) {
|
|
2451
|
+
lines.push(renderedLine("", true));
|
|
2452
|
+
} else {
|
|
2453
|
+
for (const wrappedLine of wrappedLiteralRows) {
|
|
2454
|
+
lines.push(renderedLine(wrappedLine, true));
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
} else {
|
|
2458
|
+
const styledLine = applyQuoteStyle(quoteLine.text);
|
|
2459
|
+
for (const wrappedLine of wrapTextWithAnsi(styledLine, quoteContentWidth)) {
|
|
2460
|
+
lines.push(renderedLine(this.#theme.quoteBorder(`${this.#theme.symbols.quoteBorder} `) + wrappedLine));
|
|
2461
|
+
}
|
|
2415
2462
|
}
|
|
2416
2463
|
sourceRowOffsets?.push(lines.length);
|
|
2417
2464
|
}
|
|
@@ -2424,8 +2471,8 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2424
2471
|
* quote styling; the remaining markup is normalized to terminal text (entities
|
|
2425
2472
|
* decoded, `<code>` themed, lists/`<br>`/`<p>` laid out).
|
|
2426
2473
|
*/
|
|
2427
|
-
#renderHtmlBlock(raw: string, width: number):
|
|
2428
|
-
const lines:
|
|
2474
|
+
#renderHtmlBlock(raw: string, width: number): RenderedLine[] {
|
|
2475
|
+
const lines: RenderedLine[] = [];
|
|
2429
2476
|
const state = createHtmlNormalizationState();
|
|
2430
2477
|
const codeHook = (text: string): string => this.#theme.code(text) + this.#getDefaultStylePrefix();
|
|
2431
2478
|
const flushText = (chunk: string): void => {
|
|
@@ -2433,7 +2480,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2433
2480
|
if (cleaned.trim() === "") return;
|
|
2434
2481
|
for (const line of splitTerminalLines(cleaned)) {
|
|
2435
2482
|
const trimmed = line.trimEnd();
|
|
2436
|
-
lines.push(trimmed.trim() === "" ? "" : this.#applyDefaultStyle(trimmed));
|
|
2483
|
+
lines.push(renderedLine(trimmed.trim() === "" ? "" : this.#applyDefaultStyle(trimmed)));
|
|
2437
2484
|
}
|
|
2438
2485
|
};
|
|
2439
2486
|
let lastIndex = 0;
|
|
@@ -2444,7 +2491,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2444
2491
|
if (match[1] !== undefined) {
|
|
2445
2492
|
lines.push(...this.#renderHtmlBlockquote(match[1], width));
|
|
2446
2493
|
} else {
|
|
2447
|
-
lines.push(this.#renderHrLine(width));
|
|
2494
|
+
lines.push(renderedLine(this.#renderHrLine(width)));
|
|
2448
2495
|
}
|
|
2449
2496
|
}
|
|
2450
2497
|
flushText(raw.slice(lastIndex));
|
|
@@ -2452,10 +2499,10 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2452
2499
|
}
|
|
2453
2500
|
|
|
2454
2501
|
/** Render the inner content of an HTML `<blockquote>` with quote styling. */
|
|
2455
|
-
#renderHtmlBlockquote(inner: string, width: number):
|
|
2502
|
+
#renderHtmlBlockquote(inner: string, width: number): RenderedLine[] {
|
|
2456
2503
|
const cleaned = normalizeHtmlForTerminal(inner, createHtmlNormalizationState(), text => this.#theme.code(text));
|
|
2457
|
-
const innerLines = splitTerminalLines(cleaned).map(line => line.trimEnd());
|
|
2458
|
-
while (innerLines.length > 0 && innerLines[innerLines.length - 1] === "") innerLines.pop();
|
|
2504
|
+
const innerLines = splitTerminalLines(cleaned).map(line => renderedLine(line.trimEnd()));
|
|
2505
|
+
while (innerLines.length > 0 && innerLines[innerLines.length - 1].text === "") innerLines.pop();
|
|
2459
2506
|
return this.#applyQuoteBorder(innerLines, width);
|
|
2460
2507
|
}
|
|
2461
2508
|
|
|
@@ -2591,27 +2638,41 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2591
2638
|
/**
|
|
2592
2639
|
* Render a list with proper nesting support
|
|
2593
2640
|
*/
|
|
2594
|
-
#renderList(token: ListToken, depth: number, width: number, styleContext?: InlineStyleContext):
|
|
2595
|
-
const lines:
|
|
2641
|
+
#renderList(token: ListToken, depth: number, width: number, styleContext?: InlineStyleContext): RenderedLine[] {
|
|
2642
|
+
const lines: RenderedLine[] = [];
|
|
2596
2643
|
const indent = " ".repeat(depth);
|
|
2597
2644
|
// Use the list's start property (defaults to 1 for ordered lists)
|
|
2598
2645
|
const startNumber = token.start ?? 1;
|
|
2599
|
-
const pushWrapped = (
|
|
2646
|
+
const pushWrapped = (line: RenderedLine, firstPrefix: string, continuationPrefix: string): void => {
|
|
2647
|
+
if (line.literalCode) {
|
|
2648
|
+
const wrappedLiteralRows = wrapTextWithAnsi(line.text, Math.max(1, width));
|
|
2649
|
+
if (wrappedLiteralRows.length === 0) {
|
|
2650
|
+
lines.push(renderedLine("", true));
|
|
2651
|
+
} else {
|
|
2652
|
+
for (const wrappedLine of wrappedLiteralRows) {
|
|
2653
|
+
lines.push(renderedLine(wrappedLine, true));
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2600
2659
|
const prefixWidth = visibleWidth(firstPrefix);
|
|
2601
2660
|
if (prefixWidth >= width) {
|
|
2602
|
-
lines.push(truncateToWidth(firstPrefix, width, Ellipsis.Omit));
|
|
2603
|
-
|
|
2661
|
+
lines.push(renderedLine(truncateToWidth(firstPrefix, width, Ellipsis.Omit)));
|
|
2662
|
+
for (const wrappedLine of wrapTextWithAnsi(line.text, Math.max(1, width))) {
|
|
2663
|
+
lines.push(renderedLine(wrappedLine));
|
|
2664
|
+
}
|
|
2604
2665
|
return;
|
|
2605
2666
|
}
|
|
2606
2667
|
const bodyWidth = width - prefixWidth;
|
|
2607
|
-
const wrapped = wrapTextWithAnsi(text, bodyWidth);
|
|
2668
|
+
const wrapped = wrapTextWithAnsi(line.text, bodyWidth);
|
|
2608
2669
|
if (wrapped.length === 0) {
|
|
2609
|
-
lines.push(firstPrefix);
|
|
2670
|
+
lines.push(renderedLine(firstPrefix));
|
|
2610
2671
|
return;
|
|
2611
2672
|
}
|
|
2612
|
-
lines.push(firstPrefix + wrapped[0]);
|
|
2673
|
+
lines.push(renderedLine(firstPrefix + wrapped[0]));
|
|
2613
2674
|
for (let lineIndex = 1; lineIndex < wrapped.length; lineIndex++) {
|
|
2614
|
-
lines.push(continuationPrefix + wrapped[lineIndex]);
|
|
2675
|
+
lines.push(renderedLine(continuationPrefix + wrapped[lineIndex]));
|
|
2615
2676
|
}
|
|
2616
2677
|
};
|
|
2617
2678
|
|
|
@@ -2630,21 +2691,21 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2630
2691
|
if (itemLines.length > 0) {
|
|
2631
2692
|
const firstLine = itemLines[0]!;
|
|
2632
2693
|
if (firstLine.nested) {
|
|
2633
|
-
lines.push(firstLine
|
|
2694
|
+
lines.push(firstLine);
|
|
2634
2695
|
} else {
|
|
2635
|
-
pushWrapped(firstLine
|
|
2696
|
+
pushWrapped(firstLine, firstPrefix, continuationIndent);
|
|
2636
2697
|
}
|
|
2637
2698
|
|
|
2638
2699
|
for (let j = 1; j < itemLines.length; j++) {
|
|
2639
2700
|
const line = itemLines[j]!;
|
|
2640
2701
|
if (line.nested) {
|
|
2641
|
-
lines.push(line
|
|
2702
|
+
lines.push(line);
|
|
2642
2703
|
} else {
|
|
2643
|
-
pushWrapped(line
|
|
2704
|
+
pushWrapped(line, continuationIndent, continuationIndent);
|
|
2644
2705
|
}
|
|
2645
2706
|
}
|
|
2646
2707
|
} else {
|
|
2647
|
-
lines.push(firstPrefix);
|
|
2708
|
+
lines.push(renderedLine(firstPrefix));
|
|
2648
2709
|
}
|
|
2649
2710
|
}
|
|
2650
2711
|
|
|
@@ -2662,8 +2723,8 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2662
2723
|
parentDepth: number,
|
|
2663
2724
|
width: number,
|
|
2664
2725
|
styleContext?: InlineStyleContext,
|
|
2665
|
-
):
|
|
2666
|
-
const lines:
|
|
2726
|
+
): RenderedListItemLine[] {
|
|
2727
|
+
const lines: RenderedListItemLine[] = [];
|
|
2667
2728
|
|
|
2668
2729
|
for (const token of tokens) {
|
|
2669
2730
|
if (token.type === "list") {
|
|
@@ -2671,7 +2732,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2671
2732
|
// These lines carry their own indent, so tag them for pass-through
|
|
2672
2733
|
const nestedLines = this.#renderList(token as ListToken, parentDepth + 1, width, styleContext);
|
|
2673
2734
|
for (const nestedLine of nestedLines) {
|
|
2674
|
-
lines.push({
|
|
2735
|
+
lines.push({ ...nestedLine, nested: true });
|
|
2675
2736
|
}
|
|
2676
2737
|
} else if (token.type === "text") {
|
|
2677
2738
|
// Text content (may have inline tokens, or a sole display-math token)
|
|
@@ -2702,7 +2763,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
|
|
|
2702
2763
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
2703
2764
|
lines.push({ text: this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`), nested: false });
|
|
2704
2765
|
for (const bodyLine of this.#renderCodeBodyLines(token, codeIndent)) {
|
|
2705
|
-
lines.push({
|
|
2766
|
+
lines.push({ ...bodyLine, nested: false });
|
|
2706
2767
|
}
|
|
2707
2768
|
lines.push({ text: this.#theme.codeBlockBorder("```"), nested: false });
|
|
2708
2769
|
} else if (isMathToken(token)) {
|
package/src/loop-watchdog.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface LoopWatchdogOptions {
|
|
|
6
6
|
intervalMs?: number;
|
|
7
7
|
/** A tick later than this past its deadline counts as a block. Default 250. */
|
|
8
8
|
thresholdMs?: number;
|
|
9
|
+
/** Overshoot beyond this likely includes system sleep, so it is suppressed. Default 60_000. */
|
|
10
|
+
sleepMs?: number;
|
|
9
11
|
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
|
10
12
|
now?: () => number;
|
|
11
13
|
/** Timer source; injectable for tests. Default `setTimeout`. */
|
|
@@ -33,11 +35,15 @@ interface LoopWatchdogTimer {
|
|
|
33
35
|
* The handle is `unref`'d so the probe never keeps the process alive, and stop()
|
|
34
36
|
* cancels the armed timer when the handle exposes `cancel` (the default
|
|
35
37
|
* `setTimeout` handle does, via `clearTimeout`). The `#generation` guard remains
|
|
36
|
-
* as a fallback for injected handles that cannot cancel.
|
|
38
|
+
* as a fallback for injected handles that cannot cancel. An overshoot beyond
|
|
39
|
+
* `sleepMs` is treated as system sleep rather than a synchronous stall: the
|
|
40
|
+
* process could not have run JS during the missed interval, and one resume
|
|
41
|
+
* should not produce a multi-minute `ui.loop-blocked` record.
|
|
37
42
|
*/
|
|
38
43
|
export class LoopWatchdog {
|
|
39
44
|
#intervalMs: number;
|
|
40
45
|
#thresholdMs: number;
|
|
46
|
+
#sleepMs: number;
|
|
41
47
|
#now: () => number;
|
|
42
48
|
#schedule: (cb: () => void, ms: number) => LoopWatchdogTimer;
|
|
43
49
|
#expected = 0;
|
|
@@ -52,6 +58,7 @@ export class LoopWatchdog {
|
|
|
52
58
|
constructor(options: LoopWatchdogOptions = {}) {
|
|
53
59
|
this.#intervalMs = options.intervalMs ?? 250;
|
|
54
60
|
this.#thresholdMs = options.thresholdMs ?? 250;
|
|
61
|
+
this.#sleepMs = options.sleepMs ?? 60_000;
|
|
55
62
|
this.#now = options.now ?? (() => performance.now());
|
|
56
63
|
this.#schedule =
|
|
57
64
|
options.schedule ??
|
|
@@ -91,7 +98,9 @@ export class LoopWatchdog {
|
|
|
91
98
|
// forward to a later, phase-less block.
|
|
92
99
|
const phase = takeRecentLoopPhase();
|
|
93
100
|
if (blockedMs > this.#thresholdMs) {
|
|
94
|
-
if (
|
|
101
|
+
if (blockedMs > this.#sleepMs) {
|
|
102
|
+
this.#wasBlocked = false;
|
|
103
|
+
} else if (!this.#wasBlocked) {
|
|
95
104
|
this.#wasBlocked = true;
|
|
96
105
|
logger.warn("ui.loop-blocked", {
|
|
97
106
|
blockedMs: Math.round(blockedMs),
|
package/src/terminal.ts
CHANGED
|
@@ -377,6 +377,8 @@ export function emergencyTerminalRestore(): void {
|
|
|
377
377
|
}
|
|
378
378
|
/** Terminal-reported appearance (dark/light mode). */
|
|
379
379
|
export type TerminalAppearance = "dark" | "light";
|
|
380
|
+
/** Identity of an accepted explicit terminal appearance refresh request. */
|
|
381
|
+
export type TerminalAppearanceRequestToken = number;
|
|
380
382
|
export interface Terminal {
|
|
381
383
|
// Start the terminal with input, resize, and host-disconnect handlers.
|
|
382
384
|
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
@@ -444,24 +446,32 @@ export interface Terminal {
|
|
|
444
446
|
* Subscribers registered after detection are invoked immediately with the
|
|
445
447
|
* already-detected appearance so late subscribers never miss it.
|
|
446
448
|
*/
|
|
447
|
-
onAppearanceChange(
|
|
449
|
+
onAppearanceChange(
|
|
450
|
+
callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void,
|
|
451
|
+
): void;
|
|
448
452
|
/**
|
|
449
453
|
* Register a callback fired for every valid OSC 11 appearance report,
|
|
450
454
|
* including reports whose classification matches the current appearance.
|
|
451
455
|
* Unlike onAppearanceChange, this does not replay an earlier report.
|
|
452
456
|
* Optional so custom Terminals built against older pi-tui versions keep working.
|
|
453
457
|
*/
|
|
454
|
-
onAppearanceReport?(
|
|
458
|
+
onAppearanceReport?(
|
|
459
|
+
callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void,
|
|
460
|
+
): (() => void) | void;
|
|
455
461
|
/**
|
|
456
462
|
* Issue a single OSC 11 background-color re-query, driving the appearance
|
|
457
463
|
* callbacks through the same parse/dedup pipeline used at startup and on Mode
|
|
458
464
|
* 2031 notifications. Bounded: one probe per call, no timers. Invoked on the
|
|
459
|
-
* user's explicit display-reset gesture
|
|
465
|
+
* user's explicit display-reset gesture so terminals that cannot
|
|
460
466
|
* deliver end-to-end Mode 2031 notifications still pick up a light/dark switch
|
|
461
|
-
* without a restart.
|
|
462
|
-
*
|
|
467
|
+
* without a restart.
|
|
468
|
+
*
|
|
469
|
+
* A caller-provided token must be propagated unchanged to callbacks and
|
|
470
|
+
* returned when the request is accepted. This lets callers establish ownership
|
|
471
|
+
* before implementations synchronously dispatch a cached response. Optional so
|
|
472
|
+
* custom Terminals built against older pi-tui versions keep working.
|
|
463
473
|
*/
|
|
464
|
-
refreshAppearance?(): void;
|
|
474
|
+
refreshAppearance?(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
465
475
|
/** The last detected terminal appearance, or undefined if not yet known. */
|
|
466
476
|
get appearance(): TerminalAppearance | undefined;
|
|
467
477
|
/**
|
|
@@ -574,11 +584,17 @@ export class ProcessTerminal implements Terminal {
|
|
|
574
584
|
|
|
575
585
|
#windowsVTInputRestore?: () => void;
|
|
576
586
|
#xtermScrollToBottomRestoreModes = new Set<number>();
|
|
577
|
-
#appearanceCallbacks: Array<
|
|
578
|
-
|
|
587
|
+
#appearanceCallbacks: Array<
|
|
588
|
+
(appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void
|
|
589
|
+
> = [];
|
|
590
|
+
#appearanceReportCallbacks: Array<
|
|
591
|
+
(appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void
|
|
592
|
+
> = [];
|
|
579
593
|
#appearance: TerminalAppearance | undefined;
|
|
580
594
|
#osc11Pending = false;
|
|
581
|
-
#
|
|
595
|
+
#osc11ActiveToken?: TerminalAppearanceRequestToken;
|
|
596
|
+
#osc11QueuedQuery?: { route: Osc11QueryRoute; token?: TerminalAppearanceRequestToken };
|
|
597
|
+
#nextAppearanceRequestToken = 1;
|
|
582
598
|
#osc11ResponseBuffer = "";
|
|
583
599
|
#osc99PendingId: string | undefined;
|
|
584
600
|
#osc99ResponseBuffer = "";
|
|
@@ -624,7 +640,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
624
640
|
return this.#appearance;
|
|
625
641
|
}
|
|
626
642
|
|
|
627
|
-
onAppearanceChange(
|
|
643
|
+
onAppearanceChange(
|
|
644
|
+
callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void,
|
|
645
|
+
): void {
|
|
628
646
|
this.#appearanceCallbacks.push(callback);
|
|
629
647
|
// Replay an already-detected appearance: the startup OSC 11 response can
|
|
630
648
|
// arrive before consumers (e.g. the theme bridge) subscribe, and the
|
|
@@ -639,7 +657,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
639
657
|
}
|
|
640
658
|
}
|
|
641
659
|
|
|
642
|
-
onAppearanceReport(
|
|
660
|
+
onAppearanceReport(
|
|
661
|
+
callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void,
|
|
662
|
+
): () => void {
|
|
643
663
|
this.#appearanceReportCallbacks.push(callback);
|
|
644
664
|
let subscribed = true;
|
|
645
665
|
return () => {
|
|
@@ -659,9 +679,14 @@ export class ProcessTerminal implements Terminal {
|
|
|
659
679
|
* armed. Suppressed while inactive, headless, or after the terminal is torn
|
|
660
680
|
* down.
|
|
661
681
|
*/
|
|
662
|
-
refreshAppearance(): void {
|
|
682
|
+
refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void {
|
|
663
683
|
if (!this.#active || this.#headless || this.#dead) return;
|
|
664
|
-
|
|
684
|
+
const token = requestToken ?? this.#nextAppearanceRequestToken++;
|
|
685
|
+
if (token >= this.#nextAppearanceRequestToken) {
|
|
686
|
+
this.#nextAppearanceRequestToken = token + 1;
|
|
687
|
+
}
|
|
688
|
+
this.#queryBackgroundColor(isInsideTmux() ? "tmux" : "direct", token);
|
|
689
|
+
return token;
|
|
665
690
|
}
|
|
666
691
|
|
|
667
692
|
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void {
|
|
@@ -1011,18 +1036,19 @@ export class ProcessTerminal implements Terminal {
|
|
|
1011
1036
|
if (this.#osc11Pending) {
|
|
1012
1037
|
// DA1 arrived before the OSC 11 reply: terminal does not support OSC 11.
|
|
1013
1038
|
this.#osc11Pending = false;
|
|
1039
|
+
this.#osc11ActiveToken = undefined;
|
|
1014
1040
|
this.#osc11ResponseBuffer = "";
|
|
1015
1041
|
}
|
|
1016
1042
|
// Start a queued OSC 11 query once the prior cycle is fully drained.
|
|
1017
1043
|
if (
|
|
1018
|
-
this.#
|
|
1044
|
+
this.#osc11QueuedQuery !== undefined &&
|
|
1019
1045
|
!this.#osc11Pending &&
|
|
1020
1046
|
!this.#da1SentinelOwners.some(o => o.kind === "osc11") &&
|
|
1021
1047
|
!this.#dead
|
|
1022
1048
|
) {
|
|
1023
|
-
const
|
|
1024
|
-
this.#
|
|
1025
|
-
this.#startOsc11Query(route);
|
|
1049
|
+
const query = this.#osc11QueuedQuery;
|
|
1050
|
+
this.#osc11QueuedQuery = undefined;
|
|
1051
|
+
this.#startOsc11Query(query.route, query.token);
|
|
1026
1052
|
}
|
|
1027
1053
|
break;
|
|
1028
1054
|
}
|
|
@@ -1103,8 +1129,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
1103
1129
|
if (!osc11Match) return;
|
|
1104
1130
|
const [, rHex, gHex, bHex] = osc11Match;
|
|
1105
1131
|
this.#osc11Pending = false;
|
|
1132
|
+
const requestToken = this.#osc11ActiveToken;
|
|
1133
|
+
this.#osc11ActiveToken = undefined;
|
|
1106
1134
|
this.#osc11ResponseBuffer = "";
|
|
1107
|
-
this.#handleOsc11Response(rHex!, gHex!, bHex
|
|
1135
|
+
this.#handleOsc11Response(rHex!, gHex!, bHex!, requestToken);
|
|
1108
1136
|
return;
|
|
1109
1137
|
}
|
|
1110
1138
|
}
|
|
@@ -1157,22 +1185,28 @@ export class ProcessTerminal implements Terminal {
|
|
|
1157
1185
|
* DA1 avoids indefinite hangs: if DA1 response arrives before OSC 11,
|
|
1158
1186
|
* the terminal does not support OSC 11.
|
|
1159
1187
|
*/
|
|
1160
|
-
#queryBackgroundColor(route: Osc11QueryRoute = "direct"): void {
|
|
1188
|
+
#queryBackgroundColor(route: Osc11QueryRoute = "direct", token?: TerminalAppearanceRequestToken): void {
|
|
1161
1189
|
if (this.#dead) return;
|
|
1162
1190
|
// Queue if an OSC 11 query is in flight or its DA1 sentinel hasn't been
|
|
1163
1191
|
// consumed yet. Starting a new query while a DA1 is outstanding would
|
|
1164
1192
|
// increment the sentinel counter, and the old DA1 arrival would then
|
|
1165
1193
|
// prematurely clear the new query's pending state. Preserve a requested
|
|
1166
|
-
// tmux passthrough route when coalescing direct and explicit queries
|
|
1194
|
+
// tmux passthrough route when coalescing direct and explicit queries, and
|
|
1195
|
+
// retain the latest explicit request identity across automatic queries.
|
|
1167
1196
|
if (this.#osc11Pending || this.#da1SentinelOwners.some(o => o.kind === "osc11")) {
|
|
1168
|
-
|
|
1197
|
+
const queued = this.#osc11QueuedQuery;
|
|
1198
|
+
this.#osc11QueuedQuery = {
|
|
1199
|
+
route: queued?.route === "tmux" || route === "tmux" ? "tmux" : "direct",
|
|
1200
|
+
token: token ?? queued?.token,
|
|
1201
|
+
};
|
|
1169
1202
|
return;
|
|
1170
1203
|
}
|
|
1171
|
-
this.#startOsc11Query(route);
|
|
1204
|
+
this.#startOsc11Query(route, token);
|
|
1172
1205
|
}
|
|
1173
1206
|
|
|
1174
|
-
#startOsc11Query(route: Osc11QueryRoute): void {
|
|
1207
|
+
#startOsc11Query(route: Osc11QueryRoute, token?: TerminalAppearanceRequestToken): void {
|
|
1175
1208
|
this.#osc11Pending = true;
|
|
1209
|
+
this.#osc11ActiveToken = token;
|
|
1176
1210
|
this.#osc11ResponseBuffer = "";
|
|
1177
1211
|
this.#da1SentinelOwners.push({ kind: "osc11" });
|
|
1178
1212
|
if (route === "tmux") {
|
|
@@ -1237,7 +1271,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1237
1271
|
* Parse an OSC 11 background color response and compute BT.601 luminance.
|
|
1238
1272
|
* Handles 1-, 2-, 3-, and 4-digit XParseColor hex components.
|
|
1239
1273
|
*/
|
|
1240
|
-
#handleOsc11Response(rHex: string, gHex: string, bHex: string): void {
|
|
1274
|
+
#handleOsc11Response(rHex: string, gHex: string, bHex: string, requestToken?: TerminalAppearanceRequestToken): void {
|
|
1241
1275
|
const normalize = (hex: string): number => {
|
|
1242
1276
|
const value = parseInt(hex, 16);
|
|
1243
1277
|
if (Number.isNaN(value)) return 0;
|
|
@@ -1250,7 +1284,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1250
1284
|
this.#appearance = mode;
|
|
1251
1285
|
for (const cb of [...this.#appearanceReportCallbacks]) {
|
|
1252
1286
|
try {
|
|
1253
|
-
cb(mode);
|
|
1287
|
+
cb(mode, requestToken);
|
|
1254
1288
|
} catch {
|
|
1255
1289
|
/* ignore callback errors */
|
|
1256
1290
|
}
|
|
@@ -1258,7 +1292,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1258
1292
|
if (!changed) return;
|
|
1259
1293
|
for (const cb of this.#appearanceCallbacks) {
|
|
1260
1294
|
try {
|
|
1261
|
-
cb(mode);
|
|
1295
|
+
cb(mode, requestToken);
|
|
1262
1296
|
} catch {
|
|
1263
1297
|
/* ignore callback errors */
|
|
1264
1298
|
}
|
|
@@ -1516,9 +1550,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
1516
1550
|
this.#mode2031DebounceTimer = undefined;
|
|
1517
1551
|
}
|
|
1518
1552
|
this.#appearanceCallbacks = [];
|
|
1553
|
+
this.#appearanceReportCallbacks = [];
|
|
1519
1554
|
this.#osc11Pending = false;
|
|
1555
|
+
this.#osc11ActiveToken = undefined;
|
|
1520
1556
|
this.#clearWindowsTerminalAppearancePoll();
|
|
1521
|
-
this.#
|
|
1557
|
+
this.#osc11QueuedQuery = undefined;
|
|
1522
1558
|
this.#osc11ResponseBuffer = "";
|
|
1523
1559
|
this.#osc99PendingId = undefined;
|
|
1524
1560
|
this.#osc99ResponseBuffer = "";
|