@prometheus-ai/tui 0.5.3 → 0.5.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.
Files changed (55) hide show
  1. package/dist/types/autocomplete.d.ts +3 -1
  2. package/dist/types/components/box.d.ts +1 -1
  3. package/dist/types/components/editor.d.ts +35 -2
  4. package/dist/types/components/image.d.ts +22 -3
  5. package/dist/types/components/input.d.ts +6 -1
  6. package/dist/types/components/loader.d.ts +9 -2
  7. package/dist/types/components/markdown.d.ts +3 -1
  8. package/dist/types/components/scroll-view.d.ts +23 -1
  9. package/dist/types/components/select-list.d.ts +19 -1
  10. package/dist/types/components/settings-list.d.ts +87 -7
  11. package/dist/types/components/spacer.d.ts +1 -1
  12. package/dist/types/components/tab-bar.d.ts +37 -4
  13. package/dist/types/components/text.d.ts +2 -2
  14. package/dist/types/components/truncated-text.d.ts +1 -1
  15. package/dist/types/fuzzy.d.ts +10 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/keybindings.d.ts +5 -3
  18. package/dist/types/keys.d.ts +1 -1
  19. package/dist/types/kill-ring.d.ts +0 -7
  20. package/dist/types/kitty-graphics.d.ts +16 -31
  21. package/dist/types/loop-watchdog.d.ts +39 -0
  22. package/dist/types/mouse.d.ts +41 -0
  23. package/dist/types/stdin-buffer.d.ts +17 -0
  24. package/dist/types/terminal-capabilities.d.ts +74 -18
  25. package/dist/types/terminal.d.ts +34 -36
  26. package/dist/types/tui.d.ts +191 -79
  27. package/dist/types/utils.d.ts +5 -2
  28. package/package.json +4 -4
  29. package/src/autocomplete.ts +79 -65
  30. package/src/components/box.ts +43 -63
  31. package/src/components/editor.ts +471 -136
  32. package/src/components/image.ts +85 -9
  33. package/src/components/input.ts +12 -3
  34. package/src/components/loader.ts +35 -21
  35. package/src/components/markdown.ts +174 -53
  36. package/src/components/scroll-view.ts +63 -2
  37. package/src/components/select-list.ts +233 -38
  38. package/src/components/settings-list.ts +626 -64
  39. package/src/components/spacer.ts +9 -5
  40. package/src/components/tab-bar.ts +153 -28
  41. package/src/components/text.ts +6 -2
  42. package/src/components/truncated-text.ts +10 -2
  43. package/src/fuzzy.ts +214 -59
  44. package/src/index.ts +3 -1
  45. package/src/keybindings.ts +72 -14
  46. package/src/keys.ts +1 -1
  47. package/src/kill-ring.ts +5 -0
  48. package/src/kitty-graphics.ts +2 -101
  49. package/src/loop-watchdog.ts +106 -0
  50. package/src/mouse.ts +55 -0
  51. package/src/stdin-buffer.ts +291 -81
  52. package/src/terminal-capabilities.ts +206 -168
  53. package/src/terminal.ts +367 -110
  54. package/src/tui.ts +2102 -1729
  55. package/src/utils.ts +92 -60
@@ -2,9 +2,7 @@ import { encodeSixel } from "@prometheus-ai/natives";
2
2
  import { $env, isBunTestRuntime } from "@prometheus-ai/utils";
3
3
  import {
4
4
  detectKittyUnicodePlaceholdersSupport,
5
- encodeKittyTempFileTransmit,
6
5
  getKittyGraphics,
7
- isPngBase64,
8
6
  KITTY_PLACEHOLDER,
9
7
  kittyPlaceholdersFit,
10
8
  renderKittyPlaceholderLines,
@@ -25,7 +23,31 @@ export enum NotifyProtocol {
25
23
 
26
24
  export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "base" | "trueColor";
27
25
 
28
- const SIXEL_DCS_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
26
+ function hasNeedleBefore(line: string, needle: string, limit: number): boolean {
27
+ const index = line.indexOf(needle);
28
+ return index !== -1 && index + needle.length <= limit;
29
+ }
30
+
31
+ function hasSixelDcsStart(line: string): boolean {
32
+ const limit = Math.min(line.length, 128);
33
+ let from = 0;
34
+ for (;;) {
35
+ const start = line.indexOf("\x1bP", from);
36
+ if (start === -1 || start + 3 > limit) return false;
37
+ let i = start + 2;
38
+ while (i < limit) {
39
+ const code = line.charCodeAt(i);
40
+ if ((code >= 0x30 && code <= 0x39) || code === 0x3b) {
41
+ i++;
42
+ continue;
43
+ }
44
+ break;
45
+ }
46
+ if (i < limit && line.charCodeAt(i) === 0x71) return true;
47
+ from = start + 2;
48
+ }
49
+ }
50
+
29
51
  /** Terminal capability details used for rendering and protocol selection. */
30
52
  export class TerminalInfo {
31
53
  constructor(
@@ -34,20 +56,28 @@ export class TerminalInfo {
34
56
  public readonly trueColor: boolean,
35
57
  public readonly hyperlinks: boolean,
36
58
  public readonly notifyProtocol: NotifyProtocol = NotifyProtocol.Bell,
37
- public readonly eagerEraseScrollbackRisk: boolean = false,
38
59
  public readonly deccara: boolean = false,
39
60
  readonly supportsScreenToScrollback: boolean = false,
40
61
  /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
41
62
  public readonly textSizing: boolean = false,
42
63
  ) {}
43
64
 
65
+ /**
66
+ * Mutable clone for the {@link TERMINAL} singleton: copies every field and
67
+ * keeps the prototype methods, so the builder and runtime setters flip
68
+ * runtime-resolved {@link RuntimeTerminal} capabilities in place instead of
69
+ * reconstructing positional constructor args.
70
+ */
71
+ clone(): RuntimeTerminal {
72
+ return Object.assign(Object.create(TerminalInfo.prototype), this) as RuntimeTerminal;
73
+ }
74
+
44
75
  isImageLine(line: string): boolean {
45
76
  if (!this.imageProtocol) return false;
46
77
  if (this.imageProtocol === ImageProtocol.Sixel) {
47
- return SIXEL_DCS_START_REGEX.test(line.slice(0, 128));
78
+ return hasSixelDcsStart(line);
48
79
  }
49
- const head = line.slice(0, 64);
50
- return head.includes(this.imageProtocol) || head.includes(KITTY_PLACEHOLDER);
80
+ return hasNeedleBefore(line, this.imageProtocol, 64) || hasNeedleBefore(line, KITTY_PLACEHOLDER, 64);
51
81
  }
52
82
 
53
83
  formatNotification(message: string | TerminalNotification): string {
@@ -118,110 +148,75 @@ export function isWindowsTerminalPreviewSixelSupported(
118
148
  }
119
149
 
120
150
  /**
121
- * Whether live-frame native scrollback rebuilds are unsafe when the terminal
122
- * viewport position is unobservable.
123
- *
124
- * A TUI history rebuild emits xterm ED3 (`CSI 3 J`, erase saved lines). Many
125
- * terminals either clamp a scrolled reader back to the active tail or erase host
126
- * scrollback when ED3 lands. The important property is not the brand name — it
127
- * is that an unknown viewport position cannot be proven safe. Environment
128
- * markers are therefore only used to prove *risk* or a strongly-known profile;
129
- * unknown POSIX/remote/multiplexer shapes default to risky for passive renders.
130
- *
131
- * Native win32 is excluded here because the renderer has dedicated ConPTY
132
- * deferral paths; a `WT_SESSION` sighting on POSIX means Windows Terminal is the
133
- * outer host fronting WSL, where the same ED3 yank applies. See #1610/#1682/#1799.
151
+ * Resolve an explicit user override for DEC 2026 synchronized output. Returns
152
+ * `false` for an opt-out, `true` for a force-on, or `null` when the user has
153
+ * expressed no preference. Shared by the static default and the runtime DECRQM
154
+ * probe so both honor the same precedence an opt-out beats a force-on.
134
155
  */
135
- export function detectTerminalEagerEraseScrollbackRisk(
136
- env: NodeJS.ProcessEnv = Bun.env,
137
- platform: NodeJS.Platform = process.platform,
138
- ): boolean {
139
- if (platform === "win32") return false;
140
-
141
- const term = env.TERM?.toLowerCase() ?? "";
142
- const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
143
- const colorTerm = env.COLORTERM?.toLowerCase() ?? "";
156
+ export function synchronizedOutputUserOverride(env: NodeJS.ProcessEnv = Bun.env): boolean | null {
157
+ if (env.PROMETHEUS_NO_SYNC_OUTPUT || env.PROMETHEUS_TUI_SYNC_OUTPUT === "0") return false;
158
+ if (env.PROMETHEUS_FORCE_SYNC_OUTPUT === "1" || env.PROMETHEUS_TUI_SYNC_OUTPUT === "1") return true;
159
+ return null;
160
+ }
144
161
 
145
- if (env.PROMETHEUS_TUI_ED3_SAFE === "1") return false;
146
- if (env.WT_SESSION) return true;
147
- if (
148
- env.SSH_CONNECTION ||
149
- env.SSH_CLIENT ||
150
- env.SSH_TTY ||
151
- env.TMUX ||
152
- env.STY ||
153
- env.ZELLIJ ||
154
- term.startsWith("tmux") ||
155
- term.startsWith("screen")
156
- ) {
157
- return true;
158
- }
159
- if (
160
- env.WEZTERM_PANE ||
161
- env.KITTY_WINDOW_ID ||
162
- env.GHOSTTY_RESOURCES_DIR ||
163
- env.ALACRITTY_WINDOW_ID ||
164
- env.VTE_VERSION ||
165
- env.ITERM_SESSION_ID
166
- ) {
167
- return true;
168
- }
169
- switch (termProgram) {
170
- case "alacritty":
171
- case "apple_terminal":
172
- case "ghostty":
173
- case "gnome-terminal":
174
- case "iterm.app":
175
- case "kgx":
176
- case "kitty":
177
- case "ptyxis":
178
- case "wezterm":
179
- case "xfce4-terminal":
180
- return true;
181
- default:
182
- break;
183
- }
184
- if (platform === "linux" && (colorTerm === "truecolor" || colorTerm === "24bit")) return true;
185
- // Unknown POSIX terminals have no scroll-position oracle. Treat them as risky
186
- // for passive ED3 until a positive terminal-specific integration proves safe.
187
- return true;
162
+ /**
163
+ * Whether `TERM_FEATURES` advertises DEC 2026 synchronized output via the `Sy`
164
+ * capability token. `TERM_FEATURES` is a run of capitalized two-letter codes
165
+ * (e.g. `…Sy…`), so a case-sensitive substring match is unambiguous: `Sy`
166
+ * cannot straddle a code boundary because those are always lowercase→uppercase.
167
+ */
168
+ function advertisesSynchronizedOutput(termFeatures: string | undefined): boolean {
169
+ return termFeatures?.includes("Sy") ?? false;
188
170
  }
189
171
 
190
- /** Whether DEC 2026 synchronized-output wrappers should be enabled by default. */
172
+ /**
173
+ * Whether DEC 2026 synchronized-output wrappers should be enabled by default.
174
+ *
175
+ * Policy (highest precedence first):
176
+ * 1. Explicit user override (`PROMETHEUS_NO_SYNC_OUTPUT`/`PROMETHEUS_TUI_SYNC_OUTPUT=0` off,
177
+ * `PROMETHEUS_FORCE_SYNC_OUTPUT=1`/`PROMETHEUS_TUI_SYNC_OUTPUT=1` on).
178
+ * 2. Positive `TERM_FEATURES` advertisement (`Sy`) — survives SSH/mux wrapping.
179
+ * 3. Windows Terminal (1.24+) via `WT_SESSION`, on native win32 and the
180
+ * WSL/SSH-fronted host alike.
181
+ * 4. Known direct terminals with confirmed support. SSH does *not* disable —
182
+ * DEC 2026 passes through SSH when the outer terminal honors it.
183
+ * 5. Everything else starts off, including risky multiplexers; the runtime
184
+ * DECRQM probe upgrades any of them when the terminal actually reports
185
+ * `?2026` supported (current zellij, tmux master, foot, contour, mintty…).
186
+ */
191
187
  export function shouldEnableSynchronizedOutputByDefault(
192
188
  env: NodeJS.ProcessEnv = Bun.env,
193
- platform: NodeJS.Platform = process.platform,
194
189
  terminalId: TerminalId = TERMINAL_ID,
195
190
  ): boolean {
196
- if (env.PROMETHEUS_NO_SYNC_OUTPUT || env.PROMETHEUS_TUI_SYNC_OUTPUT === "0") return false;
197
- if (env.PROMETHEUS_FORCE_SYNC_OUTPUT === "1" || env.PROMETHEUS_TUI_SYNC_OUTPUT === "1") return true;
198
- if (platform === "win32") return false;
191
+ const override = synchronizedOutputUserOverride(env);
192
+ if (override !== null) return override;
199
193
 
194
+ if (advertisesSynchronizedOutput(env.TERM_FEATURES)) return true;
195
+ if (env.WT_SESSION) return true;
196
+
197
+ // Risky multiplexers start off even when an inner terminal id leaks through:
198
+ // older tmux/screen synchronized-output handling is flaky and a mux may not
199
+ // pass DEC 2026 to the outer host. The DECRQM probe re-enables sync when the
200
+ // mux reports `?2026` supported.
200
201
  const term = env.TERM?.toLowerCase() ?? "";
201
- const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
202
- if (
203
- env.SSH_CONNECTION ||
204
- env.SSH_CLIENT ||
205
- env.SSH_TTY ||
206
- env.TMUX ||
207
- env.STY ||
208
- env.ZELLIJ ||
209
- term.startsWith("tmux") ||
210
- term.startsWith("screen")
211
- ) {
202
+ if (env.TMUX || env.STY || env.ZELLIJ || term.startsWith("tmux") || term.startsWith("screen")) {
212
203
  return false;
213
204
  }
214
- if (env.VTE_VERSION) return false;
215
- switch (termProgram) {
216
- case "gnome-terminal":
217
- case "kgx":
218
- case "ptyxis":
219
- case "xfce4-terminal":
220
- return false;
205
+
206
+ switch (terminalId) {
207
+ case "kitty":
208
+ case "ghostty":
209
+ case "wezterm":
210
+ case "iterm2":
211
+ case "alacritty":
212
+ case "vscode":
213
+ return true;
221
214
  default:
222
- break;
215
+ // VTE family, GNU screen, Apple Terminal, legacy native console host
216
+ // (no WT_SESSION), and bare/unknown xterm profiles stay off until the
217
+ // DECRQM probe proves support.
218
+ return false;
223
219
  }
224
- return terminalId === "kitty" || terminalId === "ghostty" || terminalId === "wezterm" || terminalId === "iterm2";
225
220
  }
226
221
 
227
222
  /**
@@ -254,6 +249,83 @@ export function detectRectangularSgrSupport(terminalId: TerminalId, env: NodeJS.
254
249
  }
255
250
  return true;
256
251
  }
252
+ /**
253
+ * Resolve an explicit user override for OSC 8 hyperlinks. Returns `false` for
254
+ * an opt-out, `true` for a force-on, or `null` when the user has expressed no
255
+ * preference. Opt-out beats force-on so a kill switch is unambiguous, mirroring
256
+ * {@link synchronizedOutputUserOverride}.
257
+ */
258
+ export function hyperlinksUserOverride(env: NodeJS.ProcessEnv = Bun.env): boolean | null {
259
+ if (env.PROMETHEUS_NO_HYPERLINKS === "1") return false;
260
+ if (env.PROMETHEUS_FORCE_HYPERLINKS === "1") return true;
261
+ return null;
262
+ }
263
+
264
+ /**
265
+ * Parse tmux's self-reported version from `TERM_PROGRAM_VERSION`. tmux sets
266
+ * `TERM_PROGRAM=tmux` and `TERM_PROGRAM_VERSION=<version>` automatically since
267
+ * 3.2a; older releases (or any path that does not surface the version) yield
268
+ * `null` and the caller treats tmux conservatively.
269
+ */
270
+ function parseTmuxVersionFromEnv(env: NodeJS.ProcessEnv): { major: number; minor: number } | null {
271
+ if (env.TERM_PROGRAM?.toLowerCase() !== "tmux") return null;
272
+ return parseMajorMinorVersion(env.TERM_PROGRAM_VERSION);
273
+ }
274
+
275
+ /**
276
+ * Whether OSC 8 hyperlinks should be enabled by default.
277
+ *
278
+ * Policy (highest precedence first):
279
+ * 1. Explicit user override (`PROMETHEUS_NO_HYPERLINKS=1` off, `PROMETHEUS_FORCE_HYPERLINKS=1`
280
+ * on). Opt-out wins ties.
281
+ * 2. Static terminal capability — terminals whose {@link TerminalInfo} marks
282
+ * `hyperlinks: false` (e.g. `base`) stay off unless the user forced on.
283
+ * 3. GNU screen's explicit session marker (`STY`) always off, even if tmux is
284
+ * also present: a screen layer anywhere in the path cannot forward OSC 8.
285
+ * 4. tmux session (`TMUX` set): enabled when tmux self-reports >= 3.4 via
286
+ * `TERM_PROGRAM_VERSION` (tmux 3.4 stores OSC 8 as a cell attribute and
287
+ * forwards it to outer terminals whose `terminal-features` include
288
+ * `hyperlinks`). Older or unknown versions stay off; on outer terminals
289
+ * without the feature configured, tmux silently drops the sequence —
290
+ * identical to today. Checked before the screen-family TERM heuristic
291
+ * because tmux's historical `default-terminal` is `screen-256color`, so
292
+ * `TERM=screen*` inside a tmux session must NOT short-circuit to off.
293
+ * 5. screen-family TERM without `TMUX` always off: screen never gained OSC 8
294
+ * support.
295
+ * 6. tmux-family TERM without `TMUX` env — unusual (e.g. inspection scripts);
296
+ * no version available, so off.
297
+ * 7. Otherwise honor the static terminal capability.
298
+ */
299
+ export function shouldEnableHyperlinksByDefault(
300
+ env: NodeJS.ProcessEnv = Bun.env,
301
+ terminalId: TerminalId = TERMINAL_ID,
302
+ ): boolean {
303
+ const override = hyperlinksUserOverride(env);
304
+ if (override !== null) return override;
305
+
306
+ if (!getTerminalInfo(terminalId).hyperlinks) return false;
307
+
308
+ // STY is GNU screen's explicit session marker. It vetoes tmux enabling when
309
+ // multiplexers are nested because screen cannot forward OSC 8 anywhere in the
310
+ // path.
311
+ if (env.STY) return false;
312
+
313
+ // tmux check before TERM heuristics: TMUX is the authoritative current-session
314
+ // signal and supersedes TERM, which may be `screen-256color` under tmux's
315
+ // historical default-terminal setting.
316
+ if (env.TMUX) {
317
+ const version = parseTmuxVersionFromEnv(env);
318
+ if (!version) return false;
319
+ return version.major > 3 || (version.major === 3 && version.minor >= 4);
320
+ }
321
+
322
+ const term = env.TERM?.toLowerCase() ?? "";
323
+ if (term.startsWith("screen")) return false;
324
+ if (term.startsWith("tmux")) return false;
325
+
326
+ return true;
327
+ }
328
+
257
329
  function getFallbackImageProtocol(terminalId: TerminalId): ImageProtocol | null {
258
330
  if (!process.stdout.isTTY) return null;
259
331
  if (terminalId === "vscode" || terminalId === "alacritty") return null;
@@ -268,12 +340,12 @@ const KNOWN_TERMINALS = Object.freeze({
268
340
  base: new TerminalInfo("base", null, false, false, NotifyProtocol.Bell),
269
341
  trueColor: new TerminalInfo("trueColor", null, true, false, NotifyProtocol.Bell),
270
342
  // Recognized terminals
271
- kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99, true, true, true, true),
272
- ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9, true),
273
- wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9, true),
274
- iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9, true),
343
+ kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99, true, true, true),
344
+ ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
345
+ wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
346
+ iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9),
275
347
  vscode: new TerminalInfo("vscode", null, true, true, NotifyProtocol.Bell),
276
- alacritty: new TerminalInfo("alacritty", null, true, true, NotifyProtocol.Bell, true),
348
+ alacritty: new TerminalInfo("alacritty", null, true, true, NotifyProtocol.Bell),
277
349
  });
278
350
 
279
351
  export const TERMINAL_ID: TerminalId = (() => {
@@ -317,65 +389,43 @@ export const TERMINAL_ID: TerminalId = (() => {
317
389
  return "base";
318
390
  })();
319
391
 
320
- /** Clone a {@link TerminalInfo} with selected fields overridden, preserving the rest. */
321
- function withTerminalOverrides(
322
- base: TerminalInfo,
323
- overrides: {
324
- imageProtocol?: ImageProtocol | null;
325
- hyperlinks?: boolean;
326
- eagerEraseScrollbackRisk?: boolean;
327
- deccara?: boolean;
328
- supportsScreenToScrollback?: boolean;
329
- },
330
- ): TerminalInfo {
331
- return new TerminalInfo(
332
- base.id,
333
- overrides.imageProtocol !== undefined ? overrides.imageProtocol : base.imageProtocol,
334
- base.trueColor,
335
- overrides.hyperlinks !== undefined ? overrides.hyperlinks : base.hyperlinks,
336
- base.notifyProtocol,
337
- overrides.eagerEraseScrollbackRisk !== undefined
338
- ? overrides.eagerEraseScrollbackRisk
339
- : base.eagerEraseScrollbackRisk,
340
- overrides.deccara !== undefined ? overrides.deccara : base.deccara,
341
- overrides.supportsScreenToScrollback !== undefined
342
- ? overrides.supportsScreenToScrollback
343
- : base.supportsScreenToScrollback,
344
- );
345
- }
346
-
347
- export const TERMINAL = (() => {
348
- let resolved = getTerminalInfo(TERMINAL_ID);
349
- const eagerEraseScrollbackRisk = detectTerminalEagerEraseScrollbackRisk(Bun.env, process.platform);
350
- if (resolved.eagerEraseScrollbackRisk !== eagerEraseScrollbackRisk) {
351
- resolved = withTerminalOverrides(resolved, { eagerEraseScrollbackRisk });
352
- }
392
+ /**
393
+ * The process-wide {@link TERMINAL} singleton: a {@link TerminalInfo} whose
394
+ * post-construction capabilities — the image protocol and the probe-driven
395
+ * flags — are writable, so the runtime setters and tests mutate them directly
396
+ * instead of through an unsound cast. Every other field stays readonly.
397
+ */
398
+ export interface RuntimeTerminal extends TerminalInfo {
399
+ imageProtocol: ImageProtocol | null;
400
+ hyperlinks: boolean;
401
+ deccara: boolean;
402
+ supportsScreenToScrollback: boolean;
403
+ textSizing: boolean;
404
+ }
405
+
406
+ export const TERMINAL: RuntimeTerminal = (() => {
407
+ const resolved = getTerminalInfo(TERMINAL_ID).clone();
353
408
 
354
409
  const forcedImageProtocol = getForcedImageProtocol();
355
410
  if (forcedImageProtocol !== undefined) {
356
- resolved = withTerminalOverrides(resolved, { imageProtocol: forcedImageProtocol });
411
+ resolved.imageProtocol = forcedImageProtocol;
357
412
  } else if (!resolved.imageProtocol) {
358
413
  const fallbackImageProtocol = getFallbackImageProtocol(resolved.id);
359
- if (fallbackImageProtocol) {
360
- resolved = withTerminalOverrides(resolved, { imageProtocol: fallbackImageProtocol });
361
- }
362
- }
363
- // tmux and screen multiplexers do not reliably forward OSC 8 hyperlinks
364
- // to the outer terminal, so force them off regardless of detected terminal.
365
- const term = Bun.env.TERM?.toLowerCase() ?? "";
366
- if (resolved.hyperlinks && (Bun.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"))) {
367
- resolved = withTerminalOverrides(resolved, { hyperlinks: false });
414
+ if (fallbackImageProtocol) resolved.imageProtocol = fallbackImageProtocol;
368
415
  }
416
+ // Hyperlink (OSC 8) capability. The static per-terminal flag lives on
417
+ // KNOWN_TERMINALS; shouldEnableHyperlinksByDefault folds in runtime context —
418
+ // PROMETHEUS_FORCE_HYPERLINKS / PROMETHEUS_NO_HYPERLINKS overrides plus a tmux>=3.4 gate so
419
+ // modern tmux forwards OSC 8 to outer terminals that opt in via
420
+ // `terminal-features "*:hyperlinks"`.
421
+ resolved.hyperlinks = shouldEnableHyperlinksByDefault(Bun.env, resolved.id);
369
422
  // DECCARA rectangular-SGR background fills. The static per-terminal capability
370
423
  // lives on KNOWN_TERMINALS; here we fold in runtime context — multiplexer and
371
424
  // the PROMETHEUS_NO_DECCARA kill switch via detectRectangularSgrSupport — and force it
372
425
  // off inside the test runtime so the xterm.js-backed virtual terminal (which
373
426
  // ignores DECCARA) exercises the padded-string fallback. Integration tests opt
374
427
  // in explicitly through setTerminalDeccara.
375
- const deccara = detectRectangularSgrSupport(resolved.id, Bun.env) && !isBunTestRuntime();
376
- if (resolved.deccara !== deccara) {
377
- resolved = withTerminalOverrides(resolved, { deccara });
378
- }
428
+ resolved.deccara = detectRectangularSgrSupport(resolved.id, Bun.env) && !isBunTestRuntime();
379
429
  return resolved;
380
430
  })();
381
431
 
@@ -385,18 +435,11 @@ export const TERMINAL = (() => {
385
435
  // glyphs, which is the "ASCII artifact + laggy scrolling" reported in #1877.
386
436
  setKittyGraphics({ unicodePlaceholders: detectKittyUnicodePlaceholdersSupport(TERMINAL.id, Bun.env) });
387
437
 
388
- type MutableTerminalInfo = {
389
- imageProtocol: ImageProtocol | null;
390
- deccara: boolean;
391
- supportsScreenToScrollback: boolean;
392
- textSizing: boolean;
393
- };
394
-
395
438
  /**
396
439
  * Override terminal image protocol at runtime after capability probes complete.
397
440
  */
398
441
  export function setTerminalImageProtocol(imageProtocol: ImageProtocol | null): void {
399
- (TERMINAL as unknown as MutableTerminalInfo).imageProtocol = imageProtocol;
442
+ TERMINAL.imageProtocol = imageProtocol;
400
443
  }
401
444
 
402
445
  /**
@@ -405,12 +448,12 @@ export function setTerminalImageProtocol(imageProtocol: ImageProtocol | null): v
405
448
  * resolved once at import and force-disabled under the test runtime.
406
449
  */
407
450
  export function setTerminalDeccara(enabled: boolean): void {
408
- (TERMINAL as unknown as MutableTerminalInfo).deccara = enabled;
451
+ TERMINAL.deccara = enabled;
409
452
  }
410
453
 
411
454
  /** Override screen-to-scrollback clear support for targeted renderer tests. */
412
455
  export function setTerminalScreenToScrollback(enabled: boolean): void {
413
- (TERMINAL as unknown as MutableTerminalInfo).supportsScreenToScrollback = enabled;
456
+ TERMINAL.supportsScreenToScrollback = enabled;
414
457
  }
415
458
 
416
459
  /**
@@ -419,7 +462,7 @@ export function setTerminalScreenToScrollback(enabled: boolean): void {
419
462
  * capability); tests flip it directly to exercise the scaled-heading path.
420
463
  */
421
464
  export function setTerminalTextSizing(enabled: boolean): void {
422
- (TERMINAL as unknown as MutableTerminalInfo).textSizing = enabled;
465
+ TERMINAL.textSizing = enabled;
423
466
  }
424
467
 
425
468
  export function getTerminalInfo(terminalId: TerminalId): TerminalInfo {
@@ -772,16 +815,11 @@ export function renderImage(
772
815
  if (options.imageId != null) {
773
816
  const placementId = options.placementId ?? options.imageId;
774
817
  const graphics = getKittyGraphics();
775
- // Transmit-once (keyed by id). Prefer a local temp file for PNGs when the
776
- // medium has been promoted; otherwise send in-band base64. Repaints reuse
777
- // the stored image, so the transmit is only emitted when requested.
818
+ // Transmit-once (keyed by id). Repaints reuse the stored image, so the
819
+ // transmit is only emitted when requested.
778
820
  let transmit: string | undefined;
779
821
  if (options.includeTransmit) {
780
- const tempFile =
781
- graphics.transmissionMedium === "temp-file" && isPngBase64(base64Data)
782
- ? encodeKittyTempFileTransmit(base64Data, options.imageId)
783
- : null;
784
- transmit = tempFile ?? encodeKittyTransmit(base64Data, options.imageId);
822
+ transmit = encodeKittyTransmit(base64Data, options.imageId);
785
823
  }
786
824
  // Unicode placeholders render the image as real text cells (which survive
787
825
  // horizontal slicing, reflow and overlaps) instead of a cursor-positioned