/** * Shared type definitions for pi-lazy-panel. * * Keep this file free of runtime code — types only. */ import type { KEY_SCOPES, PANE_IDS, SESSION_SORT_MODES } from "./constants.ts"; import type { Locale } from "./i18n/index.ts"; // --------------------------------------------------------------------------- // Panes / focus // --------------------------------------------------------------------------- /** One of the three focusable panes. */ export type PaneId = (typeof PANE_IDS)[number]; /** Which set of sessions the sessions pane lists. */ export type ListScope = "current-folder" | "all"; /** Archive visibility is independent of the directory scope. */ export type SessionListView = "normal" | "archived"; export interface SessionListFilter { view: SessionListView; archived: ReadonlySet; } /** Persisted together so archiving and removing pins are one atomic change. */ export interface SessionFileState { pinned: string[]; archived: string[]; } /** Successful commit may still need attention if releasing the lock failed. */ export interface SessionFileUpdate extends SessionFileState { warning?: string; } /** Apply explicit targets to the latest on-disk state, never a stale snapshot. */ export interface SessionFileChange { type: "archive" | "unarchive" | "pin" | "unpin" | "delete"; files: readonly string[]; } /** Sort order of the sessions pane (mirrors pi's /resume picker; `s` cycles them in `SESSION_SORT_MODES` order). */ export type SessionSortMode = (typeof SESSION_SORT_MODES)[number]; /** * Input mode of the panel, shown in the footer (vim-like). `restore` = the * summary menu / custom prompt of TREE Enter is open, `tree` = the full tree * dialog (`a` in the tree pane) is open, `confirm` / `rename` / `info` = the * delete confirmation, the Rename prompt or the Session Info dialog is open; * `export` / `import` / `share` = the dialogs of SESSIONS `e` / `I` / `S`; * `changelog` = the `@` changelog dialog, `usage` = the `u` context-usage box. */ export type PanelMode = | "normal" | "search" | "label" | "restore" | "tree" | "tree-filter" | "confirm" | "rename" | "info" | "usage" | "new" | "fork" | "clone" | "compact" | "export" | "import" | "share" | "changelog" | "settings" | "visual" | "preview"; /** A `key description` pair shown as a hint in the footer or a prompt bar. An empty key means text-only. */ export type KeyHint = [key: string, text: string]; /** * What Enter did (SESSIONS resume / TREE restore): * `switched` = pi now shows another session, `restored` = the leaf moved to the * chosen node, `unchanged` = pi was already there, `compacted` = the current * session's active branch was compacted in place. Failures throw instead. */ export type EnterOutcome = "switched" | "restored" | "unchanged" | "compacted"; /** * How a session file was removed (SESSIONS `d`, same as pi's /resume): * `trash` = moved to the system trash by the `trash` CLI, `unlink` = deleted * for good because `trash` was unavailable or failed. */ export type DeleteMethod = "trash" | "unlink"; /** File format of SESSIONS `e` (pi's /export): the whole tree as HTML, or the active branch as JSONL. */ export type ExportFormat = "html" | "jsonl"; /** * Where SESSIONS `S` uploads, from pi's `sessionShareDestination` (pi 1.3.0): * `auto` = Radius when signed in, otherwise a private GitHub gist; `gist` / * `radius` force one destination (an explicit `radius` never falls back). */ export type SessionShareDestination = "auto" | "gist" | "radius"; /** The destination a share actually used (`auto` resolved to one of these). */ export type ShareDestination = Exclude; /** Where an export would be written, resolved from what the user typed (blank = pi's default name in the cwd). */ export interface ExportTarget { /** Absolute output path. */ path: string; /** A file is already there (the panel asks before overwriting it). */ exists: boolean; } /** What SESSIONS `S` (pi's /share) produced: the viewer link and, for a gist, the gist behind it. */ export interface ShareResult { url: string; /** Only for `destination: "gist"`: the gist page itself. */ gistUrl?: string; destination: ShareDestination; } /** How to sign in to Radius, the two choices pi's `/login radius` offers: an account (OAuth) or an API key. */ export type RadiusLoginMethod = "oauth" | "api_key"; /** One choice of a Radius sign-in menu (browser / device code, supplied by pi's OAuth provider). */ export interface RadiusLoginOption { id: string; label: string; description?: string; } /** * A prompt pi's Radius sign-in asks for. Kept structurally equal to pi-ai's * `AuthPrompt` so the UI layer never has to import pi-ai (not a dependency of * this plugin); `ModelRuntime.login` accepts it as-is. */ export interface RadiusLoginPrompt { type: "text" | "secret" | "select" | "manual_code"; message: string; placeholder?: string; options?: readonly RadiusLoginOption[]; signal?: AbortSignal; } /** A link in an informational sign-in event. */ export interface RadiusLoginLink { url: string; label?: string; } /** * What pi's Radius sign-in reports while it runs: the browser URL or device * code to show, informational text, and progress. Structurally equal to * pi-ai's `AuthEvent`. */ export type RadiusLoginEvent = | { type: "info"; message: string; links?: readonly RadiusLoginLink[] } | { type: "auth_url"; url: string; instructions?: string } | { type: "device_code"; userCode: string; verificationUri: string; intervalSeconds?: number; expiresInSeconds?: number } | { type: "progress"; message: string }; /** The callbacks the panel gives pi's Radius sign-in; `signal` cancels the whole flow. */ export interface RadiusLoginInteraction { signal: AbortSignal; prompt(prompt: RadiusLoginPrompt): Promise; notify(event: RadiusLoginEvent): void; } /** * How TREE Enter leaves the branch it abandons — the three choices of pi's * `/tree`: no summary, a model-written summary of the abandoned branch, or a * summary written with extra instructions. Passed through to `ctx.navigateTree`. */ export interface RestoreOptions { summarize: boolean; /** Extra instructions for the summarizer ("Summarize with custom prompt"). */ customInstructions?: string; } // --------------------------------------------------------------------------- // Data rows // --------------------------------------------------------------------------- /** A row in the sessions (left-top) pane. */ export interface SessionRow { /** Absolute path to the session .jsonl file. */ file: string; /** Session UUID. */ id: string; /** User-defined display name, if any (/name). */ name?: string; /** Working directory the session was created in. */ cwd: string; /** Session file this one was forked from, if any (drives "threaded" sort). */ parentFile?: string; /** Indentation level assigned by "threaded" sort (0 = root). */ threadDepth?: number; /** Model id last used in the session, if known. */ model?: string; /** Short preview of the first user message. */ preview: string; createdAt: number; updatedAt: number; messageCount: number; } /** A row in the tree (left-bottom) pane. */ export interface TreeRow { /** Entry id inside the session. */ entryId: string; /** * Nearest ancestor that is also a row, undefined for roots. Entries that * never become rows (labels, hidden by the filter…) are skipped over, so the * rows always form a consistent forest the UI can draw guide lines for. */ parentId?: string; role: "user" | "assistant" | "system" | "tool"; /** * Coarse category used by the tree filters (same split as pi's /tree): * `message` user / assistant, `tool` tool results, `system` structural * entries always shown (system prompt, bash, compaction, branch summary), * `meta` bookkeeping hidden by default (model / thinking / name changes). */ kind: "message" | "tool" | "system" | "meta"; /** User-defined label on this entry. */ label?: string; /** Timestamp of the latest label change, in milliseconds. */ labelTimestamp?: number; /** Truncated single-line text. */ text: string; timestamp: number; /** Whether this entry is on the currently active branch. */ onActiveBranch: boolean; /** * Restoring here would leave the conversation where it already is (the leaf, * or a message followed only by bookkeeping entries — see `isEffectiveLeaf`), * so Enter skips the summary menu. */ isLeaf?: boolean; } /** * Tree filters (mirror /tree ctrl+d/t/u/l/a: `no-tools` is pi's ctrl+t, * `labeled` pi's `labeled-only`). Set from the tree dialog (d/t/u/l/a); the * pane shows the same filtered tree. */ export type TreeFilter = "default" | "no-tools" | "user-only" | "labeled" | "all"; /** A message block rendered in the content (right) pane. */ export interface ContentBlock { entryId: string; role: "user" | "assistant"; timestamp: number; /** Raw markdown to render. */ markdown: string; } /** A user message the `/fork` selector can fork before (entry id + one-line preview). */ export interface ForkPoint { entryId: string; text: string; } /** Data shown in the Session Info dialog (mirrors /session). */ export interface SessionInfo { name?: string; model?: string; messages: number; tokens: number; cost: number; createdAt: number; updatedAt: number; path: string; /** Working directory the session was created in (pi header `cwd`), full path. */ cwd?: string; id: string; /** Source session this one was forked / cloned from (pi header `parentSession`), full path. */ parentPath?: string; /** Readable title of the source session (its name, else first message); undefined when that file is gone. */ parentName?: string; } /** Theme colour a usage category paints its swatch / bar segment with. */ export type ContextUsageColor = "accent" | "error" | "thinkingMax" | "success" | "mdLink" | "warning" | "customMessageLabel" | "dim"; /** * One row of the context breakdown, keyed by an i18n suffix under `usage.` * (systemPrompt / memory / skills / tools / toolResults / context / other / * freeSpace). Tokens are estimates reconciled against the provider total. */ export interface ContextUsageCategory { key: string; tokens: number; color: ContextUsageColor; /** Markdown source for Enter preview; empty means no content, absent means a statistics-only row. */ prompt?: string; } /** * Data shown in the Context usage dialog (`u`): how full the model's context * window is for a session, and what fills it — pi's footer percentage, broken * down like pi-cc's `/context`. `contextWindow` / `percent` are undefined when * the model (hence the window) is unknown; then only token counts are shown. */ export interface ContextUsageInfo { model?: string; /** Messages on the active branch. */ messages: number; /** Accumulated cost so far, when known. */ cost?: number; /** Total context tokens in use (provider-resolved, floored by the itemized prompt side). */ used: number; /** Context window size of the session's model. */ contextWindow?: number; /** `used / contextWindow`, 0..1; undefined when the window is unknown. */ percent?: number; /** Fraction of the window at which pi auto-compacts (`shouldCompact` threshold); undefined when the window is unknown. */ compactThreshold?: number; /** Tokens still free before the auto-compaction threshold; undefined when unknown. */ compactRemaining?: number; /** Breakdown rows in display order, ending with Other and (when the window is known) Free space. */ categories: ContextUsageCategory[]; } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- /** * Parsed search query. Supports GitHub-style qualifiers: * name:foo model:opus path:bar tag:scan role:user after:2026-09-01 before:2026-09-20 * plus free text. */ export interface SearchQuery { text: string; name?: string; model?: string; path?: string; tag?: string; /** Tree rows: the role (`user`, `assistant`, `system`, `tool`) must contain it. */ role?: string; after?: Date; before?: Date; } /** * The `/` search active in one pane (lazygit-style: the rows are not filtered, * the cursor jumps between the matches with n / N). Kept per pane, so switching * panes does not lose a query. * * 每个面板各自的搜索状态:列表不过滤,只在匹配之间跳转。 */ export interface PaneSearch { /** Raw query as typed after `/`. */ query: string; /** * Matches, ascending: row indices for the list panes (the sessions list; the * whole tree, folded rows included) and body-line indices of the rendered * layout for the content pane. */ matches: number[]; /** Position in `matches` of the current match (the content pane's scroll target), -1 when there is none. */ current: number; } /** * What a pane paints for the search active in it: which of the rows / lines it * renders match, which one is the current match, the terms to highlight and * the counts for its header ("2/7 matches"). */ export interface SearchView { /** Terms to highlight on matching rows (free-text tokens and qualifier values). */ terms: string[]; /** Indices, in what the pane renders, of the matching rows (list panes) or body lines (content pane). */ matches: ReadonlySet; /** Index of the current match, undefined when the cursor is not on one. */ current: number | undefined; /** 1-based position of the current match among the matches, 0 when there is none. */ position: number; total: number; } // --------------------------------------------------------------------------- // Keymap // --------------------------------------------------------------------------- /** Logical actions the panel can perform. Keys are bound to these. */ export type ActionId = // global | "focus-next" | "focus-prev" | "focus-sessions" | "focus-tree" | "focus-content" | "scope-current" | "scope-all" | "help" | "quit" | "search" | "search-next" | "search-prev" | "changelog" | "settings-open" // list navigation | "move-down" | "move-up" | "go-top" | "go-bottom" | "scroll-content-down" | "scroll-content-up" // sessions pane | "session-resume" | "session-delete" | "session-rename" | "session-fork" | "session-toggle-select" | "session-pin" | "session-archive" | "session-archive-view" | "session-export" | "session-import" | "session-share" | "session-clone" | "session-copy-last-reply" | "session-compact" | "session-sort" | "session-new" | "session-info" | "session-context-usage" // tree pane | "tree-restore" | "tree-copy" | "tree-label" | "tree-toggle-label-time" | "tree-open" | "tree-filter-menu" | "tree-fold" // tree dialog only (the dialog also uses the tree pane's actions and the global `search`) | "tree-filter-default" | "tree-filter-no-tools" | "tree-filter-user" | "tree-filter-labeled" | "tree-filter-all" | "tree-dialog-close" // content pane | "content-copy" | "content-center"; // Besides the shared navigation actions (move-down / move-up / go-top / go-bottom) // and the global search, the content pane can copy the highlighted message // (`content-copy`) and center it in the viewport (`content-center`), see docs/design.md. // Tree filters are shared by the pane (`tree-filter-menu`) and the tree dialog // (d/t/u/l/a). The dialog has its own filtering search; pane searches jump between matches. /** * A single key chord in pi-tui key syntax, e.g. "j", "ctrl+d", "tab". * Multi-key sequences are written either verbatim when every step is one * printable character ("gg", "yy") or space-separated ("ctrl+w h"). * An uppercase letter ("G") means shift + that letter. */ export type KeyChord = string; /** * Binding scope: the global scope, one pane, or the tree dialog. Keys are * resolved innermost first: `tree-dialog` → `tree` → `global` inside the * dialog, `` → `global` in a pane (see `scopeChain` in config/keys.ts). */ export type KeyScope = (typeof KEY_SCOPES)[number]; /** Keymap for one scope (a pane, the tree dialog or the global scope). */ export type PaneKeymap = Partial>; /** Full keymap: global bindings plus per-scope overrides. */ export type Keymap = Record; // --------------------------------------------------------------------------- // User config // --------------------------------------------------------------------------- /** Per-scope keymap in the user file. `null` unbinds an action. */ export type UserPaneKeymap = Partial>; /** Shape of `~/.pi/agent/lazy-panel.json`. All fields optional. */ export interface UserConfig { keymap?: Partial>; /** UI 语言("en" / "zh");缺省时按系统语言自动检测。 */ locale?: Locale; defaultScope?: ListScope; defaultSort?: SessionSortMode; leftColumnRatio?: number; /** Not a real setting: accepted only so writing it produces a warning (the panel follows pi's theme). */ theme?: unknown; }