fix(tui): approval flow + input ergonomics + selection perf

- tui_gateway: route approvals through gateway callback (HERMES_GATEWAY_SESSION/
  HERMES_EXEC_ASK) so dangerous commands emit approval.request instead of
  silently falling through the CLI input() path and auto-denying
- approval UX: dedicated PromptZone between transcript and composer, safer
  defaults (sel=0, numeric quick-picks, no Esc=deny), activity trail line,
  outcome footer under the cost row
- text input: Ctrl+A select-all, real forward Delete, Ctrl+W always consumed
  (fixes Ctrl+Backspace at cursor 0 inserting literal w)
- hermes-ink selection: swap synchronous onRender() for throttled
  scheduleRender() on drag, and only notify React subscribers on presence
  change — no more per-cell paint/subscribe spam
- useConfigSync: silence config.get polling failures instead of surfacing
  'error: timeout: config.get' in the transcript
This commit is contained in:
Brooklyn Nicholson 2026-04-17 10:37:48 -05:00
parent 0219da9626
commit 5b386ced71
15 changed files with 319 additions and 129 deletions

View file

@ -202,6 +202,7 @@ export default class Ink {
// Fired alongside the terminal repaint whenever the selection mutates
// so UI (e.g. footer hints) can react to selection appearing/clearing.
private readonly selectionListeners = new Set<() => void>()
private selectionWasActive = false
// DOM nodes currently under the pointer (mode-1003 motion). Held here
// so App.tsx's handleMouseEvent is stateless — dispatchHover diffs
// against this set and mutates it in place.
@ -1506,10 +1507,16 @@ export default class Ink {
return () => this.selectionListeners.delete(cb)
}
private notifySelectionChange(): void {
this.onRender()
this.scheduleRender()
for (const cb of this.selectionListeners) {
cb()
const active = hasSelection(this.selection)
if (active !== this.selectionWasActive) {
this.selectionWasActive = active
for (const cb of this.selectionListeners) {
cb()
}
}
}