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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user