@makefinks/daemon 0.6.0 → 0.7.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/README.md +4 -0
- package/package.json +6 -4
- package/src/ai/daemon-ai.ts +48 -8
- package/src/ai/memory/index.ts +6 -0
- package/src/ai/memory/memory-injection.ts +80 -0
- package/src/ai/memory/memory-manager.ts +320 -0
- package/src/ai/model-config.ts +13 -0
- package/src/ai/system-prompt.ts +29 -5
- package/src/app/components/AppOverlays.tsx +3 -0
- package/src/avatar/DaemonAvatarRenderable.ts +0 -22
- package/src/components/HotkeysPane.tsx +1 -0
- package/src/components/MemoryMenu.tsx +338 -0
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-controller.ts +8 -0
- package/src/hooks/use-app-menus.ts +6 -0
- package/src/hooks/use-daemon-keyboard.ts +17 -1
- package/src/hooks/use-overlay-controller.ts +6 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/daemon-state.ts +9 -1
- package/src/types/index.ts +36 -0
- package/src/utils/config.ts +65 -0
- package/src/utils/debug-logger.ts +3 -0
- package/src/utils/markdown.ts +14 -0
|
@@ -241,28 +241,6 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
|
|
|
241
241
|
const w = fb.width;
|
|
242
242
|
const h = fb.height;
|
|
243
243
|
fb.clear(RGBA.fromValues(0, 0, 0, 0));
|
|
244
|
-
|
|
245
|
-
const glyph = w >= 18 ? "⟡ SUMMONING ⟡" : "SUMMONING";
|
|
246
|
-
const gx = Math.max(0, Math.floor(w / 2) - Math.floor(glyph.length / 2));
|
|
247
|
-
const gy = Math.floor(h / 2);
|
|
248
|
-
fb.drawText(
|
|
249
|
-
glyph,
|
|
250
|
-
gx,
|
|
251
|
-
Math.max(0, Math.min(h - 1, gy)),
|
|
252
|
-
RGBA.fromInts(180, 255, 245, 230),
|
|
253
|
-
RGBA.fromInts(0, 0, 0, 0),
|
|
254
|
-
TextAttributes.DIM
|
|
255
|
-
);
|
|
256
|
-
if (h >= 2) {
|
|
257
|
-
fb.drawText(
|
|
258
|
-
"DAEMON",
|
|
259
|
-
Math.max(0, Math.floor(w / 2) - 3),
|
|
260
|
-
Math.max(0, Math.min(h - 1, gy + 1)),
|
|
261
|
-
RGBA.fromInts(130, 190, 255, 200),
|
|
262
|
-
RGBA.fromInts(0, 0, 0, 0),
|
|
263
|
-
TextAttributes.BOLD
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
244
|
} else {
|
|
267
245
|
this.kickRenderFrame();
|
|
268
246
|
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { getMemoryManager, isMemoryAvailable } from "../ai/memory";
|
|
5
|
+
import { useMenuKeyboard } from "../hooks/use-menu-keyboard";
|
|
6
|
+
import type { MemoryEntry } from "../types";
|
|
7
|
+
import { COLORS } from "../ui/constants";
|
|
8
|
+
|
|
9
|
+
interface MemoryMenuProps {
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MEMORY_ITEM_HEIGHT = 2;
|
|
14
|
+
const MAX_SCROLLBOX_HEIGHT = 16;
|
|
15
|
+
|
|
16
|
+
function formatTimestamp(value: string | undefined): string {
|
|
17
|
+
if (!value) return "";
|
|
18
|
+
return value.replace("T", " ").slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function truncateText(text: string, maxLen: number): string {
|
|
22
|
+
if (text.length <= maxLen) return text;
|
|
23
|
+
if (maxLen <= 3) return text.slice(0, Math.max(0, maxLen));
|
|
24
|
+
return text.slice(0, Math.max(0, maxLen - 3)) + "...";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function MemoryMenu({ onClose }: MemoryMenuProps) {
|
|
28
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
29
|
+
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
|
30
|
+
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const searchInputRef = useRef<TextareaRenderable | null>(null);
|
|
34
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
35
|
+
|
|
36
|
+
// Load all memories on mount
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
|
|
40
|
+
const loadMemories = async () => {
|
|
41
|
+
if (!isMemoryAvailable()) {
|
|
42
|
+
setError("Memory system not available (requires OPENAI_API_KEY and OPENROUTER_API_KEY)");
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const manager = getMemoryManager();
|
|
49
|
+
await manager.initialize();
|
|
50
|
+
|
|
51
|
+
if (!manager.isAvailable) {
|
|
52
|
+
setError("Memory system not available (check API keys and configuration)");
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const allMemories = await manager.getAll();
|
|
58
|
+
if (!cancelled) {
|
|
59
|
+
setMemories(allMemories);
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
void loadMemories();
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const filteredMemories = useMemo(() => {
|
|
77
|
+
const query = searchQuery.trim().toLowerCase();
|
|
78
|
+
if (!query) return memories;
|
|
79
|
+
return memories.filter((memory) => memory.memory.toLowerCase().includes(query));
|
|
80
|
+
}, [memories, searchQuery]);
|
|
81
|
+
|
|
82
|
+
const handleDelete = useCallback(
|
|
83
|
+
async (index: number) => {
|
|
84
|
+
const memory = filteredMemories[index];
|
|
85
|
+
if (!memory) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const manager = getMemoryManager();
|
|
89
|
+
const success = await manager.delete(memory.id);
|
|
90
|
+
if (success) {
|
|
91
|
+
setMemories((prev) => prev.filter((entry) => entry.id !== memory.id));
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Silently fail delete
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[filteredMemories]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const { selectedIndex } = useMenuKeyboard({
|
|
101
|
+
itemCount: filteredMemories.length,
|
|
102
|
+
onClose,
|
|
103
|
+
onSelect: () => {}, // No action on select, just navigation
|
|
104
|
+
enableViKeys: !isSearchFocused,
|
|
105
|
+
ignoreEscape: isSearchFocused,
|
|
106
|
+
closeOnSelect: false,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
useKeyboard((key) => {
|
|
110
|
+
if (key.eventType !== "press") return;
|
|
111
|
+
|
|
112
|
+
// X to delete selected memory
|
|
113
|
+
if (!isSearchFocused && (key.name === "x" || key.sequence?.toLowerCase() === "x")) {
|
|
114
|
+
void handleDelete(selectedIndex);
|
|
115
|
+
key.preventDefault();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// / or Shift+Tab to focus search
|
|
120
|
+
if ((key.name === "tab" && key.shift) || (!isSearchFocused && key.name === "/")) {
|
|
121
|
+
setIsSearchFocused(true);
|
|
122
|
+
searchInputRef.current?.focus();
|
|
123
|
+
key.preventDefault();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const scrollboxHeight = Math.min(
|
|
128
|
+
MAX_SCROLLBOX_HEIGHT,
|
|
129
|
+
Math.max(MEMORY_ITEM_HEIGHT, filteredMemories.length * MEMORY_ITEM_HEIGHT)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Auto-scroll to selected item
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const scrollbox = scrollRef.current;
|
|
135
|
+
if (!scrollbox) return;
|
|
136
|
+
const viewportHeight = scrollbox.viewport?.height ?? 0;
|
|
137
|
+
if (viewportHeight <= 0) return;
|
|
138
|
+
|
|
139
|
+
const maxScrollTop = Math.max(0, scrollbox.scrollHeight - viewportHeight);
|
|
140
|
+
const itemTop = selectedIndex * MEMORY_ITEM_HEIGHT;
|
|
141
|
+
const itemBottom = itemTop + MEMORY_ITEM_HEIGHT;
|
|
142
|
+
const currentTop = scrollbox.scrollTop;
|
|
143
|
+
const currentBottom = currentTop + viewportHeight;
|
|
144
|
+
let nextTop = currentTop;
|
|
145
|
+
|
|
146
|
+
if (itemTop < currentTop) {
|
|
147
|
+
nextTop = itemTop;
|
|
148
|
+
} else if (itemBottom > currentBottom) {
|
|
149
|
+
nextTop = itemBottom - viewportHeight;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
nextTop = Math.max(0, Math.min(nextTop, maxScrollTop));
|
|
153
|
+
if (nextTop !== currentTop) {
|
|
154
|
+
scrollbox.scrollTop = nextTop;
|
|
155
|
+
}
|
|
156
|
+
}, [filteredMemories.length, selectedIndex]);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<box
|
|
160
|
+
position="absolute"
|
|
161
|
+
left={0}
|
|
162
|
+
top={0}
|
|
163
|
+
width="100%"
|
|
164
|
+
height="100%"
|
|
165
|
+
flexDirection="column"
|
|
166
|
+
alignItems="center"
|
|
167
|
+
justifyContent="center"
|
|
168
|
+
zIndex={100}
|
|
169
|
+
>
|
|
170
|
+
<box
|
|
171
|
+
flexDirection="column"
|
|
172
|
+
alignItems="flex-start"
|
|
173
|
+
backgroundColor={COLORS.MENU_BG}
|
|
174
|
+
borderStyle="single"
|
|
175
|
+
borderColor={COLORS.MENU_BORDER}
|
|
176
|
+
paddingLeft={2}
|
|
177
|
+
paddingRight={2}
|
|
178
|
+
paddingTop={1}
|
|
179
|
+
paddingBottom={1}
|
|
180
|
+
width="80%"
|
|
181
|
+
minWidth={60}
|
|
182
|
+
maxWidth={140}
|
|
183
|
+
>
|
|
184
|
+
<box marginBottom={1}>
|
|
185
|
+
<text>
|
|
186
|
+
<span fg={COLORS.DAEMON_LABEL}>[ MEMORIES ]</span>
|
|
187
|
+
</text>
|
|
188
|
+
</box>
|
|
189
|
+
|
|
190
|
+
<box marginBottom={1}>
|
|
191
|
+
<text>
|
|
192
|
+
<span fg={COLORS.REASONING_DIM}>↑/↓ j/k navigate · X delete · / search · ESC close</span>
|
|
193
|
+
</text>
|
|
194
|
+
</box>
|
|
195
|
+
|
|
196
|
+
<box marginBottom={0}>
|
|
197
|
+
<text>
|
|
198
|
+
<span fg={COLORS.USER_LABEL}>— SEARCH —</span>
|
|
199
|
+
</text>
|
|
200
|
+
</box>
|
|
201
|
+
|
|
202
|
+
<box
|
|
203
|
+
marginBottom={1}
|
|
204
|
+
marginTop={0}
|
|
205
|
+
width="100%"
|
|
206
|
+
height={1}
|
|
207
|
+
flexDirection="row"
|
|
208
|
+
alignItems="center"
|
|
209
|
+
paddingLeft={1}
|
|
210
|
+
backgroundColor={isSearchFocused ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
|
|
211
|
+
>
|
|
212
|
+
<box width={2}>
|
|
213
|
+
<text>
|
|
214
|
+
<span fg={isSearchFocused ? COLORS.TYPING_PROMPT : COLORS.REASONING_DIM}>/ </span>
|
|
215
|
+
</text>
|
|
216
|
+
</box>
|
|
217
|
+
<box flexGrow={1} height={1}>
|
|
218
|
+
<textarea
|
|
219
|
+
ref={searchInputRef}
|
|
220
|
+
focused={isSearchFocused}
|
|
221
|
+
width="100%"
|
|
222
|
+
height={1}
|
|
223
|
+
placeholder="Type to search memories... (/ or Shift+Tab)"
|
|
224
|
+
style={{
|
|
225
|
+
backgroundColor: "transparent",
|
|
226
|
+
focusedBackgroundColor: "transparent",
|
|
227
|
+
textColor: COLORS.MENU_TEXT,
|
|
228
|
+
focusedTextColor: COLORS.TYPING_PROMPT,
|
|
229
|
+
cursorColor: COLORS.TYPING_PROMPT,
|
|
230
|
+
}}
|
|
231
|
+
onContentChange={() => {
|
|
232
|
+
const text = searchInputRef.current?.plainText ?? "";
|
|
233
|
+
const cleaned = text.replace(/[\r\n]/g, "");
|
|
234
|
+
if (cleaned !== text) {
|
|
235
|
+
searchInputRef.current?.setText(cleaned);
|
|
236
|
+
}
|
|
237
|
+
setSearchQuery(cleaned);
|
|
238
|
+
}}
|
|
239
|
+
onKeyDown={(key) => {
|
|
240
|
+
if (key.eventType === "press") {
|
|
241
|
+
if (key.name === "escape") {
|
|
242
|
+
setIsSearchFocused(false);
|
|
243
|
+
key.preventDefault();
|
|
244
|
+
}
|
|
245
|
+
if (key.name === "return") {
|
|
246
|
+
key.preventDefault();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</box>
|
|
252
|
+
</box>
|
|
253
|
+
|
|
254
|
+
<box marginBottom={0}>
|
|
255
|
+
<text>
|
|
256
|
+
<span fg={COLORS.USER_LABEL}>— MEMORIES ({filteredMemories.length}) —</span>
|
|
257
|
+
</text>
|
|
258
|
+
</box>
|
|
259
|
+
|
|
260
|
+
{isLoading ? (
|
|
261
|
+
<box marginTop={1} paddingLeft={1}>
|
|
262
|
+
<text>
|
|
263
|
+
<span fg={COLORS.REASONING_DIM}>Loading memories...</span>
|
|
264
|
+
</text>
|
|
265
|
+
</box>
|
|
266
|
+
) : error ? (
|
|
267
|
+
<box marginTop={1} paddingLeft={1}>
|
|
268
|
+
<text>
|
|
269
|
+
<span fg={COLORS.STATUS_FAILED}>{error}</span>
|
|
270
|
+
</text>
|
|
271
|
+
</box>
|
|
272
|
+
) : filteredMemories.length === 0 ? (
|
|
273
|
+
<box marginTop={1} paddingLeft={1}>
|
|
274
|
+
<text>
|
|
275
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
276
|
+
{searchQuery ? "No memories match your search" : "No memories stored yet"}
|
|
277
|
+
</span>
|
|
278
|
+
</text>
|
|
279
|
+
</box>
|
|
280
|
+
) : (
|
|
281
|
+
<scrollbox
|
|
282
|
+
ref={scrollRef}
|
|
283
|
+
height={scrollboxHeight}
|
|
284
|
+
alignSelf="flex-start"
|
|
285
|
+
focused={false}
|
|
286
|
+
scrollY={true}
|
|
287
|
+
scrollX={false}
|
|
288
|
+
style={{
|
|
289
|
+
rootOptions: { backgroundColor: COLORS.MENU_BG },
|
|
290
|
+
wrapperOptions: { backgroundColor: COLORS.MENU_BG },
|
|
291
|
+
viewportOptions: { backgroundColor: COLORS.MENU_BG },
|
|
292
|
+
contentOptions: { backgroundColor: COLORS.MENU_BG },
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<box flexDirection="column" width="100%">
|
|
296
|
+
{filteredMemories.map((memory, idx) => {
|
|
297
|
+
const isSelected = idx === selectedIndex;
|
|
298
|
+
const labelColor = isSelected ? COLORS.DAEMON_LABEL : COLORS.MENU_TEXT;
|
|
299
|
+
const detailColor = COLORS.REASONING_DIM;
|
|
300
|
+
const scoreText = memory.score !== undefined ? ` (${(memory.score * 100).toFixed(0)}%)` : "";
|
|
301
|
+
const dateText = formatTimestamp(memory.createdAt || memory.updatedAt);
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<box
|
|
305
|
+
key={memory.id}
|
|
306
|
+
backgroundColor={isSelected ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
|
|
307
|
+
paddingLeft={1}
|
|
308
|
+
paddingRight={1}
|
|
309
|
+
flexDirection="column"
|
|
310
|
+
width="100%"
|
|
311
|
+
>
|
|
312
|
+
<box>
|
|
313
|
+
<text>
|
|
314
|
+
<span fg={labelColor}>
|
|
315
|
+
{isSelected ? "▶ " : " "}
|
|
316
|
+
{truncateText(memory.memory, 80)}
|
|
317
|
+
</span>
|
|
318
|
+
{scoreText && <span fg={COLORS.STATUS_COMPLETED}>{scoreText}</span>}
|
|
319
|
+
</text>
|
|
320
|
+
</box>
|
|
321
|
+
<box marginLeft={4}>
|
|
322
|
+
<text>
|
|
323
|
+
<span fg={detailColor}>
|
|
324
|
+
{dateText}
|
|
325
|
+
{memory.metadata?.category ? ` · ${memory.metadata.category}` : ""}
|
|
326
|
+
</span>
|
|
327
|
+
</text>
|
|
328
|
+
</box>
|
|
329
|
+
</box>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</box>
|
|
333
|
+
</scrollbox>
|
|
334
|
+
)}
|
|
335
|
+
</box>
|
|
336
|
+
</box>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -43,6 +43,8 @@ export interface UseAppContextBuilderParams {
|
|
|
43
43
|
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
44
44
|
showToolsMenu: boolean;
|
|
45
45
|
setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
46
|
+
showMemoryMenu: boolean;
|
|
47
|
+
setShowMemoryMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
device: {
|
|
@@ -91,6 +91,8 @@ export function useAppController({
|
|
|
91
91
|
setShowUrlMenu,
|
|
92
92
|
showToolsMenu,
|
|
93
93
|
setShowToolsMenu,
|
|
94
|
+
showMemoryMenu,
|
|
95
|
+
setShowMemoryMenu,
|
|
94
96
|
} = menus;
|
|
95
97
|
|
|
96
98
|
const session = useSessionController({ showSessionMenu });
|
|
@@ -286,6 +288,7 @@ export function useAppController({
|
|
|
286
288
|
setShowGroundingMenu,
|
|
287
289
|
setShowUrlMenu,
|
|
288
290
|
setShowToolsMenu,
|
|
291
|
+
setShowMemoryMenu,
|
|
289
292
|
setTypingInput: daemon.typing.setTypingInput,
|
|
290
293
|
setCurrentTranscription: daemon.setCurrentTranscription,
|
|
291
294
|
setCurrentResponse: daemon.setCurrentResponse,
|
|
@@ -310,6 +313,7 @@ export function useAppController({
|
|
|
310
313
|
setShowGroundingMenu,
|
|
311
314
|
setShowUrlMenu,
|
|
312
315
|
setShowToolsMenu,
|
|
316
|
+
setShowMemoryMenu,
|
|
313
317
|
daemon.typing.setTypingInput,
|
|
314
318
|
daemon.setCurrentTranscription,
|
|
315
319
|
daemon.setCurrentResponse,
|
|
@@ -335,6 +339,7 @@ export function useAppController({
|
|
|
335
339
|
showGroundingMenu,
|
|
336
340
|
showUrlMenu,
|
|
337
341
|
showToolsMenu,
|
|
342
|
+
showMemoryMenu,
|
|
338
343
|
onboardingActive: bootstrap.onboardingActive,
|
|
339
344
|
},
|
|
340
345
|
{
|
|
@@ -347,6 +352,7 @@ export function useAppController({
|
|
|
347
352
|
setShowGroundingMenu,
|
|
348
353
|
setShowUrlMenu,
|
|
349
354
|
setShowToolsMenu,
|
|
355
|
+
setShowMemoryMenu,
|
|
350
356
|
}
|
|
351
357
|
);
|
|
352
358
|
|
|
@@ -437,6 +443,8 @@ export function useAppController({
|
|
|
437
443
|
setShowUrlMenu,
|
|
438
444
|
showToolsMenu,
|
|
439
445
|
setShowToolsMenu,
|
|
446
|
+
showMemoryMenu,
|
|
447
|
+
setShowMemoryMenu,
|
|
440
448
|
},
|
|
441
449
|
device: {
|
|
442
450
|
devices: bootstrap.devices,
|
|
@@ -27,6 +27,9 @@ export interface UseAppMenusReturn {
|
|
|
27
27
|
|
|
28
28
|
showToolsMenu: boolean;
|
|
29
29
|
setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
|
+
|
|
31
|
+
showMemoryMenu: boolean;
|
|
32
|
+
setShowMemoryMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function useAppMenus(): UseAppMenusReturn {
|
|
@@ -39,6 +42,7 @@ export function useAppMenus(): UseAppMenusReturn {
|
|
|
39
42
|
const [showGroundingMenu, setShowGroundingMenu] = useState(false);
|
|
40
43
|
const [showUrlMenu, setShowUrlMenu] = useState(false);
|
|
41
44
|
const [showToolsMenu, setShowToolsMenu] = useState(false);
|
|
45
|
+
const [showMemoryMenu, setShowMemoryMenu] = useState(false);
|
|
42
46
|
|
|
43
47
|
return {
|
|
44
48
|
showDeviceMenu,
|
|
@@ -59,5 +63,7 @@ export function useAppMenus(): UseAppMenusReturn {
|
|
|
59
63
|
setShowUrlMenu,
|
|
60
64
|
showToolsMenu,
|
|
61
65
|
setShowToolsMenu,
|
|
66
|
+
showMemoryMenu,
|
|
67
|
+
setShowMemoryMenu,
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -25,6 +25,7 @@ export interface KeyboardHandlerActions {
|
|
|
25
25
|
setShowGroundingMenu: (show: boolean) => void;
|
|
26
26
|
setShowUrlMenu: (show: boolean) => void;
|
|
27
27
|
setShowToolsMenu: (show: boolean) => void;
|
|
28
|
+
setShowMemoryMenu: (show: boolean) => void;
|
|
28
29
|
setTypingInput: (input: string | ((prev: string) => string)) => void;
|
|
29
30
|
setCurrentTranscription: (text: string) => void;
|
|
30
31
|
setCurrentResponse: (text: string) => void;
|
|
@@ -54,6 +55,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
54
55
|
actions.setShowGroundingMenu(false);
|
|
55
56
|
actions.setShowUrlMenu(false);
|
|
56
57
|
actions.setShowToolsMenu(false);
|
|
58
|
+
actions.setShowMemoryMenu(false);
|
|
57
59
|
}, [actions]);
|
|
58
60
|
|
|
59
61
|
const handleKeyPress = useCallback(
|
|
@@ -232,6 +234,20 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
// 'B' key to open memory menu (in IDLE, SPEAKING, or RESPONDING state)
|
|
238
|
+
if (
|
|
239
|
+
(key.sequence === "b" || key.sequence === "B") &&
|
|
240
|
+
key.eventType === "press" &&
|
|
241
|
+
(currentState === DaemonState.IDLE ||
|
|
242
|
+
currentState === DaemonState.SPEAKING ||
|
|
243
|
+
currentState === DaemonState.RESPONDING)
|
|
244
|
+
) {
|
|
245
|
+
closeAllMenus();
|
|
246
|
+
actions.setShowMemoryMenu(true);
|
|
247
|
+
key.preventDefault();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
// 'R' key to toggle full reasoning display (in IDLE, SPEAKING, or RESPONDING state)
|
|
236
252
|
if (
|
|
237
253
|
(key.sequence === "r" || key.sequence === "R") &&
|
|
@@ -293,7 +309,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
293
309
|
!key.meta &&
|
|
294
310
|
currentState !== DaemonState.TYPING
|
|
295
311
|
) {
|
|
296
|
-
if (currentState === DaemonState.IDLE) {
|
|
312
|
+
if (currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) {
|
|
297
313
|
// Check for OpenRouter API key first (needed for any AI response)
|
|
298
314
|
if (!process.env.OPENROUTER_API_KEY) {
|
|
299
315
|
actions.setApiKeyMissingError(
|
|
@@ -10,6 +10,7 @@ export interface OverlayControllerState {
|
|
|
10
10
|
showGroundingMenu: boolean;
|
|
11
11
|
showUrlMenu: boolean;
|
|
12
12
|
showToolsMenu: boolean;
|
|
13
|
+
showMemoryMenu: boolean;
|
|
13
14
|
onboardingActive: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -23,6 +24,7 @@ export interface OverlayControllerActions {
|
|
|
23
24
|
setShowGroundingMenu: (show: boolean) => void;
|
|
24
25
|
setShowUrlMenu: (show: boolean) => void;
|
|
25
26
|
setShowToolsMenu: (show: boolean) => void;
|
|
27
|
+
setShowMemoryMenu: (show: boolean) => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function useOverlayController(state: OverlayControllerState, actions: OverlayControllerActions) {
|
|
@@ -36,6 +38,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
|
|
|
36
38
|
showGroundingMenu,
|
|
37
39
|
showUrlMenu,
|
|
38
40
|
showToolsMenu,
|
|
41
|
+
showMemoryMenu,
|
|
39
42
|
onboardingActive,
|
|
40
43
|
} = state;
|
|
41
44
|
|
|
@@ -50,6 +53,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
|
|
|
50
53
|
showGroundingMenu ||
|
|
51
54
|
showUrlMenu ||
|
|
52
55
|
showToolsMenu ||
|
|
56
|
+
showMemoryMenu ||
|
|
53
57
|
onboardingActive
|
|
54
58
|
);
|
|
55
59
|
}, [
|
|
@@ -62,6 +66,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
|
|
|
62
66
|
showGroundingMenu,
|
|
63
67
|
showUrlMenu,
|
|
64
68
|
showToolsMenu,
|
|
69
|
+
showMemoryMenu,
|
|
65
70
|
onboardingActive,
|
|
66
71
|
]);
|
|
67
72
|
|
|
@@ -75,6 +80,7 @@ export function useOverlayController(state: OverlayControllerState, actions: Ove
|
|
|
75
80
|
actions.setShowGroundingMenu(false);
|
|
76
81
|
actions.setShowUrlMenu(false);
|
|
77
82
|
actions.setShowToolsMenu(false);
|
|
83
|
+
actions.setShowMemoryMenu(false);
|
|
78
84
|
}, [actions]);
|
|
79
85
|
|
|
80
86
|
return {
|
|
@@ -33,6 +33,8 @@ export interface MenuState {
|
|
|
33
33
|
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
34
34
|
showToolsMenu: boolean;
|
|
35
35
|
setShowToolsMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
36
|
+
showMemoryMenu: boolean;
|
|
37
|
+
setShowMemoryMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface DeviceState {
|
|
@@ -183,10 +183,18 @@ class DaemonStateManager {
|
|
|
183
183
|
* Start listening for voice input (called when space is pressed)
|
|
184
184
|
*/
|
|
185
185
|
startListening(): void {
|
|
186
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
this._state !== DaemonState.IDLE &&
|
|
188
|
+
this._state !== DaemonState.TYPING &&
|
|
189
|
+
this._state !== DaemonState.SPEAKING
|
|
190
|
+
) {
|
|
187
191
|
return;
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
if (this._state === DaemonState.SPEAKING) {
|
|
195
|
+
this.stopSpeaking();
|
|
196
|
+
}
|
|
197
|
+
|
|
190
198
|
this._transcription = "";
|
|
191
199
|
this._response = "";
|
|
192
200
|
this.setState(DaemonState.LISTENING);
|
package/src/types/index.ts
CHANGED
|
@@ -442,3 +442,39 @@ export interface UrlMenuItem {
|
|
|
442
442
|
error?: string;
|
|
443
443
|
lastSeenIndex: number;
|
|
444
444
|
}
|
|
445
|
+
|
|
446
|
+
// ============================================================
|
|
447
|
+
// Memory Types
|
|
448
|
+
// ============================================================
|
|
449
|
+
|
|
450
|
+
/** A single memory entry returned from mem0 */
|
|
451
|
+
export interface MemoryEntry {
|
|
452
|
+
id: string;
|
|
453
|
+
memory: string;
|
|
454
|
+
hash?: string;
|
|
455
|
+
metadata?: Record<string, unknown>;
|
|
456
|
+
score?: number;
|
|
457
|
+
createdAt?: string;
|
|
458
|
+
updatedAt?: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Result from memory search operations */
|
|
462
|
+
export interface MemorySearchResult {
|
|
463
|
+
results: MemoryEntry[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Result from memory add operations */
|
|
467
|
+
export interface MemoryAddResult {
|
|
468
|
+
results: Array<{
|
|
469
|
+
id: string;
|
|
470
|
+
memory: string;
|
|
471
|
+
event: "ADD" | "UPDATE" | "DELETE" | "NONE";
|
|
472
|
+
}>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Memory context injected into first message */
|
|
476
|
+
export interface MemoryContext {
|
|
477
|
+
memories: MemoryEntry[];
|
|
478
|
+
retrievedAt: number;
|
|
479
|
+
query: string;
|
|
480
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getAppConfigDir } from "./preferences";
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = "config.json";
|
|
6
|
+
|
|
7
|
+
export interface ManualConfig {
|
|
8
|
+
memoryModel?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let cachedConfig: ManualConfig | null = null;
|
|
12
|
+
let configLoadedAt: number | null = null;
|
|
13
|
+
|
|
14
|
+
const CACHE_TTL_MS = 5000;
|
|
15
|
+
|
|
16
|
+
function getConfigPath(): string {
|
|
17
|
+
return path.join(getAppConfigDir(), CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadManualConfig(): ManualConfig {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
|
|
23
|
+
if (cachedConfig !== null && configLoadedAt !== null && now - configLoadedAt < CACHE_TTL_MS) {
|
|
24
|
+
return cachedConfig;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const configPath = getConfigPath();
|
|
28
|
+
|
|
29
|
+
if (!existsSync(configPath)) {
|
|
30
|
+
cachedConfig = {};
|
|
31
|
+
configLoadedAt = now;
|
|
32
|
+
return cachedConfig;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const contents = readFileSync(configPath, "utf8");
|
|
37
|
+
const parsed = JSON.parse(contents) as unknown;
|
|
38
|
+
|
|
39
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
40
|
+
cachedConfig = {};
|
|
41
|
+
} else {
|
|
42
|
+
cachedConfig = parseManualConfig(parsed as Record<string, unknown>);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
cachedConfig = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
configLoadedAt = now;
|
|
49
|
+
return cachedConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseManualConfig(raw: Record<string, unknown>): ManualConfig {
|
|
53
|
+
const config: ManualConfig = {};
|
|
54
|
+
|
|
55
|
+
if (typeof raw.memoryModel === "string" && raw.memoryModel.trim().length > 0) {
|
|
56
|
+
config.memoryModel = raw.memoryModel.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearConfigCache(): void {
|
|
63
|
+
cachedConfig = null;
|
|
64
|
+
configLoadedAt = null;
|
|
65
|
+
}
|