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.
@@ -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 type { DbConnection } from "../../db/connection.ts";
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
- conn: DbConnection;
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
- conn,
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 listSchedules(conn, filters);
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
- }, [conn, enabledFilter, refreshTick]);
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
- deleteSchedule(conn, selectedSchedule.id).then(() => {
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
- updateSchedule(conn, selectedSchedule.id, {
246
- enabled: !selectedSchedule.enabled,
247
- }).then(() => {
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 type { DbConnection } from "../../db/connection.ts";
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
- conn: DbConnection;
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
- conn,
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 listTasks(conn, { status: "pending" });
39
- const inProgress = await listTasks(conn, { status: "in_progress" });
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, conn, onDaemonStatusChange]);
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 type { DbConnection } from "../../db/connection.ts";
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
- conn: DbConnection;
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
- conn,
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
- }, [conn, statusFilter, priorityFilter, refreshTick]);
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 type { DbConnection } from "../../db/connection.ts";
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
- conn: DbConnection;
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
- conn,
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
- }, [conn, typeFilter, refreshTick]);
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((result) => {
249
- if (mounted && result) {
250
- setSelectedDetail(result);
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
- }, [conn, selectedThread?.id, following]);
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 getInteractionsAfter(
268
- conn,
269
- selectedThread.id,
270
- lastSeenSequenceRef.current,
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 isThreadEnded(conn, selectedThread.id);
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 getThread(conn, selectedThread.id);
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
- }, [conn, following, selectedThread?.id]);
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
- deleteThread(conn, selectedThread.id).then(() => {
386
+ withDb(dbPath, (conn) =>
387
+ deleteThread(conn, selectedThread.id),
388
+ ).then(() => {
379
389
  forceRefresh();
380
390
  });
381
391
  }
@@ -1,16 +1,18 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import type { BotholomewConfig } from "../config/schemas.ts";
3
- import type { DbConnection } from "../db/connection.ts";
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
- conn: DbConnection,
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}`);