@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.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.js +22 -0
- package/package.json +79 -0
- package/src/ai/agent-turn-runner.ts +130 -0
- package/src/ai/daemon-ai.ts +403 -0
- package/src/ai/exa-client.ts +21 -0
- package/src/ai/exa-fetch-cache.ts +104 -0
- package/src/ai/model-config.ts +99 -0
- package/src/ai/sanitize-messages.ts +83 -0
- package/src/ai/system-prompt.ts +363 -0
- package/src/ai/tools/fetch-urls.ts +187 -0
- package/src/ai/tools/grounding-manager.ts +94 -0
- package/src/ai/tools/index.ts +52 -0
- package/src/ai/tools/read-file.ts +100 -0
- package/src/ai/tools/render-url.ts +275 -0
- package/src/ai/tools/run-bash.ts +224 -0
- package/src/ai/tools/subagents.ts +195 -0
- package/src/ai/tools/todo-manager.ts +150 -0
- package/src/ai/tools/web-search.ts +91 -0
- package/src/app/App.tsx +711 -0
- package/src/app/components/AppOverlays.tsx +131 -0
- package/src/app/components/AvatarLayer.tsx +51 -0
- package/src/app/components/ConversationPane.tsx +476 -0
- package/src/avatar/DaemonAvatarRenderable.ts +343 -0
- package/src/avatar/daemon-avatar-rig.ts +1165 -0
- package/src/avatar-preview.ts +186 -0
- package/src/cli.ts +26 -0
- package/src/components/ApiKeyInput.tsx +99 -0
- package/src/components/ApiKeyStep.tsx +95 -0
- package/src/components/ApprovalPicker.tsx +109 -0
- package/src/components/ContentBlockView.tsx +141 -0
- package/src/components/DaemonText.tsx +34 -0
- package/src/components/DeviceMenu.tsx +166 -0
- package/src/components/GroundingBadge.tsx +21 -0
- package/src/components/GroundingMenu.tsx +310 -0
- package/src/components/HotkeysPane.tsx +115 -0
- package/src/components/InlineStatusIndicator.tsx +106 -0
- package/src/components/ModelMenu.tsx +411 -0
- package/src/components/OnboardingOverlay.tsx +446 -0
- package/src/components/ProviderMenu.tsx +177 -0
- package/src/components/SessionMenu.tsx +297 -0
- package/src/components/SettingsMenu.tsx +291 -0
- package/src/components/StatusBar.tsx +126 -0
- package/src/components/TokenUsageDisplay.tsx +92 -0
- package/src/components/ToolCallView.tsx +113 -0
- package/src/components/TypingInputBar.tsx +131 -0
- package/src/components/tool-layouts/components.tsx +120 -0
- package/src/components/tool-layouts/defaults.ts +9 -0
- package/src/components/tool-layouts/index.ts +22 -0
- package/src/components/tool-layouts/layouts/bash.ts +110 -0
- package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
- package/src/components/tool-layouts/layouts/index.ts +8 -0
- package/src/components/tool-layouts/layouts/read-file.ts +59 -0
- package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
- package/src/components/tool-layouts/layouts/system-info.ts +8 -0
- package/src/components/tool-layouts/layouts/todo.tsx +139 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
- package/src/components/tool-layouts/layouts/web-search.ts +110 -0
- package/src/components/tool-layouts/registry.ts +17 -0
- package/src/components/tool-layouts/types.ts +94 -0
- package/src/hooks/daemon-event-handlers.ts +944 -0
- package/src/hooks/keyboard-handlers.ts +399 -0
- package/src/hooks/menu-navigation.ts +147 -0
- package/src/hooks/use-app-audio-devices-loader.ts +71 -0
- package/src/hooks/use-app-callbacks.ts +202 -0
- package/src/hooks/use-app-context-builder.ts +159 -0
- package/src/hooks/use-app-display-state.ts +162 -0
- package/src/hooks/use-app-menus.ts +51 -0
- package/src/hooks/use-app-model-pricing-loader.ts +45 -0
- package/src/hooks/use-app-model.ts +123 -0
- package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
- package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
- package/src/hooks/use-app-sessions.ts +105 -0
- package/src/hooks/use-app-settings.ts +62 -0
- package/src/hooks/use-conversation-manager.ts +163 -0
- package/src/hooks/use-copy-on-select.ts +50 -0
- package/src/hooks/use-daemon-events.ts +396 -0
- package/src/hooks/use-daemon-keyboard.ts +397 -0
- package/src/hooks/use-grounding.ts +46 -0
- package/src/hooks/use-input-history.ts +92 -0
- package/src/hooks/use-menu-keyboard.ts +93 -0
- package/src/hooks/use-playwright-notification.ts +23 -0
- package/src/hooks/use-reasoning-animation.ts +97 -0
- package/src/hooks/use-response-timer.ts +55 -0
- package/src/hooks/use-tool-approval.tsx +202 -0
- package/src/hooks/use-typing-mode.ts +137 -0
- package/src/hooks/use-voice-dependencies-notification.ts +37 -0
- package/src/index.tsx +48 -0
- package/src/scripts/setup-browsers.ts +42 -0
- package/src/state/app-context.tsx +160 -0
- package/src/state/daemon-events.ts +67 -0
- package/src/state/daemon-state.ts +493 -0
- package/src/state/migrations/001-init.ts +33 -0
- package/src/state/migrations/index.ts +8 -0
- package/src/state/model-history-store.ts +45 -0
- package/src/state/runtime-context.ts +21 -0
- package/src/state/session-store.ts +359 -0
- package/src/types/index.ts +405 -0
- package/src/types/theme.ts +52 -0
- package/src/ui/constants.ts +157 -0
- package/src/utils/clipboard.ts +89 -0
- package/src/utils/debug-logger.ts +69 -0
- package/src/utils/formatters.ts +242 -0
- package/src/utils/js-rendering.ts +77 -0
- package/src/utils/markdown-tables.ts +234 -0
- package/src/utils/model-metadata.ts +191 -0
- package/src/utils/openrouter-endpoints.ts +212 -0
- package/src/utils/openrouter-models.ts +205 -0
- package/src/utils/openrouter-pricing.ts +59 -0
- package/src/utils/openrouter-reported-cost.ts +16 -0
- package/src/utils/paste.ts +33 -0
- package/src/utils/preferences.ts +289 -0
- package/src/utils/text-fragment.ts +39 -0
- package/src/utils/tool-output-preview.ts +250 -0
- package/src/utils/voice-dependencies.ts +107 -0
- package/src/utils/workspace-manager.ts +85 -0
- package/src/voice/audio-recorder.ts +579 -0
- package/src/voice/mic-level.ts +35 -0
- package/src/voice/tts/openai-tts-stream.ts +222 -0
- package/src/voice/tts/speech-controller.ts +64 -0
- package/src/voice/tts/tts-player.ts +257 -0
- 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
|
+
}
|