@linxiraos/pi-tui 1.3.0 → 1.3.1
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 +157 -199
- package/dist/types/chrome/status-notice.d.ts +2 -0
- package/dist/types/prompt/composer.d.ts +2 -0
- package/dist/types/tools/web-search-types.d.ts +1 -0
- package/package.json +9 -9
- package/src/charts/table-data.ts +17 -6
- package/src/chrome/status-notice.ts +5 -0
- package/src/components/markdown.ts +21 -1
- package/src/prompt/composer-cache.ts +3 -0
- package/src/prompt/composer.ts +8 -2
- package/src/status-line/component.ts +56 -1
- package/src/stdin-buffer.ts +289 -61
- package/src/terminal.ts +30 -17
- package/src/tools/web-search-types.d.ts +1 -0
- package/src/tools/web-search-types.ts +1 -0
- package/src/tui.ts +20 -7
|
@@ -25,6 +25,7 @@ export declare const SEARCH_PROVIDER_LABELS: {
|
|
|
25
25
|
readonly tavily: "Tavily";
|
|
26
26
|
readonly firecrawl: "Firecrawl";
|
|
27
27
|
readonly brave: "Brave";
|
|
28
|
+
readonly deepseek: "DeepSeek";
|
|
28
29
|
readonly kimi: "Kimi";
|
|
29
30
|
readonly synthetic: "Synthetic";
|
|
30
31
|
readonly ollama: "Ollama";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-tui",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@linxiraos/pi-omptype": "1.3.
|
|
42
|
-
"@linxiraos/pi-agent-core": "1.3.
|
|
43
|
-
"@linxiraos/pi-ai": "1.3.
|
|
44
|
-
"@linxiraos/pi-catalog": "1.3.
|
|
45
|
-
"@linxiraos/pi-natives": "1.3.
|
|
46
|
-
"@linxiraos/pi-utils": "1.3.
|
|
47
|
-
"@linxiraos/pi-wire": "1.3.
|
|
48
|
-
"@linxiraos/pi-snapcompact": "1.3.
|
|
41
|
+
"@linxiraos/pi-omptype": "1.3.1",
|
|
42
|
+
"@linxiraos/pi-agent-core": "1.3.1",
|
|
43
|
+
"@linxiraos/pi-ai": "1.3.1",
|
|
44
|
+
"@linxiraos/pi-catalog": "1.3.1",
|
|
45
|
+
"@linxiraos/pi-natives": "1.3.1",
|
|
46
|
+
"@linxiraos/pi-utils": "1.3.1",
|
|
47
|
+
"@linxiraos/pi-wire": "1.3.1",
|
|
48
|
+
"@linxiraos/pi-snapcompact": "1.3.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/charts/table-data.ts
CHANGED
|
@@ -148,7 +148,17 @@ export interface TableAnalysis {
|
|
|
148
148
|
readonly composition?: Composition;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
/**
|
|
152
|
+
* A whole number as written: bare, or grouped in thousands by commas (`1,988`)
|
|
153
|
+
* or by spaces as many locales write it (`1 988`, also U+00A0 and U+202F).
|
|
154
|
+
* Space groups take exactly three digits, so `12 5` and `128 2560` stay apart.
|
|
155
|
+
*/
|
|
156
|
+
const INTEGER = String.raw`\d{1,3}(?:,\d{3})+|\d{1,3}(?:[ \u00A0\u202F]\d{3})+(?!\d)|\d+`;
|
|
157
|
+
const NUMBER = String.raw`(?:${INTEGER})(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+`;
|
|
158
|
+
/** A cell holding one whole number and nothing else (`38`, `1,980`). */
|
|
159
|
+
const WHOLE = new RegExp(String.raw`^(?:${INTEGER})$`);
|
|
160
|
+
/** The first number of a figure, its whole part and its decimals apart. */
|
|
161
|
+
const WRITTEN = new RegExp(String.raw`(${INTEGER})(?:\.(\d+))?`);
|
|
152
162
|
const CELL = new RegExp(
|
|
153
163
|
String.raw`^(?<approx>~|≈|<=|>=|<|>|≤|≥)?\s*(?<sign>[+\-−±])?\s*(?<cur>[$€£¥])?\s*(?<num>${NUMBER})(?<plus>\+(?=\s*[A-Za-zµμ%]))?\s*(?<unit>%|[A-Za-zµμ×]+(?:\/[A-Za-z]+)?|\/s)?(?<rest>.*)$`,
|
|
154
164
|
);
|
|
@@ -517,8 +527,9 @@ export function parseCell(markdown: string): Cell {
|
|
|
517
527
|
return { ...cell, fit: "prose" };
|
|
518
528
|
}
|
|
519
529
|
|
|
530
|
+
/** The value of a number {@link NUMBER} matched, its comma or space group separators removed. */
|
|
520
531
|
function toNumber(written: string): number {
|
|
521
|
-
return Number(written.
|
|
532
|
+
return Number(written.replace(/[,\s]/g, ""));
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
/** A number of a list or sum: its value as written, its unit (known or a noun) and how it was marked. */
|
|
@@ -632,7 +643,7 @@ function listCell(text: string, base: CellBase): NumberCell | undefined {
|
|
|
632
643
|
// A spaced pair of bare numbers that is no score (`25.0 / 27.8`, `12.6 / 9.4`: the second is no whole, or
|
|
633
644
|
// the first exceeds it) lists two values of the column's quantity; `27 / 30` stays a score.
|
|
634
645
|
const [first, second] = items;
|
|
635
|
-
const whole =
|
|
646
|
+
const whole = WHOLE.test(text.slice(text.indexOf("/") + 1, end).trim());
|
|
636
647
|
const score = whole && first!.value >= 0 && first!.value <= second!.value;
|
|
637
648
|
const pair = items.length === 2 && !marked.some(Boolean) && separators[0] !== "/" && !score;
|
|
638
649
|
if (!pair && (!marked.at(-1) || !(marked.every(Boolean) || carried))) return undefined;
|
|
@@ -876,9 +887,9 @@ const WHOLE_HEADER = /\b(?:total|all|sum|overall)\b/i;
|
|
|
876
887
|
*/
|
|
877
888
|
export function writtenSlack(cell: NumberCell): number {
|
|
878
889
|
if (cell.denominator !== undefined && cell.dim === "percent") return 0;
|
|
879
|
-
const match =
|
|
890
|
+
const match = WRITTEN.exec(cell.figure);
|
|
880
891
|
if (!match) return 0;
|
|
881
|
-
const written =
|
|
892
|
+
const written = toNumber(`${match[1]!}${match[2] ? `.${match[2]}` : ""}`);
|
|
882
893
|
const factor = written ? Math.abs(cell.value) / written : 1;
|
|
883
894
|
const decimals = match[2]?.length ?? 0;
|
|
884
895
|
if (cell.dim === "count" && decimals === 0 && factor === 1 && !cell.approx) return 0;
|
|
@@ -1097,7 +1108,7 @@ const MIN_LISTS = 2;
|
|
|
1097
1108
|
/** A spaced slash between list items. */
|
|
1098
1109
|
const LIST_SLASH = /\s+\/\s+/;
|
|
1099
1110
|
/** A list item that is one number, as written (`12`, `~1,400`, `3.5 ms`, `40%`). */
|
|
1100
|
-
const LISTED_NUMBER =
|
|
1111
|
+
const LISTED_NUMBER = new RegExp(String.raw`^[~≈]?[$€£¥]?(?:${INTEGER})(?:\.\d+)?\s*(?:%|[A-Za-zµμ]+)?$`);
|
|
1101
1112
|
|
|
1102
1113
|
/**
|
|
1103
1114
|
* Rows folding several rows into one (`response anthropic / chat / responses /
|
|
@@ -42,6 +42,11 @@ export class StatusNotice extends Container {
|
|
|
42
42
|
this.addChild(this.#text);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** The text the notice currently shows. */
|
|
46
|
+
get message(): string {
|
|
47
|
+
return this.#message;
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
setMessage(message: string, options: StatusNoticeOptions = {}): void {
|
|
46
51
|
const toast = options.toast ?? true;
|
|
47
52
|
this.#text.setStyleFn(options.styleFn);
|
|
@@ -4611,7 +4611,27 @@ export class Markdown implements Component {
|
|
|
4611
4611
|
const text = this.#renderInlineTokens(cell.tokens || [], styleContext);
|
|
4612
4612
|
return this.#wrapCellText(text, columnWidths[i]);
|
|
4613
4613
|
});
|
|
4614
|
-
|
|
4614
|
+
let rowLineCount = Math.max(...rowCellLines.map(c => c.length));
|
|
4615
|
+
// The open cell is laid out in widths measured without it, so it can
|
|
4616
|
+
// wrap past the lines its row's finished cells need. Those extra lines
|
|
4617
|
+
// come and go as the cell grows and the column widens, moving every row
|
|
4618
|
+
// below the table each frame; hold the row at its finished cells' height
|
|
4619
|
+
// until the cell closes and counts toward the widths. While lines are
|
|
4620
|
+
// held back, only the last kept line makes room for a `…`: it is
|
|
4621
|
+
// re-wrapped one column narrower, since the wrapper closes the line's
|
|
4622
|
+
// links and styles where truncation would cut them off, and dropped when
|
|
4623
|
+
// even that overflows (a wide glyph in a two-cell column).
|
|
4624
|
+
if (rowIndex === lastRow && rowCellLines[openCell]) {
|
|
4625
|
+
const settledLineCount = Math.max(1, ...rowCellLines.filter((_, i) => i !== openCell).map(c => c.length));
|
|
4626
|
+
if (rowCellLines[openCell].length > settledLineCount) {
|
|
4627
|
+
const width = columnWidths[openCell];
|
|
4628
|
+
const kept = rowCellLines[openCell].slice(0, settledLineCount);
|
|
4629
|
+
const last = width > 1 ? (this.#wrapCellText(kept[settledLineCount - 1], width - 1)[0] ?? "") : "";
|
|
4630
|
+
kept[settledLineCount - 1] = `${visibleWidth(last) < width ? last : ""}…`;
|
|
4631
|
+
rowCellLines[openCell] = kept;
|
|
4632
|
+
}
|
|
4633
|
+
rowLineCount = settledLineCount;
|
|
4634
|
+
}
|
|
4615
4635
|
|
|
4616
4636
|
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
|
|
4617
4637
|
const rowParts = rowCellLines.map((cellLines, colIdx) => {
|
|
@@ -94,6 +94,7 @@ function parseUiState(
|
|
|
94
94
|
showHardwareCursor,
|
|
95
95
|
maxInlineImages,
|
|
96
96
|
resizeScrollback,
|
|
97
|
+
completeScrollback,
|
|
97
98
|
imeSafeCursor,
|
|
98
99
|
autocompleteMaxVisible,
|
|
99
100
|
spellingTypoDetection,
|
|
@@ -109,6 +110,7 @@ function parseUiState(
|
|
|
109
110
|
resizeScrollback !== "append" &&
|
|
110
111
|
resizeScrollback !== "rebuild" &&
|
|
111
112
|
resizeScrollback !== "preserve") ||
|
|
113
|
+
(completeScrollback !== undefined && typeof completeScrollback !== "boolean") ||
|
|
112
114
|
typeof imeSafeCursor !== "boolean" ||
|
|
113
115
|
typeof autocompleteMaxVisible !== "number" ||
|
|
114
116
|
typeof spellingTypoDetection !== "boolean" ||
|
|
@@ -136,6 +138,7 @@ function parseUiState(
|
|
|
136
138
|
showHardwareCursor,
|
|
137
139
|
maxInlineImages,
|
|
138
140
|
resizeScrollback: resizeScrollback ?? "rebuild",
|
|
141
|
+
completeScrollback: completeScrollback ?? false,
|
|
139
142
|
imeSafeCursor,
|
|
140
143
|
autocompleteMaxVisible,
|
|
141
144
|
spellingTypoDetection,
|
package/src/prompt/composer.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface ComposerPreferences {
|
|
|
37
37
|
readonly showHardwareCursor: boolean;
|
|
38
38
|
readonly maxInlineImages: number;
|
|
39
39
|
readonly resizeScrollback: ResizeScrollbackMode;
|
|
40
|
+
/** Send rows a growing HUD or dialog pushes off screen straight to scrollback instead of holding them back until it shrinks. */
|
|
41
|
+
readonly completeScrollback: boolean;
|
|
40
42
|
readonly imeSafeCursor: boolean;
|
|
41
43
|
readonly autocompleteMaxVisible: number;
|
|
42
44
|
readonly spellingTypoDetection: boolean;
|
|
@@ -51,6 +53,7 @@ export const COMPOSER_DEFAULTS: ComposerPreferences = {
|
|
|
51
53
|
showHardwareCursor: true,
|
|
52
54
|
maxInlineImages: 8,
|
|
53
55
|
resizeScrollback: "rebuild",
|
|
56
|
+
completeScrollback: false,
|
|
54
57
|
imeSafeCursor: false,
|
|
55
58
|
autocompleteMaxVisible: 10,
|
|
56
59
|
spellingTypoDetection: true,
|
|
@@ -271,6 +274,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
271
274
|
// displaced rows, so those rows refill the screen when it shrinks (#11007).
|
|
272
275
|
// Displaced rows reach history when newer output pushes them past the floor
|
|
273
276
|
// or persistent idle chrome settles and raises it.
|
|
277
|
+
// With `completeScrollback`, rows the chrome pushes off screen go straight
|
|
278
|
+
// to scrollback, and when it shrinks the freed rows stay blank.
|
|
274
279
|
// The floor is terminal-height independent — chrome heights do not scale
|
|
275
280
|
// with rows — so it is retained across resizes rather than rediscovered
|
|
276
281
|
// from whatever chrome is expanded when the height changes.
|
|
@@ -429,7 +434,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
429
434
|
const promotedChromeContracted = this.#chromeFloorPromoted && eligibleChrome < (this.#chromeFloor ?? 0);
|
|
430
435
|
this.#chromeFloor = Math.min(this.#chromeFloor ?? eligibleChrome, eligibleChrome);
|
|
431
436
|
this.#rebaselineChromeFloor(this.#runActive || transcript.hasActiveBlock(), eligibleChrome);
|
|
432
|
-
const belowFloor = this.#chromeFloor;
|
|
437
|
+
const belowFloor = this.#preferences.completeScrollback ? after.length : this.#chromeFloor;
|
|
433
438
|
// Offer history under capacity pressure only: blocks stay live (and keep
|
|
434
439
|
// reflowing to the current width) while the screen has room. A batch
|
|
435
440
|
// leaves the mutable viewport in the same frame it is appended, so its
|
|
@@ -447,7 +452,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
447
452
|
// billed against, so its compaction allocator (one row per block, no
|
|
448
453
|
// inter-block blanks) engages only when a block genuinely cannot retire.
|
|
449
454
|
// Rows the chrome above its floor displaces are clipped from the top by
|
|
450
|
-
// the `drop` slice below; they stay live until the chrome shrinks.
|
|
455
|
+
// the `drop` slice below; they stay live until the chrome shrinks. With
|
|
456
|
+
// `completeScrollback` none are held back.
|
|
451
457
|
const active = transcript.renderViewport(width, Math.max(0, rows - before.length - belowFloor), frame);
|
|
452
458
|
const activeSpans: ViewportClickSpan[] = [];
|
|
453
459
|
for (const span of transcript.getLastViewportSpans()) {
|
|
@@ -43,7 +43,7 @@ import type { TspMeterMark, TspProps } from "@linxiraos/pi-wire";
|
|
|
43
43
|
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
44
44
|
import { col, node, span } from "../native/describe";
|
|
45
45
|
import { getContextMeterThresholds } from "../chrome/context-thresholds";
|
|
46
|
-
import { isNativeRendering } from "../native/state";
|
|
46
|
+
import { isNativeRendering, onNativeRenderingChange } from "../native/state";
|
|
47
47
|
import { getSeparator } from "./separators";
|
|
48
48
|
import type {
|
|
49
49
|
CollabStatus,
|
|
@@ -162,6 +162,10 @@ const WATCHER_FAILURE_POLL_TTL_MS = 5000;
|
|
|
162
162
|
const BRAND_FADE_MS = 450;
|
|
163
163
|
/** Repaint cadence while the brand fade is in flight (rust zeta's `FADE_FRAME`). */
|
|
164
164
|
const BRAND_FADE_FRAME_MS = 40;
|
|
165
|
+
/** Repaint cadence while a turn's active-processing window is open: keeps the
|
|
166
|
+
* status bar's clock-driven readouts (pi spinner, turn timer) animating when
|
|
167
|
+
* no other component is requesting renders. */
|
|
168
|
+
const ACTIVE_WINDOW_REPAINT_MS = 250;
|
|
165
169
|
|
|
166
170
|
/**
|
|
167
171
|
* Providers whose subscription quota is a single monthly bucket, so their
|
|
@@ -698,6 +702,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
698
702
|
#gitWatcherUnavailable = false;
|
|
699
703
|
#onBranchChange: (() => void) | null = null;
|
|
700
704
|
#disposed = false;
|
|
705
|
+
/** Unsubscribe for the native-rendering feed; re-arms the turn-window ticker when a TSP surface is revoked. */
|
|
706
|
+
#unwatchNativeRendering: (() => void) | undefined;
|
|
701
707
|
#autoCompactEnabled: boolean = true;
|
|
702
708
|
/** Pulse timer for the running-speculation indicator; live only while speculation runs. */
|
|
703
709
|
#speculationBlinkTimer: NodeJS.Timeout | undefined;
|
|
@@ -708,6 +714,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
708
714
|
#brandWorking = false;
|
|
709
715
|
/** Frame timer driving repaints while the brand fade is unsettled. */
|
|
710
716
|
#brandFadeTimer: NodeJS.Timeout | undefined;
|
|
717
|
+
/** Repaint ticker while a turn's active-processing window is open (see #syncActiveWindowRepaint). */
|
|
718
|
+
#activeWindowRepaintTimer: NodeJS.Timeout | undefined;
|
|
711
719
|
/** One wall-clock wakeup for the active model's next tariff change, including while idle. */
|
|
712
720
|
#pricingTimer: NodeJS.Timeout | undefined;
|
|
713
721
|
#pricingTimerCost: ModelCost | undefined;
|
|
@@ -833,6 +841,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
833
841
|
private readonly host: StatusLineHost<TSession>,
|
|
834
842
|
) {
|
|
835
843
|
this.#settings = host.getSettings();
|
|
844
|
+
this.#unwatchNativeRendering = onNativeRenderingChange(() => this.#syncActiveWindowRepaint());
|
|
836
845
|
}
|
|
837
846
|
|
|
838
847
|
#gitEnabled(): boolean {
|
|
@@ -939,6 +948,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
939
948
|
this.#invalidateSessionCaches();
|
|
940
949
|
this.#closeStaleActiveWindow();
|
|
941
950
|
}
|
|
951
|
+
// The ticker self-stops once its tick sees no open window for the
|
|
952
|
+
// newly-attached session; re-pointing focus at a session whose turn
|
|
953
|
+
// is still running must re-arm it, and no other path reliably fires
|
|
954
|
+
// here (closeStaleActiveWindow returns early while streaming, and
|
|
955
|
+
// the synthesized agent_start no-ops on an already-open window).
|
|
956
|
+
this.#syncActiveWindowRepaint();
|
|
942
957
|
this.invalidate();
|
|
943
958
|
}
|
|
944
959
|
|
|
@@ -1037,6 +1052,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1037
1052
|
meter.activeMs = 0;
|
|
1038
1053
|
meter.activeStartedAt = null;
|
|
1039
1054
|
this.#invalidateStatusLineRenderCache();
|
|
1055
|
+
this.#syncActiveWindowRepaint();
|
|
1040
1056
|
}
|
|
1041
1057
|
|
|
1042
1058
|
/**
|
|
@@ -1051,6 +1067,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1051
1067
|
if (meter.activeStartedAt !== null) return;
|
|
1052
1068
|
meter.activeStartedAt = Date.now();
|
|
1053
1069
|
this.#invalidateStatusLineRenderCache();
|
|
1070
|
+
this.#syncActiveWindowRepaint();
|
|
1054
1071
|
}
|
|
1055
1072
|
|
|
1056
1073
|
/**
|
|
@@ -1065,6 +1082,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1065
1082
|
meter.activeMs += Math.max(0, Date.now() - meter.activeStartedAt);
|
|
1066
1083
|
meter.activeStartedAt = null;
|
|
1067
1084
|
this.#invalidateStatusLineRenderCache();
|
|
1085
|
+
this.#syncActiveWindowRepaint();
|
|
1068
1086
|
}
|
|
1069
1087
|
|
|
1070
1088
|
/**
|
|
@@ -1294,8 +1312,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1294
1312
|
this.#onBranchChange = null;
|
|
1295
1313
|
this.#stopSpeculationBlink();
|
|
1296
1314
|
this.#stopBrandFadeTimer();
|
|
1315
|
+
this.#stopActiveWindowRepaint();
|
|
1297
1316
|
this.#stopPricingTimer();
|
|
1298
1317
|
this.#clearUsageStartTimer();
|
|
1318
|
+
this.#unwatchNativeRendering?.();
|
|
1319
|
+
this.#unwatchNativeRendering = undefined;
|
|
1299
1320
|
this.#onCodexResetFireworks = undefined;
|
|
1300
1321
|
this.#onResetExpiryNotice = undefined;
|
|
1301
1322
|
this.#codexResetSnapshots.clear();
|
|
@@ -1399,6 +1420,40 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1399
1420
|
this.#brandFadeTimer = undefined;
|
|
1400
1421
|
}
|
|
1401
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Keep the status bar repainting while a turn's active-processing window
|
|
1425
|
+
* is open. The `pi` segment's spinner and turn timer are painted from the
|
|
1426
|
+
* wall clock at render time, so they only animate when frames flow; during
|
|
1427
|
+
* a quiet turn the working-row loader's 80 ms tick is normally the sole
|
|
1428
|
+
* repaint source, and paths that stop or bypass that loader mid-turn (an
|
|
1429
|
+
* error banner, a stale retry/compaction loader reference) froze the
|
|
1430
|
+
* spinner until the next keystroke. The status line owns the active
|
|
1431
|
+
* window, so it owns a repaint source for it; a native surface clocks its
|
|
1432
|
+
* own spinner and needs none.
|
|
1433
|
+
*/
|
|
1434
|
+
#syncActiveWindowRepaint(): void {
|
|
1435
|
+
if (this.#disposed || isNativeRendering() || this.getTurnElapsedMs() === null) {
|
|
1436
|
+
this.#stopActiveWindowRepaint();
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (this.#activeWindowRepaintTimer) return;
|
|
1440
|
+
const timer = setInterval(() => {
|
|
1441
|
+
if (this.#disposed || isNativeRendering() || this.getTurnElapsedMs() === null) {
|
|
1442
|
+
this.#stopActiveWindowRepaint();
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
this.#onBranchChange?.();
|
|
1446
|
+
}, ACTIVE_WINDOW_REPAINT_MS);
|
|
1447
|
+
timer.unref?.();
|
|
1448
|
+
this.#activeWindowRepaintTimer = timer;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
#stopActiveWindowRepaint(): void {
|
|
1452
|
+
if (!this.#activeWindowRepaintTimer) return;
|
|
1453
|
+
clearInterval(this.#activeWindowRepaintTimer);
|
|
1454
|
+
this.#activeWindowRepaintTimer = undefined;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1402
1457
|
#stopPricingTimer(): void {
|
|
1403
1458
|
clearTimeout(this.#pricingTimer);
|
|
1404
1459
|
this.#pricingTimer = undefined;
|