fix(tui): preserved visible ANSI content when truncating terminal rows
- Replaced the TUI row truncation path with per-character and ANSI-sequence iteration that tracks visible cells before clipping output. - Added ANSI helpers to parse sequence boundaries and preserve OSC66 visible payloads while enforcing max source length during truncation. - Added natives regression coverage for Ghostty super+alt backspace key matching and parsing.
This commit is contained in:
@@ -14,7 +14,9 @@ import {
|
||||
invalidateFsScanCache,
|
||||
listWorkspace,
|
||||
MacOSPowerAssertion,
|
||||
matchesKey,
|
||||
PtySession,
|
||||
parseKey,
|
||||
summarizeCode,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
@@ -148,6 +150,17 @@ describe("pi-natives", () => {
|
||||
expect(summarizeCode({ path: "fixture.ts", code, minBodyLines: 3 }).elided).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("keys", () => {
|
||||
it("matches Ghostty's super+alt Backspace Kitty wire", () => {
|
||||
const ghosttyOptionBackspace = "\x1b[127;11u";
|
||||
|
||||
expect(matchesKey(ghosttyOptionBackspace, "super+alt+backspace", true)).toBe(true);
|
||||
expect(matchesKey(ghosttyOptionBackspace, "alt+super+backspace", true)).toBe(true);
|
||||
expect(matchesKey(ghosttyOptionBackspace, "alt+backspace", true)).toBe(false);
|
||||
expect(parseKey(ghosttyOptionBackspace, true)).toBe("alt+super+backspace");
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep", () => {
|
||||
it("should find patterns in files", async () => {
|
||||
const result = await grep({
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed the kitty keyboard progressive-enhancement probe to honor the `CSI ? <flags> u` reply even when the terminal answers the DA1 sentinel first. Previously the kitty reply was discarded once the DA1-driven `modifyOtherKeys` fallback engaged, so terminals like Superset/xterm-on-Electron stayed on the fallback and delivered Shift+Enter as a bare `\r` ([#2042](https://github.com/can1357/oh-my-pi/issues/2042)).
|
||||
- Bounded TUI line fitting for oversized raw rows so ANSI-heavy subagent output cannot grow render buffers independently of the viewport ([#2045](https://github.com/can1357/oh-my-pi/issues/2045)).
|
||||
- Bounded TUI line fitting for oversized raw rows so ANSI-heavy subagent output and zero-width-heavy text cannot grow render buffers independently of the viewport or hide visible suffix text ([#2045](https://github.com/can1357/oh-my-pi/issues/2045)).
|
||||
- Fixed tmux offscreen-shrink frames to skip repainting when the visible tail is unchanged, avoiding intermittent blank/refresh flashes in pane terminals ([#2046](https://github.com/can1357/oh-my-pi/issues/2046)).
|
||||
- Fixed Windows ConPTY hosts (Windows Terminal, Tabby, Hyper, VS Code) parking the viewport at the top of a full paint after a `/resume` or any long-session repaint. `ProcessTerminal#safeWrite` now splits oversized writes into ≤ 8 KiB pieces at line boundaries on `win32` and inside WSL (where stdout still crosses ConPTY at the `wslhost` boundary) so each underlying `WriteFile` stays below the ~32 KiB threshold where ConPTY stops tracking the cursor; the data was always delivered, but the host UI's scroll position would not follow until any focus event forced a re-query. ([#2034](https://github.com/can1357/oh-my-pi/issues/2034))
|
||||
|
||||
|
||||
+75
-1
@@ -2515,7 +2515,81 @@ export class TUI extends Container {
|
||||
);
|
||||
if (raw.length <= maxSourceLength) return raw;
|
||||
|
||||
return truncateToWidth(raw, safeWidth, Ellipsis.Omit) + SEGMENT_RESET;
|
||||
let output = "";
|
||||
let cells = 0;
|
||||
for (let i = 0; i < raw.length && cells < safeWidth; ) {
|
||||
if (raw.charCodeAt(i) === 0x1b) {
|
||||
const end = this.#ansiSequenceEnd(raw, i);
|
||||
if (end < 0) break;
|
||||
if (this.#ansiSequenceHasVisiblePayload(raw, i)) {
|
||||
const sequence = raw.slice(i, end);
|
||||
if (output.length + sequence.length <= maxSourceLength) {
|
||||
output += sequence;
|
||||
cells += visibleWidth(sequence);
|
||||
}
|
||||
}
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
const code = raw.charCodeAt(i);
|
||||
const next = code >= 0xd800 && code <= 0xdbff && i + 1 < raw.length ? i + 2 : i + 1;
|
||||
const char = raw.slice(i, next);
|
||||
const charWidth = visibleWidth(char);
|
||||
if (charWidth > 0 && cells + charWidth > safeWidth) break;
|
||||
if (output.length + char.length > maxSourceLength) {
|
||||
if (charWidth > 0) break;
|
||||
i = next;
|
||||
continue;
|
||||
}
|
||||
if (charWidth === 0) {
|
||||
const remainingVisibleCells = safeWidth - cells;
|
||||
const reservedCodeUnits = remainingVisibleCells * 2;
|
||||
if (output.length + char.length > maxSourceLength - reservedCodeUnits) {
|
||||
i = next;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
output += char;
|
||||
cells += charWidth;
|
||||
i = next;
|
||||
}
|
||||
|
||||
return output + SEGMENT_RESET;
|
||||
}
|
||||
|
||||
#ansiSequenceEnd(line: string, start: number): number {
|
||||
const next = line.charCodeAt(start + 1);
|
||||
if (next === 0x5b) {
|
||||
let i = start + 2;
|
||||
while (i < line.length) {
|
||||
const final = line.charCodeAt(i);
|
||||
if (final >= 0x40 && final <= 0x7e) return i + 1;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (next === 0x5d) {
|
||||
let i = start + 2;
|
||||
while (i < line.length) {
|
||||
const osc = line.charCodeAt(i);
|
||||
if (osc === 0x07) return i + 1;
|
||||
if (osc === 0x1b && line.charCodeAt(i + 1) === 0x5c) return i + 2;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return start + 2 <= line.length ? start + 2 : -1;
|
||||
}
|
||||
|
||||
#ansiSequenceHasVisiblePayload(line: string, start: number): boolean {
|
||||
// OSC 66 (`\x1b]66;META;TEXT\x1b\\`) carries visible cells inside the payload.
|
||||
return (
|
||||
line.charCodeAt(start + 1) === 0x5d &&
|
||||
line.charCodeAt(start + 2) === 0x36 &&
|
||||
line.charCodeAt(start + 3) === 0x36 &&
|
||||
line.charCodeAt(start + 4) === 0x3b
|
||||
);
|
||||
}
|
||||
|
||||
#ansiAsciiLineWidth(line: string, maxWidth: number): number | undefined {
|
||||
|
||||
Reference in New Issue
Block a user