/** * Interactive mode for the coding agent. * Handles TUI rendering and user interaction, delegating business logic to AgentSession. */ import type { ImageContent } from "@kolisachint/hoocode-ai"; import type { Terminal } from "@kolisachint/hoocode-tui"; import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js"; import type { TeamViewConnection } from "../../core/team-view.js"; /** * Options for InteractiveMode initialization. */ export interface InteractiveModeOptions { /** Providers that were migrated to auth.json (shows warning) */ migratedProviders?: string[]; /** Warning message if session model couldn't be restored */ modelFallbackMessage?: string; /** Initial message to send on startup (can include @file content) */ initialMessage?: string; /** Images to attach to the initial message */ initialImages?: ImageContent[]; /** Additional messages to send after the initial message */ initialMessages?: string[]; /** Force verbose startup (overrides quietStartup setting) */ verbose?: boolean; /** * Terminal the TUI draws to. Defaults to the real process terminal; tests * pass a capturing one to mount the whole mode headlessly and screenshot it. */ terminal?: Terminal; } export declare class InteractiveMode { private options; private runtimeHost; private ui; private chatContainer; private pendingMessagesContainer; private statusContainer; private defaultEditor; /** Teardown for the pinned-view key capture; re-installed with the editor. */ private removeScrollView?; private editor; private editorComponentFactory; private voice; private autocompleteProvider; private autocompleteProviderWrappers; private fdPath; private editorContainer; /** * The rows nobody is using, handed to the renderer so the app is the size of * the screen. It sits directly below the banner, so on a fresh session the * logo holds the first row, the leftover rows sit between it and the * conversation, and the prompt and footer stay packed against the foot of * the screen. Past a screenful the fill is 0 and the layout is exactly the * old pack-against-the-floor. See `TUI.setFlexSpacer`. */ private readonly screenFill; /** The transient band above the prompt; see components/notification-panel.ts. */ private notifications; private footer; private footerDataProvider; private keybindings; private version; private isInitialized; private onInputCallback?; private loadingAnimation; private workingMessage; private workingVisible; private workingIndicatorOptions; private readonly defaultWorkingMessage; private readonly defaultHiddenThinkingLabel; private hiddenThinkingLabel; private lastSigintTime; private lastEscapeTime; private changelogMarkdown; private startupNoticesShown; private lastStatusSpacer; /** Dial reverses already named this session; see showDialStep. */ private dialReverseTaught; private lastStatusText; private streamingComponent; private streamingMessage; /** Throttled re-parse/re-render of the in-flight assistant message. The * guard makes a trailing run after message_end/agent_end a no-op; a direct * updateContent on message_end flushes the final state regardless. */ private readonly scheduleStreamingRender; private pendingTools; private toolOutputView; private viewBeforeJump?; /** * Whether everything foldable is currently open. * * Not a state of its own any more — it is the top of the view dial. The * header, compaction and branch summaries and skill blocks follow the dial * for the same reason tool bodies do: `full` means nothing is held back. */ private get toolOutputExpanded(); private openChain?; /** The newest tool block in the transcript; radar marks it. */ private latestToolBlock?; /** The run that block belongs to; radar marks its folded line too. */ private latestChain?; private sawTextInCurrentMessage; private hideThinkingBlock; private skillCommands; private unsubscribe?; private taskStoreUnsubscribe?; private startupProgressUnsubscribe?; private signalCleanupHandlers; private isBashMode; private autoCompactionLoader; private autoCompactionEscapeHandler?; private retryLoader; private retryCountdown; private retryEscapeHandler?; private chime?; private tips?; /** Drops the keystroke observer that feeds the tip controller's idle clock. */ private tipActivityUnsubscribe?; private chimePendingRetry; private turnStopReason; private turnCostAnchor; private shutdownRequested; private dialogs; private extensionTerminalInputUnsubscribers; private chrome; /** * The two pieces of chrome the density dial can take away, each in a slot so * hiding one costs nothing and never moves the tree. The prompt has no slot * on purpose — see `chrome-layout.ts`. */ private footerSlot; private tasksSlot; private chromeLayout; private widgetContainerAbove; private widgetContainerBelow; private taskPanel; private teamFocus; private bashExecution; private messageQueue; private modelController; private loginController; private headerContainer; private builtInHeader; private get session(); private _commandExecutor?; /** * Lazily-built command executor. The context uses getters for mutable * dependencies (e.g. the active session) so handlers always operate on the * current state even after a session switch. */ private get commandExecutor(); private stopLoadingAnimation; private get agent(); private get sessionManager(); private get settingsManager(); constructor(runtimeHost: AgentSessionRuntime, options?: InteractiveModeOptions); private getAutocompleteSourceTag; private prefixAutocompleteDescription; private getBuiltInCommandConflictDiagnostics; private createBaseAutocompleteProvider; private setupAutocompleteProvider; private showStartupNoticesIfNeeded; /** Feed a first-run tool-binary download into the footer's startup-progress line. */ private reportToolDownload; init(): Promise; /** * Refresh everything in the chrome that says *which session this is*: the * terminal title and the chip on the input box. Both read from the same * source, so they can never disagree about a rename. */ private refreshSessionIdentity; /** * Rebuild the session chip and hand it to the editor, telling the footer to * stand down from showing the name itself. Cheap enough to call on any change * that could move the name, the colour, or the theme they are drawn in. */ private updateSessionChip; /** * Update terminal title with session name and cwd. */ private updateTerminalTitle; /** * Run the interactive mode. This is the main entry point. * Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop. */ run(): Promise; private getMarkdownThemeWithSettings; private getStartupExpansionState; /** Render the startup/reload resource listing (see resource-display.ts). */ private showLoadedResources; /** * Agents the model can actually dispatch. Zero when the Task tool is off, so * the summary never advertises a capability the session cannot use. */ private getDispatchableAgentCount; private bindCurrentSessionExtensions; /** * Push the current settings and session onto the chrome: keybindings, footer, * editor, cursor. * * Startup, a session swap (/new, /resume, /fork, /cd) and /reload all rebuild * the same things from disk, so they must all repaint the same way. They used * to do it with two hand-kept copies of this list, and the /reload copy was * missing pieces — a `compaction.enabled` edit left the footer still * promising auto-compaction. One list, called from both paths; the theme is * the second half, in `applySessionTheme`. */ private applyRuntimeSettings; /** * Load the theme the settings name, and rebuild what holds colour baked in. * * Runs after extensions are bound (and after `session.reload()`), because a * `resources_discover` handler can contribute the very theme directory the * name resolves in — resolving it earlier would fall back to dark for anyone * whose theme ships with an extension. * * The banner is rebuilt rather than invalidated: a `Text` holds its string * with the escapes already in it, so invalidating only drops the line cache * and the old theme's colours come straight back. It also names the working * directory, which `/cd` moves. */ private applySessionTheme; private finishRuntimeSettings; private rebindCurrentSession; private handleFatalRuntimeError; /** * Drop every view-layer reference into the transcript that is about to be * rebuilt. Shared by the session swaps and by /reload: a component that * survives the clear is detached, so anything still pointing at it (the open * chain the next tool call would join, the block the unfold key peels back) * would be writing to a frame nobody renders. */ private resetTranscriptView; /** * Reset the transcript to whatever the just-swapped session holds. * * Every caller — /new, /resume, /fork, /cd, /import — reaches here after the * runtime has rebound extensions. Drawing the listing here rather than in the * rebind is what makes the surface common: one call site, so a session change * of any kind reports the same capabilities /reload does, and neither path * draws the listing twice. */ private renderCurrentSessionState; /** * Bound the view layer's memory: freeze finished tool blocks once more than * LIVE_TOOL_WINDOW of them are live, releasing their retained tool output and * base64 image copies. Runs on tool completion (infrequent) and only ever * freezes blocks far above the viewport. The session data stays intact, so a * later full rebuild (theme toggle / reload) restores full fidelity. */ private trimTranscriptMemory; /** * Get a registered tool definition by name (for custom rendering). */ private getRegisteredToolDefinition; /** * Set up keyboard shortcuts registered by extensions. */ private setupExtensionShortcuts; /** * Set extension status text in the footer. */ private setExtensionStatus; private getWorkingLoaderMessage; private createWorkingLoader; private stopWorkingLoader; private setWorkingVisible; private setWorkingIndicator; private setHiddenThinkingLabel; private resetExtensionUI; private addExtensionTerminalInputListener; private clearExtensionTerminalInputListeners; /** * Create the ExtensionUIContext for extensions. */ private createExtensionUIContext; private promptForMissingSessionCwd; /** * Set a custom editor component from an extension. * Pass undefined to restore the default editor. */ private setCustomEditorComponent; /** * Show a notification for extensions. */ private showExtensionNotify; /** * Show an extension error in the UI. */ private showExtensionError; private setupKeyHandlers; private handleClipboardImagePaste; /** * Wire a hooteams connection into the TUI. Called by main.ts when `--team` * is set, before run(). See TeamFocusController for the feature itself. */ attachTeamClient(client: TeamViewConnection): void; /** * Built-in slash commands dispatched by the editor submit handler. * `withArgs` commands also match "/name " and receive the full text. * Each handler keeps its own editor-clearing order (before vs after the * await) — some commands must show their UI before the prompt is wiped. */ private createBuiltInSlashCommands; private setupEditorSubmitHandler; private subscribeToAgent; /** * Deferred end-of-request settle. Called from the agent_end handler, it waits * one tick so the streaming flag settles and any retry has had a chance to arm, * then acts only when the session is genuinely idle (not streaming, not * compacting) and no retry is pending — so a retried or auto-continued request * neither rings a premature "it's done" nor prints a partial cost line. * * The completion chime, the transcript cost line, and settling dangling plan * items all hang off this single check because they answer the same question: * has the request actually ended? */ private settleRequestOnIdle; /** * Flip main-plan rows the model left at in_progress to a settled status now the * request is over, instead of letting them claim live work until the next user * message wipes the pane. * * The model marks its own TodoWrite items and routinely drops the final call * that completes the last one, so the row outlives the work. A clean "stop" is * taken as the model believing it was finished — it chose to stop talking, and * its closing message is the report — so those items settle to done. An abort, * error, or length cutoff says the opposite, so they settle to cancelled: an * honest "never finished" rather than a fabricated completion. * * Skipped while messages are queued: a follow-up/steer arriving during the run * means the request continues, and the plan with it. */ private settleDanglingPlanItems; /** * Append this request's own token/time/cost to the transcript. * * This is the honest home for the number. The task panel is a live instrument — * present tense, wiped on the next user message — and the footer carries * cumulative session vitals, so after the fact neither can answer "what did that * request cost". Fired at agent_end rather than turn_end because one request is * commonly tens of turns; per-turn would wedge a number between every tool block. * * Delegated runs are reported separately rather than folded into the totals: * subagents bill against their own sessions, so their tokens never appear in this * session's entries and adding them to ↑/↓ would misreport what the parent spent. */ private showTurnCost; private handleEvent; /** Extract text content from a user message */ private getUserMessageText; /** * Show a status message in the chat. * * If multiple status messages are emitted back-to-back (without anything else being added to the chat), * we update the previous status line instead of appending new ones to avoid log spam. */ /** * Report a dial step, and name the key that steps it back — once. * * The instant after someone moves a dial is the one moment they are primed to * learn its other half: they have just used one direction and can feel the * missing one. So the reverse rides along with the first step of each dial in * a session and never again. A hint that repeats forever stops being read, * and its cost falls entirely on the people who already know it. * * All six dials come through here now. Three of them used not to, because * the answer was already visible somewhere — the mode chip and the view * glyph change in place in the footer, the task panel prints its own key in * its header — and a permanent transcript line was too much to pay for * repeating it. The band is not permanent, so that trade is gone: a dial * that says nothing when you step it is a dial you press twice to be sure. */ private showDialStep; /** * A glimpse of something that just changed, on the band above the prompt. * * `topic` names what the glimpse is *of*, for anything that can be stepped: * a second reading replaces the first wherever it is, on screen included. */ notify(message: string, note?: string, topic?: string): void; /** * A tip, on the band. * * Deliberately a `notify` like any other rather than a fourth kind of * message: a tip earns no special treatment on screen, and the one thing it * does get -- a `topic` -- exists so that a second tip can never stack behind * the first. It replaces it, or it does not appear. * * The TTL is longer than a glimpse's because a glimpse confirms something the * user just did, which they only have to recognise, while a tip is telling * them something new, which they have to read. */ private showTip; /** * What a command has to say for itself — on the band, not in the transcript. * * This used to write a dimmed row into the conversation, and every command * that reported anything wrote one: `Mode set to "build"`, `Cloned to new * session`, `Copied last agent message`, the plugin catalogue, what `/learn` * read. All of them true for a moment and litter for the rest of the * session, sitting between the messages the transcript exists to keep. * * The band is where they belong: it is directly above the prompt, where the * user's eye already is after typing a command, and it clears itself. What * cannot be reconstructed once it fades does not come through here at all — * it goes to `showRecord`, which still writes the row. */ private showStatus; /** * The status rows that stay: a share URL, an export path, a saved-to path. * * The one exception to the band, and the test for it is not importance but * whether the screen can still answer the question in a minute's time. A * mode, a model, a session name are all on the footer or the prompt border * afterwards; a gist URL is nowhere but here, and a notification that fades * with a URL on it is a notification that cost the user the thing they asked * for. */ private showRecord; private addMessageToChat; /** * Render session context to chat. Used for initial load and rebuild after compaction. * @param sessionContext Session context to render * @param options.updateFooter Update footer state * @param options.populateHistory Add user messages to editor history */ private renderSessionContext; renderInitialMessages(): void; getUserInput(): Promise; private rebuildChatFromMessages; private handleCtrlC; private handleCtrlD; /** * Gracefully shutdown the agent. * Stops the TUI before emitting shutdown events so extension UI cleanup cannot * repaint the final frame while the process is exiting. */ private isShuttingDown; private shutdown; private emergencyTerminalExit; private checkShutdownRequested; private registerSignalHandlers; private unregisterSignalHandlers; private handleCtrlZ; private handleFollowUp; private handleDequeue; private updateEditorBorderColor; private updateEditorPromptPrefix; private cycleThinkingLevel; /** * Put a tool block into the transcript, extending the open chain or starting * one. Every path that renders a tool call comes through here — live * streaming, execution start, and rebuilding history — so a chain cannot be * assembled correctly in one place and forgotten in another. */ private attachToolBlock; /** * Settle the open chain. * * Called when the agent speaks (the run it was doing is over, whatever comes * next is a new one) and when the turn settles. Closing on the agent's next * words rather than only at turn end is what keeps the flip cheap: the line * is still at the bottom of the screen, so the TUI rewrites it in place * instead of taking the full-redraw path that clears terminal scrollback. */ private closeOpenChain; /** Every tool block in the transcript, in order, across all chains. */ private transcriptToolBlocks; /** * Jump to the full view, or back to where the jump started. * * The dial's top stop is the whole result with nothing trimmed, which is what * "expand" always meant — so expand is not a second state layered over the * dial any more, it is a jump along it. From any stop the first press lands * on `full`; the next press returns to the stop you came from (`peek` if the * jump started there, so the key is never a no-op). * * Only `app.view.cycleForward` writes the setting. This key deliberately does * not: the dial is the decision you keep, the jump is the look you take. */ private jumpToFullView; /** * Move the view dial one stop; the footer shows where it landed. * * This is the persistent decision ("how much do I ever want to see"), which * is why it saves. `app.tools.expand` jumps along the same dial without * saving, which is what keeps "the look I am taking now" from overwriting * "the view I live in". */ private cycleToolOutputView; /** * How much of a thinking trace this view shows. * * Radar drops them outright, regardless of the setting. The dial is one * question asked once — "how much do I ever want to see" — and a view whose * whole job is to fold a run of tool calls down to a row cannot then spend * forty lines on the reasoning that led to it. Folding to the label is not * enough either: a message that only thought and called tools has nothing * else on screen once its calls join the chain, so its label would stand * alone under the summary and one row per chain would become one row plus a * label per call. * * It is also what keeps a running chain on screen. A chain stays open across * a thinking block (thinking is not text, so it is not a chain boundary), and * a trace rendering below pushes the chain's own summary line off the top — * where every subsequent call rewrites a line above the viewport and forces * the full redraw that clears terminal scrollback. */ private thinkingDisplayForView; private applyToolOutputView; /** * Open or close everything that folds but is not a tool call — the header, * compaction and branch summaries, skill blocks. Tool blocks and chains are * not in this sweep: they take the view dial directly. */ private setToolsExpanded; private cycleAgentMode; /** * Step the session chip to the next colour slot, no picker involved. * * The picker exists for choosing a colour; this exists for telling two * terminals apart, which is a different job — you press the key until the two * chips stop looking alike, and the chip repaints on each press because * setSessionColor writes the slot and the session_info event refreshes the * identity. The status line names the slot as well, since the point of the * names is that `/color ` takes them. */ private cycleSessionColor; /** * Step the chrome dial and say where it landed. * * The step is announced like every other dial's, and it has to be: at `bare` * the footer that normally shows a dial's new stop is the very thing that * just went away, so the status line is the only place left to say what * happened. Without it the screen would simply lose two rows with no * explanation — which is how a feature gets reported as a glitch. */ private cycleChromeDensity; /** * `/chrome `, and the reason the dial is not reachable by key alone. * * macOS Terminal.app composes characters instead of sending alt, so a dial * whose only key is `alt+` is *unreachable* there. That is survivable * for a cosmetic dial and not for this one: the stop persists globally, so * someone who picked `bare` on one machine would open on that terminal with * no footer, no ledger and no way to ask for them back. Every dial that can * strand a setting has a slash command; this is that command. */ private handleChromeCommand; private toggleThinkingBlockVisibility; private openExternalEditor; clearEditor(): void; /** * Paint a message as a filled block, the shape errors and warnings share. * * These two used to render differently for no reason anyone could name: an * error got a blank line above it and a column of padding, a warning got * neither, so a warning collided with whatever was printed before it and hung * off the left margin. Both are the same kind of interruption, so both get the * same frame, and the fill is what separates them from ordinary chat output — * a single coloured line is easy to scroll straight past. */ private showBlock; /** * Split a message into its headline and the rest. * * Multi-line notifications — `/learn` reporting where it searched, a settings * dump — read as a heading over detail, and rendering them as one undifferen- * tiated coloured block loses that. A single-line message is all headline. */ private splitBlockMessage; showError(errorMessage: string): void; /** * A warning, on the band, for as long as a warning is worth. * * It used to be a filled block in the transcript, which outlived what it was * warning about by the whole session: "No previous directory to return to" * is worth a glance and then nothing, and a permanent block for it is a * permanent hole in the conversation. Warnings queue rather than replace, so * a run of them at startup is still shown one after another. * * `showNotice` is the exception that proves this rule — see below. */ showWarning(warningMessage: string): void; /** * A warning the user pays for if they miss it. * * The one thing still drawn as a filled block in the transcript, and the * reason is in the name: these say money is being spent on the wrong * account, or a tool is running on a backend the user did not choose. * Someone who looked away for four seconds has to still be able to find it, * which is exactly what the band cannot promise. */ showNotice(title: string, body: string[]): void; /** * Say once, at startup, that `websearch` is running on the keyless backend. * Shown only while the tool is actually active and no keyed provider is * configured; silenced by `warnings.websearchApiKey`. */ private maybeWarnAboutMissingWebSearchKey; showNewVersionNotification(newVersion: string): void; showPackageUpdateNotification(packages: string[]): void; /** * Shows a selector component in place of the editor. * @param create Factory that receives a `done` callback and returns the component and focus target */ private showSelector; private showSettingsSelector; private showUserMessageSelector; private showTreeSelector; /** * Swatch picker for the session chip's colour. Moving through the list * repaints the live chip, so the choice is made by looking at the real thing * in the real theme rather than at a preview of it. */ private showSessionColorSelector; private showSessionSelector; private handleResumeSession; private handleReloadCommand; private handleCompactCommand; stop(): void; } //# sourceMappingURL=interactive-mode.d.ts.map