@makefinks/daemon 0.7.2 → 0.8.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 +2 -1
- package/src/ai/agent-turn-runner.ts +5 -0
- package/src/ai/daemon-ai.ts +74 -24
- package/src/ai/mcp/mcp-manager.ts +348 -0
- package/src/ai/memory/memory-manager.ts +90 -2
- package/src/ai/model-config.ts +1 -1
- package/src/ai/tools/index.ts +14 -12
- package/src/ai/tools/subagents.ts +17 -13
- package/src/app/components/AppOverlays.tsx +2 -0
- package/src/components/SettingsMenu.tsx +13 -0
- package/src/components/ToolCallView.tsx +51 -12
- package/src/components/ToolsMenu.tsx +81 -10
- package/src/hooks/daemon-event-handlers.ts +9 -0
- package/src/hooks/keyboard-handlers.ts +24 -0
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-controller.ts +5 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +11 -0
- package/src/hooks/use-app-settings.ts +6 -0
- package/src/hooks/use-daemon-events.ts +4 -0
- package/src/index.tsx +3 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/daemon-events.ts +2 -0
- package/src/state/daemon-state.ts +10 -0
- package/src/types/index.ts +10 -0
- package/src/utils/config.ts +33 -0
- package/src/utils/preferences.ts +3 -0
- package/src/utils/tool-output-preview.ts +104 -0
|
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
import { getDaemonManager } from "../../state/daemon-state";
|
|
12
12
|
import type { SubagentProgressEmitter } from "../../types";
|
|
13
13
|
import { getOpenRouterReportedCost } from "../../utils/openrouter-reported-cost";
|
|
14
|
+
import { getMcpManager } from "../mcp/mcp-manager";
|
|
14
15
|
import { extractFinalAssistantText } from "../message-utils";
|
|
15
16
|
import { buildOpenRouterChatSettings, getSubagentModel } from "../model-config";
|
|
16
17
|
import { buildToolSet } from "./tool-registry";
|
|
@@ -21,25 +22,28 @@ const openrouter = createOpenRouter();
|
|
|
21
22
|
// Maximum steps for subagent loops
|
|
22
23
|
const MAX_SUBAGENT_STEPS = 30;
|
|
23
24
|
|
|
24
|
-
let
|
|
25
|
+
let cachedSubagentBaseTools: Promise<ToolSet> | null = null;
|
|
25
26
|
|
|
26
27
|
export function invalidateSubagentToolsCache(): void {
|
|
27
|
-
|
|
28
|
+
cachedSubagentBaseTools = null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// Subagent tools (all tools except subagent itself to prevent recursion)
|
|
31
32
|
async function getSubagentTools(): Promise<ToolSet> {
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
if (!cachedSubagentBaseTools) {
|
|
34
|
+
cachedSubagentBaseTools = (async () => {
|
|
35
|
+
const toggles = getDaemonManager().toolToggles;
|
|
36
|
+
const { tools } = await buildToolSet(toggles, {
|
|
37
|
+
omit: ["groundingManager", "subagent"],
|
|
38
|
+
});
|
|
39
|
+
return tools;
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const baseTools = await cachedSubagentBaseTools;
|
|
44
|
+
const mcpTools = getMcpManager().getToolsSnapshot();
|
|
45
|
+
if (Object.keys(mcpTools).length === 0) return baseTools;
|
|
46
|
+
return { ...baseTools, ...mcpTools };
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// System prompt for subagents
|
|
@@ -63,6 +63,7 @@ function AppOverlaysImpl({ conversationHistory, currentContentBlocks }: AppOverl
|
|
|
63
63
|
canEnableVoiceOutput={settings.canEnableVoiceOutput}
|
|
64
64
|
showFullReasoning={settings.showFullReasoning}
|
|
65
65
|
showToolOutput={settings.showToolOutput}
|
|
66
|
+
memoryEnabled={settings.memoryEnabled}
|
|
66
67
|
onClose={() => menus.setShowSettingsMenu(false)}
|
|
67
68
|
toggleInteractionMode={settingsCallbacks.onToggleInteractionMode}
|
|
68
69
|
setVoiceInteractionType={settingsCallbacks.onSetVoiceInteractionType}
|
|
@@ -71,6 +72,7 @@ function AppOverlaysImpl({ conversationHistory, currentContentBlocks }: AppOverl
|
|
|
71
72
|
setBashApprovalLevel={settingsCallbacks.onSetBashApprovalLevel}
|
|
72
73
|
setShowFullReasoning={settings.setShowFullReasoning}
|
|
73
74
|
setShowToolOutput={settings.setShowToolOutput}
|
|
75
|
+
setMemoryEnabled={settings.setMemoryEnabled}
|
|
74
76
|
persistPreferences={settings.persistPreferences}
|
|
75
77
|
/>
|
|
76
78
|
)}
|
|
@@ -35,6 +35,7 @@ interface SettingsMenuProps {
|
|
|
35
35
|
canEnableVoiceOutput: boolean;
|
|
36
36
|
showFullReasoning: boolean;
|
|
37
37
|
showToolOutput: boolean;
|
|
38
|
+
memoryEnabled: boolean;
|
|
38
39
|
onClose: () => void;
|
|
39
40
|
toggleInteractionMode: () => void;
|
|
40
41
|
setVoiceInteractionType: (type: VoiceInteractionType) => void;
|
|
@@ -43,6 +44,7 @@ interface SettingsMenuProps {
|
|
|
43
44
|
setBashApprovalLevel: (level: BashApprovalLevel) => void;
|
|
44
45
|
setShowFullReasoning: (show: boolean) => void;
|
|
45
46
|
setShowToolOutput: (show: boolean) => void;
|
|
47
|
+
setMemoryEnabled: (enabled: boolean) => void;
|
|
46
48
|
persistPreferences: (updates: Partial<AppPreferences>) => void;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -56,6 +58,7 @@ export function SettingsMenu({
|
|
|
56
58
|
canEnableVoiceOutput,
|
|
57
59
|
showFullReasoning,
|
|
58
60
|
showToolOutput,
|
|
61
|
+
memoryEnabled,
|
|
59
62
|
onClose,
|
|
60
63
|
toggleInteractionMode,
|
|
61
64
|
setVoiceInteractionType,
|
|
@@ -64,6 +67,7 @@ export function SettingsMenu({
|
|
|
64
67
|
setBashApprovalLevel,
|
|
65
68
|
setShowFullReasoning,
|
|
66
69
|
setShowToolOutput,
|
|
70
|
+
setMemoryEnabled,
|
|
67
71
|
persistPreferences,
|
|
68
72
|
}: SettingsMenuProps) {
|
|
69
73
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
@@ -115,6 +119,13 @@ export function SettingsMenu({
|
|
|
115
119
|
description: "Require approval for bash commands (NONE / DANGEROUS / ALL)",
|
|
116
120
|
isCyclic: true,
|
|
117
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: "memory-enabled",
|
|
124
|
+
label: "Memory",
|
|
125
|
+
value: memoryEnabled ? "ON" : "OFF",
|
|
126
|
+
description: "Auto-save messages + inject relevant memories",
|
|
127
|
+
isToggle: true,
|
|
128
|
+
},
|
|
118
129
|
];
|
|
119
130
|
|
|
120
131
|
if (interactionMode === "voice") {
|
|
@@ -181,6 +192,7 @@ export function SettingsMenu({
|
|
|
181
192
|
canEnableVoiceOutput,
|
|
182
193
|
showFullReasoning,
|
|
183
194
|
showToolOutput,
|
|
195
|
+
memoryEnabled,
|
|
184
196
|
setSelectedIdx,
|
|
185
197
|
toggleInteractionMode,
|
|
186
198
|
setVoiceInteractionType,
|
|
@@ -189,6 +201,7 @@ export function SettingsMenu({
|
|
|
189
201
|
setBashApprovalLevel,
|
|
190
202
|
setShowFullReasoning,
|
|
191
203
|
setShowToolOutput,
|
|
204
|
+
setMemoryEnabled,
|
|
192
205
|
persistPreferences,
|
|
193
206
|
onClose,
|
|
194
207
|
manager,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { getMcpManager } from "../ai/mcp/mcp-manager";
|
|
2
3
|
import { useToolApprovalForCall } from "../hooks/use-tool-approval";
|
|
3
4
|
import type { ToolCall } from "../types";
|
|
4
5
|
import { COLORS } from "../ui/constants";
|
|
6
|
+
import { formatToolInputLines } from "../utils/formatters";
|
|
7
|
+
import { formatGenericToolOutputPreview } from "../utils/tool-output-preview";
|
|
5
8
|
import { ApprovalPicker } from "./ApprovalPicker";
|
|
6
9
|
import {
|
|
7
10
|
ErrorPreviewView,
|
|
@@ -37,8 +40,19 @@ function ApprovalResultBadge({ result }: { result: "approved" | "denied" }) {
|
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function ToolSectionDivider({ label }: { label: string }) {
|
|
44
|
+
return (
|
|
45
|
+
<box flexDirection="column" paddingLeft={2} marginTop={1}>
|
|
46
|
+
<text>
|
|
47
|
+
<span fg={COLORS.REASONING_DIM}>{`--- ${label} ---`}</span>
|
|
48
|
+
</text>
|
|
49
|
+
</box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
export function ToolCallView({ call, result, showOutput = true }: ToolCallViewProps) {
|
|
41
54
|
const layout = getToolLayout(call.name) ?? defaultToolLayout;
|
|
55
|
+
const mcpMeta = useMemo(() => getMcpManager().getToolMeta(call.name), [call.name]);
|
|
42
56
|
const isAwaitingApproval = call.status === "awaiting_approval";
|
|
43
57
|
const isRunning = call.status === "running" || call.status === "streaming";
|
|
44
58
|
const isFailed = call.status === "failed";
|
|
@@ -47,17 +61,42 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
|
|
|
47
61
|
call.toolCallId
|
|
48
62
|
);
|
|
49
63
|
|
|
50
|
-
const header = useMemo(() =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
const header = useMemo(() => {
|
|
65
|
+
const base = layout.getHeader?.(call.input, result) ?? null;
|
|
66
|
+
if (base) return base;
|
|
67
|
+
if (mcpMeta) {
|
|
68
|
+
return {
|
|
69
|
+
primary: mcpMeta.serverId,
|
|
70
|
+
secondary: mcpMeta.originalToolName,
|
|
71
|
+
secondaryStyle: "dim" as const,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}, [call.input, result, layout, mcpMeta]);
|
|
76
|
+
|
|
77
|
+
const body = useMemo(() => {
|
|
78
|
+
const base = layout.getBody?.(call.input, result, call) ?? null;
|
|
79
|
+
if (base) return base;
|
|
80
|
+
if (!mcpMeta) return null;
|
|
81
|
+
const lines = formatToolInputLines(call.input);
|
|
82
|
+
const normalized = lines.length > 0 ? lines : ["(no input)"];
|
|
83
|
+
return {
|
|
84
|
+
lines: normalized.map((text) => ({
|
|
85
|
+
text,
|
|
86
|
+
color: COLORS.REASONING_DIM,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}, [call.input, result, call, layout, mcpMeta]);
|
|
56
90
|
|
|
57
91
|
const resultPreviewLines = useMemo(() => {
|
|
58
92
|
if (!showOutput) return null;
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
const formatted = layout.formatResult?.(result) ?? null;
|
|
94
|
+
if (formatted) return formatted;
|
|
95
|
+
if (mcpMeta) return formatGenericToolOutputPreview(result);
|
|
96
|
+
return null;
|
|
97
|
+
}, [result, showOutput, layout, mcpMeta]);
|
|
98
|
+
|
|
99
|
+
const hasResultPreview = Boolean(showOutput && resultPreviewLines && resultPreviewLines.length > 0);
|
|
61
100
|
|
|
62
101
|
const toolColor =
|
|
63
102
|
call.status === "completed"
|
|
@@ -65,7 +104,7 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
|
|
|
65
104
|
: isAwaitingApproval
|
|
66
105
|
? COLORS.STATUS_APPROVAL
|
|
67
106
|
: COLORS.TOOLS;
|
|
68
|
-
const toolName = layout.abbreviation ?? getDefaultAbbreviation(call.name);
|
|
107
|
+
const toolName = mcpMeta ? "mcp" : (layout.abbreviation ?? getDefaultAbbreviation(call.name));
|
|
69
108
|
const borderColor = getStatusBorderColor(call.status);
|
|
70
109
|
|
|
71
110
|
const customBody = layout.renderBody ? layout.renderBody({ call, result, showOutput }) : null;
|
|
@@ -98,10 +137,10 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
|
|
|
98
137
|
/>
|
|
99
138
|
)}
|
|
100
139
|
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
)}
|
|
140
|
+
{hasResultPreview && <ToolSectionDivider label="OUTPUT" />}
|
|
141
|
+
{hasResultPreview && <ResultPreviewView lines={resultPreviewLines ?? []} />}
|
|
104
142
|
|
|
143
|
+
{isFailed && call.error && <ToolSectionDivider label="ERROR" />}
|
|
105
144
|
{isFailed && call.error && <ErrorPreviewView error={call.error} />}
|
|
106
145
|
|
|
107
146
|
{call.approvalResult && <ApprovalResultBadge result={call.approvalResult} />}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
|
|
3
|
+
import { type McpServerStatus, getMcpManager } from "../ai/mcp/mcp-manager";
|
|
3
4
|
import { invalidateDaemonToolsCache } from "../ai/tools/index";
|
|
4
5
|
import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
|
|
5
6
|
import {
|
|
@@ -13,6 +14,7 @@ import { getDaemonManager } from "../state/daemon-state";
|
|
|
13
14
|
import type { ToolToggleId, ToolToggles } from "../types";
|
|
14
15
|
import { DEFAULT_TOOL_TOGGLES } from "../types";
|
|
15
16
|
import { COLORS } from "../ui/constants";
|
|
17
|
+
import { getManualConfigPath } from "../utils/config";
|
|
16
18
|
|
|
17
19
|
interface ToolsMenuProps {
|
|
18
20
|
persistPreferences: (updates: Partial<{ toolToggles: ToolToggles }>) => void;
|
|
@@ -52,9 +54,21 @@ function getToolLabel(id: ToolToggleId): string {
|
|
|
52
54
|
export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
|
|
53
55
|
const manager = getDaemonManager();
|
|
54
56
|
const [toggles, setToggles] = useState<ToolToggles>(manager.toolToggles ?? { ...DEFAULT_TOOL_TOGGLES });
|
|
57
|
+
const [mcpServers, setMcpServers] = useState<McpServerStatus[]>(() => getMcpManager().getServersSnapshot());
|
|
55
58
|
|
|
56
59
|
const [toolAvailability, setToolAvailability] = useState<Record<ToolToggleId, MenuToolItem> | null>(null);
|
|
57
60
|
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const mcp = getMcpManager();
|
|
63
|
+
const handleUpdate = () => {
|
|
64
|
+
setMcpServers(mcp.getServersSnapshot());
|
|
65
|
+
};
|
|
66
|
+
mcp.on("update", handleUpdate);
|
|
67
|
+
return () => {
|
|
68
|
+
mcp.off("update", handleUpdate);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
58
72
|
useEffect(() => {
|
|
59
73
|
let cancelled = false;
|
|
60
74
|
const loadAvailability = async () => {
|
|
@@ -134,12 +148,20 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
|
|
|
134
148
|
}, [items, showReasonColumn]);
|
|
135
149
|
|
|
136
150
|
const statusWidth = 8;
|
|
151
|
+
const mcpStatusWidth = 8;
|
|
137
152
|
|
|
138
153
|
function truncateText(text: string, maxLen: number): string {
|
|
139
154
|
if (text.length <= maxLen) return text;
|
|
140
155
|
return text.slice(0, Math.max(0, maxLen - 1)) + "…";
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
const mcpConfigPath = useMemo(() => getManualConfigPath(), []);
|
|
159
|
+
|
|
160
|
+
const mcpIdWidth = useMemo(() => {
|
|
161
|
+
const raw = mcpServers.reduce((max, server) => Math.max(max, server.id.length), 0);
|
|
162
|
+
return Math.min(Math.max(raw, 10), 28);
|
|
163
|
+
}, [mcpServers]);
|
|
164
|
+
|
|
143
165
|
return (
|
|
144
166
|
<box
|
|
145
167
|
position="absolute"
|
|
@@ -176,15 +198,13 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
|
|
|
176
198
|
</text>
|
|
177
199
|
</box>
|
|
178
200
|
|
|
179
|
-
{
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
</box>
|
|
187
|
-
)}
|
|
201
|
+
<box marginBottom={1}>
|
|
202
|
+
<text>
|
|
203
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
204
|
+
{` ${"TOOL".padEnd(labelWidth)} ${"STATUS".padEnd(statusWidth)}${showReasonColumn ? " REASON" : ""}`}
|
|
205
|
+
</span>
|
|
206
|
+
</text>
|
|
207
|
+
</box>
|
|
188
208
|
|
|
189
209
|
<box flexDirection="column">
|
|
190
210
|
{items.map((item, idx) => {
|
|
@@ -218,7 +238,7 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
|
|
|
218
238
|
<span fg={labelColor}>{labelText}</span>
|
|
219
239
|
<span fg={COLORS.REASONING_DIM}> </span>
|
|
220
240
|
<span fg={statusColor}>{statusText}</span>
|
|
221
|
-
{showReasonColumn
|
|
241
|
+
{showReasonColumn ? (
|
|
222
242
|
<>
|
|
223
243
|
<span fg={COLORS.REASONING_DIM}> </span>
|
|
224
244
|
<span fg={reasonColor}>{reasonText}</span>
|
|
@@ -229,6 +249,57 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
|
|
|
229
249
|
);
|
|
230
250
|
})}
|
|
231
251
|
</box>
|
|
252
|
+
|
|
253
|
+
<box flexDirection="column" marginTop={1}>
|
|
254
|
+
<box marginBottom={1}>
|
|
255
|
+
<text>
|
|
256
|
+
<span fg={COLORS.DAEMON_LABEL}>[ MCP ]</span>
|
|
257
|
+
<span fg={COLORS.REASONING_DIM}>{` ${truncateText(mcpConfigPath, 80)}`}</span>
|
|
258
|
+
</text>
|
|
259
|
+
</box>
|
|
260
|
+
|
|
261
|
+
{mcpServers.length === 0 ? (
|
|
262
|
+
<text>
|
|
263
|
+
<span fg={COLORS.REASONING_DIM}>No MCP servers configured.</span>
|
|
264
|
+
</text>
|
|
265
|
+
) : (
|
|
266
|
+
<box flexDirection="column">
|
|
267
|
+
<box marginBottom={1}>
|
|
268
|
+
<text>
|
|
269
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
270
|
+
{`SERVER`.padEnd(mcpIdWidth)} {`STATUS`.padEnd(mcpStatusWidth)} TOOLS
|
|
271
|
+
</span>
|
|
272
|
+
</text>
|
|
273
|
+
</box>
|
|
274
|
+
{mcpServers.map((server) => {
|
|
275
|
+
const statusLabel = server.status.toUpperCase();
|
|
276
|
+
const statusColor =
|
|
277
|
+
server.status === "ready"
|
|
278
|
+
? COLORS.STATUS_COMPLETED
|
|
279
|
+
: server.status === "loading"
|
|
280
|
+
? COLORS.STATUS_RUNNING
|
|
281
|
+
: server.status === "error"
|
|
282
|
+
? COLORS.STATUS_FAILED
|
|
283
|
+
: COLORS.REASONING_DIM;
|
|
284
|
+
const idText = truncateText(server.id, mcpIdWidth).padEnd(mcpIdWidth);
|
|
285
|
+
const toolsText = String(server.toolCount).padStart(4);
|
|
286
|
+
const errorText = server.error ? truncateText(server.error, 60) : "";
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<box key={server.id} flexDirection="column">
|
|
290
|
+
<text>
|
|
291
|
+
<span fg={COLORS.MENU_TEXT}>{idText}</span>
|
|
292
|
+
<span fg={COLORS.REASONING_DIM}> </span>
|
|
293
|
+
<span fg={statusColor}>{statusLabel.padEnd(mcpStatusWidth)}</span>
|
|
294
|
+
<span fg={COLORS.REASONING_DIM}> {toolsText}</span>
|
|
295
|
+
{errorText ? <span fg={COLORS.REASONING_DIM}>{` ${errorText}`}</span> : null}
|
|
296
|
+
</text>
|
|
297
|
+
</box>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
300
|
+
</box>
|
|
301
|
+
)}
|
|
302
|
+
</box>
|
|
232
303
|
</box>
|
|
233
304
|
</box>
|
|
234
305
|
);
|
|
@@ -12,6 +12,7 @@ import { saveSessionSnapshot } from "../state/session-store";
|
|
|
12
12
|
import type {
|
|
13
13
|
ContentBlock,
|
|
14
14
|
ConversationMessage,
|
|
15
|
+
MemoryToastPreview,
|
|
15
16
|
ModelMessage,
|
|
16
17
|
SubagentStep,
|
|
17
18
|
TokenUsage,
|
|
@@ -56,6 +57,14 @@ function clearAvatarToolEffects(avatar: DaemonAvatarRenderable | null): void {
|
|
|
56
57
|
avatar.setTypingMode(false);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
export function createMemorySavedHandler() {
|
|
61
|
+
return (preview: MemoryToastPreview) => {
|
|
62
|
+
const description = preview.description?.trim();
|
|
63
|
+
if (!description) return;
|
|
64
|
+
toast.success(`Memory saved (${preview.operation})`, { description });
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
function finalizePendingUserMessage(
|
|
60
69
|
prev: ConversationMessage[],
|
|
61
70
|
userText: string,
|
|
@@ -256,6 +256,7 @@ interface SettingsMenuContext {
|
|
|
256
256
|
canEnableVoiceOutput: boolean;
|
|
257
257
|
showFullReasoning: boolean;
|
|
258
258
|
showToolOutput: boolean;
|
|
259
|
+
memoryEnabled: boolean;
|
|
259
260
|
setSelectedIdx: (fn: (prev: number) => number) => void;
|
|
260
261
|
toggleInteractionMode: () => void;
|
|
261
262
|
setVoiceInteractionType: (type: VoiceInteractionType) => void;
|
|
@@ -264,6 +265,7 @@ interface SettingsMenuContext {
|
|
|
264
265
|
setBashApprovalLevel: (level: BashApprovalLevel) => void;
|
|
265
266
|
setShowFullReasoning: (show: boolean) => void;
|
|
266
267
|
setShowToolOutput: (show: boolean) => void;
|
|
268
|
+
setMemoryEnabled: (enabled: boolean) => void;
|
|
267
269
|
persistPreferences: (updates: Partial<AppPreferences>) => void;
|
|
268
270
|
onClose: () => void;
|
|
269
271
|
manager: {
|
|
@@ -272,6 +274,7 @@ interface SettingsMenuContext {
|
|
|
272
274
|
speechSpeed: SpeechSpeed;
|
|
273
275
|
reasoningEffort: ReasoningEffort;
|
|
274
276
|
bashApprovalLevel: BashApprovalLevel;
|
|
277
|
+
memoryEnabled: boolean;
|
|
275
278
|
};
|
|
276
279
|
}
|
|
277
280
|
|
|
@@ -353,6 +356,16 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
|
|
|
353
356
|
settingIdx++;
|
|
354
357
|
|
|
355
358
|
if (ctx.interactionMode === "voice") {
|
|
359
|
+
if (ctx.selectedIdx === settingIdx) {
|
|
360
|
+
const next = !ctx.manager.memoryEnabled;
|
|
361
|
+
ctx.manager.memoryEnabled = next;
|
|
362
|
+
ctx.setMemoryEnabled(next);
|
|
363
|
+
ctx.persistPreferences({ memoryEnabled: next });
|
|
364
|
+
key.preventDefault();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
settingIdx++;
|
|
368
|
+
|
|
356
369
|
if (ctx.selectedIdx === settingIdx) {
|
|
357
370
|
const speeds: SpeechSpeed[] = [1.0, 1.25, 1.5, 1.75, 2.0];
|
|
358
371
|
const currentSpeed = ctx.manager.speechSpeed;
|
|
@@ -367,6 +380,17 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
|
|
|
367
380
|
}
|
|
368
381
|
settingIdx++;
|
|
369
382
|
}
|
|
383
|
+
if (ctx.interactionMode !== "voice") {
|
|
384
|
+
if (ctx.selectedIdx === settingIdx) {
|
|
385
|
+
const next = !ctx.manager.memoryEnabled;
|
|
386
|
+
ctx.manager.memoryEnabled = next;
|
|
387
|
+
ctx.setMemoryEnabled(next);
|
|
388
|
+
ctx.persistPreferences({ memoryEnabled: next });
|
|
389
|
+
key.preventDefault();
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
settingIdx++;
|
|
393
|
+
}
|
|
370
394
|
|
|
371
395
|
if (ctx.selectedIdx === settingIdx) {
|
|
372
396
|
const next = !ctx.showFullReasoning;
|
|
@@ -70,6 +70,8 @@ export interface UseAppContextBuilderParams {
|
|
|
70
70
|
setShowFullReasoning: (show: boolean) => void;
|
|
71
71
|
showToolOutput: boolean;
|
|
72
72
|
setShowToolOutput: (show: boolean) => void;
|
|
73
|
+
memoryEnabled: boolean;
|
|
74
|
+
setMemoryEnabled: (enabled: boolean) => void;
|
|
73
75
|
setBashApprovalLevel: (level: BashApprovalLevel) => void;
|
|
74
76
|
persistPreferences: (updates: Partial<AppPreferences>) => void;
|
|
75
77
|
};
|
|
@@ -113,6 +113,8 @@ export function useAppController({
|
|
|
113
113
|
setShowFullReasoning,
|
|
114
114
|
showToolOutput,
|
|
115
115
|
setShowToolOutput,
|
|
116
|
+
memoryEnabled,
|
|
117
|
+
setMemoryEnabled,
|
|
116
118
|
canEnableVoiceOutput,
|
|
117
119
|
} = appSettings;
|
|
118
120
|
|
|
@@ -180,6 +182,7 @@ export function useAppController({
|
|
|
180
182
|
setBashApprovalLevel,
|
|
181
183
|
setShowFullReasoning,
|
|
182
184
|
setShowToolOutput,
|
|
185
|
+
setMemoryEnabled,
|
|
183
186
|
setLoadedPreferences: bootstrap.setLoadedPreferences,
|
|
184
187
|
setOnboardingActive: bootstrap.setOnboardingActive,
|
|
185
188
|
setOnboardingStep: bootstrap.setOnboardingStep,
|
|
@@ -468,6 +471,8 @@ export function useAppController({
|
|
|
468
471
|
setShowFullReasoning,
|
|
469
472
|
showToolOutput,
|
|
470
473
|
setShowToolOutput,
|
|
474
|
+
memoryEnabled,
|
|
475
|
+
setMemoryEnabled,
|
|
471
476
|
setBashApprovalLevel,
|
|
472
477
|
persistPreferences,
|
|
473
478
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { startMcpManager } from "../ai/mcp/mcp-manager";
|
|
2
3
|
import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
|
|
3
4
|
import type {
|
|
4
5
|
AppPreferences,
|
|
@@ -20,6 +21,7 @@ export interface UseAppPreferencesBootstrapParams {
|
|
|
20
21
|
speechSpeed: SpeechSpeed;
|
|
21
22
|
reasoningEffort: ReasoningEffort;
|
|
22
23
|
bashApprovalLevel: BashApprovalLevel;
|
|
24
|
+
memoryEnabled: boolean;
|
|
23
25
|
toolToggles?: ToolToggles;
|
|
24
26
|
audioDeviceName?: string;
|
|
25
27
|
outputDeviceName?: string;
|
|
@@ -35,6 +37,7 @@ export interface UseAppPreferencesBootstrapParams {
|
|
|
35
37
|
setBashApprovalLevel: (level: BashApprovalLevel) => void;
|
|
36
38
|
setShowFullReasoning: (show: boolean) => void;
|
|
37
39
|
setShowToolOutput: (show: boolean) => void;
|
|
40
|
+
setMemoryEnabled: (enabled: boolean) => void;
|
|
38
41
|
setLoadedPreferences: (prefs: AppPreferences | null) => void;
|
|
39
42
|
setOnboardingActive: (active: boolean) => void;
|
|
40
43
|
setOnboardingStep: (step: OnboardingStep) => void;
|
|
@@ -61,6 +64,7 @@ export function useAppPreferencesBootstrap(
|
|
|
61
64
|
setBashApprovalLevel,
|
|
62
65
|
setShowFullReasoning,
|
|
63
66
|
setShowToolOutput,
|
|
67
|
+
setMemoryEnabled,
|
|
64
68
|
setLoadedPreferences,
|
|
65
69
|
setOnboardingActive,
|
|
66
70
|
setOnboardingStep,
|
|
@@ -93,6 +97,9 @@ export function useAppPreferencesBootstrap(
|
|
|
93
97
|
process.env.EXA_API_KEY = prefs.exaApiKey;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
// Start MCP discovery in the background (non-blocking)
|
|
101
|
+
startMcpManager();
|
|
102
|
+
|
|
96
103
|
if (prefs?.modelId) {
|
|
97
104
|
setResponseModel(prefs.modelId);
|
|
98
105
|
setCurrentModelId(prefs.modelId);
|
|
@@ -154,6 +161,10 @@ export function useAppPreferencesBootstrap(
|
|
|
154
161
|
if (prefs?.showToolOutput !== undefined) {
|
|
155
162
|
setShowToolOutput(prefs.showToolOutput);
|
|
156
163
|
}
|
|
164
|
+
if (prefs?.memoryEnabled !== undefined) {
|
|
165
|
+
manager.memoryEnabled = prefs.memoryEnabled;
|
|
166
|
+
setMemoryEnabled(prefs.memoryEnabled);
|
|
167
|
+
}
|
|
157
168
|
|
|
158
169
|
const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY);
|
|
159
170
|
const hasOpenAiKey = Boolean(process.env.OPENAI_API_KEY);
|
|
@@ -24,6 +24,9 @@ export interface UseAppSettingsReturn {
|
|
|
24
24
|
showToolOutput: boolean;
|
|
25
25
|
setShowToolOutput: React.Dispatch<React.SetStateAction<boolean>>;
|
|
26
26
|
|
|
27
|
+
memoryEnabled: boolean;
|
|
28
|
+
setMemoryEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
|
29
|
+
|
|
27
30
|
canEnableVoiceOutput: boolean;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -39,6 +42,7 @@ export function useAppSettings(): UseAppSettingsReturn {
|
|
|
39
42
|
);
|
|
40
43
|
const [showFullReasoning, setShowFullReasoning] = useState(true);
|
|
41
44
|
const [showToolOutput, setShowToolOutput] = useState(false);
|
|
45
|
+
const [memoryEnabled, setMemoryEnabled] = useState(manager.memoryEnabled);
|
|
42
46
|
|
|
43
47
|
const canEnableVoiceOutput = Boolean(process.env.OPENAI_API_KEY);
|
|
44
48
|
|
|
@@ -57,6 +61,8 @@ export function useAppSettings(): UseAppSettingsReturn {
|
|
|
57
61
|
setShowFullReasoning,
|
|
58
62
|
showToolOutput,
|
|
59
63
|
setShowToolOutput,
|
|
64
|
+
memoryEnabled,
|
|
65
|
+
setMemoryEnabled,
|
|
60
66
|
canEnableVoiceOutput,
|
|
61
67
|
};
|
|
62
68
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createCancelledHandler,
|
|
21
21
|
createCompleteHandler,
|
|
22
22
|
createErrorHandler,
|
|
23
|
+
createMemorySavedHandler,
|
|
23
24
|
createMicLevelHandler,
|
|
24
25
|
createReasoningTokenHandler,
|
|
25
26
|
createStateChangeHandler,
|
|
@@ -320,6 +321,7 @@ export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsR
|
|
|
320
321
|
const handleToolResult = createToolResultHandler(refs, setters);
|
|
321
322
|
const handleComplete = createCompleteHandler(refs, setters, deps);
|
|
322
323
|
const handleCancelled = createCancelledHandler(refs, setters, deps);
|
|
324
|
+
const handleMemorySaved = createMemorySavedHandler();
|
|
323
325
|
const handleError = createErrorHandler(setters);
|
|
324
326
|
|
|
325
327
|
daemonEvents.on("stateChange", handleStateChange);
|
|
@@ -337,6 +339,7 @@ export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsR
|
|
|
337
339
|
daemonEvents.on("subagentToolResult", handleSubagentToolResult);
|
|
338
340
|
daemonEvents.on("subagentComplete", handleSubagentComplete);
|
|
339
341
|
daemonEvents.on("stepUsage", handleStepUsage);
|
|
342
|
+
daemonEvents.on("memorySaved", handleMemorySaved);
|
|
340
343
|
daemonEvents.on("responseToken", handleToken);
|
|
341
344
|
daemonEvents.on("responseComplete", handleComplete);
|
|
342
345
|
daemonEvents.on("cancelled", handleCancelled);
|
|
@@ -364,6 +367,7 @@ export function useDaemonEvents(params: UseDaemonEventsParams): UseDaemonEventsR
|
|
|
364
367
|
daemonEvents.off("subagentToolResult", handleSubagentToolResult);
|
|
365
368
|
daemonEvents.off("subagentComplete", handleSubagentComplete);
|
|
366
369
|
daemonEvents.off("stepUsage", handleStepUsage);
|
|
370
|
+
daemonEvents.off("memorySaved", handleMemorySaved);
|
|
367
371
|
daemonEvents.off("responseToken", handleToken);
|
|
368
372
|
daemonEvents.off("responseComplete", handleComplete);
|
|
369
373
|
daemonEvents.off("cancelled", handleCancelled);
|
package/src/index.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ConsolePosition, createCliRenderer } from "@opentui/core";
|
|
7
7
|
import { createRoot } from "@opentui/react";
|
|
8
|
+
import { destroyMcpManager } from "./ai/mcp/mcp-manager";
|
|
8
9
|
import { App } from "./app/App";
|
|
9
10
|
import { destroyDaemonManager } from "./state/daemon-state";
|
|
10
11
|
import { COLORS } from "./ui/constants";
|
|
@@ -38,10 +39,12 @@ renderer.keyInput.on("paste", (event) => {
|
|
|
38
39
|
// Cleanup on exit
|
|
39
40
|
process.on("exit", () => {
|
|
40
41
|
destroyDaemonManager();
|
|
42
|
+
destroyMcpManager();
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
process.on("SIGINT", () => {
|
|
44
46
|
destroyDaemonManager();
|
|
47
|
+
destroyMcpManager();
|
|
45
48
|
process.exit(0);
|
|
46
49
|
});
|
|
47
50
|
|
|
@@ -60,6 +60,8 @@ export interface SettingsState {
|
|
|
60
60
|
setShowFullReasoning: (show: boolean) => void;
|
|
61
61
|
showToolOutput: boolean;
|
|
62
62
|
setShowToolOutput: (show: boolean) => void;
|
|
63
|
+
memoryEnabled: boolean;
|
|
64
|
+
setMemoryEnabled: (enabled: boolean) => void;
|
|
63
65
|
setBashApprovalLevel: (level: BashApprovalLevel) => void;
|
|
64
66
|
persistPreferences: (updates: Partial<AppPreferences>) => void;
|
|
65
67
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
MemoryToastPreview,
|
|
4
5
|
ModelMessage,
|
|
5
6
|
TokenUsage,
|
|
6
7
|
ToolCallStatus,
|
|
@@ -32,6 +33,7 @@ export type DaemonStateEvents = {
|
|
|
32
33
|
subagentComplete: (toolCallId: string, success: boolean) => void;
|
|
33
34
|
responseToken: (token: string) => void;
|
|
34
35
|
stepUsage: (usage: TokenUsage) => void;
|
|
36
|
+
memorySaved: (preview: MemoryToastPreview) => void;
|
|
35
37
|
responseComplete: (fullText: string, responseMessages: ModelMessage[], usage?: TokenUsage) => void;
|
|
36
38
|
userMessage: (text: string) => void;
|
|
37
39
|
speakingStart: () => void;
|