@makefinks/daemon 0.1.4 → 0.3.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 (34) hide show
  1. package/package.json +5 -4
  2. package/src/ai/daemon-ai.ts +30 -85
  3. package/src/ai/system-prompt.ts +134 -111
  4. package/src/ai/tool-approval-coordinator.ts +113 -0
  5. package/src/ai/tools/index.ts +12 -32
  6. package/src/ai/tools/subagents.ts +16 -30
  7. package/src/ai/tools/tool-registry.ts +203 -0
  8. package/src/app/App.tsx +23 -631
  9. package/src/app/components/AppOverlays.tsx +25 -1
  10. package/src/app/components/ConversationPane.tsx +5 -3
  11. package/src/components/HotkeysPane.tsx +3 -1
  12. package/src/components/TokenUsageDisplay.tsx +11 -11
  13. package/src/components/ToolsMenu.tsx +235 -0
  14. package/src/components/UrlMenu.tsx +182 -0
  15. package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
  16. package/src/hooks/daemon-event-handlers.ts +11 -151
  17. package/src/hooks/use-app-context-builder.ts +4 -0
  18. package/src/hooks/use-app-controller.ts +546 -0
  19. package/src/hooks/use-app-menus.ts +12 -0
  20. package/src/hooks/use-app-preferences-bootstrap.ts +9 -0
  21. package/src/hooks/use-bootstrap-controller.ts +92 -0
  22. package/src/hooks/use-daemon-keyboard.ts +63 -57
  23. package/src/hooks/use-daemon-runtime-controller.ts +147 -0
  24. package/src/hooks/use-grounding-menu-controller.ts +51 -0
  25. package/src/hooks/use-overlay-controller.ts +84 -0
  26. package/src/hooks/use-session-controller.ts +79 -0
  27. package/src/hooks/use-url-menu-items.ts +19 -0
  28. package/src/state/app-context.tsx +4 -0
  29. package/src/state/daemon-state.ts +19 -8
  30. package/src/state/session-store.ts +4 -0
  31. package/src/types/index.ts +39 -0
  32. package/src/utils/derive-url-menu-items.ts +155 -0
  33. package/src/utils/formatters.ts +1 -7
  34. package/src/utils/preferences.ts +10 -0
package/src/app/App.tsx CHANGED
@@ -1,51 +1,15 @@
1
1
  import { Toaster } from "@opentui-ui/toast/react";
2
- import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
3
- import { extend, useRenderer } from "@opentui/react";
2
+ import { extend } from "@opentui/react";
4
3
  import "opentui-spinner/react";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
4
+
7
5
  import { DaemonAvatarRenderable } from "../avatar/DaemonAvatarRenderable";
8
- import { useAppAudioDevicesLoader } from "../hooks/use-app-audio-devices-loader";
9
- import { useAppCallbacks } from "../hooks/use-app-callbacks";
10
- import { useAppContextBuilder } from "../hooks/use-app-context-builder";
11
- import { useAppDisplayState } from "../hooks/use-app-display-state";
12
- import { useAppMenus } from "../hooks/use-app-menus";
13
- import { useAppModel } from "../hooks/use-app-model";
14
- import { useAppPreferencesBootstrap } from "../hooks/use-app-preferences-bootstrap";
15
- import { useAppSessions } from "../hooks/use-app-sessions";
16
- import { useAppSettings } from "../hooks/use-app-settings";
17
- import { useConversationManager } from "../hooks/use-conversation-manager";
18
- import { useCopyOnSelect } from "../hooks/use-copy-on-select";
19
- import { useDaemonEvents } from "../hooks/use-daemon-events";
20
- import { useDaemonKeyboard } from "../hooks/use-daemon-keyboard";
21
- import { useGrounding } from "../hooks/use-grounding";
22
- import { useInputHistory } from "../hooks/use-input-history";
23
- import { usePlaywrightNotification } from "../hooks/use-playwright-notification";
24
- import { useReasoningAnimation } from "../hooks/use-reasoning-animation";
25
- import { useResponseTimer } from "../hooks/use-response-timer";
6
+ import { useAppController } from "../hooks/use-app-controller";
26
7
  import { ToolApprovalProvider } from "../hooks/use-tool-approval";
27
- import { useTypingMode } from "../hooks/use-typing-mode";
28
- import { useVoiceDependenciesNotification } from "../hooks/use-voice-dependencies-notification";
29
8
  import { AppProvider } from "../state/app-context";
30
- import { daemonEvents } from "../state/daemon-events";
31
- import { getDaemonManager } from "../state/daemon-state";
32
- import { deleteSession } from "../state/session-store";
33
- import { DaemonState } from "../types";
34
- import type { AppPreferences, AudioDevice, OnboardingStep } from "../types";
35
9
  import { COLORS } from "../ui/constants";
36
- import { openUrlInBrowser } from "../utils/preferences";
37
- import { buildTextFragmentUrl } from "../utils/text-fragment";
38
- import { getSoxInstallHint, isSoxAvailable, setAudioDevice } from "../voice/audio-recorder";
39
10
  import { AppOverlays } from "./components/AppOverlays";
40
11
  import { AvatarLayer } from "./components/AvatarLayer";
41
- import {
42
- type ConversationDisplayState,
43
- ConversationPane,
44
- type ProgressDisplayState,
45
- type ReasoningDisplayState,
46
- type StatusDisplayState,
47
- type TypingInputState,
48
- } from "./components/ConversationPane";
12
+ import { ConversationPane } from "./components/ConversationPane";
49
13
 
50
14
  const INITIAL_STATUS_TOP = "70%";
51
15
 
@@ -79,568 +43,7 @@ extend({
79
43
  });
80
44
 
81
45
  export function App() {
82
- const renderer = useRenderer();
83
-
84
- const [onboardingActive, setOnboardingActive] = useState(false);
85
- usePlaywrightNotification({ enabled: !onboardingActive });
86
- useVoiceDependenciesNotification({ enabled: !onboardingActive });
87
- const { handleCopyOnSelectMouseUp } = useCopyOnSelect();
88
-
89
- const {
90
- reasoningQueue,
91
- reasoningDisplay,
92
- fullReasoning,
93
- setReasoningQueue,
94
- setFullReasoning,
95
- fullReasoningRef,
96
- clearReasoningState,
97
- clearReasoningTicker,
98
- } = useReasoningAnimation();
99
-
100
- const [preferencesLoaded, setPreferencesLoaded] = useState(false);
101
-
102
- const menus = useAppMenus();
103
- const {
104
- showDeviceMenu,
105
- setShowDeviceMenu,
106
- showSettingsMenu,
107
- setShowSettingsMenu,
108
- showModelMenu,
109
- setShowModelMenu,
110
- showProviderMenu,
111
- setShowProviderMenu,
112
- showSessionMenu,
113
- setShowSessionMenu,
114
- showHotkeysPane,
115
- setShowHotkeysPane,
116
- showGroundingMenu,
117
- setShowGroundingMenu,
118
- } = menus;
119
-
120
- const {
121
- currentSessionId,
122
- setCurrentSessionIdSafe,
123
- currentSessionIdRef,
124
- ensureSessionId,
125
- setSessions,
126
- sessionMenuItems,
127
- handleFirstMessage,
128
- } = useAppSessions({ showSessionMenu });
129
-
130
- const { latestGroundingMap, hasGrounding } = useGrounding(currentSessionId);
131
- const [groundingSelectedIndex, setGroundingSelectedIndex] = useState(0);
132
-
133
- const appSettings = useAppSettings();
134
- const {
135
- interactionMode,
136
- setInteractionMode,
137
- voiceInteractionType,
138
- setVoiceInteractionType,
139
- speechSpeed,
140
- setSpeechSpeed,
141
- reasoningEffort,
142
- setReasoningEffort,
143
- bashApprovalLevel,
144
- setBashApprovalLevel,
145
- showFullReasoning,
146
- setShowFullReasoning,
147
- showToolOutput,
148
- setShowToolOutput,
149
- canEnableVoiceOutput,
150
- } = appSettings;
151
-
152
- const appModel = useAppModel({
153
- preferencesLoaded,
154
- showProviderMenu,
155
- });
156
- const {
157
- currentModelId,
158
- setCurrentModelId,
159
- currentOpenRouterProviderTag,
160
- setCurrentOpenRouterProviderTag,
161
- modelsWithPricing,
162
- openRouterModels,
163
- openRouterModelsLoading,
164
- openRouterModelsUpdatedAt,
165
- providerMenuItems,
166
- refreshOpenRouterModels,
167
- } = appModel;
168
-
169
- const { addToHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
170
-
171
- const {
172
- daemonState,
173
- conversationHistory,
174
- currentTranscription,
175
- currentResponse,
176
- currentContentBlocks,
177
- error,
178
- sessionUsage,
179
- modelMetadata,
180
- avatarRef,
181
- currentUserInputRef,
182
- hydrateConversationHistory,
183
- setCurrentTranscription,
184
- setCurrentResponse,
185
- clearCurrentContentBlocks,
186
- resetSessionUsage,
187
- setSessionUsage,
188
- applyAvatarForState,
189
- } = useDaemonEvents({
190
- currentModelId,
191
- preferencesLoaded,
192
- setReasoningQueue,
193
- setFullReasoning,
194
- clearReasoningState,
195
- clearReasoningTicker,
196
- fullReasoningRef,
197
- sessionId: currentSessionId,
198
- sessionIdRef: currentSessionIdRef,
199
- ensureSessionId,
200
- addToHistory,
201
- onFirstMessage: handleFirstMessage,
202
- });
203
-
204
- const {
205
- setTypingInput,
206
- typingTextareaRef,
207
- handleTypingContentChange,
208
- handleTypingSubmit,
209
- prefillTypingInput,
210
- handleHistoryUp,
211
- handleHistoryDown,
212
- } = useTypingMode({
213
- daemonState,
214
- currentUserInputRef,
215
- setCurrentTranscription,
216
- onTypingActivity: useCallback(() => {
217
- avatarRef.current?.triggerTypingPulse();
218
- }, [avatarRef]),
219
- navigateUp,
220
- navigateDown,
221
- resetNavigation,
222
- });
223
-
224
- const { responseElapsedMs } = useResponseTimer({ daemonState });
225
-
226
- const [loadedPreferences, setLoadedPreferences] = useState<AppPreferences | null>(null);
227
- const [onboardingStep, setOnboardingStep] = useState<OnboardingStep>("intro");
228
- const [devices, setDevices] = useState<AudioDevice[]>([]);
229
- const [currentDevice, setCurrentDevice] = useState<string | undefined>(undefined);
230
- const [currentOutputDevice, setCurrentOutputDevice] = useState<string | undefined>(undefined);
231
- const [resetNotification, setResetNotification] = useState<string>("");
232
- const [apiKeyMissingError, setApiKeyMissingError] = useState<string>("");
233
- const [escPendingCancel, setEscPendingCancel] = useState(false);
234
- const [deviceLoadTimedOut, setDeviceLoadTimedOut] = useState(false);
235
- const soxAvailable = useMemo(() => isSoxAvailable(), []);
236
- const soxInstallHint = useMemo(() => getSoxInstallHint(), []);
237
- const apiKeyTextareaRef = useRef<TextareaRenderable | null>(null);
238
- const conversationScrollRef = useRef<ScrollBoxRenderable | null>(null);
239
-
240
- const manager = getDaemonManager();
241
- const supportsReasoning = modelMetadata?.supportsReasoning ?? false;
242
-
243
- useEffect(() => {
244
- const handleTranscriptionReady = (text: string) => {
245
- prefillTypingInput(text);
246
- };
247
- daemonEvents.on("transcriptionReady", handleTranscriptionReady);
248
- return () => {
249
- daemonEvents.off("transcriptionReady", handleTranscriptionReady);
250
- };
251
- }, [manager, prefillTypingInput]);
252
-
253
- useEffect(() => {
254
- manager.setEnsureSessionId(() => ensureSessionId());
255
- return () => manager.setEnsureSessionId(null);
256
- }, [manager, ensureSessionId]);
257
-
258
- const { persistPreferences } = useAppPreferencesBootstrap({
259
- manager,
260
- setCurrentModelId,
261
- setCurrentOpenRouterProviderTag,
262
- setCurrentDevice,
263
- setCurrentOutputDevice,
264
- setInteractionMode,
265
- setVoiceInteractionType,
266
- setSpeechSpeed,
267
- setReasoningEffort,
268
- setBashApprovalLevel,
269
- setShowFullReasoning,
270
- setShowToolOutput,
271
- setLoadedPreferences,
272
- setOnboardingActive,
273
- setOnboardingStep,
274
- setPreferencesLoaded,
275
- });
276
-
277
- useAppAudioDevicesLoader({
278
- preferencesLoaded,
279
- showDeviceMenu,
280
- onboardingStep,
281
- setDevices,
282
- setCurrentDevice,
283
- setDeviceLoadTimedOut,
284
- });
285
-
286
- const conversationManager = useConversationManager({
287
- conversationHistory,
288
- sessionUsage,
289
- currentSessionId,
290
- ensureSessionId,
291
- setCurrentSessionIdSafe,
292
- currentSessionIdRef,
293
- setSessions,
294
- hydrateConversationHistory,
295
- setCurrentTranscription,
296
- setCurrentResponse,
297
- clearCurrentContentBlocks,
298
- clearReasoningState,
299
- resetSessionUsage,
300
- setSessionUsage,
301
- currentUserInputRef,
302
- });
303
- const { clearConversationState, loadSessionById, startNewSession, undoLastTurn } = conversationManager;
304
- const startNewSessionAndReset = useCallback(() => {
305
- startNewSession();
306
- applyAvatarForState(DaemonState.IDLE);
307
- }, [startNewSession, applyAvatarForState]);
308
-
309
- const {
310
- handleDeviceSelect,
311
- handleOutputDeviceSelect,
312
- handleModelSelect,
313
- handleProviderSelect,
314
- toggleInteractionMode,
315
- completeOnboarding,
316
- handleApiKeySubmit,
317
- } = useAppCallbacks({
318
- currentModelId,
319
- setCurrentModelId,
320
- setCurrentDevice,
321
- setCurrentOutputDevice,
322
- setCurrentOpenRouterProviderTag,
323
- setInteractionMode,
324
- setVoiceInteractionType,
325
- setSpeechSpeed,
326
- setReasoningEffort,
327
- persistPreferences,
328
- loadedPreferences,
329
- onboardingStep,
330
- setOnboardingStep,
331
- apiKeyTextareaRef,
332
- setShowDeviceMenu,
333
- setShowModelMenu,
334
- setShowProviderMenu,
335
- setShowSettingsMenu,
336
- setShowSessionMenu,
337
- setOnboardingActive,
338
- });
339
-
340
- const handleSessionSelect = useCallback(
341
- (selectedIdx: number) => {
342
- const item = sessionMenuItems[selectedIdx];
343
- if (!item) return;
344
- void loadSessionById(item.id);
345
- },
346
- [sessionMenuItems, loadSessionById]
347
- );
348
-
349
- const handleSessionDelete = useCallback(
350
- (selectedIdx: number) => {
351
- const item = sessionMenuItems[selectedIdx];
352
- if (!item) return;
353
-
354
- void (async () => {
355
- await deleteSession(item.id);
356
- setSessions((prev) => prev.filter((s) => s.id !== item.id));
357
-
358
- if (currentSessionIdRef.current === item.id) {
359
- clearConversationState();
360
- setCurrentSessionIdSafe(null);
361
- setResetNotification("SESSION DELETED");
362
- setTimeout(() => setResetNotification(""), 2000);
363
- }
364
- })();
365
- },
366
- [sessionMenuItems, setSessions, currentSessionIdRef, clearConversationState, setCurrentSessionIdSafe]
367
- );
368
-
369
- useEffect(() => {
370
- setGroundingSelectedIndex(0);
371
- }, [currentSessionId]);
372
-
373
- const keyboardActions = useMemo(
374
- () => ({
375
- setShowDeviceMenu,
376
- setShowSettingsMenu,
377
- setShowModelMenu,
378
- setShowProviderMenu,
379
- setShowSessionMenu,
380
- setShowHotkeysPane,
381
- setShowGroundingMenu,
382
- setTypingInput,
383
- setCurrentTranscription,
384
- setCurrentResponse,
385
- setApiKeyMissingError,
386
- setEscPendingCancel,
387
- setShowFullReasoning,
388
- setShowToolOutput,
389
- persistPreferences,
390
- clearReasoningState,
391
- currentUserInputRef,
392
- conversationScrollRef,
393
- startNewSession: startNewSessionAndReset,
394
- undoLastTurn,
395
- }),
396
- [
397
- setShowDeviceMenu,
398
- setShowSettingsMenu,
399
- setShowModelMenu,
400
- setShowProviderMenu,
401
- setShowSessionMenu,
402
- setShowHotkeysPane,
403
- setShowGroundingMenu,
404
- setTypingInput,
405
- setCurrentTranscription,
406
- setCurrentResponse,
407
- setApiKeyMissingError,
408
- setEscPendingCancel,
409
- setShowFullReasoning,
410
- setShowToolOutput,
411
- persistPreferences,
412
- clearReasoningState,
413
- currentUserInputRef,
414
- conversationScrollRef,
415
- startNewSessionAndReset,
416
- undoLastTurn,
417
- ]
418
- );
419
-
420
- const hasInteracted =
421
- conversationHistory.length > 0 || currentTranscription.length > 0 || currentContentBlocks.length > 0;
422
-
423
- useDaemonKeyboard(
424
- {
425
- isOverlayOpen:
426
- showDeviceMenu ||
427
- showSettingsMenu ||
428
- showModelMenu ||
429
- showProviderMenu ||
430
- showSessionMenu ||
431
- showHotkeysPane ||
432
- showGroundingMenu ||
433
- onboardingActive,
434
- escPendingCancel,
435
- hasInteracted,
436
- hasGrounding,
437
- showFullReasoning,
438
- showToolOutput,
439
- },
440
- keyboardActions
441
- );
442
-
443
- const displayState = useAppDisplayState({
444
- daemonState,
445
- currentContentBlocks,
446
- currentResponse,
447
- reasoningDisplay,
448
- reasoningQueue,
449
- responseElapsedMs,
450
- hasInteracted,
451
- currentModelId,
452
- modelMetadata,
453
- preferencesLoaded,
454
- currentSessionId,
455
- sessionMenuItems,
456
- terminalWidth: renderer.terminalWidth,
457
- terminalHeight: renderer.terminalHeight,
458
- });
459
-
460
- const {
461
- isToolCalling,
462
- isReasoning,
463
- statusText,
464
- statusColor,
465
- showWorkingSpinner,
466
- workingSpinnerLabel,
467
- modelName,
468
- sessionTitle,
469
- avatarWidth,
470
- avatarHeight,
471
- frostColor,
472
- isListening,
473
- isListeningDim,
474
- } = displayState;
475
-
476
- const statusBarHeight = hasInteracted ? (apiKeyMissingError ? 5 : 3) : 0;
477
-
478
- useEffect(() => {
479
- if (daemonState === DaemonState.IDLE) {
480
- setEscPendingCancel(false);
481
- }
482
- }, [daemonState]);
483
-
484
- const openGroundingSource = useCallback(
485
- (idx: number) => {
486
- if (!latestGroundingMap) return;
487
- const item = latestGroundingMap.items[idx];
488
- if (!item) return;
489
- const { source } = item;
490
- const url = source.textFragment
491
- ? buildTextFragmentUrl(source.url, { fragmentText: source.textFragment })
492
- : source.url;
493
- openUrlInBrowser(url);
494
- },
495
- [latestGroundingMap]
496
- );
497
-
498
- const groundingInitialIndex = latestGroundingMap
499
- ? Math.min(groundingSelectedIndex, Math.max(0, latestGroundingMap.items.length - 1))
500
- : 0;
501
-
502
- const conversationDisplayState: ConversationDisplayState = {
503
- conversationHistory,
504
- currentTranscription,
505
- currentResponse,
506
- currentContentBlocks,
507
- };
508
-
509
- const statusDisplayState: StatusDisplayState = {
510
- daemonState,
511
- statusText,
512
- statusColor,
513
- apiKeyMissingError,
514
- error,
515
- resetNotification,
516
- escPendingCancel,
517
- };
518
-
519
- const reasoningDisplayState: ReasoningDisplayState = {
520
- showFullReasoning,
521
- showToolOutput,
522
- reasoningQueue,
523
- reasoningDisplay,
524
- fullReasoning,
525
- };
526
-
527
- const progressDisplayState: ProgressDisplayState = {
528
- showWorkingSpinner,
529
- workingSpinnerLabel,
530
- isToolCalling,
531
- responseElapsedMs,
532
- };
533
-
534
- const typingInputState: TypingInputState = {
535
- typingTextareaRef,
536
- conversationScrollRef,
537
- onTypingContentChange: handleTypingContentChange,
538
- onTypingSubmit: handleTypingSubmit,
539
- onHistoryUp: handleHistoryUp,
540
- onHistoryDown: handleHistoryDown,
541
- };
542
-
543
- const appContextValue = useAppContextBuilder({
544
- menus: {
545
- showDeviceMenu,
546
- setShowDeviceMenu,
547
- showSettingsMenu,
548
- setShowSettingsMenu,
549
- showModelMenu,
550
- setShowModelMenu,
551
- showProviderMenu,
552
- setShowProviderMenu,
553
- showSessionMenu,
554
- setShowSessionMenu,
555
- showHotkeysPane,
556
- setShowHotkeysPane,
557
- showGroundingMenu,
558
- setShowGroundingMenu,
559
- },
560
- device: {
561
- devices,
562
- currentDevice,
563
- setCurrentDevice,
564
- currentOutputDevice,
565
- setCurrentOutputDevice,
566
- deviceLoadTimedOut,
567
- soxAvailable,
568
- soxInstallHint,
569
- },
570
- settings: {
571
- interactionMode,
572
- voiceInteractionType,
573
- speechSpeed,
574
- reasoningEffort,
575
- bashApprovalLevel,
576
- supportsReasoning,
577
- canEnableVoiceOutput,
578
- showFullReasoning,
579
- setShowFullReasoning,
580
- showToolOutput,
581
- setShowToolOutput,
582
- setBashApprovalLevel,
583
- persistPreferences,
584
- },
585
- model: {
586
- curatedModels: modelsWithPricing,
587
- openRouterModels,
588
- openRouterModelsLoading,
589
- openRouterModelsUpdatedAt,
590
- currentModelId,
591
- setCurrentModelId,
592
- providerMenuItems,
593
- currentOpenRouterProviderTag,
594
- },
595
- session: {
596
- sessionMenuItems,
597
- currentSessionId,
598
- },
599
- grounding: {
600
- latestGroundingMap,
601
- groundingInitialIndex,
602
- groundingSelectedIndex,
603
- setGroundingSelectedIndex,
604
- },
605
- onboarding: {
606
- onboardingActive,
607
- onboardingStep,
608
- setOnboardingStep,
609
- onboardingPreferences: loadedPreferences,
610
- apiKeyTextareaRef,
611
- },
612
- deviceCallbacks: {
613
- onDeviceSelect: handleDeviceSelect,
614
- onOutputDeviceSelect: handleOutputDeviceSelect,
615
- },
616
- settingsCallbacks: {
617
- onToggleInteractionMode: toggleInteractionMode,
618
- onSetVoiceInteractionType: setVoiceInteractionType,
619
- onSetSpeechSpeed: setSpeechSpeed,
620
- onSetReasoningEffort: setReasoningEffort,
621
- onSetBashApprovalLevel: setBashApprovalLevel,
622
- },
623
- modelCallbacks: {
624
- onModelSelect: handleModelSelect,
625
- onModelRefresh: refreshOpenRouterModels,
626
- onProviderSelect: handleProviderSelect,
627
- },
628
- sessionCallbacks: {
629
- onSessionSelect: handleSessionSelect,
630
- onSessionDelete: handleSessionDelete,
631
- },
632
- groundingCallbacks: {
633
- onGroundingSelect: (index: number) => {
634
- setGroundingSelectedIndex(index);
635
- openGroundingSource(index);
636
- },
637
- onGroundingIndexChange: setGroundingSelectedIndex,
638
- },
639
- onboardingCallbacks: {
640
- onKeySubmit: handleApiKeySubmit,
641
- completeOnboarding,
642
- },
643
- });
46
+ const controller = useAppController({ initialStatusTop: INITIAL_STATUS_TOP });
644
47
 
645
48
  return (
646
49
  <ToolApprovalProvider>
@@ -649,7 +52,7 @@ export function App() {
649
52
  width="100%"
650
53
  height="100%"
651
54
  backgroundColor={COLORS.BACKGROUND}
652
- onMouseUp={handleCopyOnSelectMouseUp}
55
+ onMouseUp={controller.handleCopyOnSelectMouseUp}
653
56
  >
654
57
  <>
655
58
  <Toaster
@@ -661,18 +64,18 @@ export function App() {
661
64
  />
662
65
 
663
66
  <AvatarLayer
664
- avatarRef={avatarRef}
665
- daemonState={daemonState}
666
- applyAvatarForState={applyAvatarForState}
667
- width={avatarWidth}
668
- height={avatarHeight}
669
- zIndex={isListening && hasInteracted ? 2 : 0}
67
+ avatarRef={controller.avatarLayerProps.avatarRef}
68
+ daemonState={controller.avatarLayerProps.daemonState}
69
+ applyAvatarForState={controller.avatarLayerProps.applyAvatarForState}
70
+ width={controller.avatarLayerProps.width}
71
+ height={controller.avatarLayerProps.height}
72
+ zIndex={controller.avatarLayerProps.zIndex}
670
73
  />
671
74
 
672
- {isListeningDim ? (
75
+ {controller.isListeningDim ? (
673
76
  <box
674
77
  position="absolute"
675
- top={statusBarHeight}
78
+ top={controller.listeningDimTop}
676
79
  left={0}
677
80
  width="100%"
678
81
  height="100%"
@@ -681,28 +84,17 @@ export function App() {
681
84
  />
682
85
  ) : null}
683
86
 
684
- <box flexDirection="column" width="100%" height="100%" zIndex={isListening ? 0 : 1}>
685
- <ConversationPane
686
- conversation={conversationDisplayState}
687
- status={statusDisplayState}
688
- reasoning={reasoningDisplayState}
689
- progress={progressDisplayState}
690
- typing={typingInputState}
691
- sessionUsage={sessionUsage}
692
- modelMetadata={modelMetadata}
693
- hasInteracted={hasInteracted}
694
- frostColor={frostColor}
695
- initialStatusTop={INITIAL_STATUS_TOP}
696
- hasGrounding={hasGrounding}
697
- groundingCount={latestGroundingMap?.items.length}
698
- modelName={modelName}
699
- sessionTitle={sessionTitle}
700
- isVoiceOutputEnabled={interactionMode === "voice"}
701
- />
87
+ <box
88
+ flexDirection="column"
89
+ width="100%"
90
+ height="100%"
91
+ zIndex={controller.conversationContainerZIndex}
92
+ >
93
+ <ConversationPane {...controller.conversationPaneProps} />
702
94
  </box>
703
95
 
704
- <AppProvider value={appContextValue}>
705
- <AppOverlays />
96
+ <AppProvider value={controller.appContextValue}>
97
+ <AppOverlays {...controller.overlaysProps} />
706
98
  </AppProvider>
707
99
  </>
708
100
  </box>