@makefinks/daemon 0.10.0 → 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 +53 -12
- 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/tools/subagents.ts +1 -1
- package/src/ai/tools/tool-registry.ts +16 -1
- 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/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 +22 -1
- package/src/utils/copilot-models.ts +77 -0
- package/src/utils/preferences.ts +3 -0
|
@@ -4,7 +4,7 @@ import type { ScrollBoxRenderable } from "@opentui/core";
|
|
|
4
4
|
import { useKeyboard } from "@opentui/react";
|
|
5
5
|
import { useCallback } from "react";
|
|
6
6
|
import { getDaemonManager } from "../state/daemon-state";
|
|
7
|
-
import { type AppPreferences, DaemonState } from "../types";
|
|
7
|
+
import { type AppPreferences, type LlmProvider, DaemonState } from "../types";
|
|
8
8
|
import { COLORS } from "../ui/constants";
|
|
9
9
|
export interface KeyboardHandlerState {
|
|
10
10
|
isOverlayOpen: boolean;
|
|
@@ -13,6 +13,7 @@ export interface KeyboardHandlerState {
|
|
|
13
13
|
hasGrounding: boolean;
|
|
14
14
|
showFullReasoning: boolean;
|
|
15
15
|
showToolOutput: boolean;
|
|
16
|
+
currentModelProvider: LlmProvider;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface KeyboardHandlerActions {
|
|
@@ -43,7 +44,14 @@ export interface KeyboardHandlerActions {
|
|
|
43
44
|
|
|
44
45
|
export function useDaemonKeyboard(state: KeyboardHandlerState, actions: KeyboardHandlerActions) {
|
|
45
46
|
const manager = getDaemonManager();
|
|
46
|
-
const {
|
|
47
|
+
const {
|
|
48
|
+
isOverlayOpen,
|
|
49
|
+
escPendingCancel,
|
|
50
|
+
hasInteracted,
|
|
51
|
+
hasGrounding,
|
|
52
|
+
showFullReasoning,
|
|
53
|
+
currentModelProvider,
|
|
54
|
+
} = state;
|
|
47
55
|
|
|
48
56
|
const closeAllMenus = useCallback(() => {
|
|
49
57
|
actions.setShowDeviceMenu(false);
|
|
@@ -161,6 +169,10 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
161
169
|
key.eventType === "press" &&
|
|
162
170
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING)
|
|
163
171
|
) {
|
|
172
|
+
if (currentModelProvider !== "openrouter") {
|
|
173
|
+
key.preventDefault();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
164
176
|
closeAllMenus();
|
|
165
177
|
actions.setShowProviderMenu(true);
|
|
166
178
|
key.preventDefault();
|
|
@@ -310,8 +322,8 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
310
322
|
currentState !== DaemonState.TYPING
|
|
311
323
|
) {
|
|
312
324
|
if (currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) {
|
|
313
|
-
//
|
|
314
|
-
if (!process.env.OPENROUTER_API_KEY) {
|
|
325
|
+
// OpenRouter provider requires OPENROUTER_API_KEY for model responses.
|
|
326
|
+
if (currentModelProvider === "openrouter" && !process.env.OPENROUTER_API_KEY) {
|
|
315
327
|
actions.setApiKeyMissingError(
|
|
316
328
|
"OPENROUTER_API_KEY not found · Set via environment variable or enter in onboarding"
|
|
317
329
|
);
|
|
@@ -340,8 +352,8 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
340
352
|
// Shift+Tab for typing mode
|
|
341
353
|
if (key.name === "tab" && key.shift && key.eventType === "press") {
|
|
342
354
|
if (currentState === DaemonState.IDLE) {
|
|
343
|
-
//
|
|
344
|
-
if (!process.env.OPENROUTER_API_KEY) {
|
|
355
|
+
// OpenRouter provider requires OPENROUTER_API_KEY for model responses.
|
|
356
|
+
if (currentModelProvider === "openrouter" && !process.env.OPENROUTER_API_KEY) {
|
|
345
357
|
actions.setApiKeyMissingError(
|
|
346
358
|
"OPENROUTER_API_KEY not found · Set via environment variable or enter in onboarding"
|
|
347
359
|
);
|
|
@@ -411,6 +423,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
411
423
|
hasGrounding,
|
|
412
424
|
showFullReasoning,
|
|
413
425
|
state.showToolOutput,
|
|
426
|
+
currentModelProvider,
|
|
414
427
|
actions,
|
|
415
428
|
]
|
|
416
429
|
);
|
|
@@ -9,6 +9,7 @@ import { useResponseTimer } from "./use-response-timer";
|
|
|
9
9
|
import { useTypingMode } from "./use-typing-mode";
|
|
10
10
|
|
|
11
11
|
import { daemonEvents } from "../state/daemon-events";
|
|
12
|
+
import type { LlmProvider } from "../types";
|
|
12
13
|
|
|
13
14
|
export interface DaemonRuntimeControllerResult {
|
|
14
15
|
reasoning: ReturnType<typeof useReasoningAnimation>;
|
|
@@ -41,6 +42,7 @@ export interface DaemonRuntimeControllerResult {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function useDaemonRuntimeController({
|
|
45
|
+
currentModelProvider,
|
|
44
46
|
currentModelId,
|
|
45
47
|
preferencesLoaded,
|
|
46
48
|
sessionId,
|
|
@@ -48,6 +50,7 @@ export function useDaemonRuntimeController({
|
|
|
48
50
|
ensureSessionId,
|
|
49
51
|
onFirstMessage,
|
|
50
52
|
}: {
|
|
53
|
+
currentModelProvider: LlmProvider;
|
|
51
54
|
currentModelId: string;
|
|
52
55
|
preferencesLoaded: boolean;
|
|
53
56
|
sessionId: string | null;
|
|
@@ -77,6 +80,7 @@ export function useDaemonRuntimeController({
|
|
|
77
80
|
setSessionUsage,
|
|
78
81
|
applyAvatarForState,
|
|
79
82
|
} = useDaemonEvents({
|
|
83
|
+
currentModelProvider,
|
|
80
84
|
currentModelId,
|
|
81
85
|
preferencesLoaded,
|
|
82
86
|
setReasoningQueue: reasoning.setReasoningQueue,
|
|
@@ -12,6 +12,7 @@ interface UseMenuKeyboardParams {
|
|
|
12
12
|
enableViKeys?: boolean;
|
|
13
13
|
closeOnSelect?: boolean;
|
|
14
14
|
ignoreEscape?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface UseMenuKeyboardReturn {
|
|
@@ -27,6 +28,7 @@ export function useMenuKeyboard({
|
|
|
27
28
|
enableViKeys = true,
|
|
28
29
|
closeOnSelect = true,
|
|
29
30
|
ignoreEscape = false,
|
|
31
|
+
disabled = false,
|
|
30
32
|
}: UseMenuKeyboardParams): UseMenuKeyboardReturn {
|
|
31
33
|
const [selectedIndex, setSelectedIndex] = useState(initialIndex);
|
|
32
34
|
|
|
@@ -49,6 +51,9 @@ export function useMenuKeyboard({
|
|
|
49
51
|
const handleKeyPress = useCallback(
|
|
50
52
|
(key: KeyEvent) => {
|
|
51
53
|
if (key.eventType !== "press") return;
|
|
54
|
+
if (disabled) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
52
57
|
if (ignoreEscape && key.name === "escape") {
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
@@ -84,7 +89,7 @@ export function useMenuKeyboard({
|
|
|
84
89
|
|
|
85
90
|
key.preventDefault();
|
|
86
91
|
},
|
|
87
|
-
[itemCount, selectedIndex, onClose, onSelect, enableViKeys, closeOnSelect]
|
|
92
|
+
[itemCount, selectedIndex, onClose, onSelect, enableViKeys, closeOnSelect, ignoreEscape, disabled]
|
|
88
93
|
);
|
|
89
94
|
|
|
90
95
|
useKeyboard(handleKeyPress);
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AudioDevice,
|
|
7
7
|
BashApprovalLevel,
|
|
8
8
|
GroundingMap,
|
|
9
|
+
LlmProvider,
|
|
9
10
|
ModelOption,
|
|
10
11
|
OnboardingStep,
|
|
11
12
|
ReasoningEffort,
|
|
@@ -55,6 +56,7 @@ export interface SettingsState {
|
|
|
55
56
|
reasoningEffort: ReasoningEffort;
|
|
56
57
|
bashApprovalLevel: BashApprovalLevel;
|
|
57
58
|
supportsReasoning: boolean;
|
|
59
|
+
supportsReasoningXHigh: boolean;
|
|
58
60
|
canEnableVoiceOutput: boolean;
|
|
59
61
|
showFullReasoning: boolean;
|
|
60
62
|
setShowFullReasoning: (show: boolean) => void;
|
|
@@ -71,6 +73,8 @@ export interface ModelState {
|
|
|
71
73
|
openRouterModels: ModelOption[];
|
|
72
74
|
openRouterModelsLoading: boolean;
|
|
73
75
|
openRouterModelsUpdatedAt: number | null;
|
|
76
|
+
currentModelProvider: LlmProvider;
|
|
77
|
+
setCurrentModelProvider: (provider: LlmProvider) => void;
|
|
74
78
|
currentModelId: string;
|
|
75
79
|
setCurrentModelId: (modelId: string) => void;
|
|
76
80
|
providerMenuItems: ProviderMenuItem[];
|
|
@@ -92,6 +96,7 @@ export interface GroundingState {
|
|
|
92
96
|
export interface OnboardingState {
|
|
93
97
|
onboardingActive: boolean;
|
|
94
98
|
onboardingStep: OnboardingStep;
|
|
99
|
+
copilotAuthenticated: boolean;
|
|
95
100
|
setOnboardingStep: (step: OnboardingStep) => void;
|
|
96
101
|
onboardingPreferences: AppPreferences | null;
|
|
97
102
|
apiKeyTextareaRef: MutableRefObject<TextareaRenderable | null>;
|
|
@@ -104,6 +109,7 @@ export interface DeviceCallbacks {
|
|
|
104
109
|
|
|
105
110
|
export interface SettingsCallbacks {
|
|
106
111
|
onToggleInteractionMode: () => void;
|
|
112
|
+
onCycleModelProvider: () => void;
|
|
107
113
|
onSetVoiceInteractionType: (type: VoiceInteractionType) => void;
|
|
108
114
|
onSetSpeechSpeed: (speed: SpeechSpeed) => void;
|
|
109
115
|
onSetReasoningEffort: (effort: ReasoningEffort) => void;
|
package/src/types/index.ts
CHANGED
|
@@ -150,6 +150,11 @@ export interface TranscriptionResult {
|
|
|
150
150
|
text: string;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* LLM backend provider used for agent responses.
|
|
155
|
+
*/
|
|
156
|
+
export type LlmProvider = "openrouter" | "copilot";
|
|
157
|
+
|
|
153
158
|
/**
|
|
154
159
|
* Callbacks for streaming AI responses
|
|
155
160
|
*/
|
|
@@ -213,18 +218,25 @@ export type SpeechSpeed = 1.0 | 1.25 | 1.5 | 1.75 | 2.0;
|
|
|
213
218
|
* - "low": SURFACE - minimal reasoning
|
|
214
219
|
* - "medium": DEEP - moderate reasoning (default)
|
|
215
220
|
* - "high": ABYSSAL - maximum reasoning depth
|
|
221
|
+
* - "xhigh": EXTREME - extended deep reasoning (model-dependent)
|
|
216
222
|
*/
|
|
217
|
-
export type ReasoningEffort = "low" | "medium" | "high";
|
|
223
|
+
export type ReasoningEffort = "low" | "medium" | "high" | "xhigh";
|
|
218
224
|
|
|
219
225
|
/** Display labels for reasoning effort levels */
|
|
220
226
|
export const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string> = {
|
|
221
227
|
low: "LOW",
|
|
222
228
|
medium: "MEDIUM",
|
|
223
229
|
high: "HIGH",
|
|
230
|
+
xhigh: "XHIGH",
|
|
224
231
|
};
|
|
225
232
|
|
|
226
233
|
/** Ordered list of reasoning effort levels for cycling */
|
|
227
234
|
export const REASONING_EFFORT_LEVELS: ReasoningEffort[] = ["low", "medium", "high"];
|
|
235
|
+
export const REASONING_EFFORT_LEVELS_WITH_XHIGH: ReasoningEffort[] = ["low", "medium", "high", "xhigh"];
|
|
236
|
+
|
|
237
|
+
export function getReasoningEffortLevels(includeXHigh: boolean): ReasoningEffort[] {
|
|
238
|
+
return includeXHigh ? REASONING_EFFORT_LEVELS_WITH_XHIGH : REASONING_EFFORT_LEVELS;
|
|
239
|
+
}
|
|
228
240
|
|
|
229
241
|
/**
|
|
230
242
|
* Bash tool approval level settings.
|
|
@@ -248,7 +260,9 @@ export const BASH_APPROVAL_LEVELS: BashApprovalLevel[] = ["none", "dangerous", "
|
|
|
248
260
|
/**
|
|
249
261
|
* Onboarding flow steps.
|
|
250
262
|
* - intro: Welcome screen
|
|
263
|
+
* - provider: Select backend provider
|
|
251
264
|
* - openrouter_key: OpenRouter API key input (for AI models)
|
|
265
|
+
* - copilot_auth: GitHub Copilot authentication
|
|
252
266
|
* - openai_key: OpenAI API key input (for transcription)
|
|
253
267
|
* - exa_key: Exa API key input (for web search)
|
|
254
268
|
* - device: Audio device selection
|
|
@@ -257,7 +271,9 @@ export const BASH_APPROVAL_LEVELS: BashApprovalLevel[] = ["none", "dangerous", "
|
|
|
257
271
|
*/
|
|
258
272
|
export type OnboardingStep =
|
|
259
273
|
| "intro"
|
|
274
|
+
| "provider"
|
|
260
275
|
| "openrouter_key"
|
|
276
|
+
| "copilot_auth"
|
|
261
277
|
| "openai_key"
|
|
262
278
|
| "exa_key"
|
|
263
279
|
| "device"
|
|
@@ -302,6 +318,7 @@ export interface AppPreferences {
|
|
|
302
318
|
onboardingCompleted: boolean;
|
|
303
319
|
audioDeviceName?: string;
|
|
304
320
|
audioOutputDeviceName?: string;
|
|
321
|
+
modelProvider?: LlmProvider;
|
|
305
322
|
modelId?: string;
|
|
306
323
|
/**
|
|
307
324
|
* OpenRouter inference provider slug (aka `provider` routing tag), e.g. "openai".
|
|
@@ -359,6 +376,10 @@ export interface ModelOption {
|
|
|
359
376
|
pricing?: ModelPricing;
|
|
360
377
|
contextLength?: number;
|
|
361
378
|
supportsCaching?: boolean;
|
|
379
|
+
/** Per-model support for reasoning effort controls (currently used by Copilot). */
|
|
380
|
+
supportsReasoningEffort?: boolean;
|
|
381
|
+
/** Whether this model supports Copilot's `xhigh` reasoning effort tier. */
|
|
382
|
+
supportsReasoningEffortXHigh?: boolean;
|
|
362
383
|
}
|
|
363
384
|
|
|
364
385
|
/**
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ModelOption } from "../types";
|
|
2
|
+
import { listCopilotModelsSafe } from "../ai/copilot-client";
|
|
3
|
+
import { debug } from "./debug-logger";
|
|
4
|
+
|
|
5
|
+
let inMemoryCache: { timestamp: number; models: ModelOption[] } | null = null;
|
|
6
|
+
function supportsXHighReasoning(modelId: string): boolean {
|
|
7
|
+
const normalized = modelId.trim().toLowerCase();
|
|
8
|
+
return normalized.includes("5.1") || normalized.includes("5.2") || normalized.includes("codex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeCopilotModels(
|
|
12
|
+
items: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
capabilities?: {
|
|
16
|
+
supports?: {
|
|
17
|
+
reasoningEffort?: boolean;
|
|
18
|
+
};
|
|
19
|
+
limits?: {
|
|
20
|
+
max_context_window_tokens?: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}>
|
|
24
|
+
): ModelOption[] {
|
|
25
|
+
return items
|
|
26
|
+
.filter((item) => typeof item.id === "string" && item.id.trim().length > 0)
|
|
27
|
+
.map((item) => ({
|
|
28
|
+
id: item.id.trim(),
|
|
29
|
+
name: typeof item.name === "string" && item.name.trim().length > 0 ? item.name.trim() : item.id.trim(),
|
|
30
|
+
contextLength: item.capabilities?.limits?.max_context_window_tokens,
|
|
31
|
+
supportsReasoningEffort: item.capabilities?.supports?.reasoningEffort === true,
|
|
32
|
+
supportsReasoningEffortXHigh: supportsXHighReasoning(item.id.trim()),
|
|
33
|
+
}))
|
|
34
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getCopilotModels(options: { forceRefresh?: boolean } = {}): Promise<{
|
|
38
|
+
models: ModelOption[];
|
|
39
|
+
timestamp: number | null;
|
|
40
|
+
fromCache: boolean;
|
|
41
|
+
}> {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const forceRefresh = options.forceRefresh === true;
|
|
44
|
+
|
|
45
|
+
if (!forceRefresh && inMemoryCache) {
|
|
46
|
+
return {
|
|
47
|
+
models: inMemoryCache.models,
|
|
48
|
+
timestamp: inMemoryCache.timestamp,
|
|
49
|
+
fromCache: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const modelInfo = await listCopilotModelsSafe();
|
|
55
|
+
const models = normalizeCopilotModels(modelInfo);
|
|
56
|
+
if (models.length > 0) {
|
|
57
|
+
inMemoryCache = {
|
|
58
|
+
timestamp: now,
|
|
59
|
+
models,
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
models,
|
|
63
|
+
timestamp: now,
|
|
64
|
+
fromCache: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
69
|
+
debug.error("Failed to fetch Copilot models:", err);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
models: inMemoryCache?.models ?? [],
|
|
74
|
+
timestamp: inMemoryCache?.timestamp ?? null,
|
|
75
|
+
fromCache: Boolean(inMemoryCache),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/utils/preferences.ts
CHANGED
|
@@ -74,6 +74,9 @@ export function parsePreferences(raw: unknown): AppPreferences | null {
|
|
|
74
74
|
if (typeof raw.modelId === "string") {
|
|
75
75
|
prefs.modelId = raw.modelId;
|
|
76
76
|
}
|
|
77
|
+
if (raw.modelProvider === "openrouter" || raw.modelProvider === "copilot") {
|
|
78
|
+
prefs.modelProvider = raw.modelProvider;
|
|
79
|
+
}
|
|
77
80
|
if (typeof raw.openRouterProviderTag === "string") {
|
|
78
81
|
prefs.openRouterProviderTag = raw.openRouterProviderTag;
|
|
79
82
|
}
|