{"version":3,"file":"interactive-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/interactive-mode.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,KAAK,EAAoB,YAAY,EAAmC,MAAM,uBAAuB,CAAC;AAY7G,OAAO,EAGN,SAAS,EAUT,GAAG,EAEH,MAAM,wBAAwB,CAAC;AAchC,OAAO,EAAE,KAAK,YAAY,EAA2C,MAAM,6BAA6B,CAAC;AA0EzG,OAAO,EAA8C,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AA2ClH;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACtC,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC/B,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,eAAe;IA8I1B,OAAO,CAAC,OAAO;IA7IhB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,wBAAwB,CAAY;IAC5C,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IAEvB,yGAAyG;IACzG,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,oBAAoB,CAAQ;IAEpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,eAAe,CAAC,CAAyB;IACjD,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAiC;IAC9D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAgB;IACtD,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,sBAAsB,CAAyB;IAEvD,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,iBAAiB,CAAiC;IAG1D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,eAAe,CAAoC;IAG3D,OAAO,CAAC,kBAAkB,CAAoD;IAC9E,OAAO,CAAC,gBAAgB,CAA2C;IAGnE,OAAO,CAAC,YAAY,CAA6C;IAGjE,OAAO,CAAC,kBAAkB,CAAS;IAGnC,OAAO,CAAC,iBAAiB,CAAS;IAGlC,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,4BAA4B,CAAC,CAAa;IAClD,OAAO,CAAC,4BAA4B,CAAC,CAAa;IAGlD,OAAO,CAAC,UAAU,CAAS;IAG3B,OAAO,CAAC,aAAa,CAAiD;IAGtE,OAAO,CAAC,qBAAqB,CAAgC;IAG7D,OAAO,CAAC,uBAAuB,CAAiB;IAGhD,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,2BAA2B,CAAC,CAAa;IAGjD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAAC,CAAa;IAGxC,OAAO,CAAC,wBAAwB,CAAiC;IAGjE,OAAO,CAAC,iBAAiB,CAAS;IAGlC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,yBAAyB,CAAS;IAG1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,WAAW,CAAkB;IAGrC,OAAO,CAAC,iBAAiB,CAAqD;IAC9E,OAAO,CAAC,cAAc,CAAkD;IACxE,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,mCAAmC,CAAyB;IAGpE,OAAO,CAAC,qBAAqB,CAAuD;IACpF,OAAO,CAAC,qBAAqB,CAAuD;IAEpF,2FAA2F;IAC3F,OAAO,CAAC,oBAAoB,CAAa;IAEzC,2FAA2F;IAC3F,OAAO,CAAC,oBAAoB,CAAa;IAGzC,OAAO,CAAC,YAAY,CAA6D;IAGjF,OAAO,CAAC,eAAe,CAAY;IAGnC,OAAO,CAAC,aAAa,CAAoC;IAGzD,OAAO,CAAC,YAAY,CAA6D;IAGjF,OAAO,CAAC,aAAa,CAAoC;IAGzD,OAAO,KAAK,KAAK,GAEhB;IACD,OAAO,KAAK,cAAc,GAEzB;IACD,OAAO,KAAK,eAAe,GAE1B;IAED,YACC,OAAO,EAAE,YAAY,EACb,OAAO,GAAE,sBAA2B,EA+D5C;IAED,OAAO,CAAC,iBAAiB;IA6EzB,OAAO,CAAC,4BAA4B;IA4BpC,OAAO,CAAC,sBAAsB;IA6C9B,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,gBAAgB;YAYV,kBAAkB;IA4BhC,OAAO,CAAC,mBAAmB;YAqWb,wBAAwB;IAOtC,OAAO,CAAC,2BAA2B;YAYrB,sBAAsB;YA+BtB,sBAAsB;IA4EpC,OAAO,CAAC,gBAAgB;IA8BxB,OAAO,CAAC,yBAAyB;IAgE3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqI1B;IAED;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAgEzB;YAMa,kBAAkB;YAkClB,sBAAsB;IA4CpC;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,4BAA4B;IAWpC,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;IACH,OAAO,CAAC,YAAY;IAiBpB,OAAO,CAAC,oBAAoB;IA0B5B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;IAkCxB,OAAO,CAAC,iBAAiB;IA8BzB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAmBpB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyDzB,OAAO,CAAC,mBAAmB;YA8Ib,cAAc;IA2F5B;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAMnC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAoD/B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAK1B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA0C1B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,sBAAsB;IA2C9B,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAE9C;;OAEG;IACH,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,qBAAqB;IAgC7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,iCAAiC;IAWzC,OAAO,CAAC,oCAAoC;IAO5C;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA2GhC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkD7B;;OAEG;IACH,OAAO,CAAC,qBAAqB;YASf,oBAAoB;IASlC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkD1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAmC3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAK3B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAkEhC;;OAEG;IACH,OAAO,CAAC,mBAAmB;YAWb,mBAAmB;IAuFjC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAsB1B,OAAO,CAAC,gBAAgB;YAkEV,yBAAyB;IAsBvC,OAAO,CAAC,wBAAwB;IA8OhC,OAAO,CAAC,gBAAgB;YAOV,WAAW;IAsYzB,+CAA+C;IAC/C,OAAO,CAAC,kBAAkB;IAS1B;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAsBlB,OAAO,CAAC,gBAAgB;IAoGxB;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAyE5B,qBAAqB,IAAI,IAAI,CAkB5B;IAEK,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAOpC;IAED,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,WAAW;IAKnB;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAS;YAEjB,QAAQ;YA2BR,sBAAsB;IAKpC,OAAO,CAAC,WAAW;YAoBL,cAAc;IAoC5B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,wBAAwB;IAIhC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,kBAAkB;YAWZ,UAAU;IAsBxB,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,6BAA6B;IAkBrC,OAAO,CAAC,kBAAkB;IAoD1B,WAAW,IAAI,IAAI,CAGlB;IAED,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAKzD;IAED,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,2BAA2B;IAgBnC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAM7D;IAED,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAwBxE;IAED;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,4BAA4B;IAmBpC,OAAO,CAAC,6BAA6B;IAqBrC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,kBAAkB;YAWZ,oBAAoB;IA8FlC,6DAA6D;IAC7D,OAAO,CAAC,0BAA0B;IAYlC;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,oBAAoB;YAwId,kBAAkB;YA4BlB,mBAAmB;YA2BnB,kBAAkB;YAclB,4BAA4B;IAM1C,OAAO,CAAC,iBAAiB;YA+BX,kBAAkB;IAsHhC,OAAO,CAAC,uBAAuB;IAmC/B,OAAO,CAAC,gBAAgB;IAgIxB,OAAO,CAAC,mBAAmB;YAmCb,mBAAmB;YAqBnB,iBAAiB;YA+CjB,eAAe;YA2Ff,mBAAmB;YA4EnB,mBAAmB;YAenB,kBAAkB;IAiGhC,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,iBAAiB;IAqBzB,OAAO,CAAC,oBAAoB;IAgF5B,OAAO,CAAC,sBAAsB;IAqB9B;;OAEG;IACH,OAAO,CAAC,aAAa;IAYrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAIxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,oBAAoB;YA+Gd,kBAAkB;IAgBhC,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,qBAAqB;YAMf,iBAAiB;YAgGjB,oBAAoB;YAYpB,iBAAiB;IAoE/B,IAAI,IAAI,IAAI,CA4BX;CACD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CACjC,SAAS,EAAE,SAAS,GAAG,GAAG,EAC1B,gBAAgB,EAAE,gBAAgB,EAClC,gBAAgB,EAAE,gBAAgB,GAChC,IAAI,CAGN","sourcesContent":["/**\n * Interactive mode for the coding agent.\n * Handles TUI rendering and user interaction, delegating business logic to AgentSession.\n */\n\nimport * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { AgentMessage } from \"@apholdings/jensen-agent-core\";\nimport type { AssistantMessage, ImageContent, Message, Model, OAuthProviderId } from \"@apholdings/jensen-ai\";\nimport type {\n\tAutocompleteItem,\n\tEditorAction,\n\tEditorComponent,\n\tEditorTheme,\n\tKeyId,\n\tMarkdownTheme,\n\tOverlayHandle,\n\tOverlayOptions,\n\tSlashCommand,\n} from \"@apholdings/jensen-tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\ttype Component,\n\tContainer,\n\tfuzzyFilter,\n\tgetEditorKeybindings,\n\tLoader,\n\tMarkdown,\n\tmatchesKey,\n\tProcessTerminal,\n\tSpacer,\n\tText,\n\tTruncatedText,\n\tTUI,\n\tvisibleWidth,\n} from \"@apholdings/jensen-tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport {\n\tAPP_NAME,\n\tFILE_PREFIX,\n\tgetAuthPath,\n\tgetDebugLogPath,\n\tgetShareViewerUrl,\n\tgetUpdateInstruction,\n\tisDevMode,\n\tPACKAGE_NAME,\n\tsemverGt,\n\tVERSION,\n} from \"../../config.js\";\nimport { type AgentSession, type AgentSessionEvent, parseSkillBlock } from \"../../core/agent-session.js\";\nimport { BRIEF_ONLY_COMMAND_USAGE, parseBriefOnlyCommand, runBriefOnlyCommand } from \"../../core/brief-only-command.js\";\nimport { BTW_COMMAND_USAGE, parseBtwCommand, runBtwCommand } from \"../../core/btw-command.js\";\nimport {\n\tCAVEMAN_COMMAND_USAGE,\n\tparseCavemanCommand,\n\tparseCavemanNaturalLanguage,\n\trunCavemanCommand,\n} from \"../../core/caveman-command.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport { describeContextFiles } from \"../../core/context-files.js\";\nimport type {\n\tExtensionContext,\n\tExtensionRunner,\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tExtensionWidgetOptions,\n} from \"../../core/extensions/index.js\";\nimport { FooterDataProvider, type ReadonlyFooterDataProvider } from \"../../core/footer-data-provider.js\";\nimport { initializeProjectScaffold } from \"../../core/init-project.js\";\nimport { type AppAction, KeybindingsManager } from \"../../core/keybindings.js\";\nimport type { MemoryHistorySnapshot } from \"../../core/memory.js\";\nimport { computeMemorySnapshotDiff } from \"../../core/memory-diff.js\";\nimport { MEMORY_STALE_AFTER_DAYS, reviewMemoryItems } from \"../../core/memory-review.js\";\nimport { createCompactionSummaryMessage } from \"../../core/messages.js\";\nimport { resolveModelScope } from \"../../core/model-resolver.js\";\nimport { formatProtocolStatusOutput, getProtocolWorkspaceStatus } from \"../../core/protocol-status.js\";\nimport type { ResourceDiagnostic } from \"../../core/resource-loader.js\";\nimport { type SessionContext, SessionManager } from \"../../core/session-manager.js\";\nimport { BUILTIN_SLASH_COMMANDS } from \"../../core/slash-commands.js\";\nimport {\n\tformatAdjacentDiffHeader,\n\tformatExplicitDiffHeader,\n\tformatSnapshotSelectorHistoryGuidance,\n\tformatSnapshotSelectorResolutionFailure,\n\tformatSnapshotShortId,\n} from \"../../core/snapshot-selector-formatter.js\";\nimport {\n\tparseSteerCommand,\n\trunSteerCommand,\n\tSTEER_COMMAND_ACTIVE_WORK_REQUIRED,\n\tSTEER_COMMAND_USAGE,\n} from \"../../core/steer-command.js\";\nimport { TerminalTitle } from \"../../core/terminal-title.js\";\nimport type { TruncationResult } from \"../../core/tools/truncate.js\";\nimport { searchDuckDuckGoLite } from \"../../core/tools/web-search.js\";\nimport { formatUltraplanShowOutput } from \"../../core/ultraplan.js\";\nimport { getChangelogPath, getNewEntries, parseChangelog } from \"../../utils/changelog.js\";\nimport { copyToClipboard } from \"../../utils/clipboard.js\";\nimport { extensionForImageMimeType, readClipboardImage } from \"../../utils/clipboard-image.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { ArminComponent } from \"./components/armin.js\";\nimport { AssistantMessageComponent } from \"./components/assistant-message.js\";\nimport { BashExecutionComponent } from \"./components/bash-execution.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { BranchSummaryMessageComponent } from \"./components/branch-summary-message.js\";\nimport { CompactionSummaryMessageComponent } from \"./components/compaction-summary-message.js\";\nimport { CustomEditor } from \"./components/custom-editor.js\";\nimport { CustomMessageComponent } from \"./components/custom-message.js\";\nimport { DaxnutsComponent } from \"./components/daxnuts.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport { ExtensionEditorComponent } from \"./components/extension-editor.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport { ExtensionSelectorComponent } from \"./components/extension-selector.js\";\nimport { FooterComponent } from \"./components/footer.js\";\nimport { Header } from \"./components/header.js\";\nimport { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from \"./components/keybinding-hints.js\";\nimport { LoginDialogComponent } from \"./components/login-dialog.js\";\nimport { MemoryEditorComponent } from \"./components/memory-editor.js\";\nimport { ModelSelectorComponent } from \"./components/model-selector.js\";\nimport { OAuthSelectorComponent } from \"./components/oauth-selector.js\";\nimport { ScopedModelsSelectorComponent } from \"./components/scoped-models-selector.js\";\nimport { SessionSelectorComponent } from \"./components/session-selector.js\";\nimport { SettingsSelectorComponent } from \"./components/settings-selector.js\";\nimport { buildSidebarTaskData, buildSidebarTodoData, SidebarTodoPanel } from \"./components/sidebar-todo-panel.js\";\nimport { SkillInvocationMessageComponent } from \"./components/skill-invocation-message.js\";\nimport { ToolExecutionComponent } from \"./components/tool-execution.js\";\nimport { TopBar } from \"./components/top-bar.js\";\nimport { TreeSelectorComponent } from \"./components/tree-selector.js\";\nimport { UserMessageComponent } from \"./components/user-message.js\";\nimport { UserMessageSelectorComponent } from \"./components/user-message-selector.js\";\nimport {\n\tgetAvailableThemes,\n\tgetAvailableThemesWithPaths,\n\tgetEditorTheme,\n\tgetMarkdownTheme,\n\tgetThemeByName,\n\tinitTheme,\n\tonThemeChange,\n\tsetRegisteredThemes,\n\tsetTheme,\n\tsetThemeInstance,\n\tTheme,\n\ttype ThemeColor,\n\ttheme,\n} from \"./theme/theme.js\";\n\n/** Interface for components that can be expanded/collapsed */\ninterface Expandable {\n\tsetExpanded(expanded: boolean): void;\n}\n\nfunction isExpandable(obj: unknown): obj is Expandable {\n\treturn typeof obj === \"object\" && obj !== null && \"setExpanded\" in obj && typeof obj.setExpanded === \"function\";\n}\n\ninterface PromptChromeEditor extends EditorComponent {\n\tborderColor?: (str: string) => string;\n\tsetPromptGlyph?: (promptGlyph: string) => void;\n\tsetPlaceholderText?: (placeholderText: string) => void;\n}\n\ntype CompactionQueuedMessage = {\n\ttext: string;\n\tmode: \"steer\" | \"followUp\";\n};\n\n/**\n * Options for InteractiveMode initialization.\n */\nexport interface InteractiveModeOptions {\n\t/** Providers that were migrated to auth.json (shows warning) */\n\tmigratedProviders?: string[];\n\t/** Warning message if session model couldn't be restored */\n\tmodelFallbackMessage?: string;\n\t/** Initial message to send on startup (can include @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Additional messages to send after the initial message */\n\tinitialMessages?: string[];\n\t/** Force verbose startup (overrides quietStartup setting) */\n\tverbose?: boolean;\n}\n\nexport class InteractiveMode {\n\tprivate session: AgentSession;\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate pendingMessagesContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate sidebarTodoPanel: SidebarTodoPanel;\n\tprivate sidebarTaskPanel: SidebarTodoPanel;\n\tprivate defaultEditor: CustomEditor;\n\tprivate editor: EditorComponent;\n\tprivate autocompleteProvider: CombinedAutocompleteProvider | undefined;\n\tprivate fdPath: string | undefined;\n\tprivate topBar: TopBar;\n\tprivate header: Header;\n\n\t/** Container that normally hosts the prompt/editor, but may be replaced by selectors or extension UI. */\n\tprivate editorContainer: Container;\n\tprivate promptAreaComponent: Component;\n\tprivate promptWidgetsVisible = true;\n\n\tprivate footer: FooterComponent;\n\tprivate footerDataProvider: FooterDataProvider;\n\tprivate keybindings: KeybindingsManager;\n\tprivate version: string;\n\tprivate isInitialized = false;\n\tprivate initializing: Promise<void> | null = null;\n\tprivate onInputCallback?: (text: string) => void;\n\tprivate loadingAnimation: Loader | undefined = undefined;\n\tprivate pendingWorkingMessage: string | undefined = undefined;\n\tprivate readonly defaultWorkingMessage = \"Working...\";\n\tprivate startupUiGateActive = true;\n\tprivate deferredStartupNotices: Array<() => void> = [];\n\n\tprivate lastSigintTime = 0;\n\tprivate lastEscapeTime = 0;\n\tprivate changelogMarkdown: string | undefined = undefined;\n\n\t// Status line tracking (for mutating immediately-sequential status updates)\n\tprivate lastStatusSpacer: Spacer | undefined = undefined;\n\tprivate lastStatusText: Text | undefined = undefined;\n\tprivate statusComponent: Component | undefined = undefined;\n\n\t// Streaming message tracking\n\tprivate streamingComponent: AssistantMessageComponent | undefined = undefined;\n\tprivate streamingMessage: AssistantMessage | undefined = undefined;\n\n\t// Tool execution tracking: toolCallId -> component\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\n\t// Tool output expansion state\n\tprivate toolOutputExpanded = false;\n\n\t// Thinking block visibility state\n\tprivate hideThinkingBlock = false;\n\n\t// Skill commands: command name -> skill file path\n\tprivate skillCommands = new Map<string, string>();\n\n\t// Agent subscription unsubscribe function\n\tprivate unsubscribe?: () => void;\n\tprivate windowFocusChangeUnsubscribe?: () => void;\n\tprivate focusTargetChangeUnsubscribe?: () => void;\n\n\t// Track if editor is in bash mode (text starts with !)\n\tprivate isBashMode = false;\n\n\t// Track current bash execution component\n\tprivate bashComponent: BashExecutionComponent | undefined = undefined;\n\n\t// Track pending bash components (shown in pending area, moved to chat on submit)\n\tprivate pendingBashComponents: BashExecutionComponent[] = [];\n\n\t// Terminal title with animated working indicator\n\tprivate terminalTitleController!: TerminalTitle;\n\n\t// Auto-compaction state\n\tprivate autoCompactionLoader: Loader | undefined = undefined;\n\tprivate autoCompactionEscapeHandler?: () => void;\n\n\t// Auto-retry state\n\tprivate retryLoader: Loader | undefined = undefined;\n\tprivate retryEscapeHandler?: () => void;\n\n\t// Messages queued while compaction is running\n\tprivate compactionQueuedMessages: CompactionQueuedMessage[] = [];\n\n\t// Shutdown state\n\tprivate shutdownRequested = false;\n\n\t// Epoch hardening\n\tprivate uiEpoch = 0;\n\tprivate sessionEpoch = 0;\n\tprivate isInitialMessagesRendered = false;\n\n\t// Visual region ownership\n\tprivate statusOwner: any = undefined;\n\tprivate promptOwner: any = undefined;\n\n\t// Extension UI state\n\tprivate extensionSelector: ExtensionSelectorComponent | undefined = undefined;\n\tprivate extensionInput: ExtensionInputComponent | undefined = undefined;\n\tprivate extensionEditor: ExtensionEditorComponent | undefined = undefined;\n\tprivate extensionTerminalInputUnsubscribers = new Set<() => void>();\n\n\t// Extension widgets (components rendered above/below the editor)\n\tprivate extensionWidgetsAbove = new Map<string, Component & { dispose?(): void }>();\n\tprivate extensionWidgetsBelow = new Map<string, Component & { dispose?(): void }>();\n\n\t/** The strip directly ABOVE the prompt/input area. Extensions: placement: \"aboveEditor\" */\n\tprivate widgetContainerAbove!: Container;\n\n\t/** The strip directly BELOW the prompt/input area. Extensions: placement: \"belowEditor\" */\n\tprivate widgetContainerBelow!: Container;\n\n\t// Custom footer from extension (undefined = use built-in footer)\n\tprivate customFooter: (Component & { dispose?(): void }) | undefined = undefined;\n\n\t// Header container that holds the built-in or custom header\n\tprivate headerContainer: Container;\n\n\t// Built-in header (logo + keybinding hints + changelog)\n\tprivate builtInHeader: Component | undefined = undefined;\n\n\t// Custom header from extension (undefined = use built-in header)\n\tprivate customHeader: (Component & { dispose?(): void }) | undefined = undefined;\n\n\t// Help output rendered as a removable chat block\n\tprivate helpContainer: Container | undefined = undefined;\n\n\t// Convenience accessors\n\tprivate get agent() {\n\t\treturn this.session.agent;\n\t}\n\tprivate get sessionManager() {\n\t\treturn this.session.sessionManager;\n\t}\n\tprivate get settingsManager() {\n\t\treturn this.session.settingsManager;\n\t}\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tprivate options: InteractiveModeOptions = {},\n\t) {\n\t\tthis.session = session;\n\t\tthis.version = VERSION;\n\t\tthis.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\tthis.headerContainer = new Container();\n\t\tthis.chatContainer = new Container();\n\t\tthis.pendingMessagesContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.sidebarTodoPanel = new SidebarTodoPanel({ title: \"Todos\" });\n\t\tthis.sidebarTaskPanel = new SidebarTodoPanel({ title: \"Tasks\" });\n\t\tthis.widgetContainerAbove = new Container();\n\t\tthis.widgetContainerBelow = new Container();\n\t\tthis.keybindings = KeybindingsManager.create();\n\t\tthis.topBar = new TopBar({\n\t\t\tgetModel: () =>\n\t\t\t\tthis.session.model as\n\t\t\t\t\t| {\n\t\t\t\t\t\t\tprovider?: string;\n\t\t\t\t\t\t\tapiProvider?: string;\n\t\t\t\t\t\t\tid?: string;\n\t\t\t\t\t\t\tmodelId?: string;\n\t\t\t\t\t\t\tname?: string;\n\t\t\t\t\t\t\treasoning?: boolean;\n\t\t\t\t\t  }\n\t\t\t\t\t| undefined,\n\t\t\tgetThinkingLevel: () => this.session.thinkingLevel || \"off\",\n\t\t\tkeybindings: this.keybindings,\n\t\t});\n\t\tthis.footerDataProvider = new FooterDataProvider();\n\t\tthis.header = new Header(this.session, this.footerDataProvider);\n\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\tthis.defaultEditor = new CustomEditor(\n\t\t\tthis.ui,\n\t\t\tgetEditorTheme({\n\t\t\t\tbgEnabled: this.settingsManager.getEditorBackgroundEnabled(),\n\t\t\t\tbgColor: this.settingsManager.getEditorBackgroundColor(),\n\t\t\t}),\n\t\t\tthis.keybindings,\n\t\t\t{\n\t\t\t\tpaddingX: editorPaddingX,\n\t\t\t\tautocompleteMaxVisible,\n\t\t\t\tpromptGlyph: this.getPromptGlyph(),\n\t\t\t\tplaceholderText: this.getPromptPlaceholderText(),\n\t\t\t\tpromptGlyphStyle: (str) => this.getPromptGlyphColor()(str),\n\t\t\t\tplaceholderStyle: (str) => theme.fg(this.isPromptActive() ? \"muted\" : \"dim\", str),\n\t\t\t},\n\t\t);\n\t\tthis.editor = this.defaultEditor;\n\t\tthis.editorContainer = new Container();\n\t\tthis.editorContainer.addChild(this.editor as Component);\n\t\tthis.promptAreaComponent = this.editor as Component;\n\t\tthis.footer = new FooterComponent(session, this.footerDataProvider);\n\t\tthis.footer.setAutoCompactEnabled(session.autoCompactionEnabled);\n\n\t\t// Load hide thinking block setting\n\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\n\t\t// Register themes from resource loader and initialize\n\t\tsetRegisteredThemes(this.session.resourceLoader.getThemes().themes);\n\t\tinitTheme(this.settingsManager.getTheme(), true);\n\t}\n\n\tprivate setupAutocomplete(fdPath: string | undefined): void {\n\t\t// Define commands for autocomplete\n\t\tconst slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.map((command) => ({\n\t\t\tname: command.name,\n\t\t\tdescription: command.description,\n\t\t}));\n\n\t\tconst modelCommand = slashCommands.find((command) => command.name === \"model\");\n\t\tif (modelCommand) {\n\t\t\tmodelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {\n\t\t\t\t// Get available models (scoped or from registry)\n\t\t\t\tconst models =\n\t\t\t\t\tthis.session.scopedModels.length > 0\n\t\t\t\t\t\t? this.session.scopedModels.map((s) => s.model)\n\t\t\t\t\t\t: this.session.modelRegistry.getAvailable();\n\n\t\t\t\tif (models.length === 0) return null;\n\n\t\t\t\t// Create items with provider/id format\n\t\t\t\tconst items = models.map((m) => ({\n\t\t\t\t\tid: m.id,\n\t\t\t\t\tprovider: m.provider,\n\t\t\t\t\tlabel: `${m.provider}/${m.id}`,\n\t\t\t\t}));\n\n\t\t\t\t// Fuzzy filter by model ID + provider (allows \"opus anthropic\" to match)\n\t\t\t\tconst filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn filtered.map((item) => ({\n\t\t\t\t\tvalue: item.label,\n\t\t\t\t\tlabel: item.id,\n\t\t\t\t\tdescription: item.provider,\n\t\t\t\t}));\n\t\t\t};\n\t\t}\n\n\t\t// Convert prompt templates to SlashCommand format for autocomplete\n\t\tconst templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({\n\t\t\tname: cmd.name,\n\t\t\tdescription: cmd.description,\n\t\t}));\n\n\t\t// Convert extension commands to SlashCommand format\n\t\tconst builtinCommandNames = new Set(slashCommands.map((c) => c.name));\n\t\tconst extensionCommands: SlashCommand[] = (\n\t\t\tthis.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []\n\t\t).map((cmd) => ({\n\t\t\tname: cmd.name,\n\t\t\tdescription: cmd.description ?? \"(extension command)\",\n\t\t\tgetArgumentCompletions: cmd.getArgumentCompletions,\n\t\t}));\n\n\t\t// Build skill commands from session.skills (if enabled)\n\t\tthis.skillCommands.clear();\n\t\tconst skillCommandList: SlashCommand[] = [];\n\t\tif (this.settingsManager.getEnableSkillCommands()) {\n\t\t\tfor (const skill of this.session.resourceLoader.getSkills().skills) {\n\t\t\t\tconst commandName = `skill:${skill.name}`;\n\t\t\t\tthis.skillCommands.set(commandName, skill.filePath);\n\t\t\t\tskillCommandList.push({ name: commandName, description: skill.description });\n\t\t\t}\n\t\t}\n\n\t\t// Setup autocomplete\n\t\tthis.autocompleteProvider = new CombinedAutocompleteProvider(\n\t\t\t[...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList],\n\t\t\tprocess.cwd(),\n\t\t\tfdPath,\n\t\t);\n\t\tthis.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.editor.setAutocompleteProvider?.(this.autocompleteProvider);\n\t\t}\n\t}\n\n\tprivate buildStartupInstructionsText(): string {\n\t\tconst kb = this.keybindings;\n\t\tconst hint = (action: AppAction, desc: string) => appKeyHint(kb, action, desc);\n\n\t\treturn [\n\t\t\thint(\"interrupt\", \"to interrupt\"),\n\t\t\thint(\"clear\", \"to clear\"),\n\t\t\trawKeyHint(`${appKey(kb, \"clear\")} twice`, \"to exit\"),\n\t\t\thint(\"exit\", \"to exit (empty)\"),\n\t\t\thint(\"suspend\", \"to suspend\"),\n\t\t\tkeyHint(\"deleteToLineEnd\", \"to delete to end\"),\n\t\t\thint(\"cycleThinkingLevel\", \"to cycle thinking level\"),\n\t\t\trawKeyHint(`${appKey(kb, \"cycleModelForward\")}/${appKey(kb, \"cycleModelBackward\")}`, \"to cycle models\"),\n\t\t\thint(\"selectModel\", \"to select model\"),\n\t\t\thint(\"expandTools\", \"to expand tools\"),\n\t\t\thint(\"toggleThinking\", \"to expand thinking\"),\n\t\t\thint(\"externalEditor\", \"for external editor\"),\n\t\t\thint(\"newline\", \"for new line\"),\n\t\t\trawKeyHint(\"/\", \"for commands\"),\n\t\t\trawKeyHint(\"!\", \"to run bash\"),\n\t\t\trawKeyHint(\"!!\", \"to run bash (no context)\"),\n\t\t\thint(\"followUp\", \"to queue follow-up\"),\n\t\t\thint(\"dequeue\", \"to edit all queued messages\"),\n\t\t\thint(\"pasteImage\", \"to paste image\"),\n\t\t\trawKeyHint(\"drop files\", \"to attach\"),\n\t\t].join(\"\\n\");\n\t}\n\n\tprivate buildHelpReferenceText(): string {\n\t\tconst commands = [\n\t\t\t[\"/settings\", \"/model\", \"/scoped-models\", \"/hotkeys\"],\n\t\t\t[\"/new\", \"/clear\", \"/resume\", \"/session\", \"/name\"],\n\t\t\t[\"/tree\", \"/fork\", \"/compact\", \"/reload\", \"/init-project\"],\n\t\t\t[\"/protocol-status\", \"/copy\", \"/export\", \"/share\", \"/login\", \"/logout\"],\n\t\t\t[\"/memory\", \"/caveman\", \"/brief\", \"/btw\", \"/steer\", \"/websearch\", \"/help\"],\n\t\t\t[\"/ultraplan\"],\n\t\t]\n\t\t\t.map((line) => line.map((cmd) => theme.fg(\"accent\", cmd)).join(theme.fg(\"dim\", \", \")))\n\t\t\t.join(\"\\n\");\n\n\t\tconst resources = [\n\t\t\t[\"Context files\", `${describeContextFiles()} from global + parent directories`],\n\t\t\t[\"Prompts\", \"/name-style prompt templates from prompts directories\"],\n\t\t\t[\"Skills\", \"/skill:name commands and automatic skill discovery\"],\n\t\t\t[\"Extensions\", \"custom tools, commands, UI, hooks, and provider integrations\"],\n\t\t\t[\"Themes\", \"built-in and custom themes with live reload\"],\n\t\t]\n\t\t\t.map(([label, desc]) => `${theme.fg(\"accent\", label)}${theme.fg(\"dim\", \": \")}${theme.fg(\"muted\", desc)}`)\n\t\t\t.join(\"\\n\");\n\n\t\treturn [\n\t\t\ttheme.bold(theme.fg(\"mdHeading\", \"Operational Reference\")),\n\t\t\t\"\",\n\t\t\t`${theme.fg(\"accent\", \"Built-in tools:\")} ${theme.fg(\"muted\", \"read, bash, edit, write, grep, find, ls\")}`,\n\t\t\t`${theme.fg(\"accent\", \"Sessions:\")} ${theme.fg(\"muted\", \"auto-saved JSONL sessions with resume, branching, tree navigation, and compaction\")}`,\n\t\t\t`${theme.fg(\"accent\", \"Modes:\")} ${theme.fg(\"muted\", \"interactive TUI, print/JSON output, RPC, and SDK embedding\")}`,\n\t\t\t`${theme.fg(\"accent\", \"Model access:\")} ${theme.fg(\"muted\", \"built-in providers, OAuth logins, API keys, scoped model cycling\")}`,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttheme.bold(theme.fg(\"mdHeading\", \"Slash Commands\")),\n\t\t\t\"\",\n\t\t\tcommands,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttheme.bold(theme.fg(\"mdHeading\", \"Customization\")),\n\t\t\t\"\",\n\t\t\tresources,\n\t\t\t\"\",\n\t\t\t`${theme.fg(\"accent\", \"Message input:\")} ${theme.fg(\"muted\", \"@file references, pasted images, drag-and-drop attachments\")}`,\n\t\t\t`${theme.fg(\"accent\", \"Shell input:\")} ${theme.fg(\"muted\", \"!cmd includes output in context, !!cmd runs without adding context\")}`,\n\t\t].join(\"\\n\");\n\t}\n\n\tprivate clearHelpContainer(): void {\n\t\tif (!this.helpContainer) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.chatContainer.children.includes(this.helpContainer)) {\n\t\t\tthis.chatContainer.removeChild(this.helpContainer);\n\t\t}\n\n\t\tthis.helpContainer = undefined;\n\t}\n\n\tprivate handleHelpCommand(): void {\n\t\tthis.clearHelpContainer();\n\n\t\tconst logo = theme.bold(theme.fg(\"accent\", APP_NAME)) + theme.fg(\"dim\", ` v${this.version}`);\n\t\tconst runner = this.session.extensionRunner;\n\t\tconst helpContainer = new Container();\n\n\t\thelpContainer.addChild(new Spacer(1));\n\t\thelpContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Help\")), 1, 0));\n\t\thelpContainer.addChild(new Spacer(1));\n\t\thelpContainer.addChild(new Text(`${logo}\\n${this.buildStartupInstructionsText()}`, 1, 0));\n\t\thelpContainer.addChild(new Spacer(1));\n\t\thelpContainer.addChild(new Text(this.buildHelpReferenceText(), 1, 0));\n\n\t\tthis.showLoadedResources({\n\t\t\textensionPaths: runner?.getExtensionPaths() ?? [],\n\t\t\tforce: true,\n\t\t\tshowDiagnosticsWhenQuiet: true,\n\t\t\ttargetContainer: helpContainer,\n\t\t});\n\n\t\tthis.helpContainer = helpContainer;\n\t\tthis.chatContainer.addChild(helpContainer);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate formatMemoryBlock(title: string, lines: string[]): string {\n\t\tconst body = lines.length > 0 ? lines.join(\"\\n\") : theme.fg(\"dim\", \"(none)\");\n\t\treturn `${theme.bold(title)}\\n\\n${body}`;\n\t}\n\n\t/**\n\t * Format a date as a relative age label (e.g., \"2 hours ago\", \"3 days ago\").\n\t */\n\tprivate getRelativeAgeLabel(date: Date): string {\n\t\tconst now = Date.now();\n\t\tconst diffMs = now - date.getTime();\n\t\tconst diffSecs = Math.floor(diffMs / 1000);\n\t\tconst diffMins = Math.floor(diffSecs / 60);\n\t\tconst diffHours = Math.floor(diffMins / 60);\n\t\tconst diffDays = Math.floor(diffHours / 24);\n\n\t\tif (diffSecs < 60) return \"just now\";\n\t\tif (diffMins < 60) return `${diffMins}m ago`;\n\t\tif (diffHours < 24) return `${diffHours}h ago`;\n\t\tif (diffDays < 30) return `${diffDays}d ago`;\n\t\tconst diffMonths = Math.floor(diffDays / 30);\n\t\tif (diffMonths < 12) return `${diffMonths}mo ago`;\n\t\tconst diffYears = Math.floor(diffMonths / 12);\n\t\treturn `${diffYears}y ago`;\n\t}\n\n\tprivate updateSidebarTodoPanel(): void {\n\t\tconst data = buildSidebarTodoData(this.session.getTodos());\n\t\tif (!data) {\n\t\t\tthis.sidebarTodoPanel.clear();\n\t\t\treturn;\n\t\t}\n\t\tthis.sidebarTodoPanel.update(data);\n\t}\n\n\tprivate updateSidebarTaskPanel(): void {\n\t\tconst data = buildSidebarTaskData(this.session.getTasks());\n\t\tif (!data) {\n\t\t\tthis.sidebarTaskPanel.clear();\n\t\t\treturn;\n\t\t}\n\t\tthis.sidebarTaskPanel.update(data);\n\t}\n\n\tprivate handleBriefCommand(text: string): void {\n\t\tconst action = parseBriefOnlyCommand(text);\n\t\tif (!action) {\n\t\t\tthis.showWarning(BRIEF_ONLY_COMMAND_USAGE);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(runBriefOnlyCommand(this.session, action), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleCavemanCommand(text: string): void {\n\t\tconst action = parseCavemanCommand(text);\n\t\tif (!action) {\n\t\t\tthis.showWarning(CAVEMAN_COMMAND_USAGE);\n\t\t\treturn;\n\t\t}\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(runCavemanCommand(this.session, action), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleBtwCommand(text: string): void {\n\t\tconst note = parseBtwCommand(text);\n\t\tif (!note) {\n\t\t\tthis.showWarning(BTW_COMMAND_USAGE);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(runBtwCommand(this.session, note), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleSteerCommand(text: string): Promise<void> {\n\t\tconst message = parseSteerCommand(text);\n\t\tif (!message) {\n\t\t\tthis.showWarning(STEER_COMMAND_USAGE);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst output = await runSteerCommand(\n\t\t\t\t{\n\t\t\t\t\tisStreaming: this.session.isStreaming,\n\t\t\t\t\tstate: this.session.state,\n\t\t\t\t\tsteer: (steerMessage) => this.session.steer(steerMessage),\n\t\t\t\t\tcontinueCurrentWork: () => this.session.agent.continue(),\n\t\t\t\t\tgetSteeringMessages: () => this.session.getSteeringMessages(),\n\t\t\t\t},\n\t\t\t\tmessage,\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(output, 1, 0));\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error) {\n\t\t\tconst warningMessage = error instanceof Error ? error.message : STEER_COMMAND_ACTIVE_WORK_REQUIRED;\n\t\t\tthis.showWarning(warningMessage || STEER_COMMAND_ACTIVE_WORK_REQUIRED);\n\t\t}\n\t}\n\n\tprivate handleMemoryCommand(text: string): void {\n\t\tconst parts = text.trim().split(/\\s+/);\n\t\tconst subcommand = parts[1]?.toLowerCase();\n\t\tconst memoryItems = this.session.getMemoryItems();\n\n\t\t// `/memory` (no args) or `/memory show`: open the interactive editor UI\n\t\tif (!subcommand || subcommand === \"show\") {\n\t\t\tthis.showMemoryEditor();\n\t\t\treturn;\n\t\t}\n\n\t\t// `/memory list`: textual list (preserves existing operator workflow)\n\t\tif (subcommand === \"list\") {\n\t\t\tconst reviewed = reviewMemoryItems([...memoryItems]);\n\t\t\tconst lines = reviewed.map((entry) => {\n\t\t\t\tconst prefix = entry.reviewRecommended ? theme.fg(\"warning\", \"⚠\") : theme.fg(\"success\", \"✓\");\n\t\t\t\tconst reviewHint = entry.reviewRecommended ? ` ${theme.fg(\"warning\", \"(review recommended)\")}` : \"\";\n\t\t\t\treturn `${prefix} ${entry.item.key}: ${entry.item.value} ${theme.fg(\"dim\", `· ${entry.label}`)}${reviewHint}`;\n\t\t\t});\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Session Memory\", lines), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (subcommand === \"review\") {\n\t\t\tconst reviewed = reviewMemoryItems([...memoryItems]);\n\t\t\tconst flagged = reviewed.filter((entry) => entry.reviewRecommended);\n\t\t\tconst lines = reviewed.flatMap((entry) => {\n\t\t\t\tconst status = entry.reviewRecommended ? theme.fg(\"warning\", \"review\") : theme.fg(\"dim\", \"ok\");\n\t\t\t\tconst output = [`- ${entry.item.key} ${theme.fg(\"dim\", `(${entry.label})`)} · ${status}`];\n\t\t\t\tif (entry.note) {\n\t\t\t\t\toutput.push(`  ${theme.fg(\"warning\", entry.note)}`);\n\t\t\t\t}\n\t\t\t\treturn output;\n\t\t\t});\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\n\t\t\t\t\t\"dim\",\n\t\t\t\t\tflagged.length > 0\n\t\t\t\t\t\t? `Freshness is heuristic. ${flagged.length} item(s) are older than ${MEMORY_STALE_AFTER_DAYS} days; verify before relying on them.`\n\t\t\t\t\t\t: `Freshness is heuristic. No items currently exceed the ${MEMORY_STALE_AFTER_DAYS}-day review threshold.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory Review\", lines), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (subcommand === \"history\") {\n\t\t\tconst snapshots = this.session.getMemoryHistory();\n\t\t\tif (snapshots.length === 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\tthis.formatMemoryBlock(\"Memory History\", [\n\t\t\t\t\t\t\ttheme.fg(\"dim\", \"No memory snapshots found in current branch.\"),\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst lines: string[] = [];\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\n\t\t\t\t\t\"dim\",\n\t\t\t\t\t`Branch history · ${snapshots.length} snapshot${snapshots.length === 1 ? \"\" : \"s\"} · oldest first`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\"dim\", \"Each snapshot is a complete memory state at that point (not individual changes).\"),\n\t\t\t);\n\t\t\tlines.push(\"\");\n\t\t\tfor (const guidanceLine of formatSnapshotSelectorHistoryGuidance()) {\n\t\t\t\tlines.push(theme.fg(\"dim\", guidanceLine));\n\t\t\t}\n\t\t\tlines.push(\"\");\n\n\t\t\t// Show newest first for better UX (current state most relevant)\n\t\t\tfor (let i = snapshots.length - 1; i >= 0; i--) {\n\t\t\t\tconst snapshot = snapshots[i]!;\n\t\t\t\tconst date = new Date(snapshot.recordedAt);\n\t\t\t\tconst ageLabel = this.getRelativeAgeLabel(date);\n\t\t\t\tconst currentMarker = snapshot.isCurrent ? ` ${theme.fg(\"accent\", \"[current]\")}` : \"\";\n\t\t\t\tconst itemCount = snapshot.items.length;\n\t\t\t\tconst itemLabel = itemCount === 1 ? \"item\" : \"items\";\n\t\t\t\t// Show short ID for copy/paste use in /memory diff <baselineId> <targetId>\n\t\t\t\tconst shortId = theme.fg(\"muted\", formatSnapshotShortId(snapshot.entryId));\n\n\t\t\t\tlines.push(\n\t\t\t\t\t`${theme.bold(theme.fg(\"text\", ageLabel))}${currentMarker} · ${itemCount} ${itemLabel} · ${theme.fg(\"dim\", date.toLocaleString())} · ${shortId}`,\n\t\t\t\t);\n\n\t\t\t\t// Show item summary for each snapshot (key + first 30 chars of value)\n\t\t\t\tif (snapshot.items.length > 0) {\n\t\t\t\t\tfor (const item of snapshot.items.slice(0, 5)) {\n\t\t\t\t\t\tconst valuePreview = item.value.length > 30 ? `${item.value.slice(0, 30)}…` : item.value;\n\t\t\t\t\t\tlines.push(`  ${theme.fg(\"muted\", `${item.key}:`)} ${valuePreview}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (snapshot.items.length > 5) {\n\t\t\t\t\t\tlines.push(`  ${theme.fg(\"dim\", `… and ${snapshot.items.length - 5} more`)}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlines.push(`  ${theme.fg(\"dim\", \"(empty)\")}`);\n\t\t\t\t}\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory History\", lines), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// `/memory diff` and `/memory history diff`: diff current vs previous snapshot, or explicit IDs\n\t\tif (subcommand === \"diff\" || subcommand === \"history_diff\") {\n\t\t\tconst snapshots = this.session.getMemoryHistory();\n\t\t\tconst explicitBaselineId = parts[2];\n\t\t\tconst explicitTargetId = parts[3];\n\n\t\t\tif (snapshots.length === 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\tthis.formatMemoryBlock(\"Memory Diff\", [\n\t\t\t\t\t\t\ttheme.fg(\"dim\", \"No memory snapshots found. Nothing to diff against.\"),\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Determine which two snapshots to compare\n\t\t\tlet baselineSnapshot: MemoryHistorySnapshot | undefined;\n\t\t\tlet targetSnapshot: MemoryHistorySnapshot | undefined;\n\t\t\tlet isExplicitIds = false;\n\n\t\t\tif (explicitBaselineId && explicitTargetId) {\n\t\t\t\t// Explicit ID comparison: /memory diff <baselineId> <targetId>\n\t\t\t\t// Uses strict resolution: exact full ID, exact short ID, or strict unique prefix.\n\t\t\t\t// Supports brackets copied from /memory history output: [abcd1234]\n\t\t\t\tisExplicitIds = true;\n\n\t\t\t\tconst baselineResult = this.session.resolveMemorySnapshotSelector(explicitBaselineId);\n\t\t\t\tconst targetResult = this.session.resolveMemorySnapshotSelector(explicitTargetId);\n\n\t\t\t\tif (baselineResult.error !== undefined || targetResult.error !== undefined) {\n\t\t\t\t\tconst errorLines = formatSnapshotSelectorResolutionFailure([\n\t\t\t\t\t\t{ label: \"Baseline\", resolution: baselineResult },\n\t\t\t\t\t\t{ label: \"Target\", resolution: targetResult },\n\t\t\t\t\t]);\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory Diff\", errorLines), 1, 0));\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tbaselineSnapshot = baselineResult.snapshot;\n\t\t\t\ttargetSnapshot = targetResult.snapshot;\n\t\t\t} else {\n\t\t\t\t// Adjacent diff default: current vs previous\n\t\t\t\ttargetSnapshot = snapshots[snapshots.length - 1];\n\t\t\t\tbaselineSnapshot = snapshots.length >= 2 ? snapshots[snapshots.length - 2] : undefined;\n\n\t\t\t\tif (!baselineSnapshot) {\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\tnew Text(\n\t\t\t\t\t\t\tthis.formatMemoryBlock(\"Memory Diff\", [\n\t\t\t\t\t\t\t\t`${theme.fg(\"accent\", \"Initial snapshot\")} — ${targetSnapshot!.items.length} item(s)`,\n\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\ttheme.fg(\"dim\", `Recorded: ${new Date(targetSnapshot!.recordedAt).toLocaleString()}`),\n\t\t\t\t\t\t\t\ttheme.fg(\"dim\", \"(snapshot comparison · not an event log)\"),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Compute diff using the shared path\n\t\t\t// Non-null asserted: both branches above return early if either snapshot is undefined\n\t\t\tconst diff = computeMemorySnapshotDiff(baselineSnapshot!, targetSnapshot!);\n\n\t\t\tconst lines: string[] = [];\n\n\t\t\tif (isExplicitIds) {\n\t\t\t\tlines.push(\n\t\t\t\t\t...formatExplicitDiffHeader({\n\t\t\t\t\t\tbaselineSnapshot: baselineSnapshot!,\n\t\t\t\t\t\ttargetSnapshot: targetSnapshot!,\n\t\t\t\t\t\tgetRelativeAgeLabel: (date) => this.getRelativeAgeLabel(date),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(\n\t\t\t\t\t...formatAdjacentDiffHeader({\n\t\t\t\t\t\tbaselineSnapshot: baselineSnapshot!,\n\t\t\t\t\t\ttargetSnapshot: targetSnapshot!,\n\t\t\t\t\t\tgetRelativeAgeLabel: (date) => this.getRelativeAgeLabel(date),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Handle same-snapshot comparison honestly\n\t\t\tif (baselineSnapshot!.entryId === targetSnapshot!.entryId) {\n\t\t\t\tlines.push(theme.fg(\"dim\", \"Baseline and target are the same snapshot — no changes to show.\"));\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory Diff\", lines), 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (diff.added.length > 0) {\n\t\t\t\tlines.push(theme.bold(theme.fg(\"success\", `+ Added (${diff.added.length})`)));\n\t\t\t\tfor (const item of diff.added) {\n\t\t\t\t\tconst preview = item.value.length > 50 ? `${item.value.slice(0, 50)}…` : item.value;\n\t\t\t\t\tlines.push(`  ${theme.fg(\"success\", \"+\")} ${theme.fg(\"accent\", item.key)}: ${preview}`);\n\t\t\t\t}\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\n\t\t\tif (diff.removed.length > 0) {\n\t\t\t\tlines.push(theme.bold(theme.fg(\"error\", `- Removed (${diff.removed.length})`)));\n\t\t\t\tfor (const item of diff.removed) {\n\t\t\t\t\tconst preview = item.value.length > 50 ? `${item.value.slice(0, 50)}…` : item.value;\n\t\t\t\t\tlines.push(`  ${theme.fg(\"error\", \"-\")} ${theme.fg(\"accent\", item.key)}: ${preview}`);\n\t\t\t\t}\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\n\t\t\tif (diff.changed.length > 0) {\n\t\t\t\tlines.push(theme.bold(theme.fg(\"warning\", `~ Changed (${diff.changed.length})`)));\n\t\t\t\tfor (const item of diff.changed) {\n\t\t\t\t\tconst prevPreview =\n\t\t\t\t\t\titem.previousValue.length > 40 ? `${item.previousValue.slice(0, 40)}…` : item.previousValue;\n\t\t\t\t\tconst currPreview =\n\t\t\t\t\t\titem.currentValue.length > 40 ? `${item.currentValue.slice(0, 40)}…` : item.currentValue;\n\t\t\t\t\tlines.push(`  ${theme.fg(\"warning\", \"~\")} ${theme.fg(\"accent\", item.key)}:`);\n\t\t\t\t\tlines.push(`    ${theme.fg(\"dim\", prevPreview)}`);\n\t\t\t\t\tlines.push(`    ${theme.fg(\"text\", currPreview)}`);\n\t\t\t\t}\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\n\t\t\tif (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {\n\t\t\t\tlines.push(theme.fg(\"dim\", \"No changes between snapshots.\"));\n\t\t\t}\n\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory Diff\", lines), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (subcommand === \"set\") {\n\t\t\tconst key = parts[2];\n\t\t\tconst value = text.trim().split(/\\s+/, 4)[3];\n\t\t\tif (!key || !value) {\n\t\t\t\tthis.showWarning(\"Usage: /memory set <key> <value>\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst items = this.session.setMemoryItem(key, value);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\tthis.formatMemoryBlock(\"Memory Updated\", [\n\t\t\t\t\t\t`${theme.fg(\"success\", \"✓\")} Stored ${key}`,\n\t\t\t\t\t\t`${theme.fg(\"dim\", \"Active items:\")} ${String(items.length)}`,\n\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.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (subcommand === \"get\") {\n\t\t\tconst key = parts[2];\n\t\t\tif (!key) {\n\t\t\t\tthis.showWarning(\"Usage: /memory get <key>\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst item = this.session.getMemoryItem(key);\n\t\t\tif (!item) {\n\t\t\t\tthis.showWarning(`No memory item found for key: ${key}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst review = reviewMemoryItems([item])[0];\n\t\t\tconst lines = [\n\t\t\t\t`${theme.fg(\"accent\", \"Key:\")} ${item.key}`,\n\t\t\t\t`${theme.fg(\"accent\", \"Value:\")} ${item.value}`,\n\t\t\t\t`${theme.fg(\"accent\", \"Recorded:\")} ${review?.label ?? \"unknown age\"}`,\n\t\t\t];\n\t\t\tif (review?.note) {\n\t\t\t\tlines.push(theme.fg(\"warning\", review.note));\n\t\t\t}\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(this.formatMemoryBlock(\"Memory Item\", lines), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (subcommand === \"clear\") {\n\t\t\tconst key = parts[2];\n\t\t\tif (key) {\n\t\t\t\tconst before = this.session.getMemoryItems().length;\n\t\t\t\tconst afterItems = this.session.deleteMemoryItem(key);\n\t\t\t\tif (afterItems.length === before) {\n\t\t\t\t\tthis.showWarning(`No memory item found for key: ${key}`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(\n\t\t\t\t\t\tthis.formatMemoryBlock(\"Memory Updated\", [\n\t\t\t\t\t\t\t`${theme.fg(\"success\", \"✓\")} Removed ${key}`,\n\t\t\t\t\t\t\t`${theme.fg(\"dim\", \"Remaining items:\")} ${String(afterItems.length)}`,\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.session.clearMemory();\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\tthis.formatMemoryBlock(\"Memory Cleared\", [`${theme.fg(\"success\", \"✓\")} Session memory is now empty`]),\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.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showWarning(\n\t\t\t\"Usage: /memory [list|show|get <key>|set <key> <value>|clear [key]|review|history|diff [baselineId targetId]]\",\n\t\t);\n\t}\n\n\tprivate async handleInitProjectCommand(includeProtocol = false): Promise<void> {\n\t\tconst result = initializeProjectScaffold(process.cwd(), { includeProtocol });\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(result.output, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleProtocolStatusCommand(): void {\n\t\tconst output = formatProtocolStatusOutput(\n\t\t\tgetProtocolWorkspaceStatus({\n\t\t\t\tcwd: process.cwd(),\n\t\t\t\tresourceLoader: this.session.resourceLoader,\n\t\t\t}),\n\t\t);\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(output, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleWebsearchCommand(text: string): Promise<void> {\n\t\tconst query = text.slice(\"/websearch\".length).trim();\n\t\tif (!query) {\n\t\t\tthis.showWarning(\"Usage: /websearch <query>\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst results = await searchDuckDuckGoLite(query);\n\t\t\tif (results.length === 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(`No web results found for \"${query}\".`, 1, 0));\n\t\t\t} else {\n\t\t\t\tconst lines = [`${theme.bold(theme.fg(\"accent\", `Web Search: ${query}`))}`, \"\"];\n\t\t\t\tfor (const [index, result] of results.entries()) {\n\t\t\t\t\tlines.push(`${index + 1}. ${theme.fg(\"accent\", result.title)}`);\n\t\t\t\t\tlines.push(`   ${result.url}`);\n\t\t\t\t\tif (result.snippet) {\n\t\t\t\t\t\tlines.push(`   ${theme.fg(\"muted\", result.snippet)}`);\n\t\t\t\t\t}\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(lines.join(\"\\n\"), 1, 0));\n\t\t\t}\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : \"Search failed\");\n\t\t}\n\t}\n\n\tprivate async handleUltraplanCommand(text: string): Promise<void> {\n\t\tconst args = text.slice(\"/ultraplan\".length).trim();\n\t\tif (!args) {\n\t\t\tthis.showWarning(\n\t\t\t\t\"Usage: /ultraplan <objective> | /ultraplan show | /ultraplan apply | /ultraplan revise <instruction> | /ultraplan regenerate <instruction>\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (args === \"show\") {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(formatUltraplanShowOutput(this.session.getLatestUltraplanPlan()), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tif (args === \"apply\") {\n\t\t\ttry {\n\t\t\t\tconst result = this.session.applyUltraplan();\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(result.displayText, 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (args.startsWith(\"revise \") || args === \"revise\") {\n\t\t\tconst instruction = args.slice(\"revise\".length).trim();\n\t\t\tif (!instruction) {\n\t\t\t\tthis.showWarning(\n\t\t\t\t\t\"Usage: /ultraplan <objective> | /ultraplan show | /ultraplan apply | /ultraplan revise <instruction> | /ultraplan regenerate <instruction>\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.session.runUltraplanRevise(instruction);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(result.displayText, 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (args.startsWith(\"regenerate \") || args === \"regenerate\") {\n\t\t\tconst instruction = args.slice(\"regenerate\".length).trim();\n\t\t\tif (!instruction) {\n\t\t\t\tthis.showWarning(\n\t\t\t\t\t\"Usage: /ultraplan <objective> | /ultraplan show | /ultraplan apply | /ultraplan revise <instruction> | /ultraplan regenerate <instruction>\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.session.runUltraplanRevise(instruction);\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(result.displayText, 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t} catch (error) {\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.session.runUltraplan(args);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(result.displayText, 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate showMemoryEditor(): void {\n\t\tthis.showSelector((done) => {\n\t\t\tconst callbacks = {\n\t\t\t\tgetMemoryItems: () => this.session.getMemoryItems(),\n\t\t\t\tgetMemoryHistory: () => this.session.getMemoryHistory(),\n\t\t\t\tsetMemoryItem: (key: string, value: string) => this.session.setMemoryItem(key, value),\n\t\t\t\tdeleteMemoryItem: (key: string) => this.session.deleteMemoryItem(key),\n\t\t\t\tclearMemory: () => this.session.clearMemory(),\n\t\t\t\trequestRender: () => this.ui.requestRender(),\n\t\t\t};\n\n\t\t\tconst editor = new MemoryEditorComponent(callbacks, () => this.ui.requestRender());\n\n\t\t\t// Proxy component that intercepts Escape to call done()\n\t\t\tconst kb = getEditorKeybindings();\n\t\t\tconst proxy = new (class extends Container {\n\t\t\t\thandleInput(keyData: string): void {\n\t\t\t\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\teditor.handleInput(keyData);\n\t\t\t\t}\n\t\t\t})();\n\t\t\tproxy.addChild(editor);\n\n\t\t\treturn { component: proxy, focus: proxy };\n\t\t});\n\t}\n\n\tprivate resetInteractiveSessionUI(renderInitialMessages = false): void {\n\t\t// Invalidate all stale async callbacks\n\t\tthis.uiEpoch++;\n\t\tthis.sessionEpoch++;\n\t\tthis.isInitialMessagesRendered = false;\n\n\t\t// Stop all loaders\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.dispose();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tif (this.autoCompactionLoader) {\n\t\t\tthis.autoCompactionLoader.dispose();\n\t\t\tthis.autoCompactionLoader = undefined;\n\t\t}\n\t\tif (this.retryLoader) {\n\t\t\tthis.retryLoader.dispose();\n\t\t\tthis.retryLoader = undefined;\n\t\t}\n\n\t\t// Clean up any transient prompt UI\n\t\tif (this.extensionSelector) {\n\t\t\tthis.extensionSelector.dispose();\n\t\t\tthis.extensionSelector = undefined;\n\t\t}\n\t\tif (this.extensionInput) {\n\t\t\tthis.extensionInput.dispose();\n\t\t\tthis.extensionInput = undefined;\n\t\t}\n\t\tif (this.extensionEditor) {\n\t\t\tthis.extensionEditor = undefined;\n\t\t}\n\t\tthis.ui.hideOverlay();\n\n\t\tthis.clearChatContainer();\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t// Force empty the status container directly in case there are other elements left behind\n\t\tthis.statusContainer.clear();\n\t\tthis.statusComponent = undefined;\n\n\t\tthis.restoreCanonicalEditor({ requestRender: false, text: \"\" });\n\n\t\tthis.compactionQueuedMessages = [];\n\t\tthis.streamingComponent = undefined;\n\t\tthis.streamingMessage = undefined;\n\t\tthis.pendingTools.clear();\n\t\tthis.pendingBashComponents = [];\n\t\tthis.bashComponent = undefined;\n\t\tthis.pendingWorkingMessage = undefined;\n\t\tthis.isBashMode = false;\n\n\t\t// Ensure clean widget state\n\t\tthis.updateSidebarTodoPanel();\n\t\tthis.updateSidebarTaskPanel();\n\t\tthis.renderWidgets(false);\n\t\tthis.updatePromptChrome();\n\n\t\tif (renderInitialMessages) {\n\t\t\tthis.renderInitialMessages();\n\t\t}\n\t\tthis.ui.invalidate();\n\t}\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\t\tif (this.initializing) return this.initializing;\n\n\t\tthis.initializing = (async () => {\n\t\t\ttry {\n\t\t\t\tif (this.isInitialized) return;\n\n\t\t\t\t// Load changelog (only show new entries, skip for resumed sessions)\n\t\t\t\tthis.changelogMarkdown = this.getChangelogForDisplay();\n\n\t\t\t\t// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)\n\t\t\t\t// Both are needed: fd for autocomplete, rg for grep tool and bash commands\n\t\t\t\tconst [fdPath] = await Promise.all([ensureTool(\"fd\"), ensureTool(\"rg\")]);\n\t\t\t\tthis.fdPath = fdPath;\n\n\t\t\t\t// Add containers only if they're not already children\n\t\t\t\tconst addIfMissing = (parent: Container | TUI, child: Component) => {\n\t\t\t\t\tif (!parent.children.includes(child)) {\n\t\t\t\t\t\tparent.addChild(child);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Add header container as first child\n\t\t\t\taddIfMissing(this.ui, this.headerContainer);\n\n\t\t\t\t// Always show the branded header/logo, even when quietStartup is enabled\n\t\t\t\taddIfMissing(this.headerContainer, this.header);\n\t\t\t\taddIfMissing(this.headerContainer, new Spacer(1));\n\n\t\t\t\t// Add startup instructions only when not silenced\n\t\t\t\tif (this.options.verbose || !this.settingsManager.getQuietStartup()) {\n\t\t\t\t\tconst instructions = this.buildStartupInstructionsText();\n\t\t\t\t\tthis.builtInHeader = new Text(instructions, 1, 0);\n\t\t\t\t\taddIfMissing(this.headerContainer, this.builtInHeader);\n\t\t\t\t\taddIfMissing(this.headerContainer, new Spacer(1));\n\n\t\t\t\t\t// Add changelog if provided\n\t\t\t\t\tif (this.changelogMarkdown) {\n\t\t\t\t\t\taddIfMissing(this.headerContainer, new DynamicBorder());\n\t\t\t\t\t\tif (this.settingsManager.getCollapseChangelog()) {\n\t\t\t\t\t\t\tconst versionMatch = this.changelogMarkdown.match(/##\\s+\\[?(\\d+\\.\\d+\\.\\d+)\\]?/);\n\t\t\t\t\t\t\tconst latestVersion = versionMatch ? versionMatch[1] : this.version;\n\t\t\t\t\t\t\tconst condensedText = `Updated to v${latestVersion}. Use ${theme.bold(\"/changelog\")} to view full changelog.`;\n\t\t\t\t\t\t\taddIfMissing(this.headerContainer, new Text(condensedText, 1, 0));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taddIfMissing(this.headerContainer, new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\t\t\t\t\t\taddIfMissing(this.headerContainer, new Spacer(1));\n\t\t\t\t\t\t\taddIfMissing(\n\t\t\t\t\t\t\t\tthis.headerContainer,\n\t\t\t\t\t\t\t\tnew Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\taddIfMissing(this.headerContainer, new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\taddIfMissing(this.headerContainer, new DynamicBorder());\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Quiet startup: keep the logo visible, but suppress instruction text\n\t\t\t\t\tthis.builtInHeader = new Text(\"\", 0, 0);\n\t\t\t\t\taddIfMissing(this.headerContainer, this.builtInHeader);\n\n\t\t\t\t\tif (this.changelogMarkdown) {\n\t\t\t\t\t\taddIfMissing(this.headerContainer, new Spacer(1));\n\t\t\t\t\t\tconst versionMatch = this.changelogMarkdown.match(/##\\s+\\[?(\\d+\\.\\d+\\.\\d+)\\]?/);\n\t\t\t\t\t\tconst latestVersion = versionMatch ? versionMatch[1] : this.version;\n\t\t\t\t\t\tconst condensedText = `Updated to v${latestVersion}. Use ${theme.bold(\"/changelog\")} to view full changelog.`;\n\t\t\t\t\t\taddIfMissing(this.headerContainer, new Text(condensedText, 1, 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\taddIfMissing(this.ui, this.chatContainer);\n\t\t\t\taddIfMissing(this.ui, this.pendingMessagesContainer);\n\t\t\t\tmountOperatorStack(this.ui, this.sidebarTodoPanel, this.sidebarTaskPanel);\n\t\t\t\tthis.updateSidebarTodoPanel();\n\t\t\t\tthis.updateSidebarTaskPanel();\n\t\t\t\taddIfMissing(this.ui, this.statusContainer);\n\t\t\t\tthis.renderWidgets(); // Initialize with default spacer\n\n\t\t\t\t// The prompt area consists of an optional widget strip, the editor, and a bottom widget strip.\n\t\t\t\t// Extensions should use ctx.ui.setWidget(..., { placement: \"aboveEditor\" }) for the top strip.\n\t\t\t\taddIfMissing(this.ui, this.widgetContainerAbove);\n\t\t\t\taddIfMissing(this.ui, this.editorContainer);\n\t\t\t\taddIfMissing(this.ui, this.widgetContainerBelow);\n\n\t\t\t\taddIfMissing(this.ui, this.footer);\n\t\t\t\tthis.ui.setFocus(this.editor);\n\t\t\t\tthis.updatePromptChrome();\n\n\t\t\t\tthis.setupKeyHandlers();\n\t\t\t\tthis.setupEditorSubmitHandler();\n\n\t\t\t\t// Initialize extensions first so resources are shown before messages\n\t\t\t\tawait this.initExtensions();\n\n\t\t\t\t// Render initial messages AFTER showing loaded resources\n\t\t\t\tthis.renderInitialMessages();\n\n\t\t\t\t// Start the UI\n\t\t\t\tthis.ui.start();\n\t\t\t\tthis.isInitialized = true;\n\n\t\t\t\t// Set terminal title\n\t\t\t\tthis.updateTerminalTitle();\n\n\t\t\t\t// Subscribe to agent events\n\t\t\t\tthis.subscribeToAgent();\n\t\t\t\tthis.windowFocusChangeUnsubscribe = this.ui.onWindowFocusChange(() => {\n\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t});\n\t\t\t\tthis.focusTargetChangeUnsubscribe = this.ui.onFocusTargetChange(() => {\n\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t});\n\n\t\t\t\t// Set up theme file watcher\n\t\t\t\tonThemeChange(() => {\n\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t});\n\n\t\t\t\t// Set up git branch watcher (uses provider instead of footer)\n\t\t\t\tthis.footerDataProvider.onBranchChange(() => {\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t});\n\n\t\t\t\t// Initialize available provider count for footer display\n\t\t\t\tawait this.updateAvailableProviderCount();\n\t\t\t} finally {\n\t\t\t\tthis.initializing = null;\n\t\t\t}\n\t\t})();\n\n\t\treturn this.initializing;\n\t}\n\n\t/**\n\t * Update terminal title with session name and cwd.\n\t */\n\tprivate updateTerminalTitle(): void {\n\t\tconst cwdBasename = path.basename(process.cwd());\n\t\tconst sessionName = this.sessionManager.getSessionName();\n\t\tconst workspaceName = sessionName ? `${sessionName} - ${cwdBasename}` : cwdBasename;\n\n\t\tif (!this.terminalTitleController) {\n\t\t\tthis.terminalTitleController = new TerminalTitle(this.ui.terminal, workspaceName);\n\t\t}\n\t\tthis.terminalTitleController.initialize(workspaceName);\n\t}\n\n\t/**\n\t * Run the interactive mode. This is the main entry point.\n\t * Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop.\n\t */\n\tasync run(): Promise<void> {\n\t\tawait this.init();\n\n\t\t// Start version check asynchronously\n\t\tthis.checkForNewVersion().then((newVersion) => {\n\t\t\tif (newVersion) {\n\t\t\t\tthis.showNewVersionNotification(newVersion);\n\t\t\t}\n\t\t});\n\n\t\t// Check tmux keyboard setup asynchronously\n\t\tthis.checkTmuxKeyboardSetup().then((warning) => {\n\t\t\tif (warning) {\n\t\t\t\tthis.showWarning(warning);\n\t\t\t}\n\t\t});\n\n\t\t// Show startup warnings\n\t\tconst { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;\n\n\t\tif (migratedProviders && migratedProviders.length > 0) {\n\t\t\tthis.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(\", \")}`);\n\t\t}\n\n\t\tconst modelsJsonError = this.session.modelRegistry.getError();\n\t\tif (modelsJsonError) {\n\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t}\n\n\t\tif (modelFallbackMessage) {\n\t\t\tthis.showWarning(modelFallbackMessage);\n\t\t}\n\n\t\t// Process initial messages\n\t\tif (initialMessage) {\n\t\t\ttry {\n\t\t\t\tawait this.session.prompt(initialMessage, { images: initialImages });\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\tthis.showError(errorMessage);\n\t\t\t}\n\t\t}\n\n\t\tif (initialMessages) {\n\t\t\tfor (const message of initialMessages) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.session.prompt(message);\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\t\tthis.showError(errorMessage);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Main interactive loop\n\t\twhile (true) {\n\t\t\tconst userInput = await this.getUserInput();\n\t\t\ttry {\n\t\t\t\tawait this.session.prompt(userInput);\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\tthis.showError(errorMessage);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Check npm registry for a newer version on the active release channel.\n\t * Suppressed in development/source mode and when the version check is skipped.\n\t */\n\tprivate async checkForNewVersion(): Promise<string | undefined> {\n\t\tif (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;\n\n\t\t// Rule U04: suppress update notice in development/source mode\n\t\tif (isDevMode()) return undefined;\n\n\t\tconst channel = this.settingsManager.getReleaseChannel();\n\n\t\t// Invalid/unknown channel: fail closed, no registry query\n\t\tif (channel === undefined) return undefined;\n\n\t\ttry {\n\t\t\tconst response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/${channel}`, {\n\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t});\n\t\t\tif (!response.ok) return undefined;\n\n\t\t\tconst data = (await response.json()) as { version?: string };\n\t\t\tconst remoteVersion = data.version;\n\n\t\t\t// Rule U05: missing or malformed version is non-authoritative\n\t\t\tif (!remoteVersion) return undefined;\n\n\t\t\t// Rule U01: show update only when remote > current (semver)\n\t\t\tif (semverGt(remoteVersion, this.version)) {\n\t\t\t\treturn remoteVersion;\n\t\t\t}\n\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async checkTmuxKeyboardSetup(): Promise<string | undefined> {\n\t\tif (!process.env.TMUX) return undefined;\n\n\t\tconst runTmuxShow = (option: string): Promise<string | undefined> => {\n\t\t\treturn new Promise((resolve) => {\n\t\t\t\tconst proc = spawn(\"tmux\", [\"show\", \"-gv\", option], {\n\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t\t\t});\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tproc.kill();\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t}, 2000);\n\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.on(\"error\", () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(code === 0 ? stdout.trim() : undefined);\n\t\t\t\t});\n\t\t\t});\n\t\t};\n\n\t\tconst [extendedKeys, extendedKeysFormat] = await Promise.all([\n\t\t\trunTmuxShow(\"extended-keys\"),\n\t\t\trunTmuxShow(\"extended-keys-format\"),\n\t\t]);\n\n\t\tif (extendedKeys !== \"on\" && extendedKeys !== \"always\") {\n\t\t\treturn \"tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.\";\n\t\t}\n\n\t\tif (extendedKeysFormat === \"xterm\") {\n\t\t\treturn `tmux extended-keys-format is xterm. ${APP_NAME} works best with csi-u. Add \\`set -g extended-keys-format csi-u\\` to ~/.tmux.conf and restart tmux.`;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get changelog entries to display on startup.\n\t * Only shows new entries since last seen version, skips for resumed sessions.\n\t */\n\tprivate getChangelogForDisplay(): string | undefined {\n\t\t// Skip changelog for resumed/continued sessions (already have messages)\n\t\tif (this.session.state.messages.length > 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lastVersion = this.settingsManager.getLastChangelogVersion();\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst entries = parseChangelog(changelogPath);\n\n\t\tif (!lastVersion) {\n\t\t\t// Fresh install - just record the version, don't show changelog\n\t\t\tthis.settingsManager.setLastChangelogVersion(VERSION);\n\t\t\treturn undefined;\n\t\t} else {\n\t\t\tconst newEntries = getNewEntries(entries, lastVersion);\n\t\t\tif (newEntries.length > 0) {\n\t\t\t\tthis.settingsManager.setLastChangelogVersion(VERSION);\n\t\t\t\treturn newEntries.map((e) => e.content).join(\"\\n\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tprivate getMarkdownThemeWithSettings(): MarkdownTheme {\n\t\treturn {\n\t\t\t...getMarkdownTheme(),\n\t\t\tcodeBlockIndent: this.settingsManager.getCodeBlockIndent(),\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Extension System\n\t// =========================================================================\n\n\tprivate formatDisplayPath(p: string): string {\n\t\tconst home = os.homedir();\n\t\tlet result = p;\n\n\t\t// Replace home directory with ~\n\t\tif (result.startsWith(home)) {\n\t\t\tresult = `~${result.slice(home.length)}`;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get a short path relative to the package root for display.\n\t */\n\tprivate getShortPath(fullPath: string, source: string): string {\n\t\t// For npm packages, show path relative to node_modules/pkg/\n\t\tconst npmMatch = fullPath.match(/node_modules\\/(@?[^/]+(?:\\/[^/]+)?)\\/(.*)/);\n\t\tif (npmMatch && source.startsWith(\"npm:\")) {\n\t\t\treturn npmMatch[2];\n\t\t}\n\n\t\t// For git packages, show path relative to repo root\n\t\tconst gitMatch = fullPath.match(/git\\/[^/]+\\/[^/]+\\/(.*)/);\n\t\tif (gitMatch && source.startsWith(\"git:\")) {\n\t\t\treturn gitMatch[1];\n\t\t}\n\n\t\t// For local/auto, just use formatDisplayPath\n\t\treturn this.formatDisplayPath(fullPath);\n\t}\n\n\tprivate getDisplaySourceInfo(\n\t\tsource: string,\n\t\tscope: string,\n\t): { label: string; scopeLabel?: string; color: \"accent\" | \"muted\" } {\n\t\tif (source === \"local\") {\n\t\t\tif (scope === \"user\") {\n\t\t\t\treturn { label: \"user\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"project\") {\n\t\t\t\treturn { label: \"project\", color: \"muted\" };\n\t\t\t}\n\t\t\tif (scope === \"temporary\") {\n\t\t\t\treturn { label: \"path\", scopeLabel: \"temp\", color: \"muted\" };\n\t\t\t}\n\t\t\treturn { label: \"path\", color: \"muted\" };\n\t\t}\n\n\t\tif (source === \"cli\") {\n\t\t\treturn { label: \"path\", scopeLabel: scope === \"temporary\" ? \"temp\" : undefined, color: \"muted\" };\n\t\t}\n\n\t\tconst scopeLabel =\n\t\t\tscope === \"user\" ? \"user\" : scope === \"project\" ? \"project\" : scope === \"temporary\" ? \"temp\" : undefined;\n\t\treturn { label: source, scopeLabel, color: \"accent\" };\n\t}\n\n\tprivate getScopeGroup(source: string, scope: string): \"user\" | \"project\" | \"path\" {\n\t\tif (source === \"cli\" || scope === \"temporary\") return \"path\";\n\t\tif (scope === \"user\") return \"user\";\n\t\tif (scope === \"project\") return \"project\";\n\t\treturn \"path\";\n\t}\n\n\tprivate isPackageSource(source: string): boolean {\n\t\treturn source.startsWith(\"npm:\") || source.startsWith(\"git:\");\n\t}\n\n\tprivate buildScopeGroups(\n\t\tpaths: string[],\n\t\tmetadata: Map<string, { source: string; scope: string; origin: string }>,\n\t): Array<{ scope: \"user\" | \"project\" | \"path\"; paths: string[]; packages: Map<string, string[]> }> {\n\t\tconst groups: Record<\n\t\t\t\"user\" | \"project\" | \"path\",\n\t\t\t{ scope: \"user\" | \"project\" | \"path\"; paths: string[]; packages: Map<string, string[]> }\n\t\t> = {\n\t\t\tuser: { scope: \"user\", paths: [], packages: new Map() },\n\t\t\tproject: { scope: \"project\", paths: [], packages: new Map() },\n\t\t\tpath: { scope: \"path\", paths: [], packages: new Map() },\n\t\t};\n\n\t\tfor (const p of paths) {\n\t\t\tconst meta = this.findMetadata(p, metadata);\n\t\t\tconst source = meta?.source ?? \"local\";\n\t\t\tconst scope = meta?.scope ?? \"project\";\n\t\t\tconst groupKey = this.getScopeGroup(source, scope);\n\t\t\tconst group = groups[groupKey];\n\n\t\t\tif (this.isPackageSource(source)) {\n\t\t\t\tconst list = group.packages.get(source) ?? [];\n\t\t\t\tlist.push(p);\n\t\t\t\tgroup.packages.set(source, list);\n\t\t\t} else {\n\t\t\t\tgroup.paths.push(p);\n\t\t\t}\n\t\t}\n\n\t\treturn [groups.project, groups.user, groups.path].filter(\n\t\t\t(group) => group.paths.length > 0 || group.packages.size > 0,\n\t\t);\n\t}\n\n\tprivate formatScopeGroups(\n\t\tgroups: Array<{ scope: \"user\" | \"project\" | \"path\"; paths: string[]; packages: Map<string, string[]> }>,\n\t\toptions: {\n\t\t\tformatPath: (p: string) => string;\n\t\t\tformatPackagePath: (p: string, source: string) => string;\n\t\t},\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\tfor (const group of groups) {\n\t\t\tlines.push(`  ${theme.fg(\"accent\", group.scope)}`);\n\n\t\t\tconst sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b));\n\t\t\tfor (const p of sortedPaths) {\n\t\t\t\tlines.push(theme.fg(\"dim\", `    ${options.formatPath(p)}`));\n\t\t\t}\n\n\t\t\tconst sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));\n\t\t\tfor (const [source, paths] of sortedPackages) {\n\t\t\t\tlines.push(`    ${theme.fg(\"mdLink\", source)}`);\n\t\t\t\tconst sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b));\n\t\t\t\tfor (const p of sortedPackagePaths) {\n\t\t\t\t\tlines.push(theme.fg(\"dim\", `      ${options.formatPackagePath(p, source)}`));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\t/**\n\t * Find metadata for a path, checking parent directories if exact match fails.\n\t * Package manager stores metadata for directories, but we display file paths.\n\t */\n\tprivate findMetadata(\n\t\tp: string,\n\t\tmetadata: Map<string, { source: string; scope: string; origin: string }>,\n\t): { source: string; scope: string; origin: string } | undefined {\n\t\t// Try exact match first\n\t\tconst exact = metadata.get(p);\n\t\tif (exact) return exact;\n\n\t\t// Try parent directories (package manager stores directory paths)\n\t\tlet current = p;\n\t\twhile (current.includes(\"/\")) {\n\t\t\tcurrent = current.substring(0, current.lastIndexOf(\"/\"));\n\t\t\tconst parent = metadata.get(current);\n\t\t\tif (parent) return parent;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Format a path with its source/scope info from metadata.\n\t */\n\tprivate formatPathWithSource(\n\t\tp: string,\n\t\tmetadata: Map<string, { source: string; scope: string; origin: string }>,\n\t): string {\n\t\tconst meta = this.findMetadata(p, metadata);\n\t\tif (meta) {\n\t\t\tconst shortPath = this.getShortPath(p, meta.source);\n\t\t\tconst { label, scopeLabel } = this.getDisplaySourceInfo(meta.source, meta.scope);\n\t\t\tconst labelText = scopeLabel ? `${label} (${scopeLabel})` : label;\n\t\t\treturn `${labelText} ${shortPath}`;\n\t\t}\n\t\treturn this.formatDisplayPath(p);\n\t}\n\n\t/**\n\t * Format resource diagnostics with nice collision display using metadata.\n\t */\n\tprivate formatDiagnostics(\n\t\tdiagnostics: readonly ResourceDiagnostic[],\n\t\tmetadata: Map<string, { source: string; scope: string; origin: string }>,\n\t): string {\n\t\tconst lines: string[] = [];\n\n\t\t// Group collision diagnostics by name\n\t\tconst collisions = new Map<string, ResourceDiagnostic[]>();\n\t\tconst otherDiagnostics: ResourceDiagnostic[] = [];\n\n\t\tfor (const d of diagnostics) {\n\t\t\tif (d.type === \"collision\" && d.collision) {\n\t\t\t\tconst list = collisions.get(d.collision.name) ?? [];\n\t\t\t\tlist.push(d);\n\t\t\t\tcollisions.set(d.collision.name, list);\n\t\t\t} else {\n\t\t\t\totherDiagnostics.push(d);\n\t\t\t}\n\t\t}\n\n\t\t// Format collision diagnostics grouped by name\n\t\tfor (const [name, collisionList] of collisions) {\n\t\t\tconst first = collisionList[0]?.collision;\n\t\t\tif (!first) continue;\n\t\t\tlines.push(theme.fg(\"warning\", `  \"${name}\" collision:`));\n\t\t\t// Show winner\n\t\t\tlines.push(\n\t\t\t\ttheme.fg(\"dim\", `    ${theme.fg(\"success\", \"✓\")} ${this.formatPathWithSource(first.winnerPath, metadata)}`),\n\t\t\t);\n\t\t\t// Show all losers\n\t\t\tfor (const d of collisionList) {\n\t\t\t\tif (d.collision) {\n\t\t\t\t\tlines.push(\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`    ${theme.fg(\"warning\", \"✗\")} ${this.formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Format other diagnostics (skill name collisions, parse errors, etc.)\n\t\tfor (const d of otherDiagnostics) {\n\t\t\tif (d.path) {\n\t\t\t\t// Use metadata-aware formatting for paths\n\t\t\t\tconst sourceInfo = this.formatPathWithSource(d.path, metadata);\n\t\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `  ${sourceInfo}`));\n\t\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `    ${d.message}`));\n\t\t\t} else {\n\t\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `  ${d.message}`));\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate showLoadedResources(options?: {\n\t\textensionPaths?: string[];\n\t\tforce?: boolean;\n\t\tshowDiagnosticsWhenQuiet?: boolean;\n\t\ttargetContainer?: Container;\n\t}): void {\n\t\tconst showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();\n\t\tconst showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;\n\t\tif (!showListing && !showDiagnostics) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst container = options?.targetContainer ?? this.chatContainer;\n\t\tconst metadata = this.session.resourceLoader.getPathMetadata();\n\t\tconst sectionHeader = (name: string, color: ThemeColor = \"mdHeading\") => theme.fg(color, `[${name}]`);\n\n\t\tconst skillsResult = this.session.resourceLoader.getSkills();\n\t\tconst promptsResult = this.session.resourceLoader.getPrompts();\n\t\tconst themesResult = this.session.resourceLoader.getThemes();\n\n\t\tif (showListing) {\n\t\t\tconst contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;\n\t\t\tif (contextFiles.length > 0) {\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t\tconst contextList = contextFiles\n\t\t\t\t\t.map((f) => theme.fg(\"dim\", `  ${this.formatDisplayPath(f.path)}`))\n\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tcontainer.addChild(new Text(`${sectionHeader(\"Context\")}\\n${contextList}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst skills = skillsResult.skills;\n\t\t\tif (skills.length > 0) {\n\t\t\t\tconst skillPaths = skills.map((s) => s.filePath);\n\t\t\t\tconst groups = this.buildScopeGroups(skillPaths, metadata);\n\t\t\t\tconst skillList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (p) => this.formatDisplayPath(p),\n\t\t\t\t\tformatPackagePath: (p, source) => this.getShortPath(p, source),\n\t\t\t\t});\n\t\t\t\tcontainer.addChild(new Text(`${sectionHeader(\"Skills\")}\\n${skillList}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst templates = this.session.promptTemplates;\n\t\t\tif (templates.length > 0) {\n\t\t\t\tconst templatePaths = templates.map((t) => t.filePath);\n\t\t\t\tconst groups = this.buildScopeGroups(templatePaths, metadata);\n\t\t\t\tconst templateByPath = new Map(templates.map((t) => [t.filePath, t]));\n\t\t\t\tconst templateList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (p) => {\n\t\t\t\t\t\tconst template = templateByPath.get(p);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(p);\n\t\t\t\t\t},\n\t\t\t\t\tformatPackagePath: (p) => {\n\t\t\t\t\t\tconst template = templateByPath.get(p);\n\t\t\t\t\t\treturn template ? `/${template.name}` : this.formatDisplayPath(p);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tcontainer.addChild(new Text(`${sectionHeader(\"Prompts\")}\\n${templateList}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst extensionPaths = options?.extensionPaths ?? [];\n\t\t\tif (extensionPaths.length > 0) {\n\t\t\t\tconst groups = this.buildScopeGroups(extensionPaths, metadata);\n\t\t\t\tconst extList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (p) => this.formatDisplayPath(p),\n\t\t\t\t\tformatPackagePath: (p, source) => this.getShortPath(p, source),\n\t\t\t\t});\n\t\t\t\tcontainer.addChild(new Text(`${sectionHeader(\"Extensions\", \"mdHeading\")}\\n${extList}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\t// Show loaded themes (excluding built-in)\n\t\t\tconst loadedThemes = themesResult.themes;\n\t\t\tconst customThemes = loadedThemes.filter((t) => t.sourcePath);\n\t\t\tif (customThemes.length > 0) {\n\t\t\t\tconst themePaths = customThemes.map((t) => t.sourcePath!);\n\t\t\t\tconst groups = this.buildScopeGroups(themePaths, metadata);\n\t\t\t\tconst themeList = this.formatScopeGroups(groups, {\n\t\t\t\t\tformatPath: (p) => this.formatDisplayPath(p),\n\t\t\t\t\tformatPackagePath: (p, source) => this.getShortPath(p, source),\n\t\t\t\t});\n\t\t\t\tcontainer.addChild(new Text(`${sectionHeader(\"Themes\")}\\n${themeList}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t}\n\n\t\tif (showDiagnostics) {\n\t\t\tconst contextDiagnostics = this.session.resourceLoader.getContextDiagnostics().diagnostics;\n\t\t\tif (contextDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(contextDiagnostics, metadata);\n\t\t\t\tcontainer.addChild(new Text(`${theme.fg(\"warning\", \"[Context issues]\")}\\n${warningLines}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst skillDiagnostics = skillsResult.diagnostics;\n\t\t\tif (skillDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(skillDiagnostics, metadata);\n\t\t\t\tcontainer.addChild(new Text(`${theme.fg(\"warning\", \"[Skill conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst promptDiagnostics = promptsResult.diagnostics;\n\t\t\tif (promptDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(promptDiagnostics, metadata);\n\t\t\t\tcontainer.addChild(new Text(`${theme.fg(\"warning\", \"[Prompt conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst extensionDiagnostics: ResourceDiagnostic[] = [];\n\t\t\tconst extensionErrors = this.session.resourceLoader.getExtensions().errors;\n\t\t\tif (extensionErrors.length > 0) {\n\t\t\t\tfor (const error of extensionErrors) {\n\t\t\t\t\textensionDiagnostics.push({ type: \"error\", message: error.error, path: error.path });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];\n\t\t\textensionDiagnostics.push(...commandDiagnostics);\n\n\t\t\tconst shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];\n\t\t\textensionDiagnostics.push(...shortcutDiagnostics);\n\n\t\t\tif (extensionDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);\n\t\t\t\tcontainer.addChild(new Text(`${theme.fg(\"warning\", \"[Extension issues]\")}\\n${warningLines}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\n\t\t\tconst themeDiagnostics = themesResult.diagnostics;\n\t\t\tif (themeDiagnostics.length > 0) {\n\t\t\t\tconst warningLines = this.formatDiagnostics(themeDiagnostics, metadata);\n\t\t\t\tcontainer.addChild(new Text(`${theme.fg(\"warning\", \"[Theme conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Initialize the extension system with TUI-based UI context.\n\t */\n\tprivate async initExtensions(): Promise<void> {\n\t\tconst uiContext = this.createExtensionUIContext();\n\t\tawait this.session.bindExtensions({\n\t\t\tuiContext,\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => this.session.agent.waitForIdle(),\n\t\t\t\tnewSession: async (options) => {\n\t\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\t\tthis.loadingAnimation.dispose();\n\t\t\t\t\t\tthis.loadingAnimation = undefined;\n\t\t\t\t\t}\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t\t\t\t// Delegate to AgentSession (handles setup + agent state sync)\n\t\t\t\t\tconst success = await this.session.newSession(options);\n\t\t\t\t\tif (!success) {\n\t\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.resetInteractiveSessionUI(true);\n\t\t\t\t\tthis.ui.requestRender();\n\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\tfork: async (entryId) => {\n\t\t\t\t\tconst capturedEpoch = this.sessionEpoch;\n\t\t\t\t\tconst result = await this.session.fork(entryId);\n\t\t\t\t\tif (result.cancelled || this.sessionEpoch !== capturedEpoch) {\n\t\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\tthis.renderInitialMessages();\n\t\t\t\t\tthis.editor.setText(result.selectedText);\n\t\t\t\t\tthis.showStatus(\"Forked to new session\");\n\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, options) => {\n\t\t\t\t\tconst capturedEpoch = this.sessionEpoch;\n\t\t\t\t\tconst result = await this.session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: options?.summarize,\n\t\t\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\t\t\tlabel: options?.label,\n\t\t\t\t\t});\n\t\t\t\t\tif (result.cancelled || this.sessionEpoch !== capturedEpoch) {\n\t\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\tthis.renderInitialMessages();\n\t\t\t\t\tif (result.editorText && !this.editor.getText().trim()) {\n\t\t\t\t\t\tthis.editor.setText(result.editorText);\n\t\t\t\t\t}\n\t\t\t\t\tthis.showStatus(\"Navigated to selected point\");\n\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath) => {\n\t\t\t\t\tawait this.handleResumeSession(sessionPath);\n\t\t\t\t\treturn { cancelled: false };\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\t},\n\t\t\t},\n\t\t\tshutdownHandler: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t\tif (!this.session.isStreaming) {\n\t\t\t\t\tvoid this.shutdown();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tthis.showExtensionError(error.extensionPath, error.error, error.stack);\n\t\t\t},\n\t\t});\n\n\t\tsetRegisteredThemes(this.session.resourceLoader.getThemes().themes);\n\t\tthis.setupAutocomplete(this.fdPath);\n\n\t\tconst extensionRunner = this.session.extensionRunner;\n\t\tif (!extensionRunner) {\n\t\t\tthis.showLoadedResources({ extensionPaths: [], force: false });\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setupExtensionShortcuts(extensionRunner);\n\t\tthis.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });\n\t}\n\n\t/**\n\t * Get a registered tool definition by name (for custom rendering).\n\t */\n\tprivate getRegisteredToolDefinition(toolName: string) {\n\t\tconst tools = this.session.extensionRunner?.getAllRegisteredTools() ?? [];\n\t\tconst registeredTool = tools.find((t) => t.definition.name === toolName);\n\t\treturn registeredTool?.definition;\n\t}\n\n\t/**\n\t * Set up keyboard shortcuts registered by extensions.\n\t */\n\tprivate setupExtensionShortcuts(extensionRunner: ExtensionRunner): void {\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size === 0) return;\n\n\t\t// Create a context for shortcut handlers\n\t\tconst createContext = (): ExtensionContext => ({\n\t\t\tui: this.createExtensionUIContext(),\n\t\t\thasUI: true,\n\t\t\tcwd: process.cwd(),\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.session.modelRegistry,\n\t\t\tmodel: this.session.model,\n\t\t\tisIdle: () => !this.session.isStreaming,\n\t\t\tabort: () => this.session.abort(),\n\t\t\thasPendingMessages: () => this.session.pendingMessageCount > 0,\n\t\t\tshutdown: () => {\n\t\t\t\tthis.shutdownRequested = true;\n\t\t\t},\n\t\t\tgetContextUsage: () => this.session.getContextUsage(),\n\t\t\tcompact: (options) => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.executeCompaction(options?.customInstructions, false);\n\t\t\t\t\t\tif (result) {\n\t\t\t\t\t\t\toptions?.onComplete?.(result);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t\toptions?.onError?.(err);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t\tgetSystemPrompt: () => this.session.systemPrompt,\n\t\t\tgetActiveMissionId: () => this.session.missionId,\n\t\t});\n\n\t\t// Set up the extension shortcut handler on the default editor\n\t\tthis.defaultEditor.onExtensionShortcut = (data: string) => {\n\t\t\tfor (const [shortcutStr, shortcut] of shortcuts) {\n\t\t\t\t// Cast to KeyId - extension shortcuts use the same format\n\t\t\t\tif (matchesKey(data, shortcutStr as KeyId)) {\n\t\t\t\t\t// Run handler async, don't block input\n\t\t\t\t\tPromise.resolve(shortcut.handler(createContext())).catch((err) => {\n\t\t\t\t\t\tthis.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t}\n\n\t/**\n\t * Set extension status text in the footer.\n\t */\n\tprivate setExtensionStatus(key: string, text: string | undefined): void {\n\t\tthis.footerDataProvider.setExtensionStatus(key, text);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/**\n\t * Set an extension widget (string array or custom component).\n\t * Routes widgets to either widgetContainerAbove or widgetContainerBelow based on placement.\n\t */\n\tprivate setExtensionWidget(\n\t\tkey: string,\n\t\tcontent: string[] | ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined,\n\t\toptions?: ExtensionWidgetOptions,\n\t): void {\n\t\tconst placement = options?.placement ?? \"aboveEditor\";\n\t\tconst removeExisting = (map: Map<string, Component & { dispose?(): void }>) => {\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing?.dispose) existing.dispose();\n\t\t\tmap.delete(key);\n\t\t};\n\n\t\tremoveExisting(this.extensionWidgetsAbove);\n\t\tremoveExisting(this.extensionWidgetsBelow);\n\n\t\tif (content === undefined) {\n\t\t\tthis.renderWidgets();\n\t\t\treturn;\n\t\t}\n\n\t\tlet component: Component & { dispose?(): void };\n\n\t\tif (Array.isArray(content)) {\n\t\t\t// Wrap string array in a Container with Text components\n\t\t\tconst container = new Container();\n\t\t\tfor (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {\n\t\t\t\tcontainer.addChild(new Text(line, 1, 0));\n\t\t\t}\n\t\t\tif (content.length > InteractiveMode.MAX_WIDGET_LINES) {\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"muted\", \"... (widget truncated)\"), 1, 0));\n\t\t\t}\n\t\t\tcomponent = container;\n\t\t} else {\n\t\t\t// Factory function - create component\n\t\t\tcomponent = content(this.ui, theme);\n\t\t}\n\n\t\tconst targetMap = placement === \"belowEditor\" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;\n\t\ttargetMap.set(key, component);\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate clearExtensionWidgets(): void {\n\t\tfor (const widget of this.extensionWidgetsAbove.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tfor (const widget of this.extensionWidgetsBelow.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tthis.extensionWidgetsAbove.clear();\n\t\tthis.extensionWidgetsBelow.clear();\n\t\tthis.renderWidgets();\n\t}\n\n\tprivate resetExtensionUI(): void {\n\t\tif (this.extensionSelector) {\n\t\t\tthis.hideExtensionSelector();\n\t\t}\n\t\tif (this.extensionInput) {\n\t\t\tthis.hideExtensionInput();\n\t\t}\n\t\tif (this.extensionEditor) {\n\t\t\tthis.hideExtensionEditor();\n\t\t}\n\t\tthis.ui.hideOverlay();\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.setExtensionFooter(undefined);\n\t\tthis.setExtensionHeader(undefined);\n\t\tthis.clearExtensionWidgets();\n\t\tthis.footerDataProvider.clearExtensionStatuses();\n\t\tthis.footer.invalidate();\n\t\tthis.setCustomEditorComponent(undefined);\n\t\tthis.defaultEditor.onExtensionShortcut = undefined;\n\t\tthis.updateTerminalTitle();\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.setMessage(\n\t\t\t\t`${this.defaultWorkingMessage} (${appKey(this.keybindings, \"interrupt\")} to interrupt)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate setPromptWidgetsVisible(visible: boolean): void {\n\t\tif (this.promptWidgetsVisible === visible) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.promptWidgetsVisible = visible;\n\t\tif (visible) {\n\t\t\tthis.renderWidgets(false);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.widgetContainerAbove.clear();\n\t\tthis.widgetContainerBelow.clear();\n\t}\n\n\tprivate mountPromptOwner(\n\t\tcomponent: Component,\n\t\towner: any,\n\t\toptions?: { focus?: Component; showWidgets?: boolean; requestRender?: boolean },\n\t): void {\n\t\tconst focus = options?.focus ?? component;\n\t\tconst showWidgets = options?.showWidgets ?? false;\n\t\tconst requestRender = options?.requestRender ?? true;\n\t\tconst isMounted =\n\t\t\tthis.promptAreaComponent === component &&\n\t\t\tthis.editorContainer.children.length === 1 &&\n\t\t\tthis.editorContainer.children[0] === component;\n\n\t\tif (!isMounted) {\n\t\t\tthis.promptOwner = owner;\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(component);\n\t\t\tthis.promptAreaComponent = component;\n\t\t}\n\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tif (this.editorContainer.children.length > 1) {\n\t\t\t\tthrow new Error(\"Invariant violation: Multiple prompt owners mounted\");\n\t\t\t}\n\t\t}\n\n\t\tthis.setPromptWidgetsVisible(showWidgets);\n\t\tif (this.ui.getFocusedComponent() !== focus) {\n\t\t\tthis.ui.setFocus(focus);\n\t\t}\n\t\tif (requestRender) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate restoreCanonicalEditor(options?: {\n\t\ttext?: string;\n\t\tfocus?: boolean;\n\t\trequestRender?: boolean;\n\t\towner?: any;\n\t}): void {\n\t\tif (options?.owner !== undefined && this.promptOwner !== options.owner) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (options?.text !== undefined) {\n\t\t\tthis.editor.setText(options.text);\n\t\t}\n\t\tconst editorComponent = this.editor as Component;\n\t\tconst isMounted =\n\t\t\tthis.promptAreaComponent === editorComponent &&\n\t\t\tthis.editorContainer.children.length === 1 &&\n\t\t\tthis.editorContainer.children[0] === editorComponent;\n\t\tif (!isMounted) {\n\t\t\tthis.promptOwner = undefined;\n\t\t\tthis.editorContainer.clear();\n\t\t\tthis.editorContainer.addChild(editorComponent);\n\t\t\tthis.promptAreaComponent = editorComponent;\n\t\t}\n\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tif (this.editorContainer.children.length > 1) {\n\t\t\t\tthrow new Error(\"Invariant violation: Multiple prompt owners mounted\");\n\t\t\t}\n\t\t\tif (this.promptAreaComponent !== editorComponent) {\n\t\t\t\tthrow new Error(\"Invariant violation: restoreCanonicalEditor mounted non-canonical editor\");\n\t\t\t}\n\t\t}\n\n\t\tthis.setPromptWidgetsVisible(true);\n\t\tif (options?.focus ?? true) {\n\t\t\tthis.ui.setFocus(editorComponent);\n\t\t}\n\t\tif (options?.requestRender ?? true) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate mountStatusOwner(component: Component, owner: any, options?: { requestRender?: boolean }): void {\n\t\tconst requestRender = options?.requestRender ?? true;\n\t\tconst isMounted =\n\t\t\tthis.statusComponent === component &&\n\t\t\tthis.statusContainer.children.length === 1 &&\n\t\t\tthis.statusContainer.children[0] === component;\n\t\tif (!isMounted) {\n\t\t\tthis.statusOwner = owner;\n\t\t\tthis.statusContainer.clear();\n\t\t\tthis.statusContainer.addChild(component);\n\t\t\tthis.statusComponent = component;\n\t\t}\n\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tif (this.statusContainer.children.length > 1) {\n\t\t\t\tthrow new Error(\"Invariant violation: Multiple status owners mounted\");\n\t\t\t}\n\t\t}\n\n\t\tif (requestRender) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate mountStatusLoader(loader: Loader, options?: { requestRender?: boolean }): void {\n\t\tconst row = new Container();\n\t\trow.addChild(new Spacer(1));\n\t\trow.addChild(loader);\n\t\tthis.mountStatusOwner(row, loader, options);\n\t}\n\n\tprivate clearStatusOwner(options?: { requestRender?: boolean; owner?: any }): void {\n\t\tif (options?.owner !== undefined && this.statusOwner !== options.owner) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst requestRender = options?.requestRender ?? true;\n\t\tif (this.statusComponent !== undefined || this.statusContainer.children.length > 0) {\n\t\t\tthis.statusOwner = undefined;\n\t\t\tthis.statusContainer.clear();\n\t\t\tthis.statusComponent = undefined;\n\t\t}\n\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tif (this.statusContainer.children.length > 0) {\n\t\t\t\tthrow new Error(\"Invariant violation: Status container not empty after clear\");\n\t\t\t}\n\t\t}\n\n\t\tif (requestRender) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\t// Maximum total widget lines to prevent viewport overflow\n\tprivate static readonly MAX_WIDGET_LINES = 10;\n\n\t/**\n\t * Render all extension widgets to the widget container.\n\t */\n\tprivate renderWidgets(requestRender = true): void {\n\t\tif (!this.widgetContainerAbove || !this.widgetContainerBelow) return;\n\t\tif (!this.promptWidgetsVisible) {\n\t\t\tthis.widgetContainerAbove.clear();\n\t\t\tthis.widgetContainerBelow.clear();\n\t\t\tif (requestRender) {\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tthis.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true, this.topBar);\n\t\tthis.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);\n\t\tif (requestRender) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate renderWidgetContainer(\n\t\tcontainer: Container,\n\t\twidgets: Map<string, Component & { dispose?(): void }>,\n\t\tspacerWhenEmpty: boolean,\n\t\tleadingSpacer: boolean,\n\t\tbuiltInComponent?: Component,\n\t): void {\n\t\tcontainer.clear();\n\n\t\tconst hasBuiltIn = builtInComponent !== undefined;\n\t\tconst hasWidgets = widgets.size > 0;\n\n\t\tif (!hasBuiltIn && !hasWidgets) {\n\t\t\tif (spacerWhenEmpty) {\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (leadingSpacer) {\n\t\t\tcontainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tif (builtInComponent) {\n\t\t\tcontainer.addChild(builtInComponent);\n\t\t}\n\n\t\tfor (const component of widgets.values()) {\n\t\t\tcontainer.addChild(component);\n\t\t}\n\t}\n\n\t/**\n\t * Set a custom footer component, or restore the built-in footer.\n\t */\n\tprivate setExtensionFooter(\n\t\tfactory:\n\t\t\t| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })\n\t\t\t| undefined,\n\t): void {\n\t\t// Dispose existing custom footer\n\t\tif (this.customFooter?.dispose) {\n\t\t\tthis.customFooter.dispose();\n\t\t}\n\n\t\t// Remove current footer from UI\n\t\tif (this.customFooter) {\n\t\t\tthis.ui.removeChild(this.customFooter);\n\t\t} else {\n\t\t\tthis.ui.removeChild(this.footer);\n\t\t}\n\n\t\tif (factory) {\n\t\t\t// Create and add custom footer, passing the data provider\n\t\t\tthis.customFooter = factory(this.ui, theme, this.footerDataProvider);\n\t\t\tthis.ui.addChild(this.customFooter);\n\t\t} else {\n\t\t\t// Restore built-in footer\n\t\t\tthis.customFooter = undefined;\n\t\t\tthis.ui.addChild(this.footer);\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\t/**\n\t * Set a custom header component, or restore the built-in header.\n\t */\n\tprivate setExtensionHeader(factory: ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined): void {\n\t\t// Header may not be initialized yet if called during early initialization\n\t\tif (!this.builtInHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Dispose existing custom header\n\t\tif (this.customHeader?.dispose) {\n\t\t\tthis.customHeader.dispose();\n\t\t}\n\n\t\t// Find the index of the current header in the header container\n\t\tconst currentHeader = this.customHeader || this.builtInHeader;\n\t\tconst index = this.headerContainer.children.indexOf(currentHeader);\n\n\t\tif (factory) {\n\t\t\t// Create and add custom header\n\t\t\tthis.customHeader = factory(this.ui, theme);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.customHeader;\n\t\t\t} else {\n\t\t\t\t// If not found (e.g. builtInHeader was never added), add at the top\n\t\t\t\tthis.headerContainer.children.unshift(this.customHeader);\n\t\t\t}\n\t\t} else {\n\t\t\t// Restore built-in header\n\t\t\tthis.customHeader = undefined;\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.headerContainer.children[index] = this.builtInHeader;\n\t\t\t}\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate addExtensionTerminalInputListener(\n\t\thandler: (data: string) => { consume?: boolean; data?: string } | undefined,\n\t): () => void {\n\t\tconst unsubscribe = this.ui.addInputListener(handler);\n\t\tthis.extensionTerminalInputUnsubscribers.add(unsubscribe);\n\t\treturn () => {\n\t\t\tunsubscribe();\n\t\t\tthis.extensionTerminalInputUnsubscribers.delete(unsubscribe);\n\t\t};\n\t}\n\n\tprivate clearExtensionTerminalInputListeners(): void {\n\t\tfor (const unsubscribe of this.extensionTerminalInputUnsubscribers) {\n\t\t\tunsubscribe();\n\t\t}\n\t\tthis.extensionTerminalInputUnsubscribers.clear();\n\t}\n\n\t/**\n\t * Create the ExtensionUIContext for extensions.\n\t */\n\tprivate createExtensionUIContext(): ExtensionUIContext {\n\t\tconst capturedUiEpoch = this.uiEpoch;\n\t\tconst capturedSessionEpoch = this.sessionEpoch;\n\n\t\tconst guard = <T extends (...args: any[]) => any>(fn: T): T => {\n\t\t\treturn ((...args: any[]) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn fn(...args);\n\t\t\t}) as T;\n\t\t};\n\n\t\treturn {\n\t\t\tselect: (title, options, opts) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t\t}\n\t\t\t\treturn this.showExtensionSelector(title, options, opts);\n\t\t\t},\n\t\t\tconfirm: (title, message, opts) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn Promise.resolve(false);\n\t\t\t\t}\n\t\t\t\treturn this.showExtensionConfirm(title, message, opts);\n\t\t\t},\n\t\t\tinput: (title, placeholder, opts) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t\t}\n\t\t\t\treturn this.showExtensionInput(title, placeholder, opts);\n\t\t\t},\n\t\t\tnotify: guard((message, type) => this.showExtensionNotify(message, type)),\n\t\t\tonTerminalInput: (handler) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn () => {};\n\t\t\t\t}\n\t\t\t\treturn this.addExtensionTerminalInputListener(handler);\n\t\t\t},\n\t\t\tsetStatus: guard((key, text) => this.setExtensionStatus(key, text)),\n\t\t\tsetWorkingMessage: guard((message) => {\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tif (message) {\n\t\t\t\t\t\tthis.loadingAnimation.setMessage(message);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.loadingAnimation.setMessage(\n\t\t\t\t\t\t\t`${this.defaultWorkingMessage} (${appKey(this.keybindings, \"interrupt\")} to interrupt)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Queue message for when loadingAnimation is created (handles agent_start race)\n\t\t\t\t\tthis.pendingWorkingMessage = message;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tsetWidget: guard((key, content, options) => this.setExtensionWidget(key, content, options)),\n\t\t\tsetFooter: guard((factory) => this.setExtensionFooter(factory)),\n\t\t\tsetHeader: guard((factory) => this.setExtensionHeader(factory)),\n\t\t\tsetTitle: guard((title) => this.ui.terminal.setTitle(title)),\n\t\t\tcustom: (factory, options) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn Promise.resolve(undefined as any);\n\t\t\t\t}\n\t\t\t\treturn this.showExtensionCustom(factory, options);\n\t\t\t},\n\t\t\tpasteToEditor: guard((text) => this.editor.handleInput(`\\x1b[200~${text}\\x1b[201~`)),\n\t\t\tsetEditorText: guard((text) => this.editor.setText(text)),\n\t\t\tgetEditorText: () => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn \"\";\n\t\t\t\t}\n\t\t\t\treturn this.editor.getText();\n\t\t\t},\n\t\t\teditor: (title, prefill) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t\t}\n\t\t\t\treturn this.showExtensionEditor(title, prefill);\n\t\t\t},\n\t\t\tsetEditorComponent: guard((factory) => this.setCustomEditorComponent(factory)),\n\t\t\tget theme() {\n\t\t\t\treturn theme;\n\t\t\t},\n\t\t\tgetAllThemes: () => getAvailableThemesWithPaths(),\n\t\t\tgetTheme: (name) => getThemeByName(name),\n\t\t\tsetTheme: (themeOrName) => {\n\t\t\t\tif (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {\n\t\t\t\t\treturn { success: false, error: \"Stale context\" };\n\t\t\t\t}\n\t\t\t\tif (themeOrName instanceof Theme) {\n\t\t\t\t\tsetThemeInstance(themeOrName);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\treturn { success: true };\n\t\t\t\t}\n\t\t\t\tconst result = setTheme(themeOrName, true);\n\t\t\t\tif (result.success) {\n\t\t\t\t\tif (this.settingsManager.getTheme() !== themeOrName) {\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeOrName);\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t\tgetToolsExpanded: () => this.toolOutputExpanded,\n\t\t\tsetToolsExpanded: guard((expanded) => this.setToolsExpanded(expanded)),\n\t\t};\n\t}\n\n\t/**\n\t * Show a selector for extensions.\n\t */\n\tprivate showExtensionSelector(\n\t\ttitle: string,\n\t\toptions: string[],\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.hideExtensionSelector();\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionSelector = new ExtensionSelectorComponent(\n\t\t\t\ttitle,\n\t\t\t\toptions,\n\t\t\t\t(option) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionSelector(this.extensionSelector);\n\t\t\t\t\tresolve(option);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionSelector(this.extensionSelector);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.mountPromptOwner(this.extensionSelector, this.extensionSelector);\n\t\t});\n\t}\n\n\t/**\n\t * Hide the extension selector.\n\t */\n\tprivate hideExtensionSelector(owner?: any): void {\n\t\tthis.extensionSelector?.dispose();\n\t\tthis.extensionSelector = undefined;\n\t\tthis.restoreCanonicalEditor({ owner });\n\t}\n\n\t/**\n\t * Show a confirmation dialog for extensions.\n\t */\n\tprivate async showExtensionConfirm(\n\t\ttitle: string,\n\t\tmessage: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<boolean> {\n\t\tconst result = await this.showExtensionSelector(`${title}\\n${message}`, [\"Yes\", \"No\"], opts);\n\t\treturn result === \"Yes\";\n\t}\n\n\t/**\n\t * Show a text input for extensions.\n\t */\n\tprivate showExtensionInput(\n\t\ttitle: string,\n\t\tplaceholder?: string,\n\t\topts?: ExtensionUIDialogOptions,\n\t): Promise<string | undefined> {\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\treturn new Promise((resolve) => {\n\t\t\tif (opts?.signal?.aborted) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.hideExtensionInput(this.extensionInput);\n\t\t\t\tresolve(undefined);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tthis.extensionInput = new ExtensionInputComponent(\n\t\t\t\ttitle,\n\t\t\t\tplaceholder,\n\t\t\t\t(value) => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionInput(this.extensionInput);\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionInput(this.extensionInput);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\t{ tui: this.ui, timeout: opts?.timeout },\n\t\t\t);\n\n\t\t\tthis.mountPromptOwner(this.extensionInput, this.extensionInput);\n\t\t});\n\t}\n\n\t/**\n\t * Hide the extension input.\n\t */\n\tprivate hideExtensionInput(owner?: any): void {\n\t\tthis.extensionInput?.dispose();\n\t\tthis.extensionInput = undefined;\n\t\tthis.restoreCanonicalEditor({ owner });\n\t}\n\n\t/**\n\t * Show a multi-line editor for extensions (with Ctrl+G support).\n\t */\n\tprivate showExtensionEditor(title: string, prefill?: string): Promise<string | undefined> {\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.extensionEditor = new ExtensionEditorComponent(\n\t\t\t\tthis.ui,\n\t\t\t\tthis.keybindings,\n\t\t\t\ttitle,\n\t\t\t\tprefill,\n\t\t\t\t(value) => {\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionEditor(this.extensionEditor);\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.hideExtensionEditor(this.extensionEditor);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t},\n\t\t\t\tundefined,\n\t\t\t\tgetEditorTheme({\n\t\t\t\t\tbgEnabled: this.settingsManager.getEditorBackgroundEnabled(),\n\t\t\t\t\tbgColor: this.settingsManager.getEditorBackgroundColor(),\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tthis.mountPromptOwner(this.extensionEditor, this.extensionEditor);\n\t\t});\n\t}\n\n\t/**\n\t * Hide the extension editor.\n\t */\n\tprivate hideExtensionEditor(owner?: any): void {\n\t\tthis.extensionEditor = undefined;\n\t\tthis.restoreCanonicalEditor({ owner });\n\t}\n\n\t/**\n\t * Set a custom editor component from an extension.\n\t * Pass undefined to restore the default editor.\n\t */\n\tprivate setCustomEditorComponent(\n\t\tfactory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent) | undefined,\n\t): void {\n\t\t// Save text from current editor before switching\n\t\tconst currentText = this.editor.getText();\n\n\t\tif (factory) {\n\t\t\t// Create the custom editor with tui, theme, and keybindings\n\t\t\tconst newEditor = factory(\n\t\t\t\tthis.ui,\n\t\t\t\tgetEditorTheme({\n\t\t\t\t\tbgEnabled: this.settingsManager.getEditorBackgroundEnabled(),\n\t\t\t\t\tbgColor: this.settingsManager.getEditorBackgroundColor(),\n\t\t\t\t}),\n\t\t\t\tthis.keybindings,\n\t\t\t);\n\n\t\t\t// Wire up callbacks from the default editor\n\t\t\tnewEditor.onSubmit = this.defaultEditor.onSubmit;\n\t\t\tnewEditor.onChange = this.defaultEditor.onChange;\n\n\t\t\t// Copy text from previous editor\n\t\t\tnewEditor.setText(currentText);\n\n\t\t\tif (newEditor.setPaddingX !== undefined) {\n\t\t\t\tnewEditor.setPaddingX(this.defaultEditor.getPaddingX());\n\t\t\t}\n\n\t\t\t// Set autocomplete if supported\n\t\t\tif (newEditor.setAutocompleteProvider && this.autocompleteProvider) {\n\t\t\t\tnewEditor.setAutocompleteProvider(this.autocompleteProvider);\n\t\t\t}\n\n\t\t\t// If extending CustomEditor, copy app-level handlers\n\t\t\t// Use duck typing since instanceof fails across jiti module boundaries\n\t\t\tconst customEditor = newEditor as unknown as Record<string, unknown>;\n\t\t\tif (\"actionHandlers\" in customEditor && customEditor.actionHandlers instanceof Map) {\n\t\t\t\tif (!customEditor.onEscape) {\n\t\t\t\t\tcustomEditor.onEscape = () => this.defaultEditor.onEscape?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onCtrlD) {\n\t\t\t\t\tcustomEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onPasteImage) {\n\t\t\t\t\tcustomEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();\n\t\t\t\t}\n\t\t\t\tif (!customEditor.onExtensionShortcut) {\n\t\t\t\t\tcustomEditor.onExtensionShortcut = (data: string) => this.defaultEditor.onExtensionShortcut?.(data);\n\t\t\t\t}\n\t\t\t\t// Copy action handlers (clear, suspend, model switching, etc.)\n\t\t\t\tfor (const [action, handler] of this.defaultEditor.actionHandlers) {\n\t\t\t\t\t(customEditor.actionHandlers as Map<string, () => void>).set(action, handler);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.editor = newEditor;\n\t\t} else {\n\t\t\t// Restore default editor with text from custom editor\n\t\t\tthis.defaultEditor.setText(currentText);\n\t\t\tthis.editor = this.defaultEditor;\n\t\t}\n\n\t\tthis.restoreCanonicalEditor({ requestRender: false });\n\t\tthis.updatePromptChrome();\n\t}\n\n\t/**\n\t * Show a notification for extensions.\n\t */\n\tprivate showExtensionNotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n\t\tif (type === \"error\") {\n\t\t\tthis.showError(message);\n\t\t} else if (type === \"warning\") {\n\t\t\tthis.showWarning(message);\n\t\t} else {\n\t\t\tthis.showStatus(message);\n\t\t}\n\t}\n\n\t/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */\n\tprivate async showExtensionCustom<T>(\n\t\tfactory: (\n\t\t\ttui: TUI,\n\t\t\ttheme: Theme,\n\t\t\tkeybindings: KeybindingsManager,\n\t\t\tdone: (result: T) => void,\n\t\t) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n\t\toptions?: {\n\t\t\toverlay?: boolean;\n\t\t\toverlayOptions?: OverlayOptions | (() => OverlayOptions);\n\t\t\tonHandle?: (handle: OverlayHandle) => void;\n\t\t},\n\t): Promise<T> {\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\tconst savedText = this.editor.getText();\n\t\tconst isOverlay = options?.overlay ?? false;\n\n\t\tlet component: Component & { dispose?(): void };\n\n\t\tconst restoreEditor = () => {\n\t\t\tthis.restoreCanonicalEditor({ text: savedText, owner: component });\n\t\t};\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet closed = false;\n\n\t\t\tconst close = (result: T) => {\n\t\t\t\tif (closed) return;\n\t\t\t\tclosed = true;\n\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t// We're stale, just resolve the promise to avoid leaks, but don't mutate UI\n\t\t\t\t\tresolve(result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (isOverlay) this.ui.hideOverlay();\n\t\t\t\telse restoreEditor();\n\t\t\t\t// Note: both branches above already call requestRender\n\t\t\t\tresolve(result);\n\t\t\t\ttry {\n\t\t\t\t\tcomponent?.dispose?.();\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore dispose errors */\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tPromise.resolve(factory(this.ui, theme, this.keybindings, close))\n\t\t\t\t.then((c) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tc?.dispose?.();\n\t\t\t\t\t\t} catch {}\n\t\t\t\t\t\treturn; // Do not mount if epoch changed\n\t\t\t\t\t}\n\n\t\t\t\t\tcomponent = c;\n\t\t\t\t\tif (isOverlay) {\n\t\t\t\t\t\t// Resolve overlay options - can be static or dynamic function\n\t\t\t\t\t\tconst resolveOptions = (): OverlayOptions | undefined => {\n\t\t\t\t\t\t\tif (options?.overlayOptions) {\n\t\t\t\t\t\t\t\tconst opts =\n\t\t\t\t\t\t\t\t\ttypeof options.overlayOptions === \"function\"\n\t\t\t\t\t\t\t\t\t\t? options.overlayOptions()\n\t\t\t\t\t\t\t\t\t\t: options.overlayOptions;\n\t\t\t\t\t\t\t\treturn opts;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Fallback: use component's width property if available\n\t\t\t\t\t\t\tconst w = (component as { width?: number }).width;\n\t\t\t\t\t\t\treturn w ? { width: w } : undefined;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst handle = this.ui.showOverlay(component, resolveOptions());\n\t\t\t\t\t\t// Expose handle to caller for visibility control\n\t\t\t\t\t\toptions?.onHandle?.(handle);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.mountPromptOwner(component, component);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tif (closed) return;\n\t\t\t\t\tif (!isOverlay) restoreEditor();\n\t\t\t\t\treject(err);\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Show an extension error in the UI.\n\t */\n\tprivate showExtensionError(extensionPath: string, error: string, stack?: string): void {\n\t\tconst errorMsg = `Extension \"${extensionPath}\" error: ${error}`;\n\t\tconst errorText = new Text(theme.fg(\"error\", errorMsg), 2, 0);\n\t\tthis.chatContainer.addChild(errorText);\n\t\tif (stack) {\n\t\t\t// Show stack trace in dim color, indented\n\t\t\tconst stackLines = stack\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.slice(1) // Skip first line (duplicates error message)\n\t\t\t\t.map((line) => theme.fg(\"dim\", `  ${line.trim()}`))\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (stackLines) {\n\t\t\t\tthis.chatContainer.addChild(new Text(stackLines, 2, 0));\n\t\t\t}\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\t// =========================================================================\n\t// Key Handlers\n\t// =========================================================================\n\n\tprivate setupKeyHandlers(): void {\n\t\t// Set up handlers on defaultEditor - they use this.editor for text access\n\t\t// so they work correctly regardless of which editor is active\n\t\tthis.defaultEditor.onEscape = () => {\n\t\t\tif (this.loadingAnimation) {\n\t\t\t\tthis.restoreQueuedMessagesToEditor({ abort: true });\n\t\t\t} else if (this.session.isBashRunning) {\n\t\t\t\tthis.session.abortBash();\n\t\t\t} else if (this.isBashMode) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tthis.isBashMode = false;\n\t\t\t\tthis.updatePromptChrome();\n\t\t\t} else if (!this.editor.getText().trim()) {\n\t\t\t\t// Double-escape with empty editor triggers /tree, /fork, or nothing based on setting\n\t\t\t\tconst action = this.settingsManager.getDoubleEscapeAction();\n\t\t\t\tif (action !== \"none\") {\n\t\t\t\t\tconst now = Date.now();\n\t\t\t\t\tif (now - this.lastEscapeTime < 500) {\n\t\t\t\t\t\tif (action === \"tree\") {\n\t\t\t\t\t\t\tthis.showTreeSelector();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.showUserMessageSelector();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.lastEscapeTime = 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.lastEscapeTime = now;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t// Register app action handlers\n\t\tthis.defaultEditor.onAction(\"clear\", () => this.handleCtrlC());\n\t\tthis.defaultEditor.onCtrlD = () => this.handleCtrlD();\n\t\tthis.defaultEditor.onAction(\"suspend\", () => this.handleCtrlZ());\n\t\tthis.defaultEditor.onAction(\"cycleThinkingLevel\", () => this.cycleThinkingLevel());\n\t\tthis.defaultEditor.onAction(\"cycleModelForward\", () => this.cycleModel(\"forward\"));\n\t\tthis.defaultEditor.onAction(\"cycleModelBackward\", () => this.cycleModel(\"backward\"));\n\n\t\t// Global debug handler on TUI (works regardless of focus)\n\t\tthis.ui.onDebug = () => this.handleDebugCommand();\n\t\tthis.defaultEditor.onAction(\"selectModel\", () => this.showModelSelector());\n\t\tthis.defaultEditor.onAction(\"expandTools\", () => this.toggleToolOutputExpansion());\n\t\tthis.defaultEditor.onAction(\"toggleThinking\", () => this.toggleThinkingBlockVisibility());\n\t\tthis.defaultEditor.onAction(\"externalEditor\", () => this.openExternalEditor());\n\t\tthis.defaultEditor.onAction(\"followUp\", () => this.handleFollowUp());\n\t\tthis.defaultEditor.onAction(\"dequeue\", () => this.handleDequeue());\n\t\tthis.defaultEditor.onAction(\"newSession\", () => this.handleClearCommand());\n\t\tthis.defaultEditor.onAction(\"tree\", () => this.showTreeSelector());\n\t\tthis.defaultEditor.onAction(\"fork\", () => this.showUserMessageSelector());\n\t\tthis.defaultEditor.onAction(\"resume\", () => this.showSessionSelector());\n\n\t\tthis.defaultEditor.onChange = (text: string) => {\n\t\t\tconst wasBashMode = this.isBashMode;\n\t\t\tthis.isBashMode = text.trimStart().startsWith(\"!\");\n\t\t\tif (wasBashMode !== this.isBashMode) {\n\t\t\t\tthis.updatePromptChrome();\n\t\t\t}\n\t\t};\n\n\t\t// Handle clipboard image paste (triggered on Ctrl+V)\n\t\tthis.defaultEditor.onPasteImage = () => {\n\t\t\tthis.handleClipboardImagePaste();\n\t\t};\n\t}\n\n\tprivate async handleClipboardImagePaste(): Promise<void> {\n\t\ttry {\n\t\t\tconst image = await readClipboardImage();\n\t\t\tif (!image) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Write to temp file\n\t\t\tconst tmpDir = os.tmpdir();\n\t\t\tconst ext = extensionForImageMimeType(image.mimeType) ?? \"png\";\n\t\t\tconst fileName = `${FILE_PREFIX}-clipboard-${crypto.randomUUID()}.${ext}`;\n\t\t\tconst filePath = path.join(tmpDir, fileName);\n\t\t\tfs.writeFileSync(filePath, Buffer.from(image.bytes));\n\n\t\t\t// Insert file path directly\n\t\t\tthis.editor.insertTextAtCursor?.(filePath);\n\t\t\tthis.ui.requestRender();\n\t\t} catch {\n\t\t\t// Silently ignore clipboard errors (may not have permission, etc.)\n\t\t}\n\t}\n\n\tprivate setupEditorSubmitHandler(): void {\n\t\tthis.defaultEditor.onSubmit = async (text: string) => {\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\n\t\t\t// Handle commands\n\t\t\tif (text === \"/settings\") {\n\t\t\t\tthis.showSettingsSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/scoped-models\") {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.showModelsSelector();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/model\" || text.startsWith(\"/model \")) {\n\t\t\t\tconst searchTerm = text.startsWith(\"/model \") ? text.slice(7).trim() : undefined;\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleModelCommand(searchTerm);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text.startsWith(\"/export\")) {\n\t\t\t\tawait this.handleExportCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/share\") {\n\t\t\t\tawait this.handleShareCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/copy\") {\n\t\t\t\tthis.handleCopyCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/name\" || text.startsWith(\"/name \")) {\n\t\t\t\tthis.handleNameCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/session\") {\n\t\t\t\tthis.handleSessionCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/changelog\") {\n\t\t\t\tthis.handleChangelogCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/hotkeys\") {\n\t\t\t\tthis.handleHotkeysCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/fork\") {\n\t\t\t\tthis.showUserMessageSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/tree\") {\n\t\t\t\tthis.showTreeSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/login\") {\n\t\t\t\tthis.showOAuthSelector(\"login\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/logout\") {\n\t\t\t\tthis.showOAuthSelector(\"logout\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/new\" || text === \"/clear\") {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleClearCommand(text === \"/clear\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/compact\" || text.startsWith(\"/compact \")) {\n\t\t\t\tconst customInstructions = text.startsWith(\"/compact \") ? text.slice(9).trim() : undefined;\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleCompactCommand(customInstructions);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/reload\") {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleReloadCommand();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/init-project\" || text.startsWith(\"/init-project \")) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tconst parts = text.trim().split(/\\s+/);\n\t\t\t\tconst includeProtocol = parts[1] === \"--protocol\";\n\t\t\t\tif (parts.length > 2 || (parts[1] !== undefined && !includeProtocol)) {\n\t\t\t\t\tthis.showWarning(\"Usage: /init-project [--protocol]\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.handleInitProjectCommand(includeProtocol);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/protocol-status\") {\n\t\t\t\tthis.handleProtocolStatusCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/debug\") {\n\t\t\t\tthis.handleDebugCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/arminsayshi\") {\n\t\t\t\tthis.handleArminSaysHi();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/resume\") {\n\t\t\t\tthis.showSessionSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text === \"/quit\" || text === \"/exit\") {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.shutdown();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/help\") {\n\t\t\t\tthis.handleHelpCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/brief\" || text.startsWith(\"/brief \")) {\n\t\t\t\tthis.handleBriefCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Detect natural language caveman activation/deactivation phrases\n\t\t\tconst cavemanAction = parseCavemanNaturalLanguage(text);\n\t\t\tif (cavemanAction) {\n\t\t\t\tthis.session.setCavemanLevel(cavemanAction);\n\t\t\t\t// DO NOT announce activation/deactivation - continue to normal processing silently\n\t\t\t}\n\n\t\t\tif (text === \"/caveman\" || text.startsWith(\"/caveman \")) {\n\t\t\t\tthis.handleCavemanCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/btw\" || text.startsWith(\"/btw \")) {\n\t\t\t\tthis.handleBtwCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/steer\" || text.startsWith(\"/steer \")) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleSteerCommand(text);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Handle /memory commands\n\t\t\tif (text === \"/memory\" || text.startsWith(\"/memory \")) {\n\t\t\t\tthis.handleMemoryCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/ultraplan\" || text.startsWith(\"/ultraplan \")) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleUltraplanCommand(text);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (text === \"/websearch\" || text.startsWith(\"/websearch \")) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.handleWebsearchCommand(text);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Handle bash command (! for normal, !! for excluded from context)\n\t\t\tif (text.startsWith(\"!\")) {\n\t\t\t\tconst isExcluded = text.startsWith(\"!!\");\n\t\t\t\tconst command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();\n\t\t\t\tif (command) {\n\t\t\t\t\tif (this.session.isBashRunning) {\n\t\t\t\t\t\tthis.showWarning(\"A bash command is already running. Press Esc to cancel it first.\");\n\t\t\t\t\t\tthis.editor.setText(text);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tawait this.handleBashCommand(command, isExcluded);\n\t\t\t\t\tthis.isBashMode = false;\n\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Queue input during compaction (extension commands execute immediately)\n\t\t\tif (this.session.isCompacting) {\n\t\t\t\tif (this.isExtensionCommand(text)) {\n\t\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tawait this.session.prompt(text);\n\t\t\t\t} else {\n\t\t\t\t\tthis.queueCompactionMessage(text, \"steer\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If streaming, use prompt() with steer behavior\n\t\t\t// This handles extension commands (execute immediately), prompt template expansion, and queueing\n\t\t\tif (this.session.isStreaming) {\n\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.session.prompt(text, { streamingBehavior: \"steer\" });\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Normal message submission\n\t\t\t// First, move any pending bash components to chat\n\t\t\tthis.flushPendingBashComponents();\n\n\t\t\tif (this.onInputCallback) {\n\t\t\t\tthis.onInputCallback(text);\n\t\t\t}\n\t\t\tthis.editor.addToHistory?.(text);\n\t\t};\n\t}\n\n\tprivate subscribeToAgent(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = this.session.subscribe(async (event) => {\n\t\t\tawait this.handleEvent(event);\n\t\t});\n\t}\n\n\tprivate async handleEvent(event: AgentSessionEvent): Promise<void> {\n\t\tconst capturedEpoch = this.sessionEpoch;\n\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\tif (this.sessionEpoch !== capturedEpoch) {\n\t\t\treturn; // Stale event from previous session/epoch\n\t\t}\n\n\t\tthis.footer.invalidate();\n\n\t\tswitch (event.type) {\n\t\t\tcase \"todo_update\":\n\t\t\tcase \"memory_update\":\n\t\t\tcase \"task_update\":\n\t\t\t\tthis.updateSidebarTodoPanel();\n\t\t\t\tthis.updateSidebarTaskPanel();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\tcase \"agent_start\":\n\t\t\t\t// Start terminal title spinner\n\t\t\t\tthis.terminalTitleController?.setWorking(true);\n\n\t\t\t\t// Restore main escape handler if retry handler is still active\n\t\t\t\t// (retry success event fires later, but we need main handler now)\n\t\t\t\tif (this.retryEscapeHandler) {\n\t\t\t\t\tthis.defaultEditor.onEscape = this.retryEscapeHandler;\n\t\t\t\t\tthis.retryEscapeHandler = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.dispose();\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t}\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.dispose();\n\t\t\t\t\tconst oldLoader = this.loadingAnimation;\n\t\t\t\t\tthis.loadingAnimation = undefined;\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: oldLoader });\n\t\t\t\t} else {\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false });\n\t\t\t\t}\n\t\t\t\tthis.loadingAnimation = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\tthis.defaultWorkingMessage,\n\t\t\t\t);\n\t\t\t\tthis.mountStatusLoader(this.loadingAnimation, { requestRender: false });\n\t\t\t\t// Apply any pending working message queued before loader existed\n\t\t\t\tif (this.pendingWorkingMessage !== undefined) {\n\t\t\t\t\tif (this.pendingWorkingMessage) {\n\t\t\t\t\t\tthis.loadingAnimation.setMessage(this.pendingWorkingMessage);\n\t\t\t\t\t}\n\t\t\t\t\tthis.pendingWorkingMessage = undefined;\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_start\":\n\t\t\t\tif (event.message.role === \"custom\") {\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tif (this.loadingAnimation || this.statusContainer.children.length > 0) {\n\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"user\") {\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t\tif (this.loadingAnimation || this.statusContainer.children.length > 0) {\n\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\t// Check if we already have a streaming component for this message to prevent duplicates\n\t\t\t\t\tif (this.streamingComponent && this.streamingMessage === event.message) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// If we have a different streaming component, something is wrong, but let's at least not leak it\n\t\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add spacing before new assistant message\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\n\t\t\t\t\tthis.streamingComponent = new AssistantMessageComponent(\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tthis.hideThinkingBlock,\n\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\tthis.session.briefOnly,\n\t\t\t\t\t);\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\n\t\t\t\t\tfor (const content of this.streamingMessage.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\tif (!this.pendingTools.has(content.id)) {\n\t\t\t\t\t\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\t\t\t\t\t\tcontent.name,\n\t\t\t\t\t\t\t\t\tcontent.arguments,\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tthis.getRegisteredToolDefinition(content.name),\n\t\t\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\t\t\t// Add spacing before tool execution\n\t\t\t\t\t\t\t\tconst lastChild =\n\t\t\t\t\t\t\t\t\tthis.chatContainer.children.length > 0\n\t\t\t\t\t\t\t\t\t\t? this.chatContainer.children[this.chatContainer.children.length - 1]\n\t\t\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t!lastChild ||\n\t\t\t\t\t\t\t\t\t(!(lastChild instanceof Spacer) && !(lastChild instanceof ToolExecutionComponent))\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tconst component = this.pendingTools.get(content.id);\n\t\t\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\t\t\tcomponent.updateArgs(content.arguments);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\tif (event.message.role === \"user\") break;\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tthis.streamingMessage = event.message;\n\t\t\t\t\tlet errorMessage: string | undefined;\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\") {\n\t\t\t\t\t\tconst retryAttempt = this.session.retryAttempt;\n\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}`\n\t\t\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\t\t\tthis.streamingMessage.errorMessage = errorMessage;\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\n\t\t\t\t\tif (this.streamingMessage.stopReason === \"aborted\" || this.streamingMessage.stopReason === \"error\") {\n\t\t\t\t\t\tif (!errorMessage) {\n\t\t\t\t\t\t\terrorMessage = this.streamingMessage.errorMessage || \"Error\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Args are now complete - trigger diff computation for edit tools\n\t\t\t\t\t\tfor (const [, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.setArgsComplete();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\tthis.updateSidebarTodoPanel();\n\t\t\t\tthis.updateSidebarTaskPanel();\n\t\t\t\tif (!this.pendingTools.has(event.toolCallId)) {\n\t\t\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\t\t\tevent.toolName,\n\t\t\t\t\t\tevent.args,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tthis.getRegisteredToolDefinition(event.toolName),\n\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t);\n\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t// Add spacing before tool execution\n\t\t\t\t\tconst lastChild =\n\t\t\t\t\t\tthis.chatContainer.children.length > 0\n\t\t\t\t\t\t\t? this.chatContainer.children[this.chatContainer.children.length - 1]\n\t\t\t\t\t\t\t: null;\n\t\t\t\t\tif (!lastChild || (!(lastChild instanceof Spacer) && !(lastChild instanceof ToolExecutionComponent))) {\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\tthis.pendingTools.set(event.toolCallId, component);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_update\": {\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.partialResult, isError: false }, true);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\tthis.updateSidebarTodoPanel();\n\t\t\t\tthis.updateSidebarTaskPanel();\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({ ...event.result, isError: event.isError });\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\":\n\t\t\t\t// Stop terminal title spinner\n\t\t\t\tthis.terminalTitleController?.setWorking(false);\n\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.dispose();\n\t\t\t\t\tconst oldLoader = this.loadingAnimation;\n\t\t\t\t\tthis.loadingAnimation = undefined;\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: oldLoader });\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent = undefined;\n\t\t\t\t\tthis.streamingMessage = undefined;\n\t\t\t\t}\n\t\t\t\tthis.pendingTools.clear();\n\n\t\t\t\tawait this.checkShutdownRequested();\n\n\t\t\t\tif (this.startupUiGateActive) {\n\t\t\t\t\tthis.startupUiGateActive = false;\n\t\t\t\t\tthis.flushDeferredStartupNotices();\n\t\t\t\t} else {\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"auto_compaction_start\": {\n\t\t\t\t// Keep editor active; submissions are queued during compaction.\n\t\t\t\t// Set up escape to abort auto-compaction\n\t\t\t\tthis.autoCompactionEscapeHandler = this.defaultEditor.onEscape;\n\t\t\t\tthis.defaultEditor.onEscape = () => {\n\t\t\t\t\tthis.session.abortCompaction();\n\t\t\t\t};\n\n\t\t\t\t// Stop loading animation\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.dispose();\n\t\t\t\t\tthis.loadingAnimation = undefined;\n\t\t\t\t}\n\t\t\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t\t\t// Show compacting indicator with reason\n\t\t\t\tconst reasonText = event.reason === \"overflow\" ? \"Context overflow detected, \" : \"\";\n\t\t\t\tthis.autoCompactionLoader = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t`${reasonText}Auto-compacting... (${appKey(this.keybindings, \"interrupt\")} to cancel)`,\n\t\t\t\t);\n\t\t\t\tthis.mountStatusLoader(this.autoCompactionLoader, { requestRender: false });\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_compaction_end\": {\n\t\t\t\t// Restore escape handler\n\t\t\t\tif (this.autoCompactionEscapeHandler) {\n\t\t\t\t\tthis.defaultEditor.onEscape = this.autoCompactionEscapeHandler;\n\t\t\t\t\tthis.autoCompactionEscapeHandler = undefined;\n\t\t\t\t}\n\t\t\t\t// Stop loader\n\t\t\t\tif (this.autoCompactionLoader) {\n\t\t\t\t\tthis.autoCompactionLoader.dispose();\n\t\t\t\t\tconst oldLoader = this.autoCompactionLoader;\n\t\t\t\t\tthis.autoCompactionLoader = undefined;\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: oldLoader });\n\t\t\t\t}\n\t\t\t\t// Handle result\n\t\t\t\tif (event.aborted) {\n\t\t\t\t\tthis.showStatus(\"Auto-compaction cancelled\");\n\t\t\t\t} else if (event.result) {\n\t\t\t\t\t// Rebuild chat to show compacted state\n\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\tthis.rebuildChatFromMessages();\n\t\t\t\t\t// Add compaction component at bottom so user sees it without scrolling\n\t\t\t\t\tthis.addMessageToChat({\n\t\t\t\t\t\trole: \"compactionSummary\",\n\t\t\t\t\t\ttokensBefore: event.result.tokensBefore,\n\t\t\t\t\t\tsummary: event.result.summary,\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t});\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t} else if (event.errorMessage) {\n\t\t\t\t\t// Compaction failed (e.g., quota exceeded, API error)\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", event.errorMessage), 2, 0));\n\t\t\t\t}\n\t\t\t\tvoid this.flushCompactionQueue({ willRetry: event.willRetry });\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_start\": {\n\t\t\t\t// Set up escape to abort retry\n\t\t\t\tthis.retryEscapeHandler = this.defaultEditor.onEscape;\n\t\t\t\tthis.defaultEditor.onEscape = () => {\n\t\t\t\t\tthis.session.abortRetry();\n\t\t\t\t};\n\n\t\t\t\t// Stop loading animation\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.dispose();\n\t\t\t\t\tthis.loadingAnimation = undefined;\n\t\t\t\t}\n\t\t\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t\t\t// Show retry indicator\n\t\t\t\tconst delaySeconds = Math.round(event.delayMs / 1000);\n\t\t\t\tthis.retryLoader = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"warning\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t`Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, \"interrupt\")} to cancel)`,\n\t\t\t\t);\n\t\t\t\tthis.mountStatusLoader(this.retryLoader, { requestRender: false });\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_retry_end\": {\n\t\t\t\t// Restore escape handler\n\t\t\t\tif (this.retryEscapeHandler) {\n\t\t\t\t\tthis.defaultEditor.onEscape = this.retryEscapeHandler;\n\t\t\t\t\tthis.retryEscapeHandler = undefined;\n\t\t\t\t}\n\t\t\t\t// Stop loader\n\t\t\t\tif (this.retryLoader) {\n\t\t\t\t\tthis.retryLoader.dispose();\n\t\t\t\t\tconst oldLoader = this.retryLoader;\n\t\t\t\t\tthis.retryLoader = undefined;\n\t\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: oldLoader });\n\t\t\t\t}\n\t\t\t\t// Show error only on final failure (success shows normal response)\n\t\t\t\tif (!event.success) {\n\t\t\t\t\tthis.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || \"Unknown error\"}`);\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"auto_fallback\": {\n\t\t\t\tthis.showStatus(`Fallback to ${event.fallbackModel.id} (${event.reason})`);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"notification\":\n\t\t\t\t// operator_discipline_advisory: shown as informational status\n\t\t\t\tif (event.kind === \"operator_discipline_advisory\") {\n\t\t\t\t\tthis.showStatus(event.message);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Extract text content from a user message */\n\tprivate getUserMessageText(message: Message): string {\n\t\tif (message.role !== \"user\") return \"\";\n\t\tconst textBlocks =\n\t\t\ttypeof message.content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: message.content }]\n\t\t\t\t: message.content.filter((c: { type: string }) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { text: string }).text).join(\"\");\n\t}\n\n\t/**\n\t * Show a status message in the chat.\n\t *\n\t * If multiple status messages are emitted back-to-back (without anything else being added to the chat),\n\t * we update the previous status line instead of appending new ones to avoid log spam.\n\t */\n\tprivate showStatus(message: string, ownerEpoch?: number): void {\n\t\tif (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch) return;\n\n\t\tconst children = this.chatContainer.children;\n\t\tconst last = children.length > 0 ? children[children.length - 1] : undefined;\n\t\tconst secondLast = children.length > 1 ? children[children.length - 2] : undefined;\n\n\t\tif (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {\n\t\t\tthis.lastStatusText.setText(theme.fg(\"dim\", message));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst spacer = new Spacer(1);\n\t\tconst text = new Text(theme.fg(\"dim\", message), 2, 0);\n\t\tthis.chatContainer.addChild(spacer);\n\t\tthis.chatContainer.addChild(text);\n\t\tthis.lastStatusSpacer = spacer;\n\t\tthis.lastStatusText = text;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {\n\t\t// Add consistent spacing before each message, unless previous component already provides spacing\n\t\tconst lastChild =\n\t\t\tthis.chatContainer.children.length > 0\n\t\t\t\t? this.chatContainer.children[this.chatContainer.children.length - 1]\n\t\t\t\t: null;\n\t\tif (!lastChild || (!(lastChild instanceof Spacer) && !(lastChild instanceof ToolExecutionComponent))) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tswitch (message.role) {\n\t\t\tcase \"bashExecution\": {\n\t\t\t\tconst component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);\n\t\t\t\tif (message.output) {\n\t\t\t\t\tcomponent.appendOutput(message.output);\n\t\t\t\t}\n\t\t\t\tcomponent.setComplete(\n\t\t\t\t\tmessage.exitCode,\n\t\t\t\t\tmessage.cancelled,\n\t\t\t\t\tmessage.truncated ? ({ truncated: true } as TruncationResult) : undefined,\n\t\t\t\t\tmessage.fullOutputPath,\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"custom\": {\n\t\t\t\tif (message.display) {\n\t\t\t\t\tconst renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);\n\t\t\t\t\tconst component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());\n\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"compactionSummary\": {\n\t\t\t\tconst component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"branchSummary\": {\n\t\t\t\tconst component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());\n\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\": {\n\t\t\t\tconst textContent = this.getUserMessageText(message);\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst skillBlock = parseSkillBlock(textContent);\n\t\t\t\t\tif (skillBlock) {\n\t\t\t\t\t\t// Render skill block (collapsible)\n\t\t\t\t\t\tconst component = new SkillInvocationMessageComponent(\n\t\t\t\t\t\t\tskillBlock,\n\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\t// Render user message separately if present\n\t\t\t\t\t\tif (skillBlock.userMessage) {\n\t\t\t\t\t\t\tconst userComponent = new UserMessageComponent(\n\t\t\t\t\t\t\t\tskillBlock.userMessage,\n\t\t\t\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());\n\t\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\t}\n\t\t\t\t\tif (options?.populateHistory) {\n\t\t\t\t\t\tthis.editor.addToHistory?.(textContent);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"assistant\": {\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(\n\t\t\t\t\tmessage,\n\t\t\t\t\tthis.hideThinkingBlock,\n\t\t\t\t\tthis.getMarkdownThemeWithSettings(),\n\t\t\t\t\tthis.session.briefOnly,\n\t\t\t\t);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"toolResult\": {\n\t\t\t\t// Tool results are rendered inline with tool calls, handled separately\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"memoryContext\": {\n\t\t\t\t// Model-facing only; intentionally hidden from chat transcript UI\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = message;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Render session context to chat. Used for initial load and rebuild after compaction.\n\t * @param sessionContext Session context to render\n\t * @param options.updateFooter Update footer state\n\t * @param options.populateHistory Add user messages to editor history\n\t */\n\tprivate renderSessionContext(\n\t\tsessionContext: SessionContext,\n\t\toptions: { updateFooter?: boolean; populateHistory?: boolean } = {},\n\t): void {\n\t\tthis.pendingTools.clear();\n\n\t\tif (options.updateFooter) {\n\t\t\tthis.footer.invalidate();\n\t\t\tthis.updatePromptChrome();\n\t\t}\n\n\t\tfor (const message of sessionContext.messages) {\n\t\t\t// Assistant messages need special handling for tool calls\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tthis.addMessageToChat(message);\n\t\t\t\t// Render tool call components\n\t\t\t\tfor (const content of message.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\t\t\t\tcontent.name,\n\t\t\t\t\t\t\tcontent.arguments,\n\t\t\t\t\t\t\t{ showImages: this.settingsManager.getShowImages() },\n\t\t\t\t\t\t\tthis.getRegisteredToolDefinition(content.name),\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcomponent.setExpanded(this.toolOutputExpanded);\n\t\t\t\t\t\t// Add spacing before tool execution\n\t\t\t\t\t\tconst lastChild =\n\t\t\t\t\t\t\tthis.chatContainer.children.length > 0\n\t\t\t\t\t\t\t\t? this.chatContainer.children[this.chatContainer.children.length - 1]\n\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t!lastChild ||\n\t\t\t\t\t\t\t(!(lastChild instanceof Spacer) && !(lastChild instanceof ToolExecutionComponent))\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\n\t\t\t\t\t\tif (message.stopReason === \"aborted\" || message.stopReason === \"error\") {\n\t\t\t\t\t\t\tlet errorMessage: string;\n\t\t\t\t\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\t\t\t\t\tconst retryAttempt = this.session.retryAttempt;\n\t\t\t\t\t\t\t\terrorMessage =\n\t\t\t\t\t\t\t\t\tretryAttempt > 0\n\t\t\t\t\t\t\t\t\t\t? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? \"s\" : \"\"}`\n\t\t\t\t\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\terrorMessage = message.errorMessage || \"Error\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcomponent.updateResult({ content: [{ type: \"text\", text: errorMessage }], isError: true });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Match tool results to pending tool components\n\t\t\t\tconst component = this.pendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult(message);\n\t\t\t\t\tthis.pendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// All other messages use standard rendering\n\t\t\t\tthis.addMessageToChat(message, options);\n\t\t\t}\n\t\t}\n\n\t\tthis.pendingTools.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\trenderInitialMessages(): void {\n\t\tif (this.isInitialMessagesRendered) return;\n\t\tthis.isInitialMessagesRendered = true;\n\n\t\t// Get aligned messages and entries from session context\n\t\tconst context = this.sessionManager.buildSessionContext();\n\t\tthis.renderSessionContext(context, {\n\t\t\tupdateFooter: true,\n\t\t\tpopulateHistory: true,\n\t\t});\n\n\t\t// Show compaction info if session was compacted\n\t\tconst allEntries = this.sessionManager.getEntries();\n\t\tconst compactionCount = allEntries.filter((e) => e.type === \"compaction\").length;\n\t\tif (compactionCount > 0) {\n\t\t\tconst times = compactionCount === 1 ? \"1 time\" : `${compactionCount} times`;\n\t\t\tthis.showStatus(`Session compacted ${times}`);\n\t\t}\n\t}\n\n\tasync getUserInput(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate clearChatContainer(): void {\n\t\tthis.chatContainer.clear();\n\t\tthis.helpContainer = undefined;\n\t\tthis.lastStatusSpacer = undefined;\n\t\tthis.lastStatusText = undefined;\n\t}\n\n\tprivate rebuildChatFromMessages(): void {\n\t\tthis.clearChatContainer();\n\t\tconst context = this.sessionManager.buildSessionContext();\n\t\tthis.renderSessionContext(context);\n\t}\n\n\t// =========================================================================\n\t// Key handlers\n\t// =========================================================================\n\n\tprivate handleCtrlC(): void {\n\t\tconst now = Date.now();\n\t\tif (now - this.lastSigintTime < 500) {\n\t\t\tvoid this.shutdown();\n\t\t} else {\n\t\t\tthis.clearEditor();\n\t\t\tthis.lastSigintTime = now;\n\t\t}\n\t}\n\n\tprivate handleCtrlD(): void {\n\t\t// Only called when editor is empty (enforced by CustomEditor)\n\t\tvoid this.shutdown();\n\t}\n\n\t/**\n\t * Gracefully shutdown the agent.\n\t * Emits shutdown event to extensions, then exits.\n\t */\n\tprivate isShuttingDown = false;\n\n\tprivate async shutdown(): Promise<void> {\n\t\tif (this.isShuttingDown) return;\n\t\tthis.isShuttingDown = true;\n\n\t\t// Emit shutdown event to extensions\n\t\tconst extensionRunner = this.session.extensionRunner;\n\t\tif (extensionRunner?.hasHandlers(\"session_shutdown\")) {\n\t\t\tawait extensionRunner.emit({\n\t\t\t\ttype: \"session_shutdown\",\n\t\t\t});\n\t\t}\n\n\t\t// Wait for any pending renders to complete\n\t\t// requestRender() uses process.nextTick(), so we wait one tick\n\t\tawait new Promise((resolve) => process.nextTick(resolve));\n\n\t\t// Drain any in-flight Kitty key release events before stopping.\n\t\t// This prevents escape sequences from leaking to the parent shell over slow SSH.\n\t\tawait this.ui.terminal.drainInput(1000);\n\n\t\tthis.stop();\n\t\tprocess.exit(0);\n\t}\n\n\t/**\n\t * Check if shutdown was requested and perform shutdown if so.\n\t */\n\tprivate async checkShutdownRequested(): Promise<void> {\n\t\tif (!this.shutdownRequested) return;\n\t\tawait this.shutdown();\n\t}\n\n\tprivate handleCtrlZ(): void {\n\t\t// Ignore SIGINT while suspended so Ctrl+C in the terminal does not\n\t\t// kill the backgrounded process. The handler is removed on resume.\n\t\tconst ignoreSigint = () => {};\n\t\tprocess.on(\"SIGINT\", ignoreSigint);\n\n\t\t// Set up handler to restore TUI when resumed\n\t\tprocess.once(\"SIGCONT\", () => {\n\t\t\tprocess.removeListener(\"SIGINT\", ignoreSigint);\n\t\t\tthis.ui.start();\n\t\t\tthis.ui.requestRender(true);\n\t\t});\n\n\t\t// Stop the TUI (restore terminal to normal mode)\n\t\tthis.ui.stop();\n\n\t\t// Send SIGTSTP to process group (pid=0 means all processes in group)\n\t\tprocess.kill(0, \"SIGTSTP\");\n\t}\n\n\tprivate async handleFollowUp(): Promise<void> {\n\t\tconst text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();\n\t\tif (!text) return;\n\n\t\tconst capturedEpoch = this.sessionEpoch;\n\n\t\t// Queue input during compaction (extension commands execute immediately)\n\t\tif (this.session.isCompacting) {\n\t\t\tif (this.isExtensionCommand(text)) {\n\t\t\t\tthis.editor.addToHistory?.(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tawait this.session.prompt(text);\n\t\t\t} else {\n\t\t\t\tthis.queueCompactionMessage(text, \"followUp\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Alt+Enter queues a follow-up message (waits until agent finishes)\n\t\t// This handles extension commands (execute immediately), prompt template expansion, and queueing\n\t\tif (this.session.isStreaming) {\n\t\t\tthis.editor.addToHistory?.(text);\n\t\t\tthis.editor.setText(\"\");\n\t\t\tawait this.session.prompt(text, { streamingBehavior: \"followUp\" });\n\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t\t// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)\n\t\telse if (this.editor.onSubmit) {\n\t\t\tthis.editor.onSubmit(text);\n\t\t}\n\t}\n\n\tprivate handleDequeue(): void {\n\t\tconst restored = this.restoreQueuedMessagesToEditor();\n\t\tif (restored === 0) {\n\t\t\tthis.showStatus(\"No queued messages to restore\");\n\t\t} else {\n\t\t\tthis.showStatus(`Restored ${restored} queued message${restored > 1 ? \"s\" : \"\"} to editor`);\n\t\t}\n\t}\n\n\tprivate getPromptGlyph(): string {\n\t\treturn this.isBashMode ? \"!\" : \"❯\";\n\t}\n\n\tprivate getPromptPlaceholderText(): string {\n\t\treturn this.isBashMode ? \"Run a bash command...\" : \" Type your message, @file, or /command\";\n\t}\n\n\tprivate getPromptGlyphColor(): (str: string) => string {\n\t\treturn this.isPromptActive() ? this.getActivePromptBorderColor() : theme.getInactivePromptBorderColor();\n\t}\n\n\tprivate applyPromptChrome(editor: EditorComponent): void {\n\t\tconst promptEditor = editor as PromptChromeEditor;\n\t\tpromptEditor.borderColor = this.isPromptActive()\n\t\t\t? this.getActivePromptBorderColor()\n\t\t\t: theme.getInactivePromptBorderColor();\n\t\tpromptEditor.setPromptGlyph?.(this.getPromptGlyph());\n\t\tpromptEditor.setPlaceholderText?.(this.getPromptPlaceholderText());\n\t}\n\n\tprivate getActivePromptBorderColor(): (str: string) => string {\n\t\tif (this.isBashMode) {\n\t\t\treturn theme.getBashModeBorderColor();\n\t\t}\n\n\t\tconst level = this.session.thinkingLevel || \"off\";\n\t\treturn theme.getThinkingBorderColor(level);\n\t}\n\n\tprivate isPromptActive(): boolean {\n\t\treturn this.ui.isWindowFocused() && this.ui.getFocusedComponent() === (this.editor as Component);\n\t}\n\n\tprivate updatePromptChrome(): void {\n\t\tthis.applyPromptChrome(this.defaultEditor);\n\t\tif (this.editor !== this.defaultEditor) {\n\t\t\tthis.applyPromptChrome(this.editor);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate cycleThinkingLevel(): void {\n\t\tconst newLevel = this.session.cycleThinkingLevel();\n\t\tif (newLevel === undefined) {\n\t\t\tthis.showStatus(\"Current model does not support thinking\");\n\t\t} else {\n\t\t\tthis.footer.invalidate();\n\t\t\tthis.updatePromptChrome();\n\t\t\t// this.showStatus(`Thinking level: ${newLevel}`);\n\t\t}\n\t}\n\n\tprivate async cycleModel(direction: \"forward\" | \"backward\"): Promise<void> {\n\t\tconst capturedEpoch = this.sessionEpoch;\n\t\ttry {\n\t\t\tconst result = await this.session.cycleModel(direction);\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\tif (result === undefined) {\n\t\t\t\tconst msg = this.session.scopedModels.length > 0 ? \"Only one model in scope\" : \"Only one model available\";\n\t\t\t\tthis.showStatus(msg);\n\t\t\t} else {\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updatePromptChrome();\n\t\t\t\tconst thinkingStr =\n\t\t\t\t\tresult.model.reasoning && result.thinkingLevel !== \"off\" ? ` (thinking: ${result.thinkingLevel})` : \"\";\n\t\t\t\tthis.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate toggleToolOutputExpansion(): void {\n\t\tthis.setToolsExpanded(!this.toolOutputExpanded);\n\t}\n\n\tprivate setToolsExpanded(expanded: boolean): void {\n\t\tthis.toolOutputExpanded = expanded;\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (isExpandable(child)) {\n\t\t\t\tchild.setExpanded(expanded);\n\t\t\t}\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate toggleThinkingBlockVisibility(): void {\n\t\tthis.hideThinkingBlock = !this.hideThinkingBlock;\n\t\tthis.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);\n\n\t\t// Rebuild chat from session messages\n\t\tthis.clearChatContainer();\n\t\tthis.rebuildChatFromMessages();\n\n\t\t// If streaming, re-add the streaming component with updated visibility and re-render\n\t\tif (this.streamingComponent && this.streamingMessage) {\n\t\t\tthis.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);\n\t\t\tthis.streamingComponent.updateContent(this.streamingMessage);\n\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t}\n\n\t\tthis.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? \"hidden\" : \"visible\"}`);\n\t}\n\n\tprivate openExternalEditor(): void {\n\t\t// Determine editor (respect $VISUAL, then $EDITOR)\n\t\tconst editorCmd = process.env.VISUAL || process.env.EDITOR;\n\t\tif (!editorCmd) {\n\t\t\tthis.showWarning(\"No editor configured. Set $VISUAL or $EDITOR environment variable.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentText = this.editor.getExpandedText?.() ?? this.editor.getText();\n\t\tconst tmpFile = path.join(os.tmpdir(), `${FILE_PREFIX}-editor-${Date.now()}.md`);\n\n\t\ttry {\n\t\t\t// Write current content to temp file\n\t\t\tfs.writeFileSync(tmpFile, currentText, \"utf-8\");\n\n\t\t\t// Stop TUI to release terminal\n\t\t\tthis.ui.stop();\n\n\t\t\t// Split by space to support editor arguments (e.g., \"code --wait\")\n\t\t\tconst [editor, ...editorArgs] = editorCmd.split(\" \");\n\n\t\t\t// Spawn editor synchronously with inherited stdio for interactive editing\n\t\t\tconst result = spawnSync(editor, [...editorArgs, tmpFile], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\n\t\t\t// On successful exit (status 0), replace editor content\n\t\t\tif (result.status === 0) {\n\t\t\t\tconst newContent = fs.readFileSync(tmpFile, \"utf-8\").replace(/\\n$/, \"\");\n\t\t\t\tthis.editor.setText(newContent);\n\t\t\t}\n\t\t\t// On non-zero exit, keep original text (no action needed)\n\t\t} finally {\n\t\t\t// Clean up temp file\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\n\t\t\t// Restart TUI\n\t\t\tthis.ui.start();\n\t\t\t// Force full re-render since external editor uses alternate screen\n\t\t\tthis.ui.requestRender(true);\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// UI helpers\n\t// =========================================================================\n\n\tclearEditor(): void {\n\t\tthis.editor.setText(\"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string, ownerEpoch?: number): void {\n\t\tif (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch) return;\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMessage}`), 2, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate enqueueStartupNotice(notice: () => void): void {\n\t\tif (this.startupUiGateActive) {\n\t\t\tthis.deferredStartupNotices.push(notice);\n\t\t\treturn;\n\t\t}\n\n\t\tnotice();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate flushDeferredStartupNotices(): void {\n\t\tif (this.deferredStartupNotices.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst notices = [...this.deferredStartupNotices];\n\t\tthis.deferredStartupNotices = [];\n\n\t\tfor (const notice of notices) {\n\t\t\tnotice();\n\t\t}\n\n\t\tthis.ui.invalidate();\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowWarning(warningMessage: string, ownerEpoch?: number): void {\n\t\tif (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch) return;\n\t\tthis.enqueueStartupNotice(() => {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"warning\", `Warning: ${warningMessage}`), 2, 0));\n\t\t});\n\t}\n\n\tshowNewVersionNotification(newVersion: string, ownerEpoch?: number): void {\n\t\tif (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch) return;\n\t\tconst channel = this.settingsManager.getReleaseChannel();\n\t\tif (channel === undefined) return;\n\t\tconst action = theme.fg(\"accent\", getUpdateInstruction(PACKAGE_NAME, channel));\n\t\tconst updateInstruction = theme.fg(\"muted\", `New version ${newVersion} is available. `) + action;\n\t\tconst changelogUrl = theme.fg(\n\t\t\t\"accent\",\n\t\t\t\"https://github.com/apholdings/jensen-code/blob/main/packages/coding-agent/CHANGELOG.md\",\n\t\t);\n\t\tconst changelogLine = theme.fg(\"muted\", \"Changelog: \") + changelogUrl;\n\n\t\tthis.enqueueStartupNotice(() => {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.bold(theme.fg(\"warning\", \"Update Available\"))}\\n${updateInstruction}\\n${changelogLine}`,\n\t\t\t\t\t2,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.chatContainer.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t});\n\t}\n\n\t/**\n\t * Get all queued messages (read-only).\n\t * Combines session queue and compaction queue.\n\t */\n\tprivate getAllQueuedMessages(): { steering: string[]; followUp: string[] } {\n\t\treturn {\n\t\t\tsteering: [\n\t\t\t\t...this.session.getSteeringMessages(),\n\t\t\t\t...this.compactionQueuedMessages.filter((msg) => msg.mode === \"steer\").map((msg) => msg.text),\n\t\t\t],\n\t\t\tfollowUp: [\n\t\t\t\t...this.session.getFollowUpMessages(),\n\t\t\t\t...this.compactionQueuedMessages.filter((msg) => msg.mode === \"followUp\").map((msg) => msg.text),\n\t\t\t],\n\t\t};\n\t}\n\n\t/**\n\t * Clear all queued messages and return their contents.\n\t * Clears both session queue and compaction queue.\n\t */\n\tprivate clearAllQueues(): { steering: string[]; followUp: string[] } {\n\t\tconst { steering, followUp } = this.session.clearQueue();\n\t\tconst compactionSteering = this.compactionQueuedMessages\n\t\t\t.filter((msg) => msg.mode === \"steer\")\n\t\t\t.map((msg) => msg.text);\n\t\tconst compactionFollowUp = this.compactionQueuedMessages\n\t\t\t.filter((msg) => msg.mode === \"followUp\")\n\t\t\t.map((msg) => msg.text);\n\t\tthis.compactionQueuedMessages = [];\n\t\treturn {\n\t\t\tsteering: [...steering, ...compactionSteering],\n\t\t\tfollowUp: [...followUp, ...compactionFollowUp],\n\t\t};\n\t}\n\n\tprivate updatePendingMessagesDisplay(): void {\n\t\tthis.pendingMessagesContainer.clear();\n\t\tconst { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();\n\t\tif (steeringMessages.length > 0 || followUpMessages.length > 0) {\n\t\t\tthis.pendingMessagesContainer.addChild(new Spacer(1));\n\t\t\tfor (const message of steeringMessages) {\n\t\t\t\tconst text = theme.fg(\"dim\", `Steering: ${message}`);\n\t\t\t\tthis.pendingMessagesContainer.addChild(new TruncatedText(text, 2, 0));\n\t\t\t}\n\t\t\tfor (const message of followUpMessages) {\n\t\t\t\tconst text = theme.fg(\"dim\", `Follow-up: ${message}`);\n\t\t\t\tthis.pendingMessagesContainer.addChild(new TruncatedText(text, 2, 0));\n\t\t\t}\n\t\t\tconst dequeueHint = this.getAppKeyDisplay(\"dequeue\");\n\t\t\tconst hintText = theme.fg(\"dim\", `↳ ${dequeueHint} to edit all queued messages`);\n\t\t\tthis.pendingMessagesContainer.addChild(new TruncatedText(hintText, 2, 0));\n\t\t}\n\t}\n\n\tprivate restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {\n\t\tconst { steering, followUp } = this.clearAllQueues();\n\t\tconst allQueued = [...steering, ...followUp];\n\t\tif (allQueued.length === 0) {\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tif (options?.abort) {\n\t\t\t\tthis.agent.abort();\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\tconst queuedText = allQueued.join(\"\\n\\n\");\n\t\tconst currentText = options?.currentText ?? this.editor.getText();\n\t\tconst combinedText = [queuedText, currentText].filter((t) => t.trim()).join(\"\\n\\n\");\n\t\tthis.editor.setText(combinedText);\n\t\tthis.updatePendingMessagesDisplay();\n\t\tif (options?.abort) {\n\t\t\tthis.agent.abort();\n\t\t}\n\t\treturn allQueued.length;\n\t}\n\n\tprivate queueCompactionMessage(text: string, mode: \"steer\" | \"followUp\"): void {\n\t\tthis.compactionQueuedMessages.push({ text, mode });\n\t\tthis.editor.addToHistory?.(text);\n\t\tthis.editor.setText(\"\");\n\t\tthis.updatePendingMessagesDisplay();\n\t\tthis.showStatus(\"Queued message for after compaction\");\n\t}\n\n\tprivate isExtensionCommand(text: string): boolean {\n\t\tif (!text.startsWith(\"/\")) return false;\n\n\t\tconst extensionRunner = this.session.extensionRunner;\n\t\tif (!extensionRunner) return false;\n\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\treturn !!extensionRunner.getCommand(commandName);\n\t}\n\n\tprivate async flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {\n\t\tif (this.compactionQueuedMessages.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst capturedEpoch = this.sessionEpoch;\n\t\tconst queuedMessages = [...this.compactionQueuedMessages];\n\t\tthis.compactionQueuedMessages = [];\n\t\tthis.updatePendingMessagesDisplay();\n\n\t\tconst restoreQueue = (error: unknown) => {\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\tthis.session.clearQueue();\n\t\t\tthis.compactionQueuedMessages = queuedMessages;\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.showError(\n\t\t\t\t`Failed to send queued message${queuedMessages.length > 1 ? \"s\" : \"\"}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`,\n\t\t\t);\n\t\t};\n\n\t\ttry {\n\t\t\tif (options?.willRetry) {\n\t\t\t\t// When retry is pending, queue messages for the retry turn\n\t\t\t\tfor (const message of queuedMessages) {\n\t\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\t\t\tif (this.isExtensionCommand(message.text)) {\n\t\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t\t} else if (message.mode === \"followUp\") {\n\t\t\t\t\t\tawait this.session.followUp(message.text);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait this.session.steer(message.text);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (this.sessionEpoch === capturedEpoch) {\n\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Find first non-extension-command message to use as prompt\n\t\t\tconst firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));\n\t\t\tif (firstPromptIndex === -1) {\n\t\t\t\t// All extension commands - execute them all\n\t\t\t\tfor (const message of queuedMessages) {\n\t\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Execute any extension commands before the first prompt\n\t\t\tconst preCommands = queuedMessages.slice(0, firstPromptIndex);\n\t\t\tconst firstPrompt = queuedMessages[firstPromptIndex];\n\t\t\tconst rest = queuedMessages.slice(firstPromptIndex + 1);\n\n\t\t\tfor (const message of preCommands) {\n\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t}\n\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\t// Send first prompt (starts streaming)\n\t\t\tconst promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {\n\t\t\t\trestoreQueue(error);\n\t\t\t});\n\n\t\t\t// Queue remaining messages\n\t\t\tfor (const message of rest) {\n\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\t\tif (this.isExtensionCommand(message.text)) {\n\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t} else if (message.mode === \"followUp\") {\n\t\t\t\t\tawait this.session.followUp(message.text);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.session.steer(message.text);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.sessionEpoch === capturedEpoch) {\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t}\n\t\t\tvoid promptPromise;\n\t\t} catch (error) {\n\t\t\trestoreQueue(error);\n\t\t}\n\t}\n\n\t/** Move pending bash components from pending area to chat */\n\tprivate flushPendingBashComponents(): void {\n\t\tfor (const component of this.pendingBashComponents) {\n\t\t\tthis.pendingMessagesContainer.removeChild(component);\n\t\t\tthis.chatContainer.addChild(component);\n\t\t}\n\t\tthis.pendingBashComponents = [];\n\t}\n\n\t// =========================================================================\n\t// Selectors\n\t// =========================================================================\n\n\t/**\n\t * Shows a selector component in place of the editor.\n\t * @param create Factory that receives a `done` callback and returns the component and focus target\n\t */\n\tprivate showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\tconst done = () => {\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.restoreCanonicalEditor({ requestRender: false, owner: component });\n\t\t};\n\t\tconst { component, focus } = create(done);\n\t\tthis.mountPromptOwner(component, component, { focus });\n\t}\n\n\tprivate showSettingsSelector(): void {\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new SettingsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tautoCompact: this.session.autoCompactionEnabled,\n\t\t\t\t\tshowImages: this.settingsManager.getShowImages(),\n\t\t\t\t\tautoResizeImages: this.settingsManager.getImageAutoResize(),\n\t\t\t\t\tblockImages: this.settingsManager.getBlockImages(),\n\t\t\t\t\tenableSkillCommands: this.settingsManager.getEnableSkillCommands(),\n\t\t\t\t\tsteeringMode: this.session.steeringMode,\n\t\t\t\t\tfollowUpMode: this.session.followUpMode,\n\t\t\t\t\ttransport: this.settingsManager.getTransport(),\n\t\t\t\t\tthinkingLevel: this.session.thinkingLevel,\n\t\t\t\t\tavailableThinkingLevels: this.session.getAvailableThinkingLevels(),\n\t\t\t\t\tcurrentTheme: this.settingsManager.getTheme() || \"dark\",\n\t\t\t\t\tavailableThemes: getAvailableThemes(),\n\t\t\t\t\thideThinkingBlock: this.hideThinkingBlock,\n\t\t\t\t\tcollapseChangelog: this.settingsManager.getCollapseChangelog(),\n\t\t\t\t\tdoubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),\n\t\t\t\t\ttreeFilterMode: this.settingsManager.getTreeFilterMode(),\n\t\t\t\t\tshowHardwareCursor: this.settingsManager.getShowHardwareCursor(),\n\t\t\t\t\teditorPaddingX: this.settingsManager.getEditorPaddingX(),\n\t\t\t\t\tautocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),\n\t\t\t\t\tquietStartup: this.settingsManager.getQuietStartup(),\n\t\t\t\t\tclearOnShrink: this.settingsManager.getClearOnShrink(),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonAutoCompactChange: (enabled) => {\n\t\t\t\t\t\tthis.session.setAutoCompactionEnabled(enabled);\n\t\t\t\t\t\tthis.footer.setAutoCompactEnabled(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonShowImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowImages(enabled);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof ToolExecutionComponent) {\n\t\t\t\t\t\t\t\tchild.setShowImages(enabled);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutoResizeImagesChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setImageAutoResize(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonBlockImagesChange: (blocked) => {\n\t\t\t\t\t\tthis.settingsManager.setBlockImages(blocked);\n\t\t\t\t\t},\n\t\t\t\t\tonEnableSkillCommandsChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setEnableSkillCommands(enabled);\n\t\t\t\t\t\tthis.setupAutocomplete(this.fdPath);\n\t\t\t\t\t},\n\t\t\t\t\tonSteeringModeChange: (mode) => {\n\t\t\t\t\t\tthis.session.setSteeringMode(mode);\n\t\t\t\t\t},\n\t\t\t\t\tonFollowUpModeChange: (mode) => {\n\t\t\t\t\t\tthis.session.setFollowUpMode(mode);\n\t\t\t\t\t},\n\t\t\t\t\tonTransportChange: (transport) => {\n\t\t\t\t\t\tthis.settingsManager.setTransport(transport);\n\t\t\t\t\t\tthis.session.agent.setTransport(transport);\n\t\t\t\t\t},\n\t\t\t\t\tonThinkingLevelChange: (level) => {\n\t\t\t\t\t\tthis.session.setThinkingLevel(level);\n\t\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\t},\n\t\t\t\t\tonThemeChange: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tthis.settingsManager.setTheme(themeName);\n\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${result.error}\\nFell back to dark theme.`);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonThemePreview: (themeName) => {\n\t\t\t\t\t\tconst result = setTheme(themeName, true);\n\t\t\t\t\t\tif (result.success) {\n\t\t\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonHideThinkingBlockChange: (hidden) => {\n\t\t\t\t\t\tthis.hideThinkingBlock = hidden;\n\t\t\t\t\t\tthis.settingsManager.setHideThinkingBlock(hidden);\n\t\t\t\t\t\tfor (const child of this.chatContainer.children) {\n\t\t\t\t\t\t\tif (child instanceof AssistantMessageComponent) {\n\t\t\t\t\t\t\t\tchild.setHideThinkingBlock(hidden);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\t\tthis.rebuildChatFromMessages();\n\t\t\t\t\t},\n\t\t\t\t\tonCollapseChangelogChange: (collapsed) => {\n\t\t\t\t\t\tthis.settingsManager.setCollapseChangelog(collapsed);\n\t\t\t\t\t},\n\t\t\t\t\tonQuietStartupChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setQuietStartup(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonDoubleEscapeActionChange: (action) => {\n\t\t\t\t\t\tthis.settingsManager.setDoubleEscapeAction(action);\n\t\t\t\t\t},\n\t\t\t\t\tonTreeFilterModeChange: (mode) => {\n\t\t\t\t\t\tthis.settingsManager.setTreeFilterMode(mode);\n\t\t\t\t\t},\n\t\t\t\t\tonShowHardwareCursorChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setShowHardwareCursor(enabled);\n\t\t\t\t\t\tthis.ui.setShowHardwareCursor(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonEditorPaddingXChange: (padding) => {\n\t\t\t\t\t\tthis.settingsManager.setEditorPaddingX(padding);\n\t\t\t\t\t\tthis.defaultEditor.setPaddingX(padding);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setPaddingX(padding);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonAutocompleteMaxVisibleChange: (maxVisible) => {\n\t\t\t\t\t\tthis.settingsManager.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\tif (this.editor !== this.defaultEditor && this.editor.setAutocompleteMaxVisible !== undefined) {\n\t\t\t\t\t\t\tthis.editor.setAutocompleteMaxVisible(maxVisible);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonClearOnShrinkChange: (enabled) => {\n\t\t\t\t\t\tthis.settingsManager.setClearOnShrink(enabled);\n\t\t\t\t\t\tthis.ui.setClearOnShrink(enabled);\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getSettingsList() };\n\t\t});\n\t}\n\n\tprivate async handleModelCommand(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst capturedEpoch = this.sessionEpoch;\n\t\tconst model = await this.findExactModelMatch(searchTerm);\n\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tawait this.session.setModel(model);\n\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\t\tthis.footer.invalidate();\n\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t// this.showStatus(`Model: ${model.id}`);\n\t\t\t\tthis.checkDaxnutsEasterEgg(model);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showModelSelector(searchTerm);\n\t}\n\n\tprivate async findExactModelMatch(searchTerm: string): Promise<Model<any> | undefined> {\n\t\tconst term = searchTerm.trim();\n\t\tif (!term) return undefined;\n\n\t\tlet targetProvider: string | undefined;\n\t\tlet targetModelId = \"\";\n\n\t\tif (term.includes(\"/\")) {\n\t\t\tconst parts = term.split(\"/\", 2);\n\t\t\ttargetProvider = parts[0]?.trim().toLowerCase();\n\t\t\ttargetModelId = parts[1]?.trim().toLowerCase() ?? \"\";\n\t\t} else {\n\t\t\ttargetModelId = term.toLowerCase();\n\t\t}\n\n\t\tif (!targetModelId) return undefined;\n\n\t\tconst models = await this.getModelCandidates();\n\t\tconst exactMatches = models.filter((item) => {\n\t\t\tconst idMatch = item.id.toLowerCase() === targetModelId;\n\t\t\tconst providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;\n\t\t\treturn idMatch && providerMatch;\n\t\t});\n\n\t\treturn exactMatches.length === 1 ? exactMatches[0] : undefined;\n\t}\n\n\tprivate async getModelCandidates(): Promise<Model<any>[]> {\n\t\tif (this.session.scopedModels.length > 0) {\n\t\t\treturn this.session.scopedModels.map((scoped) => scoped.model);\n\t\t}\n\n\t\tthis.session.modelRegistry.refresh();\n\t\ttry {\n\t\t\treturn await this.session.modelRegistry.getAvailable();\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/** Update the footer's available provider count from current model candidates */\n\tprivate async updateAvailableProviderCount(): Promise<void> {\n\t\tconst models = await this.getModelCandidates();\n\t\tconst uniqueProviders = new Set(models.map((m) => m.provider));\n\t\tthis.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);\n\t}\n\n\tprivate showModelSelector(initialSearchInput?: string): void {\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ModelSelectorComponent(\n\t\t\t\tthis.ui,\n\t\t\t\tthis.session.model,\n\t\t\t\tthis.settingsManager,\n\t\t\t\tthis.session.modelRegistry,\n\t\t\t\tthis.session.scopedModels,\n\t\t\t\tasync (model) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.session.setModel(model);\n\t\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t\t\tthis.updatePromptChrome();\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\t// this.showStatus(`Model: ${model.id}`);\n\t\t\t\t\t\tthis.checkDaxnutsEasterEgg(model);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tinitialSearchInput,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async showModelsSelector(): Promise<void> {\n\t\t// Get all available models\n\t\tthis.session.modelRegistry.refresh();\n\t\tconst allModels = this.session.modelRegistry.getAvailable();\n\n\t\tif (allModels.length === 0) {\n\t\t\tthis.showStatus(\"No models available\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if session has scoped models (from previous session-only changes or CLI --models)\n\t\tconst sessionScopedModels = this.session.scopedModels;\n\t\tconst hasSessionScope = sessionScopedModels.length > 0;\n\n\t\t// Build enabled model IDs from session state or settings\n\t\tconst enabledModelIds = new Set<string>();\n\t\tlet hasFilter = false;\n\n\t\tif (hasSessionScope) {\n\t\t\t// Use current session's scoped models\n\t\t\tfor (const sm of sessionScopedModels) {\n\t\t\t\tenabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);\n\t\t\t}\n\t\t\thasFilter = true;\n\t\t} else {\n\t\t\t// Fall back to settings\n\t\t\tconst patterns = this.settingsManager.getEnabledModels();\n\t\t\tif (patterns !== undefined && patterns.length > 0) {\n\t\t\t\thasFilter = true;\n\t\t\t\tconst scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);\n\t\t\t\tfor (const sm of scopedModels) {\n\t\t\t\t\tenabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Track current enabled state (session-only until persisted)\n\t\tconst currentEnabledIds = new Set(enabledModelIds);\n\t\tlet currentHasFilter = hasFilter;\n\n\t\t// Helper to update session's scoped models (session-only, no persist)\n\t\tconst updateSessionModels = async (enabledIds: Set<string>) => {\n\t\t\tif (enabledIds.size > 0 && enabledIds.size < allModels.length) {\n\t\t\t\tconst newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);\n\t\t\t\tthis.session.setScopedModels(\n\t\t\t\t\tnewScopedModels.map((sm) => ({\n\t\t\t\t\t\tmodel: sm.model,\n\t\t\t\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t// All enabled or none enabled = no filter\n\t\t\t\tthis.session.setScopedModels([]);\n\t\t\t}\n\t\t\tawait this.updateAvailableProviderCount();\n\t\t\tthis.ui.requestRender();\n\t\t};\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new ScopedModelsSelectorComponent(\n\t\t\t\t{\n\t\t\t\t\tallModels,\n\t\t\t\t\tenabledModelIds: currentEnabledIds,\n\t\t\t\t\thasEnabledModelsFilter: currentHasFilter,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonModelToggle: async (modelId, enabled) => {\n\t\t\t\t\t\tif (enabled) {\n\t\t\t\t\t\t\tcurrentEnabledIds.add(modelId);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcurrentEnabledIds.delete(modelId);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentHasFilter = true;\n\t\t\t\t\t\tawait updateSessionModels(currentEnabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonEnableAll: async (allModelIds) => {\n\t\t\t\t\t\tcurrentEnabledIds.clear();\n\t\t\t\t\t\tfor (const id of allModelIds) {\n\t\t\t\t\t\t\tcurrentEnabledIds.add(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentHasFilter = false;\n\t\t\t\t\t\tawait updateSessionModels(currentEnabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonClearAll: async () => {\n\t\t\t\t\t\tcurrentEnabledIds.clear();\n\t\t\t\t\t\tcurrentHasFilter = true;\n\t\t\t\t\t\tawait updateSessionModels(currentEnabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonToggleProvider: async (_provider, modelIds, enabled) => {\n\t\t\t\t\t\tfor (const id of modelIds) {\n\t\t\t\t\t\t\tif (enabled) {\n\t\t\t\t\t\t\t\tcurrentEnabledIds.add(id);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcurrentEnabledIds.delete(id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentHasFilter = true;\n\t\t\t\t\t\tawait updateSessionModels(currentEnabledIds);\n\t\t\t\t\t},\n\t\t\t\t\tonPersist: (enabledIds) => {\n\t\t\t\t\t\t// Persist to settings\n\t\t\t\t\t\tconst newPatterns =\n\t\t\t\t\t\t\tenabledIds.length === allModels.length\n\t\t\t\t\t\t\t\t? undefined // All enabled = clear filter\n\t\t\t\t\t\t\t\t: enabledIds;\n\t\t\t\t\t\tthis.settingsManager.setEnabledModels(newPatterns);\n\t\t\t\t\t\tthis.showStatus(\"Model selection saved to settings\");\n\t\t\t\t\t},\n\t\t\t\t\tonCancel: () => {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate showUserMessageSelector(): void {\n\t\tconst userMessages = this.session.getUserMessagesForForking();\n\n\t\tif (userMessages.length === 0) {\n\t\t\tthis.showStatus(\"No messages to fork from\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new UserMessageSelectorComponent(\n\t\t\t\tuserMessages.map((m) => ({ id: m.entryId, text: m.text })),\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\tconst result = await this.session.fork(entryId);\n\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t// Extension cancelled the fork\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\tthis.renderInitialMessages();\n\t\t\t\t\tthis.editor.setText(result.selectedText);\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.showStatus(\"Branched to new session\");\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector.getMessageList() };\n\t\t});\n\t}\n\n\tprivate showTreeSelector(initialSelectedId?: string): void {\n\t\tconst tree = this.sessionManager.getTree();\n\t\tconst realLeafId = this.sessionManager.getLeafId();\n\t\tconst initialFilterMode = this.settingsManager.getTreeFilterMode();\n\n\t\tif (tree.length === 0) {\n\t\t\tthis.showStatus(\"No entries in session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new TreeSelectorComponent(\n\t\t\t\ttree,\n\t\t\t\trealLeafId,\n\t\t\t\tthis.ui.terminal.rows,\n\t\t\t\tasync (entryId) => {\n\t\t\t\t\t// Selecting the current leaf is a no-op (already there)\n\t\t\t\t\tif (entryId === realLeafId) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.showStatus(\"Already at this point\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Ask about summarization\n\t\t\t\t\tdone(); // Close selector first\n\n\t\t\t\t\t// Loop until user makes a complete choice or cancels to tree\n\t\t\t\t\tlet wantsSummary = false;\n\t\t\t\t\tlet customInstructions: string | undefined;\n\n\t\t\t\t\t// Check if we should skip the prompt (user preference to always default to no summary)\n\t\t\t\t\tif (!this.settingsManager.getBranchSummarySkipPrompt()) {\n\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t\tconst summaryChoice = await this.showExtensionSelector(\"Summarize branch?\", [\n\t\t\t\t\t\t\t\t\"No summary\",\n\t\t\t\t\t\t\t\t\"Summarize\",\n\t\t\t\t\t\t\t\t\"Summarize with custom prompt\",\n\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\tif (summaryChoice === undefined) {\n\t\t\t\t\t\t\t\t// User pressed escape - re-show tree selector with same selection\n\t\t\t\t\t\t\t\tthis.showTreeSelector(entryId);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\twantsSummary = summaryChoice !== \"No summary\";\n\n\t\t\t\t\t\t\tif (summaryChoice === \"Summarize with custom prompt\") {\n\t\t\t\t\t\t\t\tcustomInstructions = await this.showExtensionEditor(\"Custom summarization instructions\");\n\t\t\t\t\t\t\t\tif (customInstructions === undefined) {\n\t\t\t\t\t\t\t\t\t// User cancelled - loop back to summary selector\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// User made a complete choice\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set up escape handler and loader if summarizing\n\t\t\t\t\tlet summaryLoader: Loader | undefined;\n\t\t\t\t\tconst originalOnEscape = this.defaultEditor.onEscape;\n\n\t\t\t\t\tif (wantsSummary) {\n\t\t\t\t\t\tthis.defaultEditor.onEscape = () => {\n\t\t\t\t\t\t\tthis.session.abortBranchSummary();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tsummaryLoader = new Loader(\n\t\t\t\t\t\t\tthis.ui,\n\t\t\t\t\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t\t\t`Summarizing branch... (${appKey(this.keybindings, \"interrupt\")} to cancel)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.mountStatusLoader(summaryLoader, { requestRender: false });\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.session.navigateTree(entryId, {\n\t\t\t\t\t\t\tsummarize: wantsSummary,\n\t\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (result.aborted) {\n\t\t\t\t\t\t\t// Summarization aborted - re-show tree selector with same selection\n\t\t\t\t\t\t\tthis.showStatus(\"Branch summarization cancelled\");\n\t\t\t\t\t\t\tthis.showTreeSelector(entryId);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (result.cancelled) {\n\t\t\t\t\t\t\tthis.showStatus(\"Navigation cancelled\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Update UI\n\t\t\t\t\t\tthis.clearChatContainer();\n\t\t\t\t\t\tthis.renderInitialMessages();\n\t\t\t\t\t\tif (result.editorText && !this.editor.getText().trim()) {\n\t\t\t\t\t\t\tthis.editor.setText(result.editorText);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.showStatus(\"Navigated to selected point\");\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (summaryLoader) {\n\t\t\t\t\t\t\tsummaryLoader.dispose();\n\t\t\t\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: summaryLoader });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.defaultEditor.onEscape = originalOnEscape;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\t(entryId, label) => {\n\t\t\t\t\tthis.sessionManager.appendLabelChange(entryId, label);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tinitialSelectedId,\n\t\t\t\tinitialFilterMode,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate showSessionSelector(): void {\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new SessionSelectorComponent(\n\t\t\t\t(onProgress) =>\n\t\t\t\t\tSessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress),\n\t\t\t\tSessionManager.listAll,\n\t\t\t\tasync (sessionPath) => {\n\t\t\t\t\tdone();\n\t\t\t\t\tawait this.handleResumeSession(sessionPath);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tvoid this.shutdown();\n\t\t\t\t},\n\t\t\t\t() => this.ui.requestRender(),\n\t\t\t\t{\n\t\t\t\t\trenameSession: async (sessionFilePath: string, nextName: string | undefined) => {\n\t\t\t\t\t\tconst next = (nextName ?? \"\").trim();\n\t\t\t\t\t\tif (!next) return;\n\t\t\t\t\t\tconst mgr = SessionManager.open(sessionFilePath);\n\t\t\t\t\t\tmgr.appendSessionInfo(next);\n\t\t\t\t\t},\n\t\t\t\t\tshowRenameHint: true,\n\t\t\t\t\tkeybindings: this.keybindings,\n\t\t\t\t},\n\n\t\t\t\tthis.sessionManager.getSessionFile(),\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async handleResumeSession(sessionPath: string): Promise<void> {\n\t\t// Stop loading animation\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.dispose();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t// Switch session via AgentSession (emits extension session events)\n\t\tconst switched = await this.session.switchSession(sessionPath);\n\t\tif (!switched) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Clear local queued UI state only after resume succeeds\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.compactionQueuedMessages = [];\n\t\tthis.resetInteractiveSessionUI(true);\n\t\tthis.showStatus(\"Resumed session\");\n\t}\n\n\tprivate async showOAuthSelector(mode: \"login\" | \"logout\"): Promise<void> {\n\t\tif (mode === \"logout\") {\n\t\t\tconst providers = this.session.modelRegistry.authStorage.list();\n\t\t\tconst loggedInProviders = providers.filter(\n\t\t\t\t(p) => this.session.modelRegistry.authStorage.get(p)?.type === \"oauth\",\n\t\t\t);\n\t\t\tif (loggedInProviders.length === 0) {\n\t\t\t\tthis.showStatus(\"No OAuth providers logged in. Use /login first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.showSelector((done) => {\n\t\t\tconst selector = new OAuthSelectorComponent(\n\t\t\t\tmode,\n\t\t\t\tthis.session.modelRegistry.authStorage,\n\t\t\t\tasync (providerId: string) => {\n\t\t\t\t\tdone();\n\n\t\t\t\t\tif (mode === \"login\") {\n\t\t\t\t\t\tawait this.showLoginDialog(providerId);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Logout flow\n\t\t\t\t\t\tconst providerInfo = this.session.modelRegistry.authStorage\n\t\t\t\t\t\t\t.getOAuthProviders()\n\t\t\t\t\t\t\t.find((p) => p.id === providerId);\n\t\t\t\t\t\tconst providerName = providerInfo?.name || providerId;\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tthis.session.modelRegistry.authStorage.logout(providerId);\n\t\t\t\t\t\t\tthis.session.modelRegistry.refresh();\n\t\t\t\t\t\t\tawait this.updateAvailableProviderCount();\n\t\t\t\t\t\t\tthis.showStatus(`Logged out of ${providerName}`);\n\t\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\t\tthis.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t}\n\n\tprivate async showLoginDialog(providerId: string): Promise<void> {\n\t\tconst providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);\n\t\tconst providerName = providerInfo?.name || providerId;\n\n\t\t// Providers that use callback servers (can paste redirect URL)\n\t\tconst usesCallbackServer = providerInfo?.usesCallbackServer ?? false;\n\n\t\t// Create login dialog component\n\t\tconst dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {\n\t\t\t// Completion handled below\n\t\t});\n\n\t\t// Show dialog in editor container\n\t\tthis.mountPromptOwner(dialog, dialog);\n\n\t\t// Promise for manual code input (racing with callback server)\n\t\tlet manualCodeResolve: ((code: string) => void) | undefined;\n\t\tlet manualCodeReject: ((err: Error) => void) | undefined;\n\t\tconst manualCodePromise = new Promise<string>((resolve, reject) => {\n\t\t\tmanualCodeResolve = resolve;\n\t\t\tmanualCodeReject = reject;\n\t\t});\n\n\t\t// Restore editor helper\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\tconst restoreEditor = () => {\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.restoreCanonicalEditor({ owner: dialog });\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.session.modelRegistry.authStorage.login(providerId as OAuthProviderId, {\n\t\t\t\tonAuth: (info: { url: string; instructions?: string }) => {\n\t\t\t\t\tdialog.showAuth(info.url, info.instructions);\n\n\t\t\t\t\tif (usesCallbackServer) {\n\t\t\t\t\t\t// Show input for manual paste, racing with callback\n\t\t\t\t\t\tdialog\n\t\t\t\t\t\t\t.showManualInput(\"Paste redirect URL below, or complete login in browser:\")\n\t\t\t\t\t\t\t.then((value) => {\n\t\t\t\t\t\t\t\tif (value && manualCodeResolve) {\n\t\t\t\t\t\t\t\t\tmanualCodeResolve(value);\n\t\t\t\t\t\t\t\t\tmanualCodeResolve = undefined;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\t\t\tif (manualCodeReject) {\n\t\t\t\t\t\t\t\t\tmanualCodeReject(new Error(\"Login cancelled\"));\n\t\t\t\t\t\t\t\t\tmanualCodeReject = undefined;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t} else if (providerId === \"github-copilot\") {\n\t\t\t\t\t\t// GitHub Copilot polls after onAuth\n\t\t\t\t\t\tdialog.showWaiting(\"Waiting for browser authentication...\");\n\t\t\t\t\t}\n\t\t\t\t\t// For Anthropic: onPrompt is called immediately after\n\t\t\t\t},\n\n\t\t\t\tonPrompt: async (prompt: { message: string; placeholder?: string }) => {\n\t\t\t\t\treturn dialog.showPrompt(prompt.message, prompt.placeholder);\n\t\t\t\t},\n\n\t\t\t\tonProgress: (message: string) => {\n\t\t\t\t\tdialog.showProgress(message);\n\t\t\t\t},\n\n\t\t\t\tonManualCodeInput: () => manualCodePromise,\n\n\t\t\t\tsignal: dialog.signal,\n\t\t\t});\n\n\t\t\t// Success\n\t\t\trestoreEditor();\n\t\t\tthis.session.modelRegistry.refresh();\n\t\t\tawait this.updateAvailableProviderCount();\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);\n\t\t} catch (error: unknown) {\n\t\t\trestoreEditor();\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tconst errorMsg = error instanceof Error ? error.message : String(error);\n\t\t\tif (errorMsg !== \"Login cancelled\") {\n\t\t\t\tthis.showError(`Failed to login to ${providerName}: ${errorMsg}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Command handlers\n\t// =========================================================================\n\n\tprivate async handleReloadCommand(): Promise<void> {\n\t\tif (this.session.isStreaming) {\n\t\t\tthis.showWarning(\"Wait for the current response to finish before reloading.\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.session.isCompacting) {\n\t\t\tthis.showWarning(\"Wait for compaction to finish before reloading.\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.resetExtensionUI();\n\n\t\tconst loader = new BorderedLoader(this.ui, theme, \"Reloading extensions, skills, prompts, themes...\", {\n\t\t\tcancellable: false,\n\t\t});\n\t\tconst previousEditor = this.editor;\n\t\tthis.mountPromptOwner(loader, loader);\n\n\t\tconst capturedEpoch = this.uiEpoch;\n\t\tconst dismissLoader = (_editor: Component) => {\n\t\t\tloader.dispose();\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.restoreCanonicalEditor({ owner: loader });\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.session.reload();\n\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\tloader.dispose();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetRegisteredThemes(this.session.resourceLoader.getThemes().themes);\n\t\t\tthis.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();\n\t\t\tconst themeName = this.settingsManager.getTheme();\n\t\t\tconst themeResult = themeName ? setTheme(themeName, true) : { success: true };\n\t\t\tif (!themeResult.success) {\n\t\t\t\tthis.showError(`Failed to load theme \"${themeName}\": ${themeResult.error}\\nFell back to dark theme.`);\n\t\t\t}\n\t\t\tconst editorPaddingX = this.settingsManager.getEditorPaddingX();\n\t\t\tconst autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();\n\t\t\tthis.defaultEditor.setPaddingX(editorPaddingX);\n\t\t\tthis.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);\n\t\t\tif (this.editor !== this.defaultEditor) {\n\t\t\t\tthis.editor.setPaddingX?.(editorPaddingX);\n\t\t\t\tthis.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);\n\t\t\t}\n\t\t\tthis.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());\n\t\t\tthis.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());\n\t\t\tthis.updatePromptChrome();\n\t\t\tthis.setupAutocomplete(this.fdPath);\n\t\t\tconst runner = this.session.extensionRunner;\n\t\t\tif (runner) {\n\t\t\t\tthis.setupExtensionShortcuts(runner);\n\t\t\t}\n\t\t\tthis.rebuildChatFromMessages();\n\t\t\tdismissLoader(this.editor as Component);\n\t\t\tthis.showLoadedResources({\n\t\t\t\textensionPaths: runner?.getExtensionPaths() ?? [],\n\t\t\t\tforce: false,\n\t\t\t\tshowDiagnosticsWhenQuiet: true,\n\t\t\t});\n\t\t\tconst modelsJsonError = this.session.modelRegistry.getError();\n\t\t\tif (modelsJsonError) {\n\t\t\t\tthis.showError(`models.json error: ${modelsJsonError}`);\n\t\t\t}\n\t\t\tthis.showStatus(\"Reloaded extensions, skills, prompts, themes\");\n\t\t} catch (error) {\n\t\t\tif (this.uiEpoch !== capturedEpoch) {\n\t\t\t\tloader.dispose();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdismissLoader(previousEditor as Component);\n\t\t\tthis.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate async handleExportCommand(text: string): Promise<void> {\n\t\tconst parts = text.split(/\\s+/);\n\t\tconst outputPath = parts.length > 1 ? parts[1] : undefined;\n\t\tconst capturedEpoch = this.uiEpoch;\n\n\t\ttry {\n\t\t\tconst filePath = await this.session.exportToHtml(outputPath);\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate async handleShareCommand(): 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.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.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst capturedEpoch = this.uiEpoch;\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.session.exportToHtml(tmpFile);\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t} catch (error: unknown) {\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.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.ui, theme, \"Creating gist...\");\n\t\tthis.mountPromptOwner(loader, loader);\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.restoreCanonicalEditor({ requestRender: false, owner: loader });\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\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\tthis.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\t\t\tif (this.uiEpoch !== capturedEpoch) 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.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.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create the preview URL\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.showStatus(`Share URL: ${previewUrl}\\nGist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tif (this.uiEpoch !== capturedEpoch) return;\n\t\t\t\tthis.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleCopyCommand(): void {\n\t\tconst text = this.session.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tcopyToClipboard(text);\n\t\t\tthis.showStatus(\"Copied last agent message to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate handleNameCommand(text: string): void {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\tconst currentName = this.sessionManager.getSessionName();\n\t\t\tif (currentName) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name: ${currentName}`), 2, 0));\n\t\t\t} else {\n\t\t\t\tthis.showWarning(\"Usage: /name <name>\");\n\t\t\t}\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.sessionManager.appendSessionInfo(name);\n\t\tthis.updateTerminalTitle();\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session name set: ${name}`), 2, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleSessionCommand(): void {\n\t\tconst stats = this.session.getSessionStats();\n\t\tconst sessionName = this.sessionManager.getSessionName();\n\t\tconst workingContext = this.session.getWorkingContext();\n\t\tconst recentDelegated = this.session.getRecentDelegatedTasks(3);\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tif (sessionName) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Name:\")} ${sessionName}\\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(\"Working Context\")}\\n`;\n\n\t\t// Memory section\n\t\tconst staleText =\n\t\t\tworkingContext.memory.staleCount > 0\n\t\t\t\t? ` ${theme.fg(\"warning\", `(${workingContext.memory.staleCount} stale)`)}`\n\t\t\t\t: \"\";\n\t\tinfo += `${theme.fg(\"dim\", \"Memory:\")} ${workingContext.memory.itemCount} active item${workingContext.memory.itemCount === 1 ? \"\" : \"s\"}${staleText}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Keys:\")} ${workingContext.memory.keyPreview.join(\", \") || \"(none)\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Scope:\")} ${workingContext.memory.scope}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Provenance:\")} persisted in session snapshots\\n`;\n\n\t\t// Todo section\n\t\tinfo += `${theme.fg(\"dim\", \"Plan:\")} ${workingContext.todo.completed}/${workingContext.todo.total} completed\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Active:\")} ${workingContext.todo.inProgress ?? \"(none)\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Scope:\")} ${workingContext.todo.scope}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Provenance:\")} persisted in session entries\\n`;\n\n\t\t// Tasks section\n\t\tinfo += `${theme.fg(\"dim\", \"Tasks:\")} ${workingContext.tasks.completed}/${workingContext.tasks.total} completed, ${workingContext.tasks.inProgress} in progress\\n`;\n\t\tif (workingContext.tasks.inProgressTask) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"  Active:\")} ${workingContext.tasks.inProgressTask.subject}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"  Provenance:\")} persisted in session entries\\n`;\n\n\t\t// Delegated work section\n\t\tinfo += `${theme.fg(\"dim\", \"Delegated:\")} ${workingContext.delegatedWork.activeCount} active, ${workingContext.delegatedWork.completedCount} done, ${workingContext.delegatedWork.failedCount} failed\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Scope:\")} ${workingContext.delegatedWork.scope}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"  Provenance:\")} ${workingContext.delegatedWork.note}\\n`;\n\n\t\tif (recentDelegated.length > 0) {\n\t\t\tfor (const task of recentDelegated) {\n\t\t\t\tconst statusColor =\n\t\t\t\t\ttask.status === \"completed\"\n\t\t\t\t\t\t? theme.fg(\"success\", task.status)\n\t\t\t\t\t\t: task.status === \"active\"\n\t\t\t\t\t\t\t? theme.fg(\"warning\", task.status)\n\t\t\t\t\t\t\t: theme.fg(\"error\", task.status);\n\t\t\t\tinfo += `${theme.fg(\"dim\", \"  -\")} ${task.agent} · ${statusColor} · ${task.task}\\n`;\n\t\t\t}\n\t\t}\n\t\tinfo += `\\n${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.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\t/**\n\t * Capitalize keybinding for display (e.g., \"ctrl+c\" -> \"Ctrl+C\").\n\t */\n\tprivate capitalizeKey(key: string): string {\n\t\treturn key\n\t\t\t.split(\"/\")\n\t\t\t.map((k) =>\n\t\t\t\tk\n\t\t\t\t\t.split(\"+\")\n\t\t\t\t\t.map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n\t\t\t\t\t.join(\"+\"),\n\t\t\t)\n\t\t\t.join(\"/\");\n\t}\n\n\t/**\n\t * Get capitalized display string for an app keybinding action.\n\t */\n\tprivate getAppKeyDisplay(action: AppAction): string {\n\t\treturn this.capitalizeKey(appKey(this.keybindings, action));\n\t}\n\n\t/**\n\t * Get capitalized display string for an editor keybinding action.\n\t */\n\tprivate getEditorKeyDisplay(action: EditorAction): string {\n\t\treturn this.capitalizeKey(editorKey(action));\n\t}\n\n\tprivate handleHotkeysCommand(): void {\n\t\t// Navigation keybindings\n\t\tconst cursorWordLeft = this.getEditorKeyDisplay(\"cursorWordLeft\");\n\t\tconst cursorWordRight = this.getEditorKeyDisplay(\"cursorWordRight\");\n\t\tconst cursorLineStart = this.getEditorKeyDisplay(\"cursorLineStart\");\n\t\tconst cursorLineEnd = this.getEditorKeyDisplay(\"cursorLineEnd\");\n\t\tconst jumpForward = this.getEditorKeyDisplay(\"jumpForward\");\n\t\tconst jumpBackward = this.getEditorKeyDisplay(\"jumpBackward\");\n\t\tconst pageUp = this.getEditorKeyDisplay(\"pageUp\");\n\t\tconst pageDown = this.getEditorKeyDisplay(\"pageDown\");\n\n\t\t// Editing keybindings\n\t\tconst submit = this.getEditorKeyDisplay(\"submit\");\n\t\tconst newLine = this.getEditorKeyDisplay(\"newLine\");\n\t\tconst deleteWordBackward = this.getEditorKeyDisplay(\"deleteWordBackward\");\n\t\tconst deleteWordForward = this.getEditorKeyDisplay(\"deleteWordForward\");\n\t\tconst deleteToLineStart = this.getEditorKeyDisplay(\"deleteToLineStart\");\n\t\tconst deleteToLineEnd = this.getEditorKeyDisplay(\"deleteToLineEnd\");\n\t\tconst yank = this.getEditorKeyDisplay(\"yank\");\n\t\tconst yankPop = this.getEditorKeyDisplay(\"yankPop\");\n\t\tconst undo = this.getEditorKeyDisplay(\"undo\");\n\t\tconst tab = this.getEditorKeyDisplay(\"tab\");\n\n\t\t// App keybindings\n\t\tconst interrupt = this.getAppKeyDisplay(\"interrupt\");\n\t\tconst clear = this.getAppKeyDisplay(\"clear\");\n\t\tconst exit = this.getAppKeyDisplay(\"exit\");\n\t\tconst suspend = this.getAppKeyDisplay(\"suspend\");\n\t\tconst cycleThinkingLevel = this.getAppKeyDisplay(\"cycleThinkingLevel\");\n\t\tconst cycleModelForward = this.getAppKeyDisplay(\"cycleModelForward\");\n\t\tconst selectModel = this.getAppKeyDisplay(\"selectModel\");\n\t\tconst expandTools = this.getAppKeyDisplay(\"expandTools\");\n\t\tconst toggleThinking = this.getAppKeyDisplay(\"toggleThinking\");\n\t\tconst externalEditor = this.getAppKeyDisplay(\"externalEditor\");\n\t\tconst followUp = this.getAppKeyDisplay(\"followUp\");\n\t\tconst dequeue = this.getAppKeyDisplay(\"dequeue\");\n\n\t\tlet hotkeys = `\n**Navigation**\n| Key | Action |\n|-----|--------|\n| \\`Arrow keys\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` | Start of line |\n| \\`${cursorLineEnd}\\` | End of line |\n| \\`${jumpForward}\\` | Jump forward to character |\n| \\`${jumpBackward}\\` | Jump backward to character |\n| \\`${pageUp}\\` / \\`${pageDown}\\` | Scroll by page |\n\n**Editing**\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${deleteWordBackward}\\` | Delete word backwards |\n| \\`${deleteWordForward}\\` | Delete word forwards |\n| \\`${deleteToLineStart}\\` | Delete to start of line |\n| \\`${deleteToLineEnd}\\` | Delete to end of line |\n| \\`${yank}\\` | Paste the most-recently-deleted text |\n| \\`${yankPop}\\` | Cycle through the deleted text after pasting |\n| \\`${undo}\\` | Undo |\n\n**Other**\n| Key | Action |\n|-----|--------|\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${interrupt}\\` | Cancel autocomplete / abort streaming |\n| \\`${clear}\\` | Clear editor (first) / exit (second) |\n| \\`${exit}\\` | Exit (when editor is empty) |\n| \\`${suspend}\\` | Suspend to background |\n| \\`${cycleThinkingLevel}\\` | Cycle thinking level |\n| \\`${cycleModelForward}\\` | Cycle models |\n| \\`${selectModel}\\` | Open model selector |\n| \\`${expandTools}\\` | Toggle tool output expansion |\n| \\`${toggleThinking}\\` | Toggle thinking block visibility |\n| \\`${externalEditor}\\` | Edit message in external editor |\n| \\`${followUp}\\` | Queue follow-up message |\n| \\`${dequeue}\\` | Restore queued messages |\n| \\`Ctrl+V\\` | Paste image from clipboard |\n| \\`/\\` | Slash commands |\n| \\`!\\` | Run bash command |\n| \\`!!\\` | Run bash command (excluded from context) |\n`;\n\n\t\t// Add extension-registered shortcuts\n\t\tconst extensionRunner = this.session.extensionRunner;\n\t\tif (extensionRunner) {\n\t\t\tconst shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());\n\t\t\tif (shortcuts.size > 0) {\n\t\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\t\tconst keyDisplay = key.replace(/\\b\\w/g, (c) => c.toUpperCase());\n\t\t\t\t\thotkeys += `| \\`${keyDisplay}\\` | ${description} |\\n`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Keyboard Shortcuts\")), 1, 0));\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(clearTerminal: boolean = false): Promise<void> {\n\t\t// New session via session (emits extension session events)\n\t\tconst success = await this.session.newSession();\n\t\tif (!success) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.resetInteractiveSessionUI(true);\n\n\t\tif (clearTerminal) {\n\t\t\tthis.ui.requestRender(true);\n\t\t} else {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate handleDebugCommand(): void {\n\t\tconst width = this.ui.terminal.columns;\n\t\tconst height = this.ui.terminal.rows;\n\t\tconst allLines = this.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.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.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(\n\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 1),\n\t\t);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleArminSaysHi(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new ArminComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDaxnuts(): void {\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DaxnutsComponent(this.ui));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate checkDaxnutsEasterEgg(model: { provider: string; id: string }): void {\n\t\tif (model.provider === \"opencode\" && model.id.toLowerCase().includes(\"kimi-k2.5\")) {\n\t\t\tthis.handleDaxnuts();\n\t\t}\n\t}\n\n\tprivate async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {\n\t\tconst capturedEpoch = this.sessionEpoch;\n\t\tconst extensionRunner = this.session.extensionRunner;\n\n\t\t// Emit user_bash event to let extensions intercept\n\t\tconst eventResult = extensionRunner\n\t\t\t? await extensionRunner.emitUserBash({\n\t\t\t\t\ttype: \"user_bash\",\n\t\t\t\t\tcommand,\n\t\t\t\t\texcludeFromContext,\n\t\t\t\t\tcwd: process.cwd(),\n\t\t\t\t})\n\t\t\t: undefined;\n\n\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t// If extension returned a full result, use it directly\n\t\tif (eventResult?.result) {\n\t\t\tconst result = eventResult.result;\n\n\t\t\t// Create UI component for display\n\t\t\tthis.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);\n\t\t\tif (this.session.isStreaming) {\n\t\t\t\tthis.pendingMessagesContainer.addChild(this.bashComponent);\n\t\t\t\tthis.pendingBashComponents.push(this.bashComponent);\n\t\t\t} else {\n\t\t\t\tthis.chatContainer.addChild(this.bashComponent);\n\t\t\t}\n\n\t\t\t// Show output and complete\n\t\t\tif (result.output) {\n\t\t\t\tthis.bashComponent.appendOutput(result.output);\n\t\t\t}\n\t\t\tthis.bashComponent.setComplete(\n\t\t\t\tresult.exitCode,\n\t\t\t\tresult.cancelled,\n\t\t\t\tresult.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,\n\t\t\t\tresult.fullOutputPath,\n\t\t\t);\n\n\t\t\t// Record the result in session\n\t\t\tthis.session.recordBashResult(command, result, { excludeFromContext });\n\t\t\tthis.bashComponent = undefined;\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Normal execution path (possibly with custom operations)\n\t\tconst isDeferred = this.session.isStreaming;\n\t\tthis.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);\n\n\t\tif (isDeferred) {\n\t\t\t// Show in pending area when agent is streaming\n\t\t\tthis.pendingMessagesContainer.addChild(this.bashComponent);\n\t\t\tthis.pendingBashComponents.push(this.bashComponent);\n\t\t} else {\n\t\t\t// Show in chat immediately when agent is idle\n\t\t\tthis.chatContainer.addChild(this.bashComponent);\n\t\t}\n\t\tthis.ui.requestRender();\n\n\t\ttry {\n\t\t\tconst result = await this.session.executeBash(\n\t\t\t\tcommand,\n\t\t\t\t(chunk) => {\n\t\t\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\t\t\tif (this.bashComponent) {\n\t\t\t\t\t\tthis.bashComponent.appendOutput(chunk);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ excludeFromContext, operations: eventResult?.operations },\n\t\t\t);\n\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\n\t\t\tif (this.bashComponent) {\n\t\t\t\tthis.bashComponent.setComplete(\n\t\t\t\t\tresult.exitCode,\n\t\t\t\t\tresult.cancelled,\n\t\t\t\t\tresult.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,\n\t\t\t\t\tresult.fullOutputPath,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return;\n\t\t\tif (this.bashComponent) {\n\t\t\t\tthis.bashComponent.setComplete(undefined, false);\n\t\t\t}\n\t\t\tthis.showError(`Bash command failed: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\n\t\tthis.bashComponent = undefined;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleCompactCommand(customInstructions?: string): Promise<void> {\n\t\tconst entries = this.sessionManager.getEntries();\n\t\tconst messageCount = entries.filter((e) => e.type === \"message\").length;\n\n\t\tif (messageCount < 2) {\n\t\t\tthis.showWarning(\"Nothing to compact (no messages yet)\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.executeCompaction(customInstructions, false);\n\t}\n\n\tprivate async executeCompaction(customInstructions?: string, isAuto = false): Promise<CompactionResult | undefined> {\n\t\tconst capturedEpoch = this.sessionEpoch;\n\n\t\t// Stop loading animation\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.dispose();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tthis.clearStatusOwner({ requestRender: false });\n\n\t\t// Set up escape handler during compaction\n\t\tconst originalOnEscape = this.defaultEditor.onEscape;\n\t\tthis.defaultEditor.onEscape = () => {\n\t\t\tthis.session.abortCompaction();\n\t\t};\n\n\t\t// Show compacting status\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tconst cancelHint = `(${appKey(this.keybindings, \"interrupt\")} to cancel)`;\n\t\tconst label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;\n\t\tconst compactingLoader = new Loader(\n\t\t\tthis.ui,\n\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tlabel,\n\t\t);\n\t\tthis.mountStatusLoader(compactingLoader, { requestRender: false });\n\t\tthis.ui.requestRender();\n\n\t\tlet result: CompactionResult | undefined;\n\n\t\ttry {\n\t\t\tresult = await this.session.compact(customInstructions);\n\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return undefined;\n\n\t\t\t// Rebuild UI\n\t\t\tthis.rebuildChatFromMessages();\n\n\t\t\t// Add compaction component at bottom so user sees it without scrolling\n\t\t\tconst msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());\n\t\t\tthis.addMessageToChat(msg);\n\n\t\t\tthis.footer.invalidate();\n\t\t} catch (error) {\n\t\t\tif (this.sessionEpoch !== capturedEpoch) return undefined;\n\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tif (message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\")) {\n\t\t\t\tthis.showError(\"Compaction cancelled\");\n\t\t\t} else {\n\t\t\t\tthis.showError(`Compaction failed: ${message}`);\n\t\t\t}\n\t\t} finally {\n\t\t\tif (this.sessionEpoch === capturedEpoch) {\n\t\t\t\tcompactingLoader.dispose();\n\t\t\t\tthis.clearStatusOwner({ requestRender: false, owner: compactingLoader });\n\t\t\t\tthis.defaultEditor.onEscape = originalOnEscape;\n\t\t\t}\n\t\t}\n\n\t\tif (this.sessionEpoch === capturedEpoch) {\n\t\t\tvoid this.flushCompactionQueue({ willRetry: false });\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tstop(): void {\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.dispose();\n\t\t\tthis.loadingAnimation = undefined;\n\t\t}\n\t\tif (this.autoCompactionLoader) {\n\t\t\tthis.autoCompactionLoader.dispose();\n\t\t\tthis.autoCompactionLoader = undefined;\n\t\t}\n\t\tif (this.retryLoader) {\n\t\t\tthis.retryLoader.dispose();\n\t\t\tthis.retryLoader = undefined;\n\t\t}\n\t\tthis.clearExtensionTerminalInputListeners();\n\t\tthis.terminalTitleController?.dispose();\n\t\tthis.footer.dispose();\n\t\tthis.footerDataProvider.dispose();\n\t\tif (this.unsubscribe) {\n\t\t\tthis.unsubscribe();\n\t\t}\n\t\tthis.windowFocusChangeUnsubscribe?.();\n\t\tthis.windowFocusChangeUnsubscribe = undefined;\n\t\tthis.focusTargetChangeUnsubscribe?.();\n\t\tthis.focusTargetChangeUnsubscribe = undefined;\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop();\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n\n/**\n * Mount the operator stack panels (Working Context → Todos → Tasks) onto a container.\n * Idempotent: panels already present in the container are not re-added.\n * Exported so tests can use the same assembly path as InteractiveMode.init().\n */\nexport function mountOperatorStack(\n\tcontainer: Container | TUI,\n\tsidebarTodoPanel: SidebarTodoPanel,\n\tsidebarTaskPanel: SidebarTodoPanel,\n): void {\n\tif (!container.children.includes(sidebarTodoPanel)) container.addChild(sidebarTodoPanel);\n\tif (!container.children.includes(sidebarTaskPanel)) container.addChild(sidebarTaskPanel);\n}\n"]}