feat(dashboard-tui): auto-toggle mouse mode based on focused panel
Default mouse reporting OFF so click-drag selection works by default —
useful for copying the auth token directly off the System panel without
needing the [c] keyboard shortcut. Auto-enable when the user focuses a
panel that consumes wheel events:
- Status mode → on while Logs is focused, off elsewhere
- Interactive → on for Files / Git / Board (which uses wheel in the
task-detail screen), off for Agents / Settings
Implemented as a useEffect in DashboardApp that maps state.activeSection
+ state.mode + state.interactiveView → controller.setMouseEnabled. [M]
remains a manual override; the next focus change reapplies the auto
policy. Controller's start() now honors initial mouseEnabled rather than
unconditionally writing the SGR enable sequence.
Updated System-panel hint to advertise click-drag selection, simplified
the help-overlay [M] entry, and flipped the createInitialState test
expectation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -245,9 +245,9 @@ describe("Default active section", () => {
|
||||
expect(controller.getSnapshot().activeSection).toBe("system");
|
||||
});
|
||||
|
||||
it("createInitialState defaults to mouseEnabled = true", () => {
|
||||
it("createInitialState defaults to mouseEnabled = false (selection-friendly; auto-toggled by panel focus)", () => {
|
||||
const state = createInitialState();
|
||||
expect(state.mouseEnabled).toBe(true);
|
||||
expect(state.mouseEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -269,19 +269,18 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
|
||||
</Box>
|
||||
{isFocused && (
|
||||
// Inline hint row — only shown when the System panel is focused.
|
||||
// Discoverability for Enter / [c] / [M] since mouse-mode blocks
|
||||
// click-drag selection of the token. Single <Text wrap="truncate-end">
|
||||
// so on narrow terminals the hint clips cleanly to one row instead
|
||||
// of wrapping into 2-3 lines and overflowing the SYSTEM_HEIGHT
|
||||
// budget. Disappears when the user moves focus.
|
||||
// Mouse reporting is auto-off here so users can click-drag to
|
||||
// select the token; it auto-toggles on when they focus Logs /
|
||||
// Files / Git / Board for wheel scrolling. [c] is the keyboard
|
||||
// shortcut to copy the token in one keystroke.
|
||||
<Box flexShrink={0}>
|
||||
<Text dimColor wrap="truncate-end">
|
||||
<Text color="cyanBright">[Enter]</Text> open URL
|
||||
{info.authToken ? (
|
||||
<Text> · <Text color="cyanBright">[c]</Text> copy token</Text>
|
||||
) : null}
|
||||
{" · "}
|
||||
<Text color="cyanBright">[M]</Text> mouse {state.mouseEnabled ? "on" : "off (drag to select)"}
|
||||
<Text> · <Text color="cyanBright">[c]</Text> copy token · drag to select</Text>
|
||||
) : (
|
||||
<Text> · drag to select</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -703,8 +702,7 @@ function HelpOverlay() {
|
||||
["[+/-]", "Adjust vitest kill memory threshold (Utilities)"],
|
||||
["[Enter]", "Open dashboard URL in browser (System)"],
|
||||
["[c]", "Copy auth token to clipboard (System)"],
|
||||
["[M]", "Toggle mouse mode (off → click-drag selects text; works under tmux)"],
|
||||
["[Shift+drag]", "Bypass mouse mode to select text (most native terminals; not tmux)"],
|
||||
["[M]", "Manual mouse-mode toggle (auto: on for Logs/Files/Git/Board, off elsewhere)"],
|
||||
["[↑/↓/k/j]", "Navigate list / log entries"],
|
||||
["[Home / G]", "First / last log entry (Logs)"],
|
||||
["[Enter/Space]", "Expand log entry (Logs)"],
|
||||
@@ -4015,6 +4013,26 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
||||
});
|
||||
}, [controller]);
|
||||
|
||||
// Auto-toggle xterm mouse reporting based on whether the focused panel
|
||||
// benefits from wheel scrolling. We default-off so click-drag selection
|
||||
// works (e.g. copying the auth token from the System panel), and switch
|
||||
// on for panels that wire `controller.onWheel` consumers:
|
||||
// - Status mode: Logs panel
|
||||
// - Interactive: Files / Git / Board (task detail uses the wheel too)
|
||||
// Other status panels (System / Stats / Utilities / Settings) leave it
|
||||
// off so the user can select text natively. [M] is still a manual
|
||||
// override, but the next focus change will reapply this policy.
|
||||
const wantsMouse = state.mode === "interactive"
|
||||
? (state.interactiveView === "files"
|
||||
|| state.interactiveView === "git"
|
||||
|| state.interactiveView === "board")
|
||||
: state.activeSection === "logs";
|
||||
useEffect(() => {
|
||||
if (state.mouseEnabled !== wantsMouse) {
|
||||
controller.setMouseEnabled(wantsMouse);
|
||||
}
|
||||
}, [controller, wantsMouse, state.mouseEnabled]);
|
||||
|
||||
// Global QR overlay state — populated when the user hits Ctrl+Q on a
|
||||
// running tunnel. `loading` covers the network request; `error` surfaces
|
||||
// the message inline so the overlay never sits blank.
|
||||
|
||||
@@ -144,11 +144,13 @@ export class DashboardTUI {
|
||||
// requested. (See ink#222 / @zenobius/ink-mouse for prior art.)
|
||||
private wheelHandlers: Set<(direction: "up" | "down") => void> = new Set();
|
||||
private mouseStdinListener: ((chunk: Buffer | string) => void) | null = null;
|
||||
// Tracks the desired mouse-reporting state. Toggled at runtime by the
|
||||
// user (e.g. to allow native click-drag selection under tmux, where
|
||||
// Shift-bypass is intercepted by tmux itself before reaching the
|
||||
// terminal). Default true; only flipped via setMouseEnabled().
|
||||
mouseEnabled: boolean = true;
|
||||
// Tracks the desired mouse-reporting state. Default OFF so click-drag
|
||||
// text selection works out of the box — terminal owns the mouse. The
|
||||
// dashboard auto-enables it via setMouseEnabled() when the user focuses
|
||||
// a panel that uses wheel scrolling (Logs / Files / Git / Board task
|
||||
// detail). [M] is also a manual override, but the next focus change
|
||||
// will reapply the auto policy.
|
||||
mouseEnabled: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.logBuffer = new LogRingBuffer();
|
||||
@@ -665,7 +667,7 @@ export class DashboardTUI {
|
||||
// before raw mode is on, the terminal can deliver the first wheel
|
||||
// report's leading ESC byte alone, which Ink would parse as a bare
|
||||
// Esc keypress (closing modals on every wheel tick).
|
||||
if (process.stdin?.isTTY) {
|
||||
if (process.stdin?.isTTY && this.mouseEnabled) {
|
||||
// Enable xterm mouse reporting with SGR-encoded coordinates.
|
||||
// ?1000h = button press/release reports (includes wheel as
|
||||
// buttons 64/65)
|
||||
@@ -677,6 +679,11 @@ export class DashboardTUI {
|
||||
// terminals plain click+drag) keeps doing native text selection.
|
||||
// Holding Shift always works as a hard override even on terminals
|
||||
// that grab the bare drag gesture.
|
||||
//
|
||||
// Gated by `this.mouseEnabled` so the default-off policy
|
||||
// (selection-friendly on the System panel) holds at startup. The
|
||||
// app component re-enables via setMouseEnabled() as soon as the
|
||||
// user focuses a panel that uses wheel scrolling.
|
||||
process.stdout.write("\x1b[?1000h\x1b[?1006h");
|
||||
this.installMouseListener();
|
||||
}
|
||||
|
||||
@@ -396,6 +396,6 @@ export function createInitialState(): DashboardState {
|
||||
updateStatus: null,
|
||||
clipboardFlash: null,
|
||||
remoteStatus: null,
|
||||
mouseEnabled: true,
|
||||
mouseEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user