mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix(tui): proactive mouse disable on ConPTY + /mouse toggle command
On Windows WSL2, ConPTY implicitly enables mouse event injection when the alternate screen buffer (DEC 1049) is entered, causing raw escape sequences to appear in the transcript as ghost characters. Fix (two parts): 1. ConPTY fix: send DISABLE_MOUSE_TRACKING immediately after entering alt screen when mouse tracking is off (AlternateScreen.tsx) 2. Runtime toggle: add /mouse [on|off|toggle] slash command with config persistence (display.tui_mouse) so users can manage this at runtime The env var HERMES_TUI_DISABLE_MOUSE continues to work as the initial default, but can now be overridden via /mouse and persisted to config. Closes: upstream ConPTY mouse injection issue Credits: OutThisLife / PR #13716 for the toggle concept
This commit is contained in:
parent
2182de55bb
commit
1c8ce33d51
9 changed files with 70 additions and 3 deletions
|
|
@ -2789,6 +2789,23 @@ def _(rid, params: dict) -> dict:
|
||||||
_write_config_key("display.tui_statusbar", nv)
|
_write_config_key("display.tui_statusbar", nv)
|
||||||
return _ok(rid, {"key": key, "value": 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"):
|
if key in ("prompt", "personality", "skin"):
|
||||||
try:
|
try:
|
||||||
cfg = _load_cfg()
|
cfg = _load_cfg()
|
||||||
|
|
@ -2917,6 +2934,10 @@ def _(rid, params: dict) -> dict:
|
||||||
display.get("tui_statusbar", "top") if isinstance(display, dict) else "top"
|
display.get("tui_statusbar", "top") if isinstance(display, dict) else "top"
|
||||||
)
|
)
|
||||||
return _ok(rid, {"value": _coerce_statusbar(raw)})
|
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":
|
if key == "mtime":
|
||||||
cfg_path = _hermes_home / "config.yaml"
|
cfg_path = _hermes_home / "config.yaml"
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function AlternateScreen(t0: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
writeRaw(
|
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)
|
ink?.setAltScreenActive(true, mouseTracking)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1121,6 +1121,23 @@ export default class Ink {
|
||||||
this.repaint()
|
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 {
|
get isAltScreenActive(): boolean {
|
||||||
return this.altScreenActive
|
return this.altScreenActive
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
|
||||||
import { GatewayProvider } from './app/gatewayContext.js'
|
import { GatewayProvider } from './app/gatewayContext.js'
|
||||||
import { useMainApp } from './app/useMainApp.js'
|
import { useMainApp } from './app/useMainApp.js'
|
||||||
|
import { $uiState } from './app/uiStore.js'
|
||||||
import { AppLayout } from './components/appLayout.js'
|
import { AppLayout } from './components/appLayout.js'
|
||||||
import { MOUSE_TRACKING } from './config/env.js'
|
|
||||||
import type { GatewayClient } from './gatewayClient.js'
|
import type { GatewayClient } from './gatewayClient.js'
|
||||||
|
|
||||||
export function App({ gw }: { gw: GatewayClient }) {
|
export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const { appActions, appComposer, appProgress, appStatus, appTranscript, gateway } = useMainApp(gw)
|
const { appActions, appComposer, appProgress, appStatus, appTranscript, gateway } = useMainApp(gw)
|
||||||
|
const { mouseTracking } = useStore($uiState)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GatewayProvider value={gateway}>
|
<GatewayProvider value={gateway}>
|
||||||
<AppLayout
|
<AppLayout
|
||||||
actions={appActions}
|
actions={appActions}
|
||||||
composer={appComposer}
|
composer={appComposer}
|
||||||
mouseTracking={MOUSE_TRACKING}
|
mouseTracking={mouseTracking}
|
||||||
progress={appProgress}
|
progress={appProgress}
|
||||||
status={appStatus}
|
status={appStatus}
|
||||||
transcript={appTranscript}
|
transcript={appTranscript}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ export interface UiState {
|
||||||
detailsMode: DetailsMode
|
detailsMode: DetailsMode
|
||||||
info: null | SessionInfo
|
info: null | SessionInfo
|
||||||
inlineDiffs: boolean
|
inlineDiffs: boolean
|
||||||
|
mouseTracking: boolean
|
||||||
sections: SectionVisibility
|
sections: SectionVisibility
|
||||||
showCost: boolean
|
showCost: boolean
|
||||||
showReasoning: boolean
|
showReasoning: boolean
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,27 @@ export const coreCommands: SlashCommand[] = [
|
||||||
run: (_arg, ctx) => ctx.session.die()
|
run: (_arg, ctx) => 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<ConfigSetResponse>('config.set', { key: 'mouse', value: next ? 'on' : 'off' })
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next ? 'on' : 'off'}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
aliases: ['new'],
|
aliases: ['new'],
|
||||||
help: 'start a new session',
|
help: 'start a new session',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { atom } from 'nanostores'
|
||||||
|
|
||||||
import { ZERO } from '../domain/usage.js'
|
import { ZERO } from '../domain/usage.js'
|
||||||
import { DEFAULT_THEME } from '../theme.js'
|
import { DEFAULT_THEME } from '../theme.js'
|
||||||
|
import { MOUSE_TRACKING } from '../config/env.js'
|
||||||
|
|
||||||
import type { UiState } from './interfaces.js'
|
import type { UiState } from './interfaces.js'
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ const buildUiState = (): UiState => ({
|
||||||
detailsMode: 'collapsed',
|
detailsMode: 'collapsed',
|
||||||
info: null,
|
info: null,
|
||||||
inlineDiffs: true,
|
inlineDiffs: true,
|
||||||
|
mouseTracking: MOUSE_TRACKING,
|
||||||
sections: {},
|
sections: {},
|
||||||
showCost: false,
|
showCost: false,
|
||||||
showReasoning: false,
|
showReasoning: false,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolea
|
||||||
compact: !!d.tui_compact,
|
compact: !!d.tui_compact,
|
||||||
detailsMode: resolveDetailsMode(d),
|
detailsMode: resolveDetailsMode(d),
|
||||||
inlineDiffs: d.inline_diffs !== false,
|
inlineDiffs: d.inline_diffs !== false,
|
||||||
|
mouseTracking: d.tui_mouse !== false,
|
||||||
sections: resolveSections(d.sections),
|
sections: resolveSections(d.sections),
|
||||||
showCost: !!d.show_cost,
|
showCost: !!d.show_cost,
|
||||||
showReasoning: !!d.show_reasoning,
|
showReasoning: !!d.show_reasoning,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export interface ConfigDisplayConfig {
|
||||||
streaming?: boolean
|
streaming?: boolean
|
||||||
thinking_mode?: string
|
thinking_mode?: string
|
||||||
tui_compact?: boolean
|
tui_compact?: boolean
|
||||||
|
tui_mouse?: boolean
|
||||||
tui_statusbar?: 'bottom' | 'off' | 'on' | 'top' | boolean
|
tui_statusbar?: 'bottom' | 'off' | 'on' | 'top' | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue