@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.
@@ -7,6 +7,7 @@ import { OnboardingOverlay } from "../../components/OnboardingOverlay";
7
7
  import { ProviderMenu } from "../../components/ProviderMenu";
8
8
  import { SessionMenu } from "../../components/SessionMenu";
9
9
  import { SettingsMenu } from "../../components/SettingsMenu";
10
+ import { ToolsMenu } from "../../components/ToolsMenu";
10
11
  import { UrlMenu } from "../../components/UrlMenu";
11
12
  import { useUrlMenuItems } from "../../hooks/use-url-menu-items";
12
13
  import { useAppContext } from "../../state/app-context";
@@ -120,6 +121,13 @@ function AppOverlaysImpl({ conversationHistory, currentContentBlocks }: AppOverl
120
121
 
121
122
  {menus.showUrlMenu && <UrlMenu items={urlMenuItems} onClose={() => menus.setShowUrlMenu(false)} />}
122
123
 
124
+ {menus.showToolsMenu && (
125
+ <ToolsMenu
126
+ onClose={() => menus.setShowToolsMenu(false)}
127
+ persistPreferences={(updates) => settings.persistPreferences(updates)}
128
+ />
129
+ )}
130
+
123
131
  {onboarding.onboardingActive && (
124
132
  <OnboardingOverlay
125
133
  step={onboarding.onboardingStep}
@@ -36,7 +36,7 @@ export function HotkeysPane({ onClose }: HotkeysPaneProps) {
36
36
  {
37
37
  title: "SESSION",
38
38
  items: [
39
- { key: "T", label: "Toggle full reasoning previews" },
39
+ { key: "R", label: "Toggle full reasoning previews" },
40
40
  { key: "O", label: "Toggle tool output previews" },
41
41
  { key: "N", label: "New session" },
42
42
  { key: "G", label: "Open Grounding Menu" },
@@ -51,6 +51,7 @@ export function HotkeysPane({ onClose }: HotkeysPaneProps) {
51
51
  { key: "M", label: "Models" },
52
52
  { key: "P", label: "Providers" },
53
53
  { key: "L", label: "Sessions" },
54
+ { key: "T", label: "Tools" },
54
55
  { key: "S", label: "Settings" },
55
56
  ],
56
57
  },
@@ -0,0 +1,235 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { invalidateDaemonToolsCache } from "../ai/tools/index";
4
+ import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
5
+ import {
6
+ buildMenuItems,
7
+ getDefaultToolOrder,
8
+ getToolLabels,
9
+ resolveToolAvailability,
10
+ } from "../ai/tools/tool-registry";
11
+ import { useMenuKeyboard } from "../hooks/use-menu-keyboard";
12
+ import { getDaemonManager } from "../state/daemon-state";
13
+ import type { ToolToggleId, ToolToggles } from "../types";
14
+ import { DEFAULT_TOOL_TOGGLES } from "../types";
15
+ import { COLORS } from "../ui/constants";
16
+
17
+ interface ToolsMenuProps {
18
+ persistPreferences: (updates: Partial<{ toolToggles: ToolToggles }>) => void;
19
+ onClose: () => void;
20
+ }
21
+
22
+ type MenuToolItem = {
23
+ id: ToolToggleId;
24
+ label: string;
25
+ envAvailable: boolean;
26
+ disabledReason?: string;
27
+ };
28
+
29
+ function getToolLabel(id: ToolToggleId): string {
30
+ switch (id) {
31
+ case "readFile":
32
+ return "readFile";
33
+ case "runBash":
34
+ return "runBash";
35
+ case "webSearch":
36
+ return "webSearch";
37
+ case "fetchUrls":
38
+ return "fetchUrls";
39
+ case "renderUrl":
40
+ return "renderUrl";
41
+ case "todoManager":
42
+ return "todoManager";
43
+ case "groundingManager":
44
+ return "groundingManager";
45
+ case "subagent":
46
+ return "subagent";
47
+ default:
48
+ return id;
49
+ }
50
+ }
51
+
52
+ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
53
+ const manager = getDaemonManager();
54
+ const [toggles, setToggles] = useState<ToolToggles>(manager.toolToggles ?? { ...DEFAULT_TOOL_TOGGLES });
55
+
56
+ const [toolAvailability, setToolAvailability] = useState<Record<ToolToggleId, MenuToolItem> | null>(null);
57
+
58
+ useEffect(() => {
59
+ let cancelled = false;
60
+ const loadAvailability = async () => {
61
+ const toggles = manager.toolToggles ?? { ...DEFAULT_TOOL_TOGGLES };
62
+ const availability = await resolveToolAvailability(toggles);
63
+ const map = buildMenuItems(availability) as Record<ToolToggleId, MenuToolItem>;
64
+
65
+ if (cancelled) return;
66
+ setToolAvailability(map);
67
+ };
68
+
69
+ void loadAvailability();
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [manager, toggles]);
74
+
75
+ const items = useMemo((): MenuToolItem[] => {
76
+ const labels = getToolLabels();
77
+ const order = getDefaultToolOrder();
78
+ if (!toolAvailability) {
79
+ return order.map((id) => ({
80
+ id,
81
+ label: labels[id],
82
+ envAvailable: true,
83
+ }));
84
+ }
85
+
86
+ return order.map((id) => toolAvailability[id]).filter((item): item is MenuToolItem => Boolean(item));
87
+ }, [toolAvailability]);
88
+
89
+ const { selectedIndex } = useMenuKeyboard({
90
+ itemCount: items.length,
91
+ onClose,
92
+ closeOnSelect: false,
93
+ onSelect: (idx) => {
94
+ const item = items[idx];
95
+ if (!item) return;
96
+
97
+ const current = manager.toolToggles ?? { ...DEFAULT_TOOL_TOGGLES };
98
+
99
+ // If env-unavailable, block enabling, but allow disabling.
100
+ if (!item.envAvailable && current[item.id]) {
101
+ return;
102
+ }
103
+
104
+ const next: ToolToggles = {
105
+ ...DEFAULT_TOOL_TOGGLES,
106
+ ...current,
107
+ [item.id]: !current[item.id],
108
+ };
109
+ manager.toolToggles = next;
110
+ setToggles(next);
111
+ persistPreferences({ toolToggles: next });
112
+
113
+ invalidateDaemonToolsCache();
114
+ invalidateSubagentToolsCache();
115
+ resolveToolAvailability(next)
116
+ .then((availability) => {
117
+ const map = buildMenuItems(availability) as Record<ToolToggleId, MenuToolItem>;
118
+ setToolAvailability(map);
119
+ })
120
+ .catch(() => {
121
+ setToolAvailability(null);
122
+ });
123
+ },
124
+ });
125
+
126
+ const showReasonColumn = useMemo(() => {
127
+ return items.some((item) => !item.envAvailable);
128
+ }, [items]);
129
+
130
+ const labelWidth = useMemo(() => {
131
+ const raw = items.reduce((max, item) => Math.max(max, item.label.length), 0);
132
+ // When no env-disabled tools exist, keep the menu compact.
133
+ return showReasonColumn ? raw : Math.min(raw, 16);
134
+ }, [items, showReasonColumn]);
135
+
136
+ const statusWidth = 8;
137
+
138
+ function truncateText(text: string, maxLen: number): string {
139
+ if (text.length <= maxLen) return text;
140
+ return text.slice(0, Math.max(0, maxLen - 1)) + "…";
141
+ }
142
+
143
+ return (
144
+ <box
145
+ position="absolute"
146
+ left={0}
147
+ top={0}
148
+ width="100%"
149
+ height="100%"
150
+ flexDirection="column"
151
+ alignItems="center"
152
+ justifyContent="center"
153
+ zIndex={100}
154
+ >
155
+ <box
156
+ flexDirection="column"
157
+ backgroundColor={COLORS.MENU_BG}
158
+ borderStyle="single"
159
+ borderColor={COLORS.MENU_BORDER}
160
+ paddingLeft={2}
161
+ paddingRight={2}
162
+ paddingTop={1}
163
+ paddingBottom={1}
164
+ width={showReasonColumn ? "70%" : "52%"}
165
+ minWidth={showReasonColumn ? 70 : 48}
166
+ maxWidth={showReasonColumn ? 150 : 90}
167
+ >
168
+ <box marginBottom={1}>
169
+ <text>
170
+ <span fg={COLORS.DAEMON_LABEL}>[ TOOLS ]</span>
171
+ </text>
172
+ </box>
173
+ <box marginBottom={1}>
174
+ <text>
175
+ <span fg={COLORS.USER_LABEL}>↑/↓ or j/k to navigate, ENTER to toggle, ESC to close</span>
176
+ </text>
177
+ </box>
178
+
179
+ {showReasonColumn && (
180
+ <box marginBottom={1}>
181
+ <text>
182
+ <span fg={COLORS.REASONING_DIM}>
183
+ {"TOOL".padEnd(labelWidth)} {"STATUS".padEnd(statusWidth)} REASON
184
+ </span>
185
+ </text>
186
+ </box>
187
+ )}
188
+
189
+ <box flexDirection="column">
190
+ {items.map((item, idx) => {
191
+ const isSelected = idx === selectedIndex;
192
+ const isEnabled = Boolean(toggles[item.id]);
193
+ const canEnable = item.envAvailable;
194
+ const statusLabel = !canEnable ? "DISABLED" : isEnabled ? "ON" : "OFF";
195
+ const reason = !canEnable && item.disabledReason ? item.disabledReason : "";
196
+
197
+ const labelColor = isSelected ? COLORS.DAEMON_LABEL : COLORS.MENU_TEXT;
198
+ const statusColor = !canEnable
199
+ ? COLORS.REASONING_DIM
200
+ : isEnabled
201
+ ? COLORS.DAEMON_TEXT
202
+ : COLORS.REASONING_DIM;
203
+ const reasonColor = COLORS.REASONING_DIM;
204
+
205
+ const labelText = truncateText(item.label, labelWidth).padEnd(labelWidth);
206
+ const statusText = statusLabel.padEnd(statusWidth);
207
+ const reasonText = reason ? truncateText(reason, 60) : "";
208
+
209
+ return (
210
+ <box
211
+ key={item.id}
212
+ backgroundColor={isSelected ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
213
+ paddingLeft={1}
214
+ paddingRight={1}
215
+ >
216
+ <text>
217
+ <span fg={labelColor}>{isSelected ? "▶ " : " "}</span>
218
+ <span fg={labelColor}>{labelText}</span>
219
+ <span fg={COLORS.REASONING_DIM}> </span>
220
+ <span fg={statusColor}>{statusText}</span>
221
+ {showReasonColumn && reasonText ? (
222
+ <>
223
+ <span fg={COLORS.REASONING_DIM}> </span>
224
+ <span fg={reasonColor}>{reasonText}</span>
225
+ </>
226
+ ) : null}
227
+ </text>
228
+ </box>
229
+ );
230
+ })}
231
+ </box>
232
+ </box>
233
+ </box>
234
+ );
235
+ }
@@ -41,6 +41,8 @@ export interface UseAppContextBuilderParams {
41
41
  setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
42
42
  showUrlMenu: boolean;
43
43
  setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
44
+ showToolsMenu: boolean;
45
+ setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
44
46
  };
45
47
 
46
48
  device: {