{"version":3,"file":"workflowPicker.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","matchesQuery","DoomOverlay","matchesKey","isControlInput","DOOM_OVERLAY_ACCENT","fit","pathTail"],"sources":["../../../../src/tui/workflow/workflowPicker.ts"],"sourcesContent":["/**\n * Scrollable, filterable list picker for every long list this extension shows.\n *\n * Replaces a bare `ui.select`, which wrapped each `name · detail` pair across\n * two or three terminal rows and left no way to reach a row other than scrolling\n * past all of them. This is a doom overlay instead: one row per item, a windowed\n * list that scrolls with the selection, and a filter field that narrows as you\n * type.\n *\n * Generic over the value it hands back. `SPC w c` picks a failed run to recover\n * through it; browsing and launching workflows moved to `workflowCatalog.ts`,\n * which needs the detail on screen beside the list rather than behind a pick.\n *\n * Rendering is pure, so the surface can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey } from '@earendil-works/pi-tui';\n\nimport {\n  DOOM_FULLSCREEN_UI_OPTIONS,\n  DOOM_OVERLAY_ACCENT,\n  DoomOverlay,\n  type DoomOverlayChrome,\n  type DoomOverlayTui,\n} from './doomOverlay';\nimport { CURSOR_BLOCK, fit, isControlInput, matchesQuery, pathTail, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PICKER_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_NAME = '(unnamed)';\nconst KEY_CLEAR = '\\x15';\n/** Rows the filter field and its trailing spacer take from the body. */\nconst FILTER_ROWS = 3;\n/** Columns the marker and its trailing space take from a row. */\nconst MARKER_COLUMNS = 2;\n/** Blank columns between the name column and the detail column. */\nconst COLUMN_GAP = 2;\nconst MIN_NAME_WIDTH = 16;\nconst NAME_RATIO = 0.45;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface PickerRow<T> {\n  /** Stable identity, used only to keep rows distinct. */\n  key: string;\n  /** Left column. */\n  name: string;\n  /** Right column, trimmed from the front so its tail survives. */\n  detail: string;\n  /** Extra text the filter should match but the row does not show. */\n  search?: string;\n  value: T;\n}\n\nexport interface WorkflowPickerConfig {\n  title: string;\n  breadcrumb: string;\n  /** Plural noun for the header count, e.g. `workflows`. */\n  unit: string;\n  /** Verb on the enter key cap, e.g. `launch`. */\n  action: string;\n  filterPlaceholder: string;\n  /** Shown when the source list itself is empty, as opposed to over-filtered. */\n  emptyMessage: string;\n}\n\nexport function filterPickerRows<T>(rows: readonly PickerRow<T>[], query: string): PickerRow<T>[] {\n  if (!query.trim()) return [...rows];\n  return rows.filter((row) => matchesQuery([row.name, row.detail, row.search ?? ''].join(' '), query));\n}\n\nexport class WorkflowPickerComponent<T> extends DoomOverlay {\n  private query = '';\n  private selected = 0;\n  private bodyHeight = 1;\n\n  constructor(\n    tui: DoomOverlayTui,\n    theme: Theme,\n    private readonly config: WorkflowPickerConfig,\n    private readonly rows: readonly PickerRow<T>[],\n    private readonly done: (value: T | undefined) => void,\n  ) {\n    super(tui, theme);\n  }\n\n  private matches(): PickerRow<T>[] {\n    return filterPickerRows(this.rows, this.query);\n  }\n\n  /** Rows the list can draw, once the filter field has taken its share. */\n  private capacity(): number {\n    return Math.max(1, this.bodyHeight - FILTER_ROWS);\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n      this.done(undefined);\n      return;\n    }\n    const matches = this.matches();\n    if (matchesKey(data, 'enter')) {\n      const row = matches[Math.min(this.selected, matches.length - 1)];\n      if (row) this.done(row.value);\n      return;\n    }\n\n    if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.move(-1, matches.length);\n    else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n')) this.move(1, matches.length);\n    else if (matchesKey(data, 'pageUp')) this.move(-this.capacity(), matches.length);\n    else if (matchesKey(data, 'pageDown')) this.move(this.capacity(), matches.length);\n    else if (matchesKey(data, 'backspace')) this.setQuery(this.query.slice(0, -1));\n    else if (data === KEY_CLEAR) this.setQuery('');\n    else if (!isControlInput(data)) this.setQuery(this.query + data);\n    else return;\n\n    this.tui.requestRender();\n  }\n\n  /** Editing the filter resets the cursor: the previous index named another row. */\n  private setQuery(query: string): void {\n    if (query === this.query) return;\n    this.query = query;\n    this.selected = 0;\n  }\n\n  private move(delta: number, count: number): void {\n    if (count === 0) return;\n    this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n  }\n\n  protected getChrome(): DoomOverlayChrome {\n    const matches = this.matches();\n    const scope = this.query ? `${matches.length} of ${this.rows.length}` : `${this.rows.length}`;\n    return {\n      title: this.config.title,\n      accent: DOOM_OVERLAY_ACCENT,\n      breadcrumb: this.config.breadcrumb,\n      headerRight: `${scope} ${this.config.unit}`,\n      footer: `↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,\n      footerHints: [\n        ['↑↓', 'select'],\n        ['enter', this.config.action],\n        ['type', 'filter'],\n        ['ctrl+u', 'clear'],\n        ['esc', 'cancel'],\n      ],\n      footerRight:\n        matches.length > 0 ? `${Math.min(this.selected, matches.length - 1) + 1}/${matches.length}` : 'no match',\n    };\n  }\n\n  protected renderBody(width: number, height: number): string[] {\n    this.bodyHeight = height;\n    return [...this.filterField(width), ...this.listLines(width)].slice(0, height);\n  }\n\n  private filterField(width: number): string[] {\n    const value = this.query\n      ? `${this.query}${CURSOR_BLOCK}`\n      : `${CURSOR_BLOCK}${this.theme.fg(DIM, this.config.filterPlaceholder)}`;\n    return [this.theme.fg(DIM, FILTER_LABEL), this.theme.bg(SELECTED_BG, fit(` ${value}`, width)), ''];\n  }\n\n  private listLines(width: number): string[] {\n    if (this.rows.length === 0) return [this.theme.fg(DIM, this.config.emptyMessage)];\n    const matches = this.matches();\n    if (matches.length === 0) return [this.theme.fg(DIM, `No ${this.config.unit} match \"${this.query}\".`)];\n\n    const capacity = this.capacity();\n    const active = Math.min(this.selected, matches.length - 1);\n    // The window follows the cursor rather than the other way round, so a filter\n    // that shrinks the list can never strand the selection off-screen.\n    const start = Math.max(0, Math.min(active - capacity + 1, Math.max(0, matches.length - capacity)));\n\n    const nameWidth = Math.max(MIN_NAME_WIDTH, Math.floor((width - MARKER_COLUMNS - COLUMN_GAP) * NAME_RATIO));\n    const detailWidth = Math.max(0, width - MARKER_COLUMNS - COLUMN_GAP - nameWidth);\n    return matches.slice(start, start + capacity).map((row, offset) => {\n      const current = start + offset === active;\n      const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n      const name = row.name || EMPTY_NAME;\n      const label = current ? this.theme.bold(fit(name, nameWidth)) : fit(name, nameWidth);\n      const detail = this.theme.fg(DIM, fit(pathTail(row.detail, detailWidth), detailWidth));\n      const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n      return this.theme.bg(background, fit(`${marker} ${label}${' '.repeat(COLUMN_GAP)}${detail}`, width));\n    });\n  }\n}\n\nexport async function openWorkflowPickerOverlay<T>(\n  ctx: ExtensionContext,\n  config: WorkflowPickerConfig,\n  rows: readonly PickerRow<T>[],\n): Promise<T | undefined> {\n  return ctx.ui.custom<T | undefined>(\n    (tui, theme, _keybindings, done) => new WorkflowPickerComponent(tui, theme, config, rows, done),\n    DOOM_FULLSCREEN_UI_OPTIONS,\n  );\n}\n"],"mappings":"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"}