botholomew 0.12.3 → 0.13.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 +91 -68
- package/package.json +3 -3
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +630 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +279 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +3 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/types/file-imports.d.ts +9 -0
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import type { Schedule } from "../../schedules/schema.ts";
|
|
4
4
|
import {
|
|
5
5
|
deleteSchedule,
|
|
6
6
|
listSchedules,
|
|
7
|
-
type Schedule,
|
|
8
7
|
updateSchedule,
|
|
9
|
-
} from "../../
|
|
8
|
+
} from "../../schedules/store.ts";
|
|
10
9
|
import { ansi, theme } from "../theme.ts";
|
|
11
10
|
|
|
12
11
|
interface SchedulePanelProps {
|
|
13
|
-
|
|
12
|
+
projectDir: string;
|
|
14
13
|
isActive: boolean;
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -39,6 +38,18 @@ const ENABLED_LABELS: Record<string, string> = {
|
|
|
39
38
|
false: "disabled",
|
|
40
39
|
};
|
|
41
40
|
|
|
41
|
+
function formatTimestamp(iso: string | null): string {
|
|
42
|
+
if (!iso) return "(never)";
|
|
43
|
+
const d = new Date(iso);
|
|
44
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
45
|
+
return d.toLocaleString([], {
|
|
46
|
+
month: "short",
|
|
47
|
+
day: "numeric",
|
|
48
|
+
hour: "2-digit",
|
|
49
|
+
minute: "2-digit",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
function buildScheduleDetailAnsi(schedule: Schedule): string {
|
|
43
54
|
const lines: string[] = [];
|
|
44
55
|
|
|
@@ -55,35 +66,15 @@ function buildScheduleDetailAnsi(schedule: Schedule): string {
|
|
|
55
66
|
`${ansi.bold}${ansi.primary}Frequency${ansi.reset} ${ansi.accent}${schedule.frequency}${ansi.reset}`,
|
|
56
67
|
);
|
|
57
68
|
|
|
58
|
-
const created = schedule.created_at.toLocaleString([], {
|
|
59
|
-
month: "short",
|
|
60
|
-
day: "numeric",
|
|
61
|
-
hour: "2-digit",
|
|
62
|
-
minute: "2-digit",
|
|
63
|
-
});
|
|
64
|
-
const updated = schedule.updated_at.toLocaleString([], {
|
|
65
|
-
month: "short",
|
|
66
|
-
day: "numeric",
|
|
67
|
-
hour: "2-digit",
|
|
68
|
-
minute: "2-digit",
|
|
69
|
-
});
|
|
70
69
|
lines.push(
|
|
71
|
-
`${ansi.bold}${ansi.primary}Created${ansi.reset} ${ansi.dim}${
|
|
70
|
+
`${ansi.bold}${ansi.primary}Created${ansi.reset} ${ansi.dim}${formatTimestamp(schedule.created_at)}${ansi.reset}`,
|
|
72
71
|
);
|
|
73
72
|
lines.push(
|
|
74
|
-
`${ansi.bold}${ansi.primary}Updated${ansi.reset} ${ansi.dim}${
|
|
73
|
+
`${ansi.bold}${ansi.primary}Updated${ansi.reset} ${ansi.dim}${formatTimestamp(schedule.updated_at)}${ansi.reset}`,
|
|
75
74
|
);
|
|
76
75
|
|
|
77
|
-
const lastRunDisplay = schedule.last_run_at
|
|
78
|
-
? schedule.last_run_at.toLocaleString([], {
|
|
79
|
-
month: "short",
|
|
80
|
-
day: "numeric",
|
|
81
|
-
hour: "2-digit",
|
|
82
|
-
minute: "2-digit",
|
|
83
|
-
})
|
|
84
|
-
: "(never)";
|
|
85
76
|
lines.push(
|
|
86
|
-
`${ansi.bold}${ansi.primary}Last Run${ansi.reset} ${
|
|
77
|
+
`${ansi.bold}${ansi.primary}Last Run${ansi.reset} ${formatTimestamp(schedule.last_run_at)}`,
|
|
87
78
|
);
|
|
88
79
|
lines.push("");
|
|
89
80
|
|
|
@@ -108,7 +99,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
108
99
|
}
|
|
109
100
|
|
|
110
101
|
export const SchedulePanel = memo(function SchedulePanel({
|
|
111
|
-
|
|
102
|
+
projectDir,
|
|
112
103
|
isActive,
|
|
113
104
|
}: SchedulePanelProps) {
|
|
114
105
|
const { stdout } = useStdout();
|
|
@@ -127,14 +118,16 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
127
118
|
const refresh = async () => {
|
|
128
119
|
const filters: { enabled?: boolean } = {};
|
|
129
120
|
if (enabledFilter !== null) filters.enabled = enabledFilter;
|
|
130
|
-
|
|
131
|
-
listSchedules(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
try {
|
|
122
|
+
const result = await listSchedules(projectDir, filters);
|
|
123
|
+
if (mounted) {
|
|
124
|
+
setSchedules(result);
|
|
125
|
+
setSelectedIndex((prev) =>
|
|
126
|
+
Math.min(prev, Math.max(0, result.length - 1)),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// ignore — next tick retries
|
|
138
131
|
}
|
|
139
132
|
};
|
|
140
133
|
|
|
@@ -144,7 +137,7 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
144
137
|
mounted = false;
|
|
145
138
|
clearInterval(interval);
|
|
146
139
|
};
|
|
147
|
-
}, [
|
|
140
|
+
}, [projectDir, enabledFilter, refreshTick]);
|
|
148
141
|
|
|
149
142
|
const selectedSchedule = schedules[selectedIndex];
|
|
150
143
|
|
|
@@ -180,13 +173,10 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
180
173
|
|
|
181
174
|
useInput(
|
|
182
175
|
(input, key) => {
|
|
183
|
-
// Delete confirmation mode
|
|
184
176
|
if (confirmDelete) {
|
|
185
177
|
if (input === "y" || input === "d") {
|
|
186
178
|
if (selectedSchedule) {
|
|
187
|
-
|
|
188
|
-
deleteSchedule(conn, selectedSchedule.id),
|
|
189
|
-
).then(() => {
|
|
179
|
+
deleteSchedule(projectDir, selectedSchedule.id).then(() => {
|
|
190
180
|
forceRefresh();
|
|
191
181
|
});
|
|
192
182
|
}
|
|
@@ -246,11 +236,9 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
246
236
|
return;
|
|
247
237
|
}
|
|
248
238
|
if (input === "e" && selectedSchedule) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}),
|
|
253
|
-
).then(() => {
|
|
239
|
+
updateSchedule(projectDir, selectedSchedule.id, {
|
|
240
|
+
enabled: !selectedSchedule.enabled,
|
|
241
|
+
}).then(() => {
|
|
254
242
|
forceRefresh();
|
|
255
243
|
});
|
|
256
244
|
return;
|
|
@@ -299,7 +287,6 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
299
287
|
|
|
300
288
|
return (
|
|
301
289
|
<Box flexGrow={1} height={visibleRows + 1} overflow="hidden">
|
|
302
|
-
{/* Left sidebar: schedule list */}
|
|
303
290
|
<Box
|
|
304
291
|
flexDirection="column"
|
|
305
292
|
width={SIDEBAR_WIDTH}
|
|
@@ -361,7 +348,6 @@ export const SchedulePanel = memo(function SchedulePanel({
|
|
|
361
348
|
})}
|
|
362
349
|
</Box>
|
|
363
350
|
|
|
364
|
-
{/* Right detail pane */}
|
|
365
351
|
<Box
|
|
366
352
|
flexDirection="column"
|
|
367
353
|
flexGrow={1}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { listWorkers } from "../../db/workers.ts";
|
|
3
|
+
import { listTasks } from "../../tasks/store.ts";
|
|
4
|
+
import { listWorkers } from "../../workers/store.ts";
|
|
6
5
|
import { LogoChar } from "./Logo.tsx";
|
|
7
6
|
|
|
8
7
|
interface StatusBarProps {
|
|
9
8
|
projectDir: string;
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Retained for callers that pass it; unused now that workers + tasks
|
|
11
|
+
* both live on disk. Drop on the next TUI cleanup pass.
|
|
12
|
+
*/
|
|
13
|
+
dbPath?: string;
|
|
11
14
|
chatTitle?: string;
|
|
12
15
|
onWorkerStatusChange?: (running: boolean) => void;
|
|
13
16
|
}
|
|
@@ -19,8 +22,8 @@ interface Status {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function StatusBar({
|
|
22
|
-
projectDir
|
|
23
|
-
dbPath,
|
|
25
|
+
projectDir,
|
|
26
|
+
dbPath: _dbPath,
|
|
24
27
|
chatTitle,
|
|
25
28
|
onWorkerStatusChange,
|
|
26
29
|
}: StatusBarProps) {
|
|
@@ -39,14 +42,11 @@ export function StatusBar({
|
|
|
39
42
|
// because logger writes to stdout and would corrupt the Ink render.
|
|
40
43
|
const refresh = async () => {
|
|
41
44
|
try {
|
|
42
|
-
const [pending, inProgress, workers] = await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await listWorkers(conn, { status: "running" }),
|
|
48
|
-
],
|
|
49
|
-
);
|
|
45
|
+
const [pending, inProgress, workers] = await Promise.all([
|
|
46
|
+
listTasks(projectDir, { status: "pending" }),
|
|
47
|
+
listTasks(projectDir, { status: "in_progress" }),
|
|
48
|
+
listWorkers(projectDir, { status: "running" }),
|
|
49
|
+
]);
|
|
50
50
|
if (mounted) {
|
|
51
51
|
setStatus({
|
|
52
52
|
workerCount: workers.length,
|
|
@@ -66,7 +66,7 @@ export function StatusBar({
|
|
|
66
66
|
mounted = false;
|
|
67
67
|
clearInterval(interval);
|
|
68
68
|
};
|
|
69
|
-
}, [
|
|
69
|
+
}, [projectDir, onWorkerStatusChange]);
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<Box paddingX={0}>
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { withDb } from "../../db/connection.ts";
|
|
4
3
|
import {
|
|
5
|
-
deleteTask,
|
|
6
|
-
listTasks,
|
|
7
4
|
TASK_PRIORITIES,
|
|
8
5
|
TASK_STATUSES,
|
|
9
6
|
type Task,
|
|
10
|
-
} from "../../
|
|
7
|
+
} from "../../tasks/schema.ts";
|
|
8
|
+
import { deleteTask, listTasks } from "../../tasks/store.ts";
|
|
11
9
|
import { ansi, theme } from "../theme.ts";
|
|
12
10
|
|
|
13
11
|
interface TaskPanelProps {
|
|
14
|
-
|
|
12
|
+
projectDir: string;
|
|
15
13
|
isActive: boolean;
|
|
16
14
|
}
|
|
17
15
|
|
|
@@ -60,6 +58,17 @@ const PRIORITY_ANSI: Record<Task["priority"], string> = {
|
|
|
60
58
|
low: ansi.muted,
|
|
61
59
|
};
|
|
62
60
|
|
|
61
|
+
function formatTimestamp(iso: string): string {
|
|
62
|
+
const d = new Date(iso);
|
|
63
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
64
|
+
return d.toLocaleString([], {
|
|
65
|
+
month: "short",
|
|
66
|
+
day: "numeric",
|
|
67
|
+
hour: "2-digit",
|
|
68
|
+
minute: "2-digit",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function buildTaskDetailAnsi(task: Task): string {
|
|
64
73
|
const lines: string[] = [];
|
|
65
74
|
|
|
@@ -80,23 +89,11 @@ function buildTaskDetailAnsi(task: Task): string {
|
|
|
80
89
|
`${ansi.bold}${ansi.primary}Claimed${ansi.reset} ${task.claimed_by ? task.claimed_by : `${ansi.dim}(unclaimed)${ansi.reset}`}`,
|
|
81
90
|
);
|
|
82
91
|
|
|
83
|
-
const created = task.created_at.toLocaleString([], {
|
|
84
|
-
month: "short",
|
|
85
|
-
day: "numeric",
|
|
86
|
-
hour: "2-digit",
|
|
87
|
-
minute: "2-digit",
|
|
88
|
-
});
|
|
89
|
-
const updated = task.updated_at.toLocaleString([], {
|
|
90
|
-
month: "short",
|
|
91
|
-
day: "numeric",
|
|
92
|
-
hour: "2-digit",
|
|
93
|
-
minute: "2-digit",
|
|
94
|
-
});
|
|
95
92
|
lines.push(
|
|
96
|
-
`${ansi.bold}${ansi.primary}Created${ansi.reset} ${ansi.dim}${
|
|
93
|
+
`${ansi.bold}${ansi.primary}Created${ansi.reset} ${ansi.dim}${formatTimestamp(task.created_at)}${ansi.reset}`,
|
|
97
94
|
);
|
|
98
95
|
lines.push(
|
|
99
|
-
`${ansi.bold}${ansi.primary}Updated${ansi.reset} ${ansi.dim}${
|
|
96
|
+
`${ansi.bold}${ansi.primary}Updated${ansi.reset} ${ansi.dim}${formatTimestamp(task.updated_at)}${ansi.reset}`,
|
|
100
97
|
);
|
|
101
98
|
lines.push("");
|
|
102
99
|
|
|
@@ -126,17 +123,16 @@ function buildTaskDetailAnsi(task: Task): string {
|
|
|
126
123
|
lines.push("");
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
if (task.
|
|
130
|
-
lines.push(`${ansi.bold}${ansi.primary}Context
|
|
131
|
-
for (const
|
|
132
|
-
lines.push(` ${ansi.dim}• ${
|
|
126
|
+
if (task.context_paths.length > 0) {
|
|
127
|
+
lines.push(`${ansi.bold}${ansi.primary}Context Paths${ansi.reset}`);
|
|
128
|
+
for (const p of task.context_paths) {
|
|
129
|
+
lines.push(` ${ansi.dim}• ${p}${ansi.reset}`);
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
return lines.join("\n");
|
|
137
134
|
}
|
|
138
135
|
|
|
139
|
-
// Cycle through filter options: null -> ...values -> null
|
|
140
136
|
function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
141
137
|
if (current === null) return values[0] ?? null;
|
|
142
138
|
const idx = values.indexOf(current);
|
|
@@ -145,7 +141,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
export const TaskPanel = memo(function TaskPanel({
|
|
148
|
-
|
|
144
|
+
projectDir,
|
|
149
145
|
isActive,
|
|
150
146
|
}: TaskPanelProps) {
|
|
151
147
|
const { stdout } = useStdout();
|
|
@@ -159,7 +155,6 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
159
155
|
);
|
|
160
156
|
const [refreshTick, setRefreshTick] = useState(0);
|
|
161
157
|
|
|
162
|
-
// Fetch tasks on mount, filter change, and every 5 seconds
|
|
163
158
|
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshTick triggers manual refresh
|
|
164
159
|
useEffect(() => {
|
|
165
160
|
let mounted = true;
|
|
@@ -171,12 +166,16 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
171
166
|
} = {};
|
|
172
167
|
if (statusFilter) filters.status = statusFilter;
|
|
173
168
|
if (priorityFilter) filters.priority = priorityFilter;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
try {
|
|
170
|
+
const result = await listTasks(projectDir, filters);
|
|
171
|
+
if (mounted) {
|
|
172
|
+
setTasks(result);
|
|
173
|
+
setSelectedIndex((prev) =>
|
|
174
|
+
Math.min(prev, Math.max(0, result.length - 1)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// ignore — next tick retries
|
|
180
179
|
}
|
|
181
180
|
};
|
|
182
181
|
|
|
@@ -186,7 +185,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
186
185
|
mounted = false;
|
|
187
186
|
clearInterval(interval);
|
|
188
187
|
};
|
|
189
|
-
}, [
|
|
188
|
+
}, [projectDir, statusFilter, priorityFilter, refreshTick]);
|
|
190
189
|
|
|
191
190
|
const selectedTask = tasks[selectedIndex];
|
|
192
191
|
|
|
@@ -210,7 +209,6 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
210
209
|
),
|
|
211
210
|
);
|
|
212
211
|
|
|
213
|
-
// Reset detail scroll when selection changes
|
|
214
212
|
// biome-ignore lint/correctness/useExhaustiveDependencies: selectedIndex is the intentional trigger
|
|
215
213
|
useEffect(() => {
|
|
216
214
|
setDetailScroll(0);
|
|
@@ -275,7 +273,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
275
273
|
return;
|
|
276
274
|
}
|
|
277
275
|
if (input === "d" && selectedTask) {
|
|
278
|
-
|
|
276
|
+
deleteTask(projectDir, selectedTask.id).then(() => {
|
|
279
277
|
forceRefresh();
|
|
280
278
|
});
|
|
281
279
|
return;
|
|
@@ -323,7 +321,6 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
323
321
|
|
|
324
322
|
return (
|
|
325
323
|
<Box flexGrow={1} height={visibleRows + 1} overflow="hidden">
|
|
326
|
-
{/* Left sidebar: task list */}
|
|
327
324
|
<Box
|
|
328
325
|
flexDirection="column"
|
|
329
326
|
width={SIDEBAR_WIDTH}
|
|
@@ -350,7 +347,7 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
350
347
|
const isSelected = i === selectedIndex;
|
|
351
348
|
const icon = STATUS_ICONS[task.status];
|
|
352
349
|
const priorityLabel = PRIORITY_LABELS[task.priority];
|
|
353
|
-
const maxName = SIDEBAR_WIDTH - 11;
|
|
350
|
+
const maxName = SIDEBAR_WIDTH - 11;
|
|
354
351
|
const nameDisplay =
|
|
355
352
|
task.name.length > maxName
|
|
356
353
|
? `${task.name.slice(0, maxName - 1)}…`
|
|
@@ -381,7 +378,6 @@ export const TaskPanel = memo(function TaskPanel({
|
|
|
381
378
|
})}
|
|
382
379
|
</Box>
|
|
383
380
|
|
|
384
|
-
{/* Right detail pane */}
|
|
385
381
|
<Box
|
|
386
382
|
flexDirection="column"
|
|
387
383
|
flexGrow={1}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { withDb } from "../../db/connection.ts";
|
|
4
3
|
import {
|
|
5
4
|
deleteThread,
|
|
6
5
|
getInteractionsAfter,
|
|
@@ -9,11 +8,11 @@ import {
|
|
|
9
8
|
isThreadEnded,
|
|
10
9
|
listThreads,
|
|
11
10
|
type Thread,
|
|
12
|
-
} from "../../
|
|
11
|
+
} from "../../threads/store.ts";
|
|
13
12
|
import { ansi, theme } from "../theme.ts";
|
|
14
13
|
|
|
15
14
|
interface ThreadPanelProps {
|
|
16
|
-
|
|
15
|
+
projectDir: string;
|
|
17
16
|
activeThreadId: string;
|
|
18
17
|
isActive: boolean;
|
|
19
18
|
}
|
|
@@ -181,7 +180,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
export const ThreadPanel = memo(function ThreadPanel({
|
|
184
|
-
|
|
183
|
+
projectDir,
|
|
185
184
|
activeThreadId,
|
|
186
185
|
isActive,
|
|
187
186
|
}: ThreadPanelProps) {
|
|
@@ -210,12 +209,16 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
210
209
|
const refresh = async () => {
|
|
211
210
|
const filters: { type?: Thread["type"] } = {};
|
|
212
211
|
if (typeFilter) filters.type = typeFilter;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
212
|
+
try {
|
|
213
|
+
const result = await listThreads(projectDir, filters);
|
|
214
|
+
if (mounted) {
|
|
215
|
+
setThreads(result);
|
|
216
|
+
setSelectedIndex((prev) =>
|
|
217
|
+
Math.min(prev, Math.max(0, result.length - 1)),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore — next tick retries
|
|
219
222
|
}
|
|
220
223
|
};
|
|
221
224
|
|
|
@@ -225,7 +228,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
225
228
|
mounted = false;
|
|
226
229
|
clearInterval(interval);
|
|
227
230
|
};
|
|
228
|
-
}, [
|
|
231
|
+
}, [projectDir, typeFilter, refreshTick]);
|
|
229
232
|
|
|
230
233
|
// Filter threads by search query
|
|
231
234
|
const filteredThreads = useMemo(() => {
|
|
@@ -245,18 +248,16 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
245
248
|
return;
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
(result)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
},
|
|
254
|
-
);
|
|
251
|
+
getThread(projectDir, selectedThread.id).then((result) => {
|
|
252
|
+
if (mounted && result) {
|
|
253
|
+
setSelectedDetail(result);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
255
256
|
|
|
256
257
|
return () => {
|
|
257
258
|
mounted = false;
|
|
258
259
|
};
|
|
259
|
-
}, [
|
|
260
|
+
}, [projectDir, selectedThread?.id, following]);
|
|
260
261
|
|
|
261
262
|
// Follow mode: poll for new interactions every 1s
|
|
262
263
|
// biome-ignore lint/correctness/useExhaustiveDependencies: following and selectedThread?.id are the intentional triggers
|
|
@@ -266,12 +267,10 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
266
267
|
|
|
267
268
|
const poll = async () => {
|
|
268
269
|
try {
|
|
269
|
-
const newInteractions = await
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
lastSeenSequenceRef.current,
|
|
274
|
-
),
|
|
270
|
+
const newInteractions = await getInteractionsAfter(
|
|
271
|
+
projectDir,
|
|
272
|
+
selectedThread.id,
|
|
273
|
+
lastSeenSequenceRef.current,
|
|
275
274
|
);
|
|
276
275
|
if (!mounted || newInteractions.length === 0) return;
|
|
277
276
|
|
|
@@ -287,24 +286,18 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
287
286
|
};
|
|
288
287
|
});
|
|
289
288
|
|
|
290
|
-
// Auto-scroll will be handled by the detailLines/maxDetailScroll recalc
|
|
291
289
|
setDetailScroll(Number.MAX_SAFE_INTEGER);
|
|
292
290
|
|
|
293
|
-
const ended = await
|
|
294
|
-
isThreadEnded(conn, selectedThread.id),
|
|
295
|
-
);
|
|
291
|
+
const ended = await isThreadEnded(projectDir, selectedThread.id);
|
|
296
292
|
if (mounted && ended) {
|
|
297
293
|
setFollowing(false);
|
|
298
|
-
|
|
299
|
-
const result = await withDb(dbPath, (conn) =>
|
|
300
|
-
getThread(conn, selectedThread.id),
|
|
301
|
-
);
|
|
294
|
+
const result = await getThread(projectDir, selectedThread.id);
|
|
302
295
|
if (mounted && result) {
|
|
303
296
|
setSelectedDetail(result);
|
|
304
297
|
}
|
|
305
298
|
}
|
|
306
299
|
} catch {
|
|
307
|
-
// Transient
|
|
300
|
+
// Transient FS errors — retry next tick
|
|
308
301
|
}
|
|
309
302
|
};
|
|
310
303
|
|
|
@@ -314,7 +307,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
314
307
|
mounted = false;
|
|
315
308
|
clearInterval(interval);
|
|
316
309
|
};
|
|
317
|
-
}, [
|
|
310
|
+
}, [projectDir, following, selectedThread?.id]);
|
|
318
311
|
|
|
319
312
|
const isActiveSelected = selectedThread?.id === activeThreadId;
|
|
320
313
|
|
|
@@ -383,9 +376,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
383
376
|
if (confirmDelete) {
|
|
384
377
|
if (input === "y" || input === "d") {
|
|
385
378
|
if (selectedThread && !isActiveSelected) {
|
|
386
|
-
|
|
387
|
-
deleteThread(conn, selectedThread.id),
|
|
388
|
-
).then(() => {
|
|
379
|
+
deleteThread(projectDir, selectedThread.id).then(() => {
|
|
389
380
|
forceRefresh();
|
|
390
381
|
});
|
|
391
382
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { withDb } from "../../db/connection.ts";
|
|
4
|
-
import { listWorkers, type Worker } from "../../db/workers.ts";
|
|
5
3
|
import { readLogTail } from "../../worker/log-reader.ts";
|
|
4
|
+
import { listWorkers, type Worker } from "../../workers/store.ts";
|
|
6
5
|
|
|
7
6
|
interface WorkerPanelProps {
|
|
8
|
-
|
|
7
|
+
projectDir: string;
|
|
9
8
|
isActive: boolean;
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -30,7 +29,9 @@ function statusColor(status: Worker["status"]): string {
|
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
function formatAge(
|
|
32
|
+
function formatAge(fromIso: string, now: Date): string {
|
|
33
|
+
const from = new Date(fromIso);
|
|
34
|
+
if (Number.isNaN(from.getTime())) return "?";
|
|
34
35
|
const secs = Math.max(0, Math.floor((now.getTime() - from.getTime()) / 1000));
|
|
35
36
|
if (secs < 60) return `${secs}s`;
|
|
36
37
|
const mins = Math.floor(secs / 60);
|
|
@@ -47,7 +48,7 @@ function formatBytes(n: number): string {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export const WorkerPanel = memo(function WorkerPanel({
|
|
50
|
-
|
|
51
|
+
projectDir,
|
|
51
52
|
isActive,
|
|
52
53
|
}: WorkerPanelProps) {
|
|
53
54
|
const { stdout } = useStdout();
|
|
@@ -68,15 +69,17 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
68
69
|
|
|
69
70
|
const refresh = async () => {
|
|
70
71
|
const status = STATUS_FILTERS[filterIdx] ?? undefined;
|
|
71
|
-
|
|
72
|
-
listWorkers(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
try {
|
|
73
|
+
const result = await listWorkers(projectDir, status ? { status } : {});
|
|
74
|
+
if (mounted) {
|
|
75
|
+
setWorkers(result);
|
|
76
|
+
setNow(new Date());
|
|
77
|
+
setSelectedIndex((prev) =>
|
|
78
|
+
Math.min(prev, Math.max(0, result.length - 1)),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore — next tick retries
|
|
80
83
|
}
|
|
81
84
|
};
|
|
82
85
|
|
|
@@ -86,7 +89,7 @@ export const WorkerPanel = memo(function WorkerPanel({
|
|
|
86
89
|
mounted = false;
|
|
87
90
|
clearInterval(interval);
|
|
88
91
|
};
|
|
89
|
-
}, [
|
|
92
|
+
}, [projectDir, filterIdx]);
|
|
90
93
|
|
|
91
94
|
const selected = workers[selectedIndex];
|
|
92
95
|
const selectedLogPath = selected?.log_path ?? null;
|
|
@@ -332,18 +335,17 @@ function WorkerDetail({ worker, now }: { worker: Worker; now: Date }) {
|
|
|
332
335
|
</Text>
|
|
333
336
|
<Text>
|
|
334
337
|
<Text dimColor>Started </Text>
|
|
335
|
-
{worker.started_at
|
|
338
|
+
{worker.started_at}{" "}
|
|
336
339
|
<Text dimColor>({formatAge(worker.started_at, now)} ago)</Text>
|
|
337
340
|
</Text>
|
|
338
341
|
<Text>
|
|
339
|
-
<Text dimColor>Heartbeat</Text>{" "}
|
|
340
|
-
{worker.last_heartbeat_at.toISOString()}{" "}
|
|
342
|
+
<Text dimColor>Heartbeat</Text> {worker.last_heartbeat_at}{" "}
|
|
341
343
|
<Text dimColor>({formatAge(worker.last_heartbeat_at, now)} ago)</Text>
|
|
342
344
|
</Text>
|
|
343
345
|
{worker.stopped_at && (
|
|
344
346
|
<Text>
|
|
345
347
|
<Text dimColor>Stopped </Text>
|
|
346
|
-
{worker.stopped_at
|
|
348
|
+
{worker.stopped_at}
|
|
347
349
|
</Text>
|
|
348
350
|
)}
|
|
349
351
|
{worker.task_id && (
|
package/src/tui/markdown.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import type { ContextItem } from "../db/context.ts";
|
|
2
|
-
|
|
3
1
|
export function renderMarkdown(text: string): string {
|
|
4
2
|
if (!text) return "";
|
|
5
3
|
return Bun.markdown.ansi(text).trimEnd();
|
|
6
4
|
}
|
|
7
5
|
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
): boolean {
|
|
11
|
-
if (item.mime_type === "text/markdown") return true;
|
|
12
|
-
if (item.path.toLowerCase().endsWith(".md")) return true;
|
|
13
|
-
return false;
|
|
6
|
+
export function isMarkdownPath(path: string): boolean {
|
|
7
|
+
return path.toLowerCase().endsWith(".md");
|
|
14
8
|
}
|