mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
* feat(tui): make display.mouse_tracking pick which DEC modes to enable
Previously the boolean flag was all-or-nothing across modes 1000+1002+1003+1006.
Inside tmux, mode 1003 (any-motion) makes every mouse cross of the prompt row
fire a clipboard probe that surfaces as "No image in clipboard" — sometimes
dozens in a row. Disabling tracking entirely killed scroll-wheel scrolling too,
since tmux's own scrollback is preempted by the alt-screen TUI.
`display.mouse_tracking` (and `/mouse <preset>`) now accepts `off | wheel |
buttons | all` in addition to the legacy booleans. `wheel` is 1000+1006:
scroll wheel + click only, no drag, no hover — the tmux-friendly subset.
`buttons` adds 1002 for drag-to-select. `all` (= legacy `true`) keeps the
hover-driven UI (scrollbar paginate-on-hover, link mouseenter, etc.).
* fix(tui): repaint + sync mouse mode when display.mouse_tracking changes
Two interacting bugs left the TUI blank when `display.mouse_tracking`
switched at runtime (config edit, /mouse <preset>):
1. AlternateScreen's effect re-runs on every `mouseTracking` change,
tearing down and re-entering the alt screen. After re-entry, ink's
frame buffers are reset by `resetFramesForAltScreen()` but nothing
schedules the follow-up render — the alt screen sits blank until
some other state change happens to trigger one. Add a
`scheduleRender()` in `setAltScreenActive`'s active=true branch so
the freshly-entered alt screen gets a full repaint immediately.
2. `setAltScreenActive` early-returns when `active` hasn't changed,
which silently drops a `mouseTracking` change if the cleanup→setup
pair somehow leaves `altScreenActive` already true. Call
`setAltScreenMouseTracking` explicitly from the AlternateScreen
effect so the in-memory mode and terminal DECSET sequence stay in
sync regardless of how `setAltScreenActive` resolved (the call is a
no-op when the mode is unchanged).
* fix(tui): address copilot review #4341269705
- tui_gateway/server.py: drop the never-referenced _MOUSE_TRACKING_MODES
frozenset (comment #3284802434). _MOUSE_TRACKING_ALIASES already
centralizes the canonical preset set via its values; the separate
constant added no behavior.
- tests/test_tui_gateway_server.py: update the existing
test_config_mouse_uses_documented_key_with_legacy_fallback to assert
the new preset strings ('all'/'off' instead of 'on'/'off',
display.mouse_tracking persisted as 'all' instead of True) and add
test_config_mouse_accepts_preset_strings_and_aliases covering /mouse
set with wheel/click/unknown (comment #3284802453). The on/off legacy
config.set return shape was an implementation detail of the boolean
flag, not a stable API — the slash command, gateway help text, and
docs all advertise the preset values now.
- ui-tui/packages/hermes-ink/src/ink/ink.tsx: schedule a render at the
end of reenterAltScreen() (comment #3284802461). Mirrors the same fix
in setAltScreenActive() from ece0a2f4c — without it, SIGCONT/resize
self-heal/stdin-gap re-entry leaves the alt screen blank because
every caller returns early after invoking us.
* fix(tui): address copilot review #4341308478 round 2
- ui-tui/src/config/env.ts (comment #3284837577): the precedence
comment was misleading. Actual behavior on origin/main is
HERMES_TUI_MOUSE_TRACKING (explicit override) > Termux default >
HERMES_TUI_DISABLE_MOUSE legacy kill-switch. This is preserved from
main; the only change here was the wrong comment that claimed
DISABLE_MOUSE kept kill-switch semantics. Rewrote the comment block
to document the actual precedence ladder.
- tui_gateway/server.py /mouse set (comment #3284837607): replaced
'str(value or "").strip().lower()' with the explicit None idiom
already used for /indicator, so programmatic callers can pass 0 /
False and have them route through _MOUSE_TRACKING_ALIASES → 'off'
instead of collapsing to '' and triggering the toggle path.
- ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx
(comment #3284837620): always prepend DISABLE_MOUSE_TRACKING before
enableMouseTrackingFor(...) on mount. Otherwise selecting
'wheel'/'buttons' from a state where DEC 1003 was already asserted
(crash, another app, debugger) would silently leave hover on. Also
unconditionally DISABLE on unmount so a crash mid-mount can't leak
DEC modes back to the host shell.
* chore(release): map nat@nthrow.io to @nthrow for #26681 salvage
* fix(tui): drop redundant setAltScreenMouseTracking in AlternateScreen
Copilot review #4341356637 (comment #3284880417). The explicit
setAltScreenMouseTracking(mouseTracking) after setAltScreenActive(true,
mouseTracking) was defensive paranoia added in the previous fix commit
that's not actually reachable in practice:
- React's cleanup always runs before the next setup, so on any prop
change (mouseTracking or writeRaw) the cleanup sets active=false
first. Setup then sees active was false and applies the new mode
via setAltScreenActive without early-returning.
- On the impossible 'active stayed true' path, the writeRaw above has
already sent DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(newMode)
to the terminal, so the in-memory mode would lag but the visible
state is already correct.
Removing the redundant call means a single DEC sequence per mount.
If the 'active stayed true' path ever manifests in practice, the
right fix is in setAltScreenActive (track mode regardless of the
active early-return), not here.
* fix(tui): always DISABLE before enableMouseTrackingFor in ink.tsx
Copilot review #4341379994 (comments #3284900825, #3284900840,
#3284900852). Three remaining call sites in ink.tsx still re-enabled
mouse tracking without first sending DISABLE_MOUSE_TRACKING:
- handleResize alt-screen recovery (line ~577)
- reassertTerminalModes stdin-gap re-assertion (line ~1351)
- reenterAltScreen SIGCONT/resize/stdin-gap self-heal (line ~1408)
For 'wheel'/'buttons' presets, omitting DISABLE leaves any externally-
asserted DEC 1003 (other apps, prior crash, tmux state) still active
and the hover-free preset silently has hover on. DISABLE_MOUSE_TRACKING
is idempotent and safe to send unconditionally — it resets all four
modes. Matches the pattern already in setAltScreenMouseTracking and
the AlternateScreen mount path.
* fix(tui): always DISABLE before enableMouseTrackingFor in exitAlternateScreen
Copilot review #4341452823 (comment #3284959762). exitAlternateScreen()
was the last call site in ink.tsx still re-enabling mouse tracking
without DISABLE first. Editors (vim/nvim/less) and tmux can leave
DEC 1003 hover asserted across the handoff back; without DISABLE,
'wheel'/'buttons' presets silently kept hover on after the editor
quit. Now all five enableMouseTrackingFor() call sites in ink.tsx
prepend DISABLE_MOUSE_TRACKING — handleResize, reassertTerminalModes,
reenterAltScreen, setAltScreenMouseTracking, exitAlternateScreen.
* fix(tui): add defensive default to enableMouseTrackingFor switch
Copilot review #4341485231 (comment #3284979323). TS exhaustive switch
returns string per the type system, but a JS caller / corrupted config
/ hot-reload-in-dev could reach the function with an unknown value at
runtime. Without a default, that path returns undefined which then
concatenates as the literal string 'undefined' into the terminal byte
stream — visibly garbling output. Treat unknown as 'off' (no DEC
sequences) so the worst case is silent input loss rather than a
wrecked screen.
---------
Co-authored-by: Nat Thrower <nat@nthrow.io>
This commit is contained in:
parent
4d58e48cdb
commit
a7cd254c29
14 changed files with 399 additions and 88 deletions
|
|
@ -59,6 +59,7 @@ AUTHOR_MAP = {
|
|||
"mgongzai@gmail.com": "vKongv",
|
||||
"0x.badfriend@gmail.com": "discodirector",
|
||||
"altriatree@gmail.com": "TruaShamu",
|
||||
"nat@nthrow.io": "nthrow",
|
||||
"m@mobrienv.dev": "mikeyobrien",
|
||||
"saeed919@pm.me": "falasi",
|
||||
"omar@techdeveloper.site": "nycomar",
|
||||
|
|
|
|||
|
|
@ -1476,8 +1476,10 @@ def test_config_mouse_uses_documented_key_with_legacy_fallback(monkeypatch):
|
|||
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)]
|
||||
# /mouse (no arg) toggles between 'all' and 'off'. Starting from
|
||||
# tui_mouse: False (→ 'off'), the toggle flips to 'all'.
|
||||
assert set_toggle["result"] == {"key": "mouse", "value": "all"}
|
||||
assert writes == [("display.mouse_tracking", "all")]
|
||||
|
||||
cfg["display"] = {"mouse_tracking": 0, "tui_mouse": True}
|
||||
get_canonical = server.handle_request(
|
||||
|
|
@ -1489,7 +1491,51 @@ def test_config_mouse_uses_documented_key_with_legacy_fallback(monkeypatch):
|
|||
get_null = server.handle_request(
|
||||
{"id": "4", "method": "config.get", "params": {"key": "mouse"}}
|
||||
)
|
||||
assert get_null["result"]["value"] == "on"
|
||||
# mouse_tracking present-but-None defers neither to tui_mouse nor to
|
||||
# the legacy off bucket: it falls through to the 'all' default.
|
||||
assert get_null["result"]["value"] == "all"
|
||||
|
||||
|
||||
def test_config_mouse_accepts_preset_strings_and_aliases(monkeypatch):
|
||||
cfg = {"display": {"mouse_tracking": "all"}}
|
||||
writes = []
|
||||
|
||||
monkeypatch.setattr(server, "_load_cfg", lambda: cfg)
|
||||
monkeypatch.setattr(
|
||||
server, "_write_config_key", lambda path, value: writes.append((path, value))
|
||||
)
|
||||
|
||||
# Direct preset.
|
||||
set_wheel = server.handle_request(
|
||||
{
|
||||
"id": "1",
|
||||
"method": "config.set",
|
||||
"params": {"key": "mouse", "value": "wheel"},
|
||||
}
|
||||
)
|
||||
assert set_wheel["result"] == {"key": "mouse", "value": "wheel"}
|
||||
assert writes[-1] == ("display.mouse_tracking", "wheel")
|
||||
|
||||
# Alias for buttons.
|
||||
set_click = server.handle_request(
|
||||
{
|
||||
"id": "2",
|
||||
"method": "config.set",
|
||||
"params": {"key": "mouse", "value": "click"},
|
||||
}
|
||||
)
|
||||
assert set_click["result"] == {"key": "mouse", "value": "buttons"}
|
||||
assert writes[-1] == ("display.mouse_tracking", "buttons")
|
||||
|
||||
# Unknown value → 4002.
|
||||
bad = server.handle_request(
|
||||
{
|
||||
"id": "3",
|
||||
"method": "config.set",
|
||||
"params": {"key": "mouse", "value": "rainbows"},
|
||||
}
|
||||
)
|
||||
assert bad["error"]["code"] == 4002
|
||||
|
||||
|
||||
def test_enable_gateway_prompts_sets_gateway_env(monkeypatch):
|
||||
|
|
|
|||
|
|
@ -845,19 +845,50 @@ def _coerce_statusbar(raw) -> str:
|
|||
return "top"
|
||||
|
||||
|
||||
def _display_mouse_tracking(display: dict) -> bool:
|
||||
"""Return canonical display.mouse_tracking with legacy tui_mouse fallback."""
|
||||
_MOUSE_TRACKING_ALIASES = {
|
||||
"0": "off",
|
||||
"1": "all",
|
||||
"all": "all",
|
||||
"any": "all",
|
||||
"button": "buttons",
|
||||
"buttons": "buttons",
|
||||
"click": "buttons",
|
||||
"false": "off",
|
||||
"full": "all",
|
||||
"no": "off",
|
||||
"off": "off",
|
||||
"on": "all",
|
||||
"scroll": "wheel",
|
||||
"true": "all",
|
||||
"wheel": "wheel",
|
||||
"yes": "all",
|
||||
}
|
||||
|
||||
|
||||
def _display_mouse_tracking(display: dict) -> str:
|
||||
"""Resolve display.mouse_tracking to one of ``off|wheel|buttons|all``.
|
||||
|
||||
Boolean values keep their legacy meaning (``True`` → ``all``, ``False`` →
|
||||
``off``). The ``wheel`` preset (DEC 1000+1006) is the tmux-friendly
|
||||
subset — wheel + click only, no hover events to trigger prompt-row
|
||||
clipboard probes. Legacy ``tui_mouse`` is honored only when
|
||||
``mouse_tracking`` is absent.
|
||||
"""
|
||||
if not isinstance(display, dict):
|
||||
return True
|
||||
return "all"
|
||||
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
|
||||
return "off"
|
||||
if raw is True or raw is None:
|
||||
return "all"
|
||||
if isinstance(raw, (int, float)):
|
||||
return "all"
|
||||
if isinstance(raw, str):
|
||||
return raw.strip().lower() not in {"0", "false", "no", "off"}
|
||||
return True
|
||||
return _MOUSE_TRACKING_ALIASES.get(raw.strip().lower(), "all")
|
||||
return "all"
|
||||
|
||||
|
||||
def _load_reasoning_config() -> dict | None:
|
||||
|
|
@ -4078,22 +4109,25 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"key": key, "value": nv})
|
||||
|
||||
if key == "mouse":
|
||||
raw = str(value or "").strip().lower()
|
||||
# Explicit None check rather than `value or ""` so falsy non-string
|
||||
# inputs (0, False) reach the alias map as themselves — both map to
|
||||
# 'off' via _MOUSE_TRACKING_ALIASES — instead of being collapsed to
|
||||
# '' and triggering the toggle path. The slash command always passes
|
||||
# a string, but programmatic JSON-RPC callers may send booleans.
|
||||
raw = ("" if value is None else str(value)).strip().lower()
|
||||
cfg = _load_cfg()
|
||||
display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {}
|
||||
current = _display_mouse_tracking(display)
|
||||
|
||||
if raw in {"", "toggle"}:
|
||||
nv = not current
|
||||
elif raw == "on":
|
||||
nv = True
|
||||
elif raw == "off":
|
||||
nv = False
|
||||
nv = "all" if current == "off" else "off"
|
||||
elif raw in _MOUSE_TRACKING_ALIASES:
|
||||
nv = _MOUSE_TRACKING_ALIASES[raw]
|
||||
else:
|
||||
return _err(rid, 4002, f"unknown mouse value: {value}")
|
||||
|
||||
_write_config_key("display.mouse_tracking", nv)
|
||||
return _ok(rid, {"key": key, "value": "on" if nv else "off"})
|
||||
return _ok(rid, {"key": key, "value": nv})
|
||||
|
||||
if key == "indicator":
|
||||
# Use an explicit None check rather than `value or ""` so falsy
|
||||
|
|
@ -4266,8 +4300,7 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"value": _coerce_statusbar(raw)})
|
||||
if key == "mouse":
|
||||
display = _load_cfg().get("display")
|
||||
on = _display_mouse_tracking(display)
|
||||
return _ok(rid, {"value": "on" if on else "off"})
|
||||
return _ok(rid, {"value": _display_mouse_tracking(display)})
|
||||
if key == "mtime":
|
||||
cfg_path = _hermes_home / "config.yaml"
|
||||
try:
|
||||
|
|
@ -4402,7 +4435,11 @@ _TUI_HIDDEN: frozenset[str] = frozenset(
|
|||
_TUI_EXTRA: list[tuple[str, str, str]] = [
|
||||
("/compact", "Toggle compact display mode", "TUI"),
|
||||
("/logs", "Show recent gateway log lines", "TUI"),
|
||||
("/mouse", "Toggle mouse/wheel tracking [on|off|toggle]", "TUI"),
|
||||
(
|
||||
"/mouse",
|
||||
"Set mouse tracking preset [on|off|toggle|wheel|buttons|all]",
|
||||
"TUI",
|
||||
),
|
||||
]
|
||||
|
||||
# Commands that queue messages onto _pending_input in the CLI.
|
||||
|
|
@ -5280,7 +5317,7 @@ def _(rid, params: dict) -> dict:
|
|||
{
|
||||
"text": "/mouse",
|
||||
"display": "/mouse",
|
||||
"meta": "Toggle mouse/wheel tracking [on|off|toggle]",
|
||||
"meta": "Set mouse tracking preset [on|off|toggle|wheel|buttons|all]",
|
||||
},
|
||||
]
|
||||
for extra in extras:
|
||||
|
|
|
|||
1
ui-tui/packages/hermes-ink/index.d.ts
vendored
1
ui-tui/packages/hermes-ink/index.d.ts
vendored
|
|
@ -7,6 +7,7 @@ export { Ansi } from './src/ink/Ansi.tsx'
|
|||
export { evictInkCaches } from './src/ink/cache-eviction.ts'
|
||||
export type { EvictLevel, InkCacheSizes } from './src/ink/cache-eviction.ts'
|
||||
export { AlternateScreen } from './src/ink/components/AlternateScreen.tsx'
|
||||
export type { MouseTrackingMode } from './src/ink/termio/dec.ts'
|
||||
export { default as Box } from './src/ink/components/Box.tsx'
|
||||
export type { Props as BoxProps } from './src/ink/components/Box.tsx'
|
||||
export { default as Link } from './src/ink/components/Link.tsx'
|
||||
|
|
|
|||
|
|
@ -28,4 +28,5 @@ export { createRoot, forceRedraw, default as render, renderSync } from './ink/ro
|
|||
export { stringWidth } from './ink/stringWidth.js'
|
||||
export { wrapAnsi } from './ink/wrapAnsi.js'
|
||||
export { isXtermJs } from './ink/terminal.js'
|
||||
export type { MouseTrackingMode } from './ink/termio/dec.js'
|
||||
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -97,9 +97,10 @@ import {
|
|||
DBP,
|
||||
DFE,
|
||||
DISABLE_MOUSE_TRACKING,
|
||||
ENABLE_MOUSE_TRACKING,
|
||||
enableMouseTrackingFor,
|
||||
ENTER_ALT_SCREEN,
|
||||
EXIT_ALT_SCREEN,
|
||||
type MouseTrackingMode,
|
||||
SHOW_CURSOR
|
||||
} from './termio/dec.js'
|
||||
import {
|
||||
|
|
@ -267,9 +268,11 @@ export default class Ink {
|
|||
// LF-induced scroll when screen.height === terminalRows) and gates
|
||||
// alt-screen-aware SIGCONT/resize/unmount handling.
|
||||
private altScreenActive = false
|
||||
// Set alongside altScreenActive so SIGCONT resume knows whether to
|
||||
// re-enable mouse tracking (not all <AlternateScreen> uses want it).
|
||||
private altScreenMouseTracking = false
|
||||
// Set alongside altScreenActive so SIGCONT resume knows which mouse
|
||||
// tracking preset to re-enable (not all <AlternateScreen> uses want
|
||||
// tracking, and tmux users routinely opt into the hover-free 'wheel'
|
||||
// subset to silence prompt-row clipboard probes).
|
||||
private altScreenMouseTracking: MouseTrackingMode = 'off'
|
||||
// True when the previous frame's screen buffer cannot be trusted for
|
||||
// blit — selection overlay mutated it, resetFramesForAltScreen()
|
||||
// replaced it with blanks, or forceRedraw() reset it to 0×0. Forces
|
||||
|
|
@ -570,9 +573,11 @@ export default class Ink {
|
|||
this.resizeSettleTimer = null
|
||||
}
|
||||
|
||||
if (this.altScreenMouseTracking) {
|
||||
this.options.stdout.write(ENABLE_MOUSE_TRACKING)
|
||||
}
|
||||
// Mouse tracking — DISABLE first so we land in the exact preset state
|
||||
// even if an external app/terminal/tmux left DEC 1003 hover asserted.
|
||||
// DISABLE_MOUSE_TRACKING is idempotent (resets all four modes
|
||||
// unconditionally), safe to send even when current preset is 'off'.
|
||||
this.options.stdout.write(DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(this.altScreenMouseTracking))
|
||||
|
||||
this.resetFramesForAltScreen()
|
||||
this.needsEraseBeforePaint = true
|
||||
|
|
@ -609,7 +614,7 @@ export default class Ink {
|
|||
// kitty/modifyOtherKeys stays active. exitAlternateScreen re-enables.
|
||||
DISABLE_KITTY_KEYBOARD +
|
||||
DISABLE_MODIFY_OTHER_KEYS +
|
||||
(this.altScreenMouseTracking ? DISABLE_MOUSE_TRACKING : '') +
|
||||
(this.altScreenMouseTracking !== 'off' ? DISABLE_MOUSE_TRACKING : '') +
|
||||
// disable mouse (no-op if off)
|
||||
(this.altScreenActive ? '' : '\x1b[?1049h') +
|
||||
// enter alt (already in alt if fullscreen)
|
||||
|
|
@ -645,7 +650,11 @@ export default class Ink {
|
|||
// clear screen (now alt if fullscreen)
|
||||
'\x1b[H' +
|
||||
// cursor home
|
||||
(this.altScreenMouseTracking ? ENABLE_MOUSE_TRACKING : '') +
|
||||
// DISABLE first so external editors/tmux that left DEC 1003 hover
|
||||
// on can't survive the handoff back — same pattern as
|
||||
// setAltScreenMouseTracking / reenterAltScreen.
|
||||
DISABLE_MOUSE_TRACKING +
|
||||
enableMouseTrackingFor(this.altScreenMouseTracking) +
|
||||
(this.altScreenActive ? '' : '\x1b[?1049l') +
|
||||
// exit alt (non-fullscreen only)
|
||||
'\x1b[?25l' // hide cursor (Ink manages)
|
||||
|
|
@ -1249,13 +1258,13 @@ export default class Ink {
|
|||
* the first alt-screen frame (and first main-screen frame on exit) is
|
||||
* a full redraw with no stale diff state.
|
||||
*/
|
||||
setAltScreenActive(active: boolean, mouseTracking = false): void {
|
||||
setAltScreenActive(active: boolean, mouseTracking: MouseTrackingMode = 'off'): void {
|
||||
if (this.altScreenActive === active) {
|
||||
return
|
||||
}
|
||||
|
||||
this.altScreenActive = active
|
||||
this.altScreenMouseTracking = active && mouseTracking
|
||||
this.altScreenMouseTracking = active ? mouseTracking : 'off'
|
||||
|
||||
// Hover state is alt-screen-scoped: dispatchHover is gated on
|
||||
// altScreenActive, so once we leave the alt screen there's no path to
|
||||
|
|
@ -1269,25 +1278,29 @@ export default class Ink {
|
|||
|
||||
if (active) {
|
||||
this.resetFramesForAltScreen()
|
||||
this.scheduleRender()
|
||||
} else {
|
||||
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.
|
||||
* Switch mouse tracking preset at runtime while the alt screen is
|
||||
* active. Always issues DISABLE first so switching between subsets (e.g.
|
||||
* 'all' → 'wheel') clears mode 1003 instead of leaving it asserted —
|
||||
* DEC private modes have no "set this exact bitmask" form, only
|
||||
* individual set/reset, and tmux's mouse-mode bookkeeping does honor the
|
||||
* reset so the prompt-row "No image in clipboard" spam stops.
|
||||
*/
|
||||
setAltScreenMouseTracking(enabled: boolean): void {
|
||||
if (this.altScreenMouseTracking === enabled) {
|
||||
setAltScreenMouseTracking(mode: MouseTrackingMode): void {
|
||||
if (this.altScreenMouseTracking === mode) {
|
||||
return
|
||||
}
|
||||
|
||||
this.altScreenMouseTracking = enabled
|
||||
this.altScreenMouseTracking = mode
|
||||
|
||||
if (this.altScreenActive) {
|
||||
this.options.stdout.write(enabled ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING)
|
||||
this.options.stdout.write(DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(mode))
|
||||
}
|
||||
}
|
||||
get isAltScreenActive(): boolean {
|
||||
|
|
@ -1340,9 +1353,10 @@ export default class Ink {
|
|||
}
|
||||
|
||||
// Mouse tracking — idempotent, safe to re-assert on every stdin gap.
|
||||
if (this.altScreenMouseTracking) {
|
||||
this.options.stdout.write(ENABLE_MOUSE_TRACKING)
|
||||
}
|
||||
// DISABLE first so we land in the exact preset state even if an
|
||||
// external app or tmux left DEC 1003 hover asserted out from under us
|
||||
// since the last assertion.
|
||||
this.options.stdout.write(DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(this.altScreenMouseTracking))
|
||||
|
||||
// Alt-screen re-entry — destructive (ERASE_SCREEN). Only for callers that
|
||||
// have a strong signal the terminal actually dropped mode 1049.
|
||||
|
|
@ -1398,10 +1412,28 @@ export default class Ink {
|
|||
* stays true. ENTER_ALT_SCREEN is a terminal-side no-op if already in alt.
|
||||
*/
|
||||
private reenterAltScreen(): void {
|
||||
// DISABLE_MOUSE_TRACKING before enableMouseTrackingFor — same as
|
||||
// setAltScreenMouseTracking / AlternateScreen mount / handleResize.
|
||||
// DEC private modes have no atomic "set this bitmask" sequence, only
|
||||
// per-mode set/reset, so for 'wheel'/'buttons' presets we must reset
|
||||
// first to drop any lingering DEC 1003 hover from before re-entry.
|
||||
this.options.stdout.write(
|
||||
ENTER_ALT_SCREEN + ERASE_SCREEN + CURSOR_HOME + (this.altScreenMouseTracking ? ENABLE_MOUSE_TRACKING : '')
|
||||
ENTER_ALT_SCREEN +
|
||||
ERASE_SCREEN +
|
||||
CURSOR_HOME +
|
||||
DISABLE_MOUSE_TRACKING +
|
||||
enableMouseTrackingFor(this.altScreenMouseTracking)
|
||||
)
|
||||
this.resetFramesForAltScreen()
|
||||
// ERASE_SCREEN above leaves the physical alt screen blank, and
|
||||
// resetFramesForAltScreen() seeds prev/back as blank rows×cols, so
|
||||
// nothing on the front frame survives the re-entry. Callers
|
||||
// (handleResume on SIGCONT, the resize self-heal, the stdin-gap
|
||||
// re-assertion) all return early after invoking us, so without an
|
||||
// explicit render schedule the alt screen sits blank until some
|
||||
// unrelated state change fires the next commit. queueing one
|
||||
// microtask matches scheduleRender's normal cadence.
|
||||
this.scheduleRender()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -47,8 +47,53 @@ export const EXIT_ALT_SCREEN = decreset(DEC.ALT_SCREEN_CLEAR)
|
|||
// Mouse tracking: 1000 reports button press/release/wheel, 1002 adds drag
|
||||
// events (button-motion), 1003 adds all-motion (no button held — for
|
||||
// hover), 1006 uses SGR format (CSI < btn;col;row M/m) instead of legacy
|
||||
// X10 bytes. Combined: wheel + click/drag for selection + hover.
|
||||
export const ENABLE_MOUSE_TRACKING =
|
||||
decset(DEC.MOUSE_NORMAL) + decset(DEC.MOUSE_BUTTON) + decset(DEC.MOUSE_ANY) + decset(DEC.MOUSE_SGR)
|
||||
// X10 bytes.
|
||||
//
|
||||
// Modes are addressable as a preset so users can opt out of 1003 (hover),
|
||||
// which is the noisy one inside tmux — every cursor cross of the prompt
|
||||
// row triggers a clipboard probe that surfaces as "No image in clipboard".
|
||||
// Presets:
|
||||
// - 'off' — no DECSET, terminal/tmux native selection + scroll work
|
||||
// - 'wheel' — 1000 + 1006: click + wheel only, no drag, no hover
|
||||
// - 'buttons' — 1000 + 1002 + 1006: adds drag (text selection), no hover
|
||||
// - 'all' — 1000 + 1002 + 1003 + 1006: legacy behavior, hover-driven
|
||||
// UI (scrollbar paginate-on-hover, link mouseenter, etc.)
|
||||
export type MouseTrackingMode = 'all' | 'buttons' | 'off' | 'wheel'
|
||||
|
||||
const MOUSE_NORMAL = decset(DEC.MOUSE_NORMAL)
|
||||
const MOUSE_BUTTON = decset(DEC.MOUSE_BUTTON)
|
||||
const MOUSE_ANY = decset(DEC.MOUSE_ANY)
|
||||
const MOUSE_SGR = decset(DEC.MOUSE_SGR)
|
||||
|
||||
/** Sequence to enable the requested mouse tracking preset, or '' for 'off'. */
|
||||
export function enableMouseTrackingFor(mode: MouseTrackingMode): string {
|
||||
switch (mode) {
|
||||
case 'all':
|
||||
return MOUSE_NORMAL + MOUSE_BUTTON + MOUSE_ANY + MOUSE_SGR
|
||||
|
||||
case 'buttons':
|
||||
return MOUSE_NORMAL + MOUSE_BUTTON + MOUSE_SGR
|
||||
|
||||
case 'wheel':
|
||||
return MOUSE_NORMAL + MOUSE_SGR
|
||||
|
||||
case 'off':
|
||||
return ''
|
||||
|
||||
default:
|
||||
// Defensive fallback: the type system guarantees exhaustiveness, but
|
||||
// JS callers / corrupted config / hot-reloads in dev could reach this
|
||||
// with an unknown value. Without a default, an unmatched mode returns
|
||||
// undefined which then concatenates as the literal string "undefined"
|
||||
// into the terminal byte stream — visibly garbling output. Treat
|
||||
// unknown as 'off' (no DEC sequences) so the worst case is silent
|
||||
// input loss rather than a wrecked screen.
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy alias for the maximal preset (1000 + 1002 + 1003 + 1006). */
|
||||
export const ENABLE_MOUSE_TRACKING = enableMouseTrackingFor('all')
|
||||
/** Reset every mouse mode unconditionally — safe to send when any subset is on. */
|
||||
export const DISABLE_MOUSE_TRACKING =
|
||||
decreset(DEC.MOUSE_SGR) + decreset(DEC.MOUSE_ANY) + decreset(DEC.MOUSE_BUTTON) + decreset(DEC.MOUSE_NORMAL)
|
||||
|
|
|
|||
|
|
@ -77,13 +77,26 @@ describe('applyDisplay', () => {
|
|||
const setBell = vi.fn()
|
||||
|
||||
applyDisplay({ config: { display: { mouse_tracking: false } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe(false)
|
||||
expect($uiState.get().mouseTracking).toBe('off')
|
||||
|
||||
applyDisplay({ config: { display: { mouse_tracking: true, tui_mouse: false } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe(true)
|
||||
expect($uiState.get().mouseTracking).toBe('all')
|
||||
|
||||
applyDisplay({ config: { display: { tui_mouse: false } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe(false)
|
||||
expect($uiState.get().mouseTracking).toBe('off')
|
||||
})
|
||||
|
||||
it('threads mouse_tracking presets through to $uiState', () => {
|
||||
const setBell = vi.fn()
|
||||
|
||||
applyDisplay({ config: { display: { mouse_tracking: 'wheel' } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe('wheel')
|
||||
|
||||
applyDisplay({ config: { display: { mouse_tracking: 'buttons' } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe('buttons')
|
||||
|
||||
applyDisplay({ config: { display: { mouse_tracking: 'all' } } }, setBell)
|
||||
expect($uiState.get().mouseTracking).toBe('all')
|
||||
})
|
||||
|
||||
it('parses display.sections into per-section overrides', () => {
|
||||
|
|
@ -183,15 +196,30 @@ 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)
|
||||
it('defaults to all and prefers canonical mouse_tracking over legacy tui_mouse', () => {
|
||||
expect(normalizeMouseTracking({})).toBe('all')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: false })).toBe('off')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 0 })).toBe('off')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'off' })).toBe('off')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'false' })).toBe('off')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: null, tui_mouse: false })).toBe('all')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: true, tui_mouse: false })).toBe('all')
|
||||
expect(normalizeMouseTracking({ tui_mouse: false })).toBe('off')
|
||||
})
|
||||
|
||||
it('accepts preset strings (wheel/buttons/all) and their aliases', () => {
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'wheel' })).toBe('wheel')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'scroll' })).toBe('wheel')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'buttons' })).toBe('buttons')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'click' })).toBe('buttons')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'all' })).toBe('all')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'full' })).toBe('all')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'on' })).toBe('all')
|
||||
expect(normalizeMouseTracking({ mouse_tracking: ' WHEEL ' })).toBe('wheel')
|
||||
})
|
||||
|
||||
it('falls back to all for unknown strings', () => {
|
||||
expect(normalizeMouseTracking({ mouse_tracking: 'rainbows' })).toBe('all')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ScrollBoxHandle } from '@hermes/ink'
|
||||
import type { MouseTrackingMode, ScrollBoxHandle } from '@hermes/ink'
|
||||
import type { MutableRefObject, ReactNode, RefObject, SetStateAction } from 'react'
|
||||
|
||||
import type { PasteEvent } from '../components/textInput.js'
|
||||
|
|
@ -104,7 +104,7 @@ export interface UiState {
|
|||
detailsModeCommandOverride: boolean
|
||||
info: null | SessionInfo
|
||||
inlineDiffs: boolean
|
||||
mouseTracking: boolean
|
||||
mouseTracking: MouseTrackingMode
|
||||
sections: SectionVisibility
|
||||
showCost: boolean
|
||||
showReasoning: boolean
|
||||
|
|
@ -351,7 +351,7 @@ export interface AppLayoutTranscriptProps {
|
|||
export interface AppLayoutProps {
|
||||
actions: AppLayoutActions
|
||||
composer: AppLayoutComposerProps
|
||||
mouseTracking: boolean
|
||||
mouseTracking: MouseTrackingMode
|
||||
progress: AppLayoutProgressProps
|
||||
status: AppLayoutStatusProps
|
||||
transcript: AppLayoutTranscriptProps
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { forceRedraw } from '@hermes/ink'
|
||||
import { forceRedraw, type MouseTrackingMode } from '@hermes/ink'
|
||||
|
||||
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
||||
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
|
||||
import { HOTKEYS } from '../../../content/hotkeys.js'
|
||||
import { SECTION_NAMES, isSectionName, nextDetailsMode, parseDetailsMode } from '../../../domain/details.js'
|
||||
import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js'
|
||||
import type {
|
||||
ConfigGetValueResponse,
|
||||
ConfigSetResponse,
|
||||
|
|
@ -44,6 +44,30 @@ const flagFromArg = (arg: string, current: boolean): boolean | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
// `/mouse` toggles between full tracking and off when called bare so the
|
||||
// old binary muscle-memory still works. Explicit presets (wheel / buttons /
|
||||
// all) target the tmux-friendly hover-free subsets.
|
||||
const MOUSE_MODE_ALIASES: Record<string, MouseTrackingMode> = {
|
||||
all: 'all',
|
||||
any: 'all',
|
||||
button: 'buttons',
|
||||
buttons: 'buttons',
|
||||
click: 'buttons',
|
||||
full: 'all',
|
||||
off: 'off',
|
||||
on: 'all',
|
||||
scroll: 'wheel',
|
||||
wheel: 'wheel'
|
||||
}
|
||||
|
||||
const mouseModeFromArg = (arg: string, current: MouseTrackingMode): MouseTrackingMode | null => {
|
||||
if (!arg || arg.trim().toLowerCase() === 'toggle') {
|
||||
return current === 'off' ? 'all' : 'off'
|
||||
}
|
||||
|
||||
return MOUSE_MODE_ALIASES[arg.trim().toLowerCase()] ?? null
|
||||
}
|
||||
|
||||
const RESET_WORDS = new Set(['reset', 'clear', 'default'])
|
||||
const CYCLE_WORDS = new Set(['cycle', 'toggle'])
|
||||
|
||||
|
|
@ -105,20 +129,20 @@ export const coreCommands: SlashCommand[] = [
|
|||
|
||||
{
|
||||
aliases: ['scroll'],
|
||||
help: 'toggle mouse/wheel tracking [on|off|toggle]',
|
||||
help: 'set mouse tracking preset [on|off|toggle|wheel|buttons|all]',
|
||||
name: 'mouse',
|
||||
run: (arg, ctx) => {
|
||||
const current = ctx.ui.mouseTracking
|
||||
const next = flagFromArg(arg, current)
|
||||
const next = mouseModeFromArg(arg, current)
|
||||
|
||||
if (next === null) {
|
||||
return ctx.transcript.sys('usage: /mouse [on|off|toggle]')
|
||||
return ctx.transcript.sys('usage: /mouse [on|off|toggle|wheel|buttons|all]')
|
||||
}
|
||||
|
||||
patchUiState({ mouseTracking: next })
|
||||
ctx.gateway.rpc<ConfigSetResponse>('config.set', { key: 'mouse', value: next ? 'on' : 'off' }).catch(() => {})
|
||||
ctx.gateway.rpc<ConfigSetResponse>('config.set', { key: 'mouse', value: next }).catch(() => {})
|
||||
|
||||
queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next ? 'on' : 'off'}`))
|
||||
queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next}`))
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { MouseTrackingMode } from '@hermes/ink'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { resolveDetailsMode, resolveSections } from '../domain/details.js'
|
||||
|
|
@ -9,8 +10,8 @@ import type {
|
|||
} from '../gatewayTypes.js'
|
||||
import {
|
||||
DEFAULT_VOICE_RECORD_KEY,
|
||||
parseVoiceRecordKey,
|
||||
type ParsedVoiceRecordKey
|
||||
type ParsedVoiceRecordKey,
|
||||
parseVoiceRecordKey
|
||||
} from '../lib/platform.js'
|
||||
import { asRpcResult } from '../lib/rpc.js'
|
||||
|
||||
|
|
@ -68,16 +69,57 @@ export const normalizeIndicatorStyle = (raw: unknown): IndicatorStyle => {
|
|||
}
|
||||
|
||||
const FALSEY_MOUSE = new Set(['0', 'false', 'no', 'off'])
|
||||
const TRUTHY_MOUSE_ALL = new Set(['1', 'true', 'yes', 'on', 'all', 'full', 'any'])
|
||||
const hasOwn = (obj: object, key: PropertyKey) => Object.prototype.hasOwnProperty.call(obj, key)
|
||||
|
||||
export const normalizeMouseTracking = (display: { mouse_tracking?: unknown; tui_mouse?: unknown }): boolean => {
|
||||
// `display.mouse_tracking` accepts boolean (`true` ⇒ all modes, `false` ⇒ off)
|
||||
// for back-compat, plus the string presets `off|wheel|buttons|all` (aliases:
|
||||
// `on`/`full`/`any`/`1`/`true`/... → `all`; `0`/`false`/`no`/`off` → `off`).
|
||||
// `wheel` enables 1000+1006 — scroll wheel + click only, no drag or hover,
|
||||
// which silences tmux's "No image in clipboard" spam over the prompt row.
|
||||
// `buttons` adds 1002 so terminal-side text selection drags still register.
|
||||
// Legacy `tui_mouse` is honored only if `mouse_tracking` is absent.
|
||||
export const normalizeMouseTracking = (display: {
|
||||
mouse_tracking?: unknown
|
||||
tui_mouse?: unknown
|
||||
}): MouseTrackingMode => {
|
||||
const raw = hasOwn(display, 'mouse_tracking') ? display.mouse_tracking : display.tui_mouse
|
||||
|
||||
if (raw === false || raw === 0) {
|
||||
return false
|
||||
return 'off'
|
||||
}
|
||||
|
||||
return typeof raw === 'string' ? !FALSEY_MOUSE.has(raw.trim().toLowerCase()) : true
|
||||
if (raw === true || raw === undefined || raw === null) {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
if (typeof raw === 'number') {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
if (typeof raw !== 'string') {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
const v = raw.trim().toLowerCase()
|
||||
|
||||
if (FALSEY_MOUSE.has(v)) {
|
||||
return 'off'
|
||||
}
|
||||
|
||||
if (TRUTHY_MOUSE_ALL.has(v)) {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
if (v === 'wheel' || v === 'scroll') {
|
||||
return 'wheel'
|
||||
}
|
||||
|
||||
if (v === 'buttons' || v === 'button' || v === 'click') {
|
||||
return 'buttons'
|
||||
}
|
||||
|
||||
return 'all'
|
||||
}
|
||||
|
||||
const MTIME_POLL_MS = 5000
|
||||
|
|
@ -114,6 +156,7 @@ export async function hydrateFullConfig(
|
|||
): Promise<ConfigFullResponse | null> {
|
||||
const cfg = await quietRpc<ConfigFullResponse>(gw, 'config.get', { key: 'full' })
|
||||
applyDisplay(cfg, setBell, setVoiceRecordKey)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +168,7 @@ export const applyDisplay = (
|
|||
const d = cfg?.config?.display ?? {}
|
||||
|
||||
setBell(!!d.bell_on_complete)
|
||||
|
||||
// Only push the voice record key when the RPC actually returned a
|
||||
// config payload. ``quietRpc()`` collapses failures to ``null``; if we
|
||||
// reset the cached shortcut on every null we would clobber a custom
|
||||
|
|
@ -135,6 +179,7 @@ export const applyDisplay = (
|
|||
if (setVoiceRecordKey && cfg) {
|
||||
setVoiceRecordKey(_voiceRecordKeyFromConfig(cfg))
|
||||
}
|
||||
|
||||
patchUiState({
|
||||
busyInputMode: normalizeBusyInputMode(d.busy_input_mode),
|
||||
compact: !!d.tui_compact,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { MouseTrackingMode } from '@hermes/ink'
|
||||
import { isTermuxTuiMode } from '../lib/termux.js'
|
||||
|
||||
const truthy = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim())
|
||||
|
|
@ -27,13 +28,24 @@ export const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
|||
export const STARTUP_QUERY = (process.env.HERMES_TUI_QUERY ?? '').trim()
|
||||
export const STARTUP_IMAGE = (process.env.HERMES_TUI_IMAGE ?? '').trim()
|
||||
|
||||
// Mouse tracking mode resolution at startup. Per-mode selection (off|wheel|
|
||||
// buttons|all) lives in display.mouse_tracking in config.yaml — these env
|
||||
// vars only set the boot-time default before that config is applied.
|
||||
//
|
||||
// Precedence (highest first):
|
||||
//
|
||||
// - HERMES_TUI_MOUSE_TRACKING (truthy/falsy) explicitly overrides everything.
|
||||
// This is the "force a value" knob and intentionally beats the legacy
|
||||
// kill-switch and the Termux default.
|
||||
// - HERMES_TUI_DISABLE_MOUSE=1 forces mouse off — the legacy kill switch.
|
||||
// - On Termux the default is mouse off so touch selection isn't intercepted
|
||||
// by terminal mouse protocols. Desktop defaults to 'all' to preserve prior
|
||||
// behavior.
|
||||
const mouseTrackingOverride = parseToggle(process.env.HERMES_TUI_MOUSE_TRACKING)
|
||||
const mouseTrackingDisabledLegacy = truthy(process.env.HERMES_TUI_DISABLE_MOUSE)
|
||||
// Mobile selection UX: on Termux default mouse tracking OFF so touch selection
|
||||
// is less likely to be intercepted by terminal mouse protocols. Desktop keeps
|
||||
// prior behavior unless explicitly overridden.
|
||||
export const MOUSE_TRACKING =
|
||||
const resolvedBootMouseEnabled =
|
||||
mouseTrackingOverride ?? (TERMUX_TUI_MODE ? false : !mouseTrackingDisabledLegacy)
|
||||
export const MOUSE_TRACKING: MouseTrackingMode = resolvedBootMouseEnabled ? 'all' : 'off'
|
||||
|
||||
export const NO_CONFIRM_DESTRUCTIVE = truthy(process.env.HERMES_TUI_NO_CONFIRM)
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ All slash commands work unchanged. A few are TUI-owned — they produce richer o
|
|||
| `/usage` | Rich token / cost / context panel |
|
||||
| `/agents` (alias `/tasks`) | Observability overlay — live subagent tree with kill/pause controls, per-branch cost / token / file rollups, turn-by-turn history |
|
||||
| `/reload` | Re-reads `~/.hermes/.env` into the running TUI process so newly added API keys take effect without a restart |
|
||||
| `/mouse` | Toggle mouse tracking on/off at runtime (also persists to `display.mouse_tracking` in `config.yaml`) |
|
||||
| `/mouse [on\|off\|toggle\|wheel\|buttons\|all]` | Pick a mouse tracking preset at runtime (also persists to `display.mouse_tracking` in `config.yaml`). `wheel` (1000+1006) keeps scroll-wheel scrolling without the hover events that make tmux spam "No image in clipboard" over the prompt row; `buttons` adds drag-to-select; `all` is the default with hover-driven UI. |
|
||||
|
||||
Every other slash command (including installed skills, quick commands, and personality toggles) works identically to the classic CLI. See [Slash Commands Reference](../reference/slash-commands.md).
|
||||
|
||||
|
|
@ -190,7 +190,13 @@ display:
|
|||
thinking: expanded # always open
|
||||
tools: expanded # always open
|
||||
activity: collapsed # opt back IN to the activity panel (hidden by default)
|
||||
mouse_tracking: true # disable if your terminal conflicts with mouse reporting
|
||||
mouse_tracking: all # off | wheel | buttons | all (or true/false for back-compat).
|
||||
# wheel — 1000+1006 (scroll + click; no drag, no hover —
|
||||
# recommended inside tmux to silence the prompt-row
|
||||
# "No image in clipboard" spam from hover events)
|
||||
# buttons — adds 1002 for terminal-side drag selection
|
||||
# all — adds 1003 for hover (scrollbar paginate-on-hover,
|
||||
# link mouseenter, etc.)
|
||||
```
|
||||
|
||||
Runtime toggles:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue