@makefinks/daemon 0.2.0 → 0.3.1

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.
@@ -0,0 +1,546 @@
1
+ import { useRenderer } from "@opentui/react";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+
4
+ import type { ConversationPaneProps } from "../app/components/ConversationPane";
5
+
6
+ import { useAppCallbacks } from "./use-app-callbacks";
7
+ import { useAppContextBuilder } from "./use-app-context-builder";
8
+ import { useAppDisplayState } from "./use-app-display-state";
9
+ import { useAppMenus } from "./use-app-menus";
10
+ import { useAppModel } from "./use-app-model";
11
+ import { useAppPreferencesBootstrap } from "./use-app-preferences-bootstrap";
12
+ import { useAppSettings } from "./use-app-settings";
13
+ import { useBootstrapController } from "./use-bootstrap-controller";
14
+ import { useConversationManager } from "./use-conversation-manager";
15
+ import { useCopyOnSelect } from "./use-copy-on-select";
16
+ import { useDaemonKeyboard } from "./use-daemon-keyboard";
17
+ import { useDaemonRuntimeController } from "./use-daemon-runtime-controller";
18
+ import { useOverlayController } from "./use-overlay-controller";
19
+ import { useSessionController } from "./use-session-controller";
20
+
21
+ import { getDaemonManager } from "../state/daemon-state";
22
+ import { deleteSession } from "../state/session-store";
23
+ import { DaemonState } from "../types";
24
+
25
+ export interface AppControllerResult {
26
+ handleCopyOnSelectMouseUp: () => void;
27
+ avatarLayerProps: {
28
+ avatarRef: ReturnType<typeof useDaemonRuntimeController>["avatarRef"];
29
+ daemonState: ReturnType<typeof useDaemonRuntimeController>["daemonState"];
30
+ applyAvatarForState: ReturnType<typeof useDaemonRuntimeController>["applyAvatarForState"];
31
+ width: number;
32
+ height: number;
33
+ zIndex: number;
34
+ };
35
+ isListeningDim: boolean;
36
+ listeningDimTop: number;
37
+ conversationContainerZIndex: number;
38
+ conversationPaneProps: ConversationPaneProps;
39
+ appContextValue: ReturnType<typeof useAppContextBuilder>;
40
+ overlaysProps: {
41
+ conversationHistory: ReturnType<typeof useDaemonRuntimeController>["conversationHistory"];
42
+ currentContentBlocks: ReturnType<typeof useDaemonRuntimeController>["currentContentBlocks"];
43
+ };
44
+ }
45
+
46
+ export function useAppController({
47
+ initialStatusTop,
48
+ }: {
49
+ initialStatusTop: ConversationPaneProps["initialStatusTop"];
50
+ }): AppControllerResult {
51
+ const renderer = useRenderer();
52
+ const manager = getDaemonManager();
53
+
54
+ const { handleCopyOnSelectMouseUp } = useCopyOnSelect();
55
+
56
+ const [preferencesLoaded, setPreferencesLoaded] = useState(false);
57
+
58
+ const menus = useAppMenus();
59
+ const {
60
+ showDeviceMenu,
61
+ setShowDeviceMenu,
62
+ showSettingsMenu,
63
+ setShowSettingsMenu,
64
+ showModelMenu,
65
+ setShowModelMenu,
66
+ showProviderMenu,
67
+ setShowProviderMenu,
68
+ showSessionMenu,
69
+ setShowSessionMenu,
70
+ showHotkeysPane,
71
+ setShowHotkeysPane,
72
+ showGroundingMenu,
73
+ setShowGroundingMenu,
74
+ showUrlMenu,
75
+ setShowUrlMenu,
76
+ showToolsMenu,
77
+ setShowToolsMenu,
78
+ } = menus;
79
+
80
+ const session = useSessionController({ showSessionMenu });
81
+
82
+ const appSettings = useAppSettings();
83
+ const {
84
+ interactionMode,
85
+ setInteractionMode,
86
+ voiceInteractionType,
87
+ setVoiceInteractionType,
88
+ speechSpeed,
89
+ setSpeechSpeed,
90
+ reasoningEffort,
91
+ setReasoningEffort,
92
+ bashApprovalLevel,
93
+ setBashApprovalLevel,
94
+ showFullReasoning,
95
+ setShowFullReasoning,
96
+ showToolOutput,
97
+ setShowToolOutput,
98
+ canEnableVoiceOutput,
99
+ } = appSettings;
100
+
101
+ const appModel = useAppModel({
102
+ preferencesLoaded,
103
+ showProviderMenu,
104
+ });
105
+ const {
106
+ currentModelId,
107
+ setCurrentModelId,
108
+ currentOpenRouterProviderTag,
109
+ setCurrentOpenRouterProviderTag,
110
+ modelsWithPricing,
111
+ openRouterModels,
112
+ openRouterModelsLoading,
113
+ openRouterModelsUpdatedAt,
114
+ providerMenuItems,
115
+ refreshOpenRouterModels,
116
+ } = appModel;
117
+
118
+ const bootstrap = useBootstrapController({
119
+ preferencesLoaded,
120
+ showDeviceMenu,
121
+ });
122
+
123
+ const daemon = useDaemonRuntimeController({
124
+ currentModelId,
125
+ preferencesLoaded,
126
+ sessionId: session.currentSessionId,
127
+ sessionIdRef: session.currentSessionIdRef,
128
+ ensureSessionId: session.ensureSessionId,
129
+ onFirstMessage: session.handleFirstMessage,
130
+ });
131
+
132
+ const [resetNotification, setResetNotification] = useState<string>("");
133
+ const [apiKeyMissingError, setApiKeyMissingError] = useState<string>("");
134
+ const [escPendingCancel, setEscPendingCancel] = useState(false);
135
+
136
+ const supportsReasoning = daemon.modelMetadata?.supportsReasoning ?? false;
137
+
138
+ // Preferences bootstrap (hook): returns a stable persist callback.
139
+ const { persistPreferences } = useAppPreferencesBootstrap({
140
+ manager,
141
+ setCurrentModelId,
142
+ setCurrentOpenRouterProviderTag,
143
+ setCurrentDevice: bootstrap.setCurrentDevice,
144
+ setCurrentOutputDevice: bootstrap.setCurrentOutputDevice,
145
+ setInteractionMode,
146
+ setVoiceInteractionType,
147
+ setSpeechSpeed,
148
+ setReasoningEffort,
149
+ setBashApprovalLevel,
150
+ setShowFullReasoning,
151
+ setShowToolOutput,
152
+ setLoadedPreferences: bootstrap.setLoadedPreferences,
153
+ setOnboardingActive: bootstrap.setOnboardingActive,
154
+ setOnboardingStep: bootstrap.setOnboardingStep,
155
+ setPreferencesLoaded,
156
+ });
157
+
158
+ const conversationManager = useConversationManager({
159
+ conversationHistory: daemon.conversationHistory,
160
+ sessionUsage: daemon.sessionUsage,
161
+ currentSessionId: session.currentSessionId,
162
+ ensureSessionId: session.ensureSessionId,
163
+ setCurrentSessionIdSafe: session.setCurrentSessionIdSafe,
164
+ currentSessionIdRef: session.currentSessionIdRef,
165
+ setSessions: session.setSessions,
166
+ hydrateConversationHistory: daemon.hydrateConversationHistory,
167
+ setCurrentTranscription: daemon.setCurrentTranscription,
168
+ setCurrentResponse: daemon.setCurrentResponse,
169
+ clearCurrentContentBlocks: daemon.clearCurrentContentBlocks,
170
+ clearReasoningState: daemon.reasoning.clearReasoningState,
171
+ resetSessionUsage: daemon.resetSessionUsage,
172
+ setSessionUsage: daemon.setSessionUsage,
173
+ currentUserInputRef: daemon.currentUserInputRef,
174
+ });
175
+
176
+ const { clearConversationState, loadSessionById, startNewSession, undoLastTurn } = conversationManager;
177
+
178
+ const startNewSessionAndReset = useCallback(() => {
179
+ startNewSession();
180
+ daemon.applyAvatarForState(DaemonState.IDLE);
181
+ }, [startNewSession, daemon.applyAvatarForState]);
182
+
183
+ const {
184
+ handleDeviceSelect,
185
+ handleOutputDeviceSelect,
186
+ handleModelSelect,
187
+ handleProviderSelect,
188
+ toggleInteractionMode,
189
+ completeOnboarding,
190
+ handleApiKeySubmit,
191
+ } = useAppCallbacks({
192
+ currentModelId,
193
+ setCurrentModelId,
194
+ setCurrentDevice: bootstrap.setCurrentDevice,
195
+ setCurrentOutputDevice: bootstrap.setCurrentOutputDevice,
196
+ setCurrentOpenRouterProviderTag,
197
+ setInteractionMode,
198
+ setVoiceInteractionType,
199
+ setSpeechSpeed,
200
+ setReasoningEffort,
201
+ persistPreferences,
202
+ loadedPreferences: bootstrap.loadedPreferences,
203
+ onboardingStep: bootstrap.onboardingStep,
204
+ setOnboardingStep: bootstrap.setOnboardingStep,
205
+ apiKeyTextareaRef: bootstrap.apiKeyTextareaRef,
206
+ setShowDeviceMenu,
207
+ setShowModelMenu,
208
+ setShowProviderMenu,
209
+ setShowSettingsMenu,
210
+ setShowSessionMenu,
211
+ setOnboardingActive: bootstrap.setOnboardingActive,
212
+ });
213
+
214
+ const handleSessionSelect = useCallback(
215
+ (selectedIdx: number) => {
216
+ const item = session.sessionMenuItems[selectedIdx];
217
+ if (!item) return;
218
+ void loadSessionById(item.id);
219
+ },
220
+ [session.sessionMenuItems, loadSessionById]
221
+ );
222
+
223
+ const handleSessionDelete = useCallback(
224
+ (selectedIdx: number) => {
225
+ const item = session.sessionMenuItems[selectedIdx];
226
+ if (!item) return;
227
+
228
+ void (async () => {
229
+ await deleteSession(item.id);
230
+ session.setSessions((prev) => prev.filter((s) => s.id !== item.id));
231
+
232
+ if (session.currentSessionIdRef.current === item.id) {
233
+ clearConversationState();
234
+ session.setCurrentSessionIdSafe(null);
235
+ setResetNotification("SESSION DELETED");
236
+ setTimeout(() => setResetNotification(""), 2000);
237
+ }
238
+ })();
239
+ },
240
+ [
241
+ session.sessionMenuItems,
242
+ session.setSessions,
243
+ session.currentSessionIdRef,
244
+ clearConversationState,
245
+ session.setCurrentSessionIdSafe,
246
+ ]
247
+ );
248
+
249
+ const keyboardActions = useMemo(
250
+ () => ({
251
+ setShowDeviceMenu,
252
+ setShowSettingsMenu,
253
+ setShowModelMenu,
254
+ setShowProviderMenu,
255
+ setShowSessionMenu,
256
+ setShowHotkeysPane,
257
+ setShowGroundingMenu,
258
+ setShowUrlMenu,
259
+ setShowToolsMenu,
260
+ setTypingInput: daemon.typing.setTypingInput,
261
+ setCurrentTranscription: daemon.setCurrentTranscription,
262
+ setCurrentResponse: daemon.setCurrentResponse,
263
+ setApiKeyMissingError,
264
+ setEscPendingCancel,
265
+ setShowFullReasoning,
266
+ setShowToolOutput,
267
+ persistPreferences,
268
+ clearReasoningState: daemon.reasoning.clearReasoningState,
269
+ currentUserInputRef: daemon.currentUserInputRef,
270
+ conversationScrollRef: daemon.conversationScrollRef,
271
+ startNewSession: startNewSessionAndReset,
272
+ undoLastTurn,
273
+ }),
274
+ [
275
+ setShowDeviceMenu,
276
+ setShowSettingsMenu,
277
+ setShowModelMenu,
278
+ setShowProviderMenu,
279
+ setShowSessionMenu,
280
+ setShowHotkeysPane,
281
+ setShowGroundingMenu,
282
+ setShowUrlMenu,
283
+ setShowToolsMenu,
284
+ daemon.typing.setTypingInput,
285
+ daemon.setCurrentTranscription,
286
+ daemon.setCurrentResponse,
287
+ setShowFullReasoning,
288
+ setShowToolOutput,
289
+ persistPreferences,
290
+ daemon.reasoning.clearReasoningState,
291
+ daemon.currentUserInputRef,
292
+ daemon.conversationScrollRef,
293
+ startNewSessionAndReset,
294
+ undoLastTurn,
295
+ ]
296
+ );
297
+
298
+ const { isOverlayOpen } = useOverlayController(
299
+ {
300
+ showDeviceMenu,
301
+ showSettingsMenu,
302
+ showModelMenu,
303
+ showProviderMenu,
304
+ showSessionMenu,
305
+ showHotkeysPane,
306
+ showGroundingMenu,
307
+ showUrlMenu,
308
+ showToolsMenu,
309
+ onboardingActive: bootstrap.onboardingActive,
310
+ },
311
+ {
312
+ setShowDeviceMenu,
313
+ setShowSettingsMenu,
314
+ setShowModelMenu,
315
+ setShowProviderMenu,
316
+ setShowSessionMenu,
317
+ setShowHotkeysPane,
318
+ setShowGroundingMenu,
319
+ setShowUrlMenu,
320
+ setShowToolsMenu,
321
+ }
322
+ );
323
+
324
+ useDaemonKeyboard(
325
+ {
326
+ isOverlayOpen,
327
+ escPendingCancel,
328
+ hasInteracted: daemon.hasInteracted,
329
+ hasGrounding: session.hasGrounding,
330
+ showFullReasoning,
331
+ showToolOutput,
332
+ },
333
+ keyboardActions
334
+ );
335
+
336
+ const displayState = useAppDisplayState({
337
+ daemonState: daemon.daemonState,
338
+ currentContentBlocks: daemon.currentContentBlocks,
339
+ currentResponse: daemon.currentResponse,
340
+ reasoningDisplay: daemon.reasoning.reasoningDisplay,
341
+ reasoningQueue: daemon.reasoning.reasoningQueue,
342
+ responseElapsedMs: daemon.responseElapsedMs,
343
+ hasInteracted: daemon.hasInteracted,
344
+ currentModelId,
345
+ modelMetadata: daemon.modelMetadata,
346
+ preferencesLoaded,
347
+ currentSessionId: session.currentSessionId,
348
+ sessionMenuItems: session.sessionMenuItems,
349
+ terminalWidth: renderer.terminalWidth,
350
+ terminalHeight: renderer.terminalHeight,
351
+ });
352
+
353
+ const {
354
+ isToolCalling,
355
+ statusText,
356
+ statusColor,
357
+ showWorkingSpinner,
358
+ workingSpinnerLabel,
359
+ modelName,
360
+ sessionTitle,
361
+ avatarWidth,
362
+ avatarHeight,
363
+ frostColor,
364
+ isListening,
365
+ isListeningDim,
366
+ } = displayState;
367
+
368
+ const statusBarHeight = daemon.hasInteracted ? (apiKeyMissingError ? 5 : 3) : 0;
369
+
370
+ useEffect(() => {
371
+ if (daemon.daemonState === DaemonState.IDLE) {
372
+ setEscPendingCancel(false);
373
+ }
374
+ }, [daemon.daemonState]);
375
+
376
+ const appContextValue = useAppContextBuilder({
377
+ menus: {
378
+ showDeviceMenu,
379
+ setShowDeviceMenu,
380
+ showSettingsMenu,
381
+ setShowSettingsMenu,
382
+ showModelMenu,
383
+ setShowModelMenu,
384
+ showProviderMenu,
385
+ setShowProviderMenu,
386
+ showSessionMenu,
387
+ setShowSessionMenu,
388
+ showHotkeysPane,
389
+ setShowHotkeysPane,
390
+ showGroundingMenu,
391
+ setShowGroundingMenu,
392
+ showUrlMenu,
393
+ setShowUrlMenu,
394
+ showToolsMenu,
395
+ setShowToolsMenu,
396
+ },
397
+ device: {
398
+ devices: bootstrap.devices,
399
+ currentDevice: bootstrap.currentDevice,
400
+ setCurrentDevice: bootstrap.setCurrentDevice,
401
+ currentOutputDevice: bootstrap.currentOutputDevice,
402
+ setCurrentOutputDevice: bootstrap.setCurrentOutputDevice,
403
+ deviceLoadTimedOut: bootstrap.deviceLoadTimedOut,
404
+ soxAvailable: bootstrap.soxAvailable,
405
+ soxInstallHint: bootstrap.soxInstallHint,
406
+ },
407
+ settings: {
408
+ interactionMode,
409
+ voiceInteractionType,
410
+ speechSpeed,
411
+ reasoningEffort,
412
+ bashApprovalLevel,
413
+ supportsReasoning,
414
+ canEnableVoiceOutput,
415
+ showFullReasoning,
416
+ setShowFullReasoning,
417
+ showToolOutput,
418
+ setShowToolOutput,
419
+ setBashApprovalLevel,
420
+ persistPreferences,
421
+ },
422
+ model: {
423
+ curatedModels: modelsWithPricing,
424
+ openRouterModels,
425
+ openRouterModelsLoading,
426
+ openRouterModelsUpdatedAt,
427
+ currentModelId,
428
+ setCurrentModelId,
429
+ providerMenuItems,
430
+ currentOpenRouterProviderTag,
431
+ },
432
+ session: {
433
+ sessionMenuItems: session.sessionMenuItems,
434
+ currentSessionId: session.currentSessionId,
435
+ },
436
+ grounding: {
437
+ latestGroundingMap: session.latestGroundingMap,
438
+ groundingInitialIndex: session.groundingInitialIndex,
439
+ groundingSelectedIndex: session.groundingSelectedIndex,
440
+ setGroundingSelectedIndex: session.setGroundingSelectedIndex,
441
+ },
442
+ onboarding: {
443
+ onboardingActive: bootstrap.onboardingActive,
444
+ onboardingStep: bootstrap.onboardingStep,
445
+ setOnboardingStep: bootstrap.setOnboardingStep,
446
+ onboardingPreferences: bootstrap.loadedPreferences,
447
+ apiKeyTextareaRef: bootstrap.apiKeyTextareaRef,
448
+ },
449
+ deviceCallbacks: {
450
+ onDeviceSelect: handleDeviceSelect,
451
+ onOutputDeviceSelect: handleOutputDeviceSelect,
452
+ },
453
+ settingsCallbacks: {
454
+ onToggleInteractionMode: toggleInteractionMode,
455
+ onSetVoiceInteractionType: setVoiceInteractionType,
456
+ onSetSpeechSpeed: setSpeechSpeed,
457
+ onSetReasoningEffort: setReasoningEffort,
458
+ onSetBashApprovalLevel: setBashApprovalLevel,
459
+ },
460
+ modelCallbacks: {
461
+ onModelSelect: handleModelSelect,
462
+ onModelRefresh: refreshOpenRouterModels,
463
+ onProviderSelect: handleProviderSelect,
464
+ },
465
+ sessionCallbacks: {
466
+ onSessionSelect: handleSessionSelect,
467
+ onSessionDelete: handleSessionDelete,
468
+ },
469
+ groundingCallbacks: {
470
+ onGroundingSelect: session.onGroundingSelect,
471
+ onGroundingIndexChange: session.onGroundingIndexChange,
472
+ },
473
+ onboardingCallbacks: {
474
+ onKeySubmit: handleApiKeySubmit,
475
+ completeOnboarding,
476
+ },
477
+ });
478
+
479
+ return {
480
+ handleCopyOnSelectMouseUp,
481
+ avatarLayerProps: {
482
+ avatarRef: daemon.avatarRef,
483
+ daemonState: daemon.daemonState,
484
+ applyAvatarForState: daemon.applyAvatarForState,
485
+ width: avatarWidth,
486
+ height: avatarHeight,
487
+ zIndex: isListening && daemon.hasInteracted ? 2 : 0,
488
+ },
489
+ isListeningDim,
490
+ listeningDimTop: statusBarHeight,
491
+ conversationContainerZIndex: isListening ? 0 : 1,
492
+ conversationPaneProps: {
493
+ conversation: {
494
+ conversationHistory: daemon.conversationHistory,
495
+ currentTranscription: daemon.currentTranscription,
496
+ currentResponse: daemon.currentResponse,
497
+ currentContentBlocks: daemon.currentContentBlocks,
498
+ },
499
+ status: {
500
+ daemonState: daemon.daemonState,
501
+ statusText,
502
+ statusColor,
503
+ apiKeyMissingError,
504
+ error: daemon.error,
505
+ resetNotification,
506
+ escPendingCancel,
507
+ },
508
+ reasoning: {
509
+ showFullReasoning,
510
+ showToolOutput,
511
+ reasoningQueue: daemon.reasoning.reasoningQueue,
512
+ reasoningDisplay: daemon.reasoning.reasoningDisplay,
513
+ fullReasoning: daemon.reasoning.fullReasoning,
514
+ },
515
+ progress: {
516
+ showWorkingSpinner,
517
+ workingSpinnerLabel,
518
+ isToolCalling,
519
+ responseElapsedMs: daemon.responseElapsedMs,
520
+ },
521
+ typing: {
522
+ typingTextareaRef: daemon.typing.typingTextareaRef,
523
+ conversationScrollRef: daemon.conversationScrollRef,
524
+ onTypingContentChange: daemon.typing.handleTypingContentChange,
525
+ onTypingSubmit: daemon.typing.handleTypingSubmit,
526
+ onHistoryUp: daemon.typing.handleHistoryUp,
527
+ onHistoryDown: daemon.typing.handleHistoryDown,
528
+ },
529
+ sessionUsage: daemon.sessionUsage,
530
+ modelMetadata: daemon.modelMetadata,
531
+ hasInteracted: daemon.hasInteracted,
532
+ frostColor,
533
+ initialStatusTop,
534
+ hasGrounding: session.hasGrounding,
535
+ groundingCount: session.latestGroundingMap?.items.length,
536
+ modelName: modelName ?? "",
537
+ sessionTitle: sessionTitle ?? "",
538
+ isVoiceOutputEnabled: interactionMode === "voice",
539
+ },
540
+ appContextValue,
541
+ overlaysProps: {
542
+ conversationHistory: daemon.conversationHistory,
543
+ currentContentBlocks: daemon.currentContentBlocks,
544
+ },
545
+ };
546
+ }
@@ -24,6 +24,9 @@ export interface UseAppMenusReturn {
24
24
 
25
25
  showUrlMenu: boolean;
26
26
  setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
27
+
28
+ showToolsMenu: boolean;
29
+ setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
27
30
  }
28
31
 
29
32
  export function useAppMenus(): UseAppMenusReturn {
@@ -35,6 +38,7 @@ export function useAppMenus(): UseAppMenusReturn {
35
38
  const [showHotkeysPane, setShowHotkeysPane] = useState(false);
36
39
  const [showGroundingMenu, setShowGroundingMenu] = useState(false);
37
40
  const [showUrlMenu, setShowUrlMenu] = useState(false);
41
+ const [showToolsMenu, setShowToolsMenu] = useState(false);
38
42
 
39
43
  return {
40
44
  showDeviceMenu,
@@ -53,5 +57,7 @@ export function useAppMenus(): UseAppMenusReturn {
53
57
  setShowGroundingMenu,
54
58
  showUrlMenu,
55
59
  setShowUrlMenu,
60
+ showToolsMenu,
61
+ setShowToolsMenu,
56
62
  };
57
63
  }
@@ -6,8 +6,10 @@ import type {
6
6
  OnboardingStep,
7
7
  ReasoningEffort,
8
8
  SpeechSpeed,
9
+ ToolToggles,
9
10
  VoiceInteractionType,
10
11
  } from "../types";
12
+ import { DEFAULT_TOOL_TOGGLES } from "../types";
11
13
  import { loadPreferences, updatePreferences } from "../utils/preferences";
12
14
  import { setAudioDevice } from "../voice/audio-recorder";
13
15
 
@@ -18,6 +20,7 @@ export interface UseAppPreferencesBootstrapParams {
18
20
  speechSpeed: SpeechSpeed;
19
21
  reasoningEffort: ReasoningEffort;
20
22
  bashApprovalLevel: BashApprovalLevel;
23
+ toolToggles?: ToolToggles;
21
24
  audioDeviceName?: string;
22
25
  outputDeviceName?: string;
23
26
  };
@@ -142,6 +145,12 @@ export function useAppPreferencesBootstrap(
142
145
  setBashApprovalLevel(prefs.bashApprovalLevel);
143
146
  }
144
147
 
148
+ if (prefs?.toolToggles) {
149
+ manager.toolToggles = { ...DEFAULT_TOOL_TOGGLES, ...prefs.toolToggles };
150
+ } else {
151
+ manager.toolToggles = { ...DEFAULT_TOOL_TOGGLES };
152
+ }
153
+
145
154
  if (prefs?.showFullReasoning !== undefined) {
146
155
  setShowFullReasoning(prefs.showFullReasoning);
147
156
  }
@@ -0,0 +1,92 @@
1
+ import type { TextareaRenderable } from "@opentui/core";
2
+ import { useMemo, useRef, useState } from "react";
3
+ import type { MutableRefObject } from "react";
4
+
5
+ import { useAppAudioDevicesLoader } from "./use-app-audio-devices-loader";
6
+ import { usePlaywrightNotification } from "./use-playwright-notification";
7
+ import { useVoiceDependenciesNotification } from "./use-voice-dependencies-notification";
8
+
9
+ import type { AppPreferences, AudioDevice, OnboardingStep } from "../types";
10
+ import { getSoxInstallHint, isSoxAvailable } from "../voice/audio-recorder";
11
+
12
+ export interface BootstrapControllerResult {
13
+ onboardingActive: boolean;
14
+ setOnboardingActive: (active: boolean) => void;
15
+
16
+ onboardingStep: OnboardingStep;
17
+ setOnboardingStep: (step: OnboardingStep) => void;
18
+
19
+ loadedPreferences: AppPreferences | null;
20
+ setLoadedPreferences: (prefs: AppPreferences | null) => void;
21
+
22
+ devices: AudioDevice[];
23
+ setDevices: (devices: AudioDevice[]) => void;
24
+
25
+ currentDevice: string | undefined;
26
+ setCurrentDevice: (deviceId: string | undefined) => void;
27
+
28
+ currentOutputDevice: string | undefined;
29
+ setCurrentOutputDevice: (deviceId: string | undefined) => void;
30
+
31
+ deviceLoadTimedOut: boolean;
32
+ setDeviceLoadTimedOut: (timedOut: boolean) => void;
33
+
34
+ soxAvailable: boolean;
35
+ soxInstallHint: string;
36
+
37
+ apiKeyTextareaRef: MutableRefObject<TextareaRenderable | null>;
38
+ }
39
+
40
+ export function useBootstrapController({
41
+ preferencesLoaded,
42
+ showDeviceMenu,
43
+ }: {
44
+ preferencesLoaded: boolean;
45
+ showDeviceMenu: boolean;
46
+ }): BootstrapControllerResult {
47
+ const [onboardingActive, setOnboardingActive] = useState(false);
48
+ usePlaywrightNotification({ enabled: !onboardingActive });
49
+ useVoiceDependenciesNotification({ enabled: !onboardingActive });
50
+
51
+ const [loadedPreferences, setLoadedPreferences] = useState<AppPreferences | null>(null);
52
+ const [onboardingStep, setOnboardingStep] = useState<OnboardingStep>("intro");
53
+
54
+ const [devices, setDevices] = useState<AudioDevice[]>([]);
55
+ const [currentDevice, setCurrentDevice] = useState<string | undefined>(undefined);
56
+ const [currentOutputDevice, setCurrentOutputDevice] = useState<string | undefined>(undefined);
57
+ const [deviceLoadTimedOut, setDeviceLoadTimedOut] = useState(false);
58
+
59
+ const soxAvailable = useMemo(() => isSoxAvailable(), []);
60
+ const soxInstallHint = useMemo(() => getSoxInstallHint(), []);
61
+
62
+ const apiKeyTextareaRef = useRef<TextareaRenderable | null>(null);
63
+
64
+ useAppAudioDevicesLoader({
65
+ preferencesLoaded,
66
+ showDeviceMenu,
67
+ onboardingStep,
68
+ setDevices,
69
+ setCurrentDevice,
70
+ setDeviceLoadTimedOut,
71
+ });
72
+
73
+ return {
74
+ onboardingActive,
75
+ setOnboardingActive,
76
+ onboardingStep,
77
+ setOnboardingStep,
78
+ loadedPreferences,
79
+ setLoadedPreferences,
80
+ devices,
81
+ setDevices,
82
+ currentDevice,
83
+ setCurrentDevice,
84
+ currentOutputDevice,
85
+ setCurrentOutputDevice,
86
+ deviceLoadTimedOut,
87
+ setDeviceLoadTimedOut,
88
+ soxAvailable,
89
+ soxInstallHint,
90
+ apiKeyTextareaRef,
91
+ };
92
+ }