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.
This commit is contained in:
Turgut Kural
2026-08-30 23:30:36 +03:00
committed by Teknium
parent ef6d3367a6
commit 3082a34669
9 changed files with 50 additions and 11 deletions
+6
View File
@@ -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.
+10
View File
@@ -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
+1
View File
@@ -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
+7 -1
View File
@@ -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
}
+1
View File
@@ -499,6 +499,7 @@ export interface GatewayEventHandlerContext {
system: {
bellOnComplete: boolean
bellOnClarify?: boolean
bellOnApproval?: boolean
stdout?: NodeJS.WriteStream
sys: (text: string) => void
}
+19 -8
View File
@@ -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<ConfigFullResponse | null> {
const cfg = await quietRpc<ConfigFullResponse>(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
+4 -2
View File
@@ -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,
+1
View File
@@ -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. */
+1
View File
@@ -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