fix(cli): make the raw logs view actually reachable, and stop it shadowing [v]
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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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("<StatusModeGrid");
|
||||
const singleRender = source.indexOf("<StatusModeSingle");
|
||||
expect(rawReturn, "raw mode must be reachable").toBeGreaterThan(-1);
|
||||
expect(gridRender).toBeGreaterThan(-1);
|
||||
expect(rawReturn, "the escape must precede the grid layout").toBeLessThan(gridRender);
|
||||
expect(rawReturn, "the escape must precede the single-pane layout").toBeLessThan(singleRender);
|
||||
|
||||
// Exactly one place may render it: a second copy inside a layout is how the grid was missed.
|
||||
expect(source.split("<RawLogs ").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
it("releases mouse reporting so a click-drag reaches the terminal", async () => {
|
||||
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"));
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<RawLogs state={state} rows={Math.max(1, rows - 1)} />
|
||||
<Box flexShrink={0}>
|
||||
<Text dimColor>{t("tui.rawLogsHint", "[v/Esc] leave raw logs · select with the mouse to copy")}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
||||
@@ -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 (
|
||||
<Box key={layoutKey} flexDirection="column" height={rows} width={cols} overflow="hidden">
|
||||
<RawLogs state={state} rows={Math.max(1, rows - 1)} />
|
||||
<Box height={1} flexShrink={0}>
|
||||
<Text dimColor>{t("tui.rawLogsHint", "[V/Esc] leave raw logs · select with the mouse to copy")}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={layoutKey} flexDirection="column" height={rows} width={cols} overflow="hidden">
|
||||
{/* Header: explicit height={1} so the wrapper always reserves row 0. */}
|
||||
|
||||
Reference in New Issue
Block a user