mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 00:11:39 +08:00
Compare commits
1 Commits
gemini-cli
...
bb/tui-mou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c275423d0d |
@@ -118,6 +118,53 @@ def test_config_set_yolo_toggles_session_scope():
|
|||||||
server._sessions.clear()
|
server._sessions.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_mouse_writes_tui_mouse(monkeypatch):
|
||||||
|
writes: list[tuple[str, object]] = []
|
||||||
|
cfg = {"display": {}}
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: cfg)
|
||||||
|
monkeypatch.setattr(server, "_write_config_key", lambda path, value: writes.append((path, value)))
|
||||||
|
|
||||||
|
resp_off = server.handle_request({"id": "1", "method": "config.set", "params": {"key": "mouse", "value": "off"}})
|
||||||
|
assert resp_off["result"] == {"key": "mouse", "value": "off"}
|
||||||
|
assert writes[-1] == ("display.tui_mouse", False)
|
||||||
|
|
||||||
|
resp_on = server.handle_request({"id": "2", "method": "config.set", "params": {"key": "mouse", "value": "on"}})
|
||||||
|
assert resp_on["result"] == {"key": "mouse", "value": "on"}
|
||||||
|
assert writes[-1] == ("display.tui_mouse", True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_mouse_toggle_inverts_persisted_value(monkeypatch):
|
||||||
|
# Persisted off → toggle flips on.
|
||||||
|
writes: list[tuple[str, object]] = []
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: {"display": {"tui_mouse": False}})
|
||||||
|
monkeypatch.setattr(server, "_write_config_key", lambda path, value: writes.append((path, value)))
|
||||||
|
|
||||||
|
resp = server.handle_request({"id": "1", "method": "config.set", "params": {"key": "mouse", "value": "toggle"}})
|
||||||
|
assert resp["result"] == {"key": "mouse", "value": "on"}
|
||||||
|
assert writes[-1] == ("display.tui_mouse", True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_mouse_rejects_unknown_value(monkeypatch):
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: {"display": {}})
|
||||||
|
monkeypatch.setattr(server, "_write_config_key", lambda path, value: None)
|
||||||
|
|
||||||
|
resp = server.handle_request({"id": "1", "method": "config.set", "params": {"key": "mouse", "value": "sure"}})
|
||||||
|
assert "error" in resp
|
||||||
|
assert "unknown mouse value" in resp["error"]["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_get_mouse_defaults_on(monkeypatch):
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: {})
|
||||||
|
|
||||||
|
resp = server.handle_request({"id": "1", "method": "config.get", "params": {"key": "mouse"}})
|
||||||
|
assert resp["result"] == {"value": "on"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: {"display": {"tui_mouse": False}})
|
||||||
|
resp_off = server.handle_request({"id": "2", "method": "config.get", "params": {"key": "mouse"}})
|
||||||
|
assert resp_off["result"] == {"value": "off"}
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1951,12 +1951,15 @@ def _(rid, params: dict) -> dict:
|
|||||||
_write_config_key("display.details_mode", "expanded" if nv == "full" else "collapsed")
|
_write_config_key("display.details_mode", "expanded" if nv == "full" else "collapsed")
|
||||||
return _ok(rid, {"key": key, "value": nv})
|
return _ok(rid, {"key": key, "value": nv})
|
||||||
|
|
||||||
if key in ("compact", "statusbar"):
|
if key in ("compact", "statusbar", "mouse"):
|
||||||
|
# compact defaults off, statusbar + mouse default on.
|
||||||
|
defaults = {"tui_compact": False, "tui_statusbar": True, "tui_mouse": True}
|
||||||
|
def_keys = {"compact": "tui_compact", "statusbar": "tui_statusbar", "mouse": "tui_mouse"}
|
||||||
|
def_key = def_keys[key]
|
||||||
raw = str(value or "").strip().lower()
|
raw = str(value or "").strip().lower()
|
||||||
cfg0 = _load_cfg()
|
cfg0 = _load_cfg()
|
||||||
d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {}
|
d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {}
|
||||||
def_key = "tui_compact" if key == "compact" else "tui_statusbar"
|
cur_b = bool(d0.get(def_key, defaults[def_key]))
|
||||||
cur_b = bool(d0.get(def_key, False if key == "compact" else True))
|
|
||||||
if raw in ("", "toggle"):
|
if raw in ("", "toggle"):
|
||||||
nv_b = not cur_b
|
nv_b = not cur_b
|
||||||
elif raw == "on":
|
elif raw == "on":
|
||||||
@@ -2053,6 +2056,9 @@ def _(rid, params: dict) -> dict:
|
|||||||
if key == "statusbar":
|
if key == "statusbar":
|
||||||
on = bool(_load_cfg().get("display", {}).get("tui_statusbar", True))
|
on = bool(_load_cfg().get("display", {}).get("tui_statusbar", True))
|
||||||
return _ok(rid, {"value": "on" if on else "off"})
|
return _ok(rid, {"value": "on" if on else "off"})
|
||||||
|
if key == "mouse":
|
||||||
|
on = bool(_load_cfg().get("display", {}).get("tui_mouse", 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:
|
||||||
@@ -2106,6 +2112,7 @@ _TUI_HIDDEN: frozenset[str] = frozenset({
|
|||||||
_TUI_EXTRA: list[tuple[str, str, str]] = [
|
_TUI_EXTRA: list[tuple[str, str, str]] = [
|
||||||
("/compact", "Toggle compact display mode", "TUI"),
|
("/compact", "Toggle compact display mode", "TUI"),
|
||||||
("/logs", "Show recent gateway log lines", "TUI"),
|
("/logs", "Show recent gateway log lines", "TUI"),
|
||||||
|
("/mouse", "Toggle SGR mouse tracking (turn off if your terminal prints escape codes on mouse move)", "TUI"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Commands that queue messages onto _pending_input in the CLI.
|
# Commands that queue messages onto _pending_input in the CLI.
|
||||||
|
|||||||
1
ui-tui/packages/hermes-ink/index.d.ts
vendored
1
ui-tui/packages/hermes-ink/index.d.ts
vendored
@@ -30,6 +30,7 @@ export { useTerminalViewport } from './src/ink/hooks/use-terminal-viewport.ts'
|
|||||||
export { default as measureElement } from './src/ink/measure-element.ts'
|
export { default as measureElement } from './src/ink/measure-element.ts'
|
||||||
export { createRoot, default as render, renderSync } from './src/ink/root.ts'
|
export { createRoot, default as render, renderSync } from './src/ink/root.ts'
|
||||||
export type { Instance, RenderOptions, Root } from './src/ink/root.ts'
|
export type { Instance, RenderOptions, Root } from './src/ink/root.ts'
|
||||||
|
export { setAltScreenMouseTracking } from './src/ink/set-alt-screen-mouse-tracking.ts'
|
||||||
export { stringWidth } from './src/ink/stringWidth.ts'
|
export { stringWidth } from './src/ink/stringWidth.ts'
|
||||||
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
|
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
|
||||||
export type { Props as TextInputProps } from 'ink-text-input'
|
export type { Props as TextInputProps } from 'ink-text-input'
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ export { useTerminalTitle } from './ink/hooks/use-terminal-title.js'
|
|||||||
export { useTerminalViewport } from './ink/hooks/use-terminal-viewport.js'
|
export { useTerminalViewport } from './ink/hooks/use-terminal-viewport.js'
|
||||||
export { default as measureElement } from './ink/measure-element.js'
|
export { default as measureElement } from './ink/measure-element.js'
|
||||||
export { createRoot, default as render, renderSync } from './ink/root.js'
|
export { createRoot, default as render, renderSync } from './ink/root.js'
|
||||||
|
export { setAltScreenMouseTracking } from './ink/set-alt-screen-mouse-tracking.js'
|
||||||
export { stringWidth } from './ink/stringWidth.js'
|
export { stringWidth } from './ink/stringWidth.js'
|
||||||
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
|
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
|
||||||
|
|||||||
@@ -1094,6 +1094,30 @@ export default class Ink {
|
|||||||
return this.altScreenActive
|
return this.altScreenActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle SGR mouse tracking (DEC 1000/1002/1003/1006) at runtime without
|
||||||
|
* re-entering the alt screen. Updates the internal flag so resize/resume/
|
||||||
|
* reenterAltScreen respect the new state, and writes ENABLE/DISABLE bytes
|
||||||
|
* if we're currently in alt-screen + TTY + not paused.
|
||||||
|
*
|
||||||
|
* Idempotent. Intended for live `/mouse on|off` toggling — the
|
||||||
|
* <AlternateScreen> prop controls setup/teardown at mount/unmount, this
|
||||||
|
* controls in-session switches without a screen flicker.
|
||||||
|
*/
|
||||||
|
setAltScreenMouseTracking(enabled: boolean): void {
|
||||||
|
if (this.altScreenMouseTracking === enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.altScreenMouseTracking = enabled
|
||||||
|
|
||||||
|
if (!this.altScreenActive || this.isPaused || !this.options.stdout.isTTY) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.stdout.write(enabled ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-assert terminal modes after a gap (>5s stdin silence or event-loop
|
* Re-assert terminal modes after a gap (>5s stdin silence or event-loop
|
||||||
* stall). Catches tmux detach→attach, ssh reconnect, and laptop
|
* stall). Catches tmux detach→attach, ssh reconnect, and laptop
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import instances from './instances.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle SGR mouse tracking (DEC 1000/1002/1003/1006) at runtime on the Ink
|
||||||
|
* instance bound to this stdout. No-op if no Ink instance is attached.
|
||||||
|
*
|
||||||
|
* Use this for in-session `/mouse on|off` toggles. The <AlternateScreen>
|
||||||
|
* prop owns setup/teardown at mount/unmount; this function sidesteps the
|
||||||
|
* full alt-screen re-entry so the toggle is flicker-free.
|
||||||
|
*
|
||||||
|
* Updates the instance's internal `altScreenMouseTracking` flag so the
|
||||||
|
* resize / SIGCONT-resume / re-enter-alt paths respect the new state.
|
||||||
|
*
|
||||||
|
* Defaults to `process.stdout` — pass a specific stream for tests or
|
||||||
|
* multi-output setups.
|
||||||
|
*/
|
||||||
|
export function setAltScreenMouseTracking(
|
||||||
|
enabled: boolean,
|
||||||
|
stdout: NodeJS.WriteStream = process.stdout
|
||||||
|
): void {
|
||||||
|
instances.get(stdout)?.setAltScreenMouseTracking(enabled)
|
||||||
|
}
|
||||||
@@ -88,6 +88,31 @@ describe('createSlashHandler', () => {
|
|||||||
expect(ctx.transcript.sys).toHaveBeenCalledWith('details: expanded')
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('details: expanded')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('toggles mouse tracking and persists it', async () => {
|
||||||
|
const ctx = buildCtx()
|
||||||
|
|
||||||
|
expect(getUiState().mouseTracking).toBe(true)
|
||||||
|
expect(createSlashHandler(ctx)('/mouse off')).toBe(true)
|
||||||
|
expect(getUiState().mouseTracking).toBe(false)
|
||||||
|
expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'mouse', value: 'off' })
|
||||||
|
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('mouse off')
|
||||||
|
|
||||||
|
expect(createSlashHandler(ctx)('/mouse')).toBe(true)
|
||||||
|
expect(getUiState().mouseTracking).toBe(true)
|
||||||
|
expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'mouse', value: 'on' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects unknown /mouse args', () => {
|
||||||
|
const ctx = buildCtx()
|
||||||
|
|
||||||
|
createSlashHandler(ctx)('/mouse wat')
|
||||||
|
expect(getUiState().mouseTracking).toBe(true)
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('usage: /mouse [on|off|toggle]')
|
||||||
|
expect(ctx.gateway.rpc).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('shows tool enable usage when names are missing', () => {
|
it('shows tool enable usage when names are missing', () => {
|
||||||
const ctx = buildCtx()
|
const ctx = buildCtx()
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ describe('applyDisplay', () => {
|
|||||||
show_reasoning: true,
|
show_reasoning: true,
|
||||||
streaming: false,
|
streaming: false,
|
||||||
tui_compact: true,
|
tui_compact: true,
|
||||||
|
tui_mouse: false,
|
||||||
tui_statusbar: false
|
tui_statusbar: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,7 @@ describe('applyDisplay', () => {
|
|||||||
expect(s.compact).toBe(true)
|
expect(s.compact).toBe(true)
|
||||||
expect(s.detailsMode).toBe('expanded')
|
expect(s.detailsMode).toBe('expanded')
|
||||||
expect(s.inlineDiffs).toBe(false)
|
expect(s.inlineDiffs).toBe(false)
|
||||||
|
expect(s.mouseTracking).toBe(false)
|
||||||
expect(s.showCost).toBe(true)
|
expect(s.showCost).toBe(true)
|
||||||
expect(s.showReasoning).toBe(true)
|
expect(s.showReasoning).toBe(true)
|
||||||
expect(s.statusBar).toBe(false)
|
expect(s.statusBar).toBe(false)
|
||||||
@@ -48,6 +50,7 @@ describe('applyDisplay', () => {
|
|||||||
const s = $uiState.get()
|
const s = $uiState.get()
|
||||||
expect(setBell).toHaveBeenCalledWith(false)
|
expect(setBell).toHaveBeenCalledWith(false)
|
||||||
expect(s.inlineDiffs).toBe(true)
|
expect(s.inlineDiffs).toBe(true)
|
||||||
|
expect(s.mouseTracking).toBe(true)
|
||||||
expect(s.showCost).toBe(false)
|
expect(s.showCost).toBe(false)
|
||||||
expect(s.showReasoning).toBe(false)
|
expect(s.showReasoning).toBe(false)
|
||||||
expect(s.statusBar).toBe(true)
|
expect(s.statusBar).toBe(true)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 { 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 }) {
|
||||||
@@ -12,7 +11,6 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
<AppLayout
|
<AppLayout
|
||||||
actions={appActions}
|
actions={appActions}
|
||||||
composer={appComposer}
|
composer={appComposer}
|
||||||
mouseTracking={MOUSE_TRACKING}
|
|
||||||
progress={appProgress}
|
progress={appProgress}
|
||||||
status={appStatus}
|
status={appStatus}
|
||||||
transcript={appTranscript}
|
transcript={appTranscript}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export interface UiState {
|
|||||||
detailsMode: DetailsMode
|
detailsMode: DetailsMode
|
||||||
info: null | SessionInfo
|
info: null | SessionInfo
|
||||||
inlineDiffs: boolean
|
inlineDiffs: boolean
|
||||||
|
mouseTracking: boolean
|
||||||
showCost: boolean
|
showCost: boolean
|
||||||
showReasoning: boolean
|
showReasoning: boolean
|
||||||
sid: null | string
|
sid: null | string
|
||||||
@@ -321,7 +322,6 @@ export interface AppLayoutTranscriptProps {
|
|||||||
export interface AppLayoutProps {
|
export interface AppLayoutProps {
|
||||||
actions: AppLayoutActions
|
actions: AppLayoutActions
|
||||||
composer: AppLayoutComposerProps
|
composer: AppLayoutComposerProps
|
||||||
mouseTracking: boolean
|
|
||||||
progress: AppLayoutProgressProps
|
progress: AppLayoutProgressProps
|
||||||
status: AppLayoutStatusProps
|
status: AppLayoutStatusProps
|
||||||
transcript: AppLayoutTranscriptProps
|
transcript: AppLayoutTranscriptProps
|
||||||
|
|||||||
@@ -293,6 +293,23 @@ export const coreCommands: SlashCommand[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
help: 'toggle SGR mouse tracking (wheel + click/drag). Turn off if your terminal prints escape codes on mouse move.',
|
||||||
|
name: 'mouse',
|
||||||
|
run: (arg, ctx) => {
|
||||||
|
const next = flagFromArg(arg, ctx.ui.mouseTracking)
|
||||||
|
|
||||||
|
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 ${next ? 'on' : 'off'}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
help: 'inspect or enqueue a message',
|
help: 'inspect or enqueue a message',
|
||||||
name: 'queue',
|
name: 'queue',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { atom } from 'nanostores'
|
import { atom } from 'nanostores'
|
||||||
|
|
||||||
|
import { MOUSE_TRACKING } from '../config/env.js'
|
||||||
import { ZERO } from '../domain/usage.js'
|
import { ZERO } from '../domain/usage.js'
|
||||||
import { DEFAULT_THEME } from '../theme.js'
|
import { DEFAULT_THEME } from '../theme.js'
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const buildUiState = (): UiState => ({
|
|||||||
detailsMode: 'collapsed',
|
detailsMode: 'collapsed',
|
||||||
info: null,
|
info: null,
|
||||||
inlineDiffs: true,
|
inlineDiffs: true,
|
||||||
|
mouseTracking: MOUSE_TRACKING,
|
||||||
showCost: false,
|
showCost: false,
|
||||||
showReasoning: false,
|
showReasoning: false,
|
||||||
sid: null,
|
sid: null,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { MOUSE_TRACKING } from '../config/env.js'
|
||||||
import { resolveDetailsMode } from '../domain/details.js'
|
import { resolveDetailsMode } from '../domain/details.js'
|
||||||
import type { GatewayClient } from '../gatewayClient.js'
|
import type { GatewayClient } from '../gatewayClient.js'
|
||||||
import type {
|
import type {
|
||||||
@@ -35,6 +36,9 @@ 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,
|
||||||
|
// HERMES_TUI_DISABLE_MOUSE=1 wins — env-var opt-out must outrank config
|
||||||
|
// since the user set it specifically because their terminal is broken.
|
||||||
|
mouseTracking: MOUSE_TRACKING && d.tui_mouse !== false,
|
||||||
showCost: !!d.show_cost,
|
showCost: !!d.show_cost,
|
||||||
showReasoning: !!d.show_reasoning,
|
showReasoning: !!d.show_reasoning,
|
||||||
statusBar: d.tui_statusbar !== false,
|
statusBar: d.tui_statusbar !== false,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
|||||||
if (isCtrl(key, ch, 'c')) {
|
if (isCtrl(key, ch, 'c')) {
|
||||||
cancelOverlayFromCtrlC()
|
cancelOverlayFromCtrlC()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
|
import { AlternateScreen, Box, NoSelect, ScrollBox, setAltScreenMouseTracking, Text } from '@hermes/ink'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { memo } from 'react'
|
import { memo, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import type { AppLayoutProgressProps, AppLayoutProps } from '../app/interfaces.js'
|
import type { AppLayoutProgressProps, AppLayoutProps } from '../app/interfaces.js'
|
||||||
import { $isBlocked } from '../app/overlayStore.js'
|
import { $isBlocked } from '../app/overlayStore.js'
|
||||||
import { $uiState } from '../app/uiStore.js'
|
import { $uiState } from '../app/uiStore.js'
|
||||||
|
import { MOUSE_TRACKING } from '../config/env.js'
|
||||||
import { PLACEHOLDER } from '../content/placeholders.js'
|
import { PLACEHOLDER } from '../content/placeholders.js'
|
||||||
import type { Theme } from '../theme.js'
|
import type { Theme } from '../theme.js'
|
||||||
import type { DetailsMode } from '../types.js'
|
import type { DetailsMode } from '../types.js'
|
||||||
@@ -256,16 +257,24 @@ const ComposerPane = memo(function ComposerPane({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AppLayout = memo(function AppLayout({
|
export const AppLayout = memo(function AppLayout({ actions, composer, progress, status, transcript }: AppLayoutProps) {
|
||||||
actions,
|
const { mouseTracking } = useStore($uiState)
|
||||||
composer,
|
// Freeze <AlternateScreen>'s mouseTracking prop at initial value — runtime
|
||||||
mouseTracking,
|
// toggles go through setAltScreenMouseTracking below. Re-running the
|
||||||
progress,
|
// AlternateScreen insertion effect on prop change would re-enter the
|
||||||
status,
|
// alt-screen (EXIT + ENTER + erase) and flash the frame. Teardown at
|
||||||
transcript
|
// unmount/exit still runs correctly because signal-exit + the final
|
||||||
}: AppLayoutProps) {
|
// useEffect cleanup both emit DISABLE_MOUSE_TRACKING regardless.
|
||||||
|
const initialMouseTracking = useRef(MOUSE_TRACKING).current
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAltScreenMouseTracking(mouseTracking)
|
||||||
|
|
||||||
|
return () => setAltScreenMouseTracking(false)
|
||||||
|
}, [mouseTracking])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlternateScreen mouseTracking={mouseTracking}>
|
<AlternateScreen mouseTracking={initialMouseTracking}>
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<Box flexDirection="row" flexGrow={1}>
|
<Box flexDirection="row" flexGrow={1}>
|
||||||
<TranscriptPane actions={actions} composer={composer} progress={progress} transcript={transcript} />
|
<TranscriptPane actions={actions} composer={composer} progress={progress} transcript={transcript} />
|
||||||
|
|||||||
@@ -60,6 +60,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?: boolean
|
tui_statusbar?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
ui-tui/src/types/hermes-ink.d.ts
vendored
2
ui-tui/src/types/hermes-ink.d.ts
vendored
@@ -77,6 +77,8 @@ declare module '@hermes/ink' {
|
|||||||
|
|
||||||
export function render(node: React.ReactNode, options?: NodeJS.WriteStream | RenderOptions): Instance
|
export function render(node: React.ReactNode, options?: NodeJS.WriteStream | RenderOptions): Instance
|
||||||
|
|
||||||
|
export function setAltScreenMouseTracking(enabled: boolean, stdout?: NodeJS.WriteStream): void
|
||||||
|
|
||||||
export function useApp(): { readonly exit: (error?: Error) => void }
|
export function useApp(): { readonly exit: (error?: Error) => void }
|
||||||
export type RunExternalProcess = () => Promise<void>
|
export type RunExternalProcess = () => Promise<void>
|
||||||
export function useExternalProcess(): (run: RunExternalProcess) => Promise<void>
|
export function useExternalProcess(): (run: RunExternalProcess) => Promise<void>
|
||||||
|
|||||||
Reference in New Issue
Block a user