@makefinks/daemon 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/cli.js +22 -0
  4. package/package.json +79 -0
  5. package/src/ai/agent-turn-runner.ts +130 -0
  6. package/src/ai/daemon-ai.ts +403 -0
  7. package/src/ai/exa-client.ts +21 -0
  8. package/src/ai/exa-fetch-cache.ts +104 -0
  9. package/src/ai/model-config.ts +99 -0
  10. package/src/ai/sanitize-messages.ts +83 -0
  11. package/src/ai/system-prompt.ts +363 -0
  12. package/src/ai/tools/fetch-urls.ts +187 -0
  13. package/src/ai/tools/grounding-manager.ts +94 -0
  14. package/src/ai/tools/index.ts +52 -0
  15. package/src/ai/tools/read-file.ts +100 -0
  16. package/src/ai/tools/render-url.ts +275 -0
  17. package/src/ai/tools/run-bash.ts +224 -0
  18. package/src/ai/tools/subagents.ts +195 -0
  19. package/src/ai/tools/todo-manager.ts +150 -0
  20. package/src/ai/tools/web-search.ts +91 -0
  21. package/src/app/App.tsx +711 -0
  22. package/src/app/components/AppOverlays.tsx +131 -0
  23. package/src/app/components/AvatarLayer.tsx +51 -0
  24. package/src/app/components/ConversationPane.tsx +476 -0
  25. package/src/avatar/DaemonAvatarRenderable.ts +343 -0
  26. package/src/avatar/daemon-avatar-rig.ts +1165 -0
  27. package/src/avatar-preview.ts +186 -0
  28. package/src/cli.ts +26 -0
  29. package/src/components/ApiKeyInput.tsx +99 -0
  30. package/src/components/ApiKeyStep.tsx +95 -0
  31. package/src/components/ApprovalPicker.tsx +109 -0
  32. package/src/components/ContentBlockView.tsx +141 -0
  33. package/src/components/DaemonText.tsx +34 -0
  34. package/src/components/DeviceMenu.tsx +166 -0
  35. package/src/components/GroundingBadge.tsx +21 -0
  36. package/src/components/GroundingMenu.tsx +310 -0
  37. package/src/components/HotkeysPane.tsx +115 -0
  38. package/src/components/InlineStatusIndicator.tsx +106 -0
  39. package/src/components/ModelMenu.tsx +411 -0
  40. package/src/components/OnboardingOverlay.tsx +446 -0
  41. package/src/components/ProviderMenu.tsx +177 -0
  42. package/src/components/SessionMenu.tsx +297 -0
  43. package/src/components/SettingsMenu.tsx +291 -0
  44. package/src/components/StatusBar.tsx +126 -0
  45. package/src/components/TokenUsageDisplay.tsx +92 -0
  46. package/src/components/ToolCallView.tsx +113 -0
  47. package/src/components/TypingInputBar.tsx +131 -0
  48. package/src/components/tool-layouts/components.tsx +120 -0
  49. package/src/components/tool-layouts/defaults.ts +9 -0
  50. package/src/components/tool-layouts/index.ts +22 -0
  51. package/src/components/tool-layouts/layouts/bash.ts +110 -0
  52. package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
  53. package/src/components/tool-layouts/layouts/index.ts +8 -0
  54. package/src/components/tool-layouts/layouts/read-file.ts +59 -0
  55. package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
  56. package/src/components/tool-layouts/layouts/system-info.ts +8 -0
  57. package/src/components/tool-layouts/layouts/todo.tsx +139 -0
  58. package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
  59. package/src/components/tool-layouts/layouts/web-search.ts +110 -0
  60. package/src/components/tool-layouts/registry.ts +17 -0
  61. package/src/components/tool-layouts/types.ts +94 -0
  62. package/src/hooks/daemon-event-handlers.ts +944 -0
  63. package/src/hooks/keyboard-handlers.ts +399 -0
  64. package/src/hooks/menu-navigation.ts +147 -0
  65. package/src/hooks/use-app-audio-devices-loader.ts +71 -0
  66. package/src/hooks/use-app-callbacks.ts +202 -0
  67. package/src/hooks/use-app-context-builder.ts +159 -0
  68. package/src/hooks/use-app-display-state.ts +162 -0
  69. package/src/hooks/use-app-menus.ts +51 -0
  70. package/src/hooks/use-app-model-pricing-loader.ts +45 -0
  71. package/src/hooks/use-app-model.ts +123 -0
  72. package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
  73. package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
  74. package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
  75. package/src/hooks/use-app-sessions.ts +105 -0
  76. package/src/hooks/use-app-settings.ts +62 -0
  77. package/src/hooks/use-conversation-manager.ts +163 -0
  78. package/src/hooks/use-copy-on-select.ts +50 -0
  79. package/src/hooks/use-daemon-events.ts +396 -0
  80. package/src/hooks/use-daemon-keyboard.ts +397 -0
  81. package/src/hooks/use-grounding.ts +46 -0
  82. package/src/hooks/use-input-history.ts +92 -0
  83. package/src/hooks/use-menu-keyboard.ts +93 -0
  84. package/src/hooks/use-playwright-notification.ts +23 -0
  85. package/src/hooks/use-reasoning-animation.ts +97 -0
  86. package/src/hooks/use-response-timer.ts +55 -0
  87. package/src/hooks/use-tool-approval.tsx +202 -0
  88. package/src/hooks/use-typing-mode.ts +137 -0
  89. package/src/hooks/use-voice-dependencies-notification.ts +37 -0
  90. package/src/index.tsx +48 -0
  91. package/src/scripts/setup-browsers.ts +42 -0
  92. package/src/state/app-context.tsx +160 -0
  93. package/src/state/daemon-events.ts +67 -0
  94. package/src/state/daemon-state.ts +493 -0
  95. package/src/state/migrations/001-init.ts +33 -0
  96. package/src/state/migrations/index.ts +8 -0
  97. package/src/state/model-history-store.ts +45 -0
  98. package/src/state/runtime-context.ts +21 -0
  99. package/src/state/session-store.ts +359 -0
  100. package/src/types/index.ts +405 -0
  101. package/src/types/theme.ts +52 -0
  102. package/src/ui/constants.ts +157 -0
  103. package/src/utils/clipboard.ts +89 -0
  104. package/src/utils/debug-logger.ts +69 -0
  105. package/src/utils/formatters.ts +242 -0
  106. package/src/utils/js-rendering.ts +77 -0
  107. package/src/utils/markdown-tables.ts +234 -0
  108. package/src/utils/model-metadata.ts +191 -0
  109. package/src/utils/openrouter-endpoints.ts +212 -0
  110. package/src/utils/openrouter-models.ts +205 -0
  111. package/src/utils/openrouter-pricing.ts +59 -0
  112. package/src/utils/openrouter-reported-cost.ts +16 -0
  113. package/src/utils/paste.ts +33 -0
  114. package/src/utils/preferences.ts +289 -0
  115. package/src/utils/text-fragment.ts +39 -0
  116. package/src/utils/tool-output-preview.ts +250 -0
  117. package/src/utils/voice-dependencies.ts +107 -0
  118. package/src/utils/workspace-manager.ts +85 -0
  119. package/src/voice/audio-recorder.ts +579 -0
  120. package/src/voice/mic-level.ts +35 -0
  121. package/src/voice/tts/openai-tts-stream.ts +222 -0
  122. package/src/voice/tts/speech-controller.ts +64 -0
  123. package/src/voice/tts/tts-player.ts +257 -0
  124. package/src/voice/voice-input-controller.ts +96 -0
@@ -0,0 +1,123 @@
1
+ import { useMemo, useState } from "react";
2
+ import { AVAILABLE_MODELS, DEFAULT_MODEL_ID } from "../ai/model-config";
3
+ import { mergePricingAverages } from "../utils/openrouter-pricing";
4
+ import { useAppModelPricingLoader } from "./use-app-model-pricing-loader";
5
+ import { useAppOpenRouterModelsLoader } from "./use-app-openrouter-models-loader";
6
+ 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
+
11
+ export interface UseAppModelParams {
12
+ preferencesLoaded: boolean;
13
+ showProviderMenu: boolean;
14
+ }
15
+
16
+ export interface UseAppModelReturn {
17
+ currentModelId: string;
18
+ setCurrentModelId: React.Dispatch<React.SetStateAction<string>>;
19
+
20
+ currentOpenRouterProviderTag: string | undefined;
21
+ setCurrentOpenRouterProviderTag: React.Dispatch<React.SetStateAction<string | undefined>>;
22
+
23
+ modelsWithPricing: ModelOption[];
24
+ openRouterModels: ModelOption[];
25
+ openRouterModelsLoading: boolean;
26
+ openRouterModelsUpdatedAt: number | null;
27
+
28
+ providerMenuItems: ProviderMenuItem[];
29
+
30
+ refreshOpenRouterModels: () => void;
31
+ }
32
+
33
+ export function useAppModel(params: UseAppModelParams): UseAppModelReturn {
34
+ const { preferencesLoaded, showProviderMenu } = params;
35
+
36
+ const [currentModelId, setCurrentModelId] = useState(DEFAULT_MODEL_ID);
37
+ const [currentOpenRouterProviderTag, setCurrentOpenRouterProviderTag] = useState<string | undefined>(
38
+ undefined
39
+ );
40
+ const [modelsWithPricing, setModelsWithPricing] = useState<ModelOption[]>(AVAILABLE_MODELS);
41
+ const [openRouterModels, setOpenRouterModels] = useState<ModelOption[]>([]);
42
+ const [openRouterModelsLoading, setOpenRouterModelsLoading] = useState(false);
43
+ const [openRouterModelsUpdatedAt, setOpenRouterModelsUpdatedAt] = useState<number | null>(null);
44
+ const [openRouterProviders, setOpenRouterProviders] = useState<OpenRouterInferenceProvider[]>([]);
45
+
46
+ useAppModelPricingLoader({
47
+ preferencesLoaded,
48
+ setModelsWithPricing,
49
+ });
50
+
51
+ useAppOpenRouterProviderLoader({
52
+ preferencesLoaded,
53
+ showProviderMenu,
54
+ modelId: currentModelId,
55
+ setProviders: setOpenRouterProviders,
56
+ });
57
+
58
+ const { refresh: refreshOpenRouterModels } = useAppOpenRouterModelsLoader({
59
+ preferencesLoaded,
60
+ setModels: setOpenRouterModels,
61
+ setLoading: setOpenRouterModelsLoading,
62
+ setUpdatedAt: setOpenRouterModelsUpdatedAt,
63
+ });
64
+
65
+ const providerMenuItems: ProviderMenuItem[] = useMemo(() => {
66
+ const pricingCandidates = openRouterProviders
67
+ .map((p) => p.pricing)
68
+ .filter((p): p is NonNullable<typeof p> => Boolean(p));
69
+ const avgPricing = pricingCandidates.length > 0 ? mergePricingAverages(pricingCandidates) : undefined;
70
+
71
+ const maxContextLength = openRouterProviders.reduce((max, p) => {
72
+ const value = typeof p.contextLength === "number" ? p.contextLength : 0;
73
+ return Math.max(max, value);
74
+ }, 0);
75
+
76
+ const anyCaching = openRouterProviders.some((p) => p.supportsCaching);
77
+
78
+ const items: ProviderMenuItem[] = [
79
+ {
80
+ tag: null,
81
+ label: "AUTO (OpenRouter routing)",
82
+ contextLength: maxContextLength || undefined,
83
+ pricing: avgPricing,
84
+ supportsCaching: anyCaching || undefined,
85
+ },
86
+ ];
87
+
88
+ const knownProviderTags = new Set<string>();
89
+ for (const provider of openRouterProviders) {
90
+ knownProviderTags.add(provider.tag);
91
+ items.push({
92
+ tag: provider.tag,
93
+ label: `${provider.providerName} (${provider.tag})`,
94
+ contextLength: provider.contextLength,
95
+ pricing: provider.pricing,
96
+ supportsCaching: provider.supportsCaching,
97
+ });
98
+ }
99
+
100
+ if (currentOpenRouterProviderTag && !knownProviderTags.has(currentOpenRouterProviderTag)) {
101
+ items.splice(1, 0, {
102
+ tag: currentOpenRouterProviderTag,
103
+ label: `SAVED (${currentOpenRouterProviderTag})`,
104
+ supportsCaching: false,
105
+ });
106
+ }
107
+
108
+ return items;
109
+ }, [openRouterProviders, currentOpenRouterProviderTag]);
110
+
111
+ return {
112
+ currentModelId,
113
+ setCurrentModelId,
114
+ currentOpenRouterProviderTag,
115
+ setCurrentOpenRouterProviderTag,
116
+ modelsWithPricing,
117
+ openRouterModels,
118
+ openRouterModelsLoading,
119
+ openRouterModelsUpdatedAt,
120
+ providerMenuItems,
121
+ refreshOpenRouterModels,
122
+ };
123
+ }
@@ -0,0 +1,44 @@
1
+ import { useCallback, useEffect } from "react";
2
+ import type { ModelOption } from "../types";
3
+ import { getOpenRouterModels } from "../utils/openrouter-models";
4
+
5
+ export interface UseAppOpenRouterModelsLoaderParams {
6
+ preferencesLoaded: boolean;
7
+ setModels: React.Dispatch<React.SetStateAction<ModelOption[]>>;
8
+ setLoading: React.Dispatch<React.SetStateAction<boolean>>;
9
+ setUpdatedAt: React.Dispatch<React.SetStateAction<number | null>>;
10
+ }
11
+
12
+ export interface UseAppOpenRouterModelsLoaderResult {
13
+ refresh: () => Promise<void>;
14
+ }
15
+
16
+ export function useAppOpenRouterModelsLoader(
17
+ params: UseAppOpenRouterModelsLoaderParams
18
+ ): UseAppOpenRouterModelsLoaderResult {
19
+ const { preferencesLoaded, setModels, setLoading, setUpdatedAt } = params;
20
+
21
+ const refresh = useCallback(
22
+ async (forceRefresh = false) => {
23
+ if (!preferencesLoaded) return;
24
+ setLoading(true);
25
+ try {
26
+ const result = await getOpenRouterModels({ forceRefresh });
27
+ setModels(result.models);
28
+ setUpdatedAt(result.timestamp);
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ },
33
+ [preferencesLoaded, setLoading, setModels, setUpdatedAt]
34
+ );
35
+
36
+ useEffect(() => {
37
+ if (!preferencesLoaded) return;
38
+ void refresh(false);
39
+ }, [preferencesLoaded, refresh]);
40
+
41
+ return {
42
+ refresh: () => refresh(true),
43
+ };
44
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect } from "react";
2
+ import type { OpenRouterInferenceProvider } from "../utils/openrouter-endpoints";
3
+ import { getOpenRouterModelProviders } from "../utils/openrouter-endpoints";
4
+
5
+ export interface UseAppOpenRouterProviderLoaderParams {
6
+ preferencesLoaded: boolean;
7
+ showProviderMenu: boolean;
8
+ modelId: string;
9
+ setProviders: React.Dispatch<React.SetStateAction<OpenRouterInferenceProvider[]>>;
10
+ }
11
+
12
+ export function useAppOpenRouterProviderLoader(params: UseAppOpenRouterProviderLoaderParams): void {
13
+ const { preferencesLoaded, showProviderMenu, modelId, setProviders } = params;
14
+
15
+ useEffect(() => {
16
+ if (!preferencesLoaded) return;
17
+ if (!showProviderMenu) return;
18
+
19
+ let cancelled = false;
20
+
21
+ (async () => {
22
+ try {
23
+ const providers = await getOpenRouterModelProviders(modelId);
24
+ if (cancelled) return;
25
+ setProviders(providers);
26
+ } catch (_err: unknown) {
27
+ // Silently fail - provider menu will just show no providers.
28
+ }
29
+ })();
30
+
31
+ return () => {
32
+ cancelled = true;
33
+ };
34
+ }, [preferencesLoaded, showProviderMenu, modelId, setProviders]);
35
+ }
@@ -0,0 +1,212 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { AVAILABLE_MODELS, setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
3
+ import type {
4
+ AppPreferences,
5
+ BashApprovalLevel,
6
+ OnboardingStep,
7
+ ReasoningEffort,
8
+ SpeechSpeed,
9
+ VoiceInteractionType,
10
+ } from "../types";
11
+ import { loadPreferences, updatePreferences } from "../utils/preferences";
12
+ import { setAudioDevice } from "../voice/audio-recorder";
13
+
14
+ export interface UseAppPreferencesBootstrapParams {
15
+ manager: {
16
+ interactionMode: "text" | "voice";
17
+ voiceInteractionType: VoiceInteractionType;
18
+ speechSpeed: SpeechSpeed;
19
+ reasoningEffort: ReasoningEffort;
20
+ bashApprovalLevel: BashApprovalLevel;
21
+ audioDeviceName?: string;
22
+ outputDeviceName?: string;
23
+ };
24
+ setCurrentModelId: (modelId: string) => void;
25
+ setCurrentOpenRouterProviderTag: (providerTag: string | undefined) => void;
26
+ setCurrentDevice: (deviceName: string | undefined) => void;
27
+ setCurrentOutputDevice: (deviceName: string | undefined) => void;
28
+ setInteractionMode: (mode: "text" | "voice") => void;
29
+ setVoiceInteractionType: (type: VoiceInteractionType) => void;
30
+ setSpeechSpeed: (speed: SpeechSpeed) => void;
31
+ setReasoningEffort: (effort: ReasoningEffort) => void;
32
+ setBashApprovalLevel: (level: BashApprovalLevel) => void;
33
+ setShowFullReasoning: (show: boolean) => void;
34
+ setShowToolOutput: (show: boolean) => void;
35
+ setLoadedPreferences: (prefs: AppPreferences | null) => void;
36
+ setOnboardingActive: (active: boolean) => void;
37
+ setOnboardingStep: (step: OnboardingStep) => void;
38
+ setPreferencesLoaded: (loaded: boolean) => void;
39
+ }
40
+
41
+ export interface UseAppPreferencesBootstrapReturn {
42
+ persistPreferences: (updates: Partial<AppPreferences>) => void;
43
+ }
44
+
45
+ export function useAppPreferencesBootstrap(
46
+ params: UseAppPreferencesBootstrapParams
47
+ ): UseAppPreferencesBootstrapReturn {
48
+ const {
49
+ manager,
50
+ setCurrentModelId,
51
+ setCurrentOpenRouterProviderTag,
52
+ setCurrentDevice,
53
+ setCurrentOutputDevice,
54
+ setInteractionMode,
55
+ setVoiceInteractionType,
56
+ setSpeechSpeed,
57
+ setReasoningEffort,
58
+ setBashApprovalLevel,
59
+ setShowFullReasoning,
60
+ setShowToolOutput,
61
+ setLoadedPreferences,
62
+ setOnboardingActive,
63
+ setOnboardingStep,
64
+ setPreferencesLoaded,
65
+ } = params;
66
+
67
+ const preferencesWriteRef = useRef<Promise<AppPreferences | null>>(Promise.resolve(null));
68
+
69
+ const persistPreferences = useCallback((updates: Partial<AppPreferences>) => {
70
+ preferencesWriteRef.current = preferencesWriteRef.current
71
+ .catch(() => null)
72
+ .then(() => updatePreferences(updates))
73
+ .catch(() => null);
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ let cancelled = false;
78
+
79
+ (async () => {
80
+ const prefs = await loadPreferences();
81
+ if (cancelled) return;
82
+
83
+ if (prefs?.openRouterApiKey && !process.env.OPENROUTER_API_KEY) {
84
+ process.env.OPENROUTER_API_KEY = prefs.openRouterApiKey;
85
+ }
86
+ if (prefs?.openAiApiKey && !process.env.OPENAI_API_KEY) {
87
+ process.env.OPENAI_API_KEY = prefs.openAiApiKey;
88
+ }
89
+ if (prefs?.exaApiKey && !process.env.EXA_API_KEY) {
90
+ process.env.EXA_API_KEY = prefs.exaApiKey;
91
+ }
92
+
93
+ if (prefs?.modelId) {
94
+ const modelIdx = AVAILABLE_MODELS.findIndex((m) => m.id === prefs.modelId);
95
+ if (modelIdx >= 0) {
96
+ setResponseModel(prefs.modelId);
97
+ setCurrentModelId(prefs.modelId);
98
+ }
99
+ }
100
+
101
+ if (prefs?.openRouterProviderTag) {
102
+ setOpenRouterProviderTag(prefs.openRouterProviderTag);
103
+ setCurrentOpenRouterProviderTag(prefs.openRouterProviderTag);
104
+ } else {
105
+ setOpenRouterProviderTag(undefined);
106
+ setCurrentOpenRouterProviderTag(undefined);
107
+ }
108
+
109
+ if (prefs?.audioDeviceName) {
110
+ manager.audioDeviceName = prefs.audioDeviceName;
111
+ setAudioDevice(prefs.audioDeviceName);
112
+ setCurrentDevice(prefs.audioDeviceName);
113
+ }
114
+
115
+ if (prefs?.audioOutputDeviceName) {
116
+ manager.outputDeviceName = prefs.audioOutputDeviceName;
117
+ setCurrentOutputDevice(prefs.audioOutputDeviceName);
118
+ }
119
+
120
+ if (prefs?.interactionMode) {
121
+ manager.interactionMode = prefs.interactionMode;
122
+ setInteractionMode(prefs.interactionMode);
123
+ }
124
+
125
+ if (prefs?.voiceInteractionType) {
126
+ manager.voiceInteractionType = prefs.voiceInteractionType;
127
+ setVoiceInteractionType(prefs.voiceInteractionType);
128
+ }
129
+
130
+ if (prefs?.speechSpeed) {
131
+ manager.speechSpeed = prefs.speechSpeed;
132
+ setSpeechSpeed(prefs.speechSpeed);
133
+ }
134
+
135
+ if (prefs?.reasoningEffort) {
136
+ manager.reasoningEffort = prefs.reasoningEffort;
137
+ setReasoningEffort(prefs.reasoningEffort);
138
+ }
139
+
140
+ if (prefs?.bashApprovalLevel) {
141
+ manager.bashApprovalLevel = prefs.bashApprovalLevel;
142
+ setBashApprovalLevel(prefs.bashApprovalLevel);
143
+ }
144
+
145
+ if (prefs?.showFullReasoning !== undefined) {
146
+ setShowFullReasoning(prefs.showFullReasoning);
147
+ }
148
+ if (prefs?.showToolOutput !== undefined) {
149
+ setShowToolOutput(prefs.showToolOutput);
150
+ }
151
+
152
+ const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY);
153
+ const hasOpenAiKey = Boolean(process.env.OPENAI_API_KEY);
154
+ const hasExaKey = Boolean(process.env.EXA_API_KEY);
155
+ const hasCoreSettings = Boolean(prefs?.audioDeviceName && prefs?.modelId);
156
+
157
+ setLoadedPreferences(prefs);
158
+
159
+ const isFreshLaunch = prefs === null;
160
+ const needsOnboarding = !hasOpenRouterKey || !hasOpenAiKey || !hasExaKey;
161
+
162
+ if (isFreshLaunch) {
163
+ setOnboardingStep("intro");
164
+ setOnboardingActive(true);
165
+ } else if (needsOnboarding) {
166
+ let startStep: OnboardingStep = "intro";
167
+ if (!hasOpenRouterKey) {
168
+ startStep = "openrouter_key";
169
+ } else if (!hasOpenAiKey) {
170
+ startStep = "openai_key";
171
+ } else if (!hasExaKey) {
172
+ startStep = "exa_key";
173
+ }
174
+ setOnboardingStep(startStep);
175
+ setOnboardingActive(true);
176
+ } else if (!prefs?.onboardingCompleted && !hasCoreSettings) {
177
+ setOnboardingStep("device");
178
+ setOnboardingActive(true);
179
+ } else {
180
+ setOnboardingActive(false);
181
+ setOnboardingStep("complete");
182
+ }
183
+
184
+ setPreferencesLoaded(true);
185
+ })();
186
+
187
+ return () => {
188
+ cancelled = true;
189
+ };
190
+ }, [
191
+ manager,
192
+ setCurrentModelId,
193
+ setCurrentOpenRouterProviderTag,
194
+ setCurrentDevice,
195
+ setCurrentOutputDevice,
196
+ setInteractionMode,
197
+ setVoiceInteractionType,
198
+ setSpeechSpeed,
199
+ setReasoningEffort,
200
+ setBashApprovalLevel,
201
+ setShowFullReasoning,
202
+ setShowToolOutput,
203
+ setLoadedPreferences,
204
+ setOnboardingActive,
205
+ setOnboardingStep,
206
+ setPreferencesLoaded,
207
+ ]);
208
+
209
+ return {
210
+ persistPreferences,
211
+ };
212
+ }
@@ -0,0 +1,105 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { generateSessionTitle } from "../ai/daemon-ai";
3
+ import { createSession, listSessions, updateSessionTitle } from "../state/session-store";
4
+ import type { SessionInfo } from "../types";
5
+
6
+ export interface UseAppSessionsParams {
7
+ showSessionMenu: boolean;
8
+ }
9
+
10
+ export interface UseAppSessionsReturn {
11
+ currentSessionId: string | null;
12
+ setCurrentSessionIdSafe: (nextSessionId: string | null) => void;
13
+ currentSessionIdRef: React.RefObject<string | null>;
14
+ ensureSessionId: () => Promise<string>;
15
+
16
+ sessions: SessionInfo[];
17
+ setSessions: React.Dispatch<React.SetStateAction<SessionInfo[]>>;
18
+
19
+ sessionCreateRef: React.RefObject<Promise<SessionInfo> | null>;
20
+
21
+ sessionMenuItems: Array<SessionInfo & { isNew: boolean }>;
22
+
23
+ handleFirstMessage: (targetSessionId: string, message: string) => void;
24
+ }
25
+
26
+ export function useAppSessions(params: UseAppSessionsParams): UseAppSessionsReturn {
27
+ const { showSessionMenu } = params;
28
+
29
+ const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
30
+ const currentSessionIdRef = useRef<string | null>(null);
31
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
32
+ const sessionCreateRef = useRef<Promise<SessionInfo> | null>(null);
33
+
34
+ const setCurrentSessionIdSafe = useCallback((nextSessionId: string | null) => {
35
+ currentSessionIdRef.current = nextSessionId;
36
+ setCurrentSessionId(nextSessionId);
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ currentSessionIdRef.current = currentSessionId;
41
+ }, [currentSessionId]);
42
+
43
+ const ensureSessionId = useCallback(async (): Promise<string> => {
44
+ if (currentSessionIdRef.current) return currentSessionIdRef.current;
45
+ if (!sessionCreateRef.current) {
46
+ sessionCreateRef.current = createSession()
47
+ .then((session) => {
48
+ setCurrentSessionIdSafe(session.id);
49
+ setSessions((prev) => [session, ...prev]);
50
+ return session;
51
+ })
52
+ .finally(() => {
53
+ sessionCreateRef.current = null;
54
+ });
55
+ }
56
+ const session = await sessionCreateRef.current;
57
+ currentSessionIdRef.current = session.id;
58
+ return session.id;
59
+ }, [setCurrentSessionIdSafe]);
60
+
61
+ const sessionMenuItems = useMemo(() => {
62
+ return sessions.map((session) => ({
63
+ ...session,
64
+ isNew: false,
65
+ }));
66
+ }, [sessions]);
67
+
68
+ useEffect(() => {
69
+ if (!showSessionMenu) return;
70
+ let cancelled = false;
71
+
72
+ (async () => {
73
+ const list = await listSessions();
74
+ if (cancelled) return;
75
+ setSessions(list);
76
+ const currentIdx = currentSessionIdRef.current
77
+ ? list.findIndex((session) => session.id === currentSessionIdRef.current)
78
+ : -1;
79
+ })();
80
+
81
+ return () => {
82
+ cancelled = true;
83
+ };
84
+ }, [showSessionMenu]);
85
+
86
+ const handleFirstMessage = useCallback((targetSessionId: string, message: string) => {
87
+ void (async () => {
88
+ const title = await generateSessionTitle(message);
89
+ await updateSessionTitle(targetSessionId, title);
90
+ setSessions((prev) => prev.map((s) => (s.id === targetSessionId ? { ...s, title } : s)));
91
+ })();
92
+ }, []);
93
+
94
+ return {
95
+ currentSessionId,
96
+ setCurrentSessionIdSafe,
97
+ currentSessionIdRef,
98
+ ensureSessionId,
99
+ sessions,
100
+ setSessions,
101
+ sessionCreateRef,
102
+ sessionMenuItems,
103
+ handleFirstMessage,
104
+ };
105
+ }
@@ -0,0 +1,62 @@
1
+ import { useState } from "react";
2
+ import { getDaemonManager } from "../state/daemon-state";
3
+ import type { BashApprovalLevel, ReasoningEffort, SpeechSpeed, VoiceInteractionType } from "../types";
4
+
5
+ export interface UseAppSettingsReturn {
6
+ interactionMode: "text" | "voice";
7
+ setInteractionMode: React.Dispatch<React.SetStateAction<"text" | "voice">>;
8
+
9
+ voiceInteractionType: VoiceInteractionType;
10
+ setVoiceInteractionType: React.Dispatch<React.SetStateAction<VoiceInteractionType>>;
11
+
12
+ speechSpeed: SpeechSpeed;
13
+ setSpeechSpeed: React.Dispatch<React.SetStateAction<SpeechSpeed>>;
14
+
15
+ reasoningEffort: ReasoningEffort;
16
+ setReasoningEffort: React.Dispatch<React.SetStateAction<ReasoningEffort>>;
17
+
18
+ bashApprovalLevel: BashApprovalLevel;
19
+ setBashApprovalLevel: React.Dispatch<React.SetStateAction<BashApprovalLevel>>;
20
+
21
+ showFullReasoning: boolean;
22
+ setShowFullReasoning: React.Dispatch<React.SetStateAction<boolean>>;
23
+
24
+ showToolOutput: boolean;
25
+ setShowToolOutput: React.Dispatch<React.SetStateAction<boolean>>;
26
+
27
+ canEnableVoiceOutput: boolean;
28
+ }
29
+
30
+ export function useAppSettings(): UseAppSettingsReturn {
31
+ const manager = getDaemonManager();
32
+
33
+ const [interactionMode, setInteractionMode] = useState(manager.interactionMode);
34
+ const [voiceInteractionType, setVoiceInteractionType] = useState(manager.voiceInteractionType);
35
+ const [speechSpeed, setSpeechSpeed] = useState<SpeechSpeed>(manager.speechSpeed);
36
+ const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(manager.reasoningEffort);
37
+ const [bashApprovalLevel, setBashApprovalLevel] = useState<BashApprovalLevel>(
38
+ manager.bashApprovalLevel ?? "dangerous"
39
+ );
40
+ const [showFullReasoning, setShowFullReasoning] = useState(true);
41
+ const [showToolOutput, setShowToolOutput] = useState(false);
42
+
43
+ const canEnableVoiceOutput = Boolean(process.env.OPENAI_API_KEY);
44
+
45
+ return {
46
+ interactionMode,
47
+ setInteractionMode,
48
+ voiceInteractionType,
49
+ setVoiceInteractionType,
50
+ speechSpeed,
51
+ setSpeechSpeed,
52
+ reasoningEffort,
53
+ setReasoningEffort,
54
+ bashApprovalLevel,
55
+ setBashApprovalLevel,
56
+ showFullReasoning,
57
+ setShowFullReasoning,
58
+ showToolOutput,
59
+ setShowToolOutput,
60
+ canEnableVoiceOutput,
61
+ };
62
+ }