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,