@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.
- package/README.md +60 -14
- package/package.json +4 -2
- package/src/ai/copilot-client.ts +775 -0
- package/src/ai/daemon-ai.ts +32 -234
- package/src/ai/model-config.ts +55 -14
- package/src/ai/providers/capabilities.ts +16 -0
- package/src/ai/providers/copilot-provider.ts +632 -0
- package/src/ai/providers/openrouter-provider.ts +217 -0
- package/src/ai/providers/registry.ts +14 -0
- package/src/ai/providers/types.ts +31 -0
- package/src/ai/system-prompt.ts +16 -0
- package/src/ai/tools/subagents.ts +1 -1
- package/src/ai/tools/tool-registry.ts +22 -1
- package/src/ai/tools/write-file.ts +51 -0
- package/src/app/components/AppOverlays.tsx +9 -1
- package/src/app/components/ConversationPane.tsx +8 -2
- package/src/components/ModelMenu.tsx +202 -140
- package/src/components/OnboardingOverlay.tsx +147 -1
- package/src/components/SettingsMenu.tsx +27 -1
- package/src/components/TokenUsageDisplay.tsx +5 -3
- package/src/components/tool-layouts/layouts/index.ts +1 -0
- package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
- package/src/hooks/daemon-event-handlers.ts +61 -14
- package/src/hooks/keyboard-handlers.ts +109 -28
- package/src/hooks/use-app-callbacks.ts +141 -43
- package/src/hooks/use-app-context-builder.ts +5 -0
- package/src/hooks/use-app-controller.ts +31 -2
- package/src/hooks/use-app-copilot-models-loader.ts +45 -0
- package/src/hooks/use-app-display-state.ts +24 -2
- package/src/hooks/use-app-model.ts +103 -17
- package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
- package/src/hooks/use-bootstrap-controller.ts +5 -0
- package/src/hooks/use-daemon-events.ts +8 -2
- package/src/hooks/use-daemon-keyboard.ts +19 -6
- package/src/hooks/use-daemon-runtime-controller.ts +4 -0
- package/src/hooks/use-menu-keyboard.ts +6 -1
- package/src/state/app-context.tsx +6 -0
- package/src/types/index.ts +24 -1
- package/src/utils/copilot-models.ts +77 -0
- 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,
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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 =
|
|
334
|
-
const nextIndex = (currentIndex + 1) %
|
|
335
|
-
const nextEffort =
|
|
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 {
|
|
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
|
-
|
|
96
|
-
setOpenRouterProviderTag(undefined);
|
|
117
|
+
setResponseModelForProvider(currentModelProvider, model.id);
|
|
97
118
|
setCurrentModelId(model.id);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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>;
|