mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix(tui): honor documented mouse_tracking config key (#17188)
* fix(tui): honor documented mouse_tracking config key The TUI runtime was reading display.tui_mouse while docs and user-facing examples pointed users at display.mouse_tracking. That made persistent mouse-disable config look like a no-op for users trying to restore native terminal selection/copy behavior on Linux/SSH/tmux terminals. Use display.mouse_tracking as the canonical key, keep display.tui_mouse as a legacy fallback, and have /mouse write the documented key. Both gateway config.get and client-side config sync now share the same precedence: the canonical key wins, then the legacy key, then default on. * review(copilot): align mouse tracking config coercion - Load gateway config once before deriving display.mouse_tracking state. - Use key-presence precedence on the TUI client too, so canonical mouse_tracking wins over legacy tui_mouse even when the value is null. - Treat numeric 0 as disabled on both gateway and client, matching the existing string "0" handling. - Widen ConfigDisplayConfig mouse fields because config.get full returns raw YAML, not normalized booleans.
This commit is contained in:
parent
6b09df39be
commit
188eaa57c4
5 changed files with 97 additions and 10 deletions
|
|
@ -944,6 +944,39 @@ def test_config_set_section_rejects_unknown_section_or_mode(tmp_path, monkeypatc
|
||||||
assert bad_mode["error"]["code"] == 4002
|
assert bad_mode["error"]["code"] == 4002
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_mouse_uses_documented_key_with_legacy_fallback(monkeypatch):
|
||||||
|
cfg = {"display": {"tui_mouse": False}}
|
||||||
|
writes = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: cfg)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
server, "_write_config_key", lambda path, value: writes.append((path, value))
|
||||||
|
)
|
||||||
|
|
||||||
|
get_legacy = server.handle_request(
|
||||||
|
{"id": "1", "method": "config.get", "params": {"key": "mouse"}}
|
||||||
|
)
|
||||||
|
assert get_legacy["result"]["value"] == "off"
|
||||||
|
|
||||||
|
set_toggle = server.handle_request(
|
||||||
|
{"id": "2", "method": "config.set", "params": {"key": "mouse"}}
|
||||||
|
)
|
||||||
|
assert set_toggle["result"] == {"key": "mouse", "value": "on"}
|
||||||
|
assert writes == [("display.mouse_tracking", True)]
|
||||||
|
|
||||||
|
cfg["display"] = {"mouse_tracking": 0, "tui_mouse": True}
|
||||||
|
get_canonical = server.handle_request(
|
||||||
|
{"id": "3", "method": "config.get", "params": {"key": "mouse"}}
|
||||||
|
)
|
||||||
|
assert get_canonical["result"]["value"] == "off"
|
||||||
|
|
||||||
|
cfg["display"] = {"mouse_tracking": None, "tui_mouse": False}
|
||||||
|
get_null = server.handle_request(
|
||||||
|
{"id": "4", "method": "config.get", "params": {"key": "mouse"}}
|
||||||
|
)
|
||||||
|
assert get_null["result"]["value"] == "on"
|
||||||
|
|
||||||
|
|
||||||
def test_enable_gateway_prompts_sets_gateway_env(monkeypatch):
|
def test_enable_gateway_prompts_sets_gateway_env(monkeypatch):
|
||||||
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
|
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
|
||||||
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
|
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
|
||||||
|
|
|
||||||
|
|
@ -690,6 +690,21 @@ def _coerce_statusbar(raw) -> str:
|
||||||
return "top"
|
return "top"
|
||||||
|
|
||||||
|
|
||||||
|
def _display_mouse_tracking(display: dict) -> bool:
|
||||||
|
"""Return canonical display.mouse_tracking with legacy tui_mouse fallback."""
|
||||||
|
if not isinstance(display, dict):
|
||||||
|
return True
|
||||||
|
if "mouse_tracking" in display:
|
||||||
|
raw = display.get("mouse_tracking")
|
||||||
|
else:
|
||||||
|
raw = display.get("tui_mouse", True)
|
||||||
|
if raw is False or raw == 0:
|
||||||
|
return False
|
||||||
|
if isinstance(raw, str):
|
||||||
|
return raw.strip().lower() not in {"0", "false", "no", "off"}
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _load_reasoning_config() -> dict | None:
|
def _load_reasoning_config() -> dict | None:
|
||||||
from hermes_constants import parse_reasoning_effort
|
from hermes_constants import parse_reasoning_effort
|
||||||
|
|
||||||
|
|
@ -3172,12 +3187,9 @@ def _(rid, params: dict) -> dict:
|
||||||
|
|
||||||
if key == "mouse":
|
if key == "mouse":
|
||||||
raw = str(value or "").strip().lower()
|
raw = str(value or "").strip().lower()
|
||||||
display = (
|
cfg = _load_cfg()
|
||||||
_load_cfg().get("display")
|
display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {}
|
||||||
if isinstance(_load_cfg().get("display"), dict)
|
current = _display_mouse_tracking(display)
|
||||||
else {}
|
|
||||||
)
|
|
||||||
current = bool(display.get("tui_mouse", True))
|
|
||||||
|
|
||||||
if raw in ("", "toggle"):
|
if raw in ("", "toggle"):
|
||||||
nv = not current
|
nv = not current
|
||||||
|
|
@ -3188,7 +3200,7 @@ def _(rid, params: dict) -> dict:
|
||||||
else:
|
else:
|
||||||
return _err(rid, 4002, f"unknown mouse value: {value}")
|
return _err(rid, 4002, f"unknown mouse value: {value}")
|
||||||
|
|
||||||
_write_config_key("display.tui_mouse", nv)
|
_write_config_key("display.mouse_tracking", nv)
|
||||||
return _ok(rid, {"key": key, "value": "on" if nv else "off"})
|
return _ok(rid, {"key": key, "value": "on" if nv else "off"})
|
||||||
|
|
||||||
if key == "indicator":
|
if key == "indicator":
|
||||||
|
|
@ -3361,7 +3373,7 @@ def _(rid, params: dict) -> dict:
|
||||||
return _ok(rid, {"value": _coerce_statusbar(raw)})
|
return _ok(rid, {"value": _coerce_statusbar(raw)})
|
||||||
if key == "mouse":
|
if key == "mouse":
|
||||||
display = _load_cfg().get("display")
|
display = _load_cfg().get("display")
|
||||||
on = display.get("tui_mouse", True) if isinstance(display, dict) else True
|
on = _display_mouse_tracking(display)
|
||||||
return _ok(rid, {"value": "on" if on else "off"})
|
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"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
applyDisplay,
|
applyDisplay,
|
||||||
normalizeBusyInputMode,
|
normalizeBusyInputMode,
|
||||||
normalizeIndicatorStyle,
|
normalizeIndicatorStyle,
|
||||||
|
normalizeMouseTracking,
|
||||||
normalizeStatusBar
|
normalizeStatusBar
|
||||||
} from '../app/useConfigSync.js'
|
} from '../app/useConfigSync.js'
|
||||||
|
|
||||||
|
|
@ -70,6 +71,19 @@ describe('applyDisplay', () => {
|
||||||
expect(s.sections).toEqual({})
|
expect(s.sections).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses documented mouse_tracking with legacy tui_mouse fallback', () => {
|
||||||
|
const setBell = vi.fn()
|
||||||
|
|
||||||
|
applyDisplay({ config: { display: { mouse_tracking: false } } }, setBell)
|
||||||
|
expect($uiState.get().mouseTracking).toBe(false)
|
||||||
|
|
||||||
|
applyDisplay({ config: { display: { mouse_tracking: true, tui_mouse: false } } }, setBell)
|
||||||
|
expect($uiState.get().mouseTracking).toBe(true)
|
||||||
|
|
||||||
|
applyDisplay({ config: { display: { tui_mouse: false } } }, setBell)
|
||||||
|
expect($uiState.get().mouseTracking).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('parses display.sections into per-section overrides', () => {
|
it('parses display.sections into per-section overrides', () => {
|
||||||
const setBell = vi.fn()
|
const setBell = vi.fn()
|
||||||
|
|
||||||
|
|
@ -166,6 +180,19 @@ describe('normalizeStatusBar', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('normalizeMouseTracking', () => {
|
||||||
|
it('defaults on and prefers canonical mouse_tracking over legacy tui_mouse', () => {
|
||||||
|
expect(normalizeMouseTracking({})).toBe(true)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: false })).toBe(false)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: 0 })).toBe(false)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: 'off' })).toBe(false)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: 'false' })).toBe(false)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: null, tui_mouse: false })).toBe(true)
|
||||||
|
expect(normalizeMouseTracking({ mouse_tracking: true, tui_mouse: false })).toBe(true)
|
||||||
|
expect(normalizeMouseTracking({ tui_mouse: false })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('normalizeBusyInputMode', () => {
|
describe('normalizeBusyInputMode', () => {
|
||||||
it('passes through the canonical CLI parity values', () => {
|
it('passes through the canonical CLI parity values', () => {
|
||||||
expect(normalizeBusyInputMode('queue')).toBe('queue')
|
expect(normalizeBusyInputMode('queue')).toBe('queue')
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,19 @@ export const normalizeIndicatorStyle = (raw: unknown): IndicatorStyle => {
|
||||||
return INDICATOR_STYLE_SET.has(v) ? v : DEFAULT_INDICATOR_STYLE
|
return INDICATOR_STYLE_SET.has(v) ? v : DEFAULT_INDICATOR_STYLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FALSEY_MOUSE = new Set(['0', 'false', 'no', 'off'])
|
||||||
|
const hasOwn = (obj: object, key: PropertyKey) => Object.prototype.hasOwnProperty.call(obj, key)
|
||||||
|
|
||||||
|
export const normalizeMouseTracking = (display: { mouse_tracking?: unknown; tui_mouse?: unknown }): boolean => {
|
||||||
|
const raw = hasOwn(display, 'mouse_tracking') ? display.mouse_tracking : display.tui_mouse
|
||||||
|
|
||||||
|
if (raw === false || raw === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof raw === 'string' ? !FALSEY_MOUSE.has(raw.trim().toLowerCase()) : true
|
||||||
|
}
|
||||||
|
|
||||||
const MTIME_POLL_MS = 5000
|
const MTIME_POLL_MS = 5000
|
||||||
|
|
||||||
const quietRpc = async <T extends Record<string, any> = Record<string, any>>(
|
const quietRpc = async <T extends Record<string, any> = Record<string, any>>(
|
||||||
|
|
@ -88,7 +101,7 @@ export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolea
|
||||||
detailsModeCommandOverride: false,
|
detailsModeCommandOverride: false,
|
||||||
indicatorStyle: normalizeIndicatorStyle(d.tui_status_indicator),
|
indicatorStyle: normalizeIndicatorStyle(d.tui_status_indicator),
|
||||||
inlineDiffs: d.inline_diffs !== false,
|
inlineDiffs: d.inline_diffs !== false,
|
||||||
mouseTracking: d.tui_mouse !== false,
|
mouseTracking: normalizeMouseTracking(d),
|
||||||
sections: resolveSections(d.sections),
|
sections: resolveSections(d.sections),
|
||||||
showCost: !!d.show_cost,
|
showCost: !!d.show_cost,
|
||||||
showReasoning: !!d.show_reasoning,
|
showReasoning: !!d.show_reasoning,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export interface ConfigDisplayConfig {
|
||||||
busy_input_mode?: string
|
busy_input_mode?: string
|
||||||
details_mode?: string
|
details_mode?: string
|
||||||
inline_diffs?: boolean
|
inline_diffs?: boolean
|
||||||
|
mouse_tracking?: boolean | null | number | string
|
||||||
sections?: Record<string, string>
|
sections?: Record<string, string>
|
||||||
show_cost?: boolean
|
show_cost?: boolean
|
||||||
show_reasoning?: boolean
|
show_reasoning?: boolean
|
||||||
|
|
@ -63,7 +64,8 @@ export interface ConfigDisplayConfig {
|
||||||
thinking_mode?: string
|
thinking_mode?: string
|
||||||
tui_auto_resume_recent?: boolean
|
tui_auto_resume_recent?: boolean
|
||||||
tui_compact?: boolean
|
tui_compact?: boolean
|
||||||
tui_mouse?: boolean
|
/** Legacy alias for display.mouse_tracking. */
|
||||||
|
tui_mouse?: boolean | null | number | string
|
||||||
// Forward-compat: backend may send styles this client doesn't know yet —
|
// Forward-compat: backend may send styles this client doesn't know yet —
|
||||||
// `normalizeIndicatorStyle` falls back to 'kaomoji' for those — but the
|
// `normalizeIndicatorStyle` falls back to 'kaomoji' for those — but the
|
||||||
// wire type is documented as `string` so consumers don't get a false
|
// wire type is documented as `string` so consumers don't get a false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue