botholomew 0.7.8 → 0.7.10
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 +2 -0
- package/package.json +1 -1
- package/src/chat/agent.ts +68 -35
- package/src/chat/session.ts +27 -31
- package/src/commands/context.ts +168 -12
- package/src/commands/daemon.ts +15 -2
- package/src/commands/schedule.ts +10 -1
- package/src/commands/skill.ts +18 -3
- package/src/commands/task.ts +28 -4
- package/src/commands/thread.ts +3 -1
- package/src/commands/tools.ts +2 -0
- package/src/commands/with-db.ts +8 -9
- package/src/context/fetcher.ts +1 -0
- package/src/context/ingest.ts +3 -1
- package/src/daemon/index.ts +6 -5
- package/src/daemon/llm.ts +68 -42
- package/src/daemon/prompt.ts +6 -4
- package/src/daemon/schedules.ts +15 -10
- package/src/daemon/tick.ts +54 -38
- package/src/db/connection.ts +143 -14
- package/src/db/context.ts +13 -0
- package/src/db/schedules.ts +7 -3
- package/src/db/tasks.ts +4 -4
- package/src/db/threads.ts +6 -4
- package/src/tools/tool.ts +8 -0
- package/src/tui/App.tsx +16 -11
- package/src/tui/components/ContextPanel.tsx +19 -15
- package/src/tui/components/SchedulePanel.tsx +15 -9
- package/src/tui/components/StatusBar.tsx +8 -6
- package/src/tui/components/TaskPanel.tsx +6 -6
- package/src/tui/components/ThreadPanel.tsx +29 -19
- package/src/utils/title.ts +5 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../../db/connection.ts";
|
|
4
4
|
import {
|
|
5
5
|
deleteSchedule,
|
|
6
6
|
listSchedules,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { ansi, theme } from "../theme.ts";
|
|
11
11
|
|
|
12
12
|
interface SchedulePanelProps {
|
|
13
|
-
|
|
13
|
+
dbPath: string;
|
|
14
14
|
isActive: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -108,7 +108,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export const SchedulePanel = memo(function SchedulePanel({
|
|
111
|
-
|
|
111
|
+
dbPath,
|
|
112
112
|
isActive,
|
|
113
113
|
}: SchedulePanelProps) {
|
|
114
114
|
const { stdout } = useStdout();
|
|
@@ -127,7 +127,9 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
127
127
|
const refresh = async () => {
|
|
128
128
|
const filters: { enabled?: boolean } = {};
|
|
129
129
|
if (enabledFilter !== null) filters.enabled = enabledFilter;
|
|
130
|
-
const result = await
|
|
130
|
+
const result = await withDb(dbPath, (conn) =>
|
|
131
|
+
listSchedules(conn, filters),
|
|
132
|
+
);
|
|
131
133
|
if (mounted) {
|
|
132
134
|
setSchedules(result);
|
|
133
135
|
setSelectedIndex((prev) =>
|
|
@@ -142,7 +144,7 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
142
144
|
mounted = false;
|
|
143
145
|
clearInterval(interval);
|
|
144
146
|
};
|
|
145
|
-
}, [
|
|
147
|
+
}, [dbPath, enabledFilter, refreshTick]);
|
|
146
148
|
|
|
147
149
|
const selectedSchedule = schedules[selectedIndex];
|
|
148
150
|
|
|
@@ -182,7 +184,9 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
182
184
|
if (confirmDelete) {
|
|
183
185
|
if (input === "y" || input === "d") {
|
|
184
186
|
if (selectedSchedule) {
|
|
185
|
-
|
|
187
|
+
withDb(dbPath, (conn) =>
|
|
188
|
+
deleteSchedule(conn, selectedSchedule.id),
|
|
189
|
+
).then(() => {
|
|
186
190
|
forceRefresh();
|
|
187
191
|
});
|
|
188
192
|
}
|
|
@@ -242,9 +246,11 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
242
246
|
return;
|
|
243
247
|
}
|
|
244
248
|
if (input === "e" && selectedSchedule) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
withDb(dbPath, (conn) =>
|
|
250
|
+
updateSchedule(conn, selectedSchedule.id, {
|
|
251
|
+
enabled: !selectedSchedule.enabled,
|
|
252
|
+
}),
|
|
253
|
+
).then(() => {
|
|
248
254
|
forceRefresh();
|
|
249
255
|
});
|
|
250
256
|
return;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../../db/connection.ts";
|
|
4
4
|
import { listTasks } from "../../db/tasks.ts";
|
|
5
5
|
import { getDaemonStatus } from "../../utils/pid.ts";
|
|
6
6
|
import { LogoChar } from "./Logo.tsx";
|
|
7
7
|
|
|
8
8
|
interface StatusBarProps {
|
|
9
9
|
projectDir: string;
|
|
10
|
-
|
|
10
|
+
dbPath: string;
|
|
11
11
|
chatTitle?: string;
|
|
12
12
|
onDaemonStatusChange?: (running: boolean) => void;
|
|
13
13
|
}
|
|
@@ -20,7 +20,7 @@ interface Status {
|
|
|
20
20
|
|
|
21
21
|
export function StatusBar({
|
|
22
22
|
projectDir,
|
|
23
|
-
|
|
23
|
+
dbPath,
|
|
24
24
|
chatTitle,
|
|
25
25
|
onDaemonStatusChange,
|
|
26
26
|
}: StatusBarProps) {
|
|
@@ -35,8 +35,10 @@ export function StatusBar({
|
|
|
35
35
|
|
|
36
36
|
const refresh = async () => {
|
|
37
37
|
const daemon = await getDaemonStatus(projectDir);
|
|
38
|
-
const pending = await
|
|
39
|
-
|
|
38
|
+
const [pending, inProgress] = await withDb(dbPath, async (conn) => [
|
|
39
|
+
await listTasks(conn, { status: "pending" }),
|
|
40
|
+
await listTasks(conn, { status: "in_progress" }),
|
|
41
|
+
]);
|
|
40
42
|
if (mounted) {
|
|
41
43
|
const daemonRunning = daemon !== null;
|
|
42
44
|
setStatus({
|
|
@@ -54,7 +56,7 @@ export function StatusBar({
|
|
|
54
56
|
mounted = false;
|
|
55
57
|
clearInterval(interval);
|
|
56
58
|
};
|
|
57
|
-
}, [projectDir,
|
|
59
|
+
}, [projectDir, dbPath, onDaemonStatusChange]);
|
|
58
60
|
|
|
59
61
|
return (
|
|
60
62
|
<Box paddingX={0}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../../db/connection.ts";
|
|
4
4
|
import {
|
|
5
5
|
deleteTask,
|
|
6
6
|
listTasks,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { ansi, theme } from "../theme.ts";
|
|
12
12
|
|
|
13
13
|
interface TaskPanelProps {
|
|
14
|
-
|
|
14
|
+
dbPath: string;
|
|
15
15
|
isActive: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -145,7 +145,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
export const TaskPanel = memo(function TaskPanel({
|
|
148
|
-
|
|
148
|
+
dbPath,
|
|
149
149
|
isActive,
|
|
150
150
|
}: TaskPanelProps) {
|
|
151
151
|
const { stdout } = useStdout();
|
|
@@ -171,7 +171,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
171
171
|
} = {};
|
|
172
172
|
if (statusFilter) filters.status = statusFilter;
|
|
173
173
|
if (priorityFilter) filters.priority = priorityFilter;
|
|
174
|
-
const result = await listTasks(conn, filters);
|
|
174
|
+
const result = await withDb(dbPath, (conn) => listTasks(conn, filters));
|
|
175
175
|
if (mounted) {
|
|
176
176
|
setTasks(result);
|
|
177
177
|
setSelectedIndex((prev) =>
|
|
@@ -186,7 +186,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
186
186
|
mounted = false;
|
|
187
187
|
clearInterval(interval);
|
|
188
188
|
};
|
|
189
|
-
}, [
|
|
189
|
+
}, [dbPath, statusFilter, priorityFilter, refreshTick]);
|
|
190
190
|
|
|
191
191
|
const selectedTask = tasks[selectedIndex];
|
|
192
192
|
|
|
@@ -275,7 +275,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
if (input === "d" && selectedTask) {
|
|
278
|
-
deleteTask(conn, selectedTask.id).then(() => {
|
|
278
|
+
withDb(dbPath, (conn) => deleteTask(conn, selectedTask.id)).then(() => {
|
|
279
279
|
forceRefresh();
|
|
280
280
|
});
|
|
281
281
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../../db/connection.ts";
|
|
4
4
|
import {
|
|
5
5
|
deleteThread,
|
|
6
6
|
getInteractionsAfter,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { ansi, theme } from "../theme.ts";
|
|
14
14
|
|
|
15
15
|
interface ThreadPanelProps {
|
|
16
|
-
|
|
16
|
+
dbPath: string;
|
|
17
17
|
activeThreadId: string;
|
|
18
18
|
isActive: boolean;
|
|
19
19
|
}
|
|
@@ -181,7 +181,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export const ThreadPanel = memo(function ThreadPanel({
|
|
184
|
-
|
|
184
|
+
dbPath,
|
|
185
185
|
activeThreadId,
|
|
186
186
|
isActive,
|
|
187
187
|
}: ThreadPanelProps) {
|
|
@@ -210,7 +210,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
210
210
|
const refresh = async () => {
|
|
211
211
|
const filters: { type?: Thread["type"] } = {};
|
|
212
212
|
if (typeFilter) filters.type = typeFilter;
|
|
213
|
-
const result = await listThreads(conn, filters);
|
|
213
|
+
const result = await withDb(dbPath, (conn) => listThreads(conn, filters));
|
|
214
214
|
if (mounted) {
|
|
215
215
|
setThreads(result);
|
|
216
216
|
setSelectedIndex((prev) =>
|
|
@@ -225,7 +225,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
225
225
|
mounted = false;
|
|
226
226
|
clearInterval(interval);
|
|
227
227
|
};
|
|
228
|
-
}, [
|
|
228
|
+
}, [dbPath, typeFilter, refreshTick]);
|
|
229
229
|
|
|
230
230
|
// Filter threads by search query
|
|
231
231
|
const filteredThreads = useMemo(() => {
|
|
@@ -245,16 +245,18 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
getThread(conn, selectedThread.id).then(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
withDb(dbPath, (conn) => getThread(conn, selectedThread.id)).then(
|
|
249
|
+
(result) => {
|
|
250
|
+
if (mounted && result) {
|
|
251
|
+
setSelectedDetail(result);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
);
|
|
253
255
|
|
|
254
256
|
return () => {
|
|
255
257
|
mounted = false;
|
|
256
258
|
};
|
|
257
|
-
}, [
|
|
259
|
+
}, [dbPath, selectedThread?.id, following]);
|
|
258
260
|
|
|
259
261
|
// Follow mode: poll for new interactions every 1s
|
|
260
262
|
// biome-ignore lint/correctness/useExhaustiveDependencies: following and selectedThread?.id are the intentional triggers
|
|
@@ -264,10 +266,12 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
264
266
|
|
|
265
267
|
const poll = async () => {
|
|
266
268
|
try {
|
|
267
|
-
const newInteractions = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const newInteractions = await withDb(dbPath, (conn) =>
|
|
270
|
+
getInteractionsAfter(
|
|
271
|
+
conn,
|
|
272
|
+
selectedThread.id,
|
|
273
|
+
lastSeenSequenceRef.current,
|
|
274
|
+
),
|
|
271
275
|
);
|
|
272
276
|
if (!mounted || newInteractions.length === 0) return;
|
|
273
277
|
|
|
@@ -286,11 +290,15 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
286
290
|
// Auto-scroll will be handled by the detailLines/maxDetailScroll recalc
|
|
287
291
|
setDetailScroll(Number.MAX_SAFE_INTEGER);
|
|
288
292
|
|
|
289
|
-
const ended = await
|
|
293
|
+
const ended = await withDb(dbPath, (conn) =>
|
|
294
|
+
isThreadEnded(conn, selectedThread.id),
|
|
295
|
+
);
|
|
290
296
|
if (mounted && ended) {
|
|
291
297
|
setFollowing(false);
|
|
292
298
|
// Refresh the thread to get the ended_at timestamp
|
|
293
|
-
const result = await
|
|
299
|
+
const result = await withDb(dbPath, (conn) =>
|
|
300
|
+
getThread(conn, selectedThread.id),
|
|
301
|
+
);
|
|
294
302
|
if (mounted && result) {
|
|
295
303
|
setSelectedDetail(result);
|
|
296
304
|
}
|
|
@@ -306,7 +314,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
306
314
|
mounted = false;
|
|
307
315
|
clearInterval(interval);
|
|
308
316
|
};
|
|
309
|
-
}, [
|
|
317
|
+
}, [dbPath, following, selectedThread?.id]);
|
|
310
318
|
|
|
311
319
|
const isActiveSelected = selectedThread?.id === activeThreadId;
|
|
312
320
|
|
|
@@ -375,7 +383,9 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
375
383
|
if (confirmDelete) {
|
|
376
384
|
if (input === "y" || input === "d") {
|
|
377
385
|
if (selectedThread && !isActiveSelected) {
|
|
378
|
-
|
|
386
|
+
withDb(dbPath, (conn) =>
|
|
387
|
+
deleteThread(conn, selectedThread.id),
|
|
388
|
+
).then(() => {
|
|
379
389
|
forceRefresh();
|
|
380
390
|
});
|
|
381
391
|
}
|
package/src/utils/title.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../db/connection.ts";
|
|
4
4
|
import { updateThreadTitle } from "../db/threads.ts";
|
|
5
5
|
import { logger } from "./logger.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Generate a short title for a thread using the chunker model (Haiku).
|
|
9
9
|
* Fire-and-forget — errors are logged at debug level and never propagated.
|
|
10
|
+
* Opens its own short-lived DB connection for the write so callers can
|
|
11
|
+
* safely `void`-chain without holding a connection during the LLM call.
|
|
10
12
|
*/
|
|
11
13
|
export async function generateThreadTitle(
|
|
12
14
|
config: Required<BotholomewConfig>,
|
|
13
|
-
|
|
15
|
+
dbPath: string,
|
|
14
16
|
threadId: string,
|
|
15
17
|
context: string,
|
|
16
18
|
): Promise<void> {
|
|
@@ -39,7 +41,7 @@ export async function generateThreadTitle(
|
|
|
39
41
|
.trim();
|
|
40
42
|
|
|
41
43
|
if (title) {
|
|
42
|
-
await updateThreadTitle(conn, threadId, title);
|
|
44
|
+
await withDb(dbPath, (conn) => updateThreadTitle(conn, threadId, title));
|
|
43
45
|
}
|
|
44
46
|
} catch (err) {
|
|
45
47
|
logger.warn(`Failed to generate thread title: ${err}`);
|