fix(FN-2722): simplify dashboard TUI header and panel switching behavior

- Remove stdout header overlay patching and related frame recovery hooks from the dashboard TUI controller
- Keep panel switching logic lightweight by relying on normal state updates without forced recover cycles
- Adjust status footer and utility/system panel rendering to maintain a single-line, truncation-friendly layout
- Add explicit number-key panel mapping while preserving existing tab/cycle panel order
This commit is contained in:
Fusion
2026-04-28 08:24:24 -07:00
committed by gsxdsm
parent 25ffd1ae84
commit 4a56b1c10f
2 changed files with 43 additions and 186 deletions

View File

@@ -96,9 +96,6 @@ export class DashboardTUI {
} & Record<string, unknown> | null = null;
// Resize listener attached at start(), detached at stop().
private resizeListener: (() => void) | null = null;
// Original process.stdout.write before we patch it for the header overlay.
// Stored so stop() can restore it.
private originalStdoutWrite: typeof process.stdout.write | null = null;
// Debounce timer for resize handling — coalesces tmux/ssh resize bursts.
private resizeDebounceTimer: ReturnType<typeof setTimeout> | null = null;
// Last observed terminal dims, used by the dim-poll fallback to detect
@@ -429,17 +426,8 @@ export class DashboardTUI {
// ── State helpers called from Ink App ────────────────────────────────────
setActiveSection(section: SectionId): void {
const changed = this.activeSection !== section;
this.activeSection = section;
this.showHelp = false;
if (changed) {
// Brute-force: wipe alt-screen + reset Ink's log-update tracking before
// rendering the new section. Some sections (Logs, Utilities) appear to
// leave residual state in tmux that scrolls the header off the top on
// the first render after switching. A full recover here is overkill on
// every switch but eliminates the artifact.
this.recoverFrameNow();
}
this.notify();
}
@@ -478,7 +466,6 @@ export class DashboardTUI {
const idx = SECTION_ORDER.indexOf(this.activeSection);
this.activeSection = SECTION_ORDER[(idx + direction + SECTION_ORDER.length) % SECTION_ORDER.length];
this.showHelp = false;
this.recoverFrameNow();
this.notify();
}
@@ -563,7 +550,7 @@ export class DashboardTUI {
// that only exercise pure logic).
const { render } = await import("ink");
const { createElement } = await import("react");
const { DashboardApp, buildHeaderAnsiLine } = await import("./app.js");
const { DashboardApp } = await import("./app.js");
// Enter the terminal's alternate-screen buffer before mounting Ink so
// the TUI gets a dedicated fullscreen surface that doesn't share
@@ -577,45 +564,6 @@ export class DashboardTUI {
process.stdout.write("\x1b[?1049h\x1b[H");
}
// Header overlay failsafe — paint the header at terminal row 1 after
// every Ink frame write. The Ink/Yoga layout is correct in synthetic
// tests, but in real tmux at narrow widths the header still goes
// missing — likely log-update line-tracking drift over many state
// updates. The overlay is a guaranteed-paint-on-top write that's
// independent of Ink's output, with cursor save/restore so log-update
// tracking is preserved.
if (process.stdout?.isTTY && typeof process.stdout.write === "function") {
const original = process.stdout.write.bind(process.stdout) as
((...args: unknown[]) => boolean);
this.originalStdoutWrite = process.stdout.write;
let inOverlay = false;
const overlay = (text: string): void => {
if (inOverlay) return;
if (typeof text !== "string" || text.length < 200) return;
const snapshot = this.getSnapshot();
// Skip overlay during splash/loading (before systemInfo arrives) so
// the splash screen has clean unobstructed real estate.
if (!snapshot.systemInfo) return;
const cols = process.stdout.columns ?? 0;
if (cols <= 0) return;
try {
inOverlay = true;
const headerLine = buildHeaderAnsiLine(snapshot, cols);
// \x1b7 = DECSC (save cursor + attrs), \x1b[1;1H = move to terminal
// row 1 col 1, header, \x1b8 = DECRC (restore cursor + attrs).
original(`\x1b7\x1b[1;1H${headerLine}\x1b8`);
} finally {
inOverlay = false;
}
};
const patched = (...args: unknown[]): boolean => {
const result = original(...args);
overlay(args[0] as string);
return result;
};
(process.stdout as unknown as { write: unknown }).write = patched;
}
this.inkInstance = render(
createElement(DashboardApp, { controller: this }),
);
@@ -713,16 +661,6 @@ export class DashboardTUI {
// visible at the bottom. \x1b[2J\x1b[H wipes the buffer first.
// Order: wipe → reset Ink's tracking → record dims → notify so React
// reads fresh dims and rerenders cleanly.
// Public version: callable from state-changing methods (e.g. switching
// panels). Reads current dims from process.stdout and runs the same
// wipe + ink-clear sequence as the resize-driven path.
recoverFrameNow(): void {
const cols = process.stdout?.columns ?? 0;
const rows = process.stdout?.rows ?? 0;
if (cols <= 0 || rows <= 0) return;
this.recoverFrame(cols, rows);
}
private recoverFrame(cols: number, rows: number): void {
tuiDebug("recoverFrame", {
cols,
@@ -774,12 +712,6 @@ export class DashboardTUI {
this.inkInstance.unmount();
this.inkInstance = null;
}
// Restore original stdout.write (undo the header-overlay patch).
if (this.originalStdoutWrite) {
(process.stdout as unknown as { write: typeof process.stdout.write }).write =
this.originalStdoutWrite;
this.originalStdoutWrite = null;
}
// Leave the alt-screen buffer last so the user's shell scrollback
// is restored cleanly. \x1b[?1049l = leave alt-screen.
if (process.stdout?.isTTY && typeof process.stdout.write === "function") {