@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,202 @@
1
+ import { useCallback } from "react";
2
+ import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
3
+ import { setAudioDevice } from "../voice/audio-recorder";
4
+ import { getDaemonManager } from "../state/daemon-state";
5
+ import type {
6
+ AppPreferences,
7
+ AudioDevice,
8
+ ModelOption,
9
+ OnboardingStep,
10
+ ReasoningEffort,
11
+ SpeechSpeed,
12
+ VoiceInteractionType,
13
+ } from "../types";
14
+ import { determineNextStep } from "./keyboard-handlers";
15
+
16
+ export interface UseAppCallbacksParams {
17
+ currentModelId: string;
18
+ setCurrentModelId: (modelId: string) => void;
19
+ setCurrentDevice: (deviceName: string | undefined) => void;
20
+ setCurrentOutputDevice: (deviceName: string | undefined) => void;
21
+ setCurrentOpenRouterProviderTag: (tag: string | undefined) => void;
22
+ setInteractionMode: (mode: "text" | "voice") => void;
23
+ setVoiceInteractionType: (type: VoiceInteractionType) => void;
24
+ setSpeechSpeed: (speed: SpeechSpeed) => void;
25
+ setReasoningEffort: (effort: ReasoningEffort) => void;
26
+ persistPreferences: (updates: Partial<AppPreferences>) => void;
27
+ loadedPreferences: AppPreferences | null;
28
+ onboardingStep: OnboardingStep;
29
+ setOnboardingStep: (step: OnboardingStep) => void;
30
+ apiKeyTextareaRef: React.RefObject<{ plainText?: string; setText: (v: string) => void } | null>;
31
+ setShowDeviceMenu: (show: boolean) => void;
32
+ setShowModelMenu: (show: boolean) => void;
33
+ setShowProviderMenu: (show: boolean) => void;
34
+ setShowSettingsMenu: (show: boolean) => void;
35
+ setShowSessionMenu: (show: boolean) => void;
36
+ setOnboardingActive: (active: boolean) => void;
37
+ }
38
+
39
+ export interface UseAppCallbacksReturn {
40
+ handleDeviceSelect: (device: AudioDevice) => void;
41
+ handleOutputDeviceSelect: (device: AudioDevice) => void;
42
+ handleModelSelect: (model: ModelOption) => void;
43
+ handleProviderSelect: (providerTag: string | undefined) => void;
44
+ toggleInteractionMode: () => void;
45
+ completeOnboarding: () => void;
46
+ handleApiKeySubmit: () => void;
47
+ }
48
+
49
+ export function useAppCallbacks(params: UseAppCallbacksParams): UseAppCallbacksReturn {
50
+ const {
51
+ currentModelId,
52
+ setCurrentModelId,
53
+ setCurrentDevice,
54
+ setCurrentOutputDevice,
55
+ setCurrentOpenRouterProviderTag,
56
+ setInteractionMode,
57
+ setVoiceInteractionType,
58
+ setSpeechSpeed,
59
+ setReasoningEffort,
60
+ persistPreferences,
61
+ loadedPreferences,
62
+ onboardingStep,
63
+ setOnboardingStep,
64
+ apiKeyTextareaRef,
65
+ setShowDeviceMenu,
66
+ setShowModelMenu,
67
+ setShowProviderMenu,
68
+ setShowSettingsMenu,
69
+ setShowSessionMenu,
70
+ setOnboardingActive,
71
+ } = params;
72
+
73
+ const handleDeviceSelect = useCallback(
74
+ (device: AudioDevice) => {
75
+ setAudioDevice(device.name);
76
+ setCurrentDevice(device.name);
77
+ persistPreferences({ audioDeviceName: device.name });
78
+ },
79
+ [setCurrentDevice, persistPreferences]
80
+ );
81
+
82
+ const handleOutputDeviceSelect = useCallback(
83
+ (device: AudioDevice) => {
84
+ const manager = getDaemonManager();
85
+ manager.outputDeviceName = device.name;
86
+ setCurrentOutputDevice(device.name);
87
+ persistPreferences({ audioOutputDeviceName: device.name });
88
+ },
89
+ [setCurrentOutputDevice, persistPreferences]
90
+ );
91
+
92
+ const handleModelSelect = useCallback(
93
+ (model: ModelOption) => {
94
+ if (model.id !== currentModelId) {
95
+ setResponseModel(model.id);
96
+ setOpenRouterProviderTag(undefined);
97
+ setCurrentModelId(model.id);
98
+ setCurrentOpenRouterProviderTag(undefined);
99
+ persistPreferences({
100
+ modelId: model.id,
101
+ openRouterProviderTag: undefined,
102
+ });
103
+ }
104
+ },
105
+ [currentModelId, setCurrentModelId, setCurrentOpenRouterProviderTag, persistPreferences]
106
+ );
107
+
108
+ const handleProviderSelect = useCallback(
109
+ (providerTag: string | undefined) => {
110
+ setOpenRouterProviderTag(providerTag);
111
+ setCurrentOpenRouterProviderTag(providerTag);
112
+ persistPreferences({ openRouterProviderTag: providerTag });
113
+ },
114
+ [setCurrentOpenRouterProviderTag, persistPreferences]
115
+ );
116
+
117
+ const toggleInteractionMode = useCallback(() => {
118
+ const mgr = getDaemonManager();
119
+ const newMode = mgr.interactionMode === "text" ? "voice" : "text";
120
+ if (newMode === "voice" && !process.env.OPENAI_API_KEY) {
121
+ return;
122
+ }
123
+ mgr.interactionMode = newMode;
124
+ setInteractionMode(newMode);
125
+ }, [setInteractionMode]);
126
+
127
+ const completeOnboarding = useCallback(() => {
128
+ persistPreferences({ onboardingCompleted: true });
129
+ setShowDeviceMenu(false);
130
+ setShowModelMenu(false);
131
+ setShowProviderMenu(false);
132
+ setShowSettingsMenu(false);
133
+ setShowSessionMenu(false);
134
+ setOnboardingActive(false);
135
+ setOnboardingStep("complete");
136
+ }, [
137
+ persistPreferences,
138
+ setShowDeviceMenu,
139
+ setShowModelMenu,
140
+ setShowProviderMenu,
141
+ setShowSettingsMenu,
142
+ setShowSessionMenu,
143
+ setOnboardingActive,
144
+ setOnboardingStep,
145
+ ]);
146
+
147
+ const handleApiKeySubmit = useCallback(() => {
148
+ const key = (apiKeyTextareaRef.current?.plainText ?? "").trim();
149
+ if (!key) return;
150
+
151
+ if (onboardingStep === "openrouter_key") {
152
+ process.env.OPENROUTER_API_KEY = key;
153
+ persistPreferences({ openRouterApiKey: key });
154
+ const nextStep = determineNextStep("openrouter_key", loadedPreferences);
155
+ if (nextStep === "complete") {
156
+ persistPreferences({ onboardingCompleted: true });
157
+ completeOnboarding();
158
+ } else {
159
+ setOnboardingStep(nextStep);
160
+ }
161
+ } else if (onboardingStep === "openai_key") {
162
+ process.env.OPENAI_API_KEY = key;
163
+ persistPreferences({ openAiApiKey: key });
164
+ const nextStep = determineNextStep("openai_key", loadedPreferences);
165
+ if (nextStep === "complete") {
166
+ persistPreferences({ onboardingCompleted: true });
167
+ completeOnboarding();
168
+ } else {
169
+ setOnboardingStep(nextStep);
170
+ }
171
+ } else if (onboardingStep === "exa_key") {
172
+ process.env.EXA_API_KEY = key;
173
+ persistPreferences({ exaApiKey: key });
174
+ const nextStep = determineNextStep("exa_key", loadedPreferences);
175
+ if (nextStep === "complete") {
176
+ persistPreferences({ onboardingCompleted: true });
177
+ completeOnboarding();
178
+ } else {
179
+ setOnboardingStep(nextStep);
180
+ }
181
+ }
182
+
183
+ apiKeyTextareaRef.current?.setText("");
184
+ }, [
185
+ onboardingStep,
186
+ persistPreferences,
187
+ loadedPreferences,
188
+ completeOnboarding,
189
+ setOnboardingStep,
190
+ apiKeyTextareaRef,
191
+ ]);
192
+
193
+ return {
194
+ handleDeviceSelect,
195
+ handleOutputDeviceSelect,
196
+ handleModelSelect,
197
+ handleProviderSelect,
198
+ toggleInteractionMode,
199
+ completeOnboarding,
200
+ handleApiKeySubmit,
201
+ };
202
+ }
@@ -0,0 +1,159 @@
1
+ import type { TextareaRenderable } from "@opentui/core";
2
+ import { type MutableRefObject, useMemo } from "react";
3
+ import type { ProviderMenuItem } from "../components/ProviderMenu";
4
+ import type {
5
+ AppContextValue,
6
+ DeviceCallbacks,
7
+ GroundingCallbacks,
8
+ ModelCallbacks,
9
+ OnboardingCallbacks,
10
+ SessionCallbacks,
11
+ SettingsCallbacks,
12
+ } from "../state/app-context";
13
+ import type {
14
+ AppPreferences,
15
+ AudioDevice,
16
+ BashApprovalLevel,
17
+ GroundingMap,
18
+ ModelOption,
19
+ OnboardingStep,
20
+ ReasoningEffort,
21
+ SessionInfo,
22
+ SpeechSpeed,
23
+ VoiceInteractionType,
24
+ } from "../types";
25
+
26
+ export interface UseAppContextBuilderParams {
27
+ menus: {
28
+ showDeviceMenu: boolean;
29
+ setShowDeviceMenu: React.Dispatch<React.SetStateAction<boolean>>;
30
+ showSettingsMenu: boolean;
31
+ setShowSettingsMenu: React.Dispatch<React.SetStateAction<boolean>>;
32
+ showModelMenu: boolean;
33
+ setShowModelMenu: React.Dispatch<React.SetStateAction<boolean>>;
34
+ showProviderMenu: boolean;
35
+ setShowProviderMenu: React.Dispatch<React.SetStateAction<boolean>>;
36
+ showSessionMenu: boolean;
37
+ setShowSessionMenu: React.Dispatch<React.SetStateAction<boolean>>;
38
+ showHotkeysPane: boolean;
39
+ setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
40
+ showGroundingMenu: boolean;
41
+ setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
42
+ };
43
+
44
+ device: {
45
+ devices: AudioDevice[];
46
+ currentDevice: string | undefined;
47
+ setCurrentDevice: (deviceName: string | undefined) => void;
48
+ currentOutputDevice: string | undefined;
49
+ setCurrentOutputDevice: (deviceName: string | undefined) => void;
50
+ deviceLoadTimedOut: boolean;
51
+ soxAvailable: boolean;
52
+ soxInstallHint: string;
53
+ };
54
+
55
+ settings: {
56
+ interactionMode: "text" | "voice";
57
+ voiceInteractionType: VoiceInteractionType;
58
+ speechSpeed: SpeechSpeed;
59
+ reasoningEffort: ReasoningEffort;
60
+ bashApprovalLevel: BashApprovalLevel;
61
+ supportsReasoning: boolean;
62
+ canEnableVoiceOutput: boolean;
63
+ showFullReasoning: boolean;
64
+ setShowFullReasoning: (show: boolean) => void;
65
+ showToolOutput: boolean;
66
+ setShowToolOutput: (show: boolean) => void;
67
+ setBashApprovalLevel: (level: BashApprovalLevel) => void;
68
+ persistPreferences: (updates: Partial<AppPreferences>) => void;
69
+ };
70
+
71
+ model: {
72
+ curatedModels: ModelOption[];
73
+ openRouterModels: ModelOption[];
74
+ openRouterModelsLoading: boolean;
75
+ openRouterModelsUpdatedAt: number | null;
76
+ currentModelId: string;
77
+ setCurrentModelId: (modelId: string) => void;
78
+ providerMenuItems: ProviderMenuItem[];
79
+ currentOpenRouterProviderTag: string | undefined;
80
+ };
81
+
82
+ session: {
83
+ sessionMenuItems: Array<SessionInfo & { isNew: boolean }>;
84
+ currentSessionId: string | null;
85
+ };
86
+
87
+ grounding: {
88
+ latestGroundingMap: GroundingMap | null;
89
+ groundingInitialIndex: number;
90
+ groundingSelectedIndex: number;
91
+ setGroundingSelectedIndex: (index: number) => void;
92
+ };
93
+
94
+ onboarding: {
95
+ onboardingActive: boolean;
96
+ onboardingStep: OnboardingStep;
97
+ setOnboardingStep: (step: OnboardingStep) => void;
98
+ onboardingPreferences: AppPreferences | null;
99
+ apiKeyTextareaRef: MutableRefObject<TextareaRenderable | null>;
100
+ };
101
+
102
+ deviceCallbacks: DeviceCallbacks;
103
+ settingsCallbacks: SettingsCallbacks;
104
+ modelCallbacks: ModelCallbacks;
105
+ sessionCallbacks: SessionCallbacks;
106
+ groundingCallbacks: GroundingCallbacks;
107
+ onboardingCallbacks: OnboardingCallbacks;
108
+ }
109
+
110
+ export function useAppContextBuilder(params: UseAppContextBuilderParams): AppContextValue {
111
+ const {
112
+ menus,
113
+ device,
114
+ settings,
115
+ model,
116
+ session,
117
+ grounding,
118
+ onboarding,
119
+ deviceCallbacks,
120
+ settingsCallbacks,
121
+ modelCallbacks,
122
+ sessionCallbacks,
123
+ groundingCallbacks,
124
+ onboardingCallbacks,
125
+ } = params;
126
+
127
+ return useMemo(
128
+ (): AppContextValue => ({
129
+ menus,
130
+ device,
131
+ settings,
132
+ model,
133
+ session,
134
+ grounding,
135
+ onboarding,
136
+ deviceCallbacks,
137
+ settingsCallbacks,
138
+ modelCallbacks,
139
+ sessionCallbacks,
140
+ groundingCallbacks,
141
+ onboardingCallbacks,
142
+ }),
143
+ [
144
+ menus,
145
+ device,
146
+ settings,
147
+ model,
148
+ session,
149
+ grounding,
150
+ onboarding,
151
+ deviceCallbacks,
152
+ settingsCallbacks,
153
+ modelCallbacks,
154
+ sessionCallbacks,
155
+ groundingCallbacks,
156
+ onboardingCallbacks,
157
+ ]
158
+ );
159
+ }
@@ -0,0 +1,162 @@
1
+ import { useMemo } from "react";
2
+ import { DaemonState } from "../types";
3
+ import type { ContentBlock, SessionInfo } from "../types";
4
+ import type { ModelMetadata } from "../utils/model-metadata";
5
+ import { COLORS, STATE_COLOR_HEX, STATUS_TEXT } from "../ui/constants";
6
+ import { formatElapsedTime } from "../utils/formatters";
7
+
8
+ export interface UseAppDisplayStateParams {
9
+ daemonState: DaemonState;
10
+ currentContentBlocks: ContentBlock[];
11
+ currentResponse: string;
12
+ reasoningDisplay: string;
13
+ reasoningQueue: string;
14
+ responseElapsedMs: number;
15
+ hasInteracted: boolean;
16
+
17
+ currentModelId: string;
18
+ modelMetadata: ModelMetadata | null;
19
+ preferencesLoaded: boolean;
20
+
21
+ currentSessionId: string | null;
22
+ sessionMenuItems: Array<SessionInfo & { isNew: boolean }>;
23
+
24
+ terminalWidth: number;
25
+ terminalHeight: number;
26
+ }
27
+
28
+ export interface UseAppDisplayStateReturn {
29
+ isToolCalling: boolean;
30
+ isReasoning: boolean;
31
+ statusText: string;
32
+ statusColor: string;
33
+ showWorkingSpinner: boolean;
34
+ workingSpinnerLabel: string;
35
+ modelName: string | undefined;
36
+ sessionTitle: string | undefined;
37
+ avatarWidth: number;
38
+ avatarHeight: number;
39
+ frostColor: string;
40
+ isListening: boolean;
41
+ isListeningDim: boolean;
42
+ }
43
+
44
+ const AVATAR_WIDTH_PERCENT = 0.8;
45
+ const AVATAR_HEIGHT_PERCENT = 0.8;
46
+ const AVATAR_MIN_WIDTH = 80;
47
+ const AVATAR_MAX_WIDTH = 500;
48
+ const AVATAR_MIN_HEIGHT = 40;
49
+ const AVATAR_MAX_HEIGHT = 300;
50
+
51
+ export function useAppDisplayState(params: UseAppDisplayStateParams): UseAppDisplayStateReturn {
52
+ const {
53
+ daemonState,
54
+ currentContentBlocks,
55
+ currentResponse,
56
+ reasoningDisplay,
57
+ reasoningQueue,
58
+ responseElapsedMs,
59
+ hasInteracted,
60
+ currentModelId,
61
+ modelMetadata,
62
+ preferencesLoaded,
63
+ currentSessionId,
64
+ sessionMenuItems,
65
+ terminalWidth,
66
+ terminalHeight,
67
+ } = params;
68
+
69
+ const isToolCalling = useMemo(() => {
70
+ if (daemonState !== DaemonState.RESPONDING) return false;
71
+ return currentContentBlocks.some((b) => b.type === "tool" && b.call.status === "running");
72
+ }, [daemonState, currentContentBlocks]);
73
+
74
+ const isReasoning =
75
+ daemonState === DaemonState.RESPONDING &&
76
+ !isToolCalling &&
77
+ (!currentResponse || !!reasoningDisplay || !!reasoningQueue);
78
+
79
+ const statusText = useMemo(() => {
80
+ if (daemonState === DaemonState.RESPONDING) {
81
+ if (isToolCalling) {
82
+ return "DAEMON INVOKES TOOL... · ESC cancel · T reasoning";
83
+ }
84
+ return isReasoning
85
+ ? "DAEMON REASONING... · ESC cancel · T reasoning"
86
+ : "DAEMON SPEAKS... · ESC cancel · T reasoning";
87
+ }
88
+ let baseStatus = STATUS_TEXT[daemonState];
89
+ if (daemonState === DaemonState.IDLE) {
90
+ if (hasInteracted) {
91
+ baseStatus = "SPACE speak · SHIFT+TAB type · N new · ? hotkeys";
92
+ }
93
+ }
94
+ return baseStatus;
95
+ }, [daemonState, isToolCalling, isReasoning, hasInteracted]);
96
+
97
+ const statusColor = isToolCalling
98
+ ? COLORS.STATUS_RUNNING
99
+ : isReasoning
100
+ ? COLORS.REASONING
101
+ : STATE_COLOR_HEX[daemonState];
102
+
103
+ const showWorkingSpinner = hasInteracted && daemonState === DaemonState.RESPONDING;
104
+ const responseElapsedLabel = formatElapsedTime(responseElapsedMs);
105
+ const workingSpinnerLabel = isToolCalling
106
+ ? `CALLING TOOL... · ${responseElapsedLabel}`
107
+ : isReasoning
108
+ ? `REASONING... · ${responseElapsedLabel}`
109
+ : `RESPONDING... · ${responseElapsedLabel}`;
110
+
111
+ const modelName = useMemo(() => {
112
+ if (!preferencesLoaded) return undefined;
113
+ if (modelMetadata?.name && modelMetadata.id === currentModelId) {
114
+ return modelMetadata.name;
115
+ }
116
+ return undefined;
117
+ }, [modelMetadata, currentModelId, preferencesLoaded]);
118
+
119
+ const sessionTitle = useMemo(() => {
120
+ if (!currentSessionId) return undefined;
121
+ const session = sessionMenuItems.find((s) => s.id === currentSessionId);
122
+ return session?.title;
123
+ }, [currentSessionId, sessionMenuItems]);
124
+
125
+ const avatarWidth = useMemo(
126
+ () =>
127
+ Math.max(
128
+ AVATAR_MIN_WIDTH,
129
+ Math.min(AVATAR_MAX_WIDTH, Math.floor(terminalWidth * AVATAR_WIDTH_PERCENT))
130
+ ),
131
+ [terminalWidth]
132
+ );
133
+
134
+ const avatarHeight = useMemo(
135
+ () =>
136
+ Math.max(
137
+ AVATAR_MIN_HEIGHT,
138
+ Math.min(AVATAR_MAX_HEIGHT, Math.floor(terminalHeight * AVATAR_HEIGHT_PERCENT))
139
+ ),
140
+ [terminalHeight]
141
+ );
142
+
143
+ const frostColor = hasInteracted ? "#05050940" : COLORS.BACKGROUND;
144
+ const isListening = daemonState === DaemonState.LISTENING || daemonState === DaemonState.TRANSCRIBING;
145
+ const isListeningDim = isListening && hasInteracted;
146
+
147
+ return {
148
+ isToolCalling,
149
+ isReasoning,
150
+ statusText,
151
+ statusColor,
152
+ showWorkingSpinner,
153
+ workingSpinnerLabel,
154
+ modelName,
155
+ sessionTitle,
156
+ avatarWidth,
157
+ avatarHeight,
158
+ frostColor,
159
+ isListening,
160
+ isListeningDim,
161
+ };
162
+ }
@@ -0,0 +1,51 @@
1
+ import { useState } from "react";
2
+
3
+ export interface UseAppMenusReturn {
4
+ showDeviceMenu: boolean;
5
+ setShowDeviceMenu: React.Dispatch<React.SetStateAction<boolean>>;
6
+
7
+ showSettingsMenu: boolean;
8
+ setShowSettingsMenu: React.Dispatch<React.SetStateAction<boolean>>;
9
+
10
+ showModelMenu: boolean;
11
+ setShowModelMenu: React.Dispatch<React.SetStateAction<boolean>>;
12
+
13
+ showProviderMenu: boolean;
14
+ setShowProviderMenu: React.Dispatch<React.SetStateAction<boolean>>;
15
+
16
+ showSessionMenu: boolean;
17
+ setShowSessionMenu: React.Dispatch<React.SetStateAction<boolean>>;
18
+
19
+ showHotkeysPane: boolean;
20
+ setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
21
+
22
+ showGroundingMenu: boolean;
23
+ setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
24
+ }
25
+
26
+ export function useAppMenus(): UseAppMenusReturn {
27
+ const [showDeviceMenu, setShowDeviceMenu] = useState(false);
28
+ const [showSettingsMenu, setShowSettingsMenu] = useState(false);
29
+ const [showModelMenu, setShowModelMenu] = useState(false);
30
+ const [showProviderMenu, setShowProviderMenu] = useState(false);
31
+ const [showSessionMenu, setShowSessionMenu] = useState(false);
32
+ const [showHotkeysPane, setShowHotkeysPane] = useState(false);
33
+ const [showGroundingMenu, setShowGroundingMenu] = useState(false);
34
+
35
+ return {
36
+ showDeviceMenu,
37
+ setShowDeviceMenu,
38
+ showSettingsMenu,
39
+ setShowSettingsMenu,
40
+ showModelMenu,
41
+ setShowModelMenu,
42
+ showProviderMenu,
43
+ setShowProviderMenu,
44
+ showSessionMenu,
45
+ setShowSessionMenu,
46
+ showHotkeysPane,
47
+ setShowHotkeysPane,
48
+ showGroundingMenu,
49
+ setShowGroundingMenu,
50
+ };
51
+ }
@@ -0,0 +1,45 @@
1
+ import { useEffect } from "react";
2
+ import type { ModelOption } from "../types";
3
+ import { AVAILABLE_MODELS } from "../ai/model-config";
4
+ import { getModelsMetadata } from "../utils/model-metadata";
5
+
6
+ export interface UseAppModelPricingLoaderParams {
7
+ preferencesLoaded: boolean;
8
+ setModelsWithPricing: React.Dispatch<React.SetStateAction<ModelOption[]>>;
9
+ }
10
+
11
+ export function useAppModelPricingLoader(params: UseAppModelPricingLoaderParams): void {
12
+ const { preferencesLoaded, setModelsWithPricing } = params;
13
+
14
+ useEffect(() => {
15
+ if (!preferencesLoaded) return;
16
+
17
+ let cancelled = false;
18
+
19
+ (async () => {
20
+ try {
21
+ const modelIds = AVAILABLE_MODELS.map((m) => m.id);
22
+ const metadata = await getModelsMetadata(modelIds);
23
+ if (cancelled) return;
24
+
25
+ const modelsWithPrices: ModelOption[] = AVAILABLE_MODELS.map((model) => {
26
+ const meta = metadata.get(model.id);
27
+ return {
28
+ ...model,
29
+ name: meta?.name ?? model.name,
30
+ pricing: meta?.pricing,
31
+ contextLength: meta?.contextLength,
32
+ supportsCaching: meta?.supportsCaching,
33
+ };
34
+ });
35
+ setModelsWithPricing(modelsWithPrices);
36
+ } catch (_err: unknown) {
37
+ // Silently fail - models will just not show pricing.
38
+ }
39
+ })();
40
+
41
+ return () => {
42
+ cancelled = true;
43
+ };
44
+ }, [preferencesLoaded, setModelsWithPricing]);
45
+ }