@makefinks/daemon 0.2.0 → 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.
@@ -24,6 +24,7 @@ export interface KeyboardHandlerActions {
24
24
  setShowHotkeysPane: (show: boolean) => void;
25
25
  setShowGroundingMenu: (show: boolean) => void;
26
26
  setShowUrlMenu: (show: boolean) => void;
27
+ setShowToolsMenu: (show: boolean) => void;
27
28
  setTypingInput: (input: string | ((prev: string) => string)) => void;
28
29
  setCurrentTranscription: (text: string) => void;
29
30
  setCurrentResponse: (text: string) => void;
@@ -41,7 +42,7 @@ export interface KeyboardHandlerActions {
41
42
 
42
43
  export function useDaemonKeyboard(state: KeyboardHandlerState, actions: KeyboardHandlerActions) {
43
44
  const manager = getDaemonManager();
44
- const { isOverlayOpen, escPendingCancel, hasInteracted, hasGrounding } = state;
45
+ const { isOverlayOpen, escPendingCancel, hasInteracted, hasGrounding, showFullReasoning } = state;
45
46
 
46
47
  const closeAllMenus = useCallback(() => {
47
48
  actions.setShowDeviceMenu(false);
@@ -52,6 +53,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
52
53
  actions.setShowHotkeysPane(false);
53
54
  actions.setShowGroundingMenu(false);
54
55
  actions.setShowUrlMenu(false);
56
+ actions.setShowToolsMenu(false);
55
57
  }, [actions]);
56
58
 
57
59
  const handleKeyPress = useCallback(
@@ -163,11 +165,13 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
163
165
  return;
164
166
  }
165
167
 
166
- // '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)
167
169
  if (
168
170
  (key.sequence === "g" || key.sequence === "G") &&
169
171
  key.eventType === "press" &&
170
- (currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) &&
172
+ (currentState === DaemonState.IDLE ||
173
+ currentState === DaemonState.SPEAKING ||
174
+ currentState === DaemonState.RESPONDING) &&
171
175
  hasGrounding
172
176
  ) {
173
177
  closeAllMenus();
@@ -176,11 +180,13 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
176
180
  return;
177
181
  }
178
182
 
179
- // 'U' key to open URL menu (in IDLE or SPEAKING state when hasInteracted)
183
+ // 'U' key to open URL menu (in IDLE, SPEAKING, or RESPONDING state when hasInteracted)
180
184
  if (
181
185
  (key.sequence === "u" || key.sequence === "U") &&
182
186
  key.eventType === "press" &&
183
- (currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) &&
187
+ (currentState === DaemonState.IDLE ||
188
+ currentState === DaemonState.SPEAKING ||
189
+ currentState === DaemonState.RESPONDING) &&
184
190
  hasInteracted
185
191
  ) {
186
192
  closeAllMenus();
@@ -212,7 +218,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
212
218
  return;
213
219
  }
214
220
 
215
- // '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)
216
222
  if (
217
223
  (key.sequence === "t" || key.sequence === "T") &&
218
224
  key.eventType === "press" &&
@@ -220,7 +226,21 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
220
226
  currentState === DaemonState.SPEAKING ||
221
227
  currentState === DaemonState.RESPONDING)
222
228
  ) {
223
- 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;
224
244
  actions.setShowFullReasoning(next);
225
245
  actions.persistPreferences({ showFullReasoning: next });
226
246
  toast.info(`FULL PREVIEWS: ${next ? "ON" : "OFF"}`, {
@@ -373,7 +393,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
373
393
  escPendingCancel,
374
394
  hasInteracted,
375
395
  hasGrounding,
376
- state.showFullReasoning,
396
+ showFullReasoning,
377
397
  state.showToolOutput,
378
398
  actions,
379
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
+ }
@@ -9,6 +9,7 @@ export interface OverlayControllerState {
9
9
  showHotkeysPane: boolean;
10
10
  showGroundingMenu: boolean;
11
11
  showUrlMenu: boolean;
12
+ showToolsMenu: boolean;
12
13
  onboardingActive: boolean;
13
14
  }
14
15
 
@@ -21,6 +22,7 @@ export interface OverlayControllerActions {
21
22
  setShowHotkeysPane: (show: boolean) => void;
22
23
  setShowGroundingMenu: (show: boolean) => void;
23
24
  setShowUrlMenu: (show: boolean) => void;
25
+ setShowToolsMenu: (show: boolean) => void;
24
26
  }
25
27
 
26
28
  export function useOverlayController(state: OverlayControllerState, actions: OverlayControllerActions) {
@@ -33,6 +35,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
33
35
  showHotkeysPane,
34
36
  showGroundingMenu,
35
37
  showUrlMenu,
38
+ showToolsMenu,
36
39
  onboardingActive,
37
40
  } = state;
38
41
 
@@ -46,6 +49,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
46
49
  showHotkeysPane ||
47
50
  showGroundingMenu ||
48
51
  showUrlMenu ||
52
+ showToolsMenu ||
49
53
  onboardingActive
50
54
  );
51
55
  }, [
@@ -57,6 +61,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
57
61
  showHotkeysPane,
58
62
  showGroundingMenu,
59
63
  showUrlMenu,
64
+ showToolsMenu,
60
65
  onboardingActive,
61
66
  ]);
62
67
 
@@ -69,6 +74,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
69
74
  actions.setShowHotkeysPane(false);
70
75
  actions.setShowGroundingMenu(false);
71
76
  actions.setShowUrlMenu(false);
77
+ actions.setShowToolsMenu(false);
72
78
  }, [actions]);
73
79
 
74
80
  return {
@@ -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
+ }
@@ -31,6 +31,8 @@ export interface MenuState {
31
31
  setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
32
32
  showUrlMenu: boolean;
33
33
  setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
34
+ showToolsMenu: boolean;
35
+ setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
34
36
  }
35
37
 
36
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
  }
@@ -259,6 +259,29 @@ export type OnboardingStep =
259
259
 
260
260
  export type VoiceInteractionType = "direct" | "review";
261
261
 
262
+ export type ToolToggleId =
263
+ | "readFile"
264
+ | "runBash"
265
+ | "webSearch"
266
+ | "fetchUrls"
267
+ | "renderUrl"
268
+ | "todoManager"
269
+ | "groundingManager"
270
+ | "subagent";
271
+
272
+ export type ToolToggles = Record<ToolToggleId, boolean>;
273
+
274
+ export const DEFAULT_TOOL_TOGGLES: ToolToggles = {
275
+ readFile: true,
276
+ runBash: true,
277
+ webSearch: true,
278
+ fetchUrls: true,
279
+ renderUrl: true,
280
+ todoManager: true,
281
+ groundingManager: true,
282
+ subagent: true,
283
+ };
284
+
262
285
  /**
263
286
  * Persisted user preferences.
264
287
  */
@@ -291,6 +314,8 @@ export interface AppPreferences {
291
314
  showToolOutput?: boolean;
292
315
  /** Bash command approval level */
293
316
  bashApprovalLevel?: BashApprovalLevel;
317
+ /** Tool toggles (on/off) */
318
+ toolToggles?: ToolToggles;
294
319
  /** Recent user inputs for up/down history navigation (max 20) */
295
320
  inputHistory?: string[];
296
321
  }
@@ -109,6 +109,16 @@ export function parsePreferences(raw: unknown): AppPreferences | null {
109
109
  if (typeof raw.showToolOutput === "boolean") {
110
110
  prefs.showToolOutput = raw.showToolOutput;
111
111
  }
112
+ if (isRecord(raw.toolToggles)) {
113
+ const record = raw.toolToggles;
114
+ const next: Record<string, boolean> = {};
115
+ for (const [k, v] of Object.entries(record)) {
116
+ if (typeof v === "boolean") {
117
+ next[k] = v;
118
+ }
119
+ }
120
+ prefs.toolToggles = next as AppPreferences["toolToggles"];
121
+ }
112
122
  if (
113
123
  raw.bashApprovalLevel === "none" ||
114
124
  raw.bashApprovalLevel === "dangerous" ||