@gajae-code/coding-agent 0.12.21 → 0.13.0

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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -14,20 +14,24 @@ import {
14
14
  truncateToWidth,
15
15
  } from "@gajae-code/tui";
16
16
  import { sanitizeText } from "@gajae-code/utils";
17
+ import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
17
18
  import {
18
19
  getModelProfilePresentation,
19
20
  groupModelProfilesForPresetLanding,
20
21
  type ModelProfileDefinition,
22
+ resolveProfileBindings,
21
23
  } from "../../config/model-profiles";
22
24
  import type { GjcModelAssignmentTargetId, ModelRegistry } from "../../config/model-registry";
23
25
  import {
24
26
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
25
27
  GJC_MODEL_ASSIGNMENT_TARGETS,
26
28
  isAuthenticated,
29
+ kNoAuth,
27
30
  } from "../../config/model-registry";
28
31
  import {
29
32
  formatModelSelectorValue,
30
33
  resolveConfiguredModelPatterns,
34
+ resolveModelChainWithAuth,
31
35
  resolveModelRoleValue,
32
36
  type ScopedModelSelection,
33
37
  } from "../../config/model-resolver";
@@ -39,7 +43,6 @@ import type { Settings } from "../../config/settings";
39
43
  import { type ThemeColor, theme } from "../../modes/theme/theme";
40
44
  import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
41
45
  import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
42
- import { getConfiguredImageModel } from "../../tools/image-gen";
43
46
  import { getTabBarTheme } from "../shared";
44
47
  import { DynamicBorder } from "./dynamic-border";
45
48
 
@@ -143,9 +146,6 @@ export type ModelSelectorSelection =
143
146
  | {
144
147
  kind: "deleteProfile";
145
148
  profileName: string;
146
- }
147
- | {
148
- kind: "imageGeneration";
149
149
  };
150
150
 
151
151
  interface PendingThinkingChoice {
@@ -211,8 +211,8 @@ interface PresetBrowseRow {
211
211
  kind: "browse";
212
212
  }
213
213
 
214
- interface PresetImageGenerationRow {
215
- kind: "imageGeneration";
214
+ interface PresetImageRoleRow {
215
+ kind: "imageRole";
216
216
  }
217
217
 
218
218
  type PresetLandingRow =
@@ -222,7 +222,7 @@ type PresetLandingRow =
222
222
  | PresetCreateUnavailableRow
223
223
  | PresetAlreadySavedRow
224
224
  | PresetBrowseRow
225
- | PresetImageGenerationRow;
225
+ | PresetImageRoleRow;
226
226
 
227
227
  // Stable logical identity for a preset landing row, independent of its current
228
228
  // list position. Used to relocate the cursor after the expanded group changes so
@@ -241,8 +241,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
241
241
  return "createUnavailable";
242
242
  case "alreadySaved":
243
243
  return `alreadySaved:${row.profile.name}`;
244
- case "imageGeneration":
245
- return "imageGeneration";
244
+ case "imageRole":
245
+ return "imageRole";
246
246
  }
247
247
  }
248
248
 
@@ -383,9 +383,11 @@ export class ModelSelectorComponent extends Container {
383
383
  #presetScopeMenuOpen: boolean = false;
384
384
  #presetScopeIndex: number = 0;
385
385
  #providerAuthById = new Map<string, boolean>();
386
+ #bareProfileAuthByName = new Map<string, boolean>();
386
387
  #providerAuthPending: boolean = false;
387
388
  #presetLoginHint?: string;
388
389
  #authSessionId?: string;
390
+ #imageRoleFilter: boolean = false;
389
391
 
390
392
  // Tab state
391
393
  #providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
@@ -515,6 +517,8 @@ export class ModelSelectorComponent extends Container {
515
517
  settings: this.#settings,
516
518
  matchPreferences,
517
519
  modelRegistry: this.#modelRegistry,
520
+ aliasIntent: this.#activeModelProfile ? "preset-equivalent" : undefined,
521
+ credentialSessionId: this.#authSessionId,
518
522
  });
519
523
  return resolved.model !== undefined && modelsAreEqual(resolved.model, this.#currentModel);
520
524
  });
@@ -525,16 +529,28 @@ export class ModelSelectorComponent extends Container {
525
529
  const allModels = this.#modelRegistry.getAll();
526
530
  const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
527
531
  const agentModelOverrides = this.#settings.get("task.agentModelOverrides");
532
+ const activeProfile = this.#activeModelProfile
533
+ ? this.#modelRegistry.getModelProfile(this.#activeModelProfile)
534
+ : undefined;
535
+ const activeProfileBindings = activeProfile ? resolveProfileBindings(activeProfile) : undefined;
528
536
  for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
529
537
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
530
538
  const roleValue =
531
539
  target.settingsPath === "modelRoles" ? this.#settings.getModelRole(role) : agentModelOverrides[role];
532
540
  if (!roleValue) continue;
541
+ const profileOwnsRole =
542
+ role === "default"
543
+ ? activeProfileBindings?.defaultSelector !== undefined
544
+ : target.settingsPath === "modelRoles"
545
+ ? Object.hasOwn(activeProfileBindings?.modelRoles ?? {}, role)
546
+ : Object.hasOwn(activeProfileBindings?.agentModelOverrides ?? {}, role);
533
547
 
534
548
  const resolved = resolveModelRoleValue(roleValue, allModels, {
535
549
  settings: this.#settings,
536
550
  matchPreferences,
537
551
  modelRegistry: this.#modelRegistry,
552
+ aliasIntent: profileOwnsRole ? "preset-equivalent" : undefined,
553
+ credentialSessionId: this.#authSessionId,
538
554
  });
539
555
  if (resolved.model) {
540
556
  roles[role] = {
@@ -1012,8 +1028,11 @@ export class ModelSelectorComponent extends Container {
1012
1028
  // Start with all models or filter by provider/canonical view
1013
1029
  let baseModels = this.#allModels;
1014
1030
  const baseCanonicalModels = this.#canonicalModels;
1031
+ if (this.#imageRoleFilter) {
1032
+ baseModels = baseModels.filter(m => m.model.output?.includes("image") ?? false);
1033
+ }
1015
1034
  if (activeProviderId) {
1016
- baseModels = this.#allModels.filter(m => m.provider === activeProviderId);
1035
+ baseModels = baseModels.filter(m => m.provider === activeProviderId);
1017
1036
  }
1018
1037
 
1019
1038
  // Apply fuzzy filter if query is present
@@ -1104,6 +1123,8 @@ export class ModelSelectorComponent extends Container {
1104
1123
  settings: this.#settings,
1105
1124
  matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
1106
1125
  modelRegistry: this.#modelRegistry,
1126
+ aliasIntent: this.#activeModelProfile ? "preset-equivalent" : undefined,
1127
+ credentialSessionId: this.#authSessionId,
1107
1128
  });
1108
1129
  if (resolved.model) {
1109
1130
  resolvedSelectors.push(
@@ -1187,7 +1208,7 @@ export class ModelSelectorComponent extends Container {
1187
1208
  } else {
1188
1209
  rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
1189
1210
  }
1190
- rows.push({ kind: "imageGeneration" });
1211
+ rows.push({ kind: "imageRole" });
1191
1212
  rows.push({ kind: "browse" });
1192
1213
  return rows;
1193
1214
  }
@@ -1207,15 +1228,74 @@ export class ModelSelectorComponent extends Container {
1207
1228
 
1208
1229
  #getMissingProviders(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): string[] {
1209
1230
  const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
1210
- const providers = new Set<string>();
1211
- for (const profile of profiles) for (const provider of profileRequiredProviders(profile)) providers.add(provider);
1212
- return [...providers]
1213
- .filter(provider => this.#isProviderAuthenticated(provider) !== true)
1214
- .sort((a, b) => a.localeCompare(b));
1231
+ const missing = new Set<string>();
1232
+ for (const profile of profiles) {
1233
+ const authenticated = new Set(
1234
+ profileRequiredProviders(profile).filter(provider => this.#isProviderAuthenticated(provider) === true),
1235
+ );
1236
+ if (isModelProfileProviderAvailable(profile, authenticated)) continue;
1237
+ const alternativeGroups = profile.alternativeProviderGroups ?? [];
1238
+ const alternativeProviders = new Set(alternativeGroups.flat());
1239
+ for (const provider of profileRequiredProviders(profile)) {
1240
+ if (!alternativeProviders.has(provider) && !authenticated.has(provider)) missing.add(provider);
1241
+ }
1242
+ for (const group of alternativeGroups) {
1243
+ if (!group.some(provider => authenticated.has(provider)))
1244
+ for (const provider of group) missing.add(provider);
1245
+ }
1246
+ }
1247
+ return [...missing].sort((a, b) => a.localeCompare(b));
1215
1248
  }
1216
1249
 
1217
1250
  #isPresetAuthenticated(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): boolean {
1218
- return this.#getMissingProviders(profileOrProfiles).length === 0;
1251
+ const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
1252
+ return profiles.every(profile => {
1253
+ if (this.#getMissingProviders(profile).length > 0) return false;
1254
+ const bindings = resolveProfileBindings(profile);
1255
+ const values = [
1256
+ ...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
1257
+ ...Object.values(bindings.modelRoles),
1258
+ ...Object.values(bindings.agentModelOverrides),
1259
+ ];
1260
+ const bareAssignmentsAvailable = this.#bareProfileAuthByName.get(profile.name) ?? true;
1261
+ return values.every(value =>
1262
+ normalizeModelSelectorValue(value).some(selector => {
1263
+ if (!selector.includes("/")) return bareAssignmentsAvailable;
1264
+ const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
1265
+ settings: this.#settings,
1266
+ modelRegistry: this.#modelRegistry,
1267
+ aliasIntent: "preset-equivalent",
1268
+ credentialSessionId: this.#authSessionId,
1269
+ }).model;
1270
+ if (!resolved) return false;
1271
+ if (this.#providerAuthById.get(resolved.provider) === true) return true;
1272
+ const alternativeGroup = profile.alternativeProviderGroups?.find(group =>
1273
+ group.includes(resolved.provider),
1274
+ );
1275
+ return (
1276
+ alternativeGroup?.some(provider => {
1277
+ if (this.#providerAuthById.get(provider) !== true) return false;
1278
+ const providerPrefix = `${resolved.provider}/`;
1279
+ const replacementSelector = selector.startsWith(providerPrefix)
1280
+ ? `${provider}/${selector.slice(providerPrefix.length)}`
1281
+ : selector;
1282
+ return (
1283
+ resolveModelRoleValue(
1284
+ replacementSelector,
1285
+ this.#modelRegistry.getAvailable().filter(model => model.provider === provider),
1286
+ {
1287
+ settings: this.#settings,
1288
+ modelRegistry: this.#modelRegistry,
1289
+ aliasIntent: "preset-equivalent",
1290
+ credentialSessionId: this.#authSessionId,
1291
+ },
1292
+ ).model !== undefined
1293
+ );
1294
+ }) ?? false
1295
+ );
1296
+ }),
1297
+ );
1298
+ });
1219
1299
  }
1220
1300
 
1221
1301
  /**
@@ -1230,21 +1310,82 @@ export class ModelSelectorComponent extends Container {
1230
1310
  async #refreshProviderAuth(): Promise<void> {
1231
1311
  const providers = new Set<string>();
1232
1312
  for (const profiles of this.#getPresetGroups().values()) {
1233
- for (const profile of profiles)
1313
+ for (const profile of profiles) {
1234
1314
  for (const provider of profileRequiredProviders(profile)) providers.add(provider);
1315
+ const bindings = resolveProfileBindings(profile);
1316
+ const values = [
1317
+ ...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
1318
+ ...Object.values(bindings.modelRoles),
1319
+ ...Object.values(bindings.agentModelOverrides),
1320
+ ];
1321
+ for (const value of values) {
1322
+ for (const selector of normalizeModelSelectorValue(value)) {
1323
+ const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
1324
+ settings: this.#settings,
1325
+ modelRegistry: this.#modelRegistry,
1326
+ aliasIntent: "preset-equivalent",
1327
+ credentialSessionId: this.#authSessionId,
1328
+ }).model;
1329
+ if (resolved) providers.add(resolved.provider);
1330
+ }
1331
+ }
1332
+ }
1235
1333
  }
1236
1334
  this.#providerAuthPending = providers.size > 0;
1237
1335
  this.#renderPresetLanding();
1238
- const entries = await Promise.all(
1239
- [...providers].map(async provider => {
1240
- const apiKey = await this.#modelRegistry.getApiKeyForProvider(provider, this.#authSessionId);
1241
- return [provider, isAuthenticated(apiKey)] as const;
1242
- }),
1243
- );
1244
- this.#providerAuthById = new Map(entries);
1245
- this.#providerAuthPending = false;
1246
- this.#renderPresetLanding();
1247
- this.#tui.requestRender();
1336
+ try {
1337
+ const entries = await Promise.all(
1338
+ [...providers].map(async provider => {
1339
+ try {
1340
+ const apiKey = await this.#modelRegistry.getApiKeyForProvider(provider, this.#authSessionId);
1341
+ return [provider, apiKey === kNoAuth || isAuthenticated(apiKey)] as const;
1342
+ } catch {
1343
+ return [provider, false] as const;
1344
+ }
1345
+ }),
1346
+ );
1347
+ this.#providerAuthById = new Map(entries);
1348
+ const profileAuthEntries = await Promise.all(
1349
+ [...this.#getPresetGroups().values()].flat().map(async profile => {
1350
+ const bindings = resolveProfileBindings(profile);
1351
+ const values = [
1352
+ ...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
1353
+ ...Object.values(bindings.modelRoles),
1354
+ ...Object.values(bindings.agentModelOverrides),
1355
+ ];
1356
+ const bareValues = values.filter(value =>
1357
+ normalizeModelSelectorValue(value).some(selector => !selector.includes("/")),
1358
+ );
1359
+ const available = await Promise.all(
1360
+ bareValues.map(async value => {
1361
+ try {
1362
+ const resolution = await resolveModelChainWithAuth(
1363
+ normalizeModelSelectorValue(value),
1364
+ this.#modelRegistry,
1365
+ this.#settings,
1366
+ this.#authSessionId,
1367
+ {
1368
+ managedFallback: true,
1369
+ aliasIntent: "preset-equivalent",
1370
+ canonicalSessionId: null,
1371
+ credentialSessionId: this.#authSessionId,
1372
+ },
1373
+ );
1374
+ return resolution.model !== undefined;
1375
+ } catch {
1376
+ return false;
1377
+ }
1378
+ }),
1379
+ );
1380
+ return [profile.name, available.every(Boolean)] as const;
1381
+ }),
1382
+ );
1383
+ this.#bareProfileAuthByName = new Map(profileAuthEntries);
1384
+ } finally {
1385
+ this.#providerAuthPending = false;
1386
+ this.#renderPresetLanding();
1387
+ this.#tui.requestRender();
1388
+ }
1248
1389
  }
1249
1390
 
1250
1391
  #clampPresetCursor(): void {
@@ -1274,7 +1415,7 @@ export class ModelSelectorComponent extends Container {
1274
1415
  selected.kind === "create" ||
1275
1416
  selected.kind === "createUnavailable" ||
1276
1417
  selected.kind === "alreadySaved" ||
1277
- selected.kind === "imageGeneration"
1418
+ selected.kind === "imageRole"
1278
1419
  )
1279
1420
  return;
1280
1421
  if (this.#expandedPresetProviderId === selected.groupId) return;
@@ -1291,7 +1432,7 @@ export class ModelSelectorComponent extends Container {
1291
1432
  selected.kind === "create" ||
1292
1433
  selected.kind === "createUnavailable" ||
1293
1434
  selected.kind === "alreadySaved" ||
1294
- selected.kind === "imageGeneration"
1435
+ selected.kind === "imageRole"
1295
1436
  )
1296
1437
  return;
1297
1438
  if (this.#expandedPresetProviderId !== selected.groupId) return;
@@ -1304,7 +1445,7 @@ export class ModelSelectorComponent extends Container {
1304
1445
  this.#searchInput.setValue(value);
1305
1446
  }
1306
1447
 
1307
- #switchToModelMode(seed?: string): void {
1448
+ #switchToModelMode(seed?: string, options?: { imageRoleFilter?: boolean }): void {
1308
1449
  this.#viewMode = "models";
1309
1450
  this.#expandedPresetProviderId = undefined;
1310
1451
  this.#previewProfileName = undefined;
@@ -1313,6 +1454,7 @@ export class ModelSelectorComponent extends Container {
1313
1454
  this.#presetLoginHint = undefined;
1314
1455
  this.#activeTabIndex = 0;
1315
1456
  this.#selectedIndex = 0;
1457
+ this.#imageRoleFilter = options?.imageRoleFilter ?? false;
1316
1458
  this.#setSearchInputValue(seed ?? this.#searchInput.getValue());
1317
1459
  this.#updateTabBar();
1318
1460
  this.#filterModels(this.#searchInput.getValue());
@@ -1391,13 +1533,14 @@ export class ModelSelectorComponent extends Container {
1391
1533
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
1392
1534
  continue;
1393
1535
  }
1394
- if (row.kind === "imageGeneration") {
1395
- const config = getConfiguredImageModel();
1396
- const current =
1397
- config && config.provider !== "auto"
1398
- ? `${config.provider}${config.model ? ` (${config.model})` : ""}`
1399
- : "Auto";
1400
- const label = `Image Generation: ${current}`;
1536
+ if (row.kind === "imageRole") {
1537
+ const imageRoleValue = this.#settings.getModelRole("image");
1538
+ const current = imageRoleValue
1539
+ ? Array.isArray(imageRoleValue)
1540
+ ? imageRoleValue[0]
1541
+ : imageRoleValue
1542
+ : "No image model set";
1543
+ const label = `Image Role: ${current}`;
1401
1544
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
1402
1545
  continue;
1403
1546
  }
@@ -1442,6 +1585,8 @@ export class ModelSelectorComponent extends Container {
1442
1585
  settings: this.#settings,
1443
1586
  matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
1444
1587
  modelRegistry: this.#modelRegistry,
1588
+ aliasIntent: "preset-equivalent",
1589
+ credentialSessionId: this.#authSessionId,
1445
1590
  });
1446
1591
  const label = GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase();
1447
1592
  this.#listContainer.addChild(
@@ -1920,6 +2065,11 @@ export class ModelSelectorComponent extends Container {
1920
2065
  this.#renderPresetLanding();
1921
2066
  return;
1922
2067
  }
2068
+ if (!this.#isPresetAuthenticated(profile)) {
2069
+ this.#presetLoginHint = "No available model matches this preset";
2070
+ this.#renderPresetLanding();
2071
+ return;
2072
+ }
1923
2073
  this.#onSelectCallback({
1924
2074
  kind: "profile",
1925
2075
  profileName: this.#previewProfileName,
@@ -1946,8 +2096,8 @@ export class ModelSelectorComponent extends Container {
1946
2096
  this.#switchToModelMode();
1947
2097
  return;
1948
2098
  }
1949
- if (row.kind === "imageGeneration") {
1950
- this.#onSelectCallback({ kind: "imageGeneration" });
2099
+ if (row.kind === "imageRole") {
2100
+ this.#switchToModelMode(undefined, { imageRoleFilter: true });
1951
2101
  return;
1952
2102
  }
1953
2103
  if (row.kind === "group") {
@@ -1956,7 +2106,10 @@ export class ModelSelectorComponent extends Container {
1956
2106
  // navigable so the user can drill in and pick a usable member.
1957
2107
  if (!this.#isPresetGroupUsable(row.profiles)) {
1958
2108
  const missing = this.#getMissingProviders(row.profiles);
1959
- this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
2109
+ this.#presetLoginHint =
2110
+ missing.length > 0
2111
+ ? `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`
2112
+ : "No available model matches this preset";
1960
2113
  this.#renderPresetLanding();
1961
2114
  return;
1962
2115
  }
@@ -1977,6 +2130,11 @@ export class ModelSelectorComponent extends Container {
1977
2130
  this.#renderPresetLanding();
1978
2131
  return;
1979
2132
  }
2133
+ if (!isCustomUserProfile(row.profile) && !this.#isPresetAuthenticated(row.profile)) {
2134
+ this.#presetLoginHint = "No available model matches this preset";
2135
+ this.#renderPresetLanding();
2136
+ return;
2137
+ }
1980
2138
  this.#previewProfileName = row.profile.name;
1981
2139
  this.#presetLoginHint = undefined;
1982
2140
  this.#renderPresetLanding();
@@ -2173,7 +2331,13 @@ export class ModelSelectorComponent extends Container {
2173
2331
  return;
2174
2332
  }
2175
2333
 
2176
- const selectedThinkingLevel = itemThinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
2334
+ const currentThinkingLevel = this.#getCurrentRoleThinkingLevel(role);
2335
+ const selectedThinkingLevel =
2336
+ itemThinkingLevel ??
2337
+ (currentThinkingLevel === ThinkingLevel.Inherit ||
2338
+ getSelectableThinkingLevels(item.model).includes(currentThinkingLevel)
2339
+ ? currentThinkingLevel
2340
+ : ThinkingLevel.Inherit);
2177
2341
  const selectorValue = roles
2178
2342
  ? formatModelSelectorValue(item.selector, selectedThinkingLevel)
2179
2343
  : role === "default"
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Live context for the Provider Priority Order editor (`modelProviderOrder`).
3
+ *
4
+ * Owns the subscriptions that keep the editor current: `modelProviderOrder`
5
+ * settings changes and credential-generation changes on the session auth
6
+ * storage. Snapshots are built on demand from the model registry catalog, so
7
+ * the editor needs no catalog event subscription.
8
+ */
9
+
10
+ import type { ModelRegistry } from "../../config/model-registry";
11
+ import { getProviderAuthHealth } from "../../config/provider-auth-health";
12
+ import type { Settings } from "../../config/settings";
13
+ import type { AuthStorage } from "../../session/auth-storage";
14
+
15
+ /** Normalize a persisted provider id the same way provider selection does: trimmed, lowercased. */
16
+ export function normalizeProviderId(raw: string): string {
17
+ return raw.trim().toLowerCase();
18
+ }
19
+
20
+ /** Normalize a persisted provider order the same way provider selection does: trimmed, lowercased, deduplicated, blanks and non-strings dropped. */
21
+ export function normalizeProviderOrder(order: readonly unknown[]): string[] {
22
+ const normalized: string[] = [];
23
+ const seen = new Set<string>();
24
+ for (const raw of order) {
25
+ if (typeof raw !== "string") continue;
26
+ const id = normalizeProviderId(raw);
27
+ if (!id || seen.has(id)) continue;
28
+ seen.add(id);
29
+ normalized.push(id);
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ /** Display label for a provider id, mirroring the model selector tab presentation. */
35
+ export function formatProviderOrderLabel(providerId: string): string {
36
+ return providerId.replace(/[-_]+/g, " ").toUpperCase();
37
+ }
38
+
39
+ export interface ProviderOrderEntry {
40
+ /** Normalized provider id (trimmed, lowercased). */
41
+ id: string;
42
+ /** Display label for the provider. */
43
+ label: string;
44
+ /** Whether the provider is present in the model registry catalog right now. */
45
+ available: boolean;
46
+ /** Whether the provider has usable auth for this session. */
47
+ authenticated: boolean;
48
+ /** Whether the provider is currently persisted in `modelProviderOrder`. */
49
+ inOrder: boolean;
50
+ }
51
+
52
+ export interface ProviderOrderSnapshot {
53
+ /** Persisted order (normalized, deduplicated). Unavailable entries are retained. */
54
+ order: string[];
55
+ /** Union of registry catalog provider ids and persisted ids. */
56
+ entries: ProviderOrderEntry[];
57
+ }
58
+
59
+ export class ProviderOrderContext {
60
+ #registry: ModelRegistry;
61
+ #settings: Settings;
62
+ #authStorage: AuthStorage;
63
+ #onChange: (() => void) | undefined;
64
+ #credentialSessionId: string | undefined;
65
+ #disposed = false;
66
+ #unsubscribeSettings: () => void;
67
+ #unsubscribeAuthGeneration: () => void;
68
+
69
+ constructor(registry: ModelRegistry, settings: Settings, onChange?: () => void, credentialSessionId?: string) {
70
+ this.#registry = registry;
71
+ this.#settings = settings;
72
+ this.#authStorage = registry.authStorage;
73
+ this.#onChange = onChange;
74
+ this.#credentialSessionId = credentialSessionId;
75
+ this.#unsubscribeSettings = settings.onChanged(path => {
76
+ // Only `modelProviderOrder` mutations matter to this editor; the
77
+ // listener takes the single path argument and filters inside.
78
+ if (path !== "modelProviderOrder") return;
79
+ this.#emit();
80
+ });
81
+ this.#unsubscribeAuthGeneration = this.#authStorage.onGenerationChanged(() => this.#emit());
82
+ }
83
+
84
+ /** Build the current provider order snapshot from live registry + settings state. */
85
+ snapshot(): ProviderOrderSnapshot {
86
+ const order = this.#readPersistedOrder();
87
+ const catalog = this.#catalogProviders();
88
+
89
+ const seen = new Set<string>();
90
+ const union: string[] = [];
91
+ for (const id of order) {
92
+ seen.add(id);
93
+ union.push(id);
94
+ }
95
+ for (const id of catalog.keys()) {
96
+ if (seen.has(id)) continue;
97
+ seen.add(id);
98
+ union.push(id);
99
+ }
100
+
101
+ const entries: ProviderOrderEntry[] = union.map(id => ({
102
+ id,
103
+ label: formatProviderOrderLabel(id),
104
+ available: catalog.has(id),
105
+ authenticated: this.#isAuthenticated(catalog.get(id) ?? id),
106
+ inOrder: order.includes(id),
107
+ }));
108
+
109
+ return { order, entries };
110
+ }
111
+
112
+ /** Persist a new ordered provider list and surface durable write failures. */
113
+ async persistOrder(order: readonly string[]): Promise<void> {
114
+ await this.#persistGlobalOrder(normalizeProviderOrder(order));
115
+ }
116
+
117
+ /** Clear the persisted provider order so default resolution applies. */
118
+ async resetOrder(): Promise<void> {
119
+ await this.#persistGlobalOrder(undefined);
120
+ }
121
+
122
+ dispose(): void {
123
+ if (this.#disposed) return;
124
+ this.#disposed = true;
125
+ this.#unsubscribeSettings();
126
+ this.#unsubscribeAuthGeneration();
127
+ this.#onChange = undefined;
128
+ }
129
+
130
+ #readPersistedOrder(): string[] {
131
+ const configured = this.#settings.getGlobal("modelProviderOrder");
132
+ return normalizeProviderOrder(Array.isArray(configured) ? configured : []);
133
+ }
134
+
135
+ async #persistGlobalOrder(order: readonly string[] | undefined): Promise<void> {
136
+ const previous = this.#settings.getGlobal("modelProviderOrder");
137
+ try {
138
+ if (order === undefined) {
139
+ this.#settings.unset("modelProviderOrder");
140
+ } else {
141
+ this.#settings.set("modelProviderOrder", [...order]);
142
+ }
143
+ await this.#settings.flushOrThrow();
144
+ } catch (error) {
145
+ const rollbackErrors: unknown[] = [];
146
+ try {
147
+ if (previous === undefined) {
148
+ this.#settings.unset("modelProviderOrder");
149
+ } else {
150
+ this.#settings.set("modelProviderOrder", previous);
151
+ }
152
+ } catch (rollbackError) {
153
+ rollbackErrors.push(rollbackError);
154
+ }
155
+ try {
156
+ await this.#settings.flushOrThrow();
157
+ } catch (rollbackError) {
158
+ rollbackErrors.push(rollbackError);
159
+ }
160
+ if (rollbackErrors.length > 0) {
161
+ throw new AggregateError(
162
+ [error, ...rollbackErrors],
163
+ "Provider order save failed and rollback was incomplete",
164
+ );
165
+ }
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ #catalogProviders(): Map<string, string> {
171
+ const providers = new Map<string, string>();
172
+ for (const model of this.#registry.getAll()) {
173
+ const id = normalizeProviderId(model.provider);
174
+ if (id && !providers.has(id)) providers.set(id, model.provider);
175
+ }
176
+ return providers;
177
+ }
178
+
179
+ #isAuthenticated(providerId: string): boolean {
180
+ const effectiveCredentialType = (this.#authStorage as Partial<Pick<AuthStorage, "getEffectiveCredentialType">>)
181
+ .getEffectiveCredentialType;
182
+ const health = getProviderAuthHealth(this.#authStorage, providerId);
183
+ if (!effectiveCredentialType)
184
+ return health ? health === "valid" : this.#registry.hasConfiguredProviderAuth(providerId);
185
+ const credentialType = effectiveCredentialType.call(this.#authStorage, providerId, this.#credentialSessionId);
186
+ return (
187
+ (credentialType !== undefined &&
188
+ health !== "invalid" &&
189
+ (this.#authStorage.hasUsableAuth?.(providerId) ?? true)) ||
190
+ this.#registry.isCredentiallessProvider?.(providerId) === true
191
+ );
192
+ }
193
+
194
+ #emit(): void {
195
+ this.#onChange?.();
196
+ }
197
+ }