@makefinks/daemon 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/ai/daemon-ai.ts +30 -85
- package/src/ai/system-prompt.ts +134 -111
- package/src/ai/tool-approval-coordinator.ts +113 -0
- package/src/ai/tools/index.ts +12 -32
- package/src/ai/tools/subagents.ts +16 -30
- package/src/ai/tools/tool-registry.ts +203 -0
- package/src/app/App.tsx +23 -631
- package/src/app/components/AppOverlays.tsx +25 -1
- package/src/app/components/ConversationPane.tsx +5 -3
- package/src/components/HotkeysPane.tsx +3 -1
- package/src/components/TokenUsageDisplay.tsx +11 -11
- package/src/components/ToolsMenu.tsx +235 -0
- package/src/components/UrlMenu.tsx +182 -0
- package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
- package/src/hooks/daemon-event-handlers.ts +11 -151
- package/src/hooks/use-app-context-builder.ts +4 -0
- package/src/hooks/use-app-controller.ts +546 -0
- package/src/hooks/use-app-menus.ts +12 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +9 -0
- package/src/hooks/use-bootstrap-controller.ts +92 -0
- package/src/hooks/use-daemon-keyboard.ts +63 -57
- package/src/hooks/use-daemon-runtime-controller.ts +147 -0
- package/src/hooks/use-grounding-menu-controller.ts +51 -0
- package/src/hooks/use-overlay-controller.ts +84 -0
- package/src/hooks/use-session-controller.ts +79 -0
- package/src/hooks/use-url-menu-items.ts +19 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/daemon-state.ts +19 -8
- package/src/state/session-store.ts +4 -0
- package/src/types/index.ts +39 -0
- package/src/utils/derive-url-menu-items.ts +155 -0
- package/src/utils/formatters.ts +1 -7
- package/src/utils/preferences.ts +10 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* Hook for handling keyboard input across all application states.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useCallback } from "react";
|
|
1
|
+
import { toast } from "@opentui-ui/toast/react";
|
|
6
2
|
import type { KeyEvent } from "@opentui/core";
|
|
3
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
7
4
|
import { useKeyboard } from "@opentui/react";
|
|
8
|
-
import {
|
|
9
|
-
import { COLORS } from "../ui/constants";
|
|
5
|
+
import { useCallback } from "react";
|
|
10
6
|
import { getDaemonManager } from "../state/daemon-state";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
7
|
+
import { type AppPreferences, DaemonState } from "../types";
|
|
8
|
+
import { COLORS } from "../ui/constants";
|
|
13
9
|
export interface KeyboardHandlerState {
|
|
14
10
|
isOverlayOpen: boolean;
|
|
15
11
|
escPendingCancel: boolean;
|
|
@@ -27,6 +23,8 @@ export interface KeyboardHandlerActions {
|
|
|
27
23
|
setShowSessionMenu: (show: boolean) => void;
|
|
28
24
|
setShowHotkeysPane: (show: boolean) => void;
|
|
29
25
|
setShowGroundingMenu: (show: boolean) => void;
|
|
26
|
+
setShowUrlMenu: (show: boolean) => void;
|
|
27
|
+
setShowToolsMenu: (show: boolean) => void;
|
|
30
28
|
setTypingInput: (input: string | ((prev: string) => string)) => void;
|
|
31
29
|
setCurrentTranscription: (text: string) => void;
|
|
32
30
|
setCurrentResponse: (text: string) => void;
|
|
@@ -44,7 +42,19 @@ export interface KeyboardHandlerActions {
|
|
|
44
42
|
|
|
45
43
|
export function useDaemonKeyboard(state: KeyboardHandlerState, actions: KeyboardHandlerActions) {
|
|
46
44
|
const manager = getDaemonManager();
|
|
47
|
-
const { isOverlayOpen, escPendingCancel, hasInteracted, hasGrounding } = state;
|
|
45
|
+
const { isOverlayOpen, escPendingCancel, hasInteracted, hasGrounding, showFullReasoning } = state;
|
|
46
|
+
|
|
47
|
+
const closeAllMenus = useCallback(() => {
|
|
48
|
+
actions.setShowDeviceMenu(false);
|
|
49
|
+
actions.setShowSettingsMenu(false);
|
|
50
|
+
actions.setShowModelMenu(false);
|
|
51
|
+
actions.setShowProviderMenu(false);
|
|
52
|
+
actions.setShowSessionMenu(false);
|
|
53
|
+
actions.setShowHotkeysPane(false);
|
|
54
|
+
actions.setShowGroundingMenu(false);
|
|
55
|
+
actions.setShowUrlMenu(false);
|
|
56
|
+
actions.setShowToolsMenu(false);
|
|
57
|
+
}, [actions]);
|
|
48
58
|
|
|
49
59
|
const handleKeyPress = useCallback(
|
|
50
60
|
(key: KeyEvent) => {
|
|
@@ -97,12 +107,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
97
107
|
currentState === DaemonState.IDLE &&
|
|
98
108
|
!hasInteracted
|
|
99
109
|
) {
|
|
100
|
-
|
|
101
|
-
actions.setShowProviderMenu(false);
|
|
102
|
-
actions.setShowSessionMenu(false);
|
|
103
|
-
actions.setShowSettingsMenu(false);
|
|
104
|
-
actions.setShowGroundingMenu(false);
|
|
105
|
-
actions.setShowHotkeysPane(false);
|
|
110
|
+
closeAllMenus();
|
|
106
111
|
actions.setShowDeviceMenu(true);
|
|
107
112
|
key.preventDefault();
|
|
108
113
|
return;
|
|
@@ -114,12 +119,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
114
119
|
key.eventType === "press" &&
|
|
115
120
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING)
|
|
116
121
|
) {
|
|
117
|
-
|
|
118
|
-
actions.setShowSettingsMenu(false);
|
|
119
|
-
actions.setShowModelMenu(false);
|
|
120
|
-
actions.setShowProviderMenu(false);
|
|
121
|
-
actions.setShowGroundingMenu(false);
|
|
122
|
-
actions.setShowHotkeysPane(false);
|
|
122
|
+
closeAllMenus();
|
|
123
123
|
actions.setShowSessionMenu(true);
|
|
124
124
|
key.preventDefault();
|
|
125
125
|
return;
|
|
@@ -133,12 +133,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
133
133
|
currentState === DaemonState.SPEAKING ||
|
|
134
134
|
currentState === DaemonState.RESPONDING)
|
|
135
135
|
) {
|
|
136
|
-
|
|
137
|
-
actions.setShowModelMenu(false);
|
|
138
|
-
actions.setShowProviderMenu(false);
|
|
139
|
-
actions.setShowSessionMenu(false);
|
|
140
|
-
actions.setShowGroundingMenu(false);
|
|
141
|
-
actions.setShowHotkeysPane(false);
|
|
136
|
+
closeAllMenus();
|
|
142
137
|
actions.setShowSettingsMenu(true);
|
|
143
138
|
key.preventDefault();
|
|
144
139
|
return;
|
|
@@ -152,12 +147,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
152
147
|
currentState === DaemonState.SPEAKING ||
|
|
153
148
|
currentState === DaemonState.RESPONDING)
|
|
154
149
|
) {
|
|
155
|
-
|
|
156
|
-
actions.setShowSettingsMenu(false);
|
|
157
|
-
actions.setShowProviderMenu(false);
|
|
158
|
-
actions.setShowSessionMenu(false);
|
|
159
|
-
actions.setShowGroundingMenu(false);
|
|
160
|
-
actions.setShowHotkeysPane(false);
|
|
150
|
+
closeAllMenus();
|
|
161
151
|
actions.setShowModelMenu(true);
|
|
162
152
|
key.preventDefault();
|
|
163
153
|
return;
|
|
@@ -169,35 +159,42 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
169
159
|
key.eventType === "press" &&
|
|
170
160
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING)
|
|
171
161
|
) {
|
|
172
|
-
|
|
173
|
-
actions.setShowSettingsMenu(false);
|
|
174
|
-
actions.setShowModelMenu(false);
|
|
175
|
-
actions.setShowSessionMenu(false);
|
|
176
|
-
actions.setShowGroundingMenu(false);
|
|
177
|
-
actions.setShowHotkeysPane(false);
|
|
162
|
+
closeAllMenus();
|
|
178
163
|
actions.setShowProviderMenu(true);
|
|
179
164
|
key.preventDefault();
|
|
180
165
|
return;
|
|
181
166
|
}
|
|
182
167
|
|
|
183
|
-
// 'G' key to open grounding menu (in IDLE or
|
|
168
|
+
// 'G' key to open grounding menu (in IDLE, SPEAKING, or RESPONDING state when grounding exists)
|
|
184
169
|
if (
|
|
185
170
|
(key.sequence === "g" || key.sequence === "G") &&
|
|
186
171
|
key.eventType === "press" &&
|
|
187
|
-
(currentState === DaemonState.IDLE ||
|
|
172
|
+
(currentState === DaemonState.IDLE ||
|
|
173
|
+
currentState === DaemonState.SPEAKING ||
|
|
174
|
+
currentState === DaemonState.RESPONDING) &&
|
|
188
175
|
hasGrounding
|
|
189
176
|
) {
|
|
190
|
-
|
|
191
|
-
actions.setShowSettingsMenu(false);
|
|
192
|
-
actions.setShowModelMenu(false);
|
|
193
|
-
actions.setShowProviderMenu(false);
|
|
194
|
-
actions.setShowSessionMenu(false);
|
|
195
|
-
actions.setShowHotkeysPane(false);
|
|
177
|
+
closeAllMenus();
|
|
196
178
|
actions.setShowGroundingMenu(true);
|
|
197
179
|
key.preventDefault();
|
|
198
180
|
return;
|
|
199
181
|
}
|
|
200
182
|
|
|
183
|
+
// 'U' key to open URL menu (in IDLE, SPEAKING, or RESPONDING state when hasInteracted)
|
|
184
|
+
if (
|
|
185
|
+
(key.sequence === "u" || key.sequence === "U") &&
|
|
186
|
+
key.eventType === "press" &&
|
|
187
|
+
(currentState === DaemonState.IDLE ||
|
|
188
|
+
currentState === DaemonState.SPEAKING ||
|
|
189
|
+
currentState === DaemonState.RESPONDING) &&
|
|
190
|
+
hasInteracted
|
|
191
|
+
) {
|
|
192
|
+
closeAllMenus();
|
|
193
|
+
actions.setShowUrlMenu(true);
|
|
194
|
+
key.preventDefault();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
201
198
|
// 'N' key to start a new session (in IDLE or SPEAKING state)
|
|
202
199
|
if (
|
|
203
200
|
(key.sequence === "n" || key.sequence === "N") &&
|
|
@@ -221,7 +218,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
221
218
|
return;
|
|
222
219
|
}
|
|
223
220
|
|
|
224
|
-
// 'T' key to
|
|
221
|
+
// 'T' key to open tools menu (in IDLE, SPEAKING, or RESPONDING state)
|
|
225
222
|
if (
|
|
226
223
|
(key.sequence === "t" || key.sequence === "T") &&
|
|
227
224
|
key.eventType === "press" &&
|
|
@@ -229,7 +226,21 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
229
226
|
currentState === DaemonState.SPEAKING ||
|
|
230
227
|
currentState === DaemonState.RESPONDING)
|
|
231
228
|
) {
|
|
232
|
-
|
|
229
|
+
closeAllMenus();
|
|
230
|
+
actions.setShowToolsMenu(true);
|
|
231
|
+
key.preventDefault();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 'R' key to toggle full reasoning display (in IDLE, SPEAKING, or RESPONDING state)
|
|
236
|
+
if (
|
|
237
|
+
(key.sequence === "r" || key.sequence === "R") &&
|
|
238
|
+
key.eventType === "press" &&
|
|
239
|
+
(currentState === DaemonState.IDLE ||
|
|
240
|
+
currentState === DaemonState.SPEAKING ||
|
|
241
|
+
currentState === DaemonState.RESPONDING)
|
|
242
|
+
) {
|
|
243
|
+
const next = !showFullReasoning;
|
|
233
244
|
actions.setShowFullReasoning(next);
|
|
234
245
|
actions.persistPreferences({ showFullReasoning: next });
|
|
235
246
|
toast.info(`FULL PREVIEWS: ${next ? "ON" : "OFF"}`, {
|
|
@@ -267,12 +278,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
267
278
|
|
|
268
279
|
// '?' key to show hotkeys pane
|
|
269
280
|
if (key.sequence === "?" && key.eventType === "press" && currentState !== DaemonState.TYPING) {
|
|
270
|
-
|
|
271
|
-
actions.setShowSettingsMenu(false);
|
|
272
|
-
actions.setShowModelMenu(false);
|
|
273
|
-
actions.setShowProviderMenu(false);
|
|
274
|
-
actions.setShowSessionMenu(false);
|
|
275
|
-
actions.setShowGroundingMenu(false);
|
|
281
|
+
closeAllMenus();
|
|
276
282
|
actions.setShowHotkeysPane(true);
|
|
277
283
|
key.preventDefault();
|
|
278
284
|
return;
|
|
@@ -387,7 +393,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
387
393
|
escPendingCancel,
|
|
388
394
|
hasInteracted,
|
|
389
395
|
hasGrounding,
|
|
390
|
-
|
|
396
|
+
showFullReasoning,
|
|
391
397
|
state.showToolOutput,
|
|
392
398
|
actions,
|
|
393
399
|
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import type { MutableRefObject } from "react";
|
|
4
|
+
|
|
5
|
+
import { useDaemonEvents } from "./use-daemon-events";
|
|
6
|
+
import { useInputHistory } from "./use-input-history";
|
|
7
|
+
import { useReasoningAnimation } from "./use-reasoning-animation";
|
|
8
|
+
import { useResponseTimer } from "./use-response-timer";
|
|
9
|
+
import { useTypingMode } from "./use-typing-mode";
|
|
10
|
+
|
|
11
|
+
import { daemonEvents } from "../state/daemon-events";
|
|
12
|
+
|
|
13
|
+
export interface DaemonRuntimeControllerResult {
|
|
14
|
+
reasoning: ReturnType<typeof useReasoningAnimation>;
|
|
15
|
+
|
|
16
|
+
daemonState: ReturnType<typeof useDaemonEvents>["daemonState"];
|
|
17
|
+
conversationHistory: ReturnType<typeof useDaemonEvents>["conversationHistory"];
|
|
18
|
+
currentTranscription: ReturnType<typeof useDaemonEvents>["currentTranscription"];
|
|
19
|
+
currentResponse: ReturnType<typeof useDaemonEvents>["currentResponse"];
|
|
20
|
+
currentContentBlocks: ReturnType<typeof useDaemonEvents>["currentContentBlocks"];
|
|
21
|
+
error: ReturnType<typeof useDaemonEvents>["error"];
|
|
22
|
+
sessionUsage: ReturnType<typeof useDaemonEvents>["sessionUsage"];
|
|
23
|
+
modelMetadata: ReturnType<typeof useDaemonEvents>["modelMetadata"];
|
|
24
|
+
avatarRef: ReturnType<typeof useDaemonEvents>["avatarRef"];
|
|
25
|
+
currentUserInputRef: ReturnType<typeof useDaemonEvents>["currentUserInputRef"];
|
|
26
|
+
hydrateConversationHistory: ReturnType<typeof useDaemonEvents>["hydrateConversationHistory"];
|
|
27
|
+
setCurrentTranscription: ReturnType<typeof useDaemonEvents>["setCurrentTranscription"];
|
|
28
|
+
setCurrentResponse: ReturnType<typeof useDaemonEvents>["setCurrentResponse"];
|
|
29
|
+
clearCurrentContentBlocks: ReturnType<typeof useDaemonEvents>["clearCurrentContentBlocks"];
|
|
30
|
+
resetSessionUsage: ReturnType<typeof useDaemonEvents>["resetSessionUsage"];
|
|
31
|
+
setSessionUsage: ReturnType<typeof useDaemonEvents>["setSessionUsage"];
|
|
32
|
+
applyAvatarForState: ReturnType<typeof useDaemonEvents>["applyAvatarForState"];
|
|
33
|
+
|
|
34
|
+
typing: ReturnType<typeof useTypingMode>;
|
|
35
|
+
|
|
36
|
+
responseElapsedMs: number;
|
|
37
|
+
|
|
38
|
+
conversationScrollRef: MutableRefObject<ScrollBoxRenderable | null>;
|
|
39
|
+
|
|
40
|
+
hasInteracted: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useDaemonRuntimeController({
|
|
44
|
+
currentModelId,
|
|
45
|
+
preferencesLoaded,
|
|
46
|
+
sessionId,
|
|
47
|
+
sessionIdRef,
|
|
48
|
+
ensureSessionId,
|
|
49
|
+
onFirstMessage,
|
|
50
|
+
}: {
|
|
51
|
+
currentModelId: string;
|
|
52
|
+
preferencesLoaded: boolean;
|
|
53
|
+
sessionId: string | null;
|
|
54
|
+
sessionIdRef: MutableRefObject<string | null>;
|
|
55
|
+
ensureSessionId: () => Promise<string>;
|
|
56
|
+
onFirstMessage: (sessionId: string, message: string) => void;
|
|
57
|
+
}): DaemonRuntimeControllerResult {
|
|
58
|
+
const reasoning = useReasoningAnimation();
|
|
59
|
+
const { addToHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
daemonState,
|
|
63
|
+
conversationHistory,
|
|
64
|
+
currentTranscription,
|
|
65
|
+
currentResponse,
|
|
66
|
+
currentContentBlocks,
|
|
67
|
+
error,
|
|
68
|
+
sessionUsage,
|
|
69
|
+
modelMetadata,
|
|
70
|
+
avatarRef,
|
|
71
|
+
currentUserInputRef,
|
|
72
|
+
hydrateConversationHistory,
|
|
73
|
+
setCurrentTranscription,
|
|
74
|
+
setCurrentResponse,
|
|
75
|
+
clearCurrentContentBlocks,
|
|
76
|
+
resetSessionUsage,
|
|
77
|
+
setSessionUsage,
|
|
78
|
+
applyAvatarForState,
|
|
79
|
+
} = useDaemonEvents({
|
|
80
|
+
currentModelId,
|
|
81
|
+
preferencesLoaded,
|
|
82
|
+
setReasoningQueue: reasoning.setReasoningQueue,
|
|
83
|
+
setFullReasoning: reasoning.setFullReasoning,
|
|
84
|
+
clearReasoningState: reasoning.clearReasoningState,
|
|
85
|
+
clearReasoningTicker: reasoning.clearReasoningTicker,
|
|
86
|
+
fullReasoningRef: reasoning.fullReasoningRef,
|
|
87
|
+
sessionId,
|
|
88
|
+
sessionIdRef,
|
|
89
|
+
ensureSessionId,
|
|
90
|
+
addToHistory,
|
|
91
|
+
onFirstMessage,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const typing = useTypingMode({
|
|
95
|
+
daemonState,
|
|
96
|
+
currentUserInputRef,
|
|
97
|
+
setCurrentTranscription,
|
|
98
|
+
onTypingActivity: useCallback(() => {
|
|
99
|
+
avatarRef.current?.triggerTypingPulse();
|
|
100
|
+
}, [avatarRef]),
|
|
101
|
+
navigateUp,
|
|
102
|
+
navigateDown,
|
|
103
|
+
resetNavigation,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const handleTranscriptionReady = (text: string) => {
|
|
108
|
+
typing.prefillTypingInput(text);
|
|
109
|
+
};
|
|
110
|
+
daemonEvents.on("transcriptionReady", handleTranscriptionReady);
|
|
111
|
+
return () => {
|
|
112
|
+
daemonEvents.off("transcriptionReady", handleTranscriptionReady);
|
|
113
|
+
};
|
|
114
|
+
}, [typing.prefillTypingInput]);
|
|
115
|
+
|
|
116
|
+
const { responseElapsedMs } = useResponseTimer({ daemonState });
|
|
117
|
+
|
|
118
|
+
const conversationScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
119
|
+
|
|
120
|
+
const hasInteracted =
|
|
121
|
+
conversationHistory.length > 0 || currentTranscription.length > 0 || currentContentBlocks.length > 0;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
reasoning,
|
|
125
|
+
daemonState,
|
|
126
|
+
conversationHistory,
|
|
127
|
+
currentTranscription,
|
|
128
|
+
currentResponse,
|
|
129
|
+
currentContentBlocks,
|
|
130
|
+
error,
|
|
131
|
+
sessionUsage,
|
|
132
|
+
modelMetadata,
|
|
133
|
+
avatarRef,
|
|
134
|
+
currentUserInputRef,
|
|
135
|
+
hydrateConversationHistory,
|
|
136
|
+
setCurrentTranscription,
|
|
137
|
+
setCurrentResponse,
|
|
138
|
+
clearCurrentContentBlocks,
|
|
139
|
+
resetSessionUsage,
|
|
140
|
+
setSessionUsage,
|
|
141
|
+
applyAvatarForState,
|
|
142
|
+
typing,
|
|
143
|
+
responseElapsedMs,
|
|
144
|
+
conversationScrollRef,
|
|
145
|
+
hasInteracted,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { GroundingMap } from "../types";
|
|
3
|
+
import { openUrlInBrowser } from "../utils/preferences";
|
|
4
|
+
import { buildTextFragmentUrl } from "../utils/text-fragment";
|
|
5
|
+
|
|
6
|
+
export function useGroundingMenuController(params: {
|
|
7
|
+
sessionId: string | null;
|
|
8
|
+
latestGroundingMap: GroundingMap | null;
|
|
9
|
+
}) {
|
|
10
|
+
const { sessionId, latestGroundingMap } = params;
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setSelectedIndex(0);
|
|
15
|
+
}, [sessionId]);
|
|
16
|
+
|
|
17
|
+
const initialIndex = useMemo(() => {
|
|
18
|
+
if (!latestGroundingMap) return 0;
|
|
19
|
+
return Math.min(selectedIndex, Math.max(0, latestGroundingMap.items.length - 1));
|
|
20
|
+
}, [latestGroundingMap, selectedIndex]);
|
|
21
|
+
|
|
22
|
+
const openGroundingSource = useCallback(
|
|
23
|
+
(idx: number) => {
|
|
24
|
+
if (!latestGroundingMap) return;
|
|
25
|
+
const item = latestGroundingMap.items[idx];
|
|
26
|
+
if (!item) return;
|
|
27
|
+
const { source } = item;
|
|
28
|
+
const url = source.textFragment
|
|
29
|
+
? buildTextFragmentUrl(source.url, { fragmentText: source.textFragment })
|
|
30
|
+
: source.url;
|
|
31
|
+
openUrlInBrowser(url);
|
|
32
|
+
},
|
|
33
|
+
[latestGroundingMap]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const handleSelect = useCallback(
|
|
37
|
+
(index: number) => {
|
|
38
|
+
setSelectedIndex(index);
|
|
39
|
+
openGroundingSource(index);
|
|
40
|
+
},
|
|
41
|
+
[openGroundingSource]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
groundingInitialIndex: initialIndex,
|
|
46
|
+
groundingSelectedIndex: selectedIndex,
|
|
47
|
+
setGroundingSelectedIndex: setSelectedIndex,
|
|
48
|
+
onGroundingSelect: handleSelect,
|
|
49
|
+
onGroundingIndexChange: setSelectedIndex,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export interface OverlayControllerState {
|
|
4
|
+
showDeviceMenu: boolean;
|
|
5
|
+
showSettingsMenu: boolean;
|
|
6
|
+
showModelMenu: boolean;
|
|
7
|
+
showProviderMenu: boolean;
|
|
8
|
+
showSessionMenu: boolean;
|
|
9
|
+
showHotkeysPane: boolean;
|
|
10
|
+
showGroundingMenu: boolean;
|
|
11
|
+
showUrlMenu: boolean;
|
|
12
|
+
showToolsMenu: boolean;
|
|
13
|
+
onboardingActive: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OverlayControllerActions {
|
|
17
|
+
setShowDeviceMenu: (show: boolean) => void;
|
|
18
|
+
setShowSettingsMenu: (show: boolean) => void;
|
|
19
|
+
setShowModelMenu: (show: boolean) => void;
|
|
20
|
+
setShowProviderMenu: (show: boolean) => void;
|
|
21
|
+
setShowSessionMenu: (show: boolean) => void;
|
|
22
|
+
setShowHotkeysPane: (show: boolean) => void;
|
|
23
|
+
setShowGroundingMenu: (show: boolean) => void;
|
|
24
|
+
setShowUrlMenu: (show: boolean) => void;
|
|
25
|
+
setShowToolsMenu: (show: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useOverlayController(state: OverlayControllerState, actions: OverlayControllerActions) {
|
|
29
|
+
const {
|
|
30
|
+
showDeviceMenu,
|
|
31
|
+
showSettingsMenu,
|
|
32
|
+
showModelMenu,
|
|
33
|
+
showProviderMenu,
|
|
34
|
+
showSessionMenu,
|
|
35
|
+
showHotkeysPane,
|
|
36
|
+
showGroundingMenu,
|
|
37
|
+
showUrlMenu,
|
|
38
|
+
showToolsMenu,
|
|
39
|
+
onboardingActive,
|
|
40
|
+
} = state;
|
|
41
|
+
|
|
42
|
+
const isOverlayOpen = useMemo(() => {
|
|
43
|
+
return (
|
|
44
|
+
showDeviceMenu ||
|
|
45
|
+
showSettingsMenu ||
|
|
46
|
+
showModelMenu ||
|
|
47
|
+
showProviderMenu ||
|
|
48
|
+
showSessionMenu ||
|
|
49
|
+
showHotkeysPane ||
|
|
50
|
+
showGroundingMenu ||
|
|
51
|
+
showUrlMenu ||
|
|
52
|
+
showToolsMenu ||
|
|
53
|
+
onboardingActive
|
|
54
|
+
);
|
|
55
|
+
}, [
|
|
56
|
+
showDeviceMenu,
|
|
57
|
+
showSettingsMenu,
|
|
58
|
+
showModelMenu,
|
|
59
|
+
showProviderMenu,
|
|
60
|
+
showSessionMenu,
|
|
61
|
+
showHotkeysPane,
|
|
62
|
+
showGroundingMenu,
|
|
63
|
+
showUrlMenu,
|
|
64
|
+
showToolsMenu,
|
|
65
|
+
onboardingActive,
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const closeAllOverlays = useCallback(() => {
|
|
69
|
+
actions.setShowDeviceMenu(false);
|
|
70
|
+
actions.setShowSettingsMenu(false);
|
|
71
|
+
actions.setShowModelMenu(false);
|
|
72
|
+
actions.setShowProviderMenu(false);
|
|
73
|
+
actions.setShowSessionMenu(false);
|
|
74
|
+
actions.setShowHotkeysPane(false);
|
|
75
|
+
actions.setShowGroundingMenu(false);
|
|
76
|
+
actions.setShowUrlMenu(false);
|
|
77
|
+
actions.setShowToolsMenu(false);
|
|
78
|
+
}, [actions]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
isOverlayOpen,
|
|
82
|
+
closeAllOverlays,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import type { MutableRefObject } from "react";
|
|
3
|
+
|
|
4
|
+
import { useAppSessions } from "./use-app-sessions";
|
|
5
|
+
import { useGrounding } from "./use-grounding";
|
|
6
|
+
import { useGroundingMenuController } from "./use-grounding-menu-controller";
|
|
7
|
+
|
|
8
|
+
import { getDaemonManager } from "../state/daemon-state";
|
|
9
|
+
|
|
10
|
+
export interface SessionControllerResult {
|
|
11
|
+
currentSessionId: string | null;
|
|
12
|
+
setCurrentSessionIdSafe: (id: string | null) => void;
|
|
13
|
+
currentSessionIdRef: MutableRefObject<string | null>;
|
|
14
|
+
ensureSessionId: () => Promise<string>;
|
|
15
|
+
setSessions: ReturnType<typeof useAppSessions>["setSessions"];
|
|
16
|
+
sessionMenuItems: ReturnType<typeof useAppSessions>["sessionMenuItems"];
|
|
17
|
+
handleFirstMessage: ReturnType<typeof useAppSessions>["handleFirstMessage"];
|
|
18
|
+
|
|
19
|
+
latestGroundingMap: ReturnType<typeof useGrounding>["latestGroundingMap"];
|
|
20
|
+
hasGrounding: boolean;
|
|
21
|
+
|
|
22
|
+
groundingInitialIndex: number;
|
|
23
|
+
groundingSelectedIndex: number;
|
|
24
|
+
setGroundingSelectedIndex: (idx: number) => void;
|
|
25
|
+
onGroundingSelect: (idx: number) => void;
|
|
26
|
+
onGroundingIndexChange: (idx: number) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useSessionController({
|
|
30
|
+
showSessionMenu,
|
|
31
|
+
}: {
|
|
32
|
+
showSessionMenu: boolean;
|
|
33
|
+
}): SessionControllerResult {
|
|
34
|
+
const {
|
|
35
|
+
currentSessionId,
|
|
36
|
+
setCurrentSessionIdSafe,
|
|
37
|
+
currentSessionIdRef,
|
|
38
|
+
ensureSessionId,
|
|
39
|
+
setSessions,
|
|
40
|
+
sessionMenuItems,
|
|
41
|
+
handleFirstMessage,
|
|
42
|
+
} = useAppSessions({ showSessionMenu });
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const manager = getDaemonManager();
|
|
46
|
+
manager.setEnsureSessionId(() => ensureSessionId());
|
|
47
|
+
return () => manager.setEnsureSessionId(null);
|
|
48
|
+
}, [ensureSessionId]);
|
|
49
|
+
|
|
50
|
+
const { latestGroundingMap, hasGrounding } = useGrounding(currentSessionId);
|
|
51
|
+
const {
|
|
52
|
+
groundingInitialIndex,
|
|
53
|
+
groundingSelectedIndex,
|
|
54
|
+
setGroundingSelectedIndex,
|
|
55
|
+
onGroundingSelect,
|
|
56
|
+
onGroundingIndexChange,
|
|
57
|
+
} = useGroundingMenuController({ sessionId: currentSessionId, latestGroundingMap });
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setGroundingSelectedIndex(0);
|
|
61
|
+
}, [currentSessionId, setGroundingSelectedIndex]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
currentSessionId,
|
|
65
|
+
setCurrentSessionIdSafe,
|
|
66
|
+
currentSessionIdRef,
|
|
67
|
+
ensureSessionId,
|
|
68
|
+
setSessions,
|
|
69
|
+
sessionMenuItems,
|
|
70
|
+
handleFirstMessage,
|
|
71
|
+
latestGroundingMap,
|
|
72
|
+
hasGrounding,
|
|
73
|
+
groundingInitialIndex,
|
|
74
|
+
groundingSelectedIndex,
|
|
75
|
+
setGroundingSelectedIndex,
|
|
76
|
+
onGroundingSelect,
|
|
77
|
+
onGroundingIndexChange,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ContentBlock, ConversationMessage, GroundingMap, UrlMenuItem } from "../types";
|
|
3
|
+
import { deriveUrlMenuItems } from "../utils/derive-url-menu-items";
|
|
4
|
+
|
|
5
|
+
export function useUrlMenuItems(params: {
|
|
6
|
+
conversationHistory: ConversationMessage[];
|
|
7
|
+
currentContentBlocks: ContentBlock[];
|
|
8
|
+
latestGroundingMap: GroundingMap | null;
|
|
9
|
+
}): UrlMenuItem[] {
|
|
10
|
+
const { conversationHistory, currentContentBlocks, latestGroundingMap } = params;
|
|
11
|
+
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
return deriveUrlMenuItems({
|
|
14
|
+
conversationHistory,
|
|
15
|
+
currentContentBlocks,
|
|
16
|
+
latestGroundingMap,
|
|
17
|
+
});
|
|
18
|
+
}, [conversationHistory, currentContentBlocks, latestGroundingMap]);
|
|
19
|
+
}
|
|
@@ -29,6 +29,10 @@ export interface MenuState {
|
|
|
29
29
|
setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
30
|
showGroundingMenu: boolean;
|
|
31
31
|
setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
32
|
+
showUrlMenu: boolean;
|
|
33
|
+
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
34
|
+
showToolsMenu: boolean;
|
|
35
|
+
setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export interface DeviceState {
|
|
@@ -5,19 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
import { AgentTurnRunner } from "../ai/agent-turn-runner";
|
|
7
7
|
import { transcribeAudio } from "../ai/daemon-ai";
|
|
8
|
-
import { debug } from "../utils/debug-logger";
|
|
9
|
-
import { SpeechController } from "../voice/tts/speech-controller";
|
|
10
|
-
import { VoiceInputController } from "../voice/voice-input-controller";
|
|
11
8
|
import type {
|
|
12
|
-
|
|
9
|
+
BashApprovalLevel,
|
|
13
10
|
InteractionMode,
|
|
14
|
-
|
|
15
|
-
SpeechSpeed,
|
|
11
|
+
ModelMessage,
|
|
16
12
|
ReasoningEffort,
|
|
17
|
-
|
|
13
|
+
SpeechSpeed,
|
|
14
|
+
ToolToggles,
|
|
15
|
+
VoiceInteractionType,
|
|
18
16
|
} from "../types";
|
|
17
|
+
import { DEFAULT_TOOL_TOGGLES } from "../types";
|
|
19
18
|
import { DaemonState } from "../types";
|
|
20
|
-
import {
|
|
19
|
+
import { debug } from "../utils/debug-logger";
|
|
20
|
+
import { SpeechController } from "../voice/tts/speech-controller";
|
|
21
|
+
import { VoiceInputController } from "../voice/voice-input-controller";
|
|
22
|
+
import { type DaemonStateEvents, daemonEvents } from "./daemon-events";
|
|
21
23
|
import { ModelHistoryStore } from "./model-history-store";
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -39,6 +41,7 @@ class DaemonStateManager {
|
|
|
39
41
|
private _speechSpeed: SpeechSpeed = 1.25;
|
|
40
42
|
private _reasoningEffort: ReasoningEffort = "medium";
|
|
41
43
|
private _bashApprovalLevel: BashApprovalLevel = "dangerous";
|
|
44
|
+
private _toolToggles: ToolToggles = { ...DEFAULT_TOOL_TOGGLES };
|
|
42
45
|
private _outputDeviceName: string | undefined = undefined;
|
|
43
46
|
private _turnId = 0;
|
|
44
47
|
private speechRunId = 0;
|
|
@@ -136,6 +139,14 @@ class DaemonStateManager {
|
|
|
136
139
|
this._bashApprovalLevel = level;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
get toolToggles(): ToolToggles {
|
|
143
|
+
return this._toolToggles;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set toolToggles(toggles: ToolToggles) {
|
|
147
|
+
this._toolToggles = toggles;
|
|
148
|
+
}
|
|
149
|
+
|
|
139
150
|
get outputDeviceName(): string | undefined {
|
|
140
151
|
return this._outputDeviceName;
|
|
141
152
|
}
|
|
@@ -131,6 +131,10 @@ function parseSessionUsage(raw: string): TokenUsage {
|
|
|
131
131
|
subagentPromptTokens: typeof parsed.subagentPromptTokens === "number" ? parsed.subagentPromptTokens : 0,
|
|
132
132
|
subagentCompletionTokens:
|
|
133
133
|
typeof parsed.subagentCompletionTokens === "number" ? parsed.subagentCompletionTokens : 0,
|
|
134
|
+
latestTurnPromptTokens:
|
|
135
|
+
typeof parsed.latestTurnPromptTokens === "number" ? parsed.latestTurnPromptTokens : undefined,
|
|
136
|
+
latestTurnCompletionTokens:
|
|
137
|
+
typeof parsed.latestTurnCompletionTokens === "number" ? parsed.latestTurnCompletionTokens : undefined,
|
|
134
138
|
};
|
|
135
139
|
} catch {
|
|
136
140
|
return { ...DEFAULT_SESSION_USAGE };
|