@linxiraos/pi-tui 1.1.7 → 1.1.8
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 +3 -7
- package/dist/types/components/editor.d.ts +9 -3
- package/dist/types/components/image.d.ts +5 -6
- package/dist/types/keys.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +5 -5
- package/dist/types/stdin-buffer.d.ts +6 -1
- package/dist/types/terminal-capabilities.d.ts +12 -7
- package/dist/types/terminal-multiplexer.d.ts +4 -0
- package/dist/types/terminal.d.ts +46 -21
- package/dist/types/tui.d.ts +18 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +1 -1
- package/src/components/editor.ts +13 -6
- package/src/components/image.ts +31 -24
- package/src/components/markdown.ts +21 -10
- package/src/components/select-list.ts +1 -0
- package/src/components/spacer.ts +1 -1
- package/src/components/text.ts +1 -1
- package/src/deccara.ts +2 -0
- package/src/keys.ts +1 -1
- package/src/kitty-graphics.ts +9 -6
- package/src/latex-block.ts +1 -0
- package/src/stdin-buffer.ts +28 -5
- package/src/terminal-capabilities.ts +22 -21
- package/src/terminal-multiplexer.ts +24 -0
- package/src/terminal.ts +173 -85
- package/src/tui.ts +158 -23
|
@@ -137,7 +137,7 @@ function normalizeHtmlEntitiesForTerminal(raw: string): string {
|
|
|
137
137
|
if (Number.isFinite(value) && value >= 0 && value <= 0x10ffff) {
|
|
138
138
|
try {
|
|
139
139
|
return String.fromCodePoint(value);
|
|
140
|
-
} catch
|
|
140
|
+
} catch {
|
|
141
141
|
// Fallback to empty string or original if invalid codepoint
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -261,7 +261,7 @@ function normalizeHtmlForTerminal(
|
|
|
261
261
|
}
|
|
262
262
|
lastIndex = index + tag.length;
|
|
263
263
|
|
|
264
|
-
const isClosing =
|
|
264
|
+
const isClosing = tag.startsWith("</");
|
|
265
265
|
const isSelfClosing = /\/\s*>$/.test(tag);
|
|
266
266
|
|
|
267
267
|
switch (name) {
|
|
@@ -794,7 +794,7 @@ export function urlTokenPossible(src: string): boolean {
|
|
|
794
794
|
}
|
|
795
795
|
if (i === 0) return false;
|
|
796
796
|
if (i >= URL_GATE_EMAIL_SCAN_LIMIT) return true; // over-long run: give up conservatively
|
|
797
|
-
return src.charCodeAt(i) === 64 /* @
|
|
797
|
+
return src.charCodeAt(i) === 64; /* @ */
|
|
798
798
|
}
|
|
799
799
|
|
|
800
800
|
// Setext-underline pre-gate for marked's `lheading` rule. The rule's lazy body
|
|
@@ -1131,7 +1131,7 @@ function listMayContinueAt(text: string, tailStart: number, listRaw: string): bo
|
|
|
1131
1131
|
// bare newline, or end-of-input (which appends can still extend).
|
|
1132
1132
|
if (i >= n) return true;
|
|
1133
1133
|
const after = text.charCodeAt(i);
|
|
1134
|
-
return after === 0x20 /* space */ || after === 0x09 /* tab */ || after === 0x0a /* \n
|
|
1134
|
+
return after === 0x20 /* space */ || after === 0x09 /* tab */ || after === 0x0a; /* \n */
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
1137
|
const NO_BLOCK_BOUNDARY = { end: 0, count: 0 } as const;
|
|
@@ -2355,8 +2355,11 @@ export class Markdown implements Component {
|
|
|
2355
2355
|
}
|
|
2356
2356
|
|
|
2357
2357
|
const recorder: TailRenderRecorder = {
|
|
2358
|
+
// [suppressed] render-cache length preallocation
|
|
2358
2359
|
rows: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2360
|
+
// [suppressed] render-cache length preallocation
|
|
2359
2361
|
raws: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2362
|
+
// [suppressed] render-cache length preallocation
|
|
2360
2363
|
nextTypes: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2361
2364
|
};
|
|
2362
2365
|
const fresh = this.#renderContentLines(tokens, spliceEnd, tokens.length, contentWidth, signature, recorder);
|
|
@@ -2369,8 +2372,11 @@ export class Markdown implements Component {
|
|
|
2369
2372
|
// `start`), so a mostly-frozen document allocates only for the
|
|
2370
2373
|
// unfrozen tail instead of the whole token list every frame.
|
|
2371
2374
|
const tailCount = tokens.length - start;
|
|
2375
|
+
// [suppressed] render-cache length preallocation
|
|
2372
2376
|
const rows: (readonly string[] | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2377
|
+
// [suppressed] render-cache length preallocation
|
|
2373
2378
|
const raws: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2379
|
+
// [suppressed] render-cache length preallocation
|
|
2374
2380
|
const nextTypes: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2375
2381
|
if (cache !== undefined && cache.tokenStart === start) {
|
|
2376
2382
|
for (let i = start; i < Math.min(cache.cachedThrough, spliceEnd); i++) {
|
|
@@ -3155,17 +3161,21 @@ export class Markdown implements Component {
|
|
|
3155
3161
|
markHtmlItemWhenContent(token.text);
|
|
3156
3162
|
const linkText = this.#renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
3157
3163
|
const styledLinkText = this.#theme.link(this.#theme.underline(linkText));
|
|
3158
|
-
const
|
|
3159
|
-
|
|
3164
|
+
const href = typeof token.href === "string" ? token.href : "";
|
|
3165
|
+
const clickableLinkText = formatHyperlink(styledLinkText, href);
|
|
3166
|
+
// If link text matches href, only show the link once. A missing
|
|
3167
|
+
// href (malformed/partial link token) renders as plain link text
|
|
3168
|
+
// instead of crashing the renderer or emitting an empty "()"
|
|
3169
|
+
// (issue #10283).
|
|
3160
3170
|
// Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes
|
|
3161
3171
|
// For mailto: links, strip the prefix before comparing (autolinked emails have
|
|
3162
3172
|
// text="foo@bar.com" but href="mailto:foo@bar.com")
|
|
3163
|
-
const hrefForComparison =
|
|
3164
|
-
if (token.text ===
|
|
3173
|
+
const hrefForComparison = href.startsWith("mailto:") ? href.slice(7) : href;
|
|
3174
|
+
if (!href || token.text === href || token.text === hrefForComparison)
|
|
3165
3175
|
result += clickableLinkText + stylePrefix;
|
|
3166
3176
|
else {
|
|
3167
|
-
const styledLinkUrl = this.#theme.linkUrl(`(${
|
|
3168
|
-
result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl,
|
|
3177
|
+
const styledLinkUrl = this.#theme.linkUrl(`(${href})`);
|
|
3178
|
+
result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl, href)}${stylePrefix}`;
|
|
3169
3179
|
}
|
|
3170
3180
|
break;
|
|
3171
3181
|
}
|
|
@@ -3464,6 +3474,7 @@ export class Markdown implements Component {
|
|
|
3464
3474
|
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
3465
3475
|
|
|
3466
3476
|
if (minCellsWidth > availableForCells) {
|
|
3477
|
+
// [suppressed] column-width allocation
|
|
3467
3478
|
minColumnWidths = new Array(numCols).fill(1);
|
|
3468
3479
|
const remaining = availableForCells - numCols;
|
|
3469
3480
|
|
|
@@ -207,6 +207,7 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
207
207
|
// every count is 1, so visualTotal == #filteredItems and overflow falls
|
|
208
208
|
// back to the original `N > maxVisible` predicate exactly.
|
|
209
209
|
const conservativeRowWidth = Math.max(0, width - 1);
|
|
210
|
+
// [suppressed] length preallocation
|
|
210
211
|
const rowCounts = new Array<number>(this.#filteredItems.length);
|
|
211
212
|
let visualTotal = 0;
|
|
212
213
|
for (let i = 0; i < this.#filteredItems.length; i++) {
|
package/src/components/spacer.ts
CHANGED
|
@@ -20,7 +20,6 @@ export class Spacer implements Component {
|
|
|
20
20
|
this.#lines = lines;
|
|
21
21
|
this.#cached = undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
23
|
invalidate(): void {
|
|
25
24
|
// No cached state to invalidate currently
|
|
26
25
|
}
|
|
@@ -28,6 +27,7 @@ export class Spacer implements Component {
|
|
|
28
27
|
render(_width: number): readonly string[] {
|
|
29
28
|
let cached = this.#cached;
|
|
30
29
|
if (cached === undefined) {
|
|
30
|
+
// [suppressed] cached line allocation
|
|
31
31
|
cached = new Array(this.#lines).fill("");
|
|
32
32
|
this.#cached = cached;
|
|
33
33
|
}
|
package/src/components/text.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class Text implements Component {
|
|
|
168
168
|
|
|
169
169
|
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
170
170
|
if (resultWidths !== undefined) {
|
|
171
|
-
//
|
|
171
|
+
// [suppressed] line-width allocation
|
|
172
172
|
const emptyWidths = new Array<number>(emptyLines.length).fill(width);
|
|
173
173
|
publishLineWidths(result, [...emptyWidths, ...resultWidths, ...emptyWidths]);
|
|
174
174
|
}
|
package/src/deccara.ts
CHANGED
|
@@ -239,7 +239,9 @@ export interface DeccaraPlan {
|
|
|
239
239
|
*/
|
|
240
240
|
export function planDeccaraFills(lines: string[], width: number, firstScreenRow = 0): DeccaraPlan {
|
|
241
241
|
const n = lines.length;
|
|
242
|
+
// [suppressed] length preallocation
|
|
242
243
|
const texts: string[] = new Array(n);
|
|
244
|
+
// [suppressed] length preallocation
|
|
243
245
|
const candidates: (FillCandidate | null)[] = new Array(n);
|
|
244
246
|
|
|
245
247
|
for (let k = 0; k < n; k++) {
|
package/src/keys.ts
CHANGED
|
@@ -198,7 +198,7 @@ export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
|
|
|
198
198
|
* modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
|
|
199
199
|
* is `"ctrl+c"`, not just `string`). This mirrors the upstream
|
|
200
200
|
* `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
|
|
201
|
-
* scope alias (`@mariozechner`, `@earendil-works`, `@
|
|
201
|
+
* scope alias (`@mariozechner`, `@earendil-works`, `@linxiraos`) keep working
|
|
202
202
|
* once the specifier shim remaps them to this package.
|
|
203
203
|
*/
|
|
204
204
|
export const Key = {
|
package/src/kitty-graphics.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* forms. Protocol gating (`imageProtocol === Kitty`) lives in the caller.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { isInsideHerdr, isInsideTerminalMultiplexer } from "./terminal-multiplexer";
|
|
18
19
|
import { wrapTmuxPassthroughIfNeeded } from "./tmux";
|
|
19
20
|
|
|
20
21
|
/** Kitty Unicode placeholder base character (U+10EEEE, Plane 16 PUA). */
|
|
@@ -62,12 +63,12 @@ export interface KittyGraphicsFeatures {
|
|
|
62
63
|
* Whether the detected terminal renders Kitty Unicode placeholders (`U=1` +
|
|
63
64
|
* U+10EEEE with row/column diacritics).
|
|
64
65
|
*
|
|
65
|
-
* Kitty and Ghostty advertise placeholder support directly. A
|
|
66
|
+
* Kitty and Ghostty advertise placeholder support directly. A multiplexer
|
|
66
67
|
* cannot use cursor-positioned placements because the outer terminal does not
|
|
67
|
-
* know pane scroll/reflow state
|
|
68
|
-
*
|
|
69
|
-
* fallback stays off because
|
|
70
|
-
*
|
|
68
|
+
* know pane scroll/reflow state. An explicit `PI_FORCE_IMAGE_PROTOCOL=kitty`
|
|
69
|
+
* opts into placeholders under any multiplexer — matching `timg -pk`.
|
|
70
|
+
* Automatic Herdr fallback stays off because its pane marker does not prove
|
|
71
|
+
* that the experimental Kitty renderer is enabled.
|
|
71
72
|
*
|
|
72
73
|
* `PI_NO_KITTY_PLACEHOLDERS=1` and `PI_KITTY_PLACEHOLDERS=0` remain hard
|
|
73
74
|
* opt-outs; `PI_KITTY_PLACEHOLDERS=1` explicitly opts in anywhere else.
|
|
@@ -78,7 +79,9 @@ export function detectKittyUnicodePlaceholdersSupport(terminalId: string, env: N
|
|
|
78
79
|
const force = env.PI_KITTY_PLACEHOLDERS?.trim().toLowerCase();
|
|
79
80
|
if (force === "1" || force === "true" || force === "on" || force === "yes" || force === "y") return true;
|
|
80
81
|
if (force === "0" || force === "false" || force === "off" || force === "no" || force === "n") return false;
|
|
81
|
-
|
|
82
|
+
const insideMultiplexer = isInsideTerminalMultiplexer(env);
|
|
83
|
+
if (insideMultiplexer && env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase() === "kitty") return true;
|
|
84
|
+
if (isInsideHerdr(env)) return false;
|
|
82
85
|
return terminalId === "kitty" || terminalId === "ghostty";
|
|
83
86
|
}
|
|
84
87
|
|
package/src/latex-block.ts
CHANGED
|
@@ -460,6 +460,7 @@ function gridBox(rows: Box[][], align: (col: number) => CellAlign, gap: (col: nu
|
|
|
460
460
|
let ncols = 0;
|
|
461
461
|
for (const row of rows) ncols = Math.max(ncols, row.length);
|
|
462
462
|
if (ncols === 0 || rows.length === 0) return textBox("");
|
|
463
|
+
// [suppressed] grid-width allocation
|
|
463
464
|
const widths = new Array<number>(ncols).fill(0);
|
|
464
465
|
for (const row of rows) {
|
|
465
466
|
row.forEach((cell, j) => {
|
package/src/stdin-buffer.ts
CHANGED
|
@@ -144,7 +144,7 @@ function resolveEscapeEnd(buffer: string, pos: number, length: number, resumeSea
|
|
|
144
144
|
return -1;
|
|
145
145
|
}
|
|
146
146
|
const capEnd = Math.min(length, pos + MAX_CSI_BYTES);
|
|
147
|
-
const isSgrMouse = buffer.charCodeAt(pos + 2) === 0x3c /* <
|
|
147
|
+
const isSgrMouse = buffer.charCodeAt(pos + 2) === 0x3c; /* < */
|
|
148
148
|
// No resume hint for CSI: `extractCompleteSequences` records
|
|
149
149
|
// hints only for OSC/DCS/APC. A partial CSI rescans from its
|
|
150
150
|
// head, bounded by the tight MAX_CSI_BYTES cap.
|
|
@@ -378,9 +378,27 @@ export type StdinBufferOptions = {
|
|
|
378
378
|
pasteByteLimit?: number;
|
|
379
379
|
};
|
|
380
380
|
|
|
381
|
+
const KITTY_ENTER = /^\x1b\[13(?:;1)?u/u;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The Enter keypress that immediately follows a bracketed-paste end marker in
|
|
385
|
+
* the same stdin read: legacy CR/LF, or the kitty CSI-u encoding with no
|
|
386
|
+
* modifier. Nothing else qualifies — any other trailing byte takes the normal
|
|
387
|
+
* data route.
|
|
388
|
+
*/
|
|
389
|
+
function leadingEnter(input: string): string | undefined {
|
|
390
|
+
if (input.startsWith("\r") || input.startsWith("\n")) return input[0];
|
|
391
|
+
return KITTY_ENTER.exec(input)?.[0];
|
|
392
|
+
}
|
|
393
|
+
|
|
381
394
|
export type StdinBufferEventMap = {
|
|
382
395
|
data: [string];
|
|
383
|
-
|
|
396
|
+
/**
|
|
397
|
+
* A completed bracketed paste. `enter` carries an Enter keypress that shared
|
|
398
|
+
* the paste's stdin read, so the terminal can dispatch paste and submit to
|
|
399
|
+
* the same focused component before a paste-triggered overlay can take focus.
|
|
400
|
+
*/
|
|
401
|
+
paste: [content: string, enter?: string];
|
|
384
402
|
};
|
|
385
403
|
|
|
386
404
|
/**
|
|
@@ -580,10 +598,15 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
580
598
|
this.#pasteBytes = 0;
|
|
581
599
|
this.#pendingKittyPrintableCodepoint = undefined;
|
|
582
600
|
|
|
583
|
-
|
|
601
|
+
// A paste-and-Enter burst (automation, terminals that batch reads) must
|
|
602
|
+
// not split into two dispatches: the paste may open an overlay that
|
|
603
|
+
// would then swallow the Enter meant to submit it.
|
|
604
|
+
const enter = leadingEnter(remaining);
|
|
605
|
+
this.emit("paste", pastedContent, enter);
|
|
584
606
|
|
|
585
|
-
|
|
586
|
-
|
|
607
|
+
const rest = enter === undefined ? remaining : remaining.slice(enter.length);
|
|
608
|
+
if (rest.length > 0) {
|
|
609
|
+
this.process(rest);
|
|
587
610
|
}
|
|
588
611
|
}
|
|
589
612
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeSixel } from "@linxiraos/pi-natives";
|
|
2
|
-
import { $env, isBunTestRuntime, isTerminalHeadless } from "@linxiraos/pi-utils";
|
|
2
|
+
import { $env, isBunTestRuntime, isTerminalHeadless, isWsl } from "@linxiraos/pi-utils";
|
|
3
3
|
import { sendDesktopNotification, shouldDeliverDesktopNotification } from "./desktop-notify";
|
|
4
4
|
import {
|
|
5
5
|
detectKittyUnicodePlaceholdersSupport,
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
renderKittyPlaceholderLines,
|
|
10
10
|
setKittyGraphics,
|
|
11
11
|
} from "./kitty-graphics";
|
|
12
|
+
import { isInsideHerdr, isInsideTerminalMultiplexer } from "./terminal-multiplexer";
|
|
12
13
|
import { isInsideTmux, wrapTmuxPassthrough, wrapTmuxPassthroughIfNeeded } from "./tmux";
|
|
13
14
|
import type { HangulCompatibilityJamoWidth } from "./utils";
|
|
14
15
|
|
|
16
|
+
export * from "./terminal-multiplexer";
|
|
15
17
|
export { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
|
|
16
18
|
|
|
17
19
|
export enum ImageProtocol {
|
|
@@ -192,19 +194,6 @@ export class TerminalInfo {
|
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
/** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */
|
|
196
|
-
export function isInsideTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
197
|
-
// TMUX/STY/ZELLIJ, Herdr, and CMUX workspace/surface/remote-transport
|
|
198
|
-
// markers are authoritative session signals. TERM can also survive when those are
|
|
199
|
-
// stripped (`sudo` without -E, `su`, env-sanitizing launchers/ssh). Do not
|
|
200
|
-
// use CMUX_SOCKET_PATH here: it is a CLI socket override and can be set
|
|
201
|
-
// outside a CMUX terminal.
|
|
202
|
-
if (env.TMUX || env.STY || env.ZELLIJ || env.HERDR_ENV === "1") return true;
|
|
203
|
-
if (env.CMUX_WORKSPACE_ID || env.CMUX_SURFACE_ID || env.CMUX_REMOTE_TRANSPORT) return true;
|
|
204
|
-
const term = env.TERM?.toLowerCase() ?? "";
|
|
205
|
-
return term.startsWith("tmux") || term.startsWith("screen");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
197
|
/**
|
|
209
198
|
* Whether the agent process is running inside a Zellij session. Read fresh on
|
|
210
199
|
* each call (like {@link isInsideTmux}) so a session attached/detached mid-run
|
|
@@ -300,9 +289,15 @@ function advertisesSynchronizedOutput(termFeatures: string | undefined): boolean
|
|
|
300
289
|
* 2. Positive `TERM_FEATURES` advertisement (`Sy`) — survives SSH/mux wrapping.
|
|
301
290
|
* 3. Windows Terminal (1.24+) via `WT_SESSION`, on native win32 and the
|
|
302
291
|
* WSL/SSH-fronted host alike.
|
|
303
|
-
* 4.
|
|
292
|
+
* 4. Herdr panes. Herdr is otherwise treated as a multiplexer so leaked
|
|
293
|
+
* kitty/ghostty identities cannot enable placeholder graphics, but its
|
|
294
|
+
* pane VTE is libghostty and already suppresses compositing while DEC 2026
|
|
295
|
+
* is set. Leaving sync off lets CUP-diff paints and split write(2) chunks
|
|
296
|
+
* composite as dirty-row patches — the live viewport tears, with the top
|
|
297
|
+
* frozen while only the bottom refreshes.
|
|
298
|
+
* 5. Known direct terminals with confirmed support. SSH does *not* disable —
|
|
304
299
|
* DEC 2026 passes through SSH when the outer terminal honors it.
|
|
305
|
-
*
|
|
300
|
+
* 6. Everything else starts off, including risky multiplexers; the runtime
|
|
306
301
|
* DECRQM probe upgrades any of them when the terminal actually reports
|
|
307
302
|
* `?2026` supported (current zellij, tmux master, foot, contour, mintty…).
|
|
308
303
|
*/
|
|
@@ -315,6 +310,7 @@ export function shouldEnableSynchronizedOutputByDefault(
|
|
|
315
310
|
|
|
316
311
|
if (advertisesSynchronizedOutput(env.TERM_FEATURES)) return true;
|
|
317
312
|
if (env.WT_SESSION) return true;
|
|
313
|
+
if (isInsideHerdr(env)) return true;
|
|
318
314
|
|
|
319
315
|
// Risky multiplexers start off even when an inner terminal id leaks through:
|
|
320
316
|
// older tmux/screen synchronized-output handling is flaky and a mux may not
|
|
@@ -469,8 +465,7 @@ export function resolveWarpImageProtocol(
|
|
|
469
465
|
platform: NodeJS.Platform = process.platform,
|
|
470
466
|
env: NodeJS.ProcessEnv = Bun.env,
|
|
471
467
|
): ImageProtocol | null {
|
|
472
|
-
const windowsHost =
|
|
473
|
-
platform === "win32" || (platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP));
|
|
468
|
+
const windowsHost = platform === "win32" || isWsl(platform, env);
|
|
474
469
|
return windowsHost ? null : ImageProtocol.Kitty;
|
|
475
470
|
}
|
|
476
471
|
|
|
@@ -489,9 +484,9 @@ export function isPaseoEmbedder(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
|
489
484
|
/**
|
|
490
485
|
* Resolve the image protocol for a non-forced runtime: static per-terminal
|
|
491
486
|
* support (with Warp's platform carve-out), then the multiplexer fallback,
|
|
492
|
-
* then
|
|
493
|
-
*
|
|
494
|
-
*
|
|
487
|
+
* then host carve-outs. `isTTY` is injectable because the fallback only fires
|
|
488
|
+
* on a real TTY — a piped subprocess cannot exercise that path, so regression
|
|
489
|
+
* tests call this directly.
|
|
495
490
|
*/
|
|
496
491
|
export function resolveImageProtocol(
|
|
497
492
|
terminalId: TerminalId,
|
|
@@ -516,6 +511,12 @@ export function resolveImageProtocol(
|
|
|
516
511
|
if (imageProtocol !== null && isPaseoEmbedder(env)) {
|
|
517
512
|
return null;
|
|
518
513
|
}
|
|
514
|
+
// Herdr owns the pane grid but does not expose whether the attached client
|
|
515
|
+
// enabled its experimental Kitty renderer. Outer-terminal identity variables
|
|
516
|
+
// can leak into the pane, so only the explicit protocol override is safe.
|
|
517
|
+
if (imageProtocol !== null && isInsideHerdr(env)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
519
520
|
return imageProtocol;
|
|
520
521
|
}
|
|
521
522
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** True when this process is running inside a Herdr pane. */
|
|
2
|
+
export function isInsideHerdr(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
3
|
+
// HERDR_ENV=1 is canonical. Identity vars survive env-sanitizing launchers
|
|
4
|
+
// that drop HERDR_ENV. Do not use HERDR_SOCKET_PATH, HERDR_BIN_PATH,
|
|
5
|
+
// HERDR_SESSION, HERDR_CONFIG_PATH, or HERDR_CLIENT_SOCKET_PATH here: they
|
|
6
|
+
// are client-side and can be set outside a Herdr pane, matching the
|
|
7
|
+
// CMUX_SOCKET_PATH warning below.
|
|
8
|
+
if (env.HERDR_ENV === "1") return true;
|
|
9
|
+
if (env.HERDR_PANE_ID || env.HERDR_TAB_ID || env.HERDR_WORKSPACE_ID) return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Detect whether a terminal multiplexer owns the current screen grid. */
|
|
14
|
+
export function isInsideTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
15
|
+
// TMUX/STY/ZELLIJ, Herdr, and CMUX workspace/surface/remote-transport
|
|
16
|
+
// markers are authoritative session signals. TERM can also survive when those are
|
|
17
|
+
// stripped (`sudo` without -E, `su`, env-sanitizing launchers/ssh). Do not
|
|
18
|
+
// use CMUX_SOCKET_PATH here: it is a CLI socket override and can be set
|
|
19
|
+
// outside a CMUX terminal.
|
|
20
|
+
if (env.TMUX || env.STY || env.ZELLIJ || isInsideHerdr(env)) return true;
|
|
21
|
+
if (env.CMUX_WORKSPACE_ID || env.CMUX_SURFACE_ID || env.CMUX_REMOTE_TRANSPORT) return true;
|
|
22
|
+
const term = env.TERM?.toLowerCase() ?? "";
|
|
23
|
+
return term.startsWith("tmux") || term.startsWith("screen");
|
|
24
|
+
}
|