Raxol.UI.Components.Harness.ApprovalPrompt (Raxol v2.6.1)

View Source

The agent-harness approval gate: shows what an action wants to do, its blast radius, and a keyboard-driven choice of how far to trust it.

Render-dual of the harness protocol's approval exchange (see docs/proposals/in-flight/harness-spec-frontend.md, ยง4 row A6):

  • Payload in โ€” approval_requested{action, blast_radius, options}, read by init/1 as the :action, :blast_radius, :options props.
  • Emitted out โ€” approval_decision{decision, scope}, returned as a command from handle_event/3 in the shape {:approval_decision, %{decision: :allow | :deny, scope: :once | :session | :root}} once the user confirms with Enter.

Built on Raxol.UI.Components.Modal.Rendering.dialog_surface/4 -- the same one-box overlay shell Modal itself renders into -- rather than a second modal implementation. render/2 embeds a Raxol.UI.Components.Harness.BlastRadiusPreview for the safety-critical part of the surface. The overlay positioning (centering, dimming the background) is the caller's job, same as Modal's own demo: wrap this component's render/2 result with Raxol.UI.Components.AbsoluteLayer.dialog_overlay/3, sized with estimate_height/1.

Keyboard

Up/Down arrows and digit keys (1-9) move the selection; digits jump straight to that option's index rather than acting as a shortcut, so Enter is always the one and only way to confirm (a mis-timed digit key can't fire off a decision by itself). Enter emits the decision for whichever option is currently selected.

Default options

Four choices, matching the harness spec's :once/:session/:root scopes (Devin's three-button pattern) plus deny:

  • Allow once -> %{decision: :allow, scope: :once}
  • Allow session -> %{decision: :allow, scope: :session}
  • Allow subtree -> %{decision: :allow, scope: :root} -- :root covers this whole agent spawn subtree, not just the current agent.
  • Deny -> %{decision: :deny, scope: :once} -- denial is inherently a single-decision act; there is no persistent "deny for session/subtree" concept in this gate, so it always carries the narrowest scope.

None of the options carry inherent color-by-danger -- the only visual differentiator between rows is which one currently holds keyboard focus (reverse video, one row, per the tui-design-science skill's "one anchor per screen" rule). Danger lives entirely in the embedded BlastRadiusPreview, not in how "Deny" or "Allow" are painted, so the safe default is never visually fighting the destructive one.

Summary

Functions

Structural line-count estimate of the prompt's total footprint height (frame + content), used to size the dialog_surface/4 box this component renders into and, standalone, by callers positioning the overlay before calling render/2 (e.g. AbsoluteLayer.dialog_overlay/3 needs a matching height to center correctly). Generous by design, like Raxol.UI.Components.Modal.Rendering.estimate_height/1.

Types

option()

@type option() :: %{
  key: atom(),
  label: String.t(),
  decision: :allow | :deny,
  scope: scope()
}

scope()

@type scope() :: :once | :session | :root

t()

@type t() :: %{
  id: String.t() | atom(),
  action: term(),
  blast_radius: Raxol.UI.Components.Harness.BlastRadiusPreview.blast_radius(),
  options: [option()],
  selected_index: non_neg_integer(),
  width: pos_integer(),
  style: map(),
  theme: map()
}

Functions

broadcast(msg)

command(cmd)

estimate_height(state)

@spec estimate_height(t()) :: pos_integer()

Structural line-count estimate of the prompt's total footprint height (frame + content), used to size the dialog_surface/4 box this component renders into and, standalone, by callers positioning the overlay before calling render/2 (e.g. AbsoluteLayer.dialog_overlay/3 needs a matching height to center correctly). Generous by design, like Raxol.UI.Components.Modal.Rendering.estimate_height/1.

mount(state)

Callback implementation for Raxol.UI.Components.Base.Component.mount/1.

schedule(msg, delay)

unmount(state)

Callback implementation for Raxol.UI.Components.Base.Component.unmount/1.

update(props, state)

Callback implementation for Raxol.UI.Components.Base.Component.update/2.