@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
@@ -123,8 +123,13 @@ export function useAppController({
123
123
  showProviderMenu,
124
124
  });
125
125
  const {
126
+ currentModelProvider,
127
+ setCurrentModelProvider,
126
128
  currentModelId,
129
+ currentModelSupportsReasoning,
130
+ currentModelSupportsReasoningXHigh,
127
131
  setCurrentModelId,
132
+ setCurrentModelForProvider,
128
133
  currentOpenRouterProviderTag,
129
134
  setCurrentOpenRouterProviderTag,
130
135
  modelsWithPricing,
@@ -154,6 +159,7 @@ export function useAppController({
154
159
  }, [onboardingComplete]);
155
160
 
156
161
  const daemon = useDaemonRuntimeController({
162
+ currentModelProvider,
157
163
  currentModelId,
158
164
  preferencesLoaded,
159
165
  sessionId: session.currentSessionId,
@@ -166,12 +172,18 @@ export function useAppController({
166
172
  const [apiKeyMissingError, setApiKeyMissingError] = useState<string>("");
167
173
  const [escPendingCancel, setEscPendingCancel] = useState(false);
168
174
 
169
- const supportsReasoning = daemon.modelMetadata?.supportsReasoning ?? false;
175
+ const supportsReasoning =
176
+ currentModelProvider === "copilot"
177
+ ? currentModelSupportsReasoning
178
+ : (daemon.modelMetadata?.supportsReasoning ?? false);
179
+ const supportsReasoningXHigh =
180
+ currentModelProvider === "copilot" ? currentModelSupportsReasoningXHigh : false;
170
181
 
171
182
  // Preferences bootstrap (hook): returns a stable persist callback.
172
183
  const { persistPreferences } = useAppPreferencesBootstrap({
173
184
  manager,
174
- setCurrentModelId,
185
+ setCurrentModelProvider,
186
+ setCurrentModelForProvider,
175
187
  setCurrentOpenRouterProviderTag,
176
188
  setCurrentDevice: bootstrap.setCurrentDevice,
177
189
  setCurrentOutputDevice: bootstrap.setCurrentOutputDevice,
@@ -186,6 +198,7 @@ export function useAppController({
186
198
  setLoadedPreferences: bootstrap.setLoadedPreferences,
187
199
  setOnboardingActive: bootstrap.setOnboardingActive,
188
200
  setOnboardingStep: bootstrap.setOnboardingStep,
201
+ setCopilotAuthenticated: bootstrap.setCopilotAuthenticated,
189
202
  setPreferencesLoaded,
190
203
  });
191
204
 
@@ -218,13 +231,17 @@ export function useAppController({
218
231
  handleDeviceSelect,
219
232
  handleOutputDeviceSelect,
220
233
  handleModelSelect,
234
+ cycleModelProvider,
221
235
  handleProviderSelect,
222
236
  toggleInteractionMode,
223
237
  completeOnboarding,
224
238
  handleApiKeySubmit,
225
239
  } = useAppCallbacks({
240
+ currentModelProvider,
241
+ setCurrentModelProvider,
226
242
  currentModelId,
227
243
  setCurrentModelId,
244
+ setCurrentModelForProvider,
228
245
  setCurrentDevice: bootstrap.setCurrentDevice,
229
246
  setCurrentOutputDevice: bootstrap.setCurrentOutputDevice,
230
247
  setCurrentOpenRouterProviderTag,
@@ -235,7 +252,9 @@ export function useAppController({
235
252
  persistPreferences,
236
253
  loadedPreferences: bootstrap.loadedPreferences,
237
254
  onboardingStep: bootstrap.onboardingStep,
255
+ copilotAuthenticated: bootstrap.copilotAuthenticated,
238
256
  setOnboardingStep: bootstrap.setOnboardingStep,
257
+ setCopilotAuthenticated: bootstrap.setCopilotAuthenticated,
239
258
  apiKeyTextareaRef: bootstrap.apiKeyTextareaRef,
240
259
  setShowDeviceMenu,
241
260
  setShowModelMenu,
@@ -367,6 +386,7 @@ export function useAppController({
367
386
  hasGrounding: session.hasGrounding,
368
387
  showFullReasoning,
369
388
  showToolOutput,
389
+ currentModelProvider,
370
390
  },
371
391
  keyboardActions
372
392
  );
@@ -379,8 +399,11 @@ export function useAppController({
379
399
  reasoningQueue: daemon.reasoning.reasoningQueue,
380
400
  responseElapsedMs: daemon.responseElapsedMs,
381
401
  hasInteracted: daemon.hasInteracted,
402
+ currentModelProvider,
382
403
  currentModelId,
383
404
  modelMetadata: daemon.modelMetadata,
405
+ curatedModels: modelsWithPricing,
406
+ availableModels: openRouterModels,
384
407
  preferencesLoaded,
385
408
  currentSessionId: session.currentSessionId,
386
409
  sessionMenuItems: session.sessionMenuItems,
@@ -466,6 +489,7 @@ export function useAppController({
466
489
  reasoningEffort,
467
490
  bashApprovalLevel,
468
491
  supportsReasoning,
492
+ supportsReasoningXHigh,
469
493
  canEnableVoiceOutput,
470
494
  showFullReasoning,
471
495
  setShowFullReasoning,
@@ -481,6 +505,8 @@ export function useAppController({
481
505
  openRouterModels,
482
506
  openRouterModelsLoading,
483
507
  openRouterModelsUpdatedAt,
508
+ currentModelProvider,
509
+ setCurrentModelProvider,
484
510
  currentModelId,
485
511
  setCurrentModelId,
486
512
  providerMenuItems,
@@ -499,6 +525,7 @@ export function useAppController({
499
525
  onboarding: {
500
526
  onboardingActive: bootstrap.onboardingActive,
501
527
  onboardingStep: bootstrap.onboardingStep,
528
+ copilotAuthenticated: bootstrap.copilotAuthenticated,
502
529
  setOnboardingStep: bootstrap.setOnboardingStep,
503
530
  onboardingPreferences: bootstrap.loadedPreferences,
504
531
  apiKeyTextareaRef: bootstrap.apiKeyTextareaRef,
@@ -509,6 +536,7 @@ export function useAppController({
509
536
  },
510
537
  settingsCallbacks: {
511
538
  onToggleInteractionMode: toggleInteractionMode,
539
+ onCycleModelProvider: cycleModelProvider,
512
540
  onSetVoiceInteractionType: setVoiceInteractionType,
513
541
  onSetSpeechSpeed: setSpeechSpeed,
514
542
  onSetReasoningEffort: setReasoningEffort,
@@ -590,6 +618,7 @@ export function useAppController({
590
618
  },
591
619
  sessionUsage: daemon.sessionUsage,
592
620
  modelMetadata: daemon.modelMetadata,
621
+ currentModelProvider,
593
622
  hasInteracted: daemon.hasInteracted,
594
623
  frostColor,
595
624
  initialStatusTop,
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect } from "react";
2
+ import type { ModelOption } from "../types";
3
+ import { getCopilotModels } from "../utils/copilot-models";
4
+
5
+ export interface UseAppCopilotModelsLoaderParams {
6
+ preferencesLoaded: boolean;
7
+ enabled?: boolean;
8
+ setModels: React.Dispatch<React.SetStateAction<ModelOption[]>>;
9
+ setLoading: React.Dispatch<React.SetStateAction<boolean>>;
10
+ setUpdatedAt: React.Dispatch<React.SetStateAction<number | null>>;
11
+ }
12
+
13
+ export interface UseAppCopilotModelsLoaderResult {
14
+ refresh: () => Promise<void>;
15
+ }
16
+
17
+ export function useAppCopilotModelsLoader(
18
+ params: UseAppCopilotModelsLoaderParams
19
+ ): UseAppCopilotModelsLoaderResult {
20
+ const { preferencesLoaded, enabled = true, setModels, setLoading, setUpdatedAt } = params;
21
+
22
+ const refresh = useCallback(
23
+ async (forceRefresh = false) => {
24
+ if (!preferencesLoaded || !enabled) return;
25
+ setLoading(true);
26
+ try {
27
+ const result = await getCopilotModels({ forceRefresh });
28
+ setModels(result.models);
29
+ setUpdatedAt(result.timestamp);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ },
34
+ [enabled, preferencesLoaded, setLoading, setModels, setUpdatedAt]
35
+ );
36
+
37
+ useEffect(() => {
38
+ if (!preferencesLoaded || !enabled) return;
39
+ void refresh(false);
40
+ }, [enabled, preferencesLoaded, refresh]);
41
+
42
+ return {
43
+ refresh: () => refresh(true),
44
+ };
45
+ }
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import { DaemonState } from "../types";
3
- import type { ContentBlock, SessionInfo } from "../types";
3
+ import type { ContentBlock, LlmProvider, ModelOption, SessionInfo } from "../types";
4
4
  import { COLORS, STATE_COLOR_HEX, STATUS_TEXT } from "../ui/constants";
5
5
  import { formatElapsedTime } from "../utils/formatters";
6
6
  import type { ModelMetadata } from "../utils/model-metadata";
@@ -14,8 +14,11 @@ export interface UseAppDisplayStateParams {
14
14
  responseElapsedMs: number;
15
15
  hasInteracted: boolean;
16
16
 
17
+ currentModelProvider: LlmProvider;
17
18
  currentModelId: string;
18
19
  modelMetadata: ModelMetadata | null;
20
+ curatedModels: ModelOption[];
21
+ availableModels: ModelOption[];
19
22
  preferencesLoaded: boolean;
20
23
 
21
24
  currentSessionId: string | null;
@@ -57,8 +60,11 @@ export function useAppDisplayState(params: UseAppDisplayStateParams): UseAppDisp
57
60
  reasoningQueue,
58
61
  responseElapsedMs,
59
62
  hasInteracted,
63
+ currentModelProvider,
60
64
  currentModelId,
61
65
  modelMetadata,
66
+ curatedModels,
67
+ availableModels,
62
68
  preferencesLoaded,
63
69
  currentSessionId,
64
70
  sessionMenuItems,
@@ -113,8 +119,24 @@ export function useAppDisplayState(params: UseAppDisplayStateParams): UseAppDisp
113
119
  if (modelMetadata?.name && modelMetadata.id === currentModelId) {
114
120
  return modelMetadata.name;
115
121
  }
122
+ const selectedModel =
123
+ availableModels.find((model) => model.id === currentModelId) ??
124
+ curatedModels.find((model) => model.id === currentModelId);
125
+ if (selectedModel?.name) {
126
+ return currentModelProvider === "copilot" ? `Copilot: ${selectedModel.name}` : selectedModel.name;
127
+ }
128
+ if (currentModelProvider === "copilot") {
129
+ return `Copilot: ${currentModelId}`;
130
+ }
116
131
  return undefined;
117
- }, [modelMetadata, currentModelId, preferencesLoaded]);
132
+ }, [
133
+ availableModels,
134
+ curatedModels,
135
+ currentModelProvider,
136
+ modelMetadata,
137
+ currentModelId,
138
+ preferencesLoaded,
139
+ ]);
118
140
 
119
141
  const sessionTitle = useMemo(() => {
120
142
  if (!currentSessionId) return undefined;
@@ -1,12 +1,18 @@
1
- import { useMemo, useState } from "react";
2
- import { AVAILABLE_MODELS, DEFAULT_MODEL_ID } from "../ai/model-config";
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import {
3
+ AVAILABLE_MODELS,
4
+ DEFAULT_COPILOT_MODEL_ID,
5
+ DEFAULT_MODEL_ID,
6
+ DEFAULT_MODEL_PROVIDER,
7
+ } from "../ai/model-config";
8
+ import type { ProviderMenuItem } from "../components/ProviderMenu";
9
+ import type { LlmProvider, ModelOption } from "../types";
10
+ import type { OpenRouterInferenceProvider } from "../utils/openrouter-endpoints";
3
11
  import { mergePricingAverages } from "../utils/openrouter-pricing";
12
+ import { useAppCopilotModelsLoader } from "./use-app-copilot-models-loader";
4
13
  import { useAppModelPricingLoader } from "./use-app-model-pricing-loader";
5
14
  import { useAppOpenRouterModelsLoader } from "./use-app-openrouter-models-loader";
6
15
  import { useAppOpenRouterProviderLoader } from "./use-app-openrouter-provider-loader";
7
- import type { ModelOption } from "../types";
8
- import type { ProviderMenuItem } from "../components/ProviderMenu";
9
- import type { OpenRouterInferenceProvider } from "../utils/openrouter-endpoints";
10
16
 
11
17
  export interface UseAppModelParams {
12
18
  preferencesLoaded: boolean;
@@ -14,8 +20,14 @@ export interface UseAppModelParams {
14
20
  }
15
21
 
16
22
  export interface UseAppModelReturn {
23
+ currentModelProvider: LlmProvider;
24
+ setCurrentModelProvider: React.Dispatch<React.SetStateAction<LlmProvider>>;
25
+
17
26
  currentModelId: string;
18
- setCurrentModelId: React.Dispatch<React.SetStateAction<string>>;
27
+ currentModelSupportsReasoning: boolean;
28
+ currentModelSupportsReasoningXHigh: boolean;
29
+ setCurrentModelId: (modelId: string) => void;
30
+ setCurrentModelForProvider: (provider: LlmProvider, modelId: string) => void;
19
31
 
20
32
  currentOpenRouterProviderTag: string | undefined;
21
33
  setCurrentOpenRouterProviderTag: React.Dispatch<React.SetStateAction<string | undefined>>;
@@ -27,41 +39,59 @@ export interface UseAppModelReturn {
27
39
 
28
40
  providerMenuItems: ProviderMenuItem[];
29
41
 
30
- refreshOpenRouterModels: () => void;
42
+ refreshOpenRouterModels: () => Promise<void>;
31
43
  }
32
44
 
33
45
  export function useAppModel(params: UseAppModelParams): UseAppModelReturn {
34
46
  const { preferencesLoaded, showProviderMenu } = params;
35
47
 
36
- const [currentModelId, setCurrentModelId] = useState(DEFAULT_MODEL_ID);
48
+ const [currentModelProvider, setCurrentModelProvider] = useState<LlmProvider>(DEFAULT_MODEL_PROVIDER);
49
+ const [openRouterModelId, setOpenRouterModelId] = useState(DEFAULT_MODEL_ID);
50
+ const [copilotModelId, setCopilotModelId] = useState(DEFAULT_COPILOT_MODEL_ID);
51
+
37
52
  const [currentOpenRouterProviderTag, setCurrentOpenRouterProviderTag] = useState<string | undefined>(
38
53
  undefined
39
54
  );
40
- const [modelsWithPricing, setModelsWithPricing] = useState<ModelOption[]>(AVAILABLE_MODELS);
55
+
56
+ const [openRouterModelsWithPricing, setOpenRouterModelsWithPricing] =
57
+ useState<ModelOption[]>(AVAILABLE_MODELS);
41
58
  const [openRouterModels, setOpenRouterModels] = useState<ModelOption[]>([]);
42
59
  const [openRouterModelsLoading, setOpenRouterModelsLoading] = useState(false);
43
60
  const [openRouterModelsUpdatedAt, setOpenRouterModelsUpdatedAt] = useState<number | null>(null);
44
61
  const [openRouterProviders, setOpenRouterProviders] = useState<OpenRouterInferenceProvider[]>([]);
45
62
 
63
+ const [copilotModels, setCopilotModels] = useState<ModelOption[]>([]);
64
+ const [copilotModelsLoading, setCopilotModelsLoading] = useState(false);
65
+ const [copilotModelsUpdatedAt, setCopilotModelsUpdatedAt] = useState<number | null>(null);
66
+
67
+ const currentModelId = currentModelProvider === "openrouter" ? openRouterModelId : copilotModelId;
68
+
46
69
  useAppModelPricingLoader({
47
70
  preferencesLoaded,
48
- setModelsWithPricing,
71
+ setModelsWithPricing: setOpenRouterModelsWithPricing,
49
72
  });
50
73
 
51
74
  useAppOpenRouterProviderLoader({
52
75
  preferencesLoaded,
53
- showProviderMenu,
54
- modelId: currentModelId,
76
+ showProviderMenu: showProviderMenu && currentModelProvider === "openrouter",
77
+ modelId: openRouterModelId,
55
78
  setProviders: setOpenRouterProviders,
56
79
  });
57
80
 
58
- const { refresh: refreshOpenRouterModels } = useAppOpenRouterModelsLoader({
81
+ const { refresh: refreshOpenRouterModelsRaw } = useAppOpenRouterModelsLoader({
59
82
  preferencesLoaded,
60
83
  setModels: setOpenRouterModels,
61
84
  setLoading: setOpenRouterModelsLoading,
62
85
  setUpdatedAt: setOpenRouterModelsUpdatedAt,
63
86
  });
64
87
 
88
+ const { refresh: refreshCopilotModels } = useAppCopilotModelsLoader({
89
+ preferencesLoaded,
90
+ setModels: setCopilotModels,
91
+ setLoading: setCopilotModelsLoading,
92
+ setUpdatedAt: setCopilotModelsUpdatedAt,
93
+ });
94
+
65
95
  const providerMenuItems: ProviderMenuItem[] = useMemo(() => {
66
96
  const pricingCandidates = openRouterProviders
67
97
  .map((p) => p.pricing)
@@ -108,16 +138,72 @@ export function useAppModel(params: UseAppModelParams): UseAppModelReturn {
108
138
  return items;
109
139
  }, [openRouterProviders, currentOpenRouterProviderTag]);
110
140
 
141
+ const modelsWithPricing =
142
+ currentModelProvider === "openrouter" ? openRouterModelsWithPricing : copilotModels;
143
+ const modelsForMenu = currentModelProvider === "openrouter" ? openRouterModels : copilotModels;
144
+ const modelsLoading =
145
+ currentModelProvider === "openrouter" ? openRouterModelsLoading : copilotModelsLoading;
146
+ const modelsUpdatedAt =
147
+ currentModelProvider === "openrouter" ? openRouterModelsUpdatedAt : copilotModelsUpdatedAt;
148
+ const currentModelSupportsReasoning = useMemo(() => {
149
+ if (currentModelProvider !== "copilot") {
150
+ return false;
151
+ }
152
+ const selected = copilotModels.find((model) => model.id === copilotModelId);
153
+ return selected?.supportsReasoningEffort === true;
154
+ }, [copilotModelId, copilotModels, currentModelProvider]);
155
+ const currentModelSupportsReasoningXHigh = useMemo(() => {
156
+ if (currentModelProvider !== "copilot") {
157
+ return false;
158
+ }
159
+ const selected = copilotModels.find((model) => model.id === copilotModelId);
160
+ return selected?.supportsReasoningEffortXHigh === true;
161
+ }, [copilotModelId, copilotModels, currentModelProvider]);
162
+
163
+ const setCurrentModelId = useCallback(
164
+ (modelId: string) => {
165
+ if (!modelId) return;
166
+ if (currentModelProvider === "openrouter") {
167
+ setOpenRouterModelId(modelId);
168
+ return;
169
+ }
170
+ setCopilotModelId(modelId);
171
+ },
172
+ [currentModelProvider]
173
+ );
174
+
175
+ const setCurrentModelForProvider = useCallback((provider: LlmProvider, modelId: string) => {
176
+ if (!modelId) return;
177
+ if (provider === "openrouter") {
178
+ setOpenRouterModelId(modelId);
179
+ return;
180
+ }
181
+ setCopilotModelId(modelId);
182
+ }, []);
183
+
184
+ const refreshOpenRouterModels = useCallback(async () => {
185
+ if (currentModelProvider === "openrouter") {
186
+ await refreshOpenRouterModelsRaw();
187
+ return;
188
+ }
189
+ await refreshCopilotModels();
190
+ }, [currentModelProvider, refreshOpenRouterModelsRaw, refreshCopilotModels]);
191
+
111
192
  return {
193
+ currentModelProvider,
194
+ setCurrentModelProvider,
112
195
  currentModelId,
196
+ currentModelSupportsReasoning,
197
+ currentModelSupportsReasoningXHigh,
113
198
  setCurrentModelId,
199
+ setCurrentModelForProvider,
114
200
  currentOpenRouterProviderTag,
115
201
  setCurrentOpenRouterProviderTag,
116
202
  modelsWithPricing,
117
- openRouterModels,
118
- openRouterModelsLoading,
119
- openRouterModelsUpdatedAt,
120
- providerMenuItems,
203
+ openRouterModels: modelsForMenu,
204
+ openRouterModelsLoading: modelsLoading,
205
+ openRouterModelsUpdatedAt: modelsUpdatedAt,
206
+ providerMenuItems: currentModelProvider === "openrouter" ? providerMenuItems : [],
121
207
  refreshOpenRouterModels,
122
208
  };
123
209
  }
@@ -1,9 +1,19 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
2
  import { startMcpManager } from "../ai/mcp/mcp-manager";
3
- import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
3
+ import { hasCopilotCliAuthSafe } from "../ai/copilot-client";
4
+ import {
5
+ getModelProvider,
6
+ getResponseModelForProvider,
7
+ setModelProvider,
8
+ setOpenRouterProviderTag,
9
+ setResponseModelForProvider,
10
+ } from "../ai/model-config";
11
+ import { invalidateDaemonToolsCache } from "../ai/tools";
12
+ import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
4
13
  import type {
5
14
  AppPreferences,
6
15
  BashApprovalLevel,
16
+ LlmProvider,
7
17
  OnboardingStep,
8
18
  ReasoningEffort,
9
19
  SpeechSpeed,
@@ -26,7 +36,8 @@ export interface UseAppPreferencesBootstrapParams {
26
36
  audioDeviceName?: string;
27
37
  outputDeviceName?: string;
28
38
  };
29
- setCurrentModelId: (modelId: string) => void;
39
+ setCurrentModelProvider: (provider: LlmProvider) => void;
40
+ setCurrentModelForProvider: (provider: LlmProvider, modelId: string) => void;
30
41
  setCurrentOpenRouterProviderTag: (providerTag: string | undefined) => void;
31
42
  setCurrentDevice: (deviceName: string | undefined) => void;
32
43
  setCurrentOutputDevice: (deviceName: string | undefined) => void;
@@ -41,6 +52,7 @@ export interface UseAppPreferencesBootstrapParams {
41
52
  setLoadedPreferences: (prefs: AppPreferences | null) => void;
42
53
  setOnboardingActive: (active: boolean) => void;
43
54
  setOnboardingStep: (step: OnboardingStep) => void;
55
+ setCopilotAuthenticated: (authenticated: boolean) => void;
44
56
  setPreferencesLoaded: (loaded: boolean) => void;
45
57
  }
46
58
 
@@ -53,7 +65,8 @@ export function useAppPreferencesBootstrap(
53
65
  ): UseAppPreferencesBootstrapReturn {
54
66
  const {
55
67
  manager,
56
- setCurrentModelId,
68
+ setCurrentModelProvider,
69
+ setCurrentModelForProvider,
57
70
  setCurrentOpenRouterProviderTag,
58
71
  setCurrentDevice,
59
72
  setCurrentOutputDevice,
@@ -68,6 +81,7 @@ export function useAppPreferencesBootstrap(
68
81
  setLoadedPreferences,
69
82
  setOnboardingActive,
70
83
  setOnboardingStep,
84
+ setCopilotAuthenticated,
71
85
  setPreferencesLoaded,
72
86
  } = params;
73
87
 
@@ -82,6 +96,7 @@ export function useAppPreferencesBootstrap(
82
96
 
83
97
  useEffect(() => {
84
98
  let cancelled = false;
99
+ let copilotAuthCheckTimer: ReturnType<typeof setTimeout> | null = null;
85
100
 
86
101
  (async () => {
87
102
  const prefs = await loadPreferences();
@@ -100,9 +115,18 @@ export function useAppPreferencesBootstrap(
100
115
  // Start MCP discovery in the background (non-blocking)
101
116
  startMcpManager();
102
117
 
118
+ const modelProvider: LlmProvider = prefs?.modelProvider ?? "openrouter";
119
+ setModelProvider(modelProvider);
120
+ invalidateDaemonToolsCache();
121
+ invalidateSubagentToolsCache();
122
+ setCurrentModelProvider(modelProvider);
123
+
103
124
  if (prefs?.modelId) {
104
- setResponseModel(prefs.modelId);
105
- setCurrentModelId(prefs.modelId);
125
+ setResponseModelForProvider(modelProvider, prefs.modelId);
126
+ setCurrentModelForProvider(modelProvider, prefs.modelId);
127
+ } else {
128
+ const fallbackModelId = getResponseModelForProvider(modelProvider);
129
+ setCurrentModelForProvider(modelProvider, fallbackModelId);
106
130
  }
107
131
 
108
132
  if (prefs?.openRouterProviderTag) {
@@ -169,20 +193,35 @@ export function useAppPreferencesBootstrap(
169
193
  const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY);
170
194
  const hasOpenAiKey = Boolean(process.env.OPENAI_API_KEY);
171
195
  const hasExaKey = Boolean(process.env.EXA_API_KEY);
196
+ const usingCopilotProvider = modelProvider === "copilot";
197
+ const hasCopilotAuth = usingCopilotProvider;
198
+ setCopilotAuthenticated(hasCopilotAuth);
199
+ copilotAuthCheckTimer = setTimeout(() => {
200
+ void (async () => {
201
+ const authenticated = await hasCopilotCliAuthSafe();
202
+ if (cancelled) return;
203
+ setCopilotAuthenticated(authenticated);
204
+ if (!authenticated && getModelProvider() === "copilot") {
205
+ setOnboardingStep("copilot_auth");
206
+ setOnboardingActive(true);
207
+ }
208
+ })();
209
+ }, 0);
172
210
  const hasCoreSettings = Boolean(prefs?.audioDeviceName && prefs?.modelId);
173
211
 
174
212
  setLoadedPreferences(prefs);
175
213
 
176
214
  const isFreshLaunch = prefs === null;
177
- const needsOnboarding = !hasOpenRouterKey || !hasOpenAiKey || !hasExaKey;
215
+ const hasProviderAuth = modelProvider === "openrouter" ? hasOpenRouterKey : hasCopilotAuth;
216
+ const needsOnboarding = !hasProviderAuth || !hasOpenAiKey || !hasExaKey;
178
217
 
179
218
  if (isFreshLaunch) {
180
219
  setOnboardingStep("intro");
181
220
  setOnboardingActive(true);
182
221
  } else if (needsOnboarding) {
183
- let startStep: OnboardingStep = "intro";
184
- if (!hasOpenRouterKey) {
185
- startStep = "openrouter_key";
222
+ let startStep: OnboardingStep = "provider";
223
+ if (!hasProviderAuth) {
224
+ startStep = modelProvider === "openrouter" ? "openrouter_key" : "copilot_auth";
186
225
  } else if (!hasOpenAiKey) {
187
226
  startStep = "openai_key";
188
227
  } else if (!hasExaKey) {
@@ -203,10 +242,14 @@ export function useAppPreferencesBootstrap(
203
242
 
204
243
  return () => {
205
244
  cancelled = true;
245
+ if (copilotAuthCheckTimer) {
246
+ clearTimeout(copilotAuthCheckTimer);
247
+ }
206
248
  };
207
249
  }, [
208
250
  manager,
209
- setCurrentModelId,
251
+ setCurrentModelProvider,
252
+ setCurrentModelForProvider,
210
253
  setCurrentOpenRouterProviderTag,
211
254
  setCurrentDevice,
212
255
  setCurrentOutputDevice,
@@ -220,6 +263,7 @@ export function useAppPreferencesBootstrap(
220
263
  setLoadedPreferences,
221
264
  setOnboardingActive,
222
265
  setOnboardingStep,
266
+ setCopilotAuthenticated,
223
267
  setPreferencesLoaded,
224
268
  ]);
225
269
 
@@ -15,6 +15,8 @@ export interface BootstrapControllerResult {
15
15
 
16
16
  onboardingStep: OnboardingStep;
17
17
  setOnboardingStep: (step: OnboardingStep) => void;
18
+ copilotAuthenticated: boolean;
19
+ setCopilotAuthenticated: (authenticated: boolean) => void;
18
20
 
19
21
  loadedPreferences: AppPreferences | null;
20
22
  setLoadedPreferences: (prefs: AppPreferences | null) => void;
@@ -50,6 +52,7 @@ export function useBootstrapController({
50
52
 
51
53
  const [loadedPreferences, setLoadedPreferences] = useState<AppPreferences | null>(null);
52
54
  const [onboardingStep, setOnboardingStep] = useState<OnboardingStep>("intro");
55
+ const [copilotAuthenticated, setCopilotAuthenticated] = useState(false);
53
56
 
54
57
  const [devices, setDevices] = useState<AudioDevice[]>([]);
55
58
  const [currentDevice, setCurrentDevice] = useState<string | undefined>(undefined);
@@ -75,6 +78,8 @@ export function useBootstrapController({
75
78
  setOnboardingActive,
76
79
  onboardingStep,
77
80
  setOnboardingStep,
81
+ copilotAuthenticated,
82
+ setCopilotAuthenticated,
78
83
  loadedPreferences,
79
84
  setLoadedPreferences,
80
85
  devices,
@@ -9,7 +9,7 @@ import { daemonEvents } from "../state/daemon-events";
9
9
  import { getDaemonManager } from "../state/daemon-state";
10
10
  import { buildModelHistoryFromConversation } from "../state/session-store";
11
11
  import { DaemonState } from "../types";
12
- import type { ContentBlock, ConversationMessage, TokenUsage, ToolCall } from "../types";
12
+ import type { ContentBlock, ConversationMessage, LlmProvider, TokenUsage, ToolCall } from "../types";
13
13
  import { REASONING_COLORS, STATE_COLORS } from "../types/theme";
14
14
  import { REASONING_ANIMATION } from "../ui/constants";
15
15
  import { type ModelMetadata, getModelMetadata } from "../utils/model-metadata";
@@ -41,6 +41,7 @@ import {
41
41
  } from "./daemon-event-handlers";
42
42
 
43
43
  export interface UseDaemonEventsParams {
44
+ currentModelProvider: LlmProvider;
44
45
  currentModelId: string;
45
46
  preferencesLoaded: boolean;
46
47
  setReasoningQueue: (queue: string | ((prev: string) => string)) => void;
@@ -79,6 +80,7 @@ export interface UseDaemonEventsReturn {
79
80
 
80
81
  export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsReturn {
81
82
  const {
83
+ currentModelProvider,
82
84
  currentModelId,
83
85
  preferencesLoaded,
84
86
  setReasoningQueue,
@@ -117,6 +119,10 @@ export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsR
117
119
 
118
120
  useEffect(() => {
119
121
  if (!preferencesLoaded) return;
122
+ if (currentModelProvider !== "openrouter") {
123
+ setModelMetadata(null);
124
+ return;
125
+ }
120
126
  let cancelled = false;
121
127
  getModelMetadata(currentModelId).then((metadata) => {
122
128
  if (!cancelled) setModelMetadata(metadata);
@@ -124,7 +130,7 @@ export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsR
124
130
  return () => {
125
131
  cancelled = true;
126
132
  };
127
- }, [currentModelId, preferencesLoaded]);
133
+ }, [currentModelId, currentModelProvider, preferencesLoaded]);
128
134
 
129
135
  useEffect(() => {
130
136
  clearFetchCache();