@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.
- 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/system-prompt.ts +47 -41
- package/src/ai/tools/fetch-urls.ts +153 -125
- 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 +49 -27
- package/src/components/ToolCallView.tsx +51 -12
- package/src/components/ToolsMenu.tsx +81 -10
- package/src/components/UrlMenu.tsx +2 -7
- package/src/components/tool-layouts/layouts/subagent.tsx +16 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +142 -80
- package/src/hooks/daemon-event-handlers.ts +9 -0
- package/src/hooks/keyboard-handlers.ts +26 -11
- 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 -1
- package/src/utils/config.ts +33 -0
- package/src/utils/derive-url-menu-items.ts +197 -37
- package/src/utils/preferences.ts +6 -0
- package/src/utils/tool-output-preview.ts +215 -27
|
@@ -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
|
|
11
|
+
interface FetchUrlsRequestInput {
|
|
11
12
|
url: string;
|
|
12
13
|
lineOffset?: number;
|
|
13
14
|
lineLimit?: number;
|
|
14
|
-
highlightQuery?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
if (!
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
135
|
+
const items = extractFetchUrlsResults(result);
|
|
136
|
+
if (!items) return null;
|
|
90
137
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
192
|
-
if (!
|
|
193
|
-
const
|
|
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:
|
|
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 =
|
|
265
|
+
const urlInput = extractRenderUrlInput(input);
|
|
208
266
|
if (!urlInput) return null;
|
|
209
|
-
const
|
|
267
|
+
const merged = mergeFetchUrlsDefaults(
|
|
268
|
+
urlInput,
|
|
269
|
+
isRecord(result) ? (result as FetchUrlsResultItem) : null
|
|
270
|
+
);
|
|
271
|
+
const headerSuffix = formatFetchUrlsHeader(merged);
|
|
210
272
|
return {
|
|
211
|
-
primary:
|
|
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.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
package/src/types/index.ts
CHANGED
|
@@ -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;
|