/** * The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live * transcript, the todo panel, the streaming line, the approval bar, the model * panel, local notices, and the input box with history and slash-command * completion. All state arrives through the transcript store (derived from * the durable session log) plus local input state; the app owns no session * mutation of its own. * * Element construction uses `createElement` (not JSX): the `dsh` source launch * compiles this file through tsx's ESM-only hook, which does not adopt this * package's `jsx: react-jsx` compiler option, and the classic JSX runtime * would demand a React global. * * @module @deepseek-ai/dsh-code/app */ import { type ReactElement } from 'react'; import type { ContentBlock, FileBlock, ImageBlock } from '@deepseek-ai/dsh-llm'; import type { AuthorizationInteraction, AuthorizationStatus } from '@deepseek-ai/dsh-authorization'; import { type ThemeName } from './theme.ts'; import { type LanguageName } from './i18n.ts'; import type { LauncherUpdateStatus } from './update.ts'; import type { TranscriptStore } from './session/store.ts'; import { type TranscriptEntry } from './render/projection.ts'; import { type SubagentAttachmentServices } from './session/attach.ts'; import type { ApprovalStore } from './approval.ts'; import type { CommandsView } from './commands.ts'; import type { ModelDirectory, ModelRow } from './models.ts'; import { type DiscoveredModelView, type ProviderConfiguration, type ProviderSettingsDirectory, type ProviderTargetView } from './provider-settings.ts'; import type { QuestionStore } from './questions.ts'; import type { SkillsView } from './skills.ts'; import type { MentionCandidate } from './mentions.ts'; import type { SubagentFeedView } from './session/subagents.ts'; import type { UsageView } from './render/usage.ts'; import { type JobRow, type SearchRow } from './panels/kernel-panels.ts'; import type { PresetRow } from './presets.ts'; import type { PermissionRow } from './permissions.ts'; import type { PluginRow } from './plugin-inventory.ts'; import type { SessionDirectoryOptions, SessionRow } from './session/session-directory.ts'; import type { GitDiffView, ReviewBranch, ReviewCommit, ReviewSelection } from './git-workflow.ts'; import type { ProviderAuthorizationDirectory, ProviderAuthorizationRow } from './authorization.ts'; import type { FilePathInspection, ImagePathInspection } from './attachments.ts'; export { completionCandidates, stepCompletionIndex } from './completion.ts'; import type { NoticeTone, QueueMutation } from './ui/ui-contract.ts'; export type { NoticeTone, QueueMutation } from './ui/ui-contract.ts'; import { type StyledLine } from './render/lines.ts'; /** Props the runner hands the app; callbacks stay owned by the runner. */ export interface AppProps { /** Event-fed transcript store for the live session. */ store: TranscriptStore; /** Approval-question store fed by the answerer listener. */ approval: ApprovalStore; /** ask_user_question store fed by the single UI provider. */ questions: QuestionStore; /** Live subagent activity feed (child sessions of the current root). */ subagents: SubagentFeedView; /** Live slash-command descriptor list (completion candidates). */ commands: CommandsView; /** Live user-invocable skill catalog (completion candidates). */ skills: SkillsView; /** `provider/model` selection serving this session (updated on /model). */ model: string; /** Effective reasoning effort in force ('' when none), for the /model picker mark. */ effort?: string; /** Working-directory basename the session serves. */ cwd: string; /** Absolute working directory used by session filters and references. */ workspaceRoot: string; /** Git branch name, empty outside a repository. */ branch: string; /** Short session identifier. */ sessionId: string; /** Whether this session was resumed from persistence. */ resumed: boolean; /** Agent preset selected for the current or pending first session. */ mode: string; /** Permission preset selected for the current or pending first session. */ permission: string; /** * Submit one line: slash commands to the registry, other text to the agent. * The optional origin names the session the submission was composed for — * an attachment prepare resolves after the app remounted onto another * session, and the runner drops the stale delivery then. */ dispatch: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void; /** * Submit one line as steering: it joins the turn already running at its next * step boundary instead of waiting for the next turn. Same stale-delivery * guard as {@link dispatch}. */ steer: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void; /** * The FULL current session identity ('' while the first session is pending) * — the stale-delivery origin above. Distinct from the short display id. */ sessionKey: string; /** Interrupt the running turn (Esc); true when a turn was cancelled. */ interrupt: () => boolean; /** Quit: unmount, flush, and request process exit. */ quit: () => void; /** Load the selectable model directory (called when /model opens). */ loadModels: () => Promise; /** Load @mention candidates for the typed query (files + sessions). */ loadMentions: (query: string, signal?: AbortSignal) => Promise; /** Validate draft image paths without committing attachment objects. */ inspectImages: (paths: readonly string[]) => Promise; /** Validate, normalize and persist images immediately before submission. */ prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise; /** Validate draft non-image file paths without committing attachment objects. */ inspectFiles: (paths: readonly string[]) => Promise; /** Persist non-image files immediately before submission as durable file blocks. */ prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise; /** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */ selectModel: (row: ModelRow, effortId?: string) => string; /** The /subagent override label, '' when delegated agents follow the current model. */ subagentModel: string; /** Apply one /subagent model pick; returns the override label. */ setSubagentModel: (row: ModelRow, effortId?: string) => string; /** Drop the /subagent override (delegated agents follow the current model). */ clearSubagentModel: () => void; /** Delete one session subtree; resolves with the outcome line. */ deleteSession: (id: string) => Promise; /** Load provider/settings/credential facts for the optional /model provider stage. */ loadModelProviders?: () => Promise; /** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */ subscribeModelProviders?: (listener: () => void) => () => void; /** Store or rotate one provider credential through the Harness credential service. */ saveModelProviderCredential?: (target: ProviderTargetView, key: string) => Promise; /** Switch a provider route to its subscription channel (drops the key reference). */ enableModelProviderSubscription?: (target: ProviderTargetView) => Promise; /** Remove one writable provider credential without removing its settings profile. */ unsetModelProviderCredential?: (target: ProviderTargetView) => Promise; /** Remove one user-owned provider profile and its page-managed credential. */ removeModelProvider?: (target: ProviderTargetView) => Promise; /** Save endpoint and explicit model capacities through the provider profile. */ saveModelProviderConfiguration?: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise; /** * Interrogate the provider's real endpoint (typed key wins over the stored * credential) for the models it actually serves — the discovery stage of * the provider setup page. */ discoverModelProvider?: (target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string; }, signal?: AbortSignal) => Promise; /** Provider authorization flows and value-free stored-record facts. */ loadProviderAuthorizations?: () => Promise; subscribeProviderAuthorizations?: (listener: () => void) => () => void; beginProviderAuthorization?: (row: ProviderAuthorizationRow, method: string, interaction: AuthorizationInteraction, signal: AbortSignal) => Promise; cancelProviderAuthorization?: (row: ProviderAuthorizationRow) => void; logoutProviderAuthorization?: (row: ProviderAuthorizationRow) => Promise; openAuthorizationUrl?: (url: string) => boolean; copyTextValue?: (text: string) => Promise; /** Cycle to the next mode station (Shift+Tab): a permission preset or a plan switch; returns the notice label. */ cycleMode: () => string; /** Pre-session plan choice: shows the plan badge before the first session exists. */ pendingPlan?: boolean; /** Select or inspect a permission preset without requiring a pre-existing session. */ setPermission: (id: string) => string; /** Export the transcript to a markdown file (/export [path]); reports via notices. */ exportTranscript: (argument: string) => Promise; /** Rename the session (/title ); returns the outcome line for the notice. */ renameTitle: (argument: string) => string; /** Copy the latest complete assistant response; resolves to notice text. */ copyLastResponse: () => Promise; /** Load a complete read-only Git diff for the file-oriented viewport. */ loadGitDiff: (argument: string) => Promise; /** Local branches for the /review picker (absent: the picker hides the branch phase's list). */ listReviewBranches?: (signal?: AbortSignal) => Promise; /** Recent commits on the current branch for the /review picker. */ listReviewCommits?: (signal?: AbortSignal) => Promise; /** Start a model review after applying the read-only permission preset. */ reviewChanges: (selection: ReviewSelection) => void; /** Preset/session/plugin kernel operations. */ loadPresets: () => Promise; switchMode: (id: string) => Promise; /** Load the switchable permission presets for the /permission panel. */ loadPermissions: () => Promise; createSession: (mode?: string) => void; /** Fork the active session at a completed-turn boundary. */ forkSession: (argument: string) => void; loadSessions: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise; loadSessionTranscript: (id: string, signal?: AbortSignal) => Promise; /** Read the current session's usage blocks (projections plus per-turn fold). */ loadUsage: () => Promise; /** * Full-text search over every persisted session (the in-process * session-query engine). Absent when the deployment disabled the row; * /search degrades to a notice instead of opening the panel. */ searchSessions?: (query: string, signal?: AbortSignal) => Promise; /** Load this session's subagent conversations (children by lineage). */ loadSubagents: () => Promise; /** Live subagent attachment: seed + the two real-time buses, runner-wired. */ attachSubagent?: SubagentAttachmentServices; switchSession: (row: SessionRow) => void; cancelSessionSwitch: () => boolean; loadPlugins: () => readonly PluginRow[]; /** Caller-visible background jobs (the host jobs registry, read-only). */ loadJobs: () => readonly JobRow[]; /** Probe the launcher's aligned update plan (read-only; never installs). */ probeUpdate: () => Promise; /** Run the launcher's aligned update; streams sanitized lines; resolves with the exit code. */ applyUpdate: (onLine: (line: string) => void, plan?: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[]; }) => Promise; /** Registers the app's notice channel with the runner (called once on mount). */ onBridgeReady: (bridge: { notify: (text: string, tone?: NoticeTone) => void; }) => void; /** Ordered enabled status items (/statusline config); the runner owns persistence. */ statusline: readonly string[]; /** Persist a new statusline item set; the runner surfaces IO failures as notices. */ saveStatusline: (items: readonly string[]) => void; /** Apply and persist one /language selection; the runner owns the language.json file. */ saveLanguage: (name: LanguageName) => void; /** Apply and persist one /theme selection; the runner owns the theme.json file. */ saveTheme?: (name: ThemeName) => void; /** Whether decorative animations run at startup (animations.json; on by * default). Functional activity indicators remain live when disabled. */ animations?: boolean; /** Apply and persist one /animation toggle; the runner owns the file. */ saveAnimations?: (enabled: boolean) => void; /** Persistent cross-session input history (oldest first); the runner owns the file. */ history: readonly string[]; /** Persist one submitted prompt to the global history file. */ recordHistory: (text: string) => void; /** Mutate one next-turn inbox message; durable inbox splices reconcile the result. */ updateQueued?: (messageId: string, action: QueueMutation) => void; /** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */ applyEditorKeys: () => Promise; } /** * The streaming buffer rendered with a hard size cap: the live region must * ALWAYS fit the terminal, or Ink's erase/rewrite of a dynamic tree taller * than the screen freezes (cursor-up past the top, garbage, no scroll). The * cap counts explicit newlines and terminal wrapping, slicing from the END so * the freshest tokens stay visible while a long reply streams; the complete * text lands in the flushed scrollback once the turn assembles it. * * Body wrap width for a streaming tail. `rowColumns` is the same width * passed to `transcriptEntryLines` (terminal minus the last-column safety); * the hanging prefix then shrinks the body so streamed text and settled * markdown wrap on the same column. */ export declare function streamTailBodyColumns(rowColumns: number, prefix: string, continuationPrefix?: string): number; /** Rows in the exact next-turn inbox order, never transcript append order. */ export declare function queuedInboxRows(entries: readonly TranscriptEntry[], ids: readonly string[]): readonly Extract[]; interface SettledPhysicalRow { /** Stable one-row Static element. */ readonly element: ReactElement; /** The same row model reused by the mutable viewport tail. */ readonly line: StyledLine; } interface SettledRowRecord { /** Physical rows for one settled entry, including roomy-prompt spacers. */ readonly physical: readonly SettledPhysicalRow[]; /** Physical rows this record contributes — the rendered-history-cap unit. */ readonly rows: number; } /** The incremental settled-history cache (see `computeSettledRows`). */ interface SettledRowsCache { /** The exact settled entries the cache covers (the WINDOW: the newest * `entries.length` settled entries, oldest dropped entries excluded). */ entries: TranscriptEntry[]; /** Records keyed by entry identity; mutated in place so the append path * never copies the whole map. */ records: Map; /** The header element (depends only on `resumed`). */ header: ReactElement; /** The `resumed` the header was built with. */ resumed: boolean; /** The toggle state the rows were built with. */ showReasoning: boolean; /** The refreshEpoch the rows was built for; a bump forces a full rebuild. */ epoch: number; /** The terminal width the rows were wrapped for; a change forces a rebuild. */ columns: number; /** Header/hint followed by one element per physical transcript row. */ flat: ReactElement[]; /** Physical transcript rows only (header/hint excluded). */ physical: SettledPhysicalRow[]; /** Settled entries dropped from the window's head (rendering only — the * event log keeps everything; /export reads all of it and Ctrl+O reads its * inspectable entries). */ droppedEntries: number; /** Physical rows the window's entries contribute (excludes header/hint). */ totalRows: number; /** The window overflowed the trim hysteresis; one source-backed replay * (epoch bump) will re-window the cache. The append path never mutates * flat's head, so only ever sees tail appends between remounts. */ needsTrim: boolean; } /** One step of `computeSettledRows`. */ interface SettledRowsResult { cache: SettledRowsCache; /** How many rows had to be BUILT by this step (0 = pure reuse). */ built: number; } /** * The settled `` row set as a PURE incremental state machine (App * drives it from the memo; tests drive it directly and read `built`). * * The settled prefix is permanently final: the projection only APPENDS below * the flush boundary, removes pending rows at or beyond it, and replaces * running tool/retry/command rows there too. So extending the cache never * rescans the old prefix — a grown boundary builds ONLY the newly settled * suffix and reuses every cached element, letting React bail out of unchanged * rows and keeping long histories out of the per-durable-event path (no O(N) * rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place * on the append/toggle paths to stay O(delta). * * RENDERED-HISTORY CAP: the window holds at most `rowCap` physical rows of * settled transcript (header and hint reserved on top). The cap exists only * here — the event log, the store projection, /export, and /resume keep the * full history; Ctrl+O keeps its inspectable subset. Ink 5's is a * consumption counter * (items.slice(index) keyed on length): deleting head items mid-stream while * appending tail items can permanently swallow new rows, so the append branch * NEVER drops the head — it only accounts rows and flags `needsTrim` once the * window overflows cap + margin. The flag fires one source-backed replay * (epoch bump = the existing clear + remount), whose rebuild branch * walks the settled entries BACKWARD from the newest, keeps whole entries * until the cap, and counts everything older as `droppedEntries` (those * entries never even reach settledEntryLines). Hysteresis bounds replays to * at most one per 25% growth; resize / Ctrl+L / idle Ctrl+R replays re-window * for free on the same path. * * Full rebuilds run only on the rare, deliberate paths: no cache yet, a * source-backed replay (`epoch` bump: resize / Ctrl+L / an idle Ctrl+R fold * toggle / a cap trim remounts `` and must re-flush the CURRENT rows * at the CURRENT fold state), a `resumed` change, or a shrink (`store.reset`). * While a turn is busy or streaming, Ctrl+R only flips the live region; rows * already emitted to native scrollback change exclusively through rebuilds. */ export declare function computeSettledRows(previous: SettledRowsCache | undefined, entries: readonly TranscriptEntry[], settled: number, showReasoning: boolean, resumed: boolean, epoch: number, columns?: number, rowCap?: number): SettledRowsResult; /** The whole terminal app; state arrives via the store, output via Ink. */ export declare function App(props: AppProps): ReactElement;