@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.
@@ -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 cachedSubagentTools: Promise<ToolSet> | null = null;
25
+ let cachedSubagentBaseTools: Promise<ToolSet> | null = null;
25
26
 
26
27
  export function invalidateSubagentToolsCache(): void {
27
- cachedSubagentTools = null;
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 (cachedSubagentTools) return cachedSubagentTools;
33
-
34
- cachedSubagentTools = (async () => {
35
- const toggles = getDaemonManager().toolToggles;
36
- const { tools } = await buildToolSet(toggles, {
37
- omit: ["groundingManager", "subagent"],
38
- });
39
- return tools;
40
- })();
41
-
42
- return cachedSubagentTools;
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(() => layout.getHeader?.(call.input, result) ?? null, [call.input, result, layout]);
51
-
52
- const body = useMemo(
53
- () => layout.getBody?.(call.input, result, call) ?? null,
54
- [call.input, result, call, layout]
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
- return layout.formatResult?.(result) ?? null;
60
- }, [result, showOutput, layout]);
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
- {showOutput && resultPreviewLines && resultPreviewLines.length > 0 && (
102
- <ResultPreviewView lines={resultPreviewLines} />
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
- {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
- )}
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 && reasonText ? (
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;