@oh-my-pi/pi-tui 18.4.2 → 18.4.3
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 +20 -0
- package/dist/types/chat/thinking-display.d.ts +2 -0
- package/dist/types/components/editor.d.ts +5 -0
- package/dist/types/loop-watchdog.d.ts +9 -0
- package/dist/types/overlays/model-picker.d.ts +2 -1
- package/dist/types/setup/scenes/sign-in.d.ts +9 -9
- package/dist/types/setup/scenes/types.d.ts +0 -26
- package/dist/types/stdin-buffer.d.ts +10 -7
- package/dist/types/terminal.d.ts +9 -0
- package/dist/types/tools/web-search-types.d.ts +90 -0
- package/dist/types/tools/web-search.d.ts +1 -168
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +192 -40
- package/src/chat/thinking-display.ts +73 -11
- package/src/chrome/transcript-container.ts +102 -17
- package/src/components/editor.ts +23 -9
- package/src/components/markdown.ts +110 -56
- package/src/loop-watchdog.ts +38 -10
- package/src/overlays/model-picker.ts +9 -6
- package/src/overlays/settings-selector.ts +2 -10
- package/src/setup/scenes/sign-in.ts +17 -12
- package/src/setup/scenes/types.ts +0 -27
- package/src/setup/wizard.ts +1 -1
- package/src/status-line/component.ts +16 -2
- package/src/stdin-buffer.ts +27 -21
- package/src/terminal.ts +23 -9
- package/src/theme/mermaid-cache.ts +10 -1
- package/src/theme/tui-adapters.ts +14 -1
- package/src/tools/bash.ts +53 -13
- package/src/tools/edit.ts +110 -29
- package/src/tools/streaming-output.ts +79 -16
- package/src/tools/web-search-types.ts +99 -0
- package/src/tools/web-search.ts +1 -172
- package/src/tui.ts +1 -1
- package/dist/types/setup/scenes/providers.d.ts +0 -3
- package/dist/types/setup/scenes/web-search.d.ts +0 -22
- package/src/setup/scenes/providers.ts +0 -110
- package/src/setup/scenes/web-search.ts +0 -165
|
@@ -880,7 +880,10 @@ for (const table of [Lexer.rules.block.normal, Lexer.rules.block.gfm]) {
|
|
|
880
880
|
|
|
881
881
|
const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
|
|
882
882
|
const RENDER_CACHE_MAX_SIZE = 4 * 1024 * 1024;
|
|
883
|
-
|
|
883
|
+
// Entry size counts the key (it embeds the whole normalized source) plus the
|
|
884
|
+
// rendered rows, so this admits roughly the same documents a rows-only 256 KiB
|
|
885
|
+
// cap did while the aggregate bound covers everything the entry retains.
|
|
886
|
+
const RENDER_CACHE_MAX_ENTRY_SIZE = 512 * 1024;
|
|
884
887
|
const EMPTY_RENDER_LINES: readonly string[] = [];
|
|
885
888
|
|
|
886
889
|
interface RenderedLine {
|
|
@@ -900,7 +903,7 @@ const renderCache = new LRUCache<string, readonly string[]>({
|
|
|
900
903
|
max: RENDER_CACHE_MAX,
|
|
901
904
|
maxSize: RENDER_CACHE_MAX_SIZE,
|
|
902
905
|
maxEntrySize: RENDER_CACHE_MAX_ENTRY_SIZE,
|
|
903
|
-
sizeCalculation: renderedLinesCacheSize,
|
|
906
|
+
sizeCalculation: (lines, key) => renderedLinesCacheSize(lines) + key.length,
|
|
904
907
|
});
|
|
905
908
|
|
|
906
909
|
function renderedLinesCacheSize(lines: readonly string[]): number {
|
|
@@ -909,6 +912,15 @@ function renderedLinesCacheSize(lines: readonly string[]): number {
|
|
|
909
912
|
return Math.max(1, size);
|
|
910
913
|
}
|
|
911
914
|
|
|
915
|
+
/**
|
|
916
|
+
* Append `src` onto `dst` element-wise. `dst.push(...src)` passes every row as
|
|
917
|
+
* a call argument, which throws past the engine's argument limit on very long
|
|
918
|
+
* documents.
|
|
919
|
+
*/
|
|
920
|
+
function appendLines(dst: string[], src: readonly string[]): void {
|
|
921
|
+
for (let i = 0; i < src.length; i++) dst.push(src[i]!);
|
|
922
|
+
}
|
|
923
|
+
|
|
912
924
|
// ---------------------------------------------------------------------------
|
|
913
925
|
// Fast-tail (B+) hazard gates
|
|
914
926
|
// ---------------------------------------------------------------------------
|
|
@@ -1661,7 +1673,9 @@ interface RenderSignature {
|
|
|
1661
1673
|
interface StreamPrefixLineCache extends RenderSignature {
|
|
1662
1674
|
text: string;
|
|
1663
1675
|
tokenCount: number;
|
|
1664
|
-
|
|
1676
|
+
// Private to the cache and never handed to callers (each frame copies it
|
|
1677
|
+
// into a fresh output array), so an advancing prefix appends in place.
|
|
1678
|
+
lines: string[];
|
|
1665
1679
|
}
|
|
1666
1680
|
/**
|
|
1667
1681
|
* Per-token row cache for the *unfrozen tail* (PoC H). The tail re-lexes every
|
|
@@ -1717,7 +1731,9 @@ interface TailRenderRecorder {
|
|
|
1717
1731
|
interface StreamingHighlightCache extends RenderSignature {
|
|
1718
1732
|
lang: string | undefined;
|
|
1719
1733
|
text: string;
|
|
1720
|
-
|
|
1734
|
+
// Appended in place as the fence grows. Only ever returned to
|
|
1735
|
+
// #renderCodeBodyLines, which reads it synchronously and keeps no reference.
|
|
1736
|
+
lines: string[];
|
|
1721
1737
|
stream: HighlightStreamSession;
|
|
1722
1738
|
}
|
|
1723
1739
|
|
|
@@ -1882,10 +1898,7 @@ export class Markdown implements Component {
|
|
|
1882
1898
|
// Blank replacement: render() early-returns before #lexTokens can see
|
|
1883
1899
|
// the non-append edit, so drop the frozen stream state here or it
|
|
1884
1900
|
// outlives the content it indexed.
|
|
1885
|
-
this.#
|
|
1886
|
-
this.#streamPrefixTokens = undefined;
|
|
1887
|
-
this.#streamPrefixLineCache = undefined;
|
|
1888
|
-
this.#tailRowCache = undefined;
|
|
1901
|
+
this.#dropStreamPrefix();
|
|
1889
1902
|
// B+: the captured fast-path rows index the replaced content — drop
|
|
1890
1903
|
// the recipe so a fresh stream cannot splice onto stale rows.
|
|
1891
1904
|
this.#fastTail = undefined;
|
|
@@ -1927,6 +1940,17 @@ export class Markdown implements Component {
|
|
|
1927
1940
|
// source — drop the fast-path recipe so stale rows cannot be served
|
|
1928
1941
|
// across the transition.
|
|
1929
1942
|
this.#fastTail = undefined;
|
|
1943
|
+
if (!next) {
|
|
1944
|
+
// Finalize: the prefix row cache, tail row cache and open-fence
|
|
1945
|
+
// highlight stream are read only by transient renders, and each
|
|
1946
|
+
// rebuilds from scratch if the block ever streams again — release
|
|
1947
|
+
// them so settled blocks stop pinning rows and a native parser.
|
|
1948
|
+
// The frozen lex prefix survives only until the next final-mode
|
|
1949
|
+
// render consumes it (see #lexTokens / the L2 hit path).
|
|
1950
|
+
this.#streamPrefixLineCache = undefined;
|
|
1951
|
+
this.#tailRowCache = undefined;
|
|
1952
|
+
this.#streamingHighlightCache = undefined;
|
|
1953
|
+
}
|
|
1930
1954
|
this.invalidate();
|
|
1931
1955
|
}
|
|
1932
1956
|
|
|
@@ -1964,8 +1988,14 @@ export class Markdown implements Component {
|
|
|
1964
1988
|
// happens exactly when the CR/ref-def trigger behind a false verdict
|
|
1965
1989
|
// may have been deleted — never left stale, and never re-scanned on
|
|
1966
1990
|
// frames where the memo is sound.
|
|
1991
|
+
const appendGrowth = this.#lastScanValid && this.#appendOnlySinceLastScan && text.length > this.#lastScanLength;
|
|
1992
|
+
// Final mode keeps the frozen prefix only while the text keeps growing
|
|
1993
|
+
// by appends (a stream rendered without the transient flag). A one-shot
|
|
1994
|
+
// or just-finalized render still lexes against any existing prefix but
|
|
1995
|
+
// then drops it, so settled blocks do not pin a token tree.
|
|
1996
|
+
const retainPrefix = this.#transientRenderCache || appendGrowth;
|
|
1967
1997
|
let canStream: boolean;
|
|
1968
|
-
if (
|
|
1998
|
+
if (appendGrowth) {
|
|
1969
1999
|
const delta = text.slice(this.#lastScanLength);
|
|
1970
2000
|
if (
|
|
1971
2001
|
!delta.includes("[") &&
|
|
@@ -1990,21 +2020,27 @@ export class Markdown implements Component {
|
|
|
1990
2020
|
if (canStream && hasPrefix) {
|
|
1991
2021
|
const tailTokens = lexDocument(refDefText);
|
|
1992
2022
|
const tokens = [...prefixTokens, ...tailTokens];
|
|
1993
|
-
this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2023
|
+
if (retainPrefix) this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2024
|
+
else this.#dropStreamPrefix();
|
|
1994
2025
|
return tokens;
|
|
1995
2026
|
}
|
|
1996
2027
|
const tokens = lexDocument(text);
|
|
1997
|
-
if (canStream) {
|
|
2028
|
+
if (canStream && retainPrefix) {
|
|
1998
2029
|
this.#freezeStablePrefix(text, tokens, { preserveExisting: false });
|
|
1999
2030
|
} else {
|
|
2000
|
-
this.#
|
|
2001
|
-
this.#streamPrefixTokens = undefined;
|
|
2002
|
-
this.#streamPrefixLineCache = undefined;
|
|
2003
|
-
this.#tailRowCache = undefined;
|
|
2031
|
+
this.#dropStreamPrefix();
|
|
2004
2032
|
}
|
|
2005
2033
|
return tokens;
|
|
2006
2034
|
}
|
|
2007
2035
|
|
|
2036
|
+
/** Drop the frozen lex prefix and the transient row caches keyed on it. */
|
|
2037
|
+
#dropStreamPrefix(): void {
|
|
2038
|
+
this.#streamPrefixText = undefined;
|
|
2039
|
+
this.#streamPrefixTokens = undefined;
|
|
2040
|
+
this.#streamPrefixLineCache = undefined;
|
|
2041
|
+
this.#tailRowCache = undefined;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2008
2044
|
// Freeze the largest run of leading blocks that end on a hard "\n\n" boundary
|
|
2009
2045
|
// (complete and immutable under append-only growth) so the next streaming
|
|
2010
2046
|
// render re-lexes only the unfrozen tail. Caller guarantees no CR / no
|
|
@@ -2032,12 +2068,7 @@ export class Markdown implements Component {
|
|
|
2032
2068
|
return;
|
|
2033
2069
|
}
|
|
2034
2070
|
|
|
2035
|
-
if (!opts.preserveExisting)
|
|
2036
|
-
this.#streamPrefixText = undefined;
|
|
2037
|
-
this.#streamPrefixTokens = undefined;
|
|
2038
|
-
this.#streamPrefixLineCache = undefined;
|
|
2039
|
-
this.#tailRowCache = undefined;
|
|
2040
|
-
}
|
|
2071
|
+
if (!opts.preserveExisting) this.#dropStreamPrefix();
|
|
2041
2072
|
}
|
|
2042
2073
|
|
|
2043
2074
|
render(width: number): readonly string[] {
|
|
@@ -2166,9 +2197,12 @@ export class Markdown implements Component {
|
|
|
2166
2197
|
);
|
|
2167
2198
|
}
|
|
2168
2199
|
// Splice onto the previous frame's rows (new array — parent may
|
|
2169
|
-
// hold the old one).
|
|
2200
|
+
// hold the old one). One copy of the head, then the few new and
|
|
2201
|
+
// trailing rows, instead of spreading two intermediate slices.
|
|
2170
2202
|
const prev = recipe.lines;
|
|
2171
|
-
const fastResult =
|
|
2203
|
+
const fastResult = prev.slice(0, recipe.rowStart);
|
|
2204
|
+
appendLines(fastResult, fastRows);
|
|
2205
|
+
for (let i = recipe.rowEnd; i < prev.length; i++) fastResult.push(prev[i]!);
|
|
2172
2206
|
this.#cachedText = this.#text;
|
|
2173
2207
|
this.#cachedWidth = width;
|
|
2174
2208
|
this.#cachedLines = fastResult;
|
|
@@ -2220,6 +2254,10 @@ export class Markdown implements Component {
|
|
|
2220
2254
|
cacheKey = this.#renderCacheKey(normalizedText, signature);
|
|
2221
2255
|
const cached = renderCache.get(cacheKey);
|
|
2222
2256
|
if (cached !== undefined) {
|
|
2257
|
+
// A final-mode hit right after finalize (or a non-append edit)
|
|
2258
|
+
// never reaches #lexTokens, which would otherwise consume and drop
|
|
2259
|
+
// the frozen lex prefix — release it here instead.
|
|
2260
|
+
if (!this.#appendOnlySinceLastScan) this.#dropStreamPrefix();
|
|
2223
2261
|
// Populate L1 so subsequent calls from this instance are O(1) map lookup.
|
|
2224
2262
|
this.#cachedText = this.#text;
|
|
2225
2263
|
this.#cachedWidth = width;
|
|
@@ -2241,8 +2279,10 @@ export class Markdown implements Component {
|
|
|
2241
2279
|
}
|
|
2242
2280
|
const emptyLines = this.#renderEmptyPaddingLines(signature);
|
|
2243
2281
|
|
|
2244
|
-
// Combine top padding, content, and bottom padding
|
|
2245
|
-
|
|
2282
|
+
// Combine top padding, content, and bottom padding. contentLines is a
|
|
2283
|
+
// fresh per-render array (never cache-owned), so without vertical
|
|
2284
|
+
// padding it is handed out as-is rather than copied again.
|
|
2285
|
+
const rawResult = emptyLines.length === 0 ? contentLines : [...emptyLines, ...contentLines, ...emptyLines];
|
|
2246
2286
|
const result = rawResult.length > 0 ? rawResult : [""];
|
|
2247
2287
|
|
|
2248
2288
|
// Update caches and hand the array out by reference. Callers must not
|
|
@@ -2329,11 +2369,15 @@ export class Markdown implements Component {
|
|
|
2329
2369
|
return this.#renderStreamingTail(tokens, 0, contentWidth, signature);
|
|
2330
2370
|
}
|
|
2331
2371
|
|
|
2332
|
-
|
|
2372
|
+
// Prefix rows live in #streamPrefixLineCache.lines, which is private to
|
|
2373
|
+
// the cache: an advancing prefix appends its newly frozen rows in place
|
|
2374
|
+
// instead of re-snapshotting, and each frame's output is one fresh copy
|
|
2375
|
+
// of prefix + tail, so no array handed to a caller is ever mutated.
|
|
2333
2376
|
const reusablePrefix = this.#matchingStreamPrefixLineCache(normalizedText, stableText, signature);
|
|
2377
|
+
let prefixLines: string[] = [];
|
|
2334
2378
|
let renderedUntil = 0;
|
|
2335
2379
|
if (reusablePrefix && reusablePrefix.tokenCount <= stableTokenCount) {
|
|
2336
|
-
|
|
2380
|
+
prefixLines = reusablePrefix.lines;
|
|
2337
2381
|
renderedUntil = reusablePrefix.tokenCount;
|
|
2338
2382
|
}
|
|
2339
2383
|
|
|
@@ -2341,13 +2385,14 @@ export class Markdown implements Component {
|
|
|
2341
2385
|
// Stable tokens render with full fidelity (syntax highlighting on)
|
|
2342
2386
|
// so these cached rows byte-match the finalized render.
|
|
2343
2387
|
this.#renderingStablePrefix = true;
|
|
2388
|
+
let frozenRows: string[];
|
|
2344
2389
|
try {
|
|
2345
|
-
|
|
2346
|
-
...this.#renderContentLines(tokens, renderedUntil, stableTokenCount, contentWidth, signature),
|
|
2347
|
-
);
|
|
2390
|
+
frozenRows = this.#renderContentLines(tokens, renderedUntil, stableTokenCount, contentWidth, signature);
|
|
2348
2391
|
} finally {
|
|
2349
2392
|
this.#renderingStablePrefix = false;
|
|
2350
2393
|
}
|
|
2394
|
+
if (prefixLines.length === 0) prefixLines = frozenRows;
|
|
2395
|
+
else appendLines(prefixLines, frozenRows);
|
|
2351
2396
|
renderedUntil = stableTokenCount;
|
|
2352
2397
|
}
|
|
2353
2398
|
|
|
@@ -2355,14 +2400,11 @@ export class Markdown implements Component {
|
|
|
2355
2400
|
...signature,
|
|
2356
2401
|
text: stableText,
|
|
2357
2402
|
tokenCount: stableTokenCount,
|
|
2358
|
-
lines:
|
|
2403
|
+
lines: prefixLines,
|
|
2359
2404
|
};
|
|
2360
2405
|
|
|
2361
|
-
if (renderedUntil
|
|
2362
|
-
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
return contentLines;
|
|
2406
|
+
if (renderedUntil >= tokens.length) return prefixLines.slice();
|
|
2407
|
+
return prefixLines.concat(this.#renderStreamingTail(tokens, renderedUntil, contentWidth, signature));
|
|
2366
2408
|
}
|
|
2367
2409
|
|
|
2368
2410
|
#matchingStreamPrefixLineCache(
|
|
@@ -2403,7 +2445,7 @@ export class Markdown implements Component {
|
|
|
2403
2445
|
if (cache !== undefined) {
|
|
2404
2446
|
spliceEnd = this.#tailSpliceEnd(cache, start, signature, tokens);
|
|
2405
2447
|
for (let i = start; i < spliceEnd; i++) {
|
|
2406
|
-
out
|
|
2448
|
+
appendLines(out, cache.rows[i - start]!);
|
|
2407
2449
|
}
|
|
2408
2450
|
}
|
|
2409
2451
|
|
|
@@ -2416,7 +2458,7 @@ export class Markdown implements Component {
|
|
|
2416
2458
|
nextTypes: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2417
2459
|
};
|
|
2418
2460
|
const fresh = this.#renderContentLines(tokens, spliceEnd, tokens.length, contentWidth, signature, recorder);
|
|
2419
|
-
out.
|
|
2461
|
+
const result = out.length === 0 ? fresh : out.concat(fresh);
|
|
2420
2462
|
|
|
2421
2463
|
// Refresh the cache: keep entries for spliced tokens (their raws stay
|
|
2422
2464
|
// valid), overlay the fresh entries, and re-derive the contiguous
|
|
@@ -2453,7 +2495,7 @@ export class Markdown implements Component {
|
|
|
2453
2495
|
raws,
|
|
2454
2496
|
nextTypes,
|
|
2455
2497
|
};
|
|
2456
|
-
return
|
|
2498
|
+
return result;
|
|
2457
2499
|
}
|
|
2458
2500
|
|
|
2459
2501
|
// Longest cache-spliceable prefix: every cached row from `start` up to
|
|
@@ -2658,7 +2700,7 @@ export class Markdown implements Component {
|
|
|
2658
2700
|
// colors reach completed rows immediately; only the trailing partial
|
|
2659
2701
|
// line stays unhighlighted.
|
|
2660
2702
|
const lineEnd = tokenText.lastIndexOf("\n");
|
|
2661
|
-
const completedLines = lineEnd >= 0 ? this.#highlightStreamingLines(tokenText
|
|
2703
|
+
const completedLines = lineEnd >= 0 ? this.#highlightStreamingLines(tokenText, lineEnd, lang) : null;
|
|
2662
2704
|
if (completedLines) {
|
|
2663
2705
|
for (const hlLine of completedLines) {
|
|
2664
2706
|
addBodyLine(hlLine);
|
|
@@ -2708,21 +2750,28 @@ export class Markdown implements Component {
|
|
|
2708
2750
|
}
|
|
2709
2751
|
|
|
2710
2752
|
/**
|
|
2711
|
-
* Highlight the completed (newline-terminated) prefix
|
|
2712
|
-
* fence. Uses a stateful per-fence
|
|
2713
|
-
* only the newly completed lines,
|
|
2714
|
-
* whole-block `highlightCode` the
|
|
2715
|
-
* when no stream is available for
|
|
2716
|
-
* code-block styling).
|
|
2753
|
+
* Highlight the completed (newline-terminated) prefix `tokenText[0,
|
|
2754
|
+
* completedEnd)` of a streaming code fence. Uses a stateful per-fence
|
|
2755
|
+
* highlight stream so each render pushes only the newly completed lines,
|
|
2756
|
+
* with output byte-identical to the whole-block `highlightCode` the
|
|
2757
|
+
* finalized render performs. Returns null when no stream is available for
|
|
2758
|
+
* `lang` (caller falls back to plain code-block styling).
|
|
2717
2759
|
*/
|
|
2718
|
-
#highlightStreamingLines(
|
|
2760
|
+
#highlightStreamingLines(
|
|
2761
|
+
tokenText: string,
|
|
2762
|
+
completedEnd: number,
|
|
2763
|
+
lang: string | undefined,
|
|
2764
|
+
): readonly string[] | null {
|
|
2719
2765
|
const signature = this.#activeRenderSignature;
|
|
2720
2766
|
const cache = this.#streamingHighlightCache;
|
|
2767
|
+
// Cheap cursor/length and signature gates first; the prefix compare
|
|
2768
|
+
// (the only O(fence) check, needed because a different fence can reach
|
|
2769
|
+
// this cache) runs last.
|
|
2721
2770
|
if (
|
|
2722
2771
|
signature &&
|
|
2723
2772
|
cache &&
|
|
2724
|
-
|
|
2725
|
-
(cache.text.length ===
|
|
2773
|
+
cache.text.length <= completedEnd &&
|
|
2774
|
+
(cache.text.length === completedEnd || tokenText.charCodeAt(cache.text.length) === 0x0a) &&
|
|
2726
2775
|
cache.lang === lang &&
|
|
2727
2776
|
cache.width === signature.width &&
|
|
2728
2777
|
cache.paddingX === signature.paddingX &&
|
|
@@ -2734,18 +2783,23 @@ export class Markdown implements Component {
|
|
|
2734
2783
|
cache.hyperlinks === signature.hyperlinks &&
|
|
2735
2784
|
cache.textSizing === signature.textSizing &&
|
|
2736
2785
|
cache.bgColorProbe === signature.bgColorProbe &&
|
|
2737
|
-
cache.headingProbe === signature.headingProbe
|
|
2786
|
+
cache.headingProbe === signature.headingProbe &&
|
|
2787
|
+
tokenText.startsWith(cache.text)
|
|
2738
2788
|
) {
|
|
2739
|
-
|
|
2789
|
+
const cachedEnd = cache.text.length;
|
|
2790
|
+
if (cachedEnd === completedEnd) return cache.lines;
|
|
2740
2791
|
// Invariant: the stream has consumed `cache.text + "\n"`, so pushing
|
|
2741
2792
|
// the added lines with a trailing newline advances it to
|
|
2742
|
-
// `
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2793
|
+
// `completed + "\n"` — every fed line stays newline-terminated. The
|
|
2794
|
+
// signature gates above matched, so advancing the cache in place is
|
|
2795
|
+
// equivalent to rebuilding it, minus the whole-array copy per frame.
|
|
2796
|
+
const pushed = cache.stream.push(`${tokenText.slice(cachedEnd + 1, completedEnd)}\n`);
|
|
2797
|
+
appendLines(cache.lines, splitPushedHighlightLines(pushed));
|
|
2798
|
+
cache.text = tokenText.slice(0, completedEnd);
|
|
2799
|
+
return cache.lines;
|
|
2747
2800
|
}
|
|
2748
2801
|
|
|
2802
|
+
const completedText = tokenText.slice(0, completedEnd);
|
|
2749
2803
|
const stream = this.#createHighlightStream(lang);
|
|
2750
2804
|
if (!stream) return null;
|
|
2751
2805
|
const lines = splitPushedHighlightLines(stream.push(`${completedText}\n`));
|
package/src/loop-watchdog.ts
CHANGED
|
@@ -64,6 +64,8 @@ export class LoopWatchdog {
|
|
|
64
64
|
#expected = 0;
|
|
65
65
|
#expectedCpu = 0;
|
|
66
66
|
#wasBlocked = false;
|
|
67
|
+
// When the most recent late tick ran — the moment a detected block ended.
|
|
68
|
+
#stallEndedAt = Number.NEGATIVE_INFINITY;
|
|
67
69
|
#running = false;
|
|
68
70
|
// Bumped by stop(); each scheduled tick captures the generation it was armed
|
|
69
71
|
// under and no-ops if it no longer matches, so a start()→stop()→start() cycle
|
|
@@ -94,17 +96,40 @@ export class LoopWatchdog {
|
|
|
94
96
|
if (this.#running) return;
|
|
95
97
|
this.#running = true;
|
|
96
98
|
this.#wasBlocked = false;
|
|
99
|
+
this.#stallEndedAt = Number.NEGATIVE_INFINITY;
|
|
97
100
|
this.#armTick();
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
stop(): void {
|
|
101
104
|
this.#running = false;
|
|
102
105
|
this.#wasBlocked = false;
|
|
106
|
+
this.#stallEndedAt = Number.NEGATIVE_INFINITY;
|
|
103
107
|
this.#generation++;
|
|
104
108
|
this.#handle?.cancel?.();
|
|
105
109
|
this.#handle = undefined;
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Whether the loop is blocked right now — the armed tick is already more
|
|
114
|
+
* than `thresholdMs` overdue — or a detected block ended within the last
|
|
115
|
+
* `thresholdMs`. Input read at such a moment may be keystrokes the block
|
|
116
|
+
* batched into one read. Resolves both orders in which a resumed loop can
|
|
117
|
+
* run the late tick and the queued stdin read. A suspended-process gap is
|
|
118
|
+
* not a stall, and a stopped watchdog never reports one.
|
|
119
|
+
*/
|
|
120
|
+
isStalled(): boolean {
|
|
121
|
+
if (!this.#running) return false;
|
|
122
|
+
const now = this.#now();
|
|
123
|
+
if (now - this.#stallEndedAt <= this.#thresholdMs) return true;
|
|
124
|
+
const overdueMs = now - this.#expected;
|
|
125
|
+
return overdueMs > this.#thresholdMs && !this.#isSuspension(overdueMs, this.#cpuNow() - this.#expectedCpu);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A long gap the process spent negligible CPU on: it was suspended, not blocked. */
|
|
129
|
+
#isSuspension(blockedMs: number, cpuMs: number): boolean {
|
|
130
|
+
return blockedMs > this.#sleepMs && cpuMs < blockedMs * CPU_BUSY_RATIO;
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
#armTick(): void {
|
|
109
134
|
const generation = this.#generation;
|
|
110
135
|
this.#expected = this.#now() + this.#intervalMs;
|
|
@@ -115,23 +140,26 @@ export class LoopWatchdog {
|
|
|
115
140
|
|
|
116
141
|
#tick(generation: number): void {
|
|
117
142
|
if (!this.#running || generation !== this.#generation) return;
|
|
118
|
-
const
|
|
143
|
+
const now = this.#now();
|
|
144
|
+
const blockedMs = now - this.#expected;
|
|
119
145
|
const cpuMs = this.#cpuNow() - this.#expectedCpu;
|
|
120
146
|
// Consume the recent phase every tick (block or not) so attribution is
|
|
121
147
|
// scoped to the just-elapsed interval and never carries a stale phase
|
|
122
148
|
// forward to a later, phase-less block.
|
|
123
149
|
const phase = takeRecentLoopPhase();
|
|
124
150
|
if (blockedMs > this.#thresholdMs) {
|
|
125
|
-
if (
|
|
126
|
-
// A long gap the process did not spend CPU on: it was suspended.
|
|
151
|
+
if (this.#isSuspension(blockedMs, cpuMs)) {
|
|
127
152
|
this.#wasBlocked = false;
|
|
128
|
-
} else
|
|
129
|
-
this.#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
} else {
|
|
154
|
+
this.#stallEndedAt = now;
|
|
155
|
+
if (!this.#wasBlocked) {
|
|
156
|
+
this.#wasBlocked = true;
|
|
157
|
+
logger.warn("ui.loop-blocked", {
|
|
158
|
+
blockedMs: Math.round(blockedMs),
|
|
159
|
+
cpuMs: Math.round(cpuMs),
|
|
160
|
+
phase: phase ?? "unknown",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
135
163
|
}
|
|
136
164
|
} else {
|
|
137
165
|
this.#wasBlocked = false;
|
|
@@ -28,7 +28,8 @@ export interface ResolvedRoleModel {
|
|
|
28
28
|
|
|
29
29
|
/** Catalog refresh capability used by the session picker. */
|
|
30
30
|
export interface ModelPickerRegistry extends ModelBrowserRegistry {
|
|
31
|
-
|
|
31
|
+
/** Bring the catalog up to date without rebuilding a current one; `true` when the picker must re-read it. */
|
|
32
|
+
refreshIfStale(): Promise<boolean>;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface ModelPickerCallbacks {
|
|
@@ -172,13 +173,15 @@ export class ModelPickerComponent implements Component {
|
|
|
172
173
|
this.#browser.selectSelector(options.currentSelector);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// the
|
|
176
|
+
// Re-read only if the catalog moves (startup discovery landing, a
|
|
177
|
+
// models.yml edit): rebuilding a current catalog on every open blocks
|
|
178
|
+
// the first paint for seconds. A --models scope is registry-independent.
|
|
178
179
|
if (this.#scopedModels.length === 0) {
|
|
179
180
|
this.#registry
|
|
180
|
-
.
|
|
181
|
-
.then(
|
|
181
|
+
.refreshIfStale()
|
|
182
|
+
.then(changed => {
|
|
183
|
+
if (changed) this.#syncFromRegistryState();
|
|
184
|
+
})
|
|
182
185
|
.catch(error => {
|
|
183
186
|
this.#configError = error instanceof Error ? error.message : String(error);
|
|
184
187
|
})
|
|
@@ -1139,15 +1139,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1139
1139
|
return entries.map(([provider, limit]) => `${provider}: ${limit}`).join(", ");
|
|
1140
1140
|
}
|
|
1141
1141
|
|
|
1142
|
-
#getMultiSelectOptions(def: SettingDef & { type: "multiselect" }) {
|
|
1143
|
-
if (def.path !== "providers.webSearchOrder") return def.options;
|
|
1144
|
-
const excluded: unknown = this.#context.settings.get("providers.webSearchExclude");
|
|
1145
|
-
if (!Array.isArray(excluded)) return def.options;
|
|
1146
|
-
return def.options.filter(option => !excluded.includes(option.value));
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
1142
|
#createMultiSelect(def: SettingDef & { type: "multiselect" }, done: (value?: string) => void): Container {
|
|
1150
|
-
const options = this.#getMultiSelectOptions(def);
|
|
1151
1143
|
const current: unknown = this.#context.settings.get(def.path);
|
|
1152
1144
|
const initial = Array.isArray(current)
|
|
1153
1145
|
? current.filter((entry): entry is string => typeof entry === "string")
|
|
@@ -1155,7 +1147,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1155
1147
|
return new MultiSelectSubmenu(
|
|
1156
1148
|
def.label,
|
|
1157
1149
|
def.description,
|
|
1158
|
-
options,
|
|
1150
|
+
def.options,
|
|
1159
1151
|
initial,
|
|
1160
1152
|
def.ordered,
|
|
1161
1153
|
value => {
|
|
@@ -1167,7 +1159,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1167
1159
|
}
|
|
1168
1160
|
|
|
1169
1161
|
#formatMultiSelectValue(def: SettingDef & { type: "multiselect" }, value: unknown): string {
|
|
1170
|
-
const options =
|
|
1162
|
+
const { options } = def;
|
|
1171
1163
|
const labels = Array.isArray(value)
|
|
1172
1164
|
? value.flatMap(entry => {
|
|
1173
1165
|
if (typeof entry !== "string") return [];
|
|
@@ -14,7 +14,7 @@ import { wrapTextWithAnsi } from "../../utils";
|
|
|
14
14
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { OAuthSelectorComponent } from "../../overlays/oauth-selector";
|
|
16
16
|
import { theme } from "../../theme/theme";
|
|
17
|
-
import type {
|
|
17
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
18
18
|
|
|
19
19
|
function loginUrlLink(url: string): string {
|
|
20
20
|
return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
|
|
@@ -69,13 +69,15 @@ interface PromptState {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* "Sign in"
|
|
73
|
-
* OAuth.
|
|
74
|
-
*
|
|
72
|
+
* "Sign in" scene: lets the user authenticate one or more model providers via
|
|
73
|
+
* OAuth. It never auto-advances the wizard — the user may sign in to several
|
|
74
|
+
* providers and then continue with Esc.
|
|
75
75
|
*/
|
|
76
|
-
export class
|
|
77
|
-
readonly
|
|
78
|
-
|
|
76
|
+
export class SignInScene implements SetupSceneController {
|
|
77
|
+
readonly title = "Sign in to your providers";
|
|
78
|
+
get subtitle(): string {
|
|
79
|
+
return `Sign in to one or more providers. Press ${editorKey("tui.select.cancel")} when you're done.`;
|
|
80
|
+
}
|
|
79
81
|
|
|
80
82
|
#authStorage: AuthStorage;
|
|
81
83
|
#selector: OAuthSelectorComponent;
|
|
@@ -99,11 +101,6 @@ export class SignInTab implements SetupTab {
|
|
|
99
101
|
this.#selector = this.#createSelector();
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
/** Modal while an OAuth flow is running so the scene won't switch tabs or finish. */
|
|
103
|
-
get modal(): boolean {
|
|
104
|
-
return this.#loggingInProvider !== undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
104
|
dispose(): void {
|
|
108
105
|
this.#disposed = true;
|
|
109
106
|
this.#selector.stopValidation();
|
|
@@ -373,3 +370,11 @@ export class SignInTab implements SetupTab {
|
|
|
373
370
|
this.#host.requestRender();
|
|
374
371
|
}
|
|
375
372
|
}
|
|
373
|
+
|
|
374
|
+
/** Onboarding scene for provider sign-in. */
|
|
375
|
+
export const providersSetupScene: SetupScene = {
|
|
376
|
+
id: "providers",
|
|
377
|
+
title: "Sign in to your providers",
|
|
378
|
+
minVersion: 1,
|
|
379
|
+
mount: host => new SignInScene(host),
|
|
380
|
+
};
|
|
@@ -5,7 +5,6 @@ import type { ComposerPreviewStatusSource } from "../../overlays/composer-shape-
|
|
|
5
5
|
import type { ComposerShape } from "../../overlays/composer-shape-registry";
|
|
6
6
|
import type { ModelBrowserSource } from "../../overlays/model-browser";
|
|
7
7
|
import type { SymbolPreset } from "../../theme/theme";
|
|
8
|
-
import type { SearchProviderId } from "../../tools/web-search";
|
|
9
8
|
import type { Component, TUI } from "../../tui";
|
|
10
9
|
|
|
11
10
|
/** Terminal capabilities used by setup overlays and the startup splash. */
|
|
@@ -21,7 +20,6 @@ export interface SetupHost extends SetupUiHost {
|
|
|
21
20
|
readonly composerShape: ComposerShape;
|
|
22
21
|
readonly symbolPreset: SymbolPreset;
|
|
23
22
|
readonly colorBlindMode: boolean;
|
|
24
|
-
readonly webSearchOrder: readonly string[];
|
|
25
23
|
readonly disabledProviders: readonly string[];
|
|
26
24
|
readonly authStorage: AuthStorage;
|
|
27
25
|
readonly modelSource: ModelBrowserSource;
|
|
@@ -33,8 +31,6 @@ export interface SetupHost extends SetupUiHost {
|
|
|
33
31
|
saveSymbolPreset(preset: SymbolPreset): void;
|
|
34
32
|
saveColorBlindMode(enabled: boolean): void;
|
|
35
33
|
saveTheme(mode: "dark" | "light", name: string): void;
|
|
36
|
-
isSearchProviderAvailable(id: SearchProviderId): Promise<boolean>;
|
|
37
|
-
saveSearchProvider(id: SearchProviderId | "auto"): void;
|
|
38
34
|
captureBrowserSession(request: OAuthBrowserSessionRequest, signal?: AbortSignal): Promise<string>;
|
|
39
35
|
copyToClipboard(text: string): Promise<void>;
|
|
40
36
|
openInBrowser(url: string): void;
|
|
@@ -78,29 +74,6 @@ export interface SetupSceneController extends Component {
|
|
|
78
74
|
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
/**
|
|
82
|
-
* A single panel inside a tabbed setup scene. The host scene owns the tab bar
|
|
83
|
-
* and forwards rendering/input to the active tab.
|
|
84
|
-
*/
|
|
85
|
-
export interface SetupTab {
|
|
86
|
-
readonly id: string;
|
|
87
|
-
readonly label: string;
|
|
88
|
-
/**
|
|
89
|
-
* While `true` the tab owns all keyboard input (e.g. an in-progress OAuth
|
|
90
|
-
* login). The parent scene MUST NOT switch tabs or finish while modal.
|
|
91
|
-
*/
|
|
92
|
-
readonly modal: boolean;
|
|
93
|
-
/** See {@link SetupSceneController.render}: `maxLines` is the tab-local row budget. */
|
|
94
|
-
render(width: number, maxLines?: number): readonly string[];
|
|
95
|
-
handleInput(data: string): void;
|
|
96
|
-
invalidate(): void;
|
|
97
|
-
/** Called when the tab becomes active (including initial mount). */
|
|
98
|
-
onActivate?(): void;
|
|
99
|
-
/** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
|
|
100
|
-
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
101
|
-
dispose(): void;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
77
|
/** Versioned onboarding scene definition. */
|
|
105
78
|
export interface SetupScene {
|
|
106
79
|
id: string;
|
package/src/setup/wizard.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { SetupHost } from "./scenes/types";
|
|
|
3
3
|
import { composerSetupScene } from "./scenes/composer";
|
|
4
4
|
import { glyphSetupScene } from "./scenes/glyph";
|
|
5
5
|
import { modelSetupScene } from "./scenes/model";
|
|
6
|
-
import { providersSetupScene } from "./scenes/
|
|
6
|
+
import { providersSetupScene } from "./scenes/sign-in";
|
|
7
7
|
import { themeSetupScene } from "./scenes/theme";
|
|
8
8
|
import type { SetupScene } from "./scenes/types";
|
|
9
9
|
import { SetupWizardComponent } from "./wizard-overlay";
|