@makefinks/daemon 0.2.0 → 0.3.1
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 +1 -1
- 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 -641
- package/src/app/components/AppOverlays.tsx +8 -0
- package/src/components/ContentBlockView.tsx +5 -11
- package/src/components/HotkeysPane.tsx +2 -1
- package/src/components/ToolsMenu.tsx +235 -0
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-controller.ts +546 -0
- package/src/hooks/use-app-menus.ts +6 -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 +28 -8
- package/src/hooks/use-daemon-runtime-controller.ts +147 -0
- package/src/hooks/use-overlay-controller.ts +6 -0
- package/src/hooks/use-reasoning-animation.ts +8 -3
- package/src/hooks/use-session-controller.ts +79 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/daemon-state.ts +19 -8
- package/src/types/index.ts +25 -0
- package/src/ui/constants.ts +5 -3
- package/src/ui/reasoning-ticker.tsx +38 -0
- package/src/utils/preferences.ts +10 -0
|
@@ -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}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Component for rendering a single content block (reasoning, tool, or text).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { DaemonText } from "./DaemonText";
|
|
6
|
-
import { ToolCallView } from "./ToolCallView";
|
|
7
|
-
import { COLORS, REASONING_MARKDOWN_STYLE } from "../ui/constants";
|
|
8
5
|
import type { ContentBlock } from "../types";
|
|
6
|
+
import { COLORS, REASONING_MARKDOWN_STYLE } from "../ui/constants";
|
|
7
|
+
import { renderReasoningTicker } from "../ui/reasoning-ticker";
|
|
9
8
|
import { formatElapsedTime, hasVisibleText } from "../utils/formatters";
|
|
9
|
+
import { DaemonText } from "./DaemonText";
|
|
10
|
+
import { ToolCallView } from "./ToolCallView";
|
|
10
11
|
|
|
11
12
|
interface ContentBlockViewProps {
|
|
12
13
|
block: ContentBlock;
|
|
@@ -64,14 +65,7 @@ export function ContentBlockView({
|
|
|
64
65
|
|
|
65
66
|
// For non-full-reasoning mode, show animated display only for the latest reasoning block
|
|
66
67
|
if (showReasoningTicker && isLastReasoningBlock && reasoningDisplay) {
|
|
67
|
-
return (
|
|
68
|
-
<text>
|
|
69
|
-
<span fg={COLORS.REASONING_DIM}>
|
|
70
|
-
{"// "}
|
|
71
|
-
{reasoningDisplay}
|
|
72
|
-
</span>
|
|
73
|
-
</text>
|
|
74
|
-
);
|
|
68
|
+
return renderReasoningTicker(reasoningDisplay);
|
|
75
69
|
}
|
|
76
70
|
const durationLabel =
|
|
77
71
|
block.durationMs !== undefined
|
|
@@ -36,7 +36,7 @@ export function HotkeysPane({ onClose }: HotkeysPaneProps) {
|
|
|
36
36
|
{
|
|
37
37
|
title: "SESSION",
|
|
38
38
|
items: [
|
|
39
|
-
{ key: "
|
|
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: {
|