@makefinks/daemon 0.7.2 → 0.9.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.
@@ -1,4 +1,5 @@
1
1
  import type { ToolLayoutConfig, ToolHeader } from "../types";
2
+ import { COLORS } from "../../../ui/constants";
2
3
  import { registerToolLayout } from "../registry";
3
4
 
4
5
  type UnknownRecord = Record<string, unknown>;
@@ -7,42 +8,77 @@ function isRecord(value: unknown): value is UnknownRecord {
7
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
9
  }
9
10
 
10
- interface FetchUrlsInput {
11
+ interface FetchUrlsRequestInput {
11
12
  url: string;
12
13
  lineOffset?: number;
13
14
  lineLimit?: number;
14
- highlightQuery?: string;
15
15
  }
16
16
 
17
- function extractFetchUrlsInput(input: unknown): FetchUrlsInput | null {
17
+ type FetchUrlsResultItem = {
18
+ success?: unknown;
19
+ url?: unknown;
20
+ text?: unknown;
21
+ lineOffset?: unknown;
22
+ lineLimit?: unknown;
23
+ remainingLines?: unknown;
24
+ error?: unknown;
25
+ title?: unknown;
26
+ };
27
+
28
+ function extractFetchUrlsRequests(input: unknown): FetchUrlsRequestInput[] | null {
29
+ if (!isRecord(input)) return null;
30
+ if (!("requests" in input) || !Array.isArray(input.requests)) return null;
31
+
32
+ const requests: FetchUrlsRequestInput[] = [];
33
+ for (const item of input.requests) {
34
+ if (!isRecord(item)) continue;
35
+ if (!("url" in item) || typeof item.url !== "string") continue;
36
+ const lineOffset =
37
+ "lineOffset" in item && typeof item.lineOffset === "number" ? item.lineOffset : undefined;
38
+ const lineLimit = "lineLimit" in item && typeof item.lineLimit === "number" ? item.lineLimit : undefined;
39
+ requests.push({ url: item.url, lineOffset, lineLimit });
40
+ }
41
+
42
+ return requests.length > 0 ? requests : null;
43
+ }
44
+
45
+ function extractRenderUrlInput(input: unknown): FetchUrlsRequestInput | null {
46
+ if (!input) return null;
18
47
  if (!isRecord(input)) return null;
19
48
  if (!("url" in input) || typeof input.url !== "string") return null;
20
49
 
21
50
  const lineOffset =
22
51
  "lineOffset" in input && typeof input.lineOffset === "number" ? input.lineOffset : undefined;
23
52
  const lineLimit = "lineLimit" in input && typeof input.lineLimit === "number" ? input.lineLimit : undefined;
24
- const highlightQuery =
25
- "highlightQuery" in input && typeof input.highlightQuery === "string" ? input.highlightQuery : undefined;
26
- return { url: input.url, lineOffset, lineLimit, highlightQuery };
53
+ return { url: input.url, lineOffset, lineLimit };
27
54
  }
28
55
 
29
- function mergeFetchUrlsDefaults(input: FetchUrlsInput | null, result?: unknown): FetchUrlsInput | null {
30
- if (!input) return null;
31
- if (!result || typeof result !== "object") return input;
56
+ function extractFetchUrlsResults(result?: unknown): FetchUrlsResultItem[] | null {
57
+ if (!result || typeof result !== "object") return null;
58
+ const record = result as Record<string, unknown>;
59
+ const container = extractToolDataContainer(record);
60
+ if (!isRecord(container)) return null;
61
+
62
+ if (Array.isArray(container.results)) {
63
+ return container.results.filter((item): item is FetchUrlsResultItem => isRecord(item));
64
+ }
32
65
 
33
- const resultRecord = result as Record<string, unknown>;
66
+ return null;
67
+ }
68
+
69
+ function mergeFetchUrlsDefaults(
70
+ input: FetchUrlsRequestInput,
71
+ result?: FetchUrlsResultItem | null
72
+ ): FetchUrlsRequestInput {
73
+ if (!result) return input;
34
74
  const lineOffset =
35
- input.lineOffset ?? (typeof resultRecord.lineOffset === "number" ? resultRecord.lineOffset : undefined);
36
- const lineLimit =
37
- input.lineLimit ?? (typeof resultRecord.lineLimit === "number" ? resultRecord.lineLimit : undefined);
75
+ input.lineOffset ?? (typeof result.lineOffset === "number" ? result.lineOffset : undefined);
76
+ const lineLimit = input.lineLimit ?? (typeof result.lineLimit === "number" ? result.lineLimit : undefined);
38
77
 
39
78
  return { ...input, lineOffset, lineLimit };
40
79
  }
41
80
 
42
- function formatFetchUrlsHeader(input: FetchUrlsInput): string {
43
- if (input.highlightQuery) {
44
- return `highlight: "${input.highlightQuery}"`;
45
- }
81
+ function formatFetchUrlsHeader(input: FetchUrlsRequestInput): string {
46
82
  const parts: string[] = [];
47
83
  if (input.lineOffset !== undefined) {
48
84
  parts.push(`lineOffset=${input.lineOffset}`);
@@ -57,6 +93,10 @@ function normalizeWhitespace(text: string): string {
57
93
  return text.replace(/\r\n/g, "\n").replace(/\t/g, " ");
58
94
  }
59
95
 
96
+ function escapeXmlAttribute(value: string): string {
97
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
98
+ }
99
+
60
100
  type ExaLikeItem = {
61
101
  title?: unknown;
62
102
  url?: unknown;
@@ -64,6 +104,8 @@ type ExaLikeItem = {
64
104
  lineOffset?: unknown;
65
105
  lineLimit?: unknown;
66
106
  remainingLines?: unknown;
107
+ totalLines?: unknown;
108
+ error?: unknown;
67
109
  };
68
110
 
69
111
  function formatExaItemLabel(item: ExaLikeItem): string {
@@ -78,76 +120,69 @@ function extractToolDataContainer(result: UnknownRecord): unknown {
78
120
  }
79
121
 
80
122
  function formatFetchUrlsResult(result: unknown): string[] | null {
123
+ if (typeof result === "string") {
124
+ const lines = result.split("\n");
125
+ const MAX_LINES = 8;
126
+ if (lines.length <= MAX_LINES) return lines;
127
+ return [...lines.slice(0, MAX_LINES - 1), " ..."];
128
+ }
81
129
  if (!isRecord(result)) return null;
82
130
  if (result.success === false && typeof result.error === "string") {
83
131
  return [`error: ${result.error}`];
84
132
  }
85
133
  if (result.success !== true) return null;
86
134
 
87
- if (Array.isArray(result.highlights)) {
88
- return formatHighlightsResult(result);
89
- }
135
+ const items = extractFetchUrlsResults(result);
136
+ if (!items) return null;
90
137
 
91
- const data = extractToolDataContainer(result);
92
- const candidate = isRecord(data) ? (data as ExaLikeItem) : {};
93
- const label = formatExaItemLabel(candidate);
94
- const url = typeof candidate.url === "string" ? candidate.url : "";
95
- const title = typeof candidate.title === "string" ? candidate.title : "";
96
- const lineOffset = typeof candidate.lineOffset === "number" ? candidate.lineOffset : undefined;
97
- const lineLimit = typeof candidate.lineLimit === "number" ? candidate.lineLimit : undefined;
98
- const remainingLines =
99
- typeof candidate.remainingLines === "number" || candidate.remainingLines === null
100
- ? candidate.remainingLines
101
- : undefined;
102
- const rangeParts: string[] = [];
103
- if (lineOffset !== undefined) rangeParts.push(`lineOffset=${lineOffset}`);
104
- if (lineLimit !== undefined) rangeParts.push(`lineLimit=${lineLimit}`);
105
- if (remainingLines !== undefined) {
106
- rangeParts.push(remainingLines === null ? "remainingLines=unknown" : `remainingLines=${remainingLines}`);
107
- }
108
- const remainingSuffix = rangeParts.length > 0 ? ` (${rangeParts.join(", ")})` : "";
109
-
110
- const headerBase = url && title ? `${label} — ${url}` : label;
111
- const header = `${headerBase}${remainingSuffix}`;
112
-
113
- const text = typeof candidate.text === "string" ? candidate.text : "";
114
- if (!text.trim()) return [header];
115
-
116
- const MAX_LINES = 4;
138
+ const lines: string[] = ["<fetchUrls>"];
139
+ const MAX_LINES = 2;
117
140
  const MAX_CHARS = 160;
118
- const snippet = normalizeWhitespace(text)
119
- .replace(/\n{3,}/g, "\n\n")
120
- .trim()
121
- .split("\n")
122
- .slice(0, MAX_LINES)
123
- .map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
124
-
125
- return [header, ...snippet];
126
- }
141
+ const maxItems = 3;
142
+
143
+ for (const item of items.slice(0, maxItems)) {
144
+ const candidate = item as ExaLikeItem;
145
+ const url = typeof candidate.url === "string" ? candidate.url : "";
146
+ if (!url) continue;
147
+
148
+ const attributes: string[] = [`href="${escapeXmlAttribute(url)}"`];
149
+ if (typeof candidate.lineOffset === "number") attributes.push(`lineOffset="${candidate.lineOffset}"`);
150
+ if (typeof candidate.lineLimit === "number") attributes.push(`lineLimit="${candidate.lineLimit}"`);
151
+ if (typeof candidate.totalLines === "number") attributes.push(`totalLines="${candidate.totalLines}"`);
152
+ if (typeof candidate.remainingLines === "number") {
153
+ attributes.push(`remainingLines="${candidate.remainingLines}"`);
154
+ } else if (candidate.remainingLines === null) {
155
+ attributes.push(`remainingLines="unknown"`);
156
+ }
127
157
 
128
- function formatHighlightsResult(result: UnknownRecord): string[] {
129
- const highlights = result.highlights as unknown[];
130
- const highlightQuery = typeof result.highlightQuery === "string" ? result.highlightQuery : "";
131
- const count = highlights.length;
158
+ if (candidate.success === false && typeof candidate.error === "string") {
159
+ attributes.push(`error="${escapeXmlAttribute(candidate.error)}"`);
160
+ lines.push(` <url ${attributes.join(" ")} />`);
161
+ continue;
162
+ }
132
163
 
133
- const lines: string[] = [`${count} highlight${count !== 1 ? "s" : ""} for "${highlightQuery}"`];
164
+ const text = typeof candidate.text === "string" ? candidate.text : "";
165
+ if (!text.trim()) {
166
+ lines.push(` <url ${attributes.join(" ")} />`);
167
+ continue;
168
+ }
134
169
 
135
- const MAX_HIGHLIGHTS = 3;
136
- const MAX_CHARS = 120;
170
+ const snippetLines = normalizeWhitespace(text)
171
+ .replace(/\n{3,}/g, "\n\n")
172
+ .trim()
173
+ .split("\n")
174
+ .slice(0, MAX_LINES)
175
+ .map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
137
176
 
138
- highlights.slice(0, MAX_HIGHLIGHTS).forEach((h, idx) => {
139
- if (typeof h === "string") {
140
- const clean = h.replace(/\n+/g, " ").trim();
141
- const truncated = clean.length > MAX_CHARS ? `${clean.slice(0, MAX_CHARS - 1)}…` : clean;
142
- lines.push(` ${idx + 1}. "${truncated}"`);
177
+ lines.push(` <url ${attributes.join(" ")}>`);
178
+ for (const line of snippetLines) {
179
+ lines.push(` ${escapeXmlAttribute(line)}`);
143
180
  }
144
- });
145
-
146
- if (highlights.length > MAX_HIGHLIGHTS) {
147
- lines.push(` ...and ${highlights.length - MAX_HIGHLIGHTS} more`);
181
+ lines.push(" </url>");
148
182
  }
149
183
 
150
- return lines;
184
+ lines.push("</fetchUrls>");
185
+ return lines.length > 2 ? lines : null;
151
186
  }
152
187
 
153
188
  function formatRenderUrlResult(result: unknown): string[] | null {
@@ -188,15 +223,38 @@ export const fetchUrlsLayout: ToolLayoutConfig = {
188
223
  abbreviation: "fetch",
189
224
 
190
225
  getHeader: (input, result): ToolHeader | null => {
191
- const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
192
- if (!urlInput) return null;
193
- const headerSuffix = formatFetchUrlsHeader(urlInput);
226
+ const requests = extractFetchUrlsRequests(input);
227
+ if (!requests) return null;
228
+ const items = extractFetchUrlsResults(result);
229
+ const firstResult = items?.[0] ?? null;
230
+ const first = mergeFetchUrlsDefaults(requests[0] as FetchUrlsRequestInput, firstResult);
231
+ if (requests.length === 1) {
232
+ const headerSuffix = formatFetchUrlsHeader(first);
233
+ return {
234
+ primary: first.url,
235
+ secondary: headerSuffix || undefined,
236
+ };
237
+ }
238
+
194
239
  return {
195
- primary: urlInput.url,
196
- secondary: headerSuffix || undefined,
240
+ primary: `${requests.length} urls`,
197
241
  };
198
242
  },
199
243
 
244
+ getBody: (input, result): ToolBody | null => {
245
+ const requests = extractFetchUrlsRequests(input);
246
+ if (!requests) return null;
247
+ if (requests.length === 1) return null;
248
+ const items = extractFetchUrlsResults(result) ?? [];
249
+ const lines = requests.map((request, index) => {
250
+ const merged = mergeFetchUrlsDefaults(request, items[index] ?? null);
251
+ const suffix = formatFetchUrlsHeader(merged);
252
+ const text = suffix ? `${merged.url} ${suffix}` : merged.url;
253
+ return { text, color: COLORS.REASONING_DIM };
254
+ });
255
+ return { lines };
256
+ },
257
+
200
258
  formatResult: formatFetchUrlsResult,
201
259
  };
202
260
 
@@ -204,11 +262,15 @@ export const renderUrlLayout: ToolLayoutConfig = {
204
262
  abbreviation: "render",
205
263
 
206
264
  getHeader: (input, result): ToolHeader | null => {
207
- const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
265
+ const urlInput = extractRenderUrlInput(input);
208
266
  if (!urlInput) return null;
209
- const headerSuffix = formatFetchUrlsHeader(urlInput);
267
+ const merged = mergeFetchUrlsDefaults(
268
+ urlInput,
269
+ isRecord(result) ? (result as FetchUrlsResultItem) : null
270
+ );
271
+ const headerSuffix = formatFetchUrlsHeader(merged);
210
272
  return {
211
- primary: urlInput.url,
273
+ primary: merged.url,
212
274
  secondary: headerSuffix || undefined,
213
275
  };
214
276
  },
@@ -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
 
@@ -352,21 +355,33 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
352
355
  }
353
356
  settingIdx++;
354
357
 
355
- if (ctx.interactionMode === "voice") {
356
- if (ctx.selectedIdx === settingIdx) {
357
- const speeds: SpeechSpeed[] = [1.0, 1.25, 1.5, 1.75, 2.0];
358
- const currentSpeed = ctx.manager.speechSpeed;
359
- const currentIndex = speeds.indexOf(currentSpeed);
360
- const nextIndex = (currentIndex + 1) % speeds.length;
361
- const nextSpeed = speeds[nextIndex] ?? 1.0;
362
- ctx.manager.speechSpeed = nextSpeed;
363
- ctx.setSpeechSpeed(nextSpeed);
364
- ctx.persistPreferences({ speechSpeed: nextSpeed });
358
+ if (ctx.selectedIdx === settingIdx) {
359
+ const next = !ctx.manager.memoryEnabled;
360
+ ctx.manager.memoryEnabled = next;
361
+ ctx.setMemoryEnabled(next);
362
+ ctx.persistPreferences({ memoryEnabled: next });
363
+ key.preventDefault();
364
+ return true;
365
+ }
366
+ settingIdx++;
367
+
368
+ if (ctx.selectedIdx === settingIdx) {
369
+ if (ctx.interactionMode !== "voice") {
365
370
  key.preventDefault();
366
371
  return true;
367
372
  }
368
- settingIdx++;
373
+ const speeds: SpeechSpeed[] = [1.0, 1.25, 1.5, 1.75, 2.0];
374
+ const currentSpeed = ctx.manager.speechSpeed;
375
+ const currentIndex = speeds.indexOf(currentSpeed);
376
+ const nextIndex = (currentIndex + 1) % speeds.length;
377
+ const nextSpeed = speeds[nextIndex] ?? 1.0;
378
+ ctx.manager.speechSpeed = nextSpeed;
379
+ ctx.setSpeechSpeed(nextSpeed);
380
+ ctx.persistPreferences({ speechSpeed: nextSpeed });
381
+ key.preventDefault();
382
+ return true;
369
383
  }
384
+ settingIdx++;
370
385
 
371
386
  if (ctx.selectedIdx === settingIdx) {
372
387
  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;
@@ -42,6 +42,7 @@ class DaemonStateManager {
42
42
  private _reasoningEffort: ReasoningEffort = "medium";
43
43
  private _bashApprovalLevel: BashApprovalLevel = "dangerous";
44
44
  private _toolToggles: ToolToggles = { ...DEFAULT_TOOL_TOGGLES };
45
+ private _memoryEnabled = true;
45
46
  private _outputDeviceName: string | undefined = undefined;
46
47
  private _turnId = 0;
47
48
  private speechRunId = 0;
@@ -147,6 +148,14 @@ class DaemonStateManager {
147
148
  this._toolToggles = toggles;
148
149
  }
149
150
 
151
+ get memoryEnabled(): boolean {
152
+ return this._memoryEnabled;
153
+ }
154
+
155
+ set memoryEnabled(enabled: boolean) {
156
+ this._memoryEnabled = enabled;
157
+ }
158
+
150
159
  get outputDeviceName(): string | undefined {
151
160
  return this._outputDeviceName;
152
161
  }
@@ -317,6 +326,7 @@ class DaemonStateManager {
317
326
  this.emitEvent("responseToken", token);
318
327
  },
319
328
  onStepUsage: (usage) => this.emitEvent("stepUsage", usage),
329
+ onMemorySaved: (preview) => this.emitEvent("memorySaved", preview),
320
330
  }
321
331
  );
322
332
 
@@ -169,6 +169,7 @@ export interface StreamCallbacks {
169
169
  onSubagentToolResult?: (toolCallId: string, toolName: string, success: boolean) => void;
170
170
  onSubagentComplete?: (toolCallId: string, success: boolean) => void;
171
171
  onStepUsage?: (usage: TokenUsage) => void;
172
+ onMemorySaved?: (preview: MemoryToastPreview) => void;
172
173
  onComplete?: (
173
174
  fullText: string,
174
175
  responseMessages: ModelMessage[],
@@ -179,6 +180,13 @@ export interface StreamCallbacks {
179
180
  onError?: (error: Error) => void;
180
181
  }
181
182
 
183
+ export type MemoryToastOperation = "ADD" | "UPDATE" | "ADD/UPDATE";
184
+
185
+ export interface MemoryToastPreview {
186
+ operation: MemoryToastOperation;
187
+ description: string;
188
+ }
189
+
182
190
  /**
183
191
  * Audio device information
184
192
  */
@@ -312,6 +320,8 @@ export interface AppPreferences {
312
320
  showFullReasoning?: boolean;
313
321
  /** Show tool output previews */
314
322
  showToolOutput?: boolean;
323
+ /** Enable memory injection + auto-write */
324
+ memoryEnabled?: boolean;
315
325
  /** Bash command approval level */
316
326
  bashApprovalLevel?: BashApprovalLevel;
317
327
  /** Tool toggles (on/off) */
@@ -437,7 +447,6 @@ export interface UrlMenuItem {
437
447
  url: string;
438
448
  groundedCount: number;
439
449
  readPercent?: number;
440
- highlightsCount?: number;
441
450
  status: "ok" | "error";
442
451
  error?: string;
443
452
  lastSeenIndex: number;