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

View Source

Overlay picker primitive (AD-U3): prompt + ranked list + async cancelable preview -- the fzf-shape that serves every "pick one of N" (sessions, runs, tool-calls, command palette, file mentions). One primitive, many projections (T15's job); this unit builds the component itself.

Substrate honesty

This component renders in ordinary buffer mode -- a ranked list laid out in a normal flow column, no AbsoluteLayer/CellDim full-viewport cover. The overlay-over-inline SUBSTRATE question (does summoning this picker as a screen overlay mean a scoped full-viewport cover via terminal save/restore, or a footer-anchored expansion?) is deferred to the D-PA/T0 verdict and lands with T15/T24, which mount this component inside whichever substrate they choose. Nothing here assumes a particular host; render/2 just needs a context with :available_width (and optionally :available_height, used to size the visible window) the way any other component does.

Filtering

Uses Raxol.UI.ListScorer.rank/4 (the list-item fuzzy scorer) against key_fn.(item) for every item on every query change -- not Raxol.Search.Fuzzy, which searches buffer cells, a different problem.

Props

  • :items -- required, the full unfiltered list.
  • :key_fn -- required, item -> String.t(); derives both the search key and the rendered label for each item.
  • :on_select -- message tag emitted on Enter, default :select. Emits {:component_event, id, {on_select, item}} for the currently selected item; a no-op if the ranked list is empty.
  • :on_cancel -- message tag emitted on Escape, default :cancel. Emits {:component_event, id, on_cancel}.
  • :preview_fn -- optional, item -> {:ok, content} | {:error, reason}, run via Task.async/1 for the currently selected item. When absent, no preview pane renders at all. Must be side-effect-free / cancel-safe: on any selection change the in-flight preview task is killed with Task.shutdown(task, :brutal_kill) (no cleanup, no terminate, no chance to close what it opened), so preview_fn must not perform unguarded side effects that leak on an abrupt kill -- no bare file handles, sockets, ports, or DB connections held across the call. Read a snapshot, compute, return; if it must acquire a resource, wrap it so an abrupt process death can't strand it.

  • :placeholder -- prompt placeholder shown while the query is empty (default "").
  • :visible_height -- rows in the scrollable list window (default 10); windowing is Raxol.UI.ScrollWindow (cursor-follow, edge-anchored).

Behavior

Type-to-filter: printable keys append to the query, Backspace removes the last grapheme; the ranked list and cursor (reset to the top match) update on every change. Up/Down move the cursor, clamped and windowed by ScrollWindow. Enter selects the current item. Escape dismisses.

Stale-preview cancellation (the fzf#3134 lesson)

Every selection change (typing that changes the top match, or Up/Down landing on a different item) cancels the in-flight preview Task for the previous selection via Task.shutdown/2 and starts a new one for the current item, tagging the pending result with the new task's own ref. Two independent guards keep a superseded result from ever reaching the screen:

  1. Task.shutdown/2 kills the old task process and flushes any completion message already sitting in this process's mailbox -- most of the time, that's the end of it.
  2. Even in the race where a result was already in flight before the shutdown call landed, update/2 only applies a {ref, result} (or {:DOWN, ref, ...}) message when ref matches state.preview_ref at the time the message is handled. A result tagged with a superseded ref is pattern-matched into a catch-all clause and dropped -- it never touches state.preview, so it can never render.

A real host process must forward whatever arrives in its mailbox for a spawned preview task straight into this component's update/2 -- the raw {ref, result} / {:DOWN, ref, :process, pid, reason} shapes Task.async/1 itself produces, unwrapped. That's what makes the ref-matching guard above work with zero translation layer.

Summary

Functions

Current preview state.

Current query string.

Current ranked (filtered) result list.

The currently selected item, or nil if the ranked list is empty.

Types

preview_state()

@type preview_state() :: :none | :loading | {:ok, String.t()} | {:error, term()}

t()

@type t() :: %{
  id: term(),
  items: [term()],
  key_fn: (term() -> String.t()),
  on_select: term(),
  on_cancel: term(),
  preview_fn: (term() -> {:ok, String.t()} | {:error, term()}) | nil,
  placeholder: String.t(),
  visible_height: pos_integer(),
  query: String.t(),
  ranked: [Raxol.UI.ListScorer.result()],
  cursor: non_neg_integer(),
  scroll_top: non_neg_integer(),
  preview: preview_state(),
  preview_task: Task.t() | nil,
  preview_ref: reference() | nil,
  style: map(),
  theme: map()
}

Functions

broadcast(msg)

command(cmd)

mount(state)

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

preview(map)

@spec preview(t()) :: preview_state()

Current preview state.

query(map)

@spec query(t()) :: String.t()

Current query string.

ranked(map)

@spec ranked(t()) :: [Raxol.UI.ListScorer.result()]

Current ranked (filtered) result list.

schedule(msg, delay)

selected_item(state)

@spec selected_item(t()) :: term() | nil

The currently selected item, or nil if the ranked list is empty.

unmount(state)

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