feat(FN-5615): add dynamic system panel height to dashboard-tui

FN-5615 improves TUI token visibility by dynamically sizing the system panel height and clarifying the manual token copy hint, with regression tests added in the dashboard-tui app and a changeset for the patch release.

Fusion-Task-Id: FN-5615

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5615
This commit is contained in:
gsxdsm
2026-05-27 12:53:48 -07:00
parent 8a0fbf00b9
commit d76b6f9503
3 changed files with 59 additions and 12 deletions

View File

@@ -661,6 +661,50 @@ describe("LogsPanel indicator", () => {
});
});
describe("SystemPanel token visibility", () => {
it("keeps full auth token visible in narrow terminals", async () => {
const controller = newController();
const authToken = "fn_abcdef0123456789abcdef0123456789";
controller.setSystemInfo({ ...makeSystemInfo(), authEnabled: true, authToken });
const rendered = render(renderDashboardAppNode(controller));
setTerminalSize(rendered, 60, 24);
rendered.rerender(renderDashboardAppNode(controller));
await waitForFrameContains(rendered.lastFrame, authToken);
rendered.unmount();
});
it("shows URL and full token in wide terminals", async () => {
const controller = newController();
const authToken = "fn_abcdef0123456789abcdef0123456789";
controller.setSystemInfo({ ...makeSystemInfo(), authEnabled: true, authToken });
const rendered = render(renderDashboardAppNode(controller));
setTerminalSize(rendered, 160, 28);
rendered.rerender(renderDashboardAppNode(controller));
await waitForFrameContains(rendered.lastFrame, "http://localhost:4040");
await waitForFrameContains(rendered.lastFrame, authToken);
rendered.unmount();
});
it("hides token row and keeps no-token hint when auth token is absent", async () => {
const controller = newController();
controller.setSystemInfo({ ...makeSystemInfo(), authEnabled: false, authToken: undefined });
const rendered = render(renderDashboardAppNode(controller));
setTerminalSize(rendered, 120, 24);
rendered.rerender(renderDashboardAppNode(controller));
await waitForFrameContains(rendered.lastFrame, "[Enter]");
const frame = rendered.lastFrame() ?? "";
expect(frame).not.toContain("Token");
expect(frame).toContain("open URL · drag to select");
rendered.unmount();
});
});
describe("StatusModeGrid layout stability", () => {
it("keeps System and Logs panel anchors stable as log content length changes", async () => {
const controller = newController();

View File

@@ -366,7 +366,7 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
<Text dimColor wrap="truncate-end">
<Text color="cyanBright">[Enter]</Text> open URL
{info.authToken ? (
<Text> · <Text color="cyanBright">[c]</Text> copy token · drag to select</Text>
<Text> · <Text color="cyanBright">[c]</Text> copy token · select token text to copy manually</Text>
) : (
<Text> · drag to select</Text>
)}
@@ -851,16 +851,14 @@ function StatusModeGrid({
const rows = stdout?.rows ?? 24;
const cols = stdout?.columns ?? 80;
// Middle area = rows - header(1) - statusbar(1) = rows-2 (no top spacer).
// System fixed at 4 rows. Bottom row scales with available space.
// Logs fills what remains.
// Bottom row scales with available space; Logs fills what remains.
const middleHeight = Math.max(1, rows - 2);
// System panel is normally 4 rows (border 2 + 2 content rows so chips wrap to
// a second line). When auth is on, the Token chip is long enough that it
// routinely wraps to a third row; bump to 5 so it isn't clipped.
// Add one extra row when the System panel is focused so the inline
// [Enter]/[c]/[M] hint isn't clipped against Logs.
const systemFocused = focused === "system";
const SYSTEM_HEIGHT = (state.systemInfo?.authToken ? 5 : 4) + (systemFocused ? 1 : 0);
// Match the same intrinsic sizing logic used by single-pane mode so URL/token
// wrapping at narrow widths cannot clip in the grid layout.
const systemContentRows = estimateSystemContentRows(state.systemInfo, cols, systemFocused);
// Grid Panel chrome: border (2) + title row (1).
const SYSTEM_HEIGHT = systemContentRows + 3 + (systemFocused ? 1 : 0);
const bottomShare = Math.min(10, Math.max(6, Math.floor(middleHeight * 0.35)));
const logsShare = Math.max(1, middleHeight - SYSTEM_HEIGHT - bottomShare);
// LogsPanel chrome: border 2 + title 1 + filter 1 = 4.
@@ -882,9 +880,9 @@ function StatusModeGrid({
same terminal row as the header overlay (covered by it), same
tradeoff as StatusModeSingle. */}
<Box flexDirection="column" flexGrow={1} overflow="hidden">
{/* System: full width, pinned to 4 rows tall (border 2 + 2 content
rows so the chips always have room to wrap to a second line if
needed). flexShrink=0 so it never shrinks below this height. */}
{/* System: full width, dynamically sized from intrinsic content rows
so wrapped URL/token lines remain visible. flexShrink=0 so it never
shrinks below this computed height. */}
<Box height={SYSTEM_HEIGHT} flexShrink={0} overflow="hidden">
<SystemPanel state={state} isFocused={focused === "system"} />
</Box>