diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 7bc0fb2e09d..891b6128e3c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2789,6 +2789,23 @@ def _(rid, params: dict) -> dict: _write_config_key("display.tui_statusbar", nv) return _ok(rid, {"key": key, "value": nv}) + if key == "mouse": + raw = str(value or "").strip().lower() + display = _load_cfg().get("display") if isinstance(_load_cfg().get("display"), dict) else {} + current = bool(display.get("tui_mouse", True)) + + if raw in ("", "toggle"): + nv = not current + elif raw == "on": + nv = True + elif raw == "off": + nv = False + else: + return _err(rid, 4002, f"unknown mouse value: {value}") + + _write_config_key("display.tui_mouse", nv) + return _ok(rid, {"key": key, "value": "on" if nv else "off"}) + if key in ("prompt", "personality", "skin"): try: cfg = _load_cfg() @@ -2917,6 +2934,10 @@ def _(rid, params: dict) -> dict: display.get("tui_statusbar", "top") if isinstance(display, dict) else "top" ) return _ok(rid, {"value": _coerce_statusbar(raw)}) + if key == "mouse": + display = _load_cfg().get("display") + on = display.get("tui_mouse", True) if isinstance(display, dict) else True + return _ok(rid, {"value": "on" if on else "off"}) if key == "mtime": cfg_path = _hermes_home / "config.yaml" try: diff --git a/ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx b/ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx index f135d70c682..f5fb660bedb 100644 --- a/ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx @@ -53,7 +53,7 @@ export function AlternateScreen(t0: Props) { } writeRaw( - ENTER_ALT_SCREEN + ERASE_SCROLLBACK + ERASE_SCREEN + CURSOR_HOME + (mouseTracking ? ENABLE_MOUSE_TRACKING : '') + ENTER_ALT_SCREEN + ERASE_SCROLLBACK + ERASE_SCREEN + CURSOR_HOME + (mouseTracking ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING) ) ink?.setAltScreenActive(true, mouseTracking) diff --git a/ui-tui/packages/hermes-ink/src/ink/ink.tsx b/ui-tui/packages/hermes-ink/src/ink/ink.tsx index 8e43f60ea67..7422cf4637b 100644 --- a/ui-tui/packages/hermes-ink/src/ink/ink.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/ink.tsx @@ -1121,6 +1121,23 @@ export default class Ink { this.repaint() } } + + /** + * Toggle mouse tracking at runtime while the alt screen is active. + * Writes the appropriate DEC reset/set sequences so the terminal + * (and ConPTY on Windows WSL2) reflects the change immediately. + */ + setAltScreenMouseTracking(enabled: boolean): void { + if (this.altScreenMouseTracking === enabled) { + return + } + + this.altScreenMouseTracking = enabled + + if (this.altScreenActive) { + this.options.stdout.write(enabled ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING) + } + } get isAltScreenActive(): boolean { return this.altScreenActive } diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 631bd7a350e..522e9829586 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -1,18 +1,21 @@ +import { useStore } from '@nanostores/react' + import { GatewayProvider } from './app/gatewayContext.js' import { useMainApp } from './app/useMainApp.js' +import { $uiState } from './app/uiStore.js' import { AppLayout } from './components/appLayout.js' -import { MOUSE_TRACKING } from './config/env.js' import type { GatewayClient } from './gatewayClient.js' export function App({ gw }: { gw: GatewayClient }) { const { appActions, appComposer, appProgress, appStatus, appTranscript, gateway } = useMainApp(gw) + const { mouseTracking } = useStore($uiState) return ( ctx.session.die() }, + { + aliases: ['scroll'], + help: 'toggle mouse/wheel tracking [on|off|toggle]', + name: 'mouse', + run: (arg, ctx) => { + const current = ctx.ui.mouseTracking + const next = flagFromArg(arg, current) + + if (next === null) { + return ctx.transcript.sys('usage: /mouse [on|off|toggle]') + } + + patchUiState({ mouseTracking: next }) + ctx.gateway + .rpc('config.set', { key: 'mouse', value: next ? 'on' : 'off' }) + .catch(() => {}) + + queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next ? 'on' : 'off'}`)) + } + }, + { aliases: ['new'], help: 'start a new session', diff --git a/ui-tui/src/app/uiStore.ts b/ui-tui/src/app/uiStore.ts index 0b3fd974021..260b26ab5a8 100644 --- a/ui-tui/src/app/uiStore.ts +++ b/ui-tui/src/app/uiStore.ts @@ -2,6 +2,7 @@ import { atom } from 'nanostores' import { ZERO } from '../domain/usage.js' import { DEFAULT_THEME } from '../theme.js' +import { MOUSE_TRACKING } from '../config/env.js' import type { UiState } from './interfaces.js' @@ -12,6 +13,7 @@ const buildUiState = (): UiState => ({ detailsMode: 'collapsed', info: null, inlineDiffs: true, + mouseTracking: MOUSE_TRACKING, sections: {}, showCost: false, showReasoning: false, diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index cb98eed8199..3ceb8c635a7 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -46,6 +46,7 @@ export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolea compact: !!d.tui_compact, detailsMode: resolveDetailsMode(d), inlineDiffs: d.inline_diffs !== false, + mouseTracking: d.tui_mouse !== false, sections: resolveSections(d.sections), showCost: !!d.show_cost, showReasoning: !!d.show_reasoning, diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 91fced32a8b..50ef505e619 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -61,6 +61,7 @@ export interface ConfigDisplayConfig { streaming?: boolean thinking_mode?: string tui_compact?: boolean + tui_mouse?: boolean tui_statusbar?: 'bottom' | 'off' | 'on' | 'top' | boolean }