@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,97 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { REASONING_ANIMATION } from "../ui/constants";
3
+
4
+ export interface ReasoningState {
5
+ reasoningQueue: string;
6
+ reasoningDisplay: string;
7
+ fullReasoning: string;
8
+ }
9
+
10
+ export interface UseReasoningAnimationReturn {
11
+ reasoningQueue: string;
12
+ reasoningDisplay: string;
13
+ fullReasoning: string;
14
+ setReasoningQueue: (queue: string | ((prev: string) => string)) => void;
15
+ setFullReasoning: (full: string | ((prev: string) => string)) => void;
16
+ fullReasoningRef: React.RefObject<string>;
17
+ clearReasoningState: () => void;
18
+ clearReasoningTicker: () => void;
19
+ }
20
+
21
+ export function useReasoningAnimation(): UseReasoningAnimationReturn {
22
+ const [reasoningQueue, setReasoningQueue] = useState<string>("");
23
+ const [reasoningDisplay, setReasoningDisplay] = useState<string>("");
24
+ const [fullReasoning, setFullReasoning] = useState<string>("");
25
+ const reasoningAnimRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
+ const fullReasoningRef = useRef<string>("");
27
+
28
+ const clearReasoningState = () => {
29
+ setReasoningQueue("");
30
+ setReasoningDisplay("");
31
+ setFullReasoning("");
32
+ fullReasoningRef.current = "";
33
+ };
34
+ const clearReasoningTicker = () => {
35
+ setReasoningQueue("");
36
+ setReasoningDisplay("");
37
+ };
38
+
39
+ // Smooth flowing animation for reasoning text
40
+ // This creates a "ticker" effect where text flows through the single line
41
+ useEffect(() => {
42
+ // Clear any existing animation
43
+ if (reasoningAnimRef.current) {
44
+ clearTimeout(reasoningAnimRef.current);
45
+ reasoningAnimRef.current = null;
46
+ }
47
+
48
+ // If no queue content, nothing to animate
49
+ if (!reasoningQueue) return;
50
+
51
+ const tick = () => {
52
+ setReasoningQueue((queue: string) => {
53
+ if (!queue) return queue;
54
+
55
+ // Take characters from the front of the queue
56
+ const charsToMove = Math.min(REASONING_ANIMATION.CHARS_PER_TICK, queue.length);
57
+ const movedChars = queue.slice(0, charsToMove);
58
+ const remainingQueue = queue.slice(charsToMove);
59
+
60
+ // Add to display, keeping it at max width by trimming from the left
61
+ setReasoningDisplay((display: string) => {
62
+ const newDisplay = display + movedChars;
63
+ if (newDisplay.length > REASONING_ANIMATION.LINE_WIDTH) {
64
+ return newDisplay.slice(-REASONING_ANIMATION.LINE_WIDTH);
65
+ }
66
+ return newDisplay;
67
+ });
68
+
69
+ return remainingQueue;
70
+ });
71
+
72
+ // Schedule next tick if there's still content in the queue
73
+ reasoningAnimRef.current = setTimeout(tick, REASONING_ANIMATION.TICK_INTERVAL_MS);
74
+ };
75
+
76
+ // Start the animation
77
+ reasoningAnimRef.current = setTimeout(tick, REASONING_ANIMATION.TICK_INTERVAL_MS);
78
+
79
+ return () => {
80
+ if (reasoningAnimRef.current) {
81
+ clearTimeout(reasoningAnimRef.current);
82
+ reasoningAnimRef.current = null;
83
+ }
84
+ };
85
+ }, [reasoningQueue]);
86
+
87
+ return {
88
+ reasoningQueue,
89
+ reasoningDisplay,
90
+ fullReasoning,
91
+ setReasoningQueue,
92
+ setFullReasoning,
93
+ fullReasoningRef,
94
+ clearReasoningState,
95
+ clearReasoningTicker,
96
+ };
97
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Hook for tracking response elapsed time during RESPONDING state.
3
+ */
4
+
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { DaemonState } from "../types";
7
+
8
+ export interface UseResponseTimerParams {
9
+ daemonState: DaemonState;
10
+ }
11
+
12
+ export interface UseResponseTimerReturn {
13
+ responseElapsedMs: number;
14
+ }
15
+
16
+ export function useResponseTimer(params: UseResponseTimerParams): UseResponseTimerReturn {
17
+ const { daemonState } = params;
18
+
19
+ const prevDaemonStateRef = useRef<DaemonState>(DaemonState.IDLE);
20
+ const [responseStartAt, setResponseStartAt] = useState<number | null>(null);
21
+ const [responseElapsedMs, setResponseElapsedMs] = useState(0);
22
+
23
+ // Track state transitions to start/stop timer
24
+ useEffect(() => {
25
+ const prevState = prevDaemonStateRef.current;
26
+ if (daemonState === DaemonState.RESPONDING && prevState !== DaemonState.RESPONDING) {
27
+ setResponseStartAt(Date.now());
28
+ setResponseElapsedMs(0);
29
+ } else if (daemonState !== DaemonState.RESPONDING && prevState === DaemonState.RESPONDING) {
30
+ setResponseStartAt(null);
31
+ setResponseElapsedMs(0);
32
+ }
33
+ prevDaemonStateRef.current = daemonState;
34
+ }, [daemonState]);
35
+
36
+ // Update elapsed time while responding
37
+ useEffect(() => {
38
+ if (daemonState !== DaemonState.RESPONDING || responseStartAt === null) return;
39
+ const tick = () => {
40
+ setResponseStartAt((start) => {
41
+ if (start !== null) {
42
+ setResponseElapsedMs(Date.now() - start);
43
+ }
44
+ return start;
45
+ });
46
+ };
47
+ tick();
48
+ const interval = setInterval(tick, 250);
49
+ return () => clearInterval(interval);
50
+ }, [daemonState, responseStartAt]);
51
+
52
+ return {
53
+ responseElapsedMs,
54
+ };
55
+ }
@@ -0,0 +1,202 @@
1
+ import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react";
2
+ import { daemonEvents } from "../state/daemon-events";
3
+ import type { ToolApprovalRequest, ToolApprovalResponse } from "../types";
4
+
5
+ interface PendingApproval {
6
+ request: ToolApprovalRequest;
7
+ respond: (response: ToolApprovalResponse) => void;
8
+ }
9
+
10
+ interface ToolApprovalContextValue {
11
+ pendingApprovals: Map<string, PendingApproval>;
12
+ activeApprovalId: string | null;
13
+ approveRequest: (toolCallId: string) => void;
14
+ denyRequest: (toolCallId: string, reason?: string) => void;
15
+ approveAll: () => void;
16
+ denyAll: (reason?: string) => void;
17
+ }
18
+
19
+ const ToolApprovalContext = createContext<ToolApprovalContextValue | null>(null);
20
+
21
+ export function useToolApproval(): ToolApprovalContextValue {
22
+ const context = useContext(ToolApprovalContext);
23
+ if (!context) {
24
+ throw new Error("useToolApproval must be used within ToolApprovalProvider");
25
+ }
26
+ return context;
27
+ }
28
+
29
+ export function useToolApprovalForCall(toolCallId: string | undefined): {
30
+ needsApproval: boolean;
31
+ isActive: boolean;
32
+ approve: () => void;
33
+ deny: () => void;
34
+ approveAll: () => void;
35
+ denyAll: () => void;
36
+ } {
37
+ const context = useContext(ToolApprovalContext);
38
+
39
+ const approval = toolCallId ? context?.pendingApprovals.get(toolCallId) : undefined;
40
+ const isActive = toolCallId !== undefined && context?.activeApprovalId === toolCallId;
41
+
42
+ const approve = useCallback(() => {
43
+ if (toolCallId && context) {
44
+ context.approveRequest(toolCallId);
45
+ }
46
+ }, [toolCallId, context]);
47
+
48
+ const deny = useCallback(() => {
49
+ if (toolCallId && context) {
50
+ context.denyRequest(toolCallId, "User denied tool execution");
51
+ }
52
+ }, [toolCallId, context]);
53
+
54
+ const approveAll = useCallback(() => {
55
+ context?.approveAll();
56
+ }, [context]);
57
+
58
+ const denyAll = useCallback(() => {
59
+ context?.denyAll("User denied tool execution");
60
+ }, [context]);
61
+
62
+ return {
63
+ needsApproval: !!approval,
64
+ isActive,
65
+ approve,
66
+ deny,
67
+ approveAll,
68
+ denyAll,
69
+ };
70
+ }
71
+
72
+ interface ToolApprovalProviderProps {
73
+ children: ReactNode;
74
+ }
75
+
76
+ export function ToolApprovalProvider({ children }: ToolApprovalProviderProps) {
77
+ const [pendingApprovals, setPendingApprovals] = useState<Map<string, PendingApproval>>(new Map());
78
+ const [activeApprovalId, setActiveApprovalId] = useState<string | null>(null);
79
+
80
+ const getFirstPendingId = useCallback((approvals: Map<string, PendingApproval>): string | null => {
81
+ const first = approvals.keys().next();
82
+ return first.done ? null : first.value;
83
+ }, []);
84
+
85
+ useEffect(() => {
86
+ const handleAwaitingApprovals = (
87
+ requests: ToolApprovalRequest[],
88
+ respondToApprovals: (responses: ToolApprovalResponse[]) => void
89
+ ) => {
90
+ const responseCollector = new Map<string, ToolApprovalResponse>();
91
+ const expectedCount = requests.length;
92
+
93
+ const checkAndRespond = () => {
94
+ if (responseCollector.size === expectedCount) {
95
+ respondToApprovals(Array.from(responseCollector.values()));
96
+ setPendingApprovals(new Map());
97
+ setActiveApprovalId(null);
98
+ }
99
+ };
100
+
101
+ const newApprovals = new Map<string, PendingApproval>();
102
+ for (const request of requests) {
103
+ newApprovals.set(request.toolCallId, {
104
+ request,
105
+ respond: (response) => {
106
+ responseCollector.set(request.approvalId, response);
107
+ checkAndRespond();
108
+ },
109
+ });
110
+ }
111
+
112
+ setPendingApprovals(newApprovals);
113
+ const firstId = requests[0]?.toolCallId ?? null;
114
+ setActiveApprovalId(firstId);
115
+ };
116
+
117
+ daemonEvents.on("awaitingApprovals", handleAwaitingApprovals);
118
+ return () => {
119
+ daemonEvents.off("awaitingApprovals", handleAwaitingApprovals);
120
+ };
121
+ }, []);
122
+
123
+ const approveRequest = useCallback(
124
+ (toolCallId: string) => {
125
+ setPendingApprovals((prev) => {
126
+ const approval = prev.get(toolCallId);
127
+ if (approval) {
128
+ approval.respond({
129
+ approvalId: approval.request.approvalId,
130
+ approved: true,
131
+ });
132
+ daemonEvents.emit("toolApprovalResolved", toolCallId, true);
133
+ const next = new Map(prev);
134
+ next.delete(toolCallId);
135
+ setActiveApprovalId(getFirstPendingId(next));
136
+ return next;
137
+ }
138
+ return prev;
139
+ });
140
+ },
141
+ [getFirstPendingId]
142
+ );
143
+
144
+ const denyRequest = useCallback(
145
+ (toolCallId: string, reason?: string) => {
146
+ setPendingApprovals((prev) => {
147
+ const approval = prev.get(toolCallId);
148
+ if (approval) {
149
+ approval.respond({
150
+ approvalId: approval.request.approvalId,
151
+ approved: false,
152
+ reason,
153
+ });
154
+ daemonEvents.emit("toolApprovalResolved", toolCallId, false);
155
+ const next = new Map(prev);
156
+ next.delete(toolCallId);
157
+ setActiveApprovalId(getFirstPendingId(next));
158
+ return next;
159
+ }
160
+ return prev;
161
+ });
162
+ },
163
+ [getFirstPendingId]
164
+ );
165
+
166
+ const approveAll = useCallback(() => {
167
+ setPendingApprovals((prev) => {
168
+ for (const [toolCallId, approval] of prev.entries()) {
169
+ approval.respond({
170
+ approvalId: approval.request.approvalId,
171
+ approved: true,
172
+ });
173
+ daemonEvents.emit("toolApprovalResolved", toolCallId, true);
174
+ }
175
+ setActiveApprovalId(null);
176
+ return new Map();
177
+ });
178
+ }, []);
179
+
180
+ const denyAll = useCallback((reason?: string) => {
181
+ setPendingApprovals((prev) => {
182
+ for (const [toolCallId, approval] of prev.entries()) {
183
+ approval.respond({
184
+ approvalId: approval.request.approvalId,
185
+ approved: false,
186
+ reason,
187
+ });
188
+ daemonEvents.emit("toolApprovalResolved", toolCallId, false);
189
+ }
190
+ setActiveApprovalId(null);
191
+ return new Map();
192
+ });
193
+ }, []);
194
+
195
+ return (
196
+ <ToolApprovalContext.Provider
197
+ value={{ pendingApprovals, activeApprovalId, approveRequest, denyRequest, approveAll, denyAll }}
198
+ >
199
+ {children}
200
+ </ToolApprovalContext.Provider>
201
+ );
202
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Hook for managing typing mode state and handlers.
3
+ */
4
+
5
+ import type { TextareaRenderable } from "@opentui/core";
6
+ import { useCallback, useEffect, useRef, useState } from "react";
7
+ import { getDaemonManager } from "../state/daemon-state";
8
+ import { DaemonState } from "../types";
9
+
10
+ export interface UseTypingModeParams {
11
+ daemonState: DaemonState;
12
+ currentUserInputRef: React.MutableRefObject<string>;
13
+ setCurrentTranscription: (text: string) => void;
14
+ onTypingActivity?: () => void;
15
+ navigateUp: (currentInput: string) => string | null;
16
+ navigateDown: () => string | null;
17
+ resetNavigation: () => void;
18
+ }
19
+
20
+ export interface UseTypingModeReturn {
21
+ typingInput: string;
22
+ setTypingInput: React.Dispatch<React.SetStateAction<string>>;
23
+ typingTextareaRef: React.RefObject<TextareaRenderable | null>;
24
+ handleTypingContentChange: (value: string) => void;
25
+ handleTypingSubmit: () => void;
26
+ prefillTypingInput: (text: string) => void;
27
+ handleHistoryUp: () => void;
28
+ handleHistoryDown: () => void;
29
+ }
30
+
31
+ export function useTypingMode(params: UseTypingModeParams): UseTypingModeReturn {
32
+ const {
33
+ daemonState,
34
+ currentUserInputRef,
35
+ setCurrentTranscription,
36
+ onTypingActivity,
37
+ navigateUp,
38
+ navigateDown,
39
+ resetNavigation,
40
+ } = params;
41
+
42
+ const [typingInput, setTypingInput] = useState<string>("");
43
+ const typingTextareaRef = useRef<TextareaRenderable | null>(null);
44
+ const pendingPrefillRef = useRef<string | null>(null);
45
+
46
+ useEffect(() => {
47
+ if (daemonState === DaemonState.TYPING && pendingPrefillRef.current !== null) {
48
+ const text = pendingPrefillRef.current;
49
+ const tryApplyPrefill = () => {
50
+ if (typingTextareaRef.current) {
51
+ typingTextareaRef.current.setText(text);
52
+ typingTextareaRef.current.gotoBufferEnd();
53
+ pendingPrefillRef.current = null;
54
+ } else {
55
+ setTimeout(tryApplyPrefill, 10);
56
+ }
57
+ };
58
+ tryApplyPrefill();
59
+ }
60
+ }, [daemonState]);
61
+
62
+ useEffect(() => {
63
+ if (daemonState !== DaemonState.TYPING) {
64
+ if (typingTextareaRef.current?.plainText) {
65
+ typingTextareaRef.current.setText("");
66
+ }
67
+ setTypingInput("");
68
+ pendingPrefillRef.current = null;
69
+ resetNavigation();
70
+ }
71
+ }, [daemonState, resetNavigation]);
72
+
73
+ const handleTypingContentChange = useCallback(
74
+ (value: string) => {
75
+ setTypingInput(value);
76
+ onTypingActivity?.();
77
+ },
78
+ [onTypingActivity]
79
+ );
80
+
81
+ const handleTypingSubmit = useCallback(() => {
82
+ const rawInput = typingTextareaRef.current?.plainText ?? typingInput;
83
+ const input = rawInput.trim();
84
+ if (input) {
85
+ const manager = getDaemonManager();
86
+ currentUserInputRef.current = input;
87
+ setCurrentTranscription(input);
88
+ manager.submitText(input);
89
+ }
90
+ typingTextareaRef.current?.setText("");
91
+ setTypingInput("");
92
+ }, [typingInput, setCurrentTranscription, currentUserInputRef]);
93
+
94
+ const prefillTypingInput = useCallback((text: string) => {
95
+ setTypingInput(text);
96
+ pendingPrefillRef.current = text;
97
+ if (typingTextareaRef.current) {
98
+ typingTextareaRef.current.setText(text);
99
+ typingTextareaRef.current.gotoBufferEnd();
100
+ pendingPrefillRef.current = null;
101
+ }
102
+ }, []);
103
+
104
+ const setTextareaValue = useCallback((value: string) => {
105
+ setTypingInput(value);
106
+ if (typingTextareaRef.current) {
107
+ typingTextareaRef.current.setText(value);
108
+ typingTextareaRef.current.gotoBufferEnd();
109
+ }
110
+ }, []);
111
+
112
+ const handleHistoryUp = useCallback(() => {
113
+ const currentInput = typingTextareaRef.current?.plainText ?? typingInput;
114
+ const historyItem = navigateUp(currentInput);
115
+ if (historyItem !== null) {
116
+ setTextareaValue(historyItem);
117
+ }
118
+ }, [typingInput, navigateUp, setTextareaValue]);
119
+
120
+ const handleHistoryDown = useCallback(() => {
121
+ const historyItem = navigateDown();
122
+ if (historyItem !== null) {
123
+ setTextareaValue(historyItem);
124
+ }
125
+ }, [navigateDown, setTextareaValue]);
126
+
127
+ return {
128
+ typingInput,
129
+ setTypingInput,
130
+ typingTextareaRef,
131
+ handleTypingContentChange,
132
+ handleTypingSubmit,
133
+ prefillTypingInput,
134
+ handleHistoryUp,
135
+ handleHistoryDown,
136
+ };
137
+ }
@@ -0,0 +1,37 @@
1
+ import { useEffect } from "react";
2
+ import { toast } from "@opentui-ui/toast/react";
3
+ import { detectVoiceDependencies, type VoiceDependencies } from "../utils/voice-dependencies";
4
+
5
+ export function useVoiceDependenciesNotification(): void {
6
+ useEffect(() => {
7
+ let cancelled = false;
8
+
9
+ void (async () => {
10
+ let deps: VoiceDependencies;
11
+ try {
12
+ deps = await detectVoiceDependencies();
13
+ } catch (error) {
14
+ if (cancelled) return;
15
+ const err = error instanceof Error ? error : new Error(String(error));
16
+ toast.warning("Voice dependency check failed", {
17
+ description: err.message,
18
+ });
19
+ return;
20
+ }
21
+
22
+ if (cancelled) return;
23
+
24
+ if (!deps.sox.available) {
25
+ const hint =
26
+ deps.sox.hint ?? (process.platform === "darwin" ? "Run: brew install sox" : "Install sox");
27
+ toast.warning("Voice features unavailable", {
28
+ description: `sox is not installed. ${hint}`,
29
+ });
30
+ }
31
+ })();
32
+
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ }, []);
37
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * DAEMON - Terminal-based AI with alien/cultic aesthetics.
3
+ * Main application entry point.
4
+ */
5
+
6
+ import { ConsolePosition, createCliRenderer } from "@opentui/core";
7
+ import { createRoot } from "@opentui/react";
8
+ import { App } from "./app/App";
9
+ import { destroyDaemonManager } from "./state/daemon-state";
10
+ import { COLORS } from "./ui/constants";
11
+ import { debug } from "./utils/debug-logger";
12
+
13
+ // Main entry point
14
+ const renderer = await createCliRenderer({
15
+ exitOnCtrlC: true,
16
+ targetFps: 60,
17
+ maxFps: 60,
18
+ useMouse: true,
19
+ enableMouseMovement: false,
20
+ useKittyKeyboard: {},
21
+ openConsoleOnError: true,
22
+ backgroundColor: COLORS.BACKGROUND,
23
+ consoleOptions: {
24
+ position: ConsolePosition.BOTTOM,
25
+ sizePercent: 30,
26
+ startInDebugMode: false,
27
+ },
28
+ });
29
+
30
+ // Debug: Log all paste events at the renderer level
31
+ renderer.keyInput.on("paste", (event) => {
32
+ debug.log("[Renderer] Paste event received", {
33
+ textLength: event.text.length,
34
+ textPreview: event.text.slice(0, 50),
35
+ });
36
+ });
37
+
38
+ // Cleanup on exit
39
+ process.on("exit", () => {
40
+ destroyDaemonManager();
41
+ });
42
+
43
+ process.on("SIGINT", () => {
44
+ destroyDaemonManager();
45
+ process.exit(0);
46
+ });
47
+
48
+ createRoot(renderer).render(<App />);
@@ -0,0 +1,42 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ function normalizeError(error: unknown): Error {
4
+ return error instanceof Error ? error : new Error(String(error));
5
+ }
6
+
7
+ function logError(message: string): void {
8
+ console.error(message);
9
+ }
10
+
11
+ async function main(): Promise<void> {
12
+ let playwrightEntryUrl: string;
13
+ try {
14
+ playwrightEntryUrl = import.meta.resolve("playwright");
15
+ } catch (error) {
16
+ logError("Playwright is not installed.");
17
+ logError("Install it first, then retry:");
18
+ logError("");
19
+ logError(" bun add -d playwright");
20
+ logError(" bun run setup:browsers");
21
+ process.exit(1);
22
+ return;
23
+ }
24
+
25
+ const cliPath = fileURLToPath(new URL("./cli.js", playwrightEntryUrl));
26
+ const proc = Bun.spawn([process.execPath, cliPath, "install", "chromium"], {
27
+ stdout: "inherit",
28
+ stderr: "inherit",
29
+ });
30
+ const code = await proc.exited;
31
+ if (code !== 0) {
32
+ process.exit(code);
33
+ }
34
+ }
35
+
36
+ try {
37
+ await main();
38
+ } catch (error) {
39
+ const err = normalizeError(error);
40
+ logError(`setup:browsers failed: ${err.message}`);
41
+ process.exit(1);
42
+ }