From 3082a34669504846bc355baecf6f88c567dc33d8 Mon Sep 17 00:00:00 2001 From: Turgut Kural <58116817+TurgutKural@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:30:36 +0300 Subject: [PATCH] feat(cli,tui): add display.bell_on_approval + fix eslint error - display.bell_on_approval (default false): same BEL mechanism as bell_on_complete, rings when a dangerous-command approval prompt opens (_approval_callback / approval.request event). Complements bell_on_clarify from the previous commit. - fix(ui-tui): eslint curly error in useConfigSync.applyDisplay (if without braces) that failed the CI JS & TS checks job. --- cli-config.yaml.example | 6 +++++ cli.py | 10 ++++++++ hermes_cli/config_defaults.py | 1 + ui-tui/src/app/createGatewayEventHandler.ts | 8 +++++- ui-tui/src/app/interfaces.ts | 1 + ui-tui/src/app/useConfigSync.ts | 27 +++++++++++++++------ ui-tui/src/app/useMainApp.ts | 6 +++-- ui-tui/src/gatewayTypes.ts | 1 + website/docs/user-guide/configuration.md | 1 + 9 files changed, 50 insertions(+), 11 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index ce459c12f1..a47f4783b3 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -1774,6 +1774,12 @@ display: # false: Silent (default) bell_on_clarify: false + # Play terminal bell when a dangerous-command approval prompt opens. + # Same mechanism as bell_on_complete (\\a) — works over SSH. + # true: Ring on every approval prompt + # false: Silent (default) + bell_on_approval: false + # Show model reasoning/thinking before each response. # When enabled, a dim box shows the model's thought process above the response. # Toggle at runtime with /reasoning show or /reasoning hide. diff --git a/cli.py b/cli.py index cb58316749..e4648fc258 100644 --- a/cli.py +++ b/cli.py @@ -5261,6 +5261,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self.bell_on_complete = CLI_CONFIG["display"].get("bell_on_complete", False) # bell_on_clarify: play terminal bell (\a) when agent asks a clarify question — same mechanism as bell_on_complete self.bell_on_clarify = CLI_CONFIG["display"].get("bell_on_clarify", False) + # bell_on_approval: play terminal bell (\a) when a dangerous-command approval prompt opens — same mechanism as bell_on_complete + self.bell_on_approval = CLI_CONFIG["display"].get("bell_on_approval", False) # show_reasoning: display model thinking/reasoning before the response self.show_reasoning = CLI_CONFIG["display"].get("show_reasoning", True) # reasoning_full: when reasoning display is on, print the post-response @@ -16537,6 +16539,14 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): } self._approval_deadline = _time.monotonic() + timeout + # Bell on approval (same mechanism as bell_on_complete — \a over SSH) + if getattr(self, "bell_on_approval", False): + try: + sys.stdout.write("\a") + sys.stdout.flush() + except Exception: + pass + # Modal prompt — paint immediately, bypassing the throttle/resize # guard. A throttled paint here can be silently dropped (250ms # window collision or in-flight resize), leaving the panel unseen so diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 26f7782746..9bc56a7bc7 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -1476,6 +1476,7 @@ DEFAULT_CONFIG = { "tui_agents_nudge": True, "bell_on_complete": False, "bell_on_clarify": False, + "bell_on_approval": False, # Stream the model's reasoning/thinking live before the response. # Default ON: on thinking models the reasoning phase can run tens of # seconds, and with this off the user stares at a spinner the whole diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 48c165f643..9704eb149a 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -420,7 +420,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: const { rpc } = ctx.gateway const { STARTUP_RESUME_ID, newSession, recoverSidRef, resumeById, setCatalog } = ctx.session - const { bellOnClarify, bellOnComplete, stdout, sys } = ctx.system + const { bellOnApproval, bellOnClarify, bellOnComplete, stdout, sys } = ctx.system const { appendMessage, panel, setHistoryItems } = ctx.transcript const { setInput } = ctx.composer const { submitLiteralRef, submitRef } = ctx.submission @@ -1250,6 +1250,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: } }) setStatus('waiting for input…') + // Same BEL mechanism as bell_on_complete — works over SSH, triggers tmux bell-action if (bellOnClarify && stdout?.isTTY) { stdout.write('\x07') @@ -1274,6 +1275,11 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: }) setStatus('approval needed') + // Same BEL mechanism as bell_on_complete — works over SSH, triggers tmux bell-action + if (bellOnApproval && stdout?.isTTY) { + stdout.write('\x07') + } + return } diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index c19b35e5b7..efa2a3452f 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -499,6 +499,7 @@ export interface GatewayEventHandlerContext { system: { bellOnComplete: boolean bellOnClarify?: boolean + bellOnApproval?: boolean stdout?: NodeJS.WriteStream sys: (text: string) => void } diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index 2139cd72c1..8effe2dbba 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -254,10 +254,11 @@ export async function hydrateFullConfig( gw: GatewayClient, setBell: (v: boolean) => void, setVoiceRecordKey?: (v: ParsedVoiceRecordKey) => void, - setBellOnClarify?: (v: boolean) => void + setBellOnClarify?: (v: boolean) => void, + setBellOnApproval?: (v: boolean) => void ): Promise { const cfg = await quietRpc(gw, 'config.get', { key: 'full' }) - applyDisplay(cfg, setBell, setVoiceRecordKey, setBellOnClarify) + applyDisplay(cfg, setBell, setVoiceRecordKey, setBellOnClarify, setBellOnApproval) return cfg } @@ -266,13 +267,21 @@ export const applyDisplay = ( cfg: ConfigFullResponse | null, setBell: (v: boolean) => void, setVoiceRecordKey?: (v: ParsedVoiceRecordKey) => void, - setBellOnClarify?: (v: boolean) => void + setBellOnClarify?: (v: boolean) => void, + setBellOnApproval?: (v: boolean) => void ) => { const d = cfg?.config?.display ?? {} const approvals = cfg?.config?.approvals setBell(!!d.bell_on_complete) - if (setBellOnClarify) setBellOnClarify(!!d.bell_on_clarify) + + if (setBellOnClarify) { + setBellOnClarify(!!d.bell_on_clarify) + } + + if (setBellOnApproval) { + setBellOnApproval(!!d.bell_on_approval) + } applyConfiguredTuiTheme(d.tui_theme) @@ -318,6 +327,7 @@ export function useConfigSync({ gw, setBellOnComplete, setBellOnClarify, + setBellOnApproval, setVoiceEnabled, setVoiceRecordKey, sid @@ -343,8 +353,8 @@ export function useConfigSync({ // mcp_rev) look like an MCP change and fire a needless reload.mcp. mcpRevRef.current.accepted = String(r?.mcp_rev ?? '') }) - void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey, setBellOnClarify) - }, [gw, setBellOnComplete, setBellOnClarify, setVoiceEnabled, setVoiceRecordKey, sid]) + void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey, setBellOnClarify, setBellOnApproval) + }, [gw, setBellOnComplete, setBellOnClarify, setBellOnApproval, setVoiceEnabled, setVoiceRecordKey, sid]) useEffect(() => { if (!sid) { @@ -391,18 +401,19 @@ export function useConfigSync({ ) } - void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey, setBellOnClarify) + void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey, setBellOnClarify, setBellOnApproval) }) }, MTIME_POLL_MS) return () => clearInterval(id) - }, [gw, setBellOnComplete, setBellOnClarify, setVoiceRecordKey, sid]) + }, [gw, setBellOnComplete, setBellOnClarify, setBellOnApproval, setVoiceRecordKey, sid]) } export interface UseConfigSyncOptions { gw: GatewayClient setBellOnComplete: (v: boolean) => void setBellOnClarify?: (v: boolean) => void + setBellOnApproval?: (v: boolean) => void setVoiceEnabled: (v: boolean) => void setVoiceRecordKey?: (v: ParsedVoiceRecordKey) => void sid: null | string diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index bbd158f4f2..7bac7a5305 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -208,6 +208,7 @@ export function useMainApp(gw: GatewayClient) { const goodVibesTick = useStore($goodVibesTick) const [bellOnComplete, setBellOnComplete] = useState(false) const [bellOnClarify, setBellOnClarify] = useState(false) + const [bellOnApproval, setBellOnApproval] = useState(false) const ui = useStore($uiState) const overlay = useStore($overlayState) @@ -579,7 +580,7 @@ export function useMainApp(gw: GatewayClient) { } }, [ui.busy, turnStartedAt]) - useConfigSync({ gw, setBellOnComplete, setBellOnClarify, setVoiceEnabled, setVoiceRecordKey, sid: ui.sid }) + useConfigSync({ gw, setBellOnComplete, setBellOnClarify, setBellOnApproval, setVoiceEnabled, setVoiceRecordKey, sid: ui.sid }) useBatteryPoll(gw) useEffect(() => { @@ -858,7 +859,7 @@ export function useMainApp(gw: GatewayClient) { setCatalog }, submission: { submitLiteralRef, submitRef }, - system: { bellOnComplete, bellOnClarify, stdout, sys }, + system: { bellOnComplete, bellOnClarify, bellOnApproval, stdout, sys }, transcript: { appendMessage, panel, setHistoryItems }, voice: { setProcessing: setVoiceProcessing, @@ -869,6 +870,7 @@ export function useMainApp(gw: GatewayClient) { }), [ appendMessage, + bellOnApproval, bellOnClarify, bellOnComplete, composerActions.setInput, diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 087271c947..8ec5d76bee 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -80,6 +80,7 @@ export interface ConfigDisplayConfig { battery?: boolean bell_on_complete?: boolean bell_on_clarify?: boolean + bell_on_approval?: boolean busy_input_mode?: string details_mode?: string /** Focus view (/focus) — display-only reduced-output mode. */ diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 581418ab84..ae7b69d091 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1910,6 +1910,7 @@ display: resume_display: full # full (show previous messages on resume) | minimal (one-liner only) bell_on_complete: false # Play terminal bell when agent finishes (great for long tasks) bell_on_clarify: false # Play terminal bell when the agent asks a clarification question (same BEL mechanism, works over SSH) + bell_on_approval: false # Play terminal bell when a dangerous-command approval prompt opens (same BEL mechanism, works over SSH) show_reasoning: true # Show model reasoning/thinking above each response (default: true; toggle with /reasoning show|hide) streaming: false # Stream tokens to terminal as they arrive (real-time output) show_cost: false # Show estimated $ cost in the CLI status bar