@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.
- package/CHANGELOG.md +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
|
-
import {
|
|
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
|
|
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 {
|
|
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.#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.#
|
|
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.#
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
876
|
-
return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length
|
|
1023
|
+
#getActionCount(_model: Model): number {
|
|
1024
|
+
return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
|
|
877
1025
|
}
|
|
878
1026
|
|
|
879
|
-
#getSelectedItem(): ModelItem | CanonicalModelItem |
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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 =
|
|
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
|
-
|
|
980
|
-
|
|
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[];
|