@code-yeongyu/senpi-tui 2026.10.19 → 2026.10.20-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -168,7 +168,8 @@ async function walkDirectoryWithFd(baseDir, fdPath, query, maxResults, signal, m
168
168
  resolve(results);
169
169
  };
170
170
  const onAbort = () => {
171
- if (child.exitCode === null) {
171
+ // A child that never started has no pid; on Node < 24.22 / 26.10 its kill() signals our own process group.
172
+ if (child.pid !== undefined && child.exitCode === null) {
172
173
  child.kill("SIGKILL");
173
174
  }
174
175
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Kitty graphics support probe for multiplexer panes whose environment says
3
+ * nothing about image support (herdr; senpi #3698).
4
+ *
5
+ * A 1x1 RGBA direct-transmission query (`a=q`) is written to the terminal; a
6
+ * terminal that implements the Kitty graphics protocol replies with an APC
7
+ * response naming the query's image id. Silence, an error payload, or a reply
8
+ * for another id all mean "no graphics" — the caller must then keep the text
9
+ * fallback instead of emitting graphics sequences blindly.
10
+ */
11
+ export declare const KITTY_GRAPHICS_PROBE_TIMEOUT_MS = 500;
12
+ /** `AAAA` is base64 for four zero bytes: one 1x1 RGBA pixel. */
13
+ export declare function buildKittyGraphicsQuery(imageId: number): string;
14
+ export type KittyGraphicsProbeOutcome = "ok" | "err";
15
+ /**
16
+ * Classify one APC chunk as the answer to our query. Returns null when the
17
+ * chunk is not a Kitty graphics response for `imageId`, so unrelated APC
18
+ * traffic (graphics commands from other writers) is left untouched.
19
+ */
20
+ export declare function parseKittyGraphicsProbeResponse(data: string, imageId: number): KittyGraphicsProbeOutcome | null;
21
+ //# sourceMappingURL=kitty-graphics-probe.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Kitty graphics support probe for multiplexer panes whose environment says
3
+ * nothing about image support (herdr; senpi #3698).
4
+ *
5
+ * A 1x1 RGBA direct-transmission query (`a=q`) is written to the terminal; a
6
+ * terminal that implements the Kitty graphics protocol replies with an APC
7
+ * response naming the query's image id. Silence, an error payload, or a reply
8
+ * for another id all mean "no graphics" — the caller must then keep the text
9
+ * fallback instead of emitting graphics sequences blindly.
10
+ */
11
+ export const KITTY_GRAPHICS_PROBE_TIMEOUT_MS = 500;
12
+ /** `AAAA` is base64 for four zero bytes: one 1x1 RGBA pixel. */
13
+ export function buildKittyGraphicsQuery(imageId) {
14
+ return `\x1b_Gi=${imageId},s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\`;
15
+ }
16
+ /**
17
+ * Classify one APC chunk as the answer to our query. Returns null when the
18
+ * chunk is not a Kitty graphics response for `imageId`, so unrelated APC
19
+ * traffic (graphics commands from other writers) is left untouched.
20
+ */
21
+ export function parseKittyGraphicsProbeResponse(data, imageId) {
22
+ const start = "\x1b_G";
23
+ const end = "\x1b\\";
24
+ if (!data.startsWith(start) || !data.endsWith(end))
25
+ return null;
26
+ const body = data.slice(start.length, -end.length);
27
+ const separator = body.indexOf(";");
28
+ if (separator === -1)
29
+ return null;
30
+ const keys = body.slice(0, separator);
31
+ const message = body.slice(separator + 1);
32
+ const idMatch = /(?:^|,)i=(\d+)(?:,|$)/.exec(keys);
33
+ if (!idMatch || Number.parseInt(idMatch[1] ?? "", 10) !== imageId)
34
+ return null;
35
+ return message.startsWith("OK") ? "ok" : "err";
36
+ }
37
+ //# sourceMappingURL=kitty-graphics-probe.js.map
@@ -8,6 +8,8 @@ export interface TerminalCapabilities {
8
8
  kittyUnicodePlaceholders?: boolean;
9
9
  /** Shown next to the image placeholder when the terminal could show images after a user action. */
10
10
  imageHint?: string;
11
+ /** Set when image support is still unknown from the environment: the TUI must query the terminal and record the answer via `setProbedImageProtocol`. */
12
+ imageProbe?: "kitty";
11
13
  }
12
14
  export interface DetectedTerminalCapabilities extends TerminalCapabilities {
13
15
  cellDimensions?: {
@@ -1,5 +1,5 @@
1
1
  import type { TerminalColorMode } from "./colors.ts";
2
- import { type DetectedTerminalCapabilities, type TerminalCapabilities } from "./terminal-capabilities.ts";
2
+ import { type DetectedTerminalCapabilities, type ImageProtocol, type TerminalCapabilities } from "./terminal-capabilities.ts";
3
3
  export type { ImageProtocol, TerminalCapabilities } from "./terminal-capabilities.ts";
4
4
  export { outerKittyGraphicsMode } from "./terminal-capabilities.ts";
5
5
  export interface TmuxPassthroughState {
@@ -27,6 +27,8 @@ export interface ImageRenderOptions {
27
27
  }
28
28
  export declare function getCellDimensions(): CellDimensions;
29
29
  export declare function setCellDimensions(dims: CellDimensions): void;
30
+ /** Record the TUI probe's answer for probe-requesting terminals. `undefined` re-arms the probe. */
31
+ export declare function setProbedImageProtocol(protocol: ImageProtocol | undefined): void;
30
32
  export declare function detectCapabilities(tmuxForwardsHyperlink?: () => boolean, tmuxPassthroughState?: () => TmuxPassthroughState): DetectedTerminalCapabilities;
31
33
  export declare function getCapabilities(): TerminalCapabilities;
32
34
  export declare function getTerminalColorMode(capabilities?: TerminalCapabilities): TerminalColorMode;
@@ -7,6 +7,10 @@ import { isOldWindowsWezTerm, WEZTERM_WINDOWS_IMAGES_HINT } from "./wezterm-wind
7
7
  export { outerKittyGraphicsMode } from "./terminal-capabilities.js";
8
8
  let cachedCapabilities = null;
9
9
  let capabilityOverrides = {};
10
+ // Answer of the TUI's Kitty graphics probe, recorded once per process. It
11
+ // outlives cachedCapabilities because focus events reset that cache and
12
+ // re-detect from the environment (senpi #3698).
13
+ let probedImageProtocol;
10
14
  // Default cell dimensions - updated by TUI when terminal responds to query
11
15
  let cellDimensions = { widthPx: 9, heightPx: 18 };
12
16
  export function getCellDimensions() {
@@ -74,6 +78,17 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
74
78
  if (terminalEmulator === "jetbrains-jediterm") {
75
79
  return { images: null, trueColor: true, hyperlinks: false };
76
80
  }
81
+ // herdr panes: the environment says nothing about the outer terminal's graphics support, and herdr
82
+ // itself leaks graphics sequences when the outer terminal cannot render them. Detection defers to
83
+ // the TUI's Kitty graphics probe and latches its answer (senpi #3698).
84
+ if (termProgram === "herdr" || process.env.HERDR_ENV === "1") {
85
+ return {
86
+ images: probedImageProtocol ?? null,
87
+ trueColor: hasTrueColorHint,
88
+ hyperlinks: false,
89
+ ...(probedImageProtocol === undefined ? { imageProbe: "kitty" } : {}),
90
+ };
91
+ }
77
92
  // Windows Terminal does not always set WT_SESSION, for example when it hosts
78
93
  // a cmd.exe launched directly from Win+R. Modern Windows consoles support
79
94
  // truecolor; keep hyperlinks off unless we positively detected support above.
@@ -89,9 +104,17 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
89
104
  function parseBooleanCapabilityOverride(value) {
90
105
  return value === "1" ? true : value === "0" ? false : undefined;
91
106
  }
92
- // The hint explains why detection turned images off; an explicit image choice makes it wrong.
107
+ /** Record the TUI probe's answer for probe-requesting terminals. `undefined` re-arms the probe. */
108
+ export function setProbedImageProtocol(protocol) {
109
+ if (probedImageProtocol === protocol)
110
+ return;
111
+ probedImageProtocol = protocol;
112
+ cachedCapabilities = null;
113
+ }
114
+ // The hint explains why detection turned images off; an explicit image choice makes it wrong. The
115
+ // probe flag is stripped for the same reason: an explicit choice settles the question, no probe needed.
93
116
  function withoutImageHint(caps) {
94
- const { imageHint: _imageHint, ...rest } = caps;
117
+ const { imageHint: _imageHint, imageProbe: _imageProbe, ...rest } = caps;
95
118
  return rest;
96
119
  }
97
120
  export function detectCapabilities(tmuxForwardsHyperlink, tmuxPassthroughState) {
package/dist/tui.d.ts CHANGED
@@ -363,6 +363,7 @@ export declare abstract class TuiBase extends Container {
363
363
  * replies belong to the oldest one. Queries stay here after a timeout to collect late replies.
364
364
  */
365
365
  private pendingTerminalColorQueries;
366
+ private pendingKittyGraphicsProbe?;
366
367
  private terminalColorSchemeListeners;
367
368
  private terminalColorSchemeNotificationsEnabled;
368
369
  /** Directory for debug/crash logs. The fork keeps a concrete default (`~/.senpi/agent`) so PI_DEBUG_REDRAW and crash dumps stay in the agent directory. */
@@ -470,6 +471,9 @@ export declare abstract class TuiBase extends Container {
470
471
  onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void;
471
472
  setTerminalColorSchemeNotifications(enabled: boolean): void;
472
473
  private queryCellSize;
474
+ private probeKittyGraphics;
475
+ private consumeKittyGraphicsProbeResponse;
476
+ private resolveKittyGraphicsProbe;
473
477
  stop(options?: TuiStopOptions): void;
474
478
  /**
475
479
  * While a reply streams, a frame that changes rows above the viewport would replay the whole scrollback
package/dist/tui.js CHANGED
@@ -7,9 +7,10 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { isKeyRelease, matchesKey } from "./keys.js";
10
+ import { buildKittyGraphicsQuery, KITTY_GRAPHICS_PROBE_TIMEOUT_MS, parseKittyGraphicsProbeResponse, } from "./kitty-graphics-probe.js";
10
11
  import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
11
12
  import { parseOscColorResponse, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
12
- import { deleteKittyImage, getCapabilities, isImageLine, resetCapabilitiesCache, setCellDimensions, } from "./terminal-image.js";
13
+ import { allocateImageId, deleteKittyImage, getCapabilities, isImageLine, resetCapabilitiesCache, setCellDimensions, setProbedImageProtocol, } from "./terminal-image.js";
13
14
  import { consumeTmuxFocusEvent, DISABLE_FOCUS_REPORTING, ENABLE_FOCUS_REPORTING } from "./tmux-focus.js";
14
15
  import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
15
16
  import { planViewportInsertScroll } from "./viewport-insert-scroll.js";
@@ -1242,6 +1243,7 @@ export class TuiBase extends Container {
1242
1243
  this.terminal.write("\x1b[?2031h");
1243
1244
  }
1244
1245
  this.queryCellSize();
1246
+ this.probeKittyGraphics();
1245
1247
  this.requestRender();
1246
1248
  }
1247
1249
  addInputListener(listener) {
@@ -1277,6 +1279,40 @@ export class TuiBase extends Container {
1277
1279
  // Response format: CSI 6 ; height ; width t
1278
1280
  this.terminal.write("\x1b[16t");
1279
1281
  }
1282
+ probeKittyGraphics() {
1283
+ if (this.pendingKittyGraphicsProbe)
1284
+ return;
1285
+ if (getCapabilities().imageProbe !== "kitty")
1286
+ return;
1287
+ const imageId = allocateImageId();
1288
+ const timer = setTimeout(() => this.resolveKittyGraphicsProbe(false), KITTY_GRAPHICS_PROBE_TIMEOUT_MS);
1289
+ this.pendingKittyGraphicsProbe = { imageId, timer };
1290
+ this.terminal.write(buildKittyGraphicsQuery(imageId));
1291
+ }
1292
+ consumeKittyGraphicsProbeResponse(data) {
1293
+ const probe = this.pendingKittyGraphicsProbe;
1294
+ if (!probe)
1295
+ return false;
1296
+ const outcome = parseKittyGraphicsProbeResponse(data, probe.imageId);
1297
+ if (outcome === null)
1298
+ return false;
1299
+ this.resolveKittyGraphicsProbe(outcome === "ok");
1300
+ return true;
1301
+ }
1302
+ resolveKittyGraphicsProbe(supported) {
1303
+ const probe = this.pendingKittyGraphicsProbe;
1304
+ if (!probe)
1305
+ return;
1306
+ this.pendingKittyGraphicsProbe = undefined;
1307
+ clearTimeout(probe.timer);
1308
+ setProbedImageProtocol(supported ? "kitty" : null);
1309
+ if (this.stopped)
1310
+ return;
1311
+ if (supported)
1312
+ this.queryCellSize();
1313
+ this.invalidate();
1314
+ this.requestRender();
1315
+ }
1280
1316
  stop(options = {}) {
1281
1317
  this.stopped = true;
1282
1318
  // The hold itself belongs to the turn (interactive mode sets and releases it); a stop()/start() handover
@@ -1287,6 +1323,10 @@ export class TuiBase extends Container {
1287
1323
  this.renderRequested = false;
1288
1324
  this.inputRenderPending = false;
1289
1325
  this.cancelRenderTimer();
1326
+ if (this.pendingKittyGraphicsProbe) {
1327
+ clearTimeout(this.pendingKittyGraphicsProbe.timer);
1328
+ this.pendingKittyGraphicsProbe = undefined;
1329
+ }
1290
1330
  if (this.terminalColorSchemeNotificationsEnabled) {
1291
1331
  this.terminal.write("\x1b[?2031l");
1292
1332
  }
@@ -1493,6 +1533,9 @@ export class TuiBase extends Container {
1493
1533
  if (this.consumeTerminalColorSchemeReport(data)) {
1494
1534
  return;
1495
1535
  }
1536
+ if (this.consumeKittyGraphicsProbeResponse(data)) {
1537
+ return;
1538
+ }
1496
1539
  // A key press means the user is at the bottom again. Terminal reports (mouse/wheel, OSC/DCS replies,
1497
1540
  // DEC private reports, window and cell-size reports) are not the user and must not trigger it.
1498
1541
  if (!isTerminalReport(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-tui",
3
- "version": "2026.10.19",
3
+ "version": "2026.10.20-1",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",