{"version":3,"file":"command-executor.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/command-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Extracted command handlers from InteractiveMode.\n * All methods receive dependencies via CommandContext rather than\n * reaching back into the parent class.\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport type { EditorComponent, MarkdownTheme, TUI } from \"@kolisachint/hoocode-tui\";\nimport { type Container, Markdown, Spacer, Text, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { APP_TITLE, getDebugLogPath, getShareViewerUrl } from \"../../config.js\";\nimport { loadAgentRegistry } from \"../../core/agent-registry.js\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport type { AgentSessionRuntime } from \"../../core/agent-session-runtime.js\";\nimport { ChangeDirectoryError, SessionImportFileNotFoundError } from \"../../core/agent-session-runtime.js\";\nimport type { KeybindingsManager } from \"../../core/keybindings.js\";\nimport { MissingSessionCwdError } from \"../../core/session-cwd.js\";\nimport {\n\tparseSessionColorSlot,\n\tSESSION_COLOR_NAME_LIST,\n\tSESSION_COLOR_SLOTS,\n\tsessionColorName,\n} from \"../../core/session-identity.js\";\nimport type { SessionManager } from \"../../core/session-manager.js\";\nimport { getSubagentPool } from \"../../core/subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../../core/subagent-result.js\";\nimport { getChangelogPath, parseChangelog } from \"../../utils/changelog.js\";\nimport { markdownToHtml } from \"../../utils/markdown-to-html.js\";\nimport { copyRichToClipboard } from \"../../utils/rich-clipboard.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport type { FooterComponent } from \"./components/footer.js\";\nimport { formatKeyText, keyDisplayLabel, keyDisplayText } from \"./components/keybinding-hints.js\";\nimport { renderSessionChip } from \"./components/session-chip.js\";\nimport { theme } from \"./theme/theme.js\";\n\nexport interface CommandContext {\n\t// Core dependencies\n\tsession: AgentSession;\n\tsessionManager: SessionManager;\n\truntimeHost: AgentSessionRuntime;\n\tui: TUI;\n\teditor: EditorComponent;\n\teditorContainer: Container;\n\tchatContainer: Container;\n\tstatusContainer: Container;\n\tfooter: FooterComponent;\n\tkeybindings: KeybindingsManager;\n\n\t// UI callbacks\n\t/** What a command has to say, on the band above the prompt; gone in seconds. */\n\tshowStatus: (message: string) => void;\n\t/**\n\t * A status the transcript keeps, for the handful that carry something the\n\t * screen cannot answer for later: a share URL, a path written to.\n\t */\n\tshowRecord: (message: string) => void;\n\t/** A glimpse on the band above the prompt; gone in a few seconds. */\n\tnotify: (message: string, note?: string) => void;\n\tshowError: (message: string) => void;\n\tshowWarning: (message: string) => void;\n\tupdateEditorBorderColor: () => void;\n\trenderCurrentSessionState: () => void;\n\trebuildChatFromMessages: () => void;\n\tgetMarkdownThemeWithSettings: () => MarkdownTheme;\n\tstopLoadingAnimation: () => void;\n\n\t// Auth/model helpers\n\tfindExactModelMatch: (searchTerm: string) => Promise<Model<any> | undefined>;\n\tmaybeWarnAboutAnthropicSubscriptionAuth: (model?: Model<any>) => Promise<void>;\n\n\t// Dialog callbacks\n\tshowModelSelector: (searchTerm?: string) => void;\n\tshowExtensionConfirm: (title: string, message: string) => Promise<boolean>;\n\tpromptForMissingSessionCwd: (error: MissingSessionCwdError) => Promise<string | undefined>;\n\n\t// Fatal error handler\n\thandleFatalRuntimeError: (prefix: string, error: unknown) => Promise<never>;\n}\n\nexport class CommandExecutor {\n\t/** Where `/cd -` returns to. Set on every successful move, session-local. */\n\tprivate previousCwd?: string;\n\n\tconstructor(private readonly ctx: CommandContext) {}\n\n\t// =========================================================================\n\t// Slash command handlers\n\t// =========================================================================\n\n\tasync handleModel(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.ctx.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst model = await this.ctx.findExactModelMatch(searchTerm);\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tawait this.ctx.session.setModel(model);\n\t\t\t\tthis.ctx.footer.invalidate();\n\t\t\t\tthis.ctx.updateEditorBorderColor();\n\t\t\t\tthis.ctx.notify(`Model: ${model.id}`);\n\t\t\t\tvoid this.ctx.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\t\t} catch (error) {\n\t\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showModelSelector(searchTerm);\n\t}\n\n\tasync handleClone(): Promise<void> {\n\t\tconst leafId = this.ctx.sessionManager.getLeafId();\n\t\tif (!leafId) {\n\t\t\tthis.ctx.showStatus(\"Nothing to clone yet\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.fork(leafId, { position: \"at\" });\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.editor.setText(\"\");\n\t\t\tthis.ctx.showStatus(\"Cloned to new session\");\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleSubagent(text: string): Promise<void> {\n\t\tconst prefix = \"/subagent \";\n\t\tconst args = text.startsWith(prefix) ? text.slice(prefix.length).trim() : \"\";\n\t\tif (!args) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst firstSpace = args.indexOf(\" \");\n\t\tif (firstSpace === -1) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst mode = args.slice(0, firstSpace).trim();\n\t\tconst task = args.slice(firstSpace + 1).trim();\n\t\tif (!task) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst validModes = loadAgentRegistry({ cwd: this.ctx.sessionManager.getCwd() })\n\t\t\t.list()\n\t\t\t.map((a) => a.name);\n\t\tif (!validModes.includes(mode)) {\n\t\t\tthis.ctx.showStatus(`Unknown subagent_type: ${mode}. Available: ${validModes.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showStatus(`Spawning ${mode} subagent...`);\n\t\ttry {\n\t\t\tconst pool = getSubagentPool(this.ctx.sessionManager.getCwd(), this.ctx.session.modelRegistry.getAvailable());\n\t\t\tconst dispatchResult = await pool.dispatch(task, {\n\t\t\t\tforceAgent: mode,\n\t\t\t\tmodel: this.ctx.session.model?.id,\n\t\t\t\tprovider: this.ctx.session.model?.provider,\n\t\t\t});\n\t\t\tconst result = dispatchResult.result;\n\t\t\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\t\t\tif (result?.ok) {\n\t\t\t\tthis.ctx.showStatus(`${mode} subagent completed`);\n\t\t\t\t// Inject the subagent answer as a custom message so the user can see it in the chat\n\t\t\t\tthis.ctx.sessionManager.appendMessage({\n\t\t\t\t\trole: \"custom\",\n\t\t\t\t\tcustomType: \"subagent\",\n\t\t\t\t\tcontent: resultData?.summary || \"(no output)\",\n\t\t\t\t\tdisplay: true,\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.ctx.showError(`Subagent (${mode}) failed: ${result?.error ?? \"unknown error\"}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleExport(text: string): Promise<void> {\n\t\tconst outputPath = this.getPathArgument(text, \"/export\");\n\n\t\ttry {\n\t\t\tif (outputPath?.endsWith(\".jsonl\")) {\n\t\t\t\tconst filePath = this.ctx.session.exportToJsonl(outputPath);\n\t\t\t\tthis.ctx.showRecord(`Session exported to: ${filePath}`);\n\t\t\t} else {\n\t\t\t\tconst filePath = await this.ctx.session.exportToHtml(outputPath);\n\t\t\t\tthis.ctx.showRecord(`Session exported to: ${filePath}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate getPathArgument(text: string, command: \"/export\" | \"/import\"): string | undefined {\n\t\tif (text === command) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!text.startsWith(`${command} `)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst argsString = text.slice(command.length + 1).trimStart();\n\t\tif (!argsString) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst firstChar = argsString[0];\n\t\tif (firstChar === '\"' || firstChar === \"'\") {\n\t\t\tconst closingQuoteIndex = argsString.indexOf(firstChar, 1);\n\t\t\tif (closingQuoteIndex < 0) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn argsString.slice(1, closingQuoteIndex);\n\t\t}\n\n\t\tconst firstWhitespaceIndex = argsString.search(/\\s/);\n\t\tif (firstWhitespaceIndex < 0) {\n\t\t\treturn argsString;\n\t\t}\n\t\treturn argsString.slice(0, firstWhitespaceIndex);\n\t}\n\n\tasync handleImport(text: string): Promise<void> {\n\t\tconst inputPath = this.getPathArgument(text, \"/import\");\n\t\tif (!inputPath) {\n\t\t\tthis.ctx.showError(\"Usage: /import <path.jsonl>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst confirmed = await this.ctx.showExtensionConfirm(\n\t\t\t\"Import session\",\n\t\t\t`Replace current session with ${inputPath}?`,\n\t\t);\n\t\tif (!confirmed) {\n\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.ctx.stopLoadingAnimation();\n\t\t\tthis.ctx.statusContainer.clear();\n\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.showRecord(`Session imported from: ${inputPath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.ctx.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath, selectedCwd);\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\t\tthis.ctx.showRecord(`Session imported from: ${inputPath}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (error instanceof SessionImportFileNotFoundError) {\n\t\t\t\tthis.ctx.showError(`Failed to import session: ${error.message}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to import session\", error);\n\t\t}\n\t}\n\n\tasync handleShare(): Promise<void> {\n\t\t// Check if gh is available and logged in\n\t\ttry {\n\t\t\tconst authResult = spawnSync(\"gh\", [\"auth\", \"status\"], { encoding: \"utf-8\" });\n\t\t\tif (authResult.status !== 0) {\n\t\t\t\tthis.ctx.showError(\"GitHub CLI is not logged in. Run 'gh auth login' first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.ctx.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Export to a temp file\n\t\tconst tmpFile = path.join(os.tmpdir(), \"session.html\");\n\t\ttry {\n\t\t\tawait this.ctx.session.exportToHtml(tmpFile);\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Show cancellable loader, replacing the editor\n\t\tconst loader = new BorderedLoader(this.ctx.ui, theme, \"Creating gist...\");\n\t\tthis.ctx.editorContainer.clear();\n\t\tthis.ctx.editorContainer.addChild(loader);\n\t\tthis.ctx.ui.setFocus(loader);\n\t\tthis.ctx.ui.requestRender();\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tthis.ctx.editorContainer.clear();\n\t\t\tthis.ctx.editorContainer.addChild(this.ctx.editor);\n\t\t\tthis.ctx.ui.setFocus(this.ctx.editor);\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t};\n\n\t\t// Create a secret gist asynchronously\n\t\tlet proc: ReturnType<typeof spawn> | null = null;\n\n\t\tloader.onAbort = () => {\n\t\t\tproc?.kill();\n\t\t\trestoreEditor();\n\t\t\tthis.ctx.showStatus(\"Share cancelled\");\n\t\t};\n\n\t\ttry {\n\t\t\tconst result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {\n\t\t\t\tproc = spawn(\"gh\", [\"gist\", \"create\", \"--public=false\", tmpFile]);\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => resolve({ stdout, stderr, code }));\n\t\t\t});\n\n\t\t\tif (loader.signal.aborted) return;\n\n\t\t\trestoreEditor();\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst errorMsg = result.stderr?.trim() || \"Unknown error\";\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${errorMsg}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Extract gist ID from the URL returned by gh\n\t\t\t// gh returns something like: https://gist.github.com/username/GIST_ID\n\t\t\tconst gistUrl = result.stdout?.trim();\n\t\t\tconst gistId = gistUrl?.split(\"/\").pop();\n\t\t\tif (!gistId) {\n\t\t\t\tthis.ctx.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// The gist is the artifact. A viewer URL is an extra only when one is\n\t\t\t// actually configured (see getShareViewerUrl).\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.ctx.showRecord(previewUrl ? `Share URL: ${previewUrl}\\nGist: ${gistUrl}` : `Gist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * `/copy`, `/copy all`, `/copy <n>` — the conversation, not a picture of it.\n\t *\n\t * What is on screen is markdown already rendered: a table is box drawing, a\n\t * code block is a bordered panel, every line is wrapped to whatever width\n\t * the window happened to be. Dragging over that and pasting it into a\n\t * document pastes the drawing — dotted rules, broken table edges, and\n\t * wrapping frozen at eighty columns — because a terminal's clipboard carries\n\t * glyphs and nothing else.\n\t *\n\t * So the copy goes back to the source and puts it on the clipboard twice:\n\t * the markdown as text, and HTML for anything that takes a rich paste. Word\n\t * and Confluence then paste real headings, lists and tables; a terminal or a\n\t * commit message still gets the markdown. Which flavours landed depends on\n\t * the platform (see `copyRichToClipboard`), so the status line says.\n\t */\n\tasync handleCopy(text = \"\"): Promise<void> {\n\t\tconst argument = text\n\t\t\t.replace(/^\\/copy\\s*/, \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase();\n\t\t// `/copy 0` is a typo, not a request for nothing: taking it at face value\n\t\t// would copy the whole session and report it as zero turns.\n\t\tconst turns = /^\\d+$/.test(argument) ? Math.max(1, Number.parseInt(argument, 10)) : undefined;\n\t\tconst whole = argument === \"all\" || argument === \"session\";\n\t\tif (argument && !whole && turns === undefined) {\n\t\t\tthis.ctx.showWarning(\"Usage: /copy [all|<number of turns>]\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst markdown =\n\t\t\twhole || turns !== undefined\n\t\t\t\t? this.ctx.session.getTranscriptMarkdown({ turns, agentLabel: APP_TITLE })\n\t\t\t\t: this.ctx.session.getLastAssistantText();\n\t\tif (!markdown) {\n\t\t\tthis.ctx.showError(\n\t\t\t\twhole || turns !== undefined ? \"Nothing in this session to copy yet.\" : \"No agent messages to copy yet.\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tconst subject = whole\n\t\t\t? \"session transcript\"\n\t\t\t: turns !== undefined\n\t\t\t\t? `last ${turns} turn${turns > 1 ? \"s\" : \"\"}`\n\t\t\t\t: \"last agent message\";\n\t\ttry {\n\t\t\tconst flavour = await copyRichToClipboard({ text: markdown, html: markdownToHtml(markdown) });\n\t\t\t// Naming the flavour is not chatter: \"copied\" meaning markdown on one\n\t\t\t// machine and a formatted paste on another is how this gets reported\n\t\t\t// as broken. The user finds out here rather than in the document.\n\t\t\tconst as = flavour === \"rich\" ? \"markdown + formatted text\" : \"markdown\";\n\t\t\tthis.ctx.showStatus(`Copied ${subject} as ${as}`);\n\t\t} catch (error) {\n\t\t\t// The usual reason is a machine with no clipboard to write to (a bare\n\t\t\t// SSH session, no xclip) or a transcript past what OSC 52 can carry.\n\t\t\t// Both have the same answer, and it is worth naming here rather than\n\t\t\t// leaving \"Failed to copy to clipboard\" as the whole reply.\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\tthis.ctx.showError(`Could not copy the ${subject}: ${reason}. /export writes it to a file instead.`);\n\t\t}\n\t}\n\n\t/** The session's chip as it currently renders, for echoing back after a change. */\n\tprivate currentChip(): string {\n\t\tconst chip = renderSessionChip(\n\t\t\tthis.ctx.sessionManager.getDisplayName(),\n\t\t\tthis.ctx.sessionManager.getSessionColorSlot(),\n\t\t);\n\t\treturn chip?.styled ?? this.ctx.sessionManager.getDisplayName();\n\t}\n\n\thandleName(text: string): void {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\t// Every session has a name now — an auto-assigned slug until someone\n\t\t\t// picks one — so there is always something to report, and the reply\n\t\t\t// shows the chip rather than describing it.\n\t\t\tconst chosen = this.ctx.sessionManager.getSessionName();\n\t\t\tconst label = chosen ? \"Session name:\" : \"Session name (auto):\";\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"dim\", label)} ${this.currentChip()}  ${theme.fg(\"dim\", \"/name <name> to change\")}`,\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.session.setSessionName(name);\n\t\t// The chip itself rides the prompt's top border, so the confirmation only\n\t\t// has to bridge the moment between pressing enter and looking at it.\n\t\tthis.ctx.notify(`${theme.fg(\"dim\", \"Session name set:\")} ${this.currentChip()}`);\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\t/**\n\t * `/color <slot>` sets the session's chip colour directly, where a slot is a\n\t * number, a name (`green`), or a name's initial (`g`). Bare `/color` is\n\t * handled by the caller, which opens the swatch picker instead — the names\n\t * describe a hue family rather than the exact colour a theme fills the slot\n\t * with, so which one reads best here is still a thing you look at.\n\t *\n\t * The confirmation names the slot it landed on, because the aliases fold\n\t * neighbouring hues together: `/color red` answering with \"magenta\" is how\n\t * you learn the palette has no red of its own.\n\t */\n\thandleColor(text: string): boolean {\n\t\tconst arg = text.replace(/^\\/color\\s*/, \"\").trim();\n\t\tif (!arg) return false;\n\t\tconst slot = parseSessionColorSlot(arg);\n\t\tif (slot === undefined) {\n\t\t\tthis.ctx.showWarning(\n\t\t\t\t`Usage: /color <1-${SESSION_COLOR_SLOTS}> or /color <${SESSION_COLOR_NAME_LIST.join(\"|\")}> ` +\n\t\t\t\t\t\"(first letter works too), or /color on its own to pick one\",\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tthis.ctx.session.setSessionColor(slot);\n\t\tconst name = sessionColorName(slot);\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\t`${theme.fg(\"dim\", \"Session color set:\")} ${this.currentChip()}${name ? `  ${theme.fg(\"dim\", name)}` : \"\"}`,\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.ctx.ui.requestRender();\n\t\treturn true;\n\t}\n\n\thandleSession(): void {\n\t\tconst stats = this.ctx.session.getSessionStats();\n\t\tconst sessionName = this.ctx.sessionManager.getSessionName();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", sessionName ? \"Name:\" : \"Name (auto):\")} ${this.currentChip()}\\n`;\n\t\tconst branch = this.ctx.sessionManager.getSessionBranch();\n\t\tif (branch) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Branch:\")} ${branch}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile ?? \"In-memory\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${stats.tokens.input.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${stats.tokens.output.toLocaleString()}\\n`;\n\t\tif (stats.tokens.cacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${stats.tokens.cacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (stats.tokens.cacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${stats.tokens.cacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.tokens.total.toLocaleString()}\\n`;\n\n\t\tif (stats.cost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.cost.toFixed(4)}`;\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleChangelog(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tif (allEntries.length === 0) {\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(theme.fg(\"dim\", \"No changelog entries found.\"), 1, 0));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst changelogMarkdown = allEntries\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.map((e) => e.content)\n\t\t\t.join(\"\\n\\n\");\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 0, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleHotkeys(): void {\n\t\t// Navigation keybindings\n\t\tconst cursorUp = keyDisplayText(\"tui.editor.cursorUp\");\n\t\tconst cursorDown = keyDisplayText(\"tui.editor.cursorDown\");\n\t\tconst cursorLeft = keyDisplayText(\"tui.editor.cursorLeft\");\n\t\tconst cursorRight = keyDisplayText(\"tui.editor.cursorRight\");\n\t\tconst cursorWordLeft = keyDisplayText(\"tui.editor.cursorWordLeft\");\n\t\tconst cursorWordRight = keyDisplayText(\"tui.editor.cursorWordRight\");\n\t\tconst cursorLineStart = keyDisplayText(\"tui.editor.cursorLineStart\");\n\t\tconst cursorLineEnd = keyDisplayText(\"tui.editor.cursorLineEnd\");\n\t\tconst jumpForward = keyDisplayText(\"tui.editor.jumpForward\");\n\t\tconst jumpBackward = keyDisplayText(\"tui.editor.jumpBackward\");\n\n\t\t// Editing keybindings\n\t\tconst submit = keyDisplayText(\"tui.input.submit\");\n\t\tconst newLine = keyDisplayText(\"tui.input.newLine\");\n\t\tconst deleteWordBackward = keyDisplayText(\"tui.editor.deleteWordBackward\");\n\t\tconst deleteWordForward = keyDisplayText(\"tui.editor.deleteWordForward\");\n\t\tconst deleteToLineStart = keyDisplayText(\"tui.editor.deleteToLineStart\");\n\t\tconst deleteToLineEnd = keyDisplayText(\"tui.editor.deleteToLineEnd\");\n\t\tconst yank = keyDisplayText(\"tui.editor.yank\");\n\t\tconst yankPop = keyDisplayText(\"tui.editor.yankPop\");\n\t\tconst undo = keyDisplayText(\"tui.editor.undo\");\n\t\tconst tab = keyDisplayText(\"tui.input.tab\");\n\n\t\t// App keybindings\n\t\tconst interrupt = keyDisplayText(\"app.interrupt\");\n\t\tconst clear = keyDisplayText(\"app.clear\");\n\t\tconst exit = keyDisplayText(\"app.exit\");\n\t\tconst suspend = keyDisplayText(\"app.suspend\");\n\t\tconst cycleThinkingLevel = keyDisplayLabel(\"app.thinking.cycleForward\");\n\t\tconst cycleThinkingLevelBack = keyDisplayLabel(\"app.thinking.cycleBackward\");\n\t\tconst cycleModelForward = keyDisplayLabel(\"app.model.cycleForward\");\n\t\tconst expandTools = keyDisplayText(\"app.tools.expand\");\n\t\tconst toggleThinking = keyDisplayText(\"app.thinking.toggle\");\n\t\tconst cycleTaskView = keyDisplayLabel(\"app.tasks.cycleForward\");\n\t\tconst cycleTaskViewBack = keyDisplayLabel(\"app.tasks.cycleBackward\");\n\t\tconst teamFocus = keyDisplayText(\"app.team.focus\");\n\t\tconst teamNudge = keyDisplayText(\"app.team.nudge\");\n\t\tconst teamAttach = keyDisplayText(\"app.team.attach\");\n\t\tconst externalEditor = keyDisplayText(\"app.editor.external\");\n\t\tconst cycleModelBackward = keyDisplayLabel(\"app.model.cycleBackward\");\n\t\tconst followUp = keyDisplayText(\"app.message.followUp\");\n\t\tconst dequeue = keyDisplayText(\"app.message.dequeue\");\n\t\tconst pasteImage = keyDisplayText(\"app.clipboard.pasteImage\");\n\t\tconst viewForward = keyDisplayLabel(\"app.view.cycleForward\");\n\t\tconst viewBackward = keyDisplayLabel(\"app.view.cycleBackward\");\n\t\tconst voice = keyDisplayText(\"app.input.voiceTranscribe\");\n\t\tconst changeDirectory = keyDisplayText(\"app.session.changeDirectory\");\n\t\tconst openSettings = keyDisplayText(\"app.settings.open\");\n\t\tconst openHotkeys = keyDisplayText(\"app.hotkeys.open\");\n\t\tconst cycleMode = keyDisplayLabel(\"app.mode.cycleForward\");\n\t\tconst cycleModeBack = keyDisplayLabel(\"app.mode.cycleBackward\");\n\t\tconst sessionResume = keyDisplayText(\"app.session.resume\");\n\t\tconst cycleSessionColor = keyDisplayLabel(\"app.session.color.cycleForward\");\n\t\tconst cycleSessionColorBackward = keyDisplayLabel(\"app.session.color.cycleBackward\");\n\t\tconst chromeForward = keyDisplayLabel(\"app.chrome.cycleForward\");\n\t\tconst chromeBackward = keyDisplayLabel(\"app.chrome.cycleBackward\");\n\t\tconst scrollPageUp = keyDisplayText(\"app.scroll.pageUp\");\n\t\tconst scrollPageDown = keyDisplayText(\"app.scroll.pageDown\");\n\t\tconst scrollTop = keyDisplayText(\"app.scroll.top\");\n\t\tconst scrollBottom = keyDisplayText(\"app.scroll.bottom\");\n\t\tconst scrollLineUp = keyDisplayText(\"app.scroll.lineUp\");\n\t\tconst scrollLineDown = keyDisplayText(\"app.scroll.lineDown\");\n\t\tconst scrollExit = keyDisplayText(\"app.scroll.exit\");\n\t\tconst scrollSearch = keyDisplayText(\"app.scroll.search\");\n\t\tconst scrollSearchInView = keyDisplayText(\"app.scroll.searchInView\");\n\t\tconst scrollSearchNext = keyDisplayText(\"app.scroll.searchNext\");\n\t\tconst scrollSearchPrevious = keyDisplayText(\"app.scroll.searchPrevious\");\n\t\tconst scrollPreviousMessage = keyDisplayText(\"app.scroll.previousMessage\");\n\t\tconst scrollNextMessage = keyDisplayText(\"app.scroll.nextMessage\");\n\t\tconst copyMessage = keyDisplayText(\"app.clipboard.copyMessage\");\n\t\tconst redo = keyDisplayText(\"tui.editor.redo\");\n\n\t\tlet hotkeys = `\nGrouped by what you are doing, not by what the key is. Seven groups, none of the\nlearned ones bigger than five — the size a person can actually hold. Three are\nfree: **Flow** is what every terminal program already taught you, **Scroll** is\nwhat every pager did, and every picker prints its own keys on its own hint line,\nso you read those instead of remembering them. **Screen** is one key.\n\n**Compose** — the message in your hands\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${externalEditor}\\` | Edit the message in \\`$VISUAL\\` / \\`$EDITOR\\` |\n| \\`${voice}\\` | Speak instead of type |\n| \\`${pasteImage}\\` | Paste image from clipboard |\n| \\`${copyMessage}\\` | Copy the agent's last message (\\`/copy\\`; \\`/copy all\\` for the session) |\n| \\`${followUp}\\` | Queue a follow-up while the agent works |\n| \\`${dequeue}\\` | Bring every queued message back to the editor |\n| \\`/\\` \\`!\\` \\`!!\\` | Slash commands · run bash · run bash off the record |\n\n**Steer** — what the agent is before it runs\nThe only three keys that change what happens next, and the only three that cost\nanything. Step forward on the first key, back on the second; the footer shows\nall three.\n\n| Key | Steps | Through |\n|-----|-------|---------|\n| \\`${cycleMode}\\` / \\`${cycleModeBack}\\` | Agent mode | ask → plan → build → debug (\\`/mode\\` picks one) |\n| \\`${cycleModelForward}\\` / \\`${cycleModelBackward}\\` | Model | your enabled models (\\`/model\\` picks one) |\n| \\`${cycleThinkingLevel}\\` / \\`${cycleThinkingLevelBack}\\` | Thinking level | off → … → high |\n\n**Read** — what you see of what it did\nFree and reversible, every one: nothing here touches the work, only the window\nonto it. Press again or add \\`Shift\\` and you are back where you were.\n\n| Key | Action |\n|-----|--------|\n| \\`${viewForward}\\` / \\`${viewBackward}\\` | Step tool output: radar → peek → full |\n| \\`${cycleTaskView}\\` / \\`${cycleTaskViewBack}\\` | Step the task panel: tasks → subagents → teams |\n| \\`${expandTools}\\` | Jump to the full view and back, without moving the dial |\n| \\`${toggleThinking}\\` | Show or hide thinking blocks |\n| \\`${teamFocus}\\` | Focus the team roster — \\`${teamNudge}\\` nudges, \\`${teamAttach}\\` attaches, \\`q\\`/\\`${interrupt}\\` leaves (\\`--team\\`) |\n\n**Screen** — how much room there is to see it in\nOne dial for the furniture. Everything below the transcript except the prompt,\nwhich never hides.\n\n| Key | Steps | Through |\n|-----|-------|---------|\n| \\`${chromeForward}\\` / \\`${chromeBackward}\\` | Chrome | full → compact (one-row footer, no task list) → bare (neither) — \\`/chrome\\` picks one |\n\nIt also gets out of the way on its own: the footer lends its rows to the\ncompletion list while that is open, and the task list keeps its counts but drops\nits rows while the agent is mid-turn. Both come back by themselves.\n\n**Scroll** — where in the session you are looking\nThe wheel and these keys move the same view, and once it is scrolled back it\n**stays** there: output keeps arriving underneath without moving what you are\nreading. The bottom row tells you where you are and how to get back.\n\n| Key | Action |\n|-----|--------|\n| \\`${scrollPageUp}\\` / \\`${scrollPageDown}\\` | Scroll a page — \\`${scrollPageUp}\\` is also how you start |\n| \\`${scrollTop}\\` / \\`${scrollBottom}\\` | Jump to the start of the session / back to live |\n| \\`${scrollLineUp}\\` / \\`${scrollLineDown}\\` | A line at a time (while scrolled back) |\n| \\`${scrollPreviousMessage}\\` / \\`${scrollNextMessage}\\` | Jump to your previous / next message |\n| \\`${scrollSearch}\\` | Search the transcript — \\`${scrollSearchInView}\\` once you are already scrolled back |\n| \\`${scrollSearchNext}\\` / \\`${scrollSearchPrevious}\\` | Step the matches, once the query is committed with \\`${submit}\\` |\n| \\`${scrollExit}\\` | Back to live output |\n| Wheel | Three lines a notch, wherever you turn it |\n\nSearch runs backwards, like \\`${scrollSearch}\\` in a shell: the newest match first,\nbecause what you are looking for in a session is behind you.\n\nScrolling to the bottom hands the live view back on its own, and so does typing:\nany key that is not one of these returns you to live and then does its usual job.\n\n**Go** — sessions and places\nEach takes the screen and hands it back on \\`${interrupt}\\`, and each has a slash\ncommand that does the same thing.\n\n| Key | Action |\n|-----|--------|\n| \\`${sessionResume}\\` | Resume a session from history (\\`/resume\\`) |\n| \\`${changeDirectory}\\` | Change working directory (\\`/cd\\`) |\n| \\`${cycleSessionColor}\\` / \\`${cycleSessionColorBackward}\\` | Step the session chip's color (\\`/color\\` picks one) |\n| \\`${openSettings}\\` | Open settings (\\`/settings\\`) |\n| \\`${openHotkeys}\\` | Show this list (\\`/hotkeys\\`) |\n| \\`/tree\\` | Open the session tree |\n\n**Flow** — getting out, getting back\n| Key | Action |\n|-----|--------|\n| \\`${interrupt}\\` | Cancel autocomplete / abort streaming |\n| \\`${clear}\\` | Clear editor (first) / exit (second) |\n| \\`${exit}\\` | Exit when the editor is empty |\n| \\`${suspend}\\` | Suspend to background |\n\n### What the chord tells you\n\n\\`Alt\\`+letter **sets a value** and nothing takes the screen — you keep typing.\nSeven of those are dials, and \\`Shift\\` always steps one back: **a**gent mode,\n**m**odel, **t**hinking, tool **o**utput, task **l**ist, session **c**olor, and\nchrome (**z**, the one letter that names nothing — its stop is the shape of the\nscreen in front of you).\n\n\\`Ctrl\\`+letter **acts on what is drawn right now** and shares its letter with the\n\\`Alt\\` key for the same subject. \\`${viewForward}\\` sets how much tool output there\never is, \\`${expandTools}\\` jumps to all of it and back; \\`${cycleThinkingLevel}\\`\nsets how much thinking there ever is, \\`${toggleThinking}\\` shows or hides what you\nhave. Two subjects, two letters, four keys.\n\nThe thinking level also answers to \\`Shift+Tab\\` — it is the one dial with no\nslash command, so that is the way to it on a terminal that does not send \\`Alt\\`.\n\nInside a picker every \\`Ctrl\\` key still edits the query, so the picker's own verbs\nare on \\`Alt\\` and its hint line names them.\n\n### The editor\nStandard readline/emacs bindings — reference, not something to memorise.\n\\`${scrollPageUp}\\` / \\`${scrollPageDown}\\` are **not** here: they scroll the session,\nnot the prompt, which is one to three lines almost every time you look at it.\n\n| Key | Action |\n|-----|--------|\n| \\`${cursorUp}\\` / \\`${cursorDown}\\` / \\`${cursorLeft}\\` / \\`${cursorRight}\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` / \\`${cursorLineEnd}\\` | Start / end of line |\n| \\`${jumpForward}\\` / \\`${jumpBackward}\\` | Jump forward / backward to character |\n| \\`${deleteWordBackward}\\` / \\`${deleteWordForward}\\` | Delete word backwards / forwards |\n| \\`${deleteToLineStart}\\` / \\`${deleteToLineEnd}\\` | Delete to start / end of line |\n| \\`${yank}\\` / \\`${yankPop}\\` | Paste the most-recently-deleted text / cycle older ones |\n| \\`${undo}\\` / \\`${redo}\\` | Undo / redo |\n`;\n\n\t\t// Add extension-registered shortcuts\n\t\tconst extensionRunner = this.ctx.session.extensionRunner;\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.ctx.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size > 0) {\n\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\tconst keyDisplay = formatKeyText(key, { capitalize: true });\n\t\t\t\thotkeys += `| \\`${keyDisplay}\\` | ${description} |\\n`;\n\t\t\t}\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Keyboard Shortcuts\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 0, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\tasync handleClear(): Promise<void> {\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.newSession();\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"accent\", \"✓ New session started\")}`, 1, 0));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t}\n\t}\n\n\t/**\n\t * `/cd [path]` — move the whole session to another directory.\n\t *\n\t * Written for the case it exists to serve: you are done in one repo and want\n\t * to work in the next one without losing the process, its provider auth, or\n\t * its warmed model list. Bare `/cd` goes home, `/cd -` goes back to where you\n\t * came from, `~` and relative paths resolve as a shell would.\n\t *\n\t * The move starts a fresh session rooted in the target — see\n\t * `AgentSessionRuntime.changeDirectory` for why the whole runtime is rebuilt\n\t * rather than a cwd string reassigned.\n\t */\n\tasync handleChangeDirectory(text: string): Promise<void> {\n\t\tconst prefix = \"/cd\";\n\t\tconst rawArg = text.startsWith(prefix) ? text.slice(prefix.length).trim() : text.trim();\n\t\tconst previousCwd = this.ctx.sessionManager.getCwd();\n\n\t\tlet target: string;\n\t\tif (!rawArg || rawArg === \"~\") {\n\t\t\ttarget = os.homedir();\n\t\t} else if (rawArg === \"-\") {\n\t\t\tif (!this.previousCwd) {\n\t\t\t\tthis.ctx.showWarning(\"No previous directory to return to\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttarget = this.previousCwd;\n\t\t} else {\n\t\t\tconst expanded = rawArg.startsWith(\"~/\") ? path.join(os.homedir(), rawArg.slice(2)) : rawArg;\n\t\t\ttarget = path.resolve(previousCwd, expanded);\n\t\t}\n\n\t\tif (path.resolve(target) === path.resolve(previousCwd)) {\n\t\t\tthis.ctx.notify(`Already in ${target}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.changeDirectory(target);\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.previousCwd = previousCwd;\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"accent\", \"✓ Working directory\")} ${theme.fg(\"muted\", result.cwd)}\\n` +\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t\t`New session started here. ${keyDisplayText(\"app.session.resume\")} reopens the session you left in ${previousCwd}.`,\n\t\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof ChangeDirectoryError) {\n\t\t\t\tthis.ctx.showError(error.message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(`Failed to change directory to ${target}`, error);\n\t\t}\n\t}\n\n\t/** Directory completions for `/cd <prefix>`, plus the two shell shorthands. */\n\tgetChangeDirectoryCompletions(argumentPrefix: string): Array<{ value: string; label: string }> {\n\t\tconst cwd = this.ctx.sessionManager.getCwd();\n\t\tconst expanded = argumentPrefix.startsWith(\"~/\")\n\t\t\t? path.join(os.homedir(), argumentPrefix.slice(2))\n\t\t\t: argumentPrefix;\n\t\t// A prefix ending in a separator names the directory to list; otherwise the\n\t\t// last segment is a partial name to filter its parent by.\n\t\tconst endsWithSep = expanded.endsWith(\"/\") || expanded.endsWith(path.sep);\n\t\tconst base = endsWithSep ? expanded : path.dirname(expanded);\n\t\tconst partial = endsWithSep ? \"\" : path.basename(expanded);\n\t\tconst searchDir = path.isAbsolute(base) ? base : path.resolve(cwd, base || \".\");\n\n\t\tconst completions: Array<{ value: string; label: string }> = [];\n\t\tif (!argumentPrefix) {\n\t\t\tif (this.previousCwd) completions.push({ value: \"-\", label: `- (${this.previousCwd})` });\n\t\t\tcompletions.push({ value: \"~\", label: `~ (${os.homedir()})` });\n\t\t}\n\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(searchDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn completions;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tif (partial && !entry.name.startsWith(partial)) continue;\n\t\t\tif (!partial && entry.name.startsWith(\".\")) continue;\n\t\t\tconst joined =\n\t\t\t\tbase && !endsWithSep\n\t\t\t\t\t? path.join(path.dirname(argumentPrefix), entry.name)\n\t\t\t\t\t: `${argumentPrefix}${entry.name}`;\n\t\t\tcompletions.push({ value: `${joined}/`, label: `${entry.name}/` });\n\t\t}\n\t\treturn completions.slice(0, 50);\n\t}\n\n\thandleDebug(): void {\n\t\tconst width = this.ctx.ui.terminal.columns;\n\t\tconst height = this.ctx.ui.terminal.rows;\n\t\tconst allLines = this.ctx.ui.render(width);\n\n\t\tconst debugLogPath = getDebugLogPath();\n\t\tconst debugData = [\n\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t`Terminal: ${width}x${height}`,\n\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\"\",\n\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t...allLines.map((line, idx) => {\n\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t}),\n\t\t\t\"\",\n\t\t\t\"=== Agent messages (JSONL) ===\",\n\t\t\t...this.ctx.session.messages.map((msg) => JSON.stringify(msg)),\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\n\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(\n\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 0),\n\t\t);\n\t\tthis.ctx.ui.requestRender();\n\t}\n}\n"]}