From 47d2890031670af67d4c7b8674ead110bf472cd6 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Thu, 27 Aug 2026 00:48:09 +0000 Subject: [PATCH] fix(cli): make the raw logs view actually reachable, and stop it shadowing [v] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects in my own previous commit, both visible in one screenshot of the dashboard. IT DID NOTHING ON A WIDE TERMINAL. The escape was added inside the single-pane layout, which is the NARROW one. A wide terminal renders the grid layout instead — System / Stats / Utilities / Settings beside Logs — so the toggle flipped state and the screen did not change. The whole point is that no chrome survives a rectangular selection, and chrome is drawn by both layouts plus the header, so the escape has to happen before either is chosen. It now replaces the entire frame, above the layout choice, and a test pins that ordering plus the fact that only one place may render it. IT SHADOWED AN EXISTING LEGEND. The Utilities panel already advertises `[v] Auto-Kill Vitest` on the same screen. The two handlers are mutually exclusive at runtime — utility actions require the Utilities section, this branch requires log focus — so nothing actually clashed, but two different `[v]` legends visible at once is a UI anyone would misread. Raw mode is now Shift+V; lowercase v stays with auto-kill. pnpm lint 0 errors, CLI typecheck clean, dashboard-tui suites 138/138, test:gate green. --- .changeset/tui-raw-logs.md | 4 +- .../__tests__/raw-logs-mode.test.ts | 61 +++++++++++++++++++ .../cli/src/commands/dashboard-tui/app.tsx | 46 +++++++++----- 3 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 packages/cli/src/commands/dashboard-tui/__tests__/raw-logs-mode.test.ts diff --git a/.changeset/tui-raw-logs.md b/.changeset/tui-raw-logs.md index f95c6f3721..ccbd65df1d 100644 --- a/.changeset/tui-raw-logs.md +++ b/.changeset/tui-raw-logs.md @@ -2,6 +2,6 @@ "@runfusion/fusion": minor --- -summary: Press [v] in the TUI Logs panel for a chrome-free view you can select and copy with the mouse. +summary: Press [Shift+V] in the TUI Logs panel for a chrome-free view you can select and copy with the mouse. category: feature -dev: The Logs panel already fills the pane when focused, but keeps a panel border, title, filter row, header and status bar, so a rectangular terminal drag captures box-drawing characters and neighbouring rows; mouse reporting is also enabled there for wheel scrolling and swallows the drag entirely. New `logsRawMode` (controller + state) renders only plain log lines starting at column 0, replaces the whole frame except one trailing hint row, and is excluded from `wantsMouse` so native click-drag selection works. Toggled with `[v]`, left with `[v]` or Esc (ordered ahead of the expanded-entry escape), and listed in the help overlay. Line shape matches the existing `[c]` single-line copy, so a mouse selection and a keyboard copy produce identical text. +dev: The Logs panel keeps a border, title and filter row and sits between a header and a status bar, so a rectangular terminal drag captures box-drawing characters and neighbouring rows; mouse reporting is also enabled there for wheel scrolling and swallows the drag entirely. New `logsRawMode` (controller + state) renders only plain log lines starting at column 0, replaces the whole frame above the narrow/grid layout choice — so it works on wide terminals, where the grid layout is used — keeps one trailing hint row, and is excluded from `wantsMouse` so native click-drag works. Bound to `Shift+V` because the Utilities panel already advertises `[v] Auto-Kill Vitest` on the same screen; Esc clears raw mode ahead of the expanded-entry escape. Line shape matches the existing `[c]` single-line copy so mouse and keyboard copies produce identical text. Covered by `raw-logs-mode.test.ts`, which pins the above-layout escape, the mouse release, the binding, and the exit hint. diff --git a/packages/cli/src/commands/dashboard-tui/__tests__/raw-logs-mode.test.ts b/packages/cli/src/commands/dashboard-tui/__tests__/raw-logs-mode.test.ts new file mode 100644 index 0000000000..78e254f5f0 --- /dev/null +++ b/packages/cli/src/commands/dashboard-tui/__tests__/raw-logs-mode.test.ts @@ -0,0 +1,61 @@ +/* +FNXC:TuiRawLogs 2026-08-26-14:40: +Raw mode exists so a terminal click-drag can select log text cleanly. Two properties make that true, +and both were wrong in the first attempt: + + - it must replace the WHOLE frame, above the layout choice. It was added inside the single-pane + layout only, which renders on a NARROW terminal; a wide terminal draws the grid layout instead + (System / Stats / Utilities / Settings beside Logs), so the toggle flipped state and nothing moved + on screen. + - it must release mouse reporting, which this panel enables for wheel scrolling and which swallows + a click-drag before the terminal ever sees it. + +And the binding is SHIFT+V because the Utilities panel already advertises `[v] Auto-Kill Vitest` on +the same screen; the handlers are mutually exclusive at runtime, but two `[v]` legends at once is a UI +anyone would misread. +*/ +import { describe, expect, it } from "vitest"; +import { readFile } from "node:fs/promises"; + +const appSource = () => readFile(new URL("../app.tsx", import.meta.url), "utf8"); + +describe("TUI raw logs mode", () => { + it("escapes above the layout choice, so it works on wide and narrow terminals alike", async () => { + const source = await appSource(); + + const rawReturn = source.indexOf("state.mode === \"status\" && state.logsRawMode"); + // The grid is the WIDE layout, and the one the first attempt never reached. + const gridRender = source.indexOf(" { + const source = await appSource(); + expect(source).toContain("!state.logsExpandedMode && !state.logsRawMode"); + }); + + it("binds SHIFT+V, leaving lowercase v to the Utilities auto-kill toggle", async () => { + const source = await appSource(); + + expect(source).toContain('if (input === "V") {'); + // The lowercase legend the Utilities panel owns must stay untouched. + expect(source).toContain('{ key: "v", label: t("tui.utilitiesAutoKillVitest"'); + // And the raw-mode handler must not claim lowercase v. + expect(source).not.toContain('if (input === "v" || input === "V") {\n controller.setLogsRawMode'); + }); + + it("leaves a visible way out", async () => { + const source = await appSource(); + expect(source).toContain("tui.rawLogsHint"); + // Esc must clear raw mode before the expanded-entry escape, or the two modes fight. + const escapeBlock = source.slice(source.indexOf("if (key.escape) {", source.indexOf("logsFocused"))); + expect(escapeBlock.indexOf("state.logsRawMode")).toBeLessThan(escapeBlock.indexOf("state.logsExpandedMode")); + }); +}); diff --git a/packages/cli/src/commands/dashboard-tui/app.tsx b/packages/cli/src/commands/dashboard-tui/app.tsx index 216253f8df..9138bb0c35 100644 --- a/packages/cli/src/commands/dashboard-tui/app.tsx +++ b/packages/cli/src/commands/dashboard-tui/app.tsx @@ -889,7 +889,7 @@ function HelpOverlay() { ["[→] / [↓] / [n]", t("tui.helpShortcutNextPanel", "Next panel (Main; ↑/↓ scroll on Logs)")], ["[←] / [↑] / [p]", t("tui.helpShortcutPrevPanel", "Previous panel (Main; ↑/↓ scroll on Logs)")], ["[Enter]", t("tui.helpShortcutExpandLog", "Expand log + release mouse for text selection (Logs)")], - ["[v]", t("tui.helpShortcutRawLogs", "Raw logs: no borders, mouse released — select a range to copy (Logs)")], + ["[V]", t("tui.helpShortcutRawLogs", "Raw logs: no borders, mouse released — select a range to copy (Logs)")], ["[r]", t("tui.helpShortcutRefreshStats", "Refresh stats (Utilities)")], ["[c]", t("tui.helpShortcutClearLogs", "Clear logs (Utilities)")], ["[k]", t("tui.helpShortcutKillVitest", "Kill all vitest processes (Utilities)")], @@ -1060,7 +1060,6 @@ function StatusModeSingle({ state: DashboardState; controller: DashboardTUI; }) { - const { t } = useTranslation("cli"); const focused = state.activeSection; const { stdout } = useStdout(); const rows = stdout?.rows ?? 24; @@ -1116,17 +1115,6 @@ function StatusModeSingle({ logs unusable. One trailing hint line stays, because a full-screen view with no visible way out is worse than one extra row. */ - if (state.logsRawMode) { - return ( - - - - {t("tui.rawLogsHint", "[v/Esc] leave raw logs · select with the mouse to copy")} - - - ); - } - return ( @@ -4643,12 +4631,17 @@ export function DashboardApp({ controller }: DashboardAppProps) { } /* - FNXC:TuiRawLogs 2026-08-26-14:10: - [v] shows the logs alone, chrome-free and mouse-released, so a terminal click-drag copies clean + FNXC:TuiRawLogs 2026-08-26-14:40: + SHIFT+V, not [v]: the Utilities panel already advertises `[v] Auto-Kill Vitest` on the same + screen. The two handlers are mutually exclusive at runtime (utility actions require the + Utilities section, this branch requires log focus), so there is no functional clash — but two + different `[v]` legends visible at once is a UI anyone would misread. + + Shows the logs alone, chrome-free and mouse-released, so a terminal click-drag copies clean text. `[c]` already copies ONE selected line; this is the path for copying a range, which no keyboard shortcut can express. Esc returns (handled with the other escapes above). */ - if (input === "v" || input === "V") { + if (input === "V") { controller.setLogsRawMode(!state.logsRawMode); if (!state.logsRawMode) controller.setShowHelp(false); return; @@ -4753,6 +4746,27 @@ export function DashboardApp({ controller }: DashboardAppProps) { hasSystemInfo: Boolean(state.systemInfo), }); + /* + FNXC:TuiRawLogs 2026-08-26-14:40: + Raw mode replaces the ENTIRE frame, above the layout choice — header included. + + It was first added inside the single-pane layout only, which is the narrow one. On a wide terminal + the dashboard renders the GRID layout instead (System / Stats / Utilities / Settings beside Logs), + so the toggle changed state and nothing moved on screen. The point of this mode is that NO chrome + survives a rectangular selection, and chrome is drawn by both layouts plus the header — so the + escape has to happen before either is chosen. + */ + if (state.mode === "status" && state.logsRawMode) { + return ( + + + + {t("tui.rawLogsHint", "[V/Esc] leave raw logs · select with the mouse to copy")} + + + ); + } + return ( {/* Header: explicit height={1} so the wrapper always reserves row 0. */}