feat(packages/coding-agent): added terminal protocol debug diagnostics

- Added debug menu options for terminal state and protocol checks with new handlers.
- Added terminal diagnostics models and collectors to report geometry, protocol flags, and multiplexer data.
- Added protocol-probe utilities to encode RGB PNGs, generate sample content, and render test panels.
- Added support components for verbatim line rendering and OSC66-scaled text rows.
- Added tests validating image encoding, OSC66 output, and terminal state formatting.
This commit is contained in:
can1357
2026-06-04 00:50:46 +02:00
parent cb6ca2115b
commit 48017b5f67
5 changed files with 591 additions and 1 deletions
@@ -0,0 +1,44 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { getImageDimensions } from "@oh-my-pi/pi-tui";
import { buildLargeTextLines, buildSampleImage, encodeRgbPng } from "../../src/debug/protocol-probe";
import { initTheme } from "../../src/modes/theme/theme";
beforeAll(async () => {
// buildLargeTextLines styles the OSC 66 span through the global theme singleton.
await initTheme();
});
describe("encodeRgbPng / buildSampleImage", () => {
it("emits a PNG whose IHDR dimensions decode back to the requested size", () => {
// The graphics test relies on producing a *valid* PNG: Kitty/iTerm2 hand
// the bytes to the terminal and Sixel decodes them natively, so a malformed
// header (wrong CRC, bad chunk framing) silently breaks the whole panel.
const sample = buildSampleImage(48, 24);
expect(sample.mimeType).toBe("image/png");
expect(sample.dimensions).toEqual({ widthPx: 48, heightPx: 24 });
const decoded = getImageDimensions(sample.base64, "image/png");
expect(decoded).toEqual({ widthPx: 48, heightPx: 24 });
});
it("begins with the PNG signature and round-trips arbitrary pixel sizes", () => {
const png = encodeRgbPng(3, 2, new Uint8Array(3 * 2 * 3));
expect(Array.from(png.subarray(0, 8))).toEqual([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const decoded = getImageDimensions(Buffer.from(png).toString("base64"), "image/png");
expect(decoded).toEqual({ widthPx: 3, heightPx: 2 });
});
});
describe("buildLargeTextLines", () => {
it("encodes each scale as an OSC 66 span and reserves scale-1 blank rows below it", () => {
const lines = buildLargeTextLines([2, 3]);
// scale 2 → 1 reserved row, scale 3 → 2 reserved rows.
expect(lines).toHaveLength(5);
expect(lines[0]).toContain("\x1b]66;s=2;");
expect(lines[1]).toBe("");
expect(lines[2]).toContain("\x1b]66;s=3;");
expect(lines[3]).toBe("");
expect(lines[4]).toBe("");
});
});
@@ -0,0 +1,85 @@
import { describe, expect, it } from "bun:test";
import { TERMINAL } from "@oh-my-pi/pi-tui";
import { collectTerminalState, formatTerminalState, type TerminalStateInfo } from "../../src/debug/terminal-info";
const sample: TerminalStateInfo = {
detectedId: "kitty",
columns: 120,
rows: 40,
cellWidthPx: 9,
cellHeightPx: 18,
trueColor: true,
imageProtocol: "Kitty graphics",
notifyProtocol: "OSC 99 (kitty desktop notifications)",
osc99Confirmed: true,
hyperlinks: false,
deccara: true,
screenToScrollback: true,
eagerEraseScrollbackRisk: true,
synchronizedOutput: false,
multiplexer: null,
env: { TERM: "xterm-kitty", TERM_PROGRAM: undefined, TERM_PROGRAM_VERSION: undefined, COLORTERM: "truecolor" },
};
describe("formatTerminalState", () => {
it("surfaces the negotiated subprotocols and their on/off state", () => {
const out = formatTerminalState(sample);
expect(out).toContain("Detected: kitty");
expect(out).toContain("Graphics: Kitty graphics");
expect(out).toContain("Notify: OSC 99 (kitty desktop notifications) · confirmed via DA");
expect(out).toContain("Hyperlinks: no (OSC 8)");
expect(out).toContain("True color: yes (24-bit SGR)");
expect(out).toContain("DECCARA: yes (rectangular-SGR background fills)");
expect(out).toContain("Sync output: no (DEC 2026)");
});
it("reports geometry, cell size, and the scrollback strategy", () => {
const out = formatTerminalState(sample);
expect(out).toContain("120x40 cells · cell 9x18px");
// supportsScreenToScrollback -> the non-destructive CSI 22 J clear.
expect(out).toContain("Screen->history clear: CSI 22 J");
expect(out).toContain("Eager-erase risk: yes");
});
it("renders the redraw fallback when screen-to-scrollback is unsupported", () => {
const out = formatTerminalState({ ...sample, screenToScrollback: false });
expect(out).toContain("Screen->history clear: CSI 2 J (redraw)");
});
it("drops the OSC-99-confirmed marker when the terminal never answered the probe", () => {
const out = formatTerminalState({ ...sample, osc99Confirmed: false });
expect(out).toContain("Notify: OSC 99 (kitty desktop notifications)");
expect(out).not.toContain("confirmed via DA");
});
it("shows 'none' for no multiplexer and '(unset)' for absent detection vars", () => {
const out = formatTerminalState(sample);
expect(out).toContain("Multiplexer: none");
expect(out).toContain("TERM: xterm-kitty");
expect(out).toContain("COLORTERM: truecolor");
expect(out).toContain("TERM_PROGRAM: (unset)");
});
it("names the multiplexer when one wraps the session", () => {
expect(formatTerminalState({ ...sample, multiplexer: "tmux" })).toContain("Multiplexer: tmux");
});
});
describe("collectTerminalState", () => {
it("passes live geometry through and maps protocols to human-readable names (never raw escapes)", () => {
const info = collectTerminalState({ columns: 88, rows: 25, synchronizedOutput: true });
expect(info.columns).toBe(88);
expect(info.rows).toBe(25);
expect(info.synchronizedOutput).toBe(true);
// The graphics/notify subprotocols are surfaced as readable labels, not the
// enum's underlying escape sequences (\x1b_G, \x1b]99;;, …).
expect(info.imageProtocol).not.toContain("\x1b");
expect(info.notifyProtocol).not.toContain("\x1b");
expect(info.imageProtocol.length).toBeGreaterThan(0);
expect(info.notifyProtocol.length).toBeGreaterThan(0);
// Capability booleans mirror the resolved TERMINAL singleton.
expect(info.deccara).toBe(TERMINAL.deccara);
expect(info.hyperlinks).toBe(TERMINAL.hyperlinks);
expect(info.trueColor).toBe(TERMINAL.trueColor);
});
});