import type { DashboardRow } from '../render.js'; import type { NodeStatus } from '../types.js'; export type Tab = 'All' | 'Live' | 'Dormant' | 'Attention' | 'Pinned'; export declare const TABS: readonly Tab[]; /** How the visible rows are ordered. * tree — spanning-tree order, ancestors shown for context (the default). * attention — FLAT list, tiered by where your attention belongs (attached/ * streaming/live), newest-message-first within each tier. * relevance — FLAT list, best query match first (super-search). * recency — FLAT list, newest `created` first. */ export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency'; export declare const SORTS: readonly SortMode[]; /** Does a node belong to this tab's slice? * All — every node. * Live — active | idle. * Dormant — done | dead | canceled. * Attention — needs a human: has pending asks OR is parked on a fault. * Pinned — user-starred (a local, view-only preference; `isPinned`). */ export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean; export interface TreeNode { row: DashboardRow; depth: number; parentId: string | null; childIds: string[]; } export interface Tree { /** Ordered root ids (live-first, then stragglers). */ roots: string[]; nodes: Map; } /** Sort rank for roots/stragglers — live first (active, then idle), dormant * after. Mirrors render.ts statusRank. */ export declare function statusRank(status: NodeStatus): number; /** * Build a spanning tree of the whole canvas. * - `rows` — one DashboardRow per node (display text + status/asks). * - `rootIds` — node ids whose `parent === null` (raw, unsorted). * - `childIdsOf` — a node's children = the nodes it subscribes to (its * reports), in edge order. (= subscriptionsOf(id) node ids.) * * Roots are sorted live-first. The graph is walked DFS-preorder; the FIRST * parent to reach a node owns it (cycle-/multi-parent-safe via `visited`). Any * node never reached from a root (orphaned by a missing subscription edge) is * appended as a depth-0 straggler so "All" is genuinely the whole canvas. */ export declare function buildTree(rows: DashboardRow[], rootIds: string[], childIdsOf: (id: string) => string[]): Tree; /** Splice a node out of an already-built tree in place (mutates `tree`). Used when * a close REAPED an empty node (row + dir hard-deleted) rather than parking it: * the node is gone from the db, so it must vanish from the in-memory tree too or * its stale row lingers on screen (the "press x, nothing happens" bug — an empty * active husk reaped by closeNode but never dropped from `tree.nodes`, so the * attention/flat view kept painting it). Surviving children are re-homed to the * reaped node's parent so a non-leaf reap never orphans them out of view. No-op * if the id is absent. */ export declare function pruneNode(tree: Tree, id: string): void; /** Case-insensitive subsequence match: every char of `query` appears in `text` * in order (gaps allowed). Empty query matches everything. Substrings are a * subsequence, so this subsumes substring matching too. */ export declare function fuzzyMatch(query: string, text: string): boolean; /** Indices in `text` consumed by a greedy left-to-right subsequence match of * `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so * the renderer can highlight them. Empty set when `query` is empty OR does not * fully match (no partial highlights). */ export declare function matchIndices(query: string, text: string): Set; /** One word-wrapped preview line: its text + the column indices WITHIN that text * to highlight (the query match). */ export interface SnippetLine { text: string; hi: Set; } /** Highlight indices for the preview: the literal case-insensitive SUBSTRING span * when present (so "where does this text appear?" is answered exactly), else the * scattered subsequence indices, else empty (empty query). This is a different, * stricter model than the subsequence super-search on purpose — a contiguous span * is what reads as a highlight. */ export declare function highlightIndices(query: string, text: string): Set; /** * Build the preview snippet for `text` under the live `query`: up to `maxLines` * word-wrapped lines (each ≤ `width` cols), WINDOWED so the best match is visible * (long conversations: the match can be thousands of chars in), with the matched * columns flagged for highlight. Empty query (or empty text) → a plain wrap from * the start with no highlight. The snippet string itself is what carries the * highlight indices, so they never drift across the windowing. */ export declare function previewSnippet(query: string, text: string, width: number, maxLines: number): SnippetLine[]; /** The searchable conversation text for a row: EVERY user prompt across the pi * session (`prompts`) when present, else the spawn prompt (`goal`) for a * never-revived node that has no session yet. Searched by super-search and * windowed in the preview, so search matches a prompt from ANYWHERE in the * conversation, not just the first one. */ export declare function promptText(row: DashboardRow): string; /** Does this row match the live query? Super-search spans name (which already * folds in the pi-generated description), kind, short-id, AND every user prompt * in the conversation (`promptText`). Empty query matches everything. */ export declare function queryMatch(query: string, row: DashboardRow): boolean; /** Score how well `query` matches one field, 0 (no match) → 1 (exact). Tiers: * exact > prefix > word-boundary substring > interior substring > subsequence. * An interior match decays slightly the later it starts so leading matches win. */ export declare function fieldScore(query: string, text: string): number; /** Weighted relevance of a row to the query across all searched fields. 0 means * no field matched (excluded from relevance results, same as `queryMatch`). */ export declare function scoreRow(query: string, row: DashboardRow): number; /** The "most recent message" signal for the attention sort: the pi session-file * mtime (ms) the snapshot stamps on each row, falling back to the `created` * birth timestamp when no session file exists. Larger = more recent. * * NOTE (phase-3 reconcile): phase 1 owns the field name on DashboardRow. We read * `mtimeMs` here as the most likely name; if phase 1 lands a different name, * update this single accessor. `created` is ISO 8601, so its epoch-ms is a * monotonic stand-in within the same tier. */ export declare function attentionMtime(row: DashboardRow): number; /** Attention tier (lower = higher priority, shown first): * T0 — attached AND streaming (`viewed && streaming`) * T1 — attached, not streaming (`viewed`) * T2 — streaming, not attached * T3 — live but neither (`status active|idle`) * T4 — everything else (dormant: done/dead/canceled). */ export declare function attentionTier(row: DashboardRow): number; export interface VisibleRow { id: string; depth: number; hasChildren: boolean; collapsed: boolean; matched: boolean; } export interface FlattenOpts { collapsed: Set; tab: Tab; query: string; /** cwd-scope filter: only rows pinned to this dir are directly-matched. null / * undefined = All dirs (no cwd filter). Like the tab predicate, it gates the * matched set — ancestors from other dirs still render dimmed for tree context. */ cwdScope?: string | null; /** Profile-scope filter: only rows belonging to this profile are directly-matched. * undefined = All profiles. It gates matches before ancestor context is added. */ profileScope?: string | undefined; /** Lifecycle filter: when true, `terminal` (one-shot worker) nodes are kept out * of the TOP-LEVEL rows and the flat search results — but, in tree mode, still * appear when you manually expand their parent fold (drilling into a resident). * The resume picker defaults this ON. Undefined/false = every lifecycle. */ residentsOnly?: boolean; /** Ordering. `tree` keeps the spanning tree + ancestor context; `relevance` / * `recency` produce a FLAT ranked list of directly-matched rows. */ sort?: SortMode; /** User-starred node ids (local, view-only). Gates the Pinned tab, and floats * pinned rows to the top of every FLAT sort mode. undefined = none pinned. */ pinned?: Set; } /** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */ export declare function cwdMatch(scope: string | null | undefined, row: DashboardRow): boolean; /** Is this row inside the active profile scope? Undefined = All profiles. */ export declare function profileMatch(scope: string | undefined, row: DashboardRow): boolean; /** Is this row inside the active lifecycle filter? `residentsOnly` hides `terminal` * nodes; off (false/undefined) = every lifecycle. A row with an unknown lifecycle * (older snapshot field absent) is treated as resident so it is never hidden. */ export declare function lifecycleMatch(residentsOnly: boolean | undefined, row: DashboardRow): boolean; /** * Flatten the tree to the ordered list of currently-visible rows. * * Inclusion: a node is shown when it directly matches (tab predicate AND query) * — flagged `matched:true` — OR it is an ANCESTOR of a directly-matched node * (shown for tree context, `matched:false`, dimmed by the renderer). * * Collapse: children are emitted only under an EXPANDED node. A node is expanded * when it is not in `collapsed` — except under a non-empty query, where every * ancestor-of-a-match is force-expanded regardless of `collapsed` so matches are * always reachable. */ export declare function flatten(tree: Tree, opts: FlattenOpts): VisibleRow[];