@makefinks/daemon 0.9.1 → 0.11.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 (40) hide show
  1. package/README.md +60 -14
  2. package/package.json +4 -2
  3. package/src/ai/copilot-client.ts +775 -0
  4. package/src/ai/daemon-ai.ts +32 -234
  5. package/src/ai/model-config.ts +55 -14
  6. package/src/ai/providers/capabilities.ts +16 -0
  7. package/src/ai/providers/copilot-provider.ts +632 -0
  8. package/src/ai/providers/openrouter-provider.ts +217 -0
  9. package/src/ai/providers/registry.ts +14 -0
  10. package/src/ai/providers/types.ts +31 -0
  11. package/src/ai/system-prompt.ts +16 -0
  12. package/src/ai/tools/subagents.ts +1 -1
  13. package/src/ai/tools/tool-registry.ts +22 -1
  14. package/src/ai/tools/write-file.ts +51 -0
  15. package/src/app/components/AppOverlays.tsx +9 -1
  16. package/src/app/components/ConversationPane.tsx +8 -2
  17. package/src/components/ModelMenu.tsx +202 -140
  18. package/src/components/OnboardingOverlay.tsx +147 -1
  19. package/src/components/SettingsMenu.tsx +27 -1
  20. package/src/components/TokenUsageDisplay.tsx +5 -3
  21. package/src/components/tool-layouts/layouts/index.ts +1 -0
  22. package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
  23. package/src/hooks/daemon-event-handlers.ts +61 -14
  24. package/src/hooks/keyboard-handlers.ts +109 -28
  25. package/src/hooks/use-app-callbacks.ts +141 -43
  26. package/src/hooks/use-app-context-builder.ts +5 -0
  27. package/src/hooks/use-app-controller.ts +31 -2
  28. package/src/hooks/use-app-copilot-models-loader.ts +45 -0
  29. package/src/hooks/use-app-display-state.ts +24 -2
  30. package/src/hooks/use-app-model.ts +103 -17
  31. package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
  32. package/src/hooks/use-bootstrap-controller.ts +5 -0
  33. package/src/hooks/use-daemon-events.ts +8 -2
  34. package/src/hooks/use-daemon-keyboard.ts +19 -6
  35. package/src/hooks/use-daemon-runtime-controller.ts +4 -0
  36. package/src/hooks/use-menu-keyboard.ts +6 -1
  37. package/src/state/app-context.tsx +6 -0
  38. package/src/types/index.ts +24 -1
  39. package/src/utils/copilot-models.ts +77 -0
  40. package/src/utils/preferences.ts +3 -0
@@ -1,16 +1,19 @@
1
1
  import type { KeyEvent } from "@opentui/core";
2
- import { setResponseModel } from "../ai/model-config";
2
+ import { getResponseModelForProvider, setModelProvider, setResponseModel } from "../ai/model-config";
3
+ import { invalidateDaemonToolsCache } from "../ai/tools";
4
+ import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
3
5
  import type {
4
6
  AppPreferences,
5
7
  AudioDevice,
6
8
  BashApprovalLevel,
9
+ LlmProvider,
7
10
  ModelOption,
8
11
  OnboardingStep,
9
12
  ReasoningEffort,
10
13
  SpeechSpeed,
11
14
  VoiceInteractionType,
12
15
  } from "../types";
13
- import { BASH_APPROVAL_LEVELS, REASONING_EFFORT_LEVELS } from "../types";
16
+ import { BASH_APPROVAL_LEVELS, getReasoningEffortLevels } from "../types";
14
17
  import { openUrlInBrowser } from "../utils/preferences";
15
18
  import { setAudioDevice } from "../voice/audio-recorder";
16
19
  import { isNavigateDownKey, isNavigateUpKey } from "./menu-navigation";
@@ -18,6 +21,7 @@ import { isNavigateDownKey, isNavigateUpKey } from "./menu-navigation";
18
21
  export type KeyHandler = (key: KeyEvent) => boolean;
19
22
 
20
23
  const API_KEY_URLS: Record<string, string> = {
24
+ copilot_auth: "https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli",
21
25
  openrouter_key: "https://openrouter.ai/keys",
22
26
  openai_key: "https://platform.openai.com/api-keys",
23
27
  exa_key: "https://dashboard.exa.ai/api-keys",
@@ -25,13 +29,18 @@ const API_KEY_URLS: Record<string, string> = {
25
29
 
26
30
  interface OnboardingContext {
27
31
  step: OnboardingStep;
32
+ selectedProviderIdx: number;
28
33
  devices: AudioDevice[];
29
34
  models: ModelOption[];
30
35
  selectedDeviceIdx: number;
31
36
  selectedModelIdx: number;
37
+ currentModelProvider: LlmProvider;
38
+ copilotAuthenticated: boolean;
32
39
  preferences: AppPreferences | null;
40
+ setSelectedProviderIdx: (fn: (prev: number) => number) => void;
33
41
  setSelectedDeviceIdx: (fn: (prev: number) => number) => void;
34
42
  setSelectedModelIdx: (fn: (prev: number) => number) => void;
43
+ setCurrentModelProvider: (provider: LlmProvider) => void;
35
44
  setCurrentDevice: (device: string) => void;
36
45
  setCurrentOutputDevice: (device: string) => void;
37
46
  setCurrentModelId: (modelId: string) => void;
@@ -49,28 +58,19 @@ export function getApiKeyUrl(step: OnboardingStep): string | null {
49
58
  }
50
59
 
51
60
  export function isApiKeyStep(step: OnboardingStep): boolean {
52
- return step === "openrouter_key" || step === "openai_key" || step === "exa_key";
61
+ return step === "openrouter_key" || step === "copilot_auth" || step === "openai_key" || step === "exa_key";
53
62
  }
54
63
 
55
- type StepCondition = {
56
- step: OnboardingStep;
57
- check: (prefs: AppPreferences | null) => boolean;
58
- /** If true, this step is only shown during initial onboarding, not when re-prompting */
59
- onboardingOnly?: boolean;
60
- };
61
-
62
- const STEP_CONDITIONS: StepCondition[] = [
63
- { step: "openrouter_key", check: () => !process.env.OPENROUTER_API_KEY },
64
- { step: "openai_key", check: () => !process.env.OPENAI_API_KEY },
65
- { step: "exa_key", check: () => !process.env.EXA_API_KEY },
66
- { step: "device", check: (prefs) => !prefs?.audioDeviceName, onboardingOnly: true },
67
- { step: "model", check: (prefs) => !prefs?.modelId, onboardingOnly: true },
68
- { step: "settings", check: () => true, onboardingOnly: true },
69
- ];
64
+ export interface DetermineNextStepOptions {
65
+ currentProvider?: LlmProvider;
66
+ copilotAuthenticated?: boolean;
67
+ }
70
68
 
71
69
  const STEP_ORDER: OnboardingStep[] = [
72
70
  "intro",
71
+ "provider",
73
72
  "openrouter_key",
73
+ "copilot_auth",
74
74
  "openai_key",
75
75
  "exa_key",
76
76
  "device",
@@ -81,18 +81,36 @@ const STEP_ORDER: OnboardingStep[] = [
81
81
 
82
82
  export function determineNextStep(
83
83
  currentStep: OnboardingStep,
84
- preferences: AppPreferences | null
84
+ preferences: AppPreferences | null,
85
+ options: DetermineNextStepOptions = {}
85
86
  ): OnboardingStep {
86
87
  const currentIndex = STEP_ORDER.indexOf(currentStep);
87
88
  if (currentIndex === -1 || currentStep === "complete") return "complete";
88
89
 
89
90
  const isReprompt = preferences?.onboardingCompleted === true;
90
-
91
- for (const condition of STEP_CONDITIONS) {
91
+ const provider: LlmProvider = options.currentProvider ?? preferences?.modelProvider ?? "openrouter";
92
+ const hasCopilotAuth = Boolean(options.copilotAuthenticated);
93
+
94
+ const conditions: Array<{
95
+ step: OnboardingStep;
96
+ enabled: boolean;
97
+ onboardingOnly?: boolean;
98
+ }> = [
99
+ { step: "provider", enabled: true, onboardingOnly: true },
100
+ { step: "openrouter_key", enabled: provider === "openrouter" && !process.env.OPENROUTER_API_KEY },
101
+ { step: "copilot_auth", enabled: provider === "copilot" && !hasCopilotAuth },
102
+ { step: "openai_key", enabled: !process.env.OPENAI_API_KEY },
103
+ { step: "exa_key", enabled: !process.env.EXA_API_KEY },
104
+ { step: "device", enabled: !preferences?.audioDeviceName, onboardingOnly: true },
105
+ { step: "model", enabled: !preferences?.modelId, onboardingOnly: true },
106
+ { step: "settings", enabled: true, onboardingOnly: true },
107
+ ];
108
+
109
+ for (const condition of conditions) {
92
110
  if (isReprompt && condition.onboardingOnly) continue;
93
111
 
94
112
  const conditionIndex = STEP_ORDER.indexOf(condition.step);
95
- if (conditionIndex > currentIndex && condition.check(preferences)) {
113
+ if (conditionIndex > currentIndex && condition.enabled) {
96
114
  return condition.step;
97
115
  }
98
116
  }
@@ -104,9 +122,14 @@ type EscapeHandler = (ctx: OnboardingContext) => void;
104
122
 
105
123
  const ESCAPE_HANDLERS: Partial<Record<OnboardingStep, EscapeHandler>> = {
106
124
  intro: () => {},
125
+ provider: () => {},
107
126
  openrouter_key: () => {},
127
+ copilot_auth: () => {},
108
128
  openai_key: (ctx) => {
109
- const nextStep = determineNextStep("openai_key", ctx.preferences);
129
+ const nextStep = determineNextStep("openai_key", ctx.preferences, {
130
+ currentProvider: ctx.currentModelProvider,
131
+ copilotAuthenticated: ctx.copilotAuthenticated,
132
+ });
110
133
  if (nextStep === "complete") {
111
134
  ctx.persistPreferences({ onboardingCompleted: true });
112
135
  ctx.completeOnboarding();
@@ -115,7 +138,10 @@ const ESCAPE_HANDLERS: Partial<Record<OnboardingStep, EscapeHandler>> = {
115
138
  }
116
139
  },
117
140
  exa_key: (ctx) => {
118
- const nextStep = determineNextStep("exa_key", ctx.preferences);
141
+ const nextStep = determineNextStep("exa_key", ctx.preferences, {
142
+ currentProvider: ctx.currentModelProvider,
143
+ copilotAuthenticated: ctx.copilotAuthenticated,
144
+ });
119
145
  if (nextStep === "complete") {
120
146
  ctx.persistPreferences({ onboardingCompleted: true });
121
147
  ctx.completeOnboarding();
@@ -156,11 +182,54 @@ export function handleOnboardingKey(key: KeyEvent, ctx: OnboardingContext): bool
156
182
 
157
183
  if (step === "intro") {
158
184
  if (key.name === "return") {
159
- ctx.setOnboardingStep(determineNextStep(step, ctx.preferences));
185
+ ctx.setOnboardingStep(
186
+ determineNextStep(step, ctx.preferences, {
187
+ currentProvider: ctx.currentModelProvider,
188
+ copilotAuthenticated: ctx.copilotAuthenticated,
189
+ })
190
+ );
160
191
  }
161
192
  return true;
162
193
  }
163
194
 
195
+ if (step === "provider") {
196
+ const providers: LlmProvider[] = ctx.copilotAuthenticated ? ["openrouter", "copilot"] : ["openrouter"];
197
+
198
+ if (isNavigateUpKey(key) || isNavigateDownKey(key)) {
199
+ ctx.setSelectedProviderIdx((prev) => {
200
+ if (isNavigateUpKey(key)) {
201
+ return prev > 0 ? prev - 1 : providers.length - 1;
202
+ }
203
+ return prev < providers.length - 1 ? prev + 1 : 0;
204
+ });
205
+ return true;
206
+ }
207
+
208
+ if (key.name === "return") {
209
+ const selectedProvider = providers[ctx.selectedProviderIdx] ?? "openrouter";
210
+ setModelProvider(selectedProvider);
211
+ invalidateDaemonToolsCache();
212
+ invalidateSubagentToolsCache();
213
+ ctx.setCurrentModelProvider(selectedProvider);
214
+
215
+ const defaultModelId = getResponseModelForProvider(selectedProvider);
216
+
217
+ ctx.persistPreferences({
218
+ modelProvider: selectedProvider,
219
+ modelId: defaultModelId,
220
+ });
221
+
222
+ ctx.setOnboardingStep(
223
+ determineNextStep("provider", ctx.preferences, {
224
+ currentProvider: selectedProvider,
225
+ copilotAuthenticated: ctx.copilotAuthenticated,
226
+ })
227
+ );
228
+ }
229
+
230
+ return true;
231
+ }
232
+
164
233
  if (isApiKeyStep(step)) {
165
234
  return false;
166
235
  }
@@ -224,6 +293,7 @@ export function handleOnboardingKey(key: KeyEvent, ctx: OnboardingContext): bool
224
293
  setResponseModel(selectedModel.id);
225
294
  ctx.setCurrentModelId(selectedModel.id);
226
295
  ctx.persistPreferences({
296
+ modelProvider: ctx.currentModelProvider,
227
297
  modelId: selectedModel.id,
228
298
  openRouterProviderTag: undefined,
229
299
  });
@@ -248,17 +318,20 @@ interface SettingsMenuContext {
248
318
  selectedIdx: number;
249
319
  menuItemCount: number;
250
320
  interactionMode: "text" | "voice";
321
+ modelProvider: LlmProvider;
251
322
  voiceInteractionType: VoiceInteractionType;
252
323
  speechSpeed: SpeechSpeed;
253
324
  reasoningEffort: ReasoningEffort;
254
325
  bashApprovalLevel: BashApprovalLevel;
255
326
  supportsReasoning: boolean;
327
+ supportsReasoningXHigh: boolean;
256
328
  canEnableVoiceOutput: boolean;
257
329
  showFullReasoning: boolean;
258
330
  showToolOutput: boolean;
259
331
  memoryEnabled: boolean;
260
332
  setSelectedIdx: (fn: (prev: number) => number) => void;
261
333
  toggleInteractionMode: () => void;
334
+ cycleModelProvider: () => void;
262
335
  setVoiceInteractionType: (type: VoiceInteractionType) => void;
263
336
  setSpeechSpeed: (speed: SpeechSpeed) => void;
264
337
  setReasoningEffort: (effort: ReasoningEffort) => void;
@@ -316,6 +389,13 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
316
389
  }
317
390
  settingIdx++;
318
391
 
392
+ if (ctx.selectedIdx === settingIdx) {
393
+ ctx.cycleModelProvider();
394
+ key.preventDefault();
395
+ return true;
396
+ }
397
+ settingIdx++;
398
+
319
399
  if (ctx.selectedIdx === settingIdx) {
320
400
  const current = ctx.manager.voiceInteractionType;
321
401
  const next = current === "direct" ? "review" : "direct";
@@ -329,10 +409,11 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
329
409
 
330
410
  if (ctx.selectedIdx === settingIdx) {
331
411
  if (ctx.supportsReasoning) {
412
+ const effortLevels = getReasoningEffortLevels(ctx.supportsReasoningXHigh);
332
413
  const currentEffort = ctx.manager.reasoningEffort;
333
- const currentIndex = REASONING_EFFORT_LEVELS.indexOf(currentEffort);
334
- const nextIndex = (currentIndex + 1) % REASONING_EFFORT_LEVELS.length;
335
- const nextEffort = REASONING_EFFORT_LEVELS[nextIndex] ?? "medium";
414
+ const currentIndex = effortLevels.indexOf(currentEffort);
415
+ const nextIndex = (currentIndex + 1) % effortLevels.length;
416
+ const nextEffort = effortLevels[nextIndex] ?? "medium";
336
417
  ctx.manager.reasoningEffort = nextEffort;
337
418
  ctx.setReasoningEffort(nextEffort);
338
419
  ctx.persistPreferences({ reasoningEffort: nextEffort });
@@ -1,10 +1,21 @@
1
1
  import { useCallback } from "react";
2
- import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
2
+ import { toast } from "@opentui-ui/toast/react";
3
+ import {
4
+ getResponseModelForProvider,
5
+ setModelProvider,
6
+ setOpenRouterProviderTag,
7
+ setResponseModel,
8
+ setResponseModelForProvider,
9
+ } from "../ai/model-config";
10
+ import { invalidateDaemonToolsCache } from "../ai/tools";
11
+ import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
12
+ import { getCopilotAuthStatusSafe, resetCopilotClient } from "../ai/copilot-client";
3
13
  import { setAudioDevice } from "../voice/audio-recorder";
4
14
  import { getDaemonManager } from "../state/daemon-state";
5
15
  import type {
6
16
  AppPreferences,
7
17
  AudioDevice,
18
+ LlmProvider,
8
19
  ModelOption,
9
20
  OnboardingStep,
10
21
  ReasoningEffort,
@@ -14,8 +25,11 @@ import type {
14
25
  import { determineNextStep } from "./keyboard-handlers";
15
26
 
16
27
  export interface UseAppCallbacksParams {
28
+ currentModelProvider: LlmProvider;
29
+ setCurrentModelProvider: (provider: LlmProvider) => void;
17
30
  currentModelId: string;
18
31
  setCurrentModelId: (modelId: string) => void;
32
+ setCurrentModelForProvider: (provider: LlmProvider, modelId: string) => void;
19
33
  setCurrentDevice: (deviceName: string | undefined) => void;
20
34
  setCurrentOutputDevice: (deviceName: string | undefined) => void;
21
35
  setCurrentOpenRouterProviderTag: (tag: string | undefined) => void;
@@ -26,7 +40,9 @@ export interface UseAppCallbacksParams {
26
40
  persistPreferences: (updates: Partial<AppPreferences>) => void;
27
41
  loadedPreferences: AppPreferences | null;
28
42
  onboardingStep: OnboardingStep;
43
+ copilotAuthenticated: boolean;
29
44
  setOnboardingStep: (step: OnboardingStep) => void;
45
+ setCopilotAuthenticated: (authenticated: boolean) => void;
30
46
  apiKeyTextareaRef: React.RefObject<{ plainText?: string; setText: (v: string) => void } | null>;
31
47
  setShowDeviceMenu: (show: boolean) => void;
32
48
  setShowModelMenu: (show: boolean) => void;
@@ -40,6 +56,7 @@ export interface UseAppCallbacksReturn {
40
56
  handleDeviceSelect: (device: AudioDevice) => void;
41
57
  handleOutputDeviceSelect: (device: AudioDevice) => void;
42
58
  handleModelSelect: (model: ModelOption) => void;
59
+ cycleModelProvider: () => void;
43
60
  handleProviderSelect: (providerTag: string | undefined) => void;
44
61
  toggleInteractionMode: () => void;
45
62
  completeOnboarding: () => void;
@@ -48,8 +65,11 @@ export interface UseAppCallbacksReturn {
48
65
 
49
66
  export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksReturn {
50
67
  const {
68
+ currentModelProvider,
69
+ setCurrentModelProvider,
51
70
  currentModelId,
52
71
  setCurrentModelId,
72
+ setCurrentModelForProvider,
53
73
  setCurrentDevice,
54
74
  setCurrentOutputDevice,
55
75
  setCurrentOpenRouterProviderTag,
@@ -60,7 +80,9 @@ export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksR
60
80
  persistPreferences,
61
81
  loadedPreferences,
62
82
  onboardingStep,
83
+ copilotAuthenticated,
63
84
  setOnboardingStep,
85
+ setCopilotAuthenticated,
64
86
  apiKeyTextareaRef,
65
87
  setShowDeviceMenu,
66
88
  setShowModelMenu,
@@ -92,19 +114,61 @@ export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksR
92
114
  const handleModelSelect = useCallback(
93
115
  (model: ModelOption) => {
94
116
  if (model.id !== currentModelId) {
95
- setResponseModel(model.id);
96
- setOpenRouterProviderTag(undefined);
117
+ setResponseModelForProvider(currentModelProvider, model.id);
97
118
  setCurrentModelId(model.id);
98
- setCurrentOpenRouterProviderTag(undefined);
99
- persistPreferences({
100
- modelId: model.id,
101
- openRouterProviderTag: undefined,
102
- });
119
+ if (currentModelProvider === "openrouter") {
120
+ setOpenRouterProviderTag(undefined);
121
+ setCurrentOpenRouterProviderTag(undefined);
122
+ persistPreferences({
123
+ modelProvider: currentModelProvider,
124
+ modelId: model.id,
125
+ openRouterProviderTag: undefined,
126
+ });
127
+ } else {
128
+ persistPreferences({
129
+ modelProvider: currentModelProvider,
130
+ modelId: model.id,
131
+ });
132
+ }
103
133
  }
104
134
  },
105
- [currentModelId, setCurrentModelId, setCurrentOpenRouterProviderTag, persistPreferences]
135
+ [
136
+ currentModelProvider,
137
+ currentModelId,
138
+ setCurrentModelId,
139
+ setCurrentOpenRouterProviderTag,
140
+ persistPreferences,
141
+ ]
106
142
  );
107
143
 
144
+ const cycleModelProvider = useCallback(() => {
145
+ const nextProvider: LlmProvider = currentModelProvider === "openrouter" ? "copilot" : "openrouter";
146
+ if (nextProvider === "copilot" && !copilotAuthenticated) {
147
+ toast.error("COPILOT LOGIN REQUIRED", {
148
+ description: "Run `gh auth login` and `copilot login` to enable the Copilot provider.",
149
+ });
150
+ return;
151
+ }
152
+ const nextModelId = getResponseModelForProvider(nextProvider);
153
+
154
+ setModelProvider(nextProvider);
155
+ invalidateDaemonToolsCache();
156
+ invalidateSubagentToolsCache();
157
+ setCurrentModelProvider(nextProvider);
158
+ setCurrentModelForProvider(nextProvider, nextModelId);
159
+
160
+ persistPreferences({
161
+ modelProvider: nextProvider,
162
+ modelId: nextModelId,
163
+ });
164
+ }, [
165
+ currentModelProvider,
166
+ copilotAuthenticated,
167
+ setCurrentModelProvider,
168
+ setCurrentModelForProvider,
169
+ persistPreferences,
170
+ ]);
171
+
108
172
  const handleProviderSelect = useCallback(
109
173
  (providerTag: string | undefined) => {
110
174
  setOpenRouterProviderTag(providerTag);
@@ -145,48 +209,81 @@ export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksR
145
209
  ]);
146
210
 
147
211
  const handleApiKeySubmit = useCallback(() => {
148
- const key = (apiKeyTextareaRef.current?.plainText ?? "").trim();
149
- if (!key) return;
150
-
151
- if (onboardingStep === "openrouter_key") {
152
- process.env.OPENROUTER_API_KEY = key;
153
- persistPreferences({ openRouterApiKey: key });
154
- const nextStep = determineNextStep("openrouter_key", loadedPreferences);
155
- if (nextStep === "complete") {
156
- persistPreferences({ onboardingCompleted: true });
157
- completeOnboarding();
158
- } else {
159
- setOnboardingStep(nextStep);
160
- }
161
- } else if (onboardingStep === "openai_key") {
162
- process.env.OPENAI_API_KEY = key;
163
- persistPreferences({ openAiApiKey: key });
164
- const nextStep = determineNextStep("openai_key", loadedPreferences);
165
- if (nextStep === "complete") {
166
- persistPreferences({ onboardingCompleted: true });
167
- completeOnboarding();
168
- } else {
169
- setOnboardingStep(nextStep);
170
- }
171
- } else if (onboardingStep === "exa_key") {
172
- process.env.EXA_API_KEY = key;
173
- persistPreferences({ exaApiKey: key });
174
- const nextStep = determineNextStep("exa_key", loadedPreferences);
175
- if (nextStep === "complete") {
176
- persistPreferences({ onboardingCompleted: true });
177
- completeOnboarding();
178
- } else {
179
- setOnboardingStep(nextStep);
212
+ void (async () => {
213
+ const key = (apiKeyTextareaRef.current?.plainText ?? "").trim();
214
+
215
+ if (onboardingStep !== "copilot_auth" && !key) return;
216
+
217
+ if (onboardingStep === "openrouter_key") {
218
+ process.env.OPENROUTER_API_KEY = key;
219
+ persistPreferences({ openRouterApiKey: key });
220
+ const nextStep = determineNextStep("openrouter_key", loadedPreferences, {
221
+ currentProvider: currentModelProvider,
222
+ });
223
+ if (nextStep === "complete") {
224
+ persistPreferences({ onboardingCompleted: true });
225
+ completeOnboarding();
226
+ } else {
227
+ setOnboardingStep(nextStep);
228
+ }
229
+ } else if (onboardingStep === "copilot_auth") {
230
+ await resetCopilotClient();
231
+ const authStatus = await getCopilotAuthStatusSafe();
232
+ setCopilotAuthenticated(authStatus.isAuthenticated);
233
+ if (!authStatus.isAuthenticated) {
234
+ toast.error("COPILOT AUTH REQUIRED", {
235
+ description: "Run `gh auth login` and `copilot login`, then press ENTER to retry.",
236
+ });
237
+ return;
238
+ }
239
+
240
+ const nextStep = determineNextStep("copilot_auth", loadedPreferences, {
241
+ currentProvider: currentModelProvider,
242
+ copilotAuthenticated: true,
243
+ });
244
+ if (nextStep === "complete") {
245
+ persistPreferences({ onboardingCompleted: true });
246
+ completeOnboarding();
247
+ } else {
248
+ setOnboardingStep(nextStep);
249
+ }
250
+ } else if (onboardingStep === "openai_key") {
251
+ process.env.OPENAI_API_KEY = key;
252
+ persistPreferences({ openAiApiKey: key });
253
+ const nextStep = determineNextStep("openai_key", loadedPreferences, {
254
+ currentProvider: currentModelProvider,
255
+ });
256
+ if (nextStep === "complete") {
257
+ persistPreferences({ onboardingCompleted: true });
258
+ completeOnboarding();
259
+ } else {
260
+ setOnboardingStep(nextStep);
261
+ }
262
+ } else if (onboardingStep === "exa_key") {
263
+ process.env.EXA_API_KEY = key;
264
+ persistPreferences({ exaApiKey: key });
265
+ const nextStep = determineNextStep("exa_key", loadedPreferences, {
266
+ currentProvider: currentModelProvider,
267
+ });
268
+ if (nextStep === "complete") {
269
+ persistPreferences({ onboardingCompleted: true });
270
+ completeOnboarding();
271
+ } else {
272
+ setOnboardingStep(nextStep);
273
+ }
180
274
  }
181
- }
182
275
 
183
- apiKeyTextareaRef.current?.setText("");
276
+ apiKeyTextareaRef.current?.setText("");
277
+ })();
184
278
  }, [
185
279
  onboardingStep,
186
280
  persistPreferences,
187
281
  loadedPreferences,
282
+ currentModelProvider,
283
+ copilotAuthenticated,
188
284
  completeOnboarding,
189
285
  setOnboardingStep,
286
+ setCopilotAuthenticated,
190
287
  apiKeyTextareaRef,
191
288
  ]);
192
289
 
@@ -194,6 +291,7 @@ export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksR
194
291
  handleDeviceSelect,
195
292
  handleOutputDeviceSelect,
196
293
  handleModelSelect,
294
+ cycleModelProvider,
197
295
  handleProviderSelect,
198
296
  toggleInteractionMode,
199
297
  completeOnboarding,
@@ -15,6 +15,7 @@ import type {
15
15
  AudioDevice,
16
16
  BashApprovalLevel,
17
17
  GroundingMap,
18
+ LlmProvider,
18
19
  ModelOption,
19
20
  OnboardingStep,
20
21
  ReasoningEffort,
@@ -65,6 +66,7 @@ export interface UseAppContextBuilderParams {
65
66
  reasoningEffort: ReasoningEffort;
66
67
  bashApprovalLevel: BashApprovalLevel;
67
68
  supportsReasoning: boolean;
69
+ supportsReasoningXHigh: boolean;
68
70
  canEnableVoiceOutput: boolean;
69
71
  showFullReasoning: boolean;
70
72
  setShowFullReasoning: (show: boolean) => void;
@@ -81,6 +83,8 @@ export interface UseAppContextBuilderParams {
81
83
  openRouterModels: ModelOption[];
82
84
  openRouterModelsLoading: boolean;
83
85
  openRouterModelsUpdatedAt: number | null;
86
+ currentModelProvider: LlmProvider;
87
+ setCurrentModelProvider: (provider: LlmProvider) => void;
84
88
  currentModelId: string;
85
89
  setCurrentModelId: (modelId: string) => void;
86
90
  providerMenuItems: ProviderMenuItem[];
@@ -102,6 +106,7 @@ export interface UseAppContextBuilderParams {
102
106
  onboarding: {
103
107
  onboardingActive: boolean;
104
108
  onboardingStep: OnboardingStep;
109
+ copilotAuthenticated: boolean;
105
110
  setOnboardingStep: (step: OnboardingStep) => void;
106
111
  onboardingPreferences: AppPreferences | null;
107
112
  apiKeyTextareaRef: MutableRefObject<TextareaRenderable | null>;