@kolisachint/hoocode-agent 0.5.57 → 0.5.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/dist/core/agent-session.d.ts +2 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +4 -3
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/runner.d.ts.map +1 -1
  7. package/dist/core/extensions/runner.js +4 -1
  8. package/dist/core/extensions/runner.js.map +1 -1
  9. package/dist/core/keybindings.d.ts +162 -98
  10. package/dist/core/keybindings.d.ts.map +1 -1
  11. package/dist/core/keybindings.js +222 -106
  12. package/dist/core/keybindings.js.map +1 -1
  13. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  14. package/dist/modes/interactive/command-executor.js +95 -62
  15. package/dist/modes/interactive/command-executor.js.map +1 -1
  16. package/dist/modes/interactive/components/ask-options.d.ts +20 -6
  17. package/dist/modes/interactive/components/ask-options.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/ask-options.js +35 -13
  19. package/dist/modes/interactive/components/ask-options.js.map +1 -1
  20. package/dist/modes/interactive/components/keybinding-hints.d.ts +9 -0
  21. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/keybinding-hints.js +12 -0
  23. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  24. package/dist/modes/interactive/components/task-panel.d.ts +2 -2
  25. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/task-panel.js +24 -11
  27. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  28. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/tree-selector.js +29 -12
  30. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  31. package/dist/modes/interactive/interactive-mode.d.ts +63 -8
  32. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  33. package/dist/modes/interactive/interactive-mode.js +189 -90
  34. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  35. package/dist/modes/interactive/model-controller.d.ts +3 -0
  36. package/dist/modes/interactive/model-controller.d.ts.map +1 -1
  37. package/dist/modes/interactive/model-controller.js +1 -1
  38. package/dist/modes/interactive/model-controller.js.map +1 -1
  39. package/docs/keybindings.md +247 -80
  40. package/docs/terminal-setup.md +8 -3
  41. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  42. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  43. package/examples/extensions/sandbox/package.json +1 -1
  44. package/examples/extensions/with-deps/package.json +1 -1
  45. package/package.json +4 -4
@@ -10,6 +10,7 @@
10
10
  import type { Model } from "@kolisachint/hoocode-ai";
11
11
  import type { Component, TUI } from "@kolisachint/hoocode-tui";
12
12
  import type { AgentSession } from "../../core/agent-session.js";
13
+ import type { AppKeybinding } from "../../core/keybindings.js";
13
14
  /** The slice of the interactive mode the model flows need. */
14
15
  export interface ModelControllerDeps {
15
16
  ui: TUI;
@@ -20,6 +21,8 @@ export interface ModelControllerDeps {
20
21
  focus: Component;
21
22
  }): void;
22
23
  showStatus(message: string): void;
24
+ /** Report a dial step, naming its reverse key the first time. */
25
+ showDialStep(backward: AppKeybinding, message: string): void;
23
26
  showError(errorMessage: string): void;
24
27
  showWarning(warningMessage: string): void;
25
28
  /** A filled notice, for warnings that cost money if ignored. */
@@ -1 +1 @@
1
- {"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;AAehE,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,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,CAiBjE;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 { 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\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.showStatus(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.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);\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.showStatus(`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.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\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.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.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"]}
1
+ {"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,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/** 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.showStatus(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.showStatus(`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.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\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.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.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"]}
@@ -96,7 +96,7 @@ export class ModelController {
96
96
  this.deps.invalidateFooter();
97
97
  this.deps.updateEditorBorderColor();
98
98
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
99
- this.deps.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
99
+ this.deps.showDialStep(direction === "forward" ? "app.model.cycleBackward" : "app.model.cycleForward", `Switched to ${result.model.name || result.model.id}${thinkingStr}`);
100
100
  void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
101
101
  }
102
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"model-controller.js","sourceRoot":"","sources":["../../../src/modes/interactive/model-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,4BAA4B,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AAEvF,MAAM,iCAAiC,GAAG,wBAAwB,CAAC;AACnE,MAAM,gCAAgC,GAAG;IACxC,2DAA2D;IAC3D,kDAAgD;CAChD,CAAC;AAEF,SAAS,8BAA8B,CAAC,MAA0B,EAAW;IAC5E,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AAAA,CACrE;AAkBD,MAAM,OAAO,eAAe;IAGE,IAAI;IAFzB,iCAAiC,GAAG,KAAK,CAAC;IAElD,YAA6B,IAAyB,EAAE;oBAA3B,IAAI;IAAwB,CAAC;IAE1D,IAAY,OAAO,GAAiB;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;IAAA,CACzB;IAED,KAAK,CAAC,mBAAmB,CAAC,UAAkB,EAAmC;QAC9E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC/C,OAAO,4BAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAAA,CACxD;IAEO,KAAK,CAAC,kBAAkB,GAA0B;QACzD,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChE,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC;YACJ,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,CAAC;QACX,CAAC;IAAA,CACD;IAED,iFAAiF;IACjF,KAAK,CAAC,4BAA4B,GAAkB;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC/C,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAAA,CAC1D;IAED,iFAAiF;IACzE,KAAK,CAAC,6BAA6B,CAAC,KAA6B,EAAoB;QAC5F,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,mBAAmB,KAAK,KAAK,EAAE,CAAC;YAC9E,OAAO,KAAK,CAAC;QACd,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAC9C,OAAO,KAAK,CAAC;QACd,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACjF,IAAI,gBAAgB,EAAE,IAAI,KAAK,OAAO,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,oBAAoB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACrF,OAAO,8BAA8B,CAAC,MAAM,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACR,uDAAuD;YACvD,OAAO,KAAK,CAAC;QACd,CAAC;IAAA,CACD;IAED;;;;OAIG;IACH,KAAK,CAAC,uCAAuC,CAAC,KAAK,GAA2B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAiB;QAChH,IAAI,IAAI,CAAC,iCAAiC,EAAE,CAAC;YAC5C,OAAO;QACR,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,6BAA6B,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACxD,OAAO;QACR,CAAC;QACD,IAAI,CAAC,iCAAiC,GAAG,IAAI,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,iCAAiC,EAAE,gCAAgC,CAAC,CAAC;IAAA,CAC1F;IAED,KAAK,CAAC,UAAU,CAAC,SAAiC,EAAiB;QAClE,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YACxD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,0BAA0B,CAAC;gBAC1G,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACP,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC;gBACpC,MAAM,WAAW,GAChB,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,MAAM,CAAC,aAAa,KAAK,KAAK,CAAC,CAAC,CAAC,eAAe,MAAM,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,WAAW,EAAE,CAAC,CAAC;gBAC1F,KAAK,IAAI,CAAC,uCAAuC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACjE,CAAC;QACF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7E,CAAC;IAAA,CACD;IAED,iBAAiB,CAAC,kBAA2B,EAAQ;QACpD,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAG,IAAI,sBAAsB,CAC1C,IAAI,CAAC,IAAI,CAAC,EAAE,EACZ,IAAI,CAAC,OAAO,CAAC,KAAK,EAClB,IAAI,CAAC,OAAO,CAAC,eAAe,EAC5B,IAAI,CAAC,OAAO,CAAC,aAAa,EAC1B,IAAI,CAAC,OAAO,CAAC,YAAY,EACzB,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACJ,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;oBACnC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBAC7B,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC;oBACpC,IAAI,EAAE,CAAC;oBACP,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;oBAC3C,KAAK,IAAI,CAAC,uCAAuC,CAAC,KAAK,CAAC,CAAC;gBAC1D,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAChB,IAAI,EAAE,CAAC;oBACP,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC7E,CAAC;YAAA,CACD,EACD,GAAG,EAAE,CAAC;gBACL,IAAI,EAAE,CAAC;gBACP,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;YAAA,CAC7B,EACD,kBAAkB,CAClB,CAAC;YACF,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAAA,CAChD,CAAC,CAAC;IAAA,CACH;IAED,KAAK,CAAC,kBAAkB,GAAkB;QACzC,2BAA2B;QAC3B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAE5D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC;YAC5C,OAAO;QACR,CAAC;QAED,0FAA0F;QAC1F,MAAM,mBAAmB,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;QACtD,MAAM,eAAe,GAAG,mBAAmB,CAAC,MAAM,GAAG,CAAC,CAAC;QAEvD,yDAAyD;QACzD,IAAI,iBAAiB,GAAoB,IAAI,CAAC;QAE9C,IAAI,eAAe,EAAE,CAAC;YACrB,sCAAsC;YACtC,iBAAiB,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QACxG,CAAC;aAAM,CAAC;YACP,wBAAwB;YACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,gBAAgB,EAAE,CAAC;YACjE,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnD,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;gBACnF,iBAAiB,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;YACjG,CAAC;QACF,CAAC;QAED,sEAAsE;QACtE,MAAM,mBAAmB,GAAG,KAAK,EAAE,UAA2B,EAAE,EAAE,CAAC;YAClE,iBAAiB,GAAG,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC;YACjE,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;gBACjF,MAAM,eAAe,GAAG,MAAM,iBAAiB,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;gBACxF,IAAI,CAAC,OAAO,CAAC,eAAe,CAC3B,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;oBAC5B,KAAK,EAAE,EAAE,CAAC,KAAK;oBACf,aAAa,EAAE,EAAE,CAAC,aAAa;iBAC/B,CAAC,CAAC,CACH,CAAC;YACH,CAAC;iBAAM,CAAC;gBACP,0CAA0C;gBAC1C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,MAAM,IAAI,CAAC,4BAA4B,EAAE,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QAAA,CAC7B,CAAC;QAEF,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAG,IAAI,6BAA6B,CACjD;gBACC,SAAS;gBACT,eAAe,EAAE,iBAAiB;aAClC,EACD;gBACC,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;oBAC/B,MAAM,mBAAmB,CAAC,UAAU,CAAC,CAAC;gBAAA,CACtC;gBACD,SAAS,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC;oBAC1B,sBAAsB;oBACtB,MAAM,WAAW,GAChB,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;wBAC5D,CAAC,CAAC,SAAS,CAAC,6BAA6B;wBACzC,CAAC,CAAC,UAAU,CAAC;oBACf,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;oBAC1F,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,mCAAmC,CAAC,CAAC;gBAAA,CAC1D;gBACD,QAAQ,EAAE,GAAG,EAAE,CAAC;oBACf,IAAI,EAAE,CAAC;oBACP,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;gBAAA,CAC7B;aACD,CACD,CAAC;YACF,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAAA,CAChD,CAAC,CAAC;IAAA,CACH;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 { 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\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.showStatus(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.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);\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.showStatus(`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.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\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.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.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"]}
1
+ {"version":3,"file":"model-controller.js","sourceRoot":"","sources":["../../../src/modes/interactive/model-controller.ts"],"names":[],"mappings":"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","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/** 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.showStatus(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.showStatus(`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.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\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.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.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"]}
@@ -4,10 +4,98 @@ All keyboard shortcuts can be customized via `~/.hoocode/keybindings.json`. Each
4
4
 
5
5
  The config file uses the same namespaced keybinding ids that hoocode uses internally and that extension authors use in `keyHint()` and injected `keybindings` managers.
6
6
 
7
- Older configs using pre-namespaced ids such as `cursorUp` or `expandTools` are migrated automatically to the namespaced ids on startup.
7
+ Older configs using pre-namespaced ids such as `cursorUp` or `expandTools` are migrated automatically to the namespaced ids on startup, as are `app.thinking.cycle` and `app.mode.cycle`, which became `…cycleForward` when each gained a backward half.
8
8
 
9
9
  After editing `keybindings.json`, run `/reload` in hoocode to apply the changes without restarting the session.
10
10
 
11
+ ## How this is grouped
12
+
13
+ There are around sixty bindings. Nobody holds sixty of anything, and the obvious
14
+ answer — sort them by mechanism, so everything that cycles sits together — makes
15
+ a list that is tidy on the page and useless at the keyboard. "It cycles" is a
16
+ fact about the widget. It is not what you are thinking when you reach for a key.
17
+
18
+ So the grouping is by **intention**: the five things you are ever doing here, in
19
+ the order the loop runs.
20
+
21
+ | Group | What it is | Keys |
22
+ |---|---|---|
23
+ | **Compose** | the message in your hands | `Alt+E` `Alt+R` `Ctrl+V` `Alt+Enter` `Alt+↑` |
24
+ | **Steer** | what the agent is before it runs | `Alt+A` `Alt+M` `Alt+T` |
25
+ | **Read** | what you see of what it did | `Alt+O` `Alt+L` `Ctrl+O` `Ctrl+T` |
26
+ | **Go** | sessions and places | `Alt+H` `Alt+W` `Alt+C` `Alt+S` `Alt+K` |
27
+ | **Flow** | getting out, getting back | `Esc` `Ctrl+C` `Ctrl+D` `Ctrl+Z` |
28
+
29
+ Five groups, none bigger than five, which is about the size a person can hold at
30
+ once. Two of them cost nothing to learn: **Flow** is the set every terminal
31
+ program already taught you, and the pickers print their own keys on their own
32
+ hint lines — read, never remembered. That leaves three groups to genuinely know.
33
+
34
+ The declaration order in `core/keybindings.ts` is this grouping, and it is not
35
+ cosmetic: your `keybindings.json` is written in the same order, so the file you
36
+ open to rebind something is grouped the way this page is.
37
+
38
+ ## What the chord tells you
39
+
40
+ The modifier says what kind of thing will happen; the letter says to what.
41
+
42
+ **`Alt`+letter sets a value.** Nothing takes the screen, nothing loses focus, you
43
+ keep typing. Six of these are dials — an ordered set of stops with the current
44
+ one painted where you can see it — and `Shift+Alt`+letter always steps back:
45
+
46
+ | Dial | Where you see it | Forward | Back | Steps through |
47
+ |---|---|---|---|---|
48
+ | **A**gent mode | footer, bold, top left | `Alt+A` | `Shift+Alt+A` | ask → plan → build → debug |
49
+ | **M**odel | footer, bottom right | `Alt+M` | `Shift+Alt+M` | your enabled models |
50
+ | **T**hinking level | footer, bottom right | `Alt+T` | `Shift+Alt+T` | off → … → high |
51
+ | Tool **o**utput | footer, top right | `Alt+O` | `Shift+Alt+O` | radar → peek → full |
52
+ | Task **l**ist | task panel header | `Alt+L` | `Shift+Alt+L` | tasks → subagents → teams |
53
+ | Session **c**olor | the session chip | `Alt+C` | `Shift+Alt+C` | six chip slots |
54
+
55
+ Reversibility is the point, not symmetry. A control you can undo invites you to
56
+ try it; a one-way control makes you stop and think first, which is the wrong tax
57
+ on a key you press all day. The first time you step each dial in a session, the
58
+ status line names the key that steps it back — once, at the moment you are
59
+ primed to learn it, and never again after that.
60
+
61
+ **`Ctrl`+letter acts on what is drawn right now**, and shares its letter with the
62
+ `Alt` key for the same subject:
63
+
64
+ - `Alt+O` sets how much tool output there ever is and saves where it lands;
65
+ `Ctrl+O` jumps to all of it and back, leaving the dial where it was.
66
+ - `Alt+T` sets how much thinking there ever is; `Ctrl+T` shows or hides the
67
+ thinking you have.
68
+
69
+ Two subjects, two letters, four keys — half what four unrelated chords cost.
70
+
71
+ **A slash command picks a stop outright.** `/mode`, `/model`, `/color`, `/tree`.
72
+ The key steps, the command chooses; that is why `app.model.select` and
73
+ `app.session.tree` ship unbound, each having given its letter to a dial.
74
+
75
+ **Inside a picker, `Ctrl` belongs to the query you are typing** — `Ctrl+A` is
76
+ start of line, `Ctrl+U` kills the line, `Ctrl+W` kills a word. A picker's verbs
77
+ are therefore all on `Alt`, mnemonic to that picker. The picker captures keys
78
+ while it is open, so a verb may reuse a letter the global set already has.
79
+
80
+ One letter in the whole set names nothing: `Alt+N`, the team roster. It survives
81
+ because the task panel prints it in its own header — read off the screen rather
82
+ than remembered, which is the fallback for anything that cannot earn a mnemonic.
83
+
84
+ The thinking level is the one dial with a second key: `Shift+Tab` still steps it.
85
+ It is the only dial with no slash command, so on a terminal that does not send
86
+ `Alt` that is the way to it.
87
+
88
+ Two consequences worth knowing before rebinding:Two consequences worth knowing before rebinding:
89
+
90
+ - No default takes a key the editor or the terminal already owns —
91
+ `Ctrl+A/E/B/F/K/U/W/Y/D/L/R/G`, `Ctrl+S` (XOFF), `Ctrl+M` (enter), `Ctrl+I`
92
+ (tab).
93
+ - No verb sits on a bare `Shift+<letter>`. Outside the Kitty keyboard protocol
94
+ that arrives as the plain uppercase letter, which is indistinguishable from
95
+ typing in any scope that has a query line.
96
+
97
+ `test/keybinding-layout.test.ts` holds all of this.
98
+
11
99
  ## Key Format
12
100
 
13
101
  `modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are:
@@ -24,14 +112,102 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
24
112
  > protocol, `Option` composes characters instead of sending `Alt` — `Option+M`
25
113
  > types `µ`. Some Linux terminals claim `Alt+<letter>` for menu mnemonics. See
26
114
  > [terminal-setup.md](terminal-setup.md#alt-keys) for the per-terminal setting.
115
+ >
116
+ > Interrupt, clear, exit, expand and submit are deliberately never on `Alt`, so
117
+ > a terminal that eats `Option` is still recoverable.
27
118
 
28
119
  ## All Actions
29
120
 
30
- ### TUI Editor Cursor Movement
121
+ Listed in the five families, in the order `core/keybindings.ts` declares them —
122
+ which is also the order your `keybindings.json` is written in.
123
+
124
+ ### Compose — the message in your hands
125
+
126
+ | Keybinding id | Default | Description |
127
+ |--------|---------|-------------|
128
+ | `tui.input.submit` | `enter` | Send message |
129
+ | `tui.input.newLine` | `shift+enter` | Insert new line |
130
+ | `tui.input.tab` | `tab` | Path completion / accept autocomplete |
131
+ | `app.editor.external` | `alt+e` | Edit the message in `$VISUAL` / `$EDITOR` |
132
+ | `app.input.voiceTranscribe` | `alt+r` | Speak instead of type |
133
+ | `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
134
+ | `app.message.followUp` | `alt+enter` | Queue a follow-up while the agent works |
135
+ | `app.message.dequeue` | `alt+up` | Bring every queued message back to the editor |
136
+
137
+ ### Steer — what the agent is before it runs
138
+
139
+ The only three that change what happens next, and the only three that cost
140
+ anything. The footer shows all three.
141
+
142
+ | Keybinding id | Default | Description |
143
+ |--------|---------|-------------|
144
+ | `app.mode.cycleForward` | `alt+a` | Step agent mode: ask → plan → build → debug |
145
+ | `app.mode.cycleBackward` | `shift+alt+a` | Step agent mode backward |
146
+ | `app.model.cycleForward` | `alt+m` | Step to the next model |
147
+ | `app.model.cycleBackward` | `shift+alt+m` | Step to the previous model |
148
+ | `app.model.select` | *(none)* | Open the model selector (`/model`) |
149
+ | `app.thinking.cycleForward` | `alt+t`, `shift+tab` | Step thinking level: off → … → high |
150
+ | `app.thinking.cycleBackward` | `shift+alt+t` | Step thinking level backward |
151
+
152
+ ### Read — what you see of what it did
153
+
154
+ Free and reversible, every one: nothing here touches the work, only the window
155
+ onto it.
156
+
157
+ | Keybinding id | Default | Description |
158
+ |--------|---------|-------------|
159
+ | `app.view.cycleForward` | `alt+o` | Step tool output: radar → peek → full |
160
+ | `app.view.cycleBackward` | `shift+alt+o` | Step tool output backward |
161
+ | `app.tools.expand` | `ctrl+o` | Jump to the full view and back, without moving the dial |
162
+ | `app.thinking.toggle` | `ctrl+t` | Show or hide thinking blocks |
163
+ | `app.tasks.cycleForward` | `alt+l` | Step task panel view: tasks → subagents → teams |
164
+ | `app.tasks.cycleBackward` | `shift+alt+l` | Step task panel view backward |
165
+ | `app.team.focus` | `alt+n` | Focus the team roster (`--team`) |
166
+
167
+ ### Go — sessions and places
168
+
169
+ Each takes the screen and hands it back on `escape`, and each has a slash command
170
+ that does the same thing.
31
171
 
32
172
  | Keybinding id | Default | Description |
33
173
  |--------|---------|-------------|
34
- | `tui.editor.cursorUp` | `up` | Move cursor up |
174
+ | `app.session.resume` | `alt+h` | Resume a session from history (`/resume`) |
175
+ | `app.session.tree` | *(none)* | Open the session tree (`/tree`) |
176
+ | `app.session.new` | *(none)* | Start a new session (`/new`) |
177
+ | `app.session.fork` | *(none)* | Fork the current session (`/fork`) |
178
+ | `app.session.changeDirectory` | `alt+w` | Change working directory (`/cd`) |
179
+ | `app.session.color.cycleForward` | `alt+c` | Step the session chip's color |
180
+ | `app.session.color.cycleBackward` | `shift+alt+c` | Step the session chip's color backward |
181
+ | `app.settings.open` | `alt+s` | Open settings (`/settings`) |
182
+ | `app.hotkeys.open` | `alt+k` | Show the shortcut list (`/hotkeys`) |
183
+
184
+ `app.session.new` and `app.session.fork` ship unbound because one replaces the
185
+ transcript and the other needs a message picked out of it, so neither wants to be
186
+ one stray chord away. `app.model.select` and `app.session.tree` are unbound for a
187
+ different reason — each gave its letter to the dial that shares it. All four are
188
+ bindable by hand.
189
+
190
+ ### Flow — getting out, getting back
191
+
192
+ Every terminal program already bound these, so they cost nothing to learn and
193
+ never move: whatever else is misconfigured, you can still stop the agent, clear
194
+ the line, and leave.
195
+
196
+ | Keybinding id | Default | Description |
197
+ |--------|---------|-------------|
198
+ | `app.interrupt` | `escape` | Cancel autocomplete / abort streaming |
199
+ | `app.clear` | `ctrl+c` | Clear editor (press twice to exit) |
200
+ | `app.exit` | `ctrl+d` | Exit when the editor is empty |
201
+ | `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background |
202
+
203
+ ### The editor
204
+
205
+ Standard readline/emacs bindings. Reference, not something to memorise — which
206
+ is why they are not one of the five groups.
207
+
208
+ | Keybinding id | Default | Description |
209
+ |--------|---------|-------------|
210
+ | `tui.editor.cursorUp` | `up` | Move cursor up / browse history when empty |
35
211
  | `tui.editor.cursorDown` | `down` | Move cursor down |
36
212
  | `tui.editor.cursorLeft` | `left`, `ctrl+b` | Move cursor left |
37
213
  | `tui.editor.cursorRight` | `right`, `ctrl+f` | Move cursor right |
@@ -43,39 +219,32 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
43
219
  | `tui.editor.jumpBackward` | `ctrl+alt+]` | Jump backward to character |
44
220
  | `tui.editor.pageUp` | `pageUp` | Scroll up by page |
45
221
  | `tui.editor.pageDown` | `pageDown` | Scroll down by page |
46
-
47
- ### TUI Editor Deletion
48
-
49
- | Keybinding id | Default | Description |
50
- |--------|---------|-------------|
51
222
  | `tui.editor.deleteCharBackward` | `backspace` | Delete character backward |
52
223
  | `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |
53
224
  | `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |
54
225
  | `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |
55
226
  | `tui.editor.deleteToLineStart` | `ctrl+u` | Delete to line start |
56
227
  | `tui.editor.deleteToLineEnd` | `ctrl+k` | Delete to line end |
228
+ | `tui.editor.yank` | `ctrl+y` | Paste the most recently deleted text |
229
+ | `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after a yank |
230
+ | `tui.editor.undo` | `ctrl+-` | Undo last edit |
231
+ | `tui.input.copy` | `ctrl+c` | Copy the selection |
57
232
 
58
- ### TUI Input
233
+ `ctrl+d` and `ctrl+c` each carry two ids. They are one key with a
234
+ state-dependent meaning, not a conflict: `ctrl+d` exits only on an empty
235
+ editor and deletes a character otherwise, and `ctrl+c` copies when there is a
236
+ selection and clears the editor when there is not.
59
237
 
60
- | Keybinding id | Default | Description |
61
- |--------|---------|-------------|
62
- | `tui.input.newLine` | `shift+enter` | Insert new line |
63
- | `tui.input.submit` | `enter` | Submit input |
64
- | `tui.input.tab` | `tab` | Tab / autocomplete |
65
-
66
- ### TUI Kill Ring
238
+ ### Overlays live only while their surface is open
67
239
 
68
- | Keybinding id | Default | Description |
69
- |--------|---------|-------------|
70
- | `tui.editor.yank` | `ctrl+y` | Paste most recently deleted text |
71
- | `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after yank |
72
- | `tui.editor.undo` | `ctrl+-` | Undo last edit |
240
+ Never a memory burden: each surface prints its own keys on its own hint line, so
241
+ these are recognised, not recalled. They may reuse a letter the global set
242
+ already has, because the surface captures keys while it is up.
73
243
 
74
- ### TUI Clipboard and Selection
244
+ #### Lists and pickers, everywhere
75
245
 
76
246
  | Keybinding id | Default | Description |
77
247
  |--------|---------|-------------|
78
- | `tui.input.copy` | `ctrl+c` | Copy selection |
79
248
  | `tui.select.up` | `up` | Move selection up |
80
249
  | `tui.select.down` | `down` | Move selection down |
81
250
  | `tui.select.pageUp` | `pageUp` | Page up in list |
@@ -83,82 +252,73 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
83
252
  | `tui.select.confirm` | `enter` | Confirm selection |
84
253
  | `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |
85
254
 
86
- ### Application
255
+ #### Session picker (`/resume`)
87
256
 
88
257
  | Keybinding id | Default | Description |
89
258
  |--------|---------|-------------|
90
- | `app.interrupt` | `escape` | Cancel / abort |
91
- | `app.clear` | `ctrl+c` | Clear editor |
92
- | `app.exit` | `ctrl+d` | Exit (when editor empty) |
93
- | `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background |
94
- | `app.editor.external` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) |
95
- | `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
259
+ | `app.session.togglePath` | `alt+p` | Toggle path display |
260
+ | `app.session.toggleSort` | `alt+o` | Toggle sort order |
261
+ | `app.session.toggleNamedFilter` | `alt+n` | Toggle the named-only filter |
262
+ | `app.session.rename` | `alt+r` | Rename the selected session |
263
+ | `app.session.delete` | `alt+x` | Delete the selected session |
264
+ | `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete, but only while the query is empty |
96
265
 
97
- ### Sessions
266
+ #### Session tree (`/tree`)
98
267
 
99
268
  | Keybinding id | Default | Description |
100
269
  |--------|---------|-------------|
101
- | `app.session.new` | *(none)* | Start a new session (`/new`) |
102
- | `app.session.tree` | *(none)* | Open session tree navigator (`/tree`) |
103
- | `app.session.fork` | *(none)* | Fork current session (`/fork`) |
104
- | `app.session.resume` | *(none)* | Open session resume picker (`/resume`) |
105
- | `app.session.togglePath` | `ctrl+p` | Toggle path display |
106
- | `app.session.toggleSort` | `ctrl+s` | Toggle sort mode |
107
- | `app.session.toggleNamedFilter` | `ctrl+n` | Toggle named-only filter |
108
- | `app.session.rename` | `ctrl+r` | Rename session |
109
- | `app.session.delete` | `ctrl+d` | Delete session |
110
- | `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete session when query is empty |
111
- | `app.session.color.cycleForward` | `alt+c` | Cycle the session chip's color |
112
- | `app.session.color.cycleBackward` | `shift+alt+c` | Cycle the session chip's color backward |
113
-
114
- ### Models and Thinking
270
+ | `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold the branch segment, or jump to the previous one |
271
+ | `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold the branch segment, or jump to the next one |
272
+ | `app.tree.editLabel` | `alt+l` | Edit the label on the selected node |
273
+ | `app.tree.toggleLabelTimestamp` | `alt+t` | Show or hide label timestamps |
274
+ | `app.tree.filter.default` | `alt+1` | Filter: default view |
275
+ | `app.tree.filter.noTools` | `alt+2` | Filter: hide tool results |
276
+ | `app.tree.filter.userOnly` | `alt+3` | Filter: user messages only |
277
+ | `app.tree.filter.labeledOnly` | `alt+4` | Filter: labeled entries only |
278
+ | `app.tree.filter.all` | `alt+5` | Filter: show everything |
279
+ | `app.tree.filter.cycleForward` | `alt+c` | Cycle the filter forward |
280
+ | `app.tree.filter.cycleBackward` | `shift+alt+c` | Cycle the filter backward |
281
+
282
+ The five lenses are numbered rather than lettered because they are an ordered
283
+ set: `alt+1`-`alt+5` needs no mnemonic. Everything else the tree types goes
284
+ into its search query.
285
+
286
+ #### Scoped models picker (`/models`)
115
287
 
116
288
  | Keybinding id | Default | Description |
117
289
  |--------|---------|-------------|
118
- | `app.model.select` | `ctrl+l` | Open model selector |
119
- | `app.model.cycleForward` | `ctrl+p` | Cycle to next model |
120
- | `app.model.cycleBackward` | `shift+ctrl+p` | Cycle to previous model |
121
- | `app.thinking.cycle` | `shift+tab` | Cycle thinking level |
122
- | `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |
290
+ | `app.models.save` | `alt+s` | Save the selection to settings |
291
+ | `app.models.enableAll` | `alt+a` | Enable all models (or all matching the query) |
292
+ | `app.models.clearAll` | `alt+x` | Clear all models (or all matching the query) |
293
+ | `app.models.toggleProvider` | `alt+g` | Toggle every model for the current provider |
294
+ | `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |
295
+ | `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |
123
296
 
124
- ### Display and Message Queue
297
+ `alt+g` ("group") rather than the obvious `alt+p`: without the Kitty protocol
298
+ `alt+p` arrives as `ESC-p`, which the parser also reads as `alt+up` — this
299
+ picker's reorder key.
125
300
 
126
- | Keybinding id | Default | Description |
127
- |--------|---------|-------------|
128
- | `app.tools.expand` | `ctrl+o` | Jump to the full tool-output view, and back to the stop you came from |
129
- | `app.view.cycleForward` | `alt+o` | Cycle the tool output view (radar → peek → full) |
130
- | `app.view.cycleBackward` | `shift+alt+o` | Cycle the tool output view backward |
131
- | `app.message.followUp` | `alt+enter` | Queue follow-up message |
132
- | `app.message.dequeue` | `alt+up` | Restore queued messages to editor |
301
+ #### Team roster (`--team`)
133
302
 
134
- ### Tree Navigation
303
+ Live only while the task panel holds focus, which is why they are plain
304
+ letters: you are never typing there.
135
305
 
136
306
  | Keybinding id | Default | Description |
137
307
  |--------|---------|-------------|
138
- | `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold current branch segment, or jump to the previous segment start |
139
- | `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold current branch segment, or jump to the next segment start or branch end |
140
- | `app.tree.editLabel` | `shift+l` | Edit the label on the selected tree node |
141
- | `app.tree.toggleLabelTimestamp` | `shift+t` | Toggle label timestamps in the tree |
142
- | `app.tree.filter.default` | `ctrl+d` | Set tree filter to default view |
143
- | `app.tree.filter.noTools` | `ctrl+t` | Toggle tree filter that hides tool results |
144
- | `app.tree.filter.userOnly` | `ctrl+u` | Toggle tree filter that shows only user messages |
145
- | `app.tree.filter.labeledOnly` | `ctrl+l` | Toggle tree filter that shows only labeled entries |
146
- | `app.tree.filter.all` | `ctrl+a` | Toggle tree filter that shows all entries |
147
- | `app.tree.filter.cycleForward` | `ctrl+o` | Cycle tree filter forward |
148
- | `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Cycle tree filter backward |
149
-
150
- ### Scoped Models Selector
151
-
152
- Used inside the scoped models selector (opened via `/scoped-models`).
308
+ | `app.team.nudge` | `n` | Nudge the selected role |
309
+ | `app.team.attach` | `a` | Attach to the selected role |
310
+
311
+ #### Options pane
312
+
313
+ The pane the agent raises to ask you a question.
153
314
 
154
315
  | Keybinding id | Default | Description |
155
316
  |--------|---------|-------------|
156
- | `app.models.save` | `ctrl+s` | Save current model selection to settings |
157
- | `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |
158
- | `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |
159
- | `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |
160
- | `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |
161
- | `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |
317
+ | `app.options.next` | `right` | Confirm the highlighted answer and advance |
318
+ | `app.options.back` | `left` | Go back to the previous question |
319
+
320
+ On the free-text row the arrows are the text cursor's first, and only act on
321
+ the step at the ends of what you have typed. `enter` always commits.
162
322
 
163
323
  ## Custom Configuration
164
324
 
@@ -192,6 +352,10 @@ On native Windows, `app.suspend` has no default binding because Windows terminal
192
352
  }
193
353
  ```
194
354
 
355
+ `ctrl+p` and `ctrl+n` are both free of app bindings by default — the model and
356
+ task-ledger dials that used to hold them are on `alt+m` and `alt+l` now — so
357
+ this config collides with nothing.
358
+
195
359
  ### Vim Example
196
360
 
197
361
  ```json
@@ -204,3 +368,6 @@ On native Windows, `app.suspend` has no default binding because Windows terminal
204
368
  "tui.editor.cursorWordRight": ["alt+right", "alt+w"]
205
369
  }
206
370
  ```
371
+
372
+ This one takes `alt+h` from the session picker, `alt+k` from the shortcut list
373
+ and `alt+w` from `/cd`; rebind those too if you use them.
@@ -8,13 +8,18 @@ Work out of the box.
8
8
 
9
9
  ## Alt keys
10
10
 
11
- 37 actions are reachable only with `Alt` — the model selector (`Alt+M`), settings
11
+ 43 actions are reachable only with `Alt` — five of the six dials (`Alt+A` mode,
12
+ `Alt+M` model, `Alt+O` output, `Alt+L` task ledger, `Alt+C` color), settings
12
13
  (`Alt+S`), the shortcut list (`Alt+K`), every picker verb, and the tree filters.
13
14
  A terminal that speaks the Kitty keyboard protocol reports `Alt` correctly and
14
15
  needs nothing here.
15
16
 
16
- Interrupt (`Esc`), clear (`Ctrl+C`), exit (`Ctrl+D`) and expand (`Ctrl+O`) are
17
- deliberately never on `Alt`, so a misconfigured terminal is recoverable.
17
+ Interrupt (`Esc`), clear (`Ctrl+C`), exit (`Ctrl+D`), expand (`Ctrl+O`) and
18
+ submit (`Enter`) are deliberately never on `Alt`, so a misconfigured terminal is
19
+ recoverable. So is every dial that changes what the agent does: the thinking
20
+ level also steps on `Shift+Tab`, and `/mode`, `/model` and `/color` reach the
21
+ rest. Only the two that change what is drawn — the tool output dial and the task
22
+ ledger's lens — need `Alt`, and `Ctrl+O` still reaches full output and back.
18
23
 
19
24
  ### macOS
20
25
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.3.57",
4
+ "version": "0.3.59",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.3.57",
4
+ "version": "0.3.59",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.3.57",
4
+ "version": "0.3.59",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"