import React from 'react'; import { type FileDiff } from './diff-view.js'; import type { AllowRule } from './permissions.js'; import { type KoneckPolicy } from './policy.js'; import type { AgentConfig } from './types.js'; type Effort = 'low' | 'medium' | 'high' | 'max'; /** A corrupt or obsolete setting must not turn into an invalid prompt modifier. */ export declare function savedEffort(value: unknown): Effort | undefined; /** 92000 → "1m 32s", 7400 → "7.4s", 12 → "12ms". Sub-second work read as a broken "0.0s". */ export declare function fmtElapsed(ms: number): string; /** A picker label uses all of its allocated column, only ellipsising at the actual boundary. */ export declare function pickerLabel(text: string, width: number): string; /** * Wraps text while keeping the shape it was written in. * * `wrapToWidth` splits every line on whitespace, which is right for prose and wrong for anything * copied out of a file: indentation disappears and runs of spaces collapse, so pasted code came * back flattened against the left margin. Here the leading whitespace of each line is preserved * and reapplied to its continuations, and interior spacing is left exactly as it was. */ export declare function wrapPreserving(text: string, width: number): string[]; /** Beyond this a paste is summarised in the transcript; it is still sent in full. */ /** * The picker row that declares a provider rather than choosing one. * * A sentinel rather than a name, so it can never collide with a real provider — somebody is entitled * to declare one called "add". */ export declare const ADD_PROVIDER = "\0add-provider"; /** Declaring a provider, a field at a time. */ export interface AddProviderPrompt { step: 'name' | 'url' | 'model' | 'confirm'; name: string; url: string; model: string; /** What is being typed into the current field. */ value: string; /** Why the last entry was refused, shown above the field. */ error?: string; } /** * Checks one field and says what is wrong with it, or nothing. * * Separate from the component so the rules can be tested without mounting an Ink app — and they are * worth testing: the name rule is what keeps a provider addressable from a command line, and the key * variable rule is the one that stops a pasted credential being written into a JSON file. */ export declare function addProviderProblem(step: AddProviderPrompt['step'], value: string): string | null; export declare const MAX_PASTE_ROWS = 80; /** * The pasted text as it should appear once submitted: what was copied, structure intact. * * Shown rather than left as "[Pasted text #1 +31 lines]", because after pressing enter the * question is what was actually said — a placeholder makes the transcript unreadable later. Very * long pastes are cut with the remainder stated out loud, never silently. */ export declare function pasteForDisplay(text: string, maxRows?: number): string; /** * How many lines of in-flight reply to show. Deliberately tiny. * * Making this taller was tried and reverted. Ink erases a repainting region by walking the cursor * up one line per line it last wrote, so a region that grows leaves its earlier, taller frames * behind; a 28-line window turned one streamed paragraph into a column of ragged copies. Two lines * is the height that has been seen to repaint cleanly in a real terminal. * * It costs nothing now, because finished blocks are committed to scrollback as they complete. This * only ever holds the fragment of the current block that has not been committed yet — the text a * reader is watching appear, rather than the whole reply waiting for the turn to end. */ /** At most this many files are previewed while a tool is still running. */ export declare const LIVE_DIFF_FILES = 2; /** * How many lines of each file's diff may be drawn while the tool writing it still runs. * * Zero is a valid and common answer, and that is the point of the function. * * Ink paints by erasing the lines it last wrote and printing the new ones, which it cannot do for * lines that have scrolled off — so when the live region is at least as tall as the terminal it * writes `clearTerminal + fullStaticOutput + output` instead: wipe the screen, wipe the * scrollback, repaint the entire session, on every render. Type while that is the state of the * world and every keystroke wipes the terminal. * * A running tool's diff is the only part of the live region with no natural ceiling — a large * edit is a large diff — and off the alternate screen it is drawn inline rather than in the side * panel. So on a short terminal it is the difference between a calm screen and a strobing one. * * An 80x24 window, which is the default almost everywhere, has no room for it at all: the chrome * above already accounts for nineteen of those twenty-four lines. There the preview is dropped * and only the file and its summary are shown while the write is in flight. Nothing is lost — * the step is committed to scrollback the instant it finishes, and drawn there in full. * * A terminal that cannot composite a redraw gets half of what it could otherwise afford. The * height rule above is what avoids the wipe; this is about how much is repainted when a frame is * shown as an erase followed by a redraw rather than as one picture. */ export declare function liveDiffLineBudget(termRows: number | undefined, composites?: boolean, files?: number): number; export declare function liveReplyLines(termRows: number | undefined): number; /** * How long one animation frame lasts. * * Every tick both advances the animation and reveals whatever the stream has added since the * last one, so this is the interface's frame rate. It was 500ms while a stream was active and * 1,000ms once it went quiet, which is where "the loader is moving slowly ... even 10x slower * than a tortoise" came from: the spinner took ten seconds to turn once and the shimmer * forty-eight to cross a word. * * The reason it was that slow was flicker, and the flicker was real — but the rate was never its * cause. Ink writes a frame by erasing the previous one and drawing the next, and the terminal * showed the gap between the two. Repainting less often only made the flash rarer. It is now * fixed where it happens, in frame-sync.ts, by presenting each frame atomically, which leaves the * frame rate free to be a frame rate. * * 80ms is what cli-spinners and ora use for braille spinners, and it is comfortably above Ink's * own 32ms write throttle (`throttle(this.onRender, 32)` in ink/build/ink.js), so each tick * becomes exactly one write rather than being coalesced with its neighbours. Measured cost of a * tick on a Raspberry Pi 5: 0.7ms of React render and about 700 bytes written, or roughly 1% of * one core at this rate. */ export declare const FRAME_MS = 80; /** * How long one work word is held before the next is shown. * * The word is there to be read and enjoyed, not to reassure by moving, so it outlasts the * animation by a long way. */ export declare const WORD_HOLD_MS = 60000; /** * How often the interface proves it is alive when the animation is switched off. * * A second is slow enough that it is not an animation — nothing moves except the clock — and * quick enough that a pause never reads as a hang. It costs one repaint of a dozen lines, which * is imperceptible where a redraw is composited and a small flash where it is not; a screen that * appears frozen for seven seconds is worse than either. */ export declare const HEARTBEAT_MS = 1000; /** The host a provider's requests go to, for saying so where it matters. */ export declare function endpointHost(url: string): string; /** * One row of the fork tree: the task on the left, what it has cost on the right. * * Right-aligning the cost is the whole point of doing this by hand. Laid out as flex siblings the * numbers sat wherever the task text left them, so a column of forks had its timings in a ragged * diagonal and could not be read down. Composed into one string they line up at the right edge — * and it is one node per row rather than five, which is what a live list of eight agents can * afford to repaint. * * The task gives way first: it is the part a reader can still recognise from its opening words, * while a truncated token count is worth nothing at all. */ /** The width of what precedes the task on a fork row: a status glyph, a space, and "fork ". */ export declare const FORK_LEAD = 8; export declare function forkRow(task: string, meta: string, width: number, indent?: number): { task: string; gap: string; meta: string; }; /** * Whether the terminal should report the wheel to KONECK, when nothing has been saved either way. * * Mouse tracking is a trade: a terminal sends mouse events to one place, so while KONECK has them * the drag-select, the right-click menu and the middle-click paste all stop working. In ordinary * scrollback that trade is a bad one — the terminal's own buffer already scrolls with the wheel, * so turning it on loses selection and gains nothing. Hence opt-in. * * It was briefly defaulted on for the alternate screen, on the reasoning that a screen with no * scrollback of its own has nothing to lose. That was answered directly: "select must happen on * mouse like selecting in a document" and "mouse move should be by default, not to be turned * on/off". Shift+drag is a workaround for a cost that should not be imposed, and the real fix was * not to take the mouse but to stop taking the screen — see `defaultAltScreen`. In ordinary * scrollback the terminal scrolls with the wheel and selects with a drag, both without being * asked, which is the behaviour being described. * * So this stays off unless somebody asks for it. `/mouse on` is there for anyone who wants the * wheel to drive the transcript inside the alternate screen and will take Shift+drag for it. */ export declare function defaultMouseTracking(saved: boolean | undefined, _altScreen: boolean): boolean; /** * Where a tool's file changes are drawn, if anywhere. * * Three states, because there were only ever two booleans and that is what made `/diff off` * behave the way it did. `showDiffs` is whether the user wants to see changes at all; * `hasFrame` is whether the alternate screen is on, which is the only place a pinned side panel * can live. Closing the panel used to flip the second and leave the first, so the changes simply * moved into the conversation — the same content, more of it, and no way to be rid of it. * * Naming the outcome makes the command honest about which of the three it just chose. */ export declare function diffPlacement(showDiffs: boolean, hasFrame: boolean): 'panel' | 'inline' | 'hidden'; /** * Which spinner cell and shimmer column a turn is showing after this much work. * * One function so the two cannot drift apart, and pure so the rates can be asserted rather than * eyeballed: both advance one step per frame, the spinner wrapping around its glyphs and the * shimmer running on for `Shimmer` to wrap over the word plus its trailing gap. */ export declare function animFrames(elapsedMs: number, spinnerLength: number): { spin: number; shimmer: number; }; export { WORK_WORDS, shouldName, tierFor, workWord, type WorkTier, type TurnEffort, } from './work-words.js'; /** Logical families for the searchable command palette; headers reuse an existing row. */ export declare function commandGroup(command: string): string; /** What the overlay is currently offering. */ type PickerKind = 'command' | 'model' | 'provider' | 'effort' | 'resume' | 'file'; interface PickerItem { value: string; label: string; desc: string; group?: string; current?: boolean; } /** Stable, grouped command inventory. Filtering preserves this order while ranking exact matches. */ export declare function commandPaletteItems(commands?: readonly { cmd: string; desc: string; }[]): PickerItem[]; /** How a held-back paste appears in the composer. */ export declare function pasteToken(index: number, lines: number): string; /** * Puts held-back pastes back into the text before it is sent. * * The token is what the user sees; the model must receive what they actually copied. Anything * typed around a token is preserved in place, so a paste can be introduced and followed by a * question in one prompt. */ export declare function expandPastes(draft: string, pastes: readonly string[]): string; /** * Number of choices to keep visible in a palette. * * A picker should feel like a real menu, not a letterbox. Sixteen rows keeps commands, models, * files, sessions, and providers equally easy to scan; on short terminals it yields space to * the composer and the rest remains available through search and paging. */ export declare function pickerRowLimit(kind: PickerKind, itemCount: number, terminalRows?: number, reservedRows?: number): number; /** * A palette wants enough width to be readable, but its minimum may never outrun its container. * * Applying `Math.max(40, Math.min(available, wanted))` looks reasonable yet is backwards on a * narrow split pane: it produces 40 even when only 35 columns are available. Ink then wraps the * box it thought was one row, so its next frame erases the wrong number of terminal rows. Keep * the preferred minimum inside the final clamp instead. */ export declare function pickerBoxWidth(availableWidth: number, wantedWidth: number): number; /** * Filters picker entries. Prefix matches rank above substring matches so typing "mo" offers * /model and /models before /memory, and a leading slash in either the query or the entry is * ignored so "/mo" and "mo" behave the same. */ export declare function filterPickerItems(items: PickerItem[], query: string): PickerItem[]; /** * Turns a near-miss slash command into a direct recovery path. * * We deliberately do not guess at distant names: a wrong suggestion steals attention, while * catching one or two transposed/omitted characters makes command use feel forgiving. */ export declare function commandSuggestion(command: string, commands?: string[]): string | undefined; /** * Keep a mistyped command palette useful. A normal search never invents a result; only when it * finds none do we surface one close, known command, clearly labelled as a recovery suggestion. * Enter still requires the user to select it, so this cannot turn `/revertx` into an undo. */ export declare function commandPaletteMatches(items: readonly PickerItem[], query: string): PickerItem[]; /** The draft restored after an unknown-command typo; it is intentionally not executed for them. */ export declare function recoveredCommandDraft(suggestion: string, arg: string): string; /** Keep a small, useful recall buffer without repeating the same submitted task. */ export declare function appendDraftHistory(history: readonly string[], draft: string, limit?: number): string[]; /** One answer to a permission question: what it says, what it grants, and whether it permits. */ interface AskOption { key: string; label: string; rule: AllowRule | null; allow: boolean; } /** * The choices offered for one waiting tool call. * * Built in one place because they are both typed and pointed at: pressing 2 and highlighting the * second row have to mean the same thing, and a prefix option that appears only for commands * would otherwise renumber the rows beneath it in one of the two paths and not the other. */ export declare function askOptions(tool: string, prefix: string): AskOption[]; /** * The last few lines of reasoning, wrapped and clipped for a glance. * * Reasoning arrives as one long stream with few newlines, so it is wrapped to the panel width * first and then the tail is taken — clipping the raw text instead would show the last 200 * characters of one enormous line, which is a fragment of a sentence and tells nobody anything. */ export declare function thinkingTail(text: string, lines: number, width: number): string[]; /** * Where a paste belongs, given what is on screen. * * Pulled out as a decision of its own because it was wrong and invisible: the paste capture ran * before every other branch and always routed to the composer, so pasting an API key into the * prompt asking for one put the key in the message box — on screen, and one keystroke from being * sent to a model. */ export type PasteTarget = 'key-prompt' | 'add-provider' | 'composer' | 'drop'; export declare function pasteTarget(open: { keyPrompt?: boolean; picker?: boolean; ask?: boolean; addProvider?: boolean; }): PasteTarget; /** Bytes as a person reads them, for a session listing. */ export declare function humanBytes(bytes: number): string; /** * What to call a session in a list. * * A name if one was given, otherwise the first thing asked of it — which is what people remember * a piece of work by. The id is the last resort, because a timestamp identifies nothing. */ export declare function sessionTitle(meta: { name?: string; task?: string; id: string; }): string; /** * The line at the top right of a picker. * * Says where you are, not merely how many rows exist — and says when a filter is hiding some, * which "9 matches" cannot. A list that silently narrowed as you typed was indistinguishable * from one that had mysteriously changed. */ export declare function pickerCountText(selected: number, shown: number, total: number): string; /** * How much of a row belongs to the label. * * Sized from the rows actually on screen rather than fixed: a session title is what identifies * its row, and the constant 36 that was here cut most of them in half. */ export declare function pickerLabelWidth(labels: readonly string[], descs: readonly string[], width: number): number; /** * The project brief is deliberately a normal agent turn, rather than a separate hidden client. * It therefore uses the connected model, honours workspace policy and checkpoints its write just * like every other edit. `--force` is required when a maintainer's existing brief is present. */ export declare function initWorkspaceTask(force?: boolean): string; type McpServerStatus = { name: string; tools: readonly { name: string; description?: string; }[]; }; /** Human-readable MCP state without ever displaying configured environment values. */ export declare function mcpStatusText(configured: readonly string[], connected: readonly McpServerStatus[]): string; /** The review brief makes findings useful in a terminal, and keeps the default path read-only. */ export declare function reviewTask(kind: 'code' | 'security', fix?: boolean): string; /** The execution brief can be detailed without impersonating a user in the transcript. */ export declare function taskTranscriptLabel(task: string, generatedLabel?: string): string; /** The summary model gets useful context, never an accidental credential from the transcript. */ export declare function handoffSummaryInput(transcript: string): string; export { asideAllowsTool } from './aside.js'; export declare function readOnlyReviewAllows(tool: string, args: string): boolean; /** A goal is compact enough to preserve context room, but rich enough to guide a long session. */ export declare function normalizeSessionGoal(value: string): string; /** Copy for the deliberate second step before a whole-workspace rollback. */ export declare function workspaceRestoreConfirmation(id: string, label: string): string; /** Keep a large recovery report inspectable without turning it into a terminal-sized wall. */ export declare function checkpointPathSummary(paths: readonly string[], limit?: number): string; /** `/checkpoint list` is a safe discovery alias, never a snapshot called "list". */ export declare function isCheckpointListCommand(arg: string): boolean; /** Parses the compact quoted argument form used by native structural refactor commands. */ export declare function parseCommandArguments(input: string): string[] | null; /** * Native AST refactors can update arbitrary import sites. A restrictive policy must be enforced * before that fan-out, rather than pretending the few command arguments describe every write. */ export declare function nativeRefactorPolicyReason(policy: KoneckPolicy | null): string | null; /** A restore report must distinguish a completed operation from an already-matching snapshot. */ export declare function workspaceRestoreWasNoop(result: { restored: readonly string[]; deleted: readonly string[]; skipped: readonly string[]; }): boolean; /** Whether Ink must stop for a confirmation instead of letting a tool run. */ export declare function inkApprovalDecision(requireApproval: boolean, mode: string, tool: string, argsJson: string, rules: readonly AllowRule[], policyRequiresApproval?: boolean): 'allow' | 'deny' | 'ask'; export type ContextWindowFact = { limit: number; source: string; }; /** A context report names the evidence for its denominator instead of pretending every model fits. */ export declare function contextStatusText(model: string, sessionPromptTokens: number, configuredLimit?: number, detected?: ContextWindowFact | null): string; export declare function commandRunsWhileBusy(cmd: string, arg?: string): boolean; export declare function commandNeedsArgument(cmd: string): boolean; /** * Whether to own the screen, when nothing has been configured either way. * * Only on a real terminal. A pipe has no cursor to move and no rows to fill, and writing the * enter-alternate-screen sequence into a log file is how a captured session becomes unreadable. */ /** * Whether a chunk of terminal input is a mouse report rather than typing. * * Two encodings. SGR (`ESC [ < button ; col ; row M`) is what KONECK asks for, and the older X10 * form (`ESC [ M` followed by three raw bytes) is what a terminal may send anyway — a session that * had tracking enabled by something else, or one that ignores the SGR request. Both are recognised * with and without the leading escape, because the escape can arrive in a previous read. * * Deliberately generous: a false positive swallows one keystroke, and a false negative pastes * "[<0;30;38M" into whatever you were writing. */ export declare function looksLikeMouseReport(input: string): boolean; /** * Some terminals turn a wheel movement into an ordinary arrow key while using native scrollback. * Arrow keys must therefore never replace the current draft: scrolling should not recall a prompt. */ export declare function draftHistoryDirection(input: string, key: { ctrl?: boolean; upArrow?: boolean; downArrow?: boolean; }): -1 | 1 | undefined; export declare function defaultAltScreen(stdout?: { isTTY?: boolean; rows?: number; columns?: number; }, env?: NodeJS.ProcessEnv, /** * Whether this terminal composites a redraw, from the probe in frame-sync.ts. * * The deciding fact, and the reason this argument exists. On the alternate screen the frame is * redrawn in place, which a terminal that implements synchronized output presents as one picture * and one that does not shows as an erase — so the same interface is smooth on Windows Terminal * and blinks on most Linux VTE terminals. Two further things follow from redrawing in place, and * both were reported: a selection is destroyed by the next repaint, so text cannot be copied out * — "only koneck cli i can't select/highlight and copy" — and there is no scrollback to select * from in the first place. * * The terminal's own scrollback has none of those problems. Finished rows go through , * which writes each one once and never touches it again: nothing to blink, and the text sits in * the terminal's buffer to be selected like any other output. What it costs is the fixed frame — * the side-by-side diff panel needs one, so opening that still switches over. * * So the alternate screen is used where it is invisible and avoided where it is not, rather than * a default chosen for one machine and endured on the other. */ compositesRedraws?: boolean): boolean; /** * Whether the session starts on the alternate screen, given what has been saved. * * The order matters, and it used to be wrong. `diffPanel` came first, because the side-by-side * panel needs a fixed frame to draw in — so `diffPanel: true`, which is the default, turned the * alternate screen on regardless of what `altScreen` said. Anyone who had gone looking for that * setting and switched it off was put straight back on it at the next start, silently. * * That is not a cosmetic mistake. On the alternate screen nothing goes through , so the * whole visible transcript is re-rendered on every repaint — and a repaint is caused by every * keystroke, not just by the animation. Measured on a Raspberry Pi 5 at thirty rows: 168ms a * frame, which is a six-frames-a-second interface where each keypress waits behind a full redraw * of the conversation. The person who had already found the setting that avoids all of that was * the last person who should have been paying for it. * * So an explicit `altScreen: false` now outranks everything. Nothing breaks by honouring it: * `diffPanelOpen` is `showDiffPanel && altScreen`, so with no frame to draw in the panel stays * shut and the diffs render inline in the transcript, where they were before the panel existed. * * Only an explicit `false` wins, not an absent setting — leaving `diffPanel: true` free to ask for * a frame when nobody has expressed a view. */ export declare function initialAltScreen(cfg: { altScreen?: boolean; diffPanel?: boolean; }, compositesRedraws: boolean, stdout?: { isTTY?: boolean; rows?: number; columns?: number; }, env?: NodeJS.ProcessEnv): boolean; /** * The live diff panel — the work as it happens, beside the conversation. * * Asked for: the same running view the web has, in the terminal. It shows every file the session * has changed, newest first, with the counts in the header, so a person watching can see the edit * land rather than reading a description of it afterwards. Bounded on both axes: the newest files * fill the height available and the rest are counted, since a panel that scrolls past what you * wanted is no better than no panel. */ export declare function DiffPanel({ files, width, height, activity, activityRows, scroll }: { files: readonly FileDiff[]; width: number; height: number; activity?: React.ReactNode; activityRows?: number; /** * How far back from the newest row the panel is looking. * * The panel scrolls independently of the conversation, which is the point of it being a * panel: reading a diff should not mean losing your place in the transcript, and following * the transcript should not drag the diff away from the hunk being read. */ scroll?: number; }): React.JSX.Element; /** * Exported for one reason: so a test can mount it. * * The CLI shipped unable to start — an expression in a useState initialiser reached for a helper * declared further down the component, and every release after that crashed on the first render. * Typechecking passes on that code and no test could reach it, because nothing in the suite mounted * an Ink app. Something does now. */ /** * Where each visual row of the composer begins and ends, as offsets into the draft. * * Not `wrapToWidth`, which normalises runs of whitespace and so cannot be mapped back to the * text it was given. Moving a caret between rows needs the opposite property: every character * accounted for, in its original position, so that row-and-column can be converted to an index * and back without drifting. * * Greedy word wrap, breaking at the last space that fits and hard-splitting a word too long for * a row — the same rule the composer is drawn with, so the rows here are the rows on screen. */ export declare function composerRows(text: string, width: number): { start: number; end: number; }[]; /** * The caret one visual row up or down, or null when there is no such row. * * Null is the useful half: with nothing above, Up is free to mean "the previous prompt", which * is what it has always meant and what a shell does. So a one-line draft keeps its history * recall and a wrapped or multi-line one becomes editable in the middle, which is what was asked * for — "must be able to move in lines/rows for easy editing/correcting when need to write in * between". * * The column is kept where it can be and clamped to the end of a shorter row, as every editor * does. */ export declare function caretRowMove(text: string, caret: number, width: number, direction: -1 | 1): number | null; /** * The selected span of the composer, or null when nothing is selected. * * Held as an anchor plus the caret rather than as a pair, because that is what makes Shift+arrow * behave the way it does everywhere else: the anchor is where selecting began and stays put, the * caret is the end being dragged, and which of the two is on the left depends on which way it * was dragged. Clamped to the text, so a draft replaced underneath a stale anchor cannot produce * a range that is not there. */ export declare function selectionRange(anchor: number | null, caret: number, length: number): [number, number] | null; /** * The draft after typing over, or deleting, whatever is selected. * * One function for both, because they are the same operation: a selection is replaced by what * was typed, and deleting is replacing it with nothing. With no selection it is an ordinary * insert at the caret, so every editing path can call this without first asking which case it is * in. */ export declare function replaceSelection(text: string, range: [number, number] | null, caret: number, insert: string): { text: string; caret: number; }; export declare function App({ config: initialConfig, clearFrame, synchronizedFrames }: { config: AgentConfig; clearFrame?: () => void; /** Whether this terminal composites a redraw; see frame-sync.ts. */ synchronizedFrames?: boolean; }): React.JSX.Element; export declare function runInkChatMode(config: AgentConfig): Promise; //# sourceMappingURL=ink-chat.d.ts.map