@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 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;
@@ -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 (Ctrl+L) so terminals that cannot
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. Optional so custom Terminals built against older pi-tui
108
- * versions keep working.
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.1",
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.1",
41
- "@oh-my-pi/pi-utils": "17.2.1",
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: string[] = [];
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 line of renderedTokenLines) {
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(line) || isOsc66Line(line)) {
1882
- wrappedLines.push(line);
1894
+ if (token.type === "list" || TERMINAL.isImageLine(renderedRow.text) || isOsc66Line(renderedRow.text)) {
1895
+ wrappedLines.push(renderedRow);
1883
1896
  } else {
1884
- wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
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
- for (const line of wrappedLines) {
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): string[] {
1969
- const bodyLines: string[] = [];
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
- bodyLines.push(`${codeIndent}${hlLine}`);
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
- bodyLines.push(`${codeIndent}${hlLine}`);
2022
+ addBodyLine(hlLine);
1994
2023
  }
1995
2024
  if (!closedFence) {
1996
2025
  for (const codeLine of tokenText.slice(lineEnd + 1).split("\n")) {
1997
- bodyLines.push(`${codeIndent}${this.#theme.codeBlock(codeLine)}`);
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
- bodyLines.push(`${codeIndent}${this.#theme.codeBlock(codeLine)}`);
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
- ): string[] {
2185
- const lines: string[] = [];
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)) lines.push(this.#applyDefaultStyle(mathLine));
2232
- if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") lines.push("");
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
- lines.push(...(hangWrapTreeGuideLines(paragraphText, width) ?? [paragraphText]));
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
- visibleWidth(asciiLine) > width ? truncateToWidth(asciiLine, width, Ellipsis.Omit) : asciiLine,
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(...tableLines);
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: string[] = [];
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: string[], width: number, sourceRowOffsets?: number[]): string[] {
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: string[] = [];
2445
+ const lines: RenderedLine[] = [];
2410
2446
  sourceRowOffsets?.push(0);
2411
2447
  for (const quoteLine of renderedLines) {
2412
- const styledLine = applyQuoteStyle(quoteLine);
2413
- for (const wrappedLine of wrapTextWithAnsi(styledLine, quoteContentWidth)) {
2414
- lines.push(this.#theme.quoteBorder(`${this.#theme.symbols.quoteBorder} `) + wrappedLine);
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): string[] {
2428
- const lines: string[] = [];
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): string[] {
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): string[] {
2595
- const lines: string[] = [];
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 = (text: string, firstPrefix: string, continuationPrefix: string): void => {
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
- lines.push(...wrapTextWithAnsi(text, Math.max(1, width)));
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.text);
2694
+ lines.push(firstLine);
2634
2695
  } else {
2635
- pushWrapped(firstLine.text, firstPrefix, continuationIndent);
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.text);
2702
+ lines.push(line);
2642
2703
  } else {
2643
- pushWrapped(line.text, continuationIndent, continuationIndent);
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
- ): Array<{ text: string; nested: boolean }> {
2666
- const lines: Array<{ text: string; nested: boolean }> = [];
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({ text: nestedLine, nested: true });
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({ text: bodyLine, nested: false });
2766
+ lines.push({ ...bodyLine, nested: false });
2706
2767
  }
2707
2768
  lines.push({ text: this.#theme.codeBlockBorder("```"), nested: false });
2708
2769
  } else if (isMathToken(token)) {
@@ -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 (!this.#wasBlocked) {
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(callback: (appearance: TerminalAppearance) => void): void;
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?(callback: (appearance: TerminalAppearance) => void): (() => void) | void;
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 (Ctrl+L) so terminals that cannot
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. Optional so custom Terminals built against older pi-tui
462
- * versions keep working.
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<(appearance: TerminalAppearance) => void> = [];
578
- #appearanceReportCallbacks: Array<(appearance: TerminalAppearance) => void> = [];
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
- #osc11QueuedRoute?: Osc11QueryRoute;
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(callback: (appearance: TerminalAppearance) => void): void {
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(callback: (appearance: TerminalAppearance) => void): () => void {
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
- this.#queryBackgroundColor(isInsideTmux() ? "tmux" : "direct");
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.#osc11QueuedRoute !== undefined &&
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 route = this.#osc11QueuedRoute;
1024
- this.#osc11QueuedRoute = undefined;
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
- if (this.#osc11QueuedRoute !== "tmux") this.#osc11QueuedRoute = route;
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.#osc11QueuedRoute = undefined;
1557
+ this.#osc11QueuedQuery = undefined;
1522
1558
  this.#osc11ResponseBuffer = "";
1523
1559
  this.#osc99PendingId = undefined;
1524
1560
  this.#osc99ResponseBuffer = "";