@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.
Files changed (34) hide show
  1. package/package.json +5 -4
  2. package/src/ai/daemon-ai.ts +30 -85
  3. package/src/ai/system-prompt.ts +134 -111
  4. package/src/ai/tool-approval-coordinator.ts +113 -0
  5. package/src/ai/tools/index.ts +12 -32
  6. package/src/ai/tools/subagents.ts +16 -30
  7. package/src/ai/tools/tool-registry.ts +203 -0
  8. package/src/app/App.tsx +23 -631
  9. package/src/app/components/AppOverlays.tsx +25 -1
  10. package/src/app/components/ConversationPane.tsx +5 -3
  11. package/src/components/HotkeysPane.tsx +3 -1
  12. package/src/components/TokenUsageDisplay.tsx +11 -11
  13. package/src/components/ToolsMenu.tsx +235 -0
  14. package/src/components/UrlMenu.tsx +182 -0
  15. package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
  16. package/src/hooks/daemon-event-handlers.ts +11 -151
  17. package/src/hooks/use-app-context-builder.ts +4 -0
  18. package/src/hooks/use-app-controller.ts +546 -0
  19. package/src/hooks/use-app-menus.ts +12 -0
  20. package/src/hooks/use-app-preferences-bootstrap.ts +9 -0
  21. package/src/hooks/use-bootstrap-controller.ts +92 -0
  22. package/src/hooks/use-daemon-keyboard.ts +63 -57
  23. package/src/hooks/use-daemon-runtime-controller.ts +147 -0
  24. package/src/hooks/use-grounding-menu-controller.ts +51 -0
  25. package/src/hooks/use-overlay-controller.ts +84 -0
  26. package/src/hooks/use-session-controller.ts +79 -0
  27. package/src/hooks/use-url-menu-items.ts +19 -0
  28. package/src/state/app-context.tsx +4 -0
  29. package/src/state/daemon-state.ts +19 -8
  30. package/src/state/session-store.ts +4 -0
  31. package/src/types/index.ts +39 -0
  32. package/src/utils/derive-url-menu-items.ts +155 -0
  33. package/src/utils/formatters.ts +1 -7
  34. 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 { toast } from "@opentui-ui/toast/react";
9
- import { COLORS } from "../ui/constants";
5
+ import { useCallback } from "react";
10
6
  import { getDaemonManager } from "../state/daemon-state";
11
- import { DaemonState, type AppPreferences } from "../types";
12
- import type { ScrollBoxRenderable } from "@opentui/core";
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
- actions.setShowModelMenu(false);
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
- actions.setShowDeviceMenu(false);
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
- actions.setShowDeviceMenu(false);
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
- actions.setShowDeviceMenu(false);
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
- actions.setShowDeviceMenu(false);
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 SPEAKING state when grounding exists)
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 || currentState === DaemonState.SPEAKING) &&
172
+ (currentState === DaemonState.IDLE ||
173
+ currentState === DaemonState.SPEAKING ||
174
+ currentState === DaemonState.RESPONDING) &&
188
175
  hasGrounding
189
176
  ) {
190
- actions.setShowDeviceMenu(false);
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 toggle full reasoning display (in IDLE, SPEAKING, or RESPONDING state)
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
- const next = !state.showFullReasoning;
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
- actions.setShowDeviceMenu(false);
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
- state.showFullReasoning,
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
- ModelMessage,
9
+ BashApprovalLevel,
13
10
  InteractionMode,
14
- VoiceInteractionType,
15
- SpeechSpeed,
11
+ ModelMessage,
16
12
  ReasoningEffort,
17
- BashApprovalLevel,
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 { daemonEvents, type DaemonStateEvents } from "./daemon-events";
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 };