/** * Startup history-session picker: a raw-mode selector shown BEFORE the main * TUI mounts, so launching without an explicit session id lands on a choice * instead of a fresh main screen. * * Reading is lazy. The first page is inspected up front and only then painted, * so nothing on screen is a raw id waiting to turn into a title; older sessions * are read only when the user reaches for them — pressing past the last row, or * filtering, which has to look deeper than the rows on screen. The visible page * is always nine rows so digits 1-9 map onto every on-screen item. */ import type { Context } from '@deepseek-ai/cordis'; import { openResumableSessionPager, type ResumableSession } from './session-list.js'; import { type TerminalCapabilities } from './terminal-caps.js'; /** What the launch picker decided. */ export type SessionPickerResult = { kind: 'resume'; id: string; } | { kind: 'attach'; id: string; sock: string; } | { kind: 'new'; } | null; /** * Visible rows in the sliding window. Locked to nine so every on-screen * session has a 1-9 shortcut; remaining history is reached with ↑/↓ / filter. */ export declare const SESSION_PICKER_WINDOW = 9; /** Digit shortcuts into the current window when the filter is empty. */ export declare const PICKER_QUICK_KEYS = "123456789"; /** Mutable picker UI state. */ export interface SessionPickerState { sessions: ResumableSession[]; query: string; cursor: number; /** * When true, digits type into the filter instead of acting as 1-9 / 0 * shortcuts. Set automatically by the first letter, or by `/` / Ctrl+F. */ filterActive: boolean; /** A page is being read right now (the first one, or a lazy load). */ loading?: boolean; /** Sessions in history that have not been read yet. */ more?: number; /** * A row whose session another window is attached to, waiting for the user to * confirm. Submitting such a row does not attach on its own: the lock says a * window is on that session, and attaching kicks that window. The note under * the row cannot prove the window is still alive, so the picker asks once * instead of either refusing forever or silently stealing the session. */ confirmTakeover?: { id: string; sock: string; pid: number; }; } /** One key / control action against {@link SessionPickerState}. */ export type SessionPickerAction = { type: 'move'; delta: number; } | { type: 'page'; delta: number; } | { type: 'home'; } | { type: 'end'; } | { type: 'type'; text: string; } | { type: 'backspace'; } | { type: 'clearQuery'; } | { type: 'startFilter'; } | { type: 'quick'; key: string; } | { type: 'submit'; } | { type: 'new'; } | { type: 'escape'; } | { type: 'cancel'; }; /** Result of applying one action: keep going, or the picker is done. */ export type SessionPickerStep = { kind: 'continue'; state: SessionPickerState; } | { kind: 'done'; result: SessionPickerResult; }; /** Haystack used by the filter: title, id, cwd, and a live pid when attached. */ export declare function sessionSearchHaystack(session: ResumableSession): string; /** Whether one session matches a whitespace-separated query (every token). */ export declare function sessionMatchesQuery(session: ResumableSession, query: string): boolean; /** * Whether a live Host is holding this session in a window *right now*. * * The lock's `state` is written by the Host: `attached` while a display relay is * connected, `paused` / `running-detached` once the window is gone (a dropped * link, a closed window) and the Host stayed behind. Only an explicit * `attached` counts — a lock from before the field existed has no state, and * treating "unknown" as "in use" would block a resume that used to work. */ export declare function sessionAttachedElsewhere(session: ResumableSession): boolean; /** Sessions still visible under the current filter, in list order. */ export declare function filterResumableSessions(sessions: readonly ResumableSession[], query: string): ResumableSession[]; /** Keep `cursor` inside `[0, total)`. Empty lists pin to 0. */ export declare function clampPickerCursor(cursor: number, total: number): number; /** * How many session rows to show. Prefer a full page of nine so 1-9 always * map onto every on-screen item. Shrink only when the tty cannot fit that * page (title + filter + nine items + hints). */ export declare function pickerCapacity(rows: number): number; /** Whether two picker states would paint the same frame. */ export declare function pickerStateUnchanged(previous: SessionPickerState, next: SessionPickerState): boolean; /** * Whether this input asked for sessions the picker has not read yet. * * Older history is read lazily, so the trigger matters: while filtering, a page * that leaves fewer matches than fit on screen is deepened (a search has to look * past the rows on screen); otherwise the user has to press past the last loaded * row — pressing Down at the end, PageDown, or End — before older sessions are * read. */ export declare function pickerWantsMorePage(input: { previous: Pick; next: Pick; actions: readonly SessionPickerAction[]; more: number; windowSize?: number; }): boolean; /** * Apply one picker action. Pure: the TTY layer maps keystrokes onto this, and * tests drive it without stdin. */ export declare function stepPicker(state: SessionPickerState, action: SessionPickerAction, windowSize?: number): SessionPickerStep; /** Map one decoded stdin chunk (or a complete CSI sequence) to actions. */ export declare function actionsForInput(text: string, state?: Pick): SessionPickerAction[]; /** Split a decoded stdin chunk into CSI/SS3 sequences and individual characters. */ export declare function splitPickerInput(text: string): string[]; /** * Feed a decoded stdin chunk through the picker. CSI / SS3 sequences are one * unit; ordinary text is applied character by character so a pasted "fix 2" * types the digit instead of treating it as a 1-9 shortcut. */ export declare function feedPicker(state: SessionPickerState, text: string, windowSize?: number, onAction?: (action: SessionPickerAction) => void): SessionPickerStep; /** * Show the picker and wait for a selection, a new-session request, or cancel. * Restores the terminal before resolving; an AbortSignal cancels as well. * @param ctx - boot context supplying sessionPersistence. * @param color - whether to apply ANSI colors. * @param signal - optional abort signal (fiber dispose) to cancel the picker. * @returns the selection, or null when cancelled. */ /** Injection seams for tests; production uses the TTY and the session index. */ export interface SessionPickerOptions { stdin?: NodeJS.ReadStream; stdout?: NodeJS.WriteStream; openPager?: typeof openResumableSessionPager; /** The terminal to act on; defaults to reading the environment (tests inject). */ terminalCaps?: TerminalCapabilities; } export declare function showSessionPicker(ctx: Context, color: boolean, signal?: AbortSignal, options?: SessionPickerOptions): Promise;