@gajae-code/coding-agent 0.4.5 → 0.5.1

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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -1,5 +1,5 @@
1
1
  import { ThinkingLevel } from "@gajae-code/agent-core";
2
- import { clampThinkingLevelForModel, getSupportedEfforts, type Model, modelsAreEqual } from "@gajae-code/ai";
2
+ import { getSupportedEfforts, type Model, modelsAreEqual } from "@gajae-code/ai";
3
3
  import {
4
4
  Container,
5
5
  fuzzyFilter,
@@ -12,9 +12,17 @@ import {
12
12
  Text,
13
13
  type TUI,
14
14
  } from "@gajae-code/tui";
15
- import type { ModelProfileDefinition } from "../../config/model-profiles";
15
+ import {
16
+ getModelProfilePresentation,
17
+ groupModelProfilesForPresetLanding,
18
+ type ModelProfileDefinition,
19
+ } from "../../config/model-profiles";
16
20
  import type { GjcModelAssignmentTargetId, ModelRegistry } from "../../config/model-registry";
17
- import { GJC_MODEL_ASSIGNMENT_TARGET_IDS, GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
21
+ import {
22
+ GJC_MODEL_ASSIGNMENT_TARGET_IDS,
23
+ GJC_MODEL_ASSIGNMENT_TARGETS,
24
+ isAuthenticated,
25
+ } from "../../config/model-registry";
18
26
  import {
19
27
  formatModelSelectorValue,
20
28
  resolveModelRoleValue,
@@ -23,7 +31,7 @@ import {
23
31
  import type { Settings } from "../../config/settings";
24
32
  import { type ThemeColor, theme } from "../../modes/theme/theme";
25
33
  import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
26
- import { getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
34
+ import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
27
35
  import { getTabBarTheme } from "../shared";
28
36
  import { DynamicBorder } from "./dynamic-border";
29
37
 
@@ -75,12 +83,6 @@ interface CanonicalModelItem {
75
83
  explicitThinkingLevel?: boolean;
76
84
  }
77
85
 
78
- interface ProfileItem {
79
- kind: "profile";
80
- name: string;
81
- profile: ModelProfileDefinition;
82
- }
83
-
84
86
  type ScopedModelItem = ScopedModelSelection;
85
87
 
86
88
  interface RoleAssignment {
@@ -88,13 +90,6 @@ interface RoleAssignment {
88
90
  thinkingLevel: ThinkingLevel;
89
91
  }
90
92
 
91
- export interface ModelAssignmentPreset {
92
- id: "openai-codex";
93
- label: string;
94
- description: string;
95
- assignments: Partial<Record<GjcModelAssignmentTargetId, ThinkingLevel>>;
96
- }
97
-
98
93
  export type ModelSelectorSelection =
99
94
  | {
100
95
  kind: "assignment";
@@ -103,13 +98,6 @@ export type ModelSelectorSelection =
103
98
  thinkingLevel?: ThinkingLevel;
104
99
  selector?: string;
105
100
  }
106
- | {
107
- kind: "preset";
108
- model: Model;
109
- selector: string;
110
- preset: ModelAssignmentPreset;
111
- assignments: Record<GjcModelAssignmentTargetId, ThinkingLevel>;
112
- }
113
101
  | {
114
102
  kind: "profile";
115
103
  profileName: string;
@@ -137,18 +125,6 @@ const STATIC_PROVIDER_TABS: ProviderTabState[] = [
137
125
  { id: ALL_TAB, label: ALL_TAB },
138
126
  { id: CANONICAL_TAB, label: CANONICAL_TAB },
139
127
  ];
140
- const OPENAI_CODE_PROFILE_PRESET: ModelAssignmentPreset = {
141
- id: "openai-codex",
142
- label: "Apply OpenAI Codex role preset",
143
- description: "Default medium, Executor low, Architect xhigh, Planner medium, Critic high",
144
- assignments: {
145
- default: ThinkingLevel.Medium,
146
- executor: ThinkingLevel.Low,
147
- architect: ThinkingLevel.XHigh,
148
- planner: ThinkingLevel.Medium,
149
- critic: ThinkingLevel.High,
150
- },
151
- };
152
128
 
153
129
  function formatProviderTabLabel(providerId: string): string {
154
130
  return providerId.replace(/[-_]+/g, " ").toUpperCase();
@@ -157,6 +133,43 @@ function formatProviderTabLabel(providerId: string): string {
157
133
  function createProviderTab(providerId: string): ProviderTabState {
158
134
  return { id: providerId, label: formatProviderTabLabel(providerId), providerId };
159
135
  }
136
+
137
+ type ModelSelectorViewMode = "presets" | "models";
138
+
139
+ interface PresetGroupRow {
140
+ kind: "group";
141
+ groupId: string;
142
+ profiles: ModelProfileDefinition[];
143
+ }
144
+
145
+ interface PresetProfileRow {
146
+ kind: "profile";
147
+ groupId: string;
148
+ profile: ModelProfileDefinition;
149
+ }
150
+
151
+ interface PresetBrowseRow {
152
+ kind: "browse";
153
+ }
154
+
155
+ type PresetLandingRow = PresetGroupRow | PresetProfileRow | PresetBrowseRow;
156
+
157
+ const PROFILE_ROLE_PREVIEW_ORDER: GjcModelAssignmentTargetId[] = [
158
+ "default",
159
+ "executor",
160
+ "planner",
161
+ "critic",
162
+ "architect",
163
+ ];
164
+ const PRESET_SCOPE_LABELS = ["Apply for this session", "Set as default"];
165
+
166
+ function isPrintableCharacter(keyData: string): boolean {
167
+ return keyData.length === 1 && keyData >= " " && keyData !== "\x7f";
168
+ }
169
+
170
+ function profileRequiredProviders(profile: ModelProfileDefinition): string[] {
171
+ return [...new Set(profile.requiredProviders)].sort((a, b) => a.localeCompare(b));
172
+ }
160
173
  /**
161
174
  * Component that renders a canonical model selector with provider tabs.
162
175
  * - Tab/Arrow Left/Right: Switch between provider tabs
@@ -173,7 +186,6 @@ export class ModelSelectorComponent extends Container {
173
186
  #filteredModels: ModelItem[] = [];
174
187
  #canonicalModels: CanonicalModelItem[] = [];
175
188
  #filteredCanonicalModels: CanonicalModelItem[] = [];
176
- #profileItems: ProfileItem[] = [];
177
189
  #selectedIndex: number = 0;
178
190
  #roles = {} as Record<string, RoleAssignment | undefined>;
179
191
  #settings = null as unknown as Settings;
@@ -189,6 +201,18 @@ export class ModelSelectorComponent extends Container {
189
201
  #pendingThinkingChoice?: PendingThinkingChoice;
190
202
  #selectedThinkingIndex: number = 0;
191
203
 
204
+ // Preset landing state
205
+ #viewMode: ModelSelectorViewMode = "presets";
206
+ #presetCursor: number = 0;
207
+ #expandedPresetProviderId?: string;
208
+ #previewProfileName?: string;
209
+ #presetScopeMenuOpen: boolean = false;
210
+ #presetScopeIndex: number = 0;
211
+ #providerAuthById = new Map<string, boolean>();
212
+ #providerAuthPending: boolean = false;
213
+ #presetLoginHint?: string;
214
+ #authSessionId?: string;
215
+
192
216
  // Tab state
193
217
  #providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
194
218
  #activeTabIndex: number = 0;
@@ -201,7 +225,7 @@ export class ModelSelectorComponent extends Container {
201
225
  scopedModels: ReadonlyArray<ScopedModelItem>,
202
226
  onSelect: RoleSelectCallback,
203
227
  onCancel: () => void,
204
- options?: { temporaryOnly?: boolean; initialSearchInput?: string },
228
+ options?: { temporaryOnly?: boolean; initialSearchInput?: string; sessionId?: string },
205
229
  ) {
206
230
  super();
207
231
 
@@ -212,7 +236,9 @@ export class ModelSelectorComponent extends Container {
212
236
  this.#onSelectCallback = onSelect;
213
237
  this.#onCancelCallback = onCancel;
214
238
  this.#temporaryOnly = options?.temporaryOnly ?? false;
239
+ this.#authSessionId = options?.sessionId;
215
240
  const initialSearchInput = options?.initialSearchInput;
241
+ this.#viewMode = this.#temporaryOnly || initialSearchInput || scopedModels.length > 0 ? "models" : "presets";
216
242
 
217
243
  // Load current role assignments from settings
218
244
  this.#loadRoleModels();
@@ -240,13 +266,7 @@ export class ModelSelectorComponent extends Container {
240
266
  }
241
267
  this.#searchInput.onSubmit = () => {
242
268
  const selectedItem = this.#getSelectedItem();
243
- if (selectedItem) {
244
- if (selectedItem.kind === "profile") {
245
- this.#beginProfileActionMenu(selectedItem);
246
- } else {
247
- this.#beginActionMenuOrSelect(selectedItem);
248
- }
249
- }
269
+ if (selectedItem) this.#beginActionMenuOrSelect(selectedItem);
250
270
  };
251
271
  this.addChild(this.#searchInput);
252
272
 
@@ -264,14 +284,23 @@ export class ModelSelectorComponent extends Container {
264
284
  // Load models and do initial render
265
285
  this.#loadModels().then(() => {
266
286
  this.#buildProviderTabs();
267
- this.#updateTabBar();
268
- // Always apply the current search query — the user may have typed
269
- // while models were loading asynchronously.
270
- const currentQuery = this.#searchInput.getValue();
271
- if (currentQuery) {
272
- this.#filterModels(currentQuery);
287
+ if (this.#viewMode === "presets" && (this.#modelRegistry.getModelProfiles?.().size ?? 0) === 0) {
288
+ this.#viewMode = "models";
289
+ }
290
+ if (this.#viewMode === "presets") {
291
+ this.#updatePresetExpansion();
292
+ void this.#refreshProviderAuth();
293
+ this.#renderPresetLanding();
273
294
  } else {
274
- this.#updateList();
295
+ this.#updateTabBar();
296
+ // Always apply the current search query — the user may have typed
297
+ // while models were loading asynchronously.
298
+ const currentQuery = this.#searchInput.getValue();
299
+ if (currentQuery) {
300
+ this.#filterModels(currentQuery);
301
+ } else {
302
+ this.#updateList();
303
+ }
275
304
  }
276
305
  // Request re-render after models are loaded
277
306
  this.#tui.requestRender();
@@ -482,18 +511,10 @@ export class ModelSelectorComponent extends Container {
482
511
 
483
512
  this.#sortModels(models);
484
513
  this.#sortCanonicalModels(canonicalModels);
485
- const profiles = this.#modelRegistry.getModelProfiles?.() ?? new Map();
486
- const profileItems = this.#temporaryOnly
487
- ? []
488
- : [...profiles.values()]
489
- .sort((a, b) => a.name.localeCompare(b.name))
490
- .map(profile => ({ kind: "profile" as const, name: profile.name, profile }));
491
-
492
514
  this.#allModels = models;
493
515
  this.#filteredModels = models;
494
516
  this.#canonicalModels = canonicalModels;
495
517
  this.#filteredCanonicalModels = canonicalModels;
496
- this.#profileItems = profileItems;
497
518
  this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, models.length - 1));
498
519
  }
499
520
 
@@ -642,6 +663,173 @@ export class ModelSelectorComponent extends Container {
642
663
  return `${ageMinutes}m ago`;
643
664
  }
644
665
 
666
+ #getPresetGroups(): Map<string, ModelProfileDefinition[]> {
667
+ return groupModelProfilesForPresetLanding(this.#modelRegistry.getModelProfiles?.() ?? new Map());
668
+ }
669
+
670
+ #getPresetRows(): PresetLandingRow[] {
671
+ const rows: PresetLandingRow[] = [];
672
+ for (const [groupId, profiles] of this.#getPresetGroups()) {
673
+ rows.push({ kind: "group", groupId, profiles });
674
+ if (this.#expandedPresetProviderId === groupId) {
675
+ for (const profile of profiles) rows.push({ kind: "profile", groupId, profile });
676
+ }
677
+ }
678
+ rows.push({ kind: "browse" });
679
+ return rows;
680
+ }
681
+
682
+ #getSelectedPresetRow(): PresetLandingRow | undefined {
683
+ return this.#getPresetRows()[this.#presetCursor];
684
+ }
685
+
686
+ #getProfileByName(name: string | undefined): ModelProfileDefinition | undefined {
687
+ if (!name) return undefined;
688
+ return this.#modelRegistry.getModelProfile?.(name) ?? this.#modelRegistry.getModelProfiles?.().get(name);
689
+ }
690
+
691
+ #isProviderAuthenticated(providerId: string): boolean | undefined {
692
+ return this.#providerAuthById.get(providerId);
693
+ }
694
+
695
+ #getMissingProviders(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): string[] {
696
+ const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
697
+ const providers = new Set<string>();
698
+ for (const profile of profiles) for (const provider of profileRequiredProviders(profile)) providers.add(provider);
699
+ return [...providers]
700
+ .filter(provider => this.#isProviderAuthenticated(provider) !== true)
701
+ .sort((a, b) => a.localeCompare(b));
702
+ }
703
+
704
+ #isPresetAuthenticated(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): boolean {
705
+ return this.#getMissingProviders(profileOrProfiles).length === 0;
706
+ }
707
+
708
+ /**
709
+ * A preset group is a list of alternative presets, not an all-or-nothing
710
+ * bundle. Treat the group as usable when at least one member preset has all
711
+ * of its required providers authenticated.
712
+ */
713
+ #isPresetGroupUsable(profiles: ModelProfileDefinition[]): boolean {
714
+ return profiles.some(profile => this.#isPresetAuthenticated(profile));
715
+ }
716
+
717
+ async #refreshProviderAuth(): Promise<void> {
718
+ const providers = new Set<string>();
719
+ for (const profiles of this.#getPresetGroups().values()) {
720
+ for (const profile of profiles)
721
+ for (const provider of profileRequiredProviders(profile)) providers.add(provider);
722
+ }
723
+ this.#providerAuthPending = providers.size > 0;
724
+ this.#renderPresetLanding();
725
+ const entries = await Promise.all(
726
+ [...providers].map(async provider => {
727
+ const apiKey = await this.#modelRegistry.getApiKeyForProvider(provider, this.#authSessionId);
728
+ return [provider, isAuthenticated(apiKey)] as const;
729
+ }),
730
+ );
731
+ this.#providerAuthById = new Map(entries);
732
+ this.#providerAuthPending = false;
733
+ this.#renderPresetLanding();
734
+ this.#tui.requestRender();
735
+ }
736
+
737
+ #updatePresetExpansion(): void {
738
+ const selected = this.#getSelectedPresetRow();
739
+ if (selected?.kind === "group") this.#expandedPresetProviderId = selected.groupId;
740
+ if (selected?.kind === "profile") this.#expandedPresetProviderId = selected.groupId;
741
+ const rows = this.#getPresetRows();
742
+ this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, rows.length - 1));
743
+ }
744
+
745
+ #switchToModelMode(seed?: string): void {
746
+ this.#viewMode = "models";
747
+ this.#expandedPresetProviderId = undefined;
748
+ this.#previewProfileName = undefined;
749
+ this.#presetScopeMenuOpen = false;
750
+ this.#presetScopeIndex = 0;
751
+ this.#presetLoginHint = undefined;
752
+ this.#activeTabIndex = 0;
753
+ this.#selectedIndex = 0;
754
+ this.#searchInput.setValue(seed ?? this.#searchInput.getValue());
755
+ this.#updateTabBar();
756
+ this.#filterModels(this.#searchInput.getValue());
757
+ }
758
+
759
+ #renderPresetLanding(): void {
760
+ this.#headerContainer.clear();
761
+ this.#tabBar = null;
762
+ this.#listContainer.clear();
763
+ this.#headerContainer.addChild(new Text(theme.fg("accent", "Model presets"), 0, 0));
764
+ const rows = this.#getPresetRows();
765
+ for (let i = 0; i < rows.length; i++) {
766
+ const row = rows[i];
767
+ const selected = i === this.#presetCursor;
768
+ const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
769
+ if (row.kind === "browse") {
770
+ const label = "Browse all models";
771
+ this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
772
+ continue;
773
+ }
774
+ if (row.kind === "group") {
775
+ const authenticated = this.#isPresetGroupUsable(row.profiles);
776
+ const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
777
+ const label = `${mark} ${row.groupId}`;
778
+ const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
779
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
780
+ continue;
781
+ }
782
+ const presentation = getModelProfilePresentation(row.profile.name);
783
+ const authenticated = this.#isPresetAuthenticated(row.profile);
784
+ const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
785
+ const label = ` ${mark} ${presentation.displayName}`;
786
+ const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
787
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
788
+ }
789
+ if (this.#presetLoginHint) {
790
+ this.#listContainer.addChild(new Spacer(1));
791
+ this.#listContainer.addChild(new Text(theme.fg("warning", ` ${this.#presetLoginHint}`), 0, 0));
792
+ }
793
+ const previewProfile = this.#getProfileByName(this.#previewProfileName);
794
+ if (previewProfile) this.#renderPresetPreview(previewProfile);
795
+ }
796
+
797
+ #renderPresetPreview(profile: ModelProfileDefinition): void {
798
+ this.#listContainer.addChild(new Spacer(1));
799
+ this.#listContainer.addChild(
800
+ new Text(
801
+ theme.fg("muted", ` Preset preview: ${getModelProfilePresentation(profile.name).displayName}`),
802
+ 0,
803
+ 0,
804
+ ),
805
+ );
806
+ for (const role of PROFILE_ROLE_PREVIEW_ORDER) {
807
+ const selector = profile.modelMapping[role];
808
+ if (!selector) continue;
809
+ const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAll(), {
810
+ settings: this.#settings,
811
+ matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
812
+ modelRegistry: this.#modelRegistry,
813
+ });
814
+ const label = GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase();
815
+ this.#listContainer.addChild(
816
+ new Text(` ${label}: ${formatClampedModelSelector(selector, resolved.model)}`, 0, 0),
817
+ );
818
+ }
819
+ this.#listContainer.addChild(new Spacer(1));
820
+ if (this.#presetScopeMenuOpen) {
821
+ for (let i = 0; i < PRESET_SCOPE_LABELS.length; i++) {
822
+ const label = PRESET_SCOPE_LABELS[i] ?? "";
823
+ const prefix = i === this.#presetScopeIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
824
+ this.#listContainer.addChild(
825
+ new Text(`${prefix}${i === this.#presetScopeIndex ? theme.fg("accent", label) : label}`, 0, 0),
826
+ );
827
+ }
828
+ } else {
829
+ this.#listContainer.addChild(new Text(theme.fg("muted", " Press Enter to apply this preset"), 0, 0));
830
+ }
831
+ }
832
+
645
833
  #formatDiscoveryErrorHint(error: string | undefined): string | undefined {
646
834
  if (!error) {
647
835
  return undefined;
@@ -688,17 +876,10 @@ export class ModelSelectorComponent extends Container {
688
876
  }
689
877
  }
690
878
 
691
- #getVisibleProfiles(): ProfileItem[] {
692
- return !this.#temporaryOnly && !this.#isCanonicalTab() && this.#getActiveTabId() === ALL_TAB
693
- ? this.#profileItems
694
- : [];
695
- }
696
-
697
879
  #updateList(): void {
698
880
  this.#listContainer.clear();
699
881
  const isCanonicalTab = this.#isCanonicalTab();
700
- const visibleProfiles = this.#getVisibleProfiles();
701
- const modelSelectedIndex = Math.max(0, this.#selectedIndex - visibleProfiles.length);
882
+ const modelSelectedIndex = this.#selectedIndex;
702
883
  const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
703
884
 
704
885
  const maxVisible = 10;
@@ -710,18 +891,6 @@ export class ModelSelectorComponent extends Container {
710
891
 
711
892
  const showProvider = this.#getActiveTabId() === ALL_TAB;
712
893
 
713
- if (visibleProfiles.length > 0) {
714
- this.#listContainer.addChild(new Text(theme.fg("muted", "Profiles"), 0, 0));
715
- for (let i = 0; i < visibleProfiles.length; i++) {
716
- const profile = visibleProfiles[i];
717
- if (!profile) continue;
718
- const isSelected = i === this.#selectedIndex;
719
- const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
720
- const label = isSelected ? theme.fg("accent", profile.name) : profile.name;
721
- this.#listContainer.addChild(new Text(`${prefix}${label}`, 0, 0));
722
- }
723
- this.#listContainer.addChild(new Spacer(1));
724
- }
725
894
  // Show visible slice of filtered models
726
895
  for (let i = startIndex; i < endIndex; i++) {
727
896
  const item = visibleItems[i];
@@ -729,7 +898,7 @@ export class ModelSelectorComponent extends Container {
729
898
  const canonicalItem = isCanonicalTab ? (item as CanonicalModelItem) : undefined;
730
899
  const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
731
900
 
732
- const isSelected = i + visibleProfiles.length === this.#selectedIndex;
901
+ const isSelected = i === this.#selectedIndex;
733
902
 
734
903
  // Build role badges (inverted: color as background, black text)
735
904
  const roleBadgeTokens: string[] = [];
@@ -786,7 +955,7 @@ export class ModelSelectorComponent extends Container {
786
955
  for (const line of errorLines) {
787
956
  this.#listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
788
957
  }
789
- } else if (visibleItems.length === 0 && visibleProfiles.length === 0) {
958
+ } else if (visibleItems.length === 0) {
790
959
  const statusMessage = this.#getProviderEmptyStateMessage();
791
960
  this.#listContainer.addChild(
792
961
  new Text(
@@ -795,11 +964,6 @@ export class ModelSelectorComponent extends Container {
795
964
  0,
796
965
  ),
797
966
  );
798
- } else if (this.#selectedIndex < visibleProfiles.length) {
799
- const selectedProfile = visibleProfiles[this.#selectedIndex];
800
- if (selectedProfile && this.#pendingActionItem) {
801
- this.#renderProfileActionMenu(selectedProfile);
802
- }
803
967
  } else {
804
968
  const selected = visibleItems[modelSelectedIndex];
805
969
  if (!selected) {
@@ -828,9 +992,7 @@ export class ModelSelectorComponent extends Container {
828
992
  for (let i = 0; i < actionCount; i++) {
829
993
  const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
830
994
  const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
831
- const label = role
832
- ? `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`
833
- : `${OPENAI_CODE_PROFILE_PRESET.label} (${OPENAI_CODE_PROFILE_PRESET.description})`;
995
+ const label = `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`;
834
996
  this.#listContainer.addChild(
835
997
  new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
836
998
  );
@@ -855,32 +1017,17 @@ export class ModelSelectorComponent extends Container {
855
1017
  }
856
1018
  }
857
1019
 
858
- #renderProfileActionMenu(profile: ProfileItem): void {
859
- this.#listContainer.addChild(new Spacer(1));
860
- this.#listContainer.addChild(new Text(theme.fg("muted", ` Action for profile: ${profile.name}`), 0, 0));
861
- this.#listContainer.addChild(new Spacer(1));
862
- const labels = ["Apply for this session", "Set as default"];
863
- for (let i = 0; i < labels.length; i++) {
864
- const label = labels[i] ?? "";
865
- const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
866
- this.#listContainer.addChild(
867
- new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
868
- );
869
- }
870
- }
871
-
872
1020
  #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
873
1021
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
874
1022
  }
875
- #getActionCount(model: Model): number {
876
- return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + (supportsOpenAICodexPreset(model) ? 1 : 0);
1023
+ #getActionCount(_model: Model): number {
1024
+ return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
877
1025
  }
878
1026
 
879
- #getSelectedItem(): ModelItem | CanonicalModelItem | ProfileItem | undefined {
880
- const visibleProfiles = this.#getVisibleProfiles();
881
- if (this.#selectedIndex < visibleProfiles.length) return visibleProfiles[this.#selectedIndex];
882
- const modelIndex = this.#selectedIndex - visibleProfiles.length;
883
- return this.#isCanonicalTab() ? this.#filteredCanonicalModels[modelIndex] : this.#filteredModels[modelIndex];
1027
+ #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
1028
+ return this.#isCanonicalTab()
1029
+ ? this.#filteredCanonicalModels[this.#selectedIndex]
1030
+ : this.#filteredModels[this.#selectedIndex];
884
1031
  }
885
1032
 
886
1033
  handleInput(keyData: string): void {
@@ -893,6 +1040,11 @@ export class ModelSelectorComponent extends Container {
893
1040
  return;
894
1041
  }
895
1042
 
1043
+ if (this.#viewMode === "presets") {
1044
+ this.#handlePresetLandingInput(keyData);
1045
+ return;
1046
+ }
1047
+
896
1048
  // Tab bar navigation
897
1049
  if (this.#tabBar?.handleInput(keyData)) {
898
1050
  return;
@@ -900,9 +1052,7 @@ export class ModelSelectorComponent extends Container {
900
1052
 
901
1053
  // Up arrow - navigate list (wrap to bottom when at top)
902
1054
  if (matchesKey(keyData, "up")) {
903
- const itemCount =
904
- this.#getVisibleProfiles().length +
905
- (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
1055
+ const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
906
1056
  if (itemCount === 0) return;
907
1057
  this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
908
1058
  this.#updateList();
@@ -911,9 +1061,7 @@ export class ModelSelectorComponent extends Container {
911
1061
 
912
1062
  // Down arrow - navigate list (wrap to top when at bottom)
913
1063
  if (matchesKey(keyData, "down")) {
914
- const itemCount =
915
- this.#getVisibleProfiles().length +
916
- (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
1064
+ const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
917
1065
  if (itemCount === 0) return;
918
1066
  this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
919
1067
  this.#updateList();
@@ -924,13 +1072,7 @@ export class ModelSelectorComponent extends Container {
924
1072
  // existing non-persistent quick-switch behavior.
925
1073
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
926
1074
  const selectedItem = this.#getSelectedItem();
927
- if (selectedItem) {
928
- if (selectedItem.kind === "profile") {
929
- this.#beginProfileActionMenu(selectedItem);
930
- } else {
931
- this.#beginActionMenuOrSelect(selectedItem);
932
- }
933
- }
1075
+ if (selectedItem) this.#beginActionMenuOrSelect(selectedItem);
934
1076
  return;
935
1077
  }
936
1078
 
@@ -944,10 +1086,107 @@ export class ModelSelectorComponent extends Container {
944
1086
  this.#searchInput.handleInput(keyData);
945
1087
  this.#filterModels(this.#searchInput.getValue());
946
1088
  }
947
- #beginProfileActionMenu(profile: ProfileItem): void {
948
- this.#pendingActionItem = profile as unknown as ModelItem;
949
- this.#selectedActionIndex = 0;
950
- this.#updateList();
1089
+
1090
+ #handlePresetLandingInput(keyData: string): void {
1091
+ if (isPrintableCharacter(keyData)) {
1092
+ this.#switchToModelMode(keyData);
1093
+ return;
1094
+ }
1095
+ if (matchesKey(keyData, "up")) {
1096
+ const rows = this.#getPresetRows();
1097
+ if (rows.length === 0) return;
1098
+ if (this.#presetScopeMenuOpen) {
1099
+ this.#presetScopeIndex =
1100
+ this.#presetScopeIndex === 0 ? PRESET_SCOPE_LABELS.length - 1 : this.#presetScopeIndex - 1;
1101
+ } else {
1102
+ this.#presetCursor = this.#presetCursor === 0 ? rows.length - 1 : this.#presetCursor - 1;
1103
+ this.#previewProfileName = undefined;
1104
+ this.#presetLoginHint = undefined;
1105
+ this.#updatePresetExpansion();
1106
+ }
1107
+ this.#renderPresetLanding();
1108
+ return;
1109
+ }
1110
+ if (matchesKey(keyData, "down")) {
1111
+ const rows = this.#getPresetRows();
1112
+ if (rows.length === 0) return;
1113
+ if (this.#presetScopeMenuOpen) {
1114
+ this.#presetScopeIndex = (this.#presetScopeIndex + 1) % PRESET_SCOPE_LABELS.length;
1115
+ } else {
1116
+ this.#presetCursor = (this.#presetCursor + 1) % rows.length;
1117
+ this.#previewProfileName = undefined;
1118
+ this.#presetLoginHint = undefined;
1119
+ this.#updatePresetExpansion();
1120
+ }
1121
+ this.#renderPresetLanding();
1122
+ return;
1123
+ }
1124
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
1125
+ this.#handlePresetEnter();
1126
+ return;
1127
+ }
1128
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
1129
+ if (this.#presetScopeMenuOpen) {
1130
+ this.#presetScopeMenuOpen = false;
1131
+ this.#renderPresetLanding();
1132
+ return;
1133
+ }
1134
+ if (this.#previewProfileName) {
1135
+ this.#previewProfileName = undefined;
1136
+ this.#renderPresetLanding();
1137
+ return;
1138
+ }
1139
+ if (this.#expandedPresetProviderId) {
1140
+ this.#expandedPresetProviderId = undefined;
1141
+ this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, this.#getPresetRows().length - 1));
1142
+ this.#renderPresetLanding();
1143
+ return;
1144
+ }
1145
+ this.#onCancelCallback();
1146
+ }
1147
+ }
1148
+
1149
+ #handlePresetEnter(): void {
1150
+ if (this.#presetScopeMenuOpen && this.#previewProfileName) {
1151
+ this.#onSelectCallback({
1152
+ kind: "profile",
1153
+ profileName: this.#previewProfileName,
1154
+ setDefault: this.#presetScopeIndex === 1,
1155
+ });
1156
+ return;
1157
+ }
1158
+ if (this.#previewProfileName) {
1159
+ this.#presetScopeMenuOpen = true;
1160
+ this.#presetScopeIndex = 0;
1161
+ this.#renderPresetLanding();
1162
+ return;
1163
+ }
1164
+ const row = this.#getSelectedPresetRow();
1165
+ if (!row) return;
1166
+ if (row.kind === "browse") {
1167
+ this.#switchToModelMode();
1168
+ return;
1169
+ }
1170
+ if (row.kind === "group") {
1171
+ // A group is a list of alternative presets; only surface a login hint
1172
+ // when none of its members are usable. A partially-usable group stays
1173
+ // navigable so the user can drill in and pick a usable member.
1174
+ if (!this.#isPresetGroupUsable(row.profiles)) {
1175
+ const missing = this.#getMissingProviders(row.profiles);
1176
+ this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1177
+ this.#renderPresetLanding();
1178
+ }
1179
+ return;
1180
+ }
1181
+ const missing = this.#getMissingProviders(row.profile);
1182
+ if (missing.length > 0) {
1183
+ this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1184
+ this.#renderPresetLanding();
1185
+ return;
1186
+ }
1187
+ this.#previewProfileName = row.profile.name;
1188
+ this.#presetLoginHint = undefined;
1189
+ this.#renderPresetLanding();
951
1190
  }
952
1191
 
953
1192
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
@@ -963,7 +1202,7 @@ export class ModelSelectorComponent extends Container {
963
1202
  #handleActionMenuInput(keyData: string): void {
964
1203
  const item = this.#pendingActionItem;
965
1204
  if (!item) return;
966
- const actionCount = (item as unknown as ProfileItem).kind === "profile" ? 2 : this.#getActionCount(item.model);
1205
+ const actionCount = this.#getActionCount(item.model);
967
1206
  if (matchesKey(keyData, "up")) {
968
1207
  this.#selectedActionIndex = this.#selectedActionIndex === 0 ? actionCount - 1 : this.#selectedActionIndex - 1;
969
1208
  this.#updateList();
@@ -976,21 +1215,8 @@ export class ModelSelectorComponent extends Container {
976
1215
  }
977
1216
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
978
1217
  this.#pendingActionItem = undefined;
979
- if ((item as unknown as ProfileItem).kind === "profile") {
980
- const profile = item as unknown as ProfileItem;
981
- this.#onSelectCallback({
982
- kind: "profile",
983
- profileName: profile.name,
984
- setDefault: this.#selectedActionIndex === 1,
985
- });
986
- } else {
987
- const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
988
- if (role) {
989
- this.#handleSelect(item, role);
990
- } else {
991
- this.#handlePresetSelect(item, OPENAI_CODE_PROFILE_PRESET);
992
- }
993
- }
1218
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
1219
+ if (role) this.#handleSelect(item, role);
994
1220
  return;
995
1221
  }
996
1222
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
@@ -1029,18 +1255,6 @@ export class ModelSelectorComponent extends Container {
1029
1255
  this.#updateList();
1030
1256
  }
1031
1257
  }
1032
- #handlePresetSelect(item: ModelItem | CanonicalModelItem, preset: ModelAssignmentPreset): void {
1033
- const selectorValue = item.selector;
1034
- const assignments = resolvePresetAssignments(item.model, preset);
1035
- for (const [role, thinkingLevel] of Object.entries(assignments) as [
1036
- GjcModelAssignmentTargetId,
1037
- ThinkingLevel,
1038
- ][]) {
1039
- this.#roles[role] = { model: item.model, thinkingLevel };
1040
- }
1041
- this.#onSelectCallback({ kind: "preset", model: item.model, selector: selectorValue, preset, assignments });
1042
- this.#updateList();
1043
- }
1044
1258
 
1045
1259
  #handleSelect(
1046
1260
  item: ModelItem | CanonicalModelItem,
@@ -1100,31 +1314,6 @@ function requiresExplicitThinkingChoice(model: Model): boolean {
1100
1314
  return model.reasoning === true && (model.provider === "openai" || model.provider === "openai-codex");
1101
1315
  }
1102
1316
 
1103
- function supportsOpenAICodexPreset(model: Model): boolean {
1104
- return model.provider === "openai-codex" && model.reasoning === true;
1105
- }
1106
-
1107
- function resolvePresetAssignments(
1108
- model: Model,
1109
- preset: ModelAssignmentPreset,
1110
- ): Record<GjcModelAssignmentTargetId, ThinkingLevel> {
1111
- const resolved = {} as Record<GjcModelAssignmentTargetId, ThinkingLevel>;
1112
- for (const [role, requestedLevel] of Object.entries(preset.assignments) as [
1113
- GjcModelAssignmentTargetId,
1114
- ThinkingLevel,
1115
- ][]) {
1116
- const clampedLevel =
1117
- requestedLevel === ThinkingLevel.Off || requestedLevel === ThinkingLevel.Inherit
1118
- ? requestedLevel
1119
- : clampThinkingLevelForModel(model, requestedLevel);
1120
- if (!clampedLevel) {
1121
- throw new Error(`Model ${model.provider}/${model.id} does not support ${requestedLevel} reasoning`);
1122
- }
1123
- resolved[role] = clampedLevel;
1124
- }
1125
- return resolved;
1126
- }
1127
-
1128
1317
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
1129
1318
  const levels: ThinkingLevel[] = [ThinkingLevel.Off];
1130
1319
  let efforts: readonly string[];