{"version":3,"file":"model-controller.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/model-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAe/D,8DAA8D;AAC9D,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,GAAG,CAAC;IACR,0EAA0E;IAC1E,IAAI,OAAO,IAAI,YAAY,CAAC;IAC5B,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK;QAAE,SAAS,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7F,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qEAAqE;IACrE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,iEAAiE;IACjE,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7D,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,gEAAgE;IAChE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChD,uBAAuB,IAAI,IAAI,CAAC;IAChC,gBAAgB,IAAI,IAAI,CAAC;IACzB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAED,qBAAa,eAAe;IAGf,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,iCAAiC,CAAS;IAElD,YAA6B,IAAI,EAAE,mBAAmB,EAAI;IAE1D,OAAO,KAAK,OAAO,GAElB;IAEK,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAG7E;YAEa,kBAAkB;IAahC,iFAAiF;IAC3E,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC,CAIlD;YAGa,6BAA6B;IAsB3C;;;;OAIG;IACG,uCAAuC,CAAC,KAAK,GAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/G;IAEK,UAAU,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBjE;IAED,iBAAiB,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CA6BnD;IAEK,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA2ExC;CACD","sourcesContent":["/**\n * Model selection for the interactive mode (/model, /models, cycle keys).\n *\n * Owns the single-model picker, the scoped-models (enable set) picker, model\n * cycling, exact-match lookup for slash-command arguments, the footer's\n * available-provider count, and the Anthropic subscription-auth warning.\n * Extracted from interactive-mode.ts behind a narrow ModelControllerDeps\n * interface.\n */\n\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport type { AppKeybinding } from \"../../core/keybindings.js\";\nimport { findExactModelReferenceMatch, resolveModelScope } from \"../../core/model-resolver.js\";\nimport { ModelSelectorComponent } from \"./components/model-selector.js\";\nimport { ScopedModelsSelectorComponent } from \"./components/scoped-models-selector.js\";\n\nconst ANTHROPIC_SUBSCRIPTION_AUTH_TITLE = \"Anthropic subscription\";\nconst ANTHROPIC_SUBSCRIPTION_AUTH_BODY = [\n\t\"Billed per token as extra usage, not against plan limits.\",\n\t\"Turn off in /settings → Anthropic extra usage.\",\n];\n\nfunction isAnthropicSubscriptionAuthKey(apiKey: string | undefined): boolean {\n\treturn typeof apiKey === \"string\" && apiKey.startsWith(\"sk-ant-oat\");\n}\n\n/** The slice of the interactive mode the model flows need. */\nexport interface ModelControllerDeps {\n\tui: TUI;\n\t/** The active session (read at call time; the session can be swapped). */\n\tget session(): AgentSession;\n\tshowSelector(create: (done: () => void) => { component: Component; focus: Component }): void;\n\tshowStatus(message: string): void;\n\t/** A glimpse on the band above the prompt; gone in a few seconds. */\n\tnotify(message: string, note?: string): void;\n\t/** Report a dial step, naming its reverse key the first time. */\n\tshowDialStep(backward: AppKeybinding, message: string): void;\n\tshowError(errorMessage: string): void;\n\tshowWarning(warningMessage: string): void;\n\t/** A filled notice, for warnings that cost money if ignored. */\n\tshowNotice(title: string, body: string[]): void;\n\tupdateEditorBorderColor(): void;\n\tinvalidateFooter(): void;\n\tsetAvailableProviderCount(count: number): void;\n}\n\nexport class ModelController {\n\tprivate anthropicSubscriptionWarningShown = false;\n\n\tconstructor(private readonly deps: ModelControllerDeps) {}\n\n\tprivate get session(): AgentSession {\n\t\treturn this.deps.session;\n\t}\n\n\tasync findExactModelMatch(searchTerm: string): Promise<Model<any> | undefined> {\n\t\tconst models = await this.getModelCandidates();\n\t\treturn findExactModelReferenceMatch(searchTerm, models);\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\tasync updateAvailableProviderCount(): Promise<void> {\n\t\tconst models = await this.getModelCandidates();\n\t\tconst uniqueProviders = new Set(models.map((m) => m.provider));\n\t\tthis.deps.setAvailableProviderCount(uniqueProviders.size);\n\t}\n\n\t/** Whether the model bills through Anthropic subscription auth (extra usage). */\n\tprivate async usesAnthropicSubscriptionAuth(model: Model<any> | undefined): Promise<boolean> {\n\t\tif (this.session.settingsManager.getWarnings().anthropicExtraUsage === false) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!model || model.provider !== \"anthropic\") {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst storedCredential = this.session.modelRegistry.authStorage.get(\"anthropic\");\n\t\tif (storedCredential?.type === \"oauth\") {\n\t\t\treturn true;\n\t\t}\n\n\t\ttry {\n\t\t\tconst apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);\n\t\t\treturn isAnthropicSubscriptionAuthKey(apiKey);\n\t\t} catch {\n\t\t\t// Ignore auth lookup failures for warning-only checks.\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Resolving the auth type can hit the keychain, so every caller fires this\n\t * without awaiting it; the once-per-session latch is what keeps the notice\n\t * from repeating, not the call site.\n\t */\n\tasync maybeWarnAboutAnthropicSubscriptionAuth(model: Model<any> | undefined = this.session.model): Promise<void> {\n\t\tif (this.anthropicSubscriptionWarningShown) {\n\t\t\treturn;\n\t\t}\n\t\tif (!(await this.usesAnthropicSubscriptionAuth(model))) {\n\t\t\treturn;\n\t\t}\n\t\tthis.anthropicSubscriptionWarningShown = true;\n\t\tthis.deps.showNotice(ANTHROPIC_SUBSCRIPTION_AUTH_TITLE, ANTHROPIC_SUBSCRIPTION_AUTH_BODY);\n\t}\n\n\tasync cycleModel(direction: \"forward\" | \"backward\"): Promise<void> {\n\t\ttry {\n\t\t\tconst result = await this.session.cycleModel(direction);\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.deps.notify(msg);\n\t\t\t} else {\n\t\t\t\tthis.deps.invalidateFooter();\n\t\t\t\tthis.deps.updateEditorBorderColor();\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.deps.showDialStep(\n\t\t\t\t\tdirection === \"forward\" ? \"app.model.cycleBackward\" : \"app.model.cycleForward\",\n\t\t\t\t\t`Switched to ${result.model.name || result.model.id}${thinkingStr}`,\n\t\t\t\t);\n\t\t\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.deps.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tshowModelSelector(initialSearchInput?: string): void {\n\t\tthis.deps.showSelector((done) => {\n\t\t\tconst selector = new ModelSelectorComponent(\n\t\t\t\tthis.deps.ui,\n\t\t\t\tthis.session.model,\n\t\t\t\tthis.session.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.deps.invalidateFooter();\n\t\t\t\t\t\tthis.deps.updateEditorBorderColor();\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.deps.notify(`Model: ${model.id}`);\n\t\t\t\t\t\tvoid this.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tdone();\n\t\t\t\t\t\tthis.deps.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.deps.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\tasync 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.deps.notify(\"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\tlet currentEnabledIds: string[] | null = null;\n\n\t\tif (hasSessionScope) {\n\t\t\t// Use current session's scoped models\n\t\t\tcurrentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t} else {\n\t\t\t// Fall back to settings\n\t\t\tconst patterns = this.session.settingsManager.getEnabledModels();\n\t\t\tif (patterns !== undefined && patterns.length > 0) {\n\t\t\t\tconst scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);\n\t\t\t\tcurrentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);\n\t\t\t}\n\t\t}\n\n\t\t// Helper to update session's scoped models (session-only, no persist)\n\t\tconst updateSessionModels = async (enabledIds: string[] | null) => {\n\t\t\tcurrentEnabledIds = enabledIds === null ? null : [...enabledIds];\n\t\t\tif (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {\n\t\t\t\tconst newScopedModels = await resolveModelScope(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.deps.ui.requestRender();\n\t\t};\n\n\t\tthis.deps.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},\n\t\t\t\t{\n\t\t\t\t\tonChange: async (enabledIds) => {\n\t\t\t\t\t\tawait updateSessionModels(enabledIds);\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 === null || enabledIds.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.session.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);\n\t\t\t\t\t\tthis.deps.notify(\"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.deps.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"]}