diff --git a/.changeset/tui-raw-logs.md b/.changeset/tui-raw-logs.md new file mode 100644 index 0000000000..f95c6f3721 --- /dev/null +++ b/.changeset/tui-raw-logs.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Press [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. diff --git a/packages/cli/src/commands/dashboard-tui/app.tsx b/packages/cli/src/commands/dashboard-tui/app.tsx index d1650f4cca..216253f8df 100644 --- a/packages/cli/src/commands/dashboard-tui/app.tsx +++ b/packages/cli/src/commands/dashboard-tui/app.tsx @@ -770,6 +770,34 @@ function LogsPanel({ ); } +/* +FNXC:TuiRawLogs 2026-08-26-14:10: +Logs, and nothing else: no panel border, no title, no filter row, no header, no status bar. A +rectangular terminal selection over the normal view captures box-drawing characters and neighbouring +rows, which is what makes a copied log unusable; here every line is plain text starting at column 0. +Mouse reporting is released alongside it (see `wantsMouse`), because it is enabled for wheel +scrolling on this panel and otherwise swallows the click-drag entirely. +The timestamp/level/prefix shape matches the `[c]` single-line copy, so a mouse selection and a +keyboard copy produce the same text. +*/ +function RawLogs({ state, rows }: { state: DashboardState; rows: number }) { + const entries = state.logEntries.filter((entry) => + state.logsSeverityFilter === "all" || entry.level === state.logsSeverityFilter); + const visible = entries.slice(Math.max(0, entries.length - Math.max(1, rows))); + return ( + + {visible.map((entry, index) => { + const prefix = entry.prefix ? `[${entry.prefix}] ` : ""; + return ( + + {`${formatTimestamp(entry.timestamp)} ${entry.level.toUpperCase()} ${prefix}${stripLeadingDetailedTimestamp(entry.message)}`} + + ); + })} + + ); +} + function ExpandedLog({ entry, index, @@ -861,6 +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)")], ["[r]", t("tui.helpShortcutRefreshStats", "Refresh stats (Utilities)")], ["[c]", t("tui.helpShortcutClearLogs", "Clear logs (Utilities)")], ["[k]", t("tui.helpShortcutKillVitest", "Kill all vitest processes (Utilities)")], @@ -1031,6 +1060,7 @@ function StatusModeSingle({ state: DashboardState; controller: DashboardTUI; }) { + const { t } = useTranslation("cli"); const focused = state.activeSection; const { stdout } = useStdout(); const rows = stdout?.rows ?? 24; @@ -1079,6 +1109,24 @@ function StatusModeSingle({ } }; + /* + FNXC:TuiRawLogs 2026-08-26-14:10: + Raw mode replaces the WHOLE frame, status bar included. Leaving any chrome on screen defeats the + point: a rectangular drag would still capture it, and the borders are precisely what made copied + 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 ( @@ -4269,7 +4317,8 @@ export function DashboardApp({ controller }: DashboardAppProps) { ? (state.interactiveView === "files" || state.interactiveView === "git" || state.interactiveView === "board") - : (statusLogsFocused && !state.logsExpandedMode); + /* FNXC:TuiRawLogs 2026-08-26-14:10: raw mode exists to allow native click-drag, so it must release the mouse. */ + : (statusLogsFocused && !state.logsExpandedMode && !state.logsRawMode); useEffect(() => { if (state.mouseEnabled !== wantsMouse) { controller.setMouseEnabled(wantsMouse); @@ -4572,7 +4621,9 @@ export function DashboardApp({ controller }: DashboardAppProps) { const filteredEntries = controller.getFilteredLogEntries(); if (key.escape) { - if (state.logsExpandedMode) { + if (state.logsRawMode) { + controller.setLogsRawMode(false); + } else if (state.logsExpandedMode) { controller.setLogsExpandedMode(false); controller.setShowHelp(false); } else if (state.showHelp) { @@ -4591,6 +4642,18 @@ export function DashboardApp({ controller }: DashboardAppProps) { return; } + /* + FNXC:TuiRawLogs 2026-08-26-14:10: + [v] 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") { + controller.setLogsRawMode(!state.logsRawMode); + if (!state.logsRawMode) controller.setShowHelp(false); + return; + } + if (input === "w" || input === "W") { controller.setLogsWrapEnabled(!state.logsWrapEnabled); return; diff --git a/packages/cli/src/commands/dashboard-tui/controller.ts b/packages/cli/src/commands/dashboard-tui/controller.ts index 768cd41e01..0edabbb043 100644 --- a/packages/cli/src/commands/dashboard-tui/controller.ts +++ b/packages/cli/src/commands/dashboard-tui/controller.ts @@ -68,6 +68,15 @@ export class DashboardTUI { logsSeverityFilter: "all" | LogEntry["level"] = "all"; logsWrapEnabled = false; logsExpandedMode = false; + /* + FNXC:TuiRawLogs 2026-08-26-14:10: + Raw mode renders the log lines with NO chrome and releases mouse reporting, so a terminal + click-drag selects clean text. Every other view keeps borders, a title and a filter row, and the + panel sits between a header and a status bar 2014 a rectangular drag therefore captures box-drawing + characters and unrelated rows, which is exactly what makes copied logs unusable. Mouse reporting is + the other half: it is enabled for wheel scrolling on this panel and swallows the drag entirely. + */ + logsRawMode = false; selectedLogIndex = 0; logsViewportStart = 0; // True when the narrow single-pane view is split and the bottom log @@ -202,6 +211,7 @@ export class DashboardTUI { logsSeverityFilter: this.logsSeverityFilter, logsWrapEnabled: this.logsWrapEnabled, logsExpandedMode: this.logsExpandedMode, + logsRawMode: this.logsRawMode, selectedLogIndex: this.selectedLogIndex, logsViewportStart: this.logsViewportStart, narrowLogSplitFocused: this.narrowLogSplitFocused, @@ -559,6 +569,11 @@ export class DashboardTUI { this.notify(); } + setLogsRawMode(raw: boolean): void { + this.logsRawMode = raw; + this.notify(); + } + setNarrowLogSplitFocused(focused: boolean): void { if (this.narrowLogSplitFocused === focused) return; this.narrowLogSplitFocused = focused; diff --git a/packages/cli/src/commands/dashboard-tui/state.ts b/packages/cli/src/commands/dashboard-tui/state.ts index fc51c81d36..52d5871dfb 100644 --- a/packages/cli/src/commands/dashboard-tui/state.ts +++ b/packages/cli/src/commands/dashboard-tui/state.ts @@ -363,6 +363,8 @@ export interface DashboardState { logsSeverityFilter: "all" | LogEntry["level"]; logsWrapEnabled: boolean; logsExpandedMode: boolean; + /* FNXC:TuiRawLogs 2026-08-26-14:10: chrome-free, mouse-released logs view for native text selection. */ + logsRawMode: boolean; selectedLogIndex: number; logsViewportStart: number; // When the narrow single-pane main view is split horizontally to show a @@ -412,6 +414,7 @@ export function createInitialState(): DashboardState { logsSeverityFilter: "all", logsWrapEnabled: false, logsExpandedMode: false, + logsRawMode: false, selectedLogIndex: 0, logsViewportStart: 0, narrowLogSplitFocused: false,