@oh-my-pi/pi-tui 18.0.6 → 18.0.7
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 +13 -0
- package/dist/types/components/markdown.d.ts +0 -2
- package/dist/types/latex-to-unicode.d.ts +0 -9
- package/dist/types/terminal-capabilities.d.ts +17 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +50 -8
- package/src/components/markdown.ts +16 -60
- package/src/latex-to-unicode.ts +15 -58
- package/src/terminal-capabilities.ts +54 -9
- package/src/tui.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.7] - 2026-08-26
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed the `inlineMathSpanEnd` and `mathStartIndex` exports; the math delimiter grammar now lives in `@oh-my-pi/pi-utils/math-delimiters`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Math spans now end at the first unescaped delimiter, so a TeX row break no longer closes a span early: `\(a \\) b\)` renders as one equation, and an escaped `\$` no longer ends `$$…$$`.
|
|
14
|
+
- Fixed image previews displaying as garbled characters in Paseo terminals.
|
|
15
|
+
- Fixed terminal resizing from duplicating committed history in native scrollback.
|
|
16
|
+
- Fixed autocomplete suggestions for bare-name skills such as `/batch` when no command matches the prefix more strongly.
|
|
17
|
+
|
|
5
18
|
## [18.0.6] - 2026-08-26
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { SymbolTheme } from "../symbols.js";
|
|
2
2
|
import type { Component } from "../tui.js";
|
|
3
3
|
/** @internal exported for tests — must stay index-identical to the old regex scan. */
|
|
4
|
-
export declare function mathStartIndex(src: string): number | undefined;
|
|
5
|
-
/** @internal exported for tests — must stay index-identical to the old regex scan. */
|
|
6
4
|
export declare function autolinkSchemeScanIndex(src: string): number | undefined;
|
|
7
5
|
/** @internal exported for tests — must never return false for a src the built-in url regex matches. */
|
|
8
6
|
export declare function urlTokenPossible(src: string): boolean;
|
|
@@ -39,12 +39,3 @@ export declare function isBareMathEnvironment(env: string): boolean;
|
|
|
39
39
|
* $10" is left untouched.
|
|
40
40
|
*/
|
|
41
41
|
export declare function renderMathInText(text: string): string;
|
|
42
|
-
/**
|
|
43
|
-
* Index of the `$` that closes an inline math span opened at `open` (the index
|
|
44
|
-
* of the opening `$`), or -1 when the run is not inline math. Applies pandoc's
|
|
45
|
-
* anti-currency heuristics: the opener must not be followed by whitespace, the
|
|
46
|
-
* closer must not be preceded by whitespace nor followed by a digit, `\$` is a
|
|
47
|
-
* literal dollar, and the span may not span a newline. Shared by
|
|
48
|
-
* `renderMathInText` and the markdown math tokenizer so the rule has one home.
|
|
49
|
-
*/
|
|
50
|
-
export declare function inlineMathSpanEnd(text: string, open: number): number;
|
|
@@ -153,6 +153,23 @@ export declare function shouldEnableHyperlinksByDefault(env?: NodeJS.ProcessEnv,
|
|
|
153
153
|
* without mutating `process.platform`.
|
|
154
154
|
*/
|
|
155
155
|
export declare function resolveWarpImageProtocol(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): ImageProtocol | null;
|
|
156
|
+
/**
|
|
157
|
+
* Paseo (getpaseo/paseo) hardcodes `TERM_PROGRAM=kitty` into every PTY
|
|
158
|
+
* (`buildTerminalEnvironment`) while its xterm.js renderer implements neither
|
|
159
|
+
* Kitty graphics nor Unicode placeholders — trusting the advertisement turns
|
|
160
|
+
* image previews into literal PUA garbage (getpaseo/paseo#3850). Paseo
|
|
161
|
+
* injects `PASEO_TERMINAL_ID` into every terminal it hosts, which makes a
|
|
162
|
+
* reliable embedder signal.
|
|
163
|
+
*/
|
|
164
|
+
export declare function isPaseoEmbedder(env?: NodeJS.ProcessEnv): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the image protocol for a non-forced runtime: static per-terminal
|
|
167
|
+
* support (with Warp's platform carve-out), then the multiplexer fallback,
|
|
168
|
+
* then the Paseo embedder carve-out. `isTTY` is injectable because the
|
|
169
|
+
* fallback only fires on a real TTY — a piped subprocess cannot exercise
|
|
170
|
+
* that path, so regression tests call this directly.
|
|
171
|
+
*/
|
|
172
|
+
export declare function resolveImageProtocol(terminalId: TerminalId, env?: NodeJS.ProcessEnv, isTTY?: boolean): ImageProtocol | null;
|
|
156
173
|
/** Resolve terminal identity from environment markers used by common emulators. */
|
|
157
174
|
export declare function detectTerminalId(env?: NodeJS.ProcessEnv): TerminalId;
|
|
158
175
|
export declare const TERMINAL_ID: TerminalId;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.7",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.7",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.7"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/autocomplete.ts
CHANGED
|
@@ -331,10 +331,19 @@ function buildSlashCommandCompletions(
|
|
|
331
331
|
let best: (AutocompleteItem & { score: number; usage: number }) | undefined;
|
|
332
332
|
|
|
333
333
|
const isSkillCommand = name.startsWith(SKILL_NAMESPACE);
|
|
334
|
+
// Skills are matched by their bare name as well as the full
|
|
335
|
+
// `skill:` name so a broken-out or mid-prompt skill ranks at
|
|
336
|
+
// prefix strength (`/batch` → `skill:batch`) instead of a weak
|
|
337
|
+
// full-name fuzzy hit.
|
|
334
338
|
const nameScore =
|
|
335
339
|
lowerPrefix.length === 0 && isSkillCommand
|
|
336
340
|
? 950
|
|
337
|
-
:
|
|
341
|
+
: isSkillCommand
|
|
342
|
+
? Math.max(
|
|
343
|
+
scoreCommandTextMatch(lowerPrefix, name.toLowerCase()),
|
|
344
|
+
scoreCommandTextMatch(lowerPrefix, name.slice(SKILL_NAMESPACE.length).toLowerCase()),
|
|
345
|
+
)
|
|
346
|
+
: scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
|
|
338
347
|
const lowerDesc = staticDesc.toLowerCase();
|
|
339
348
|
const descScore =
|
|
340
349
|
lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
@@ -391,23 +400,56 @@ function hasPromptTextBeforeSlash(
|
|
|
391
400
|
|
|
392
401
|
export const SKILL_NAMESPACE = "skill:";
|
|
393
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Match tier used to compare a skill's bare name against non-skill command
|
|
405
|
+
* names when deciding whether the skill may break out of the collapsed
|
|
406
|
+
* `skill:` group: exact (1000) > prefix (900) > anything weaker (0). Fuzzy
|
|
407
|
+
* hits deliberately map to 0 — a fuzzy skill match is never strong enough to
|
|
408
|
+
* mix skills into the command popup.
|
|
409
|
+
*/
|
|
410
|
+
function skillBreakoutTier(lowerPrefix: string, lowerTarget: string): number {
|
|
411
|
+
if (lowerPrefix === lowerTarget) return 1000;
|
|
412
|
+
if (lowerTarget.startsWith(lowerPrefix)) return 900;
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
|
|
394
416
|
/**
|
|
395
417
|
* Collapse `skill:*` commands into a single `/skill:` namespace row while the
|
|
396
|
-
* typed prefix has not committed to the namespace.
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
418
|
+
* typed prefix has not committed to the namespace. A lone group entry (shown
|
|
419
|
+
* only while the prefix is still a prefix of `skill:`) keeps the `/` popup
|
|
420
|
+
* readable. A skill breaks out of the group only when its bare name matches
|
|
421
|
+
* the prefix at a strictly stronger tier than every non-skill command name
|
|
422
|
+
* and alias (`/batch` → `skill:batch` while no command prefix-matches
|
|
423
|
+
* `batch`); a tie keeps the popup command-only, and fuzzy-only skill hits
|
|
424
|
+
* never surface. Accepting the group inserts `/skill:` without a trailing
|
|
425
|
+
* space so the reopened popup expands to the individual skills.
|
|
401
426
|
*/
|
|
402
427
|
function collapseSkillNamespace(commands: CommandEntry[], lowerPrefix: string): CommandEntry[] {
|
|
403
428
|
if (lowerPrefix.startsWith(SKILL_NAMESPACE)) return commands;
|
|
429
|
+
const approachesNamespace = SKILL_NAMESPACE.startsWith(lowerPrefix);
|
|
430
|
+
let commandTier = 0;
|
|
431
|
+
if (!approachesNamespace) {
|
|
432
|
+
for (const cmd of commands) {
|
|
433
|
+
const name = getCommandName(cmd);
|
|
434
|
+
if (!name || name.startsWith(SKILL_NAMESPACE)) continue;
|
|
435
|
+
commandTier = Math.max(commandTier, skillBreakoutTier(lowerPrefix, name.toLowerCase()));
|
|
436
|
+
for (const alias of getCommandAliases(cmd)) {
|
|
437
|
+
commandTier = Math.max(commandTier, skillBreakoutTier(lowerPrefix, alias.toLowerCase()));
|
|
438
|
+
}
|
|
439
|
+
if (commandTier === 1000) break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
404
442
|
let skillCount = 0;
|
|
405
443
|
let skillIcon: string | undefined;
|
|
406
444
|
const rest = commands.filter(cmd => {
|
|
407
|
-
|
|
445
|
+
const name = getCommandName(cmd);
|
|
446
|
+
if (!name?.startsWith(SKILL_NAMESPACE)) return true;
|
|
408
447
|
skillCount += 1;
|
|
409
448
|
skillIcon ??= cmd.icon;
|
|
410
|
-
return
|
|
449
|
+
return (
|
|
450
|
+
!approachesNamespace &&
|
|
451
|
+
skillBreakoutTier(lowerPrefix, name.slice(SKILL_NAMESPACE.length).toLowerCase()) > commandTier
|
|
452
|
+
);
|
|
411
453
|
});
|
|
412
454
|
if (skillCount === 0) return commands;
|
|
413
455
|
if (!SKILL_NAMESPACE.startsWith(lowerPrefix)) return rest;
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
type TokenizerAndRendererExtension,
|
|
8
8
|
type Tokens,
|
|
9
9
|
} from "@oh-my-pi/pi-utils/marked";
|
|
10
|
+
import { mathBlockAt, mathSpanAt, mathStartIndex } from "@oh-my-pi/pi-utils/math-delimiters";
|
|
10
11
|
import { latexToBlock } from "../latex-block";
|
|
11
|
-
import {
|
|
12
|
+
import { isBareMathEnvironment, latexToUnicode } from "../latex-to-unicode";
|
|
12
13
|
import type { SymbolTheme } from "../symbols";
|
|
13
14
|
import { TERMINAL } from "../terminal-capabilities";
|
|
14
15
|
import type { Component } from "../tui";
|
|
@@ -528,7 +529,7 @@ markdownParser.setOptions({
|
|
|
528
529
|
// `math` inline token before markdown's escape/emphasis/link rules run, so
|
|
529
530
|
// backslash commands (`\frac`, `\alpha`) and intraword underscores (`x_i`)
|
|
530
531
|
// survive intact instead of being mangled or split. The `$…$` form uses
|
|
531
|
-
// pandoc's anti-currency heuristic (`
|
|
532
|
+
// pandoc's anti-currency heuristic (`mathSpanAt`) so "$5 and $10" is
|
|
532
533
|
// never math. Inline extensions run before marked's escape tokenizer, so
|
|
533
534
|
// `\(…\)` becomes math while a genuinely escaped `\$` is left to `escape` and
|
|
534
535
|
// renders as a literal dollar.
|
|
@@ -582,78 +583,33 @@ const customHrExtension: TokenizerAndRendererExtension = {
|
|
|
582
583
|
},
|
|
583
584
|
};
|
|
584
585
|
|
|
585
|
-
//
|
|
586
|
-
// marked calls start() on the remaining source at every inline position, so
|
|
587
|
-
// the regex alternation showed up in CPU profiles (part of a ~4.3% start()
|
|
588
|
-
// tail). Three indexOf scans yield the identical leftmost index.
|
|
589
|
-
/** @internal exported for tests — must stay index-identical to the old regex scan. */
|
|
590
|
-
export function mathStartIndex(src: string): number | undefined {
|
|
591
|
-
let best = src.indexOf("$");
|
|
592
|
-
const paren = src.indexOf("\\(");
|
|
593
|
-
if (paren !== -1 && (best === -1 || paren < best)) best = paren;
|
|
594
|
-
const bracket = src.indexOf("\\[");
|
|
595
|
-
if (bracket !== -1 && (best === -1 || bracket < best)) best = bracket;
|
|
596
|
-
return best === -1 ? undefined : best;
|
|
597
|
-
}
|
|
598
|
-
|
|
586
|
+
// Delimiters come from `@oh-my-pi/pi-utils/math-delimiters`; rendering policy stays here.
|
|
599
587
|
const mathExtension: TokenizerAndRendererExtension = {
|
|
600
588
|
name: "math",
|
|
601
589
|
level: "inline",
|
|
602
|
-
start
|
|
603
|
-
return mathStartIndex(src);
|
|
604
|
-
},
|
|
590
|
+
start: mathStartIndex,
|
|
605
591
|
tokenizer(src) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: true };
|
|
610
|
-
}
|
|
611
|
-
return undefined;
|
|
612
|
-
}
|
|
613
|
-
if (src.startsWith("\\[")) {
|
|
614
|
-
const end = src.indexOf("\\]", 2);
|
|
615
|
-
if (end !== -1) return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: true };
|
|
616
|
-
return undefined;
|
|
617
|
-
}
|
|
618
|
-
if (src.startsWith("\\(")) {
|
|
619
|
-
const end = src.indexOf("\\)", 2);
|
|
620
|
-
if (end !== -1) return { type: "math", raw: src.slice(0, end + 2), text: src.slice(2, end), display: false };
|
|
621
|
-
return undefined;
|
|
622
|
-
}
|
|
623
|
-
if (src.charCodeAt(0) === 0x24 /* $ */) {
|
|
624
|
-
const end = inlineMathSpanEnd(src, 0);
|
|
625
|
-
if (end !== -1) return { type: "math", raw: src.slice(0, end + 1), text: src.slice(1, end), display: false };
|
|
626
|
-
}
|
|
627
|
-
return undefined;
|
|
592
|
+
const span = mathSpanAt(src, 0);
|
|
593
|
+
if (!span) return undefined;
|
|
594
|
+
return { type: "math", raw: src.slice(0, span.end), text: span.body, display: span.display };
|
|
628
595
|
},
|
|
629
596
|
renderer(token) {
|
|
630
|
-
return
|
|
597
|
+
return typeof token.text === "string" ? token.text : "";
|
|
631
598
|
},
|
|
632
599
|
};
|
|
633
600
|
|
|
634
|
-
// Display math blocks: opening `$$` / `\[` and closing `$$` / `\]` each alone on
|
|
635
|
-
// their own line (≤3 leading spaces). Matched at the block level — before
|
|
636
|
-
// paragraph/list parsing — so a multi-line equation (e.g. a matrix with `\\`
|
|
637
|
-
// row breaks) renders across several lines instead of being collapsed onto one,
|
|
638
|
-
// and blank lines inside the block don't split it. The own-line requirement
|
|
639
|
-
// keeps inline `$$…$$` inside prose for the inline tokenizer above.
|
|
640
|
-
const MATH_BLOCK_DOLLAR = /^ {0,3}\$\$[ \t]*\n([\s\S]+?)\n {0,3}\$\$[ \t]*(?:\n|$)/;
|
|
641
|
-
const MATH_BLOCK_BRACKET = /^ {0,3}\\\[[ \t]*\n([\s\S]+?)\n {0,3}\\\][ \t]*(?:\n|$)/;
|
|
642
|
-
const MATH_BLOCK_START = /(?:^|\n) {0,3}(?:\$\$|\\\[)[ \t]*\n/;
|
|
643
601
|
const mathBlockExtension: TokenizerAndRendererExtension = {
|
|
644
602
|
name: "mathBlock",
|
|
645
603
|
level: "block",
|
|
646
|
-
start
|
|
647
|
-
|
|
648
|
-
return m ? m.index : undefined;
|
|
649
|
-
},
|
|
604
|
+
// No `start` hint: marked only probes block extensions at a block boundary
|
|
605
|
+
// here and never consults their hints.
|
|
650
606
|
tokenizer(src) {
|
|
651
|
-
const
|
|
652
|
-
if (!
|
|
653
|
-
return { type: "math", raw:
|
|
607
|
+
const block = mathBlockAt(src);
|
|
608
|
+
if (!block) return undefined;
|
|
609
|
+
return { type: "math", raw: block.raw, text: block.body, display: true };
|
|
654
610
|
},
|
|
655
611
|
renderer(token) {
|
|
656
|
-
return
|
|
612
|
+
return typeof token.text === "string" ? token.text : "";
|
|
657
613
|
},
|
|
658
614
|
};
|
|
659
615
|
|
|
@@ -718,7 +674,7 @@ const mathEnvBlockExtension: TokenizerAndRendererExtension = {
|
|
|
718
674
|
// (return undefined) to marked's own autolink handling unchanged.
|
|
719
675
|
const AUTOLINK_SCHEME_REGEX = /^(?:www\.|https?:\/\/|ftp:\/\/)/i;
|
|
720
676
|
// Case-insensitive scheme scan replacing /www\.|https?:\/\/|ftp:\/\//i in
|
|
721
|
-
// boundedAutolinkExtension.start — like mathStartIndex
|
|
677
|
+
// boundedAutolinkExtension.start — like `mathStartIndex`, this runs on the
|
|
722
678
|
// remaining source at every inline position (part of a ~4.3% CPU start() scan
|
|
723
679
|
// tail in profiles). charCode-only: no allocation, no toLowerCase copies.
|
|
724
680
|
// `| 32` lower-cases ASCII letters; `.`/`:`/`/` are compared exactly, matching
|
package/src/latex-to-unicode.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mathSpanAt } from "@oh-my-pi/pi-utils/math-delimiters";
|
|
1
2
|
import { TERMINAL } from "./terminal-capabilities";
|
|
2
3
|
|
|
3
4
|
// LaTeX → Unicode/ANSI converter.
|
|
@@ -1934,47 +1935,34 @@ export function renderMathInText(text: string): string {
|
|
|
1934
1935
|
i += 2;
|
|
1935
1936
|
continue;
|
|
1936
1937
|
}
|
|
1937
|
-
if (d === "
|
|
1938
|
-
const close = text.indexOf("\\)", i + 2);
|
|
1939
|
-
if (close !== -1) {
|
|
1940
|
-
out += conv(text.slice(i + 2, close));
|
|
1941
|
-
i = close + 2;
|
|
1942
|
-
continue;
|
|
1943
|
-
}
|
|
1944
|
-
} else if (d === "[") {
|
|
1945
|
-
const close = text.indexOf("\\]", i + 2);
|
|
1946
|
-
if (close !== -1) {
|
|
1947
|
-
out += conv(text.slice(i + 2, close));
|
|
1948
|
-
i = close + 2;
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
} else if (d === "$") {
|
|
1938
|
+
if (d === "$") {
|
|
1952
1939
|
out += "$";
|
|
1953
1940
|
i += 2;
|
|
1954
1941
|
continue;
|
|
1955
1942
|
}
|
|
1943
|
+
// `i` as the escape bound: the walk above already consumed `\\` and `\$`.
|
|
1944
|
+
const span = mathSpanAt(text, i, i);
|
|
1945
|
+
if (span) {
|
|
1946
|
+
out += conv(span.body);
|
|
1947
|
+
i = span.end;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1956
1950
|
out += c;
|
|
1957
1951
|
i++;
|
|
1958
1952
|
continue;
|
|
1959
1953
|
}
|
|
1960
1954
|
if (c === "$") {
|
|
1955
|
+
const span = mathSpanAt(text, i, i);
|
|
1956
|
+
if (span) {
|
|
1957
|
+
out += conv(span.body);
|
|
1958
|
+
i = span.end;
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
1961
|
if (text[i + 1] === "$") {
|
|
1962
|
-
const close = text.indexOf("$$", i + 2);
|
|
1963
|
-
if (close !== -1 && text.slice(i + 2, close).trim().length > 0) {
|
|
1964
|
-
out += conv(text.slice(i + 2, close));
|
|
1965
|
-
i = close + 2;
|
|
1966
|
-
continue;
|
|
1967
|
-
}
|
|
1968
1962
|
out += "$$";
|
|
1969
1963
|
i += 2;
|
|
1970
1964
|
continue;
|
|
1971
1965
|
}
|
|
1972
|
-
const close = inlineMathSpanEnd(text, i);
|
|
1973
|
-
if (close !== -1) {
|
|
1974
|
-
out += conv(text.slice(i + 1, close));
|
|
1975
|
-
i = close + 1;
|
|
1976
|
-
continue;
|
|
1977
|
-
}
|
|
1978
1966
|
out += "$";
|
|
1979
1967
|
i++;
|
|
1980
1968
|
continue;
|
|
@@ -1984,34 +1972,3 @@ export function renderMathInText(text: string): string {
|
|
|
1984
1972
|
}
|
|
1985
1973
|
return renderBareMathInText(out);
|
|
1986
1974
|
}
|
|
1987
|
-
|
|
1988
|
-
/**
|
|
1989
|
-
* Index of the `$` that closes an inline math span opened at `open` (the index
|
|
1990
|
-
* of the opening `$`), or -1 when the run is not inline math. Applies pandoc's
|
|
1991
|
-
* anti-currency heuristics: the opener must not be followed by whitespace, the
|
|
1992
|
-
* closer must not be preceded by whitespace nor followed by a digit, `\$` is a
|
|
1993
|
-
* literal dollar, and the span may not span a newline. Shared by
|
|
1994
|
-
* `renderMathInText` and the markdown math tokenizer so the rule has one home.
|
|
1995
|
-
*/
|
|
1996
|
-
export function inlineMathSpanEnd(text: string, open: number): number {
|
|
1997
|
-
const after = text[open + 1];
|
|
1998
|
-
if (after === undefined || after === " " || after === "\t" || after === "\n" || after === "$") {
|
|
1999
|
-
return -1;
|
|
2000
|
-
}
|
|
2001
|
-
for (let j = open + 1; j < text.length; j++) {
|
|
2002
|
-
const ch = text[j];
|
|
2003
|
-
if (ch === "\\") {
|
|
2004
|
-
j++;
|
|
2005
|
-
continue;
|
|
2006
|
-
}
|
|
2007
|
-
if (ch === "\n") return -1;
|
|
2008
|
-
if (ch === "$") {
|
|
2009
|
-
const prev = text[j - 1];
|
|
2010
|
-
if (prev === " " || prev === "\t") return -1;
|
|
2011
|
-
const next = text[j + 1];
|
|
2012
|
-
if (next !== undefined && next >= "0" && next <= "9") continue; // currency: keep scanning
|
|
2013
|
-
return text.slice(open + 1, j).trim().length > 0 ? j : -1;
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
return -1;
|
|
2017
|
-
}
|
|
@@ -446,10 +446,14 @@ export function shouldEnableHyperlinksByDefault(
|
|
|
446
446
|
return true;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
function getFallbackImageProtocol(
|
|
450
|
-
|
|
449
|
+
function getFallbackImageProtocol(
|
|
450
|
+
terminalId: TerminalId,
|
|
451
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
452
|
+
isTTY: boolean = process.stdout.isTTY === true,
|
|
453
|
+
): ImageProtocol | null {
|
|
454
|
+
if (!isTTY) return null;
|
|
451
455
|
if (terminalId === "vscode" || terminalId === "alacritty") return null;
|
|
452
|
-
const term =
|
|
456
|
+
const term = env.TERM?.toLowerCase() ?? "";
|
|
453
457
|
if (term.includes("screen") || term.includes("tmux") || term.includes("ghostty")) {
|
|
454
458
|
return ImageProtocol.Kitty;
|
|
455
459
|
}
|
|
@@ -470,6 +474,51 @@ export function resolveWarpImageProtocol(
|
|
|
470
474
|
return windowsHost ? null : ImageProtocol.Kitty;
|
|
471
475
|
}
|
|
472
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Paseo (getpaseo/paseo) hardcodes `TERM_PROGRAM=kitty` into every PTY
|
|
479
|
+
* (`buildTerminalEnvironment`) while its xterm.js renderer implements neither
|
|
480
|
+
* Kitty graphics nor Unicode placeholders — trusting the advertisement turns
|
|
481
|
+
* image previews into literal PUA garbage (getpaseo/paseo#3850). Paseo
|
|
482
|
+
* injects `PASEO_TERMINAL_ID` into every terminal it hosts, which makes a
|
|
483
|
+
* reliable embedder signal.
|
|
484
|
+
*/
|
|
485
|
+
export function isPaseoEmbedder(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
486
|
+
return Boolean(env.PASEO_TERMINAL_ID);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resolve the image protocol for a non-forced runtime: static per-terminal
|
|
491
|
+
* support (with Warp's platform carve-out), then the multiplexer fallback,
|
|
492
|
+
* then the Paseo embedder carve-out. `isTTY` is injectable because the
|
|
493
|
+
* fallback only fires on a real TTY — a piped subprocess cannot exercise
|
|
494
|
+
* that path, so regression tests call this directly.
|
|
495
|
+
*/
|
|
496
|
+
export function resolveImageProtocol(
|
|
497
|
+
terminalId: TerminalId,
|
|
498
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
499
|
+
isTTY: boolean = process.stdout.isTTY === true,
|
|
500
|
+
): ImageProtocol | null {
|
|
501
|
+
let imageProtocol: ImageProtocol | null;
|
|
502
|
+
if (terminalId === "warp") {
|
|
503
|
+
// Warp advertises Kitty graphics on macOS/Linux only; drop it on win32.
|
|
504
|
+
imageProtocol = resolveWarpImageProtocol(process.platform, env);
|
|
505
|
+
} else {
|
|
506
|
+
imageProtocol = getTerminalInfo(terminalId).imageProtocol;
|
|
507
|
+
if (!imageProtocol) {
|
|
508
|
+
const fallbackImageProtocol = getFallbackImageProtocol(terminalId, env, isTTY);
|
|
509
|
+
if (fallbackImageProtocol) imageProtocol = fallbackImageProtocol;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Paseo's xterm.js renderer draws neither Kitty APC nor placeholders —
|
|
513
|
+
// applied after the multiplexer fallback so tmux/screen inside a Paseo
|
|
514
|
+
// pane cannot restore Kitty via getFallbackImageProtocol
|
|
515
|
+
// (getpaseo/paseo#3850).
|
|
516
|
+
if (imageProtocol !== null && isPaseoEmbedder(env)) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
return imageProtocol;
|
|
520
|
+
}
|
|
521
|
+
|
|
473
522
|
function getWarpTerminalInfo(platform: NodeJS.Platform, env: NodeJS.ProcessEnv = Bun.env): TerminalInfo {
|
|
474
523
|
return new TerminalInfo(
|
|
475
524
|
"warp",
|
|
@@ -572,12 +621,8 @@ export const TERMINAL: RuntimeTerminal = (() => {
|
|
|
572
621
|
const forcedImageProtocol = getForcedImageProtocol();
|
|
573
622
|
if (forcedImageProtocol !== undefined) {
|
|
574
623
|
resolved.imageProtocol = forcedImageProtocol;
|
|
575
|
-
} else
|
|
576
|
-
|
|
577
|
-
resolved.imageProtocol = resolveWarpImageProtocol();
|
|
578
|
-
} else if (!resolved.imageProtocol) {
|
|
579
|
-
const fallbackImageProtocol = getFallbackImageProtocol(resolved.id);
|
|
580
|
-
if (fallbackImageProtocol) resolved.imageProtocol = fallbackImageProtocol;
|
|
624
|
+
} else {
|
|
625
|
+
resolved.imageProtocol = resolveImageProtocol(resolved.id, Bun.env, process.stdout.isTTY === true);
|
|
581
626
|
}
|
|
582
627
|
// Hyperlink (OSC 8) capability. The static per-terminal flag lives on
|
|
583
628
|
// KNOWN_TERMINALS; shouldEnableHyperlinksByDefault folds in runtime context —
|
package/src/tui.ts
CHANGED
|
@@ -2218,6 +2218,14 @@ export class TUI extends Container {
|
|
|
2218
2218
|
* Append mode leaves the terminal's prior copy in place and writes a
|
|
2219
2219
|
* current-width copy below it. Rebuild mode routes through the destructive
|
|
2220
2220
|
* reset latch so ED3 removes stale history before the same replay.
|
|
2221
|
+
*
|
|
2222
|
+
* The append replay only exists to refresh width-shredded scrollback: a
|
|
2223
|
+
* width change leaves the terminal's committed history wrapped at the old
|
|
2224
|
+
* width, so the copy below it is the intended fresh current-width render. A
|
|
2225
|
+
* height-only change reflows nothing (the terminal merely pulls rows back
|
|
2226
|
+
* out of scrollback), so replaying would write an identical duplicate — the
|
|
2227
|
+
* editor/status chrome included — below the retained copy. Skip it, matching
|
|
2228
|
+
* the `widthChanged`-gated commit-ledger logic in {@link #doRender}.
|
|
2221
2229
|
*/
|
|
2222
2230
|
#prepareResizeReplay(width: number, height: number): void {
|
|
2223
2231
|
const size = `${width}x${height}`;
|
|
@@ -2240,6 +2248,7 @@ export class TUI extends Container {
|
|
|
2240
2248
|
this.#prepareForcedRender(true);
|
|
2241
2249
|
return;
|
|
2242
2250
|
}
|
|
2251
|
+
if (width === this.#previousWidth) return;
|
|
2243
2252
|
provider.beginHistoryReplay();
|
|
2244
2253
|
this.#forceViewportRepaintOnNextRender = true;
|
|
2245
2254
|
}
|
|
@@ -2471,10 +2480,16 @@ export class TUI extends Container {
|
|
|
2471
2480
|
this.#altActive = false;
|
|
2472
2481
|
this.#altMouseTrackingActive = false;
|
|
2473
2482
|
this.#altPreviousLines = [];
|
|
2474
|
-
// The alt
|
|
2475
|
-
//
|
|
2476
|
-
//
|
|
2483
|
+
// The alt-buffer restore put the pre-overlay normal screen back. If
|
|
2484
|
+
// that buffer resized while covered, its cursor moved with width
|
|
2485
|
+
// rewrap or a height-grow scrollback pull, while our viewport anchor
|
|
2486
|
+
// stayed frozen. Recover the restored cursor position before any
|
|
2487
|
+
// provider repaint can overwrite history at the stale row.
|
|
2477
2488
|
if (width !== this.#altEnterWidth || height !== this.#altEnterHeight) {
|
|
2489
|
+
if (this.#frameProvider !== undefined) {
|
|
2490
|
+
this.#beginResizeAnchorProbe();
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2478
2493
|
this.#forceViewportRepaintOnNextRender = true;
|
|
2479
2494
|
}
|
|
2480
2495
|
} else if (wantMouseTracking !== this.#altMouseTrackingActive) {
|