mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
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
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
|
|
import { resolveDetailsMode, resolveSections } from '../domain/details.js'
|
|
import type { GatewayClient } from '../gatewayClient.js'
|
|
import type {
|
|
ConfigFullResponse,
|
|
ConfigMtimeResponse,
|
|
ReloadMcpResponse,
|
|
VoiceToggleResponse
|
|
} from '../gatewayTypes.js'
|
|
import { asRpcResult } from '../lib/rpc.js'
|
|
|
|
import type { StatusBarMode } from './interfaces.js'
|
|
import { turnController } from './turnController.js'
|
|
import { patchUiState } from './uiStore.js'
|
|
|
|
const STATUSBAR_ALIAS: Record<string, StatusBarMode> = {
|
|
bottom: 'bottom',
|
|
off: 'off',
|
|
on: 'top',
|
|
top: 'top'
|
|
}
|
|
|
|
export const normalizeStatusBar = (raw: unknown): StatusBarMode =>
|
|
raw === false ? 'off' : typeof raw === 'string' ? (STATUSBAR_ALIAS[raw.trim().toLowerCase()] ?? 'top') : 'top'
|
|
|
|
const MTIME_POLL_MS = 5000
|
|
|
|
const quietRpc = async <T extends Record<string, any> = Record<string, any>>(
|
|
gw: GatewayClient,
|
|
method: string,
|
|
params: Record<string, unknown> = {}
|
|
): Promise<null | T> => {
|
|
try {
|
|
return asRpcResult<T>(await gw.request<T>(method, params))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolean) => void) => {
|
|
const d = cfg?.config?.display ?? {}
|
|
|
|
setBell(!!d.bell_on_complete)
|
|
patchUiState({
|
|
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,
|
|
statusBar: normalizeStatusBar(d.tui_statusbar),
|
|
streaming: d.streaming !== false
|
|
})
|
|
}
|
|
|
|
export function useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, sid }: UseConfigSyncOptions) {
|
|
const mtimeRef = useRef(0)
|
|
|
|
useEffect(() => {
|
|
if (!sid) {
|
|
return
|
|
}
|
|
|
|
quietRpc<VoiceToggleResponse>(gw, 'voice.toggle', { action: 'status' }).then(r => setVoiceEnabled(!!r?.enabled))
|
|
quietRpc<ConfigMtimeResponse>(gw, 'config.get', { key: 'mtime' }).then(r => {
|
|
mtimeRef.current = Number(r?.mtime ?? 0)
|
|
})
|
|
quietRpc<ConfigFullResponse>(gw, 'config.get', { key: 'full' }).then(r => applyDisplay(r, setBellOnComplete))
|
|
}, [gw, setBellOnComplete, setVoiceEnabled, sid])
|
|
|
|
useEffect(() => {
|
|
if (!sid) {
|
|
return
|
|
}
|
|
|
|
const id = setInterval(() => {
|
|
quietRpc<ConfigMtimeResponse>(gw, 'config.get', { key: 'mtime' }).then(r => {
|
|
const next = Number(r?.mtime ?? 0)
|
|
|
|
if (!mtimeRef.current) {
|
|
if (next) {
|
|
mtimeRef.current = next
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (!next || next === mtimeRef.current) {
|
|
return
|
|
}
|
|
|
|
mtimeRef.current = next
|
|
|
|
quietRpc<ReloadMcpResponse>(gw, 'reload.mcp', { session_id: sid }).then(
|
|
r => r && turnController.pushActivity('MCP reloaded after config change')
|
|
)
|
|
quietRpc<ConfigFullResponse>(gw, 'config.get', { key: 'full' }).then(r => applyDisplay(r, setBellOnComplete))
|
|
})
|
|
}, MTIME_POLL_MS)
|
|
|
|
return () => clearInterval(id)
|
|
}, [gw, setBellOnComplete, sid])
|
|
}
|
|
|
|
export interface UseConfigSyncOptions {
|
|
gw: GatewayClient
|
|
setBellOnComplete: (v: boolean) => void
|
|
setVoiceEnabled: (v: boolean) => void
|
|
sid: null | string
|
|
}
|