@gajae-code/coding-agent 0.4.5 → 0.5.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 (87) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/types/commands/harness.d.ts +3 -0
  3. package/dist/types/config/model-profile-activation.d.ts +11 -2
  4. package/dist/types/config/model-profiles.d.ts +7 -0
  5. package/dist/types/config/model-registry.d.ts +3 -0
  6. package/dist/types/config/model-resolver.d.ts +2 -0
  7. package/dist/types/config/models-config-schema.d.ts +30 -0
  8. package/dist/types/config/settings-schema.d.ts +4 -3
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  10. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  11. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  12. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  13. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  14. package/dist/types/harness-control-plane/types.d.ts +4 -0
  15. package/dist/types/hindsight/mental-models.d.ts +5 -5
  16. package/dist/types/modes/components/model-selector.d.ts +1 -12
  17. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  18. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  19. package/dist/types/sdk.d.ts +5 -0
  20. package/dist/types/session/agent-session.d.ts +2 -0
  21. package/dist/types/session/blob-store.d.ts +20 -1
  22. package/dist/types/session/session-manager.d.ts +24 -6
  23. package/dist/types/session/streaming-output.d.ts +3 -2
  24. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  25. package/dist/types/task/receipt.d.ts +1 -0
  26. package/dist/types/task/types.d.ts +7 -0
  27. package/dist/types/thinking-metadata.d.ts +16 -0
  28. package/dist/types/thinking.d.ts +3 -12
  29. package/dist/types/tools/index.d.ts +2 -0
  30. package/dist/types/tools/resolve.d.ts +0 -10
  31. package/dist/types/utils/tool-choice.d.ts +14 -1
  32. package/package.json +7 -7
  33. package/src/cli.ts +8 -4
  34. package/src/commands/harness.ts +36 -2
  35. package/src/commands/launch.ts +2 -2
  36. package/src/commands/session.ts +3 -1
  37. package/src/config/model-profile-activation.ts +15 -3
  38. package/src/config/model-profiles.ts +255 -56
  39. package/src/config/model-resolver.ts +9 -6
  40. package/src/config/models-config-schema.ts +1 -0
  41. package/src/config/settings-schema.ts +6 -3
  42. package/src/coordinator-mcp/server.ts +54 -23
  43. package/src/cursor.ts +16 -2
  44. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  45. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  46. package/src/export/html/index.ts +13 -9
  47. package/src/gjc-runtime/team-runtime.ts +33 -7
  48. package/src/gjc-runtime/tmux-common.ts +15 -0
  49. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  50. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  51. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  52. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  53. package/src/harness-control-plane/owner.ts +78 -27
  54. package/src/harness-control-plane/receipt-spool.ts +128 -0
  55. package/src/harness-control-plane/state-machine.ts +27 -6
  56. package/src/harness-control-plane/storage.ts +23 -0
  57. package/src/harness-control-plane/types.ts +4 -0
  58. package/src/hindsight/mental-models.ts +17 -16
  59. package/src/internal-urls/docs-index.generated.ts +2 -2
  60. package/src/modes/components/assistant-message.ts +26 -14
  61. package/src/modes/components/diff.ts +97 -0
  62. package/src/modes/components/model-selector.ts +353 -181
  63. package/src/modes/components/tool-execution.ts +30 -13
  64. package/src/modes/controllers/selector-controller.ts +33 -42
  65. package/src/modes/rpc/rpc-client.ts +3 -2
  66. package/src/modes/rpc/rpc-mode.ts +44 -14
  67. package/src/modes/rpc/rpc-types.ts +5 -2
  68. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  69. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  70. package/src/sdk.ts +29 -2
  71. package/src/secrets/obfuscator.ts +102 -27
  72. package/src/session/agent-session.ts +105 -20
  73. package/src/session/blob-store.ts +89 -3
  74. package/src/session/session-manager.ts +309 -58
  75. package/src/session/streaming-output.ts +185 -122
  76. package/src/session/tool-choice-queue.ts +23 -0
  77. package/src/task/executor.ts +69 -6
  78. package/src/task/receipt.ts +5 -0
  79. package/src/task/render.ts +21 -1
  80. package/src/task/types.ts +8 -0
  81. package/src/thinking-metadata.ts +51 -0
  82. package/src/thinking.ts +26 -46
  83. package/src/tools/bash.ts +1 -1
  84. package/src/tools/index.ts +2 -0
  85. package/src/tools/resolve.ts +93 -18
  86. package/src/utils/edit-mode.ts +1 -1
  87. 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,164 @@ 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
+ async #refreshProviderAuth(): Promise<void> {
709
+ const providers = new Set<string>();
710
+ for (const profiles of this.#getPresetGroups().values()) {
711
+ for (const profile of profiles)
712
+ for (const provider of profileRequiredProviders(profile)) providers.add(provider);
713
+ }
714
+ this.#providerAuthPending = providers.size > 0;
715
+ this.#renderPresetLanding();
716
+ const entries = await Promise.all(
717
+ [...providers].map(async provider => {
718
+ const apiKey = await this.#modelRegistry.getApiKeyForProvider(provider, this.#authSessionId);
719
+ return [provider, isAuthenticated(apiKey)] as const;
720
+ }),
721
+ );
722
+ this.#providerAuthById = new Map(entries);
723
+ this.#providerAuthPending = false;
724
+ this.#renderPresetLanding();
725
+ this.#tui.requestRender();
726
+ }
727
+
728
+ #updatePresetExpansion(): void {
729
+ const selected = this.#getSelectedPresetRow();
730
+ if (selected?.kind === "group") this.#expandedPresetProviderId = selected.groupId;
731
+ if (selected?.kind === "profile") this.#expandedPresetProviderId = selected.groupId;
732
+ const rows = this.#getPresetRows();
733
+ this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, rows.length - 1));
734
+ }
735
+
736
+ #switchToModelMode(seed?: string): void {
737
+ this.#viewMode = "models";
738
+ this.#expandedPresetProviderId = undefined;
739
+ this.#previewProfileName = undefined;
740
+ this.#presetScopeMenuOpen = false;
741
+ this.#presetScopeIndex = 0;
742
+ this.#presetLoginHint = undefined;
743
+ this.#activeTabIndex = 0;
744
+ this.#selectedIndex = 0;
745
+ this.#searchInput.setValue(seed ?? this.#searchInput.getValue());
746
+ this.#updateTabBar();
747
+ this.#filterModels(this.#searchInput.getValue());
748
+ }
749
+
750
+ #renderPresetLanding(): void {
751
+ this.#headerContainer.clear();
752
+ this.#tabBar = null;
753
+ this.#listContainer.clear();
754
+ this.#headerContainer.addChild(new Text(theme.fg("accent", "Model presets"), 0, 0));
755
+ const rows = this.#getPresetRows();
756
+ for (let i = 0; i < rows.length; i++) {
757
+ const row = rows[i];
758
+ const selected = i === this.#presetCursor;
759
+ const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
760
+ if (row.kind === "browse") {
761
+ const label = "Browse all models";
762
+ this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
763
+ continue;
764
+ }
765
+ if (row.kind === "group") {
766
+ const authenticated = this.#isPresetAuthenticated(row.profiles);
767
+ const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
768
+ const label = `${mark} ${row.groupId}`;
769
+ const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
770
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
771
+ continue;
772
+ }
773
+ const presentation = getModelProfilePresentation(row.profile.name);
774
+ const authenticated = this.#isPresetAuthenticated(row.profile);
775
+ const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
776
+ const label = ` ${mark} ${presentation.displayName}`;
777
+ const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
778
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
779
+ }
780
+ if (this.#presetLoginHint) {
781
+ this.#listContainer.addChild(new Spacer(1));
782
+ this.#listContainer.addChild(new Text(theme.fg("warning", ` ${this.#presetLoginHint}`), 0, 0));
783
+ }
784
+ const previewProfile = this.#getProfileByName(this.#previewProfileName);
785
+ if (previewProfile) this.#renderPresetPreview(previewProfile);
786
+ }
787
+
788
+ #renderPresetPreview(profile: ModelProfileDefinition): void {
789
+ this.#listContainer.addChild(new Spacer(1));
790
+ this.#listContainer.addChild(
791
+ new Text(
792
+ theme.fg("muted", ` Preset preview: ${getModelProfilePresentation(profile.name).displayName}`),
793
+ 0,
794
+ 0,
795
+ ),
796
+ );
797
+ for (const role of PROFILE_ROLE_PREVIEW_ORDER) {
798
+ const selector = profile.modelMapping[role];
799
+ if (!selector) continue;
800
+ const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAll(), {
801
+ settings: this.#settings,
802
+ matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
803
+ modelRegistry: this.#modelRegistry,
804
+ });
805
+ const label = GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase();
806
+ this.#listContainer.addChild(
807
+ new Text(` ${label}: ${formatClampedModelSelector(selector, resolved.model)}`, 0, 0),
808
+ );
809
+ }
810
+ this.#listContainer.addChild(new Spacer(1));
811
+ if (this.#presetScopeMenuOpen) {
812
+ for (let i = 0; i < PRESET_SCOPE_LABELS.length; i++) {
813
+ const label = PRESET_SCOPE_LABELS[i] ?? "";
814
+ const prefix = i === this.#presetScopeIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
815
+ this.#listContainer.addChild(
816
+ new Text(`${prefix}${i === this.#presetScopeIndex ? theme.fg("accent", label) : label}`, 0, 0),
817
+ );
818
+ }
819
+ } else {
820
+ this.#listContainer.addChild(new Text(theme.fg("muted", " Press Enter to apply this preset"), 0, 0));
821
+ }
822
+ }
823
+
645
824
  #formatDiscoveryErrorHint(error: string | undefined): string | undefined {
646
825
  if (!error) {
647
826
  return undefined;
@@ -688,17 +867,10 @@ export class ModelSelectorComponent extends Container {
688
867
  }
689
868
  }
690
869
 
691
- #getVisibleProfiles(): ProfileItem[] {
692
- return !this.#temporaryOnly && !this.#isCanonicalTab() && this.#getActiveTabId() === ALL_TAB
693
- ? this.#profileItems
694
- : [];
695
- }
696
-
697
870
  #updateList(): void {
698
871
  this.#listContainer.clear();
699
872
  const isCanonicalTab = this.#isCanonicalTab();
700
- const visibleProfiles = this.#getVisibleProfiles();
701
- const modelSelectedIndex = Math.max(0, this.#selectedIndex - visibleProfiles.length);
873
+ const modelSelectedIndex = this.#selectedIndex;
702
874
  const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
703
875
 
704
876
  const maxVisible = 10;
@@ -710,18 +882,6 @@ export class ModelSelectorComponent extends Container {
710
882
 
711
883
  const showProvider = this.#getActiveTabId() === ALL_TAB;
712
884
 
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
885
  // Show visible slice of filtered models
726
886
  for (let i = startIndex; i < endIndex; i++) {
727
887
  const item = visibleItems[i];
@@ -729,7 +889,7 @@ export class ModelSelectorComponent extends Container {
729
889
  const canonicalItem = isCanonicalTab ? (item as CanonicalModelItem) : undefined;
730
890
  const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
731
891
 
732
- const isSelected = i + visibleProfiles.length === this.#selectedIndex;
892
+ const isSelected = i === this.#selectedIndex;
733
893
 
734
894
  // Build role badges (inverted: color as background, black text)
735
895
  const roleBadgeTokens: string[] = [];
@@ -786,7 +946,7 @@ export class ModelSelectorComponent extends Container {
786
946
  for (const line of errorLines) {
787
947
  this.#listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
788
948
  }
789
- } else if (visibleItems.length === 0 && visibleProfiles.length === 0) {
949
+ } else if (visibleItems.length === 0) {
790
950
  const statusMessage = this.#getProviderEmptyStateMessage();
791
951
  this.#listContainer.addChild(
792
952
  new Text(
@@ -795,11 +955,6 @@ export class ModelSelectorComponent extends Container {
795
955
  0,
796
956
  ),
797
957
  );
798
- } else if (this.#selectedIndex < visibleProfiles.length) {
799
- const selectedProfile = visibleProfiles[this.#selectedIndex];
800
- if (selectedProfile && this.#pendingActionItem) {
801
- this.#renderProfileActionMenu(selectedProfile);
802
- }
803
958
  } else {
804
959
  const selected = visibleItems[modelSelectedIndex];
805
960
  if (!selected) {
@@ -828,9 +983,7 @@ export class ModelSelectorComponent extends Container {
828
983
  for (let i = 0; i < actionCount; i++) {
829
984
  const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
830
985
  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})`;
986
+ const label = `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`;
834
987
  this.#listContainer.addChild(
835
988
  new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
836
989
  );
@@ -855,32 +1008,17 @@ export class ModelSelectorComponent extends Container {
855
1008
  }
856
1009
  }
857
1010
 
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
1011
  #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
873
1012
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
874
1013
  }
875
- #getActionCount(model: Model): number {
876
- return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + (supportsOpenAICodexPreset(model) ? 1 : 0);
1014
+ #getActionCount(_model: Model): number {
1015
+ return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
877
1016
  }
878
1017
 
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];
1018
+ #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
1019
+ return this.#isCanonicalTab()
1020
+ ? this.#filteredCanonicalModels[this.#selectedIndex]
1021
+ : this.#filteredModels[this.#selectedIndex];
884
1022
  }
885
1023
 
886
1024
  handleInput(keyData: string): void {
@@ -893,6 +1031,11 @@ export class ModelSelectorComponent extends Container {
893
1031
  return;
894
1032
  }
895
1033
 
1034
+ if (this.#viewMode === "presets") {
1035
+ this.#handlePresetLandingInput(keyData);
1036
+ return;
1037
+ }
1038
+
896
1039
  // Tab bar navigation
897
1040
  if (this.#tabBar?.handleInput(keyData)) {
898
1041
  return;
@@ -900,9 +1043,7 @@ export class ModelSelectorComponent extends Container {
900
1043
 
901
1044
  // Up arrow - navigate list (wrap to bottom when at top)
902
1045
  if (matchesKey(keyData, "up")) {
903
- const itemCount =
904
- this.#getVisibleProfiles().length +
905
- (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
1046
+ const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
906
1047
  if (itemCount === 0) return;
907
1048
  this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
908
1049
  this.#updateList();
@@ -911,9 +1052,7 @@ export class ModelSelectorComponent extends Container {
911
1052
 
912
1053
  // Down arrow - navigate list (wrap to top when at bottom)
913
1054
  if (matchesKey(keyData, "down")) {
914
- const itemCount =
915
- this.#getVisibleProfiles().length +
916
- (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
1055
+ const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
917
1056
  if (itemCount === 0) return;
918
1057
  this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
919
1058
  this.#updateList();
@@ -924,13 +1063,7 @@ export class ModelSelectorComponent extends Container {
924
1063
  // existing non-persistent quick-switch behavior.
925
1064
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
926
1065
  const selectedItem = this.#getSelectedItem();
927
- if (selectedItem) {
928
- if (selectedItem.kind === "profile") {
929
- this.#beginProfileActionMenu(selectedItem);
930
- } else {
931
- this.#beginActionMenuOrSelect(selectedItem);
932
- }
933
- }
1066
+ if (selectedItem) this.#beginActionMenuOrSelect(selectedItem);
934
1067
  return;
935
1068
  }
936
1069
 
@@ -944,10 +1077,99 @@ export class ModelSelectorComponent extends Container {
944
1077
  this.#searchInput.handleInput(keyData);
945
1078
  this.#filterModels(this.#searchInput.getValue());
946
1079
  }
947
- #beginProfileActionMenu(profile: ProfileItem): void {
948
- this.#pendingActionItem = profile as unknown as ModelItem;
949
- this.#selectedActionIndex = 0;
950
- this.#updateList();
1080
+
1081
+ #handlePresetLandingInput(keyData: string): void {
1082
+ if (isPrintableCharacter(keyData)) {
1083
+ this.#switchToModelMode(keyData);
1084
+ return;
1085
+ }
1086
+ if (matchesKey(keyData, "up")) {
1087
+ const rows = this.#getPresetRows();
1088
+ if (rows.length === 0) return;
1089
+ if (this.#presetScopeMenuOpen) {
1090
+ this.#presetScopeIndex =
1091
+ this.#presetScopeIndex === 0 ? PRESET_SCOPE_LABELS.length - 1 : this.#presetScopeIndex - 1;
1092
+ } else {
1093
+ this.#presetCursor = this.#presetCursor === 0 ? rows.length - 1 : this.#presetCursor - 1;
1094
+ this.#previewProfileName = undefined;
1095
+ this.#presetLoginHint = undefined;
1096
+ this.#updatePresetExpansion();
1097
+ }
1098
+ this.#renderPresetLanding();
1099
+ return;
1100
+ }
1101
+ if (matchesKey(keyData, "down")) {
1102
+ const rows = this.#getPresetRows();
1103
+ if (rows.length === 0) return;
1104
+ if (this.#presetScopeMenuOpen) {
1105
+ this.#presetScopeIndex = (this.#presetScopeIndex + 1) % PRESET_SCOPE_LABELS.length;
1106
+ } else {
1107
+ this.#presetCursor = (this.#presetCursor + 1) % rows.length;
1108
+ this.#previewProfileName = undefined;
1109
+ this.#presetLoginHint = undefined;
1110
+ this.#updatePresetExpansion();
1111
+ }
1112
+ this.#renderPresetLanding();
1113
+ return;
1114
+ }
1115
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
1116
+ this.#handlePresetEnter();
1117
+ return;
1118
+ }
1119
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
1120
+ if (this.#presetScopeMenuOpen) {
1121
+ this.#presetScopeMenuOpen = false;
1122
+ this.#renderPresetLanding();
1123
+ return;
1124
+ }
1125
+ if (this.#previewProfileName) {
1126
+ this.#previewProfileName = undefined;
1127
+ this.#renderPresetLanding();
1128
+ return;
1129
+ }
1130
+ if (this.#expandedPresetProviderId) {
1131
+ this.#expandedPresetProviderId = undefined;
1132
+ this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, this.#getPresetRows().length - 1));
1133
+ this.#renderPresetLanding();
1134
+ return;
1135
+ }
1136
+ this.#onCancelCallback();
1137
+ }
1138
+ }
1139
+
1140
+ #handlePresetEnter(): void {
1141
+ if (this.#presetScopeMenuOpen && this.#previewProfileName) {
1142
+ this.#onSelectCallback({
1143
+ kind: "profile",
1144
+ profileName: this.#previewProfileName,
1145
+ setDefault: this.#presetScopeIndex === 1,
1146
+ });
1147
+ return;
1148
+ }
1149
+ if (this.#previewProfileName) {
1150
+ this.#presetScopeMenuOpen = true;
1151
+ this.#presetScopeIndex = 0;
1152
+ this.#renderPresetLanding();
1153
+ return;
1154
+ }
1155
+ const row = this.#getSelectedPresetRow();
1156
+ if (!row) return;
1157
+ if (row.kind === "browse") {
1158
+ this.#switchToModelMode();
1159
+ return;
1160
+ }
1161
+ const missing =
1162
+ row.kind === "group" ? this.#getMissingProviders(row.profiles) : this.#getMissingProviders(row.profile);
1163
+ if (missing.length > 0) {
1164
+ this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1165
+ this.#renderPresetLanding();
1166
+ return;
1167
+ }
1168
+ if (row.kind === "profile") {
1169
+ this.#previewProfileName = row.profile.name;
1170
+ this.#presetLoginHint = undefined;
1171
+ this.#renderPresetLanding();
1172
+ }
951
1173
  }
952
1174
 
953
1175
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
@@ -963,7 +1185,7 @@ export class ModelSelectorComponent extends Container {
963
1185
  #handleActionMenuInput(keyData: string): void {
964
1186
  const item = this.#pendingActionItem;
965
1187
  if (!item) return;
966
- const actionCount = (item as unknown as ProfileItem).kind === "profile" ? 2 : this.#getActionCount(item.model);
1188
+ const actionCount = this.#getActionCount(item.model);
967
1189
  if (matchesKey(keyData, "up")) {
968
1190
  this.#selectedActionIndex = this.#selectedActionIndex === 0 ? actionCount - 1 : this.#selectedActionIndex - 1;
969
1191
  this.#updateList();
@@ -976,21 +1198,8 @@ export class ModelSelectorComponent extends Container {
976
1198
  }
977
1199
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
978
1200
  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
- }
1201
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
1202
+ if (role) this.#handleSelect(item, role);
994
1203
  return;
995
1204
  }
996
1205
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
@@ -1029,18 +1238,6 @@ export class ModelSelectorComponent extends Container {
1029
1238
  this.#updateList();
1030
1239
  }
1031
1240
  }
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
1241
 
1045
1242
  #handleSelect(
1046
1243
  item: ModelItem | CanonicalModelItem,
@@ -1100,31 +1297,6 @@ function requiresExplicitThinkingChoice(model: Model): boolean {
1100
1297
  return model.reasoning === true && (model.provider === "openai" || model.provider === "openai-codex");
1101
1298
  }
1102
1299
 
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
1300
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
1129
1301
  const levels: ThinkingLevel[] = [ThinkingLevel.Off];
1130
1302
  let efforts: readonly string[];