@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,711 @@
1
+ import { Toaster } from "@opentui-ui/toast/react";
2
+ import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
3
+ import { extend, useRenderer } from "@opentui/react";
4
+ import "opentui-spinner/react";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
7
+ 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";
26
+ 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
+ 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
+ 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
+ import { AppOverlays } from "./components/AppOverlays";
40
+ 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";
49
+
50
+ const INITIAL_STATUS_TOP = "70%";
51
+
52
+ const TOAST_OPTIONS = {
53
+ style: {
54
+ border: true,
55
+ borderStyle: "single",
56
+ borderColor: COLORS.REASONING,
57
+ backgroundColor: "#0a0a0f",
58
+ foregroundColor: "#e5e7eb",
59
+ mutedColor: "#9ca3af",
60
+ paddingX: 1,
61
+ paddingY: 0,
62
+ minHeight: 3,
63
+ },
64
+ success: { style: { borderColor: COLORS.DAEMON_TEXT } },
65
+ error: { style: { borderColor: COLORS.ERROR } },
66
+ warning: { style: { borderColor: "#fbbf24" } },
67
+ info: { style: { borderColor: COLORS.REASONING } },
68
+ loading: { style: { borderColor: COLORS.REASONING_DIM } },
69
+ } as const;
70
+
71
+ declare module "@opentui/react" {
72
+ interface OpenTUIComponents {
73
+ "daemon-avatar": typeof DaemonAvatarRenderable;
74
+ }
75
+ }
76
+
77
+ extend({
78
+ "daemon-avatar": DaemonAvatarRenderable,
79
+ });
80
+
81
+ export function App() {
82
+ const renderer = useRenderer();
83
+
84
+ usePlaywrightNotification();
85
+ useVoiceDependenciesNotification();
86
+ const { handleCopyOnSelectMouseUp } = useCopyOnSelect();
87
+
88
+ const {
89
+ reasoningQueue,
90
+ reasoningDisplay,
91
+ fullReasoning,
92
+ setReasoningQueue,
93
+ setFullReasoning,
94
+ fullReasoningRef,
95
+ clearReasoningState,
96
+ clearReasoningTicker,
97
+ } = useReasoningAnimation();
98
+
99
+ const [preferencesLoaded, setPreferencesLoaded] = useState(false);
100
+
101
+ const menus = useAppMenus();
102
+ const {
103
+ showDeviceMenu,
104
+ setShowDeviceMenu,
105
+ showSettingsMenu,
106
+ setShowSettingsMenu,
107
+ showModelMenu,
108
+ setShowModelMenu,
109
+ showProviderMenu,
110
+ setShowProviderMenu,
111
+ showSessionMenu,
112
+ setShowSessionMenu,
113
+ showHotkeysPane,
114
+ setShowHotkeysPane,
115
+ showGroundingMenu,
116
+ setShowGroundingMenu,
117
+ } = menus;
118
+
119
+ const {
120
+ currentSessionId,
121
+ setCurrentSessionIdSafe,
122
+ currentSessionIdRef,
123
+ ensureSessionId,
124
+ setSessions,
125
+ sessionMenuItems,
126
+ handleFirstMessage,
127
+ } = useAppSessions({ showSessionMenu });
128
+
129
+ const { latestGroundingMap, hasGrounding } = useGrounding(currentSessionId);
130
+ const [groundingSelectedIndex, setGroundingSelectedIndex] = useState(0);
131
+
132
+ const appSettings = useAppSettings();
133
+ const {
134
+ interactionMode,
135
+ setInteractionMode,
136
+ voiceInteractionType,
137
+ setVoiceInteractionType,
138
+ speechSpeed,
139
+ setSpeechSpeed,
140
+ reasoningEffort,
141
+ setReasoningEffort,
142
+ bashApprovalLevel,
143
+ setBashApprovalLevel,
144
+ showFullReasoning,
145
+ setShowFullReasoning,
146
+ showToolOutput,
147
+ setShowToolOutput,
148
+ canEnableVoiceOutput,
149
+ } = appSettings;
150
+
151
+ const appModel = useAppModel({
152
+ preferencesLoaded,
153
+ showProviderMenu,
154
+ });
155
+ const {
156
+ currentModelId,
157
+ setCurrentModelId,
158
+ currentOpenRouterProviderTag,
159
+ setCurrentOpenRouterProviderTag,
160
+ modelsWithPricing,
161
+ openRouterModels,
162
+ openRouterModelsLoading,
163
+ openRouterModelsUpdatedAt,
164
+ providerMenuItems,
165
+ refreshOpenRouterModels,
166
+ } = appModel;
167
+
168
+ const { addToHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
169
+
170
+ const {
171
+ daemonState,
172
+ conversationHistory,
173
+ currentTranscription,
174
+ currentResponse,
175
+ currentContentBlocks,
176
+ error,
177
+ sessionUsage,
178
+ modelMetadata,
179
+ avatarRef,
180
+ currentUserInputRef,
181
+ hydrateConversationHistory,
182
+ setCurrentTranscription,
183
+ setCurrentResponse,
184
+ clearCurrentContentBlocks,
185
+ resetSessionUsage,
186
+ setSessionUsage,
187
+ applyAvatarForState,
188
+ } = useDaemonEvents({
189
+ currentModelId,
190
+ preferencesLoaded,
191
+ setReasoningQueue,
192
+ setFullReasoning,
193
+ clearReasoningState,
194
+ clearReasoningTicker,
195
+ fullReasoningRef,
196
+ sessionId: currentSessionId,
197
+ sessionIdRef: currentSessionIdRef,
198
+ ensureSessionId,
199
+ addToHistory,
200
+ onFirstMessage: handleFirstMessage,
201
+ });
202
+
203
+ const {
204
+ setTypingInput,
205
+ typingTextareaRef,
206
+ handleTypingContentChange,
207
+ handleTypingSubmit,
208
+ prefillTypingInput,
209
+ handleHistoryUp,
210
+ handleHistoryDown,
211
+ } = useTypingMode({
212
+ daemonState,
213
+ currentUserInputRef,
214
+ setCurrentTranscription,
215
+ onTypingActivity: useCallback(() => {
216
+ avatarRef.current?.triggerTypingPulse();
217
+ }, [avatarRef]),
218
+ navigateUp,
219
+ navigateDown,
220
+ resetNavigation,
221
+ });
222
+
223
+ const { responseElapsedMs } = useResponseTimer({ daemonState });
224
+
225
+ const [loadedPreferences, setLoadedPreferences] = useState<AppPreferences | null>(null);
226
+ const [onboardingActive, setOnboardingActive] = useState(false);
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
+ });
644
+
645
+ return (
646
+ <ToolApprovalProvider>
647
+ <box
648
+ flexDirection="column"
649
+ width="100%"
650
+ height="100%"
651
+ backgroundColor={COLORS.BACKGROUND}
652
+ onMouseUp={handleCopyOnSelectMouseUp}
653
+ >
654
+ <>
655
+ <Toaster
656
+ position="top-right"
657
+ stackingMode="stack"
658
+ visibleToasts={2}
659
+ maxWidth={60}
660
+ toastOptions={TOAST_OPTIONS}
661
+ />
662
+
663
+ <AvatarLayer
664
+ avatarRef={avatarRef}
665
+ daemonState={daemonState}
666
+ applyAvatarForState={applyAvatarForState}
667
+ width={avatarWidth}
668
+ height={avatarHeight}
669
+ zIndex={isListening && hasInteracted ? 2 : 0}
670
+ />
671
+
672
+ {isListeningDim ? (
673
+ <box
674
+ position="absolute"
675
+ top={statusBarHeight}
676
+ left={0}
677
+ width="100%"
678
+ height="100%"
679
+ backgroundColor={COLORS.LISTENING_DIM}
680
+ zIndex={1}
681
+ />
682
+ ) : null}
683
+
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
+ />
702
+ </box>
703
+
704
+ <AppProvider value={appContextValue}>
705
+ <AppOverlays />
706
+ </AppProvider>
707
+ </>
708
+ </box>
709
+ </ToolApprovalProvider>
710
+ );
711
+ }