botholomew 0.12.5 → 0.14.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 +2 -2
- package/src/chat/agent.ts +59 -86
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +178 -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 +803 -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 +293 -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 +74 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +53 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +27 -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 +8 -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/tool.ts +5 -0
- package/src/tools/util/sleep.ts +77 -0
- 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/SleepProgress.tsx +70 -0
- 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/ToolCall.tsx +10 -0
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- 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
|
@@ -18,10 +18,12 @@ const inputSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const outputSchema = z.object({
|
|
21
|
-
worker_pid: z.number(),
|
|
21
|
+
worker_pid: z.number().nullable(),
|
|
22
22
|
mode: z.enum(["once", "persist"]),
|
|
23
23
|
message: z.string(),
|
|
24
24
|
is_error: z.boolean(),
|
|
25
|
+
error_type: z.string().optional(),
|
|
26
|
+
next_action_hint: z.string().optional(),
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
export const spawnWorkerTool = {
|
|
@@ -33,18 +35,30 @@ export const spawnWorkerTool = {
|
|
|
33
35
|
outputSchema,
|
|
34
36
|
execute: async (input, ctx) => {
|
|
35
37
|
const mode = input.persist ? "persist" : "once";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
try {
|
|
39
|
+
const { pid } = await spawnWorker(ctx.projectDir, {
|
|
40
|
+
mode,
|
|
41
|
+
taskId: input.task_id,
|
|
42
|
+
});
|
|
43
|
+
const target = input.task_id
|
|
44
|
+
? `task ${input.task_id}`
|
|
45
|
+
: "next eligible task";
|
|
46
|
+
return {
|
|
47
|
+
worker_pid: pid,
|
|
48
|
+
mode,
|
|
49
|
+
message: `Spawned ${mode} worker (pid ${pid}) for ${target}.`,
|
|
50
|
+
is_error: false,
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
worker_pid: null,
|
|
55
|
+
mode,
|
|
56
|
+
message: err instanceof Error ? err.message : String(err),
|
|
57
|
+
is_error: true,
|
|
58
|
+
error_type: "spawn_failed",
|
|
59
|
+
next_action_hint:
|
|
60
|
+
"Bun must be on PATH for the spawned child to launch. Confirm with `which bun` from the same shell that runs the agent.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
49
63
|
},
|
|
50
64
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tui/App.tsx
CHANGED
|
@@ -8,14 +8,12 @@ import {
|
|
|
8
8
|
sendMessage,
|
|
9
9
|
startChatSession,
|
|
10
10
|
} from "../chat/session.ts";
|
|
11
|
-
import { withDb } from "../db/connection.ts";
|
|
12
|
-
import type { Interaction } from "../db/threads.ts";
|
|
13
|
-
import { getThread } from "../db/threads.ts";
|
|
14
11
|
import {
|
|
15
12
|
BUILTIN_SLASH_COMMANDS,
|
|
16
13
|
handleSlashCommand,
|
|
17
14
|
type SlashCommand,
|
|
18
15
|
} from "../skills/commands.ts";
|
|
16
|
+
import { getThread, type Interaction } from "../threads/store.ts";
|
|
19
17
|
import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../worker/large-results.ts";
|
|
20
18
|
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
21
19
|
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
@@ -172,8 +170,9 @@ export function App({
|
|
|
172
170
|
sessionRef.current = session;
|
|
173
171
|
|
|
174
172
|
if (session.messages.length > 0) {
|
|
175
|
-
const threadData = await
|
|
176
|
-
|
|
173
|
+
const threadData = await getThread(
|
|
174
|
+
session.projectDir,
|
|
175
|
+
session.threadId,
|
|
177
176
|
);
|
|
178
177
|
if (threadData) {
|
|
179
178
|
setMessages(
|
|
@@ -490,9 +489,7 @@ export function App({
|
|
|
490
489
|
const refreshTitle = async () => {
|
|
491
490
|
const session = sessionRef.current;
|
|
492
491
|
if (!session) return;
|
|
493
|
-
const result = await
|
|
494
|
-
getThread(conn, session.threadId),
|
|
495
|
-
);
|
|
492
|
+
const result = await getThread(session.projectDir, session.threadId);
|
|
496
493
|
if (mounted && result?.thread.title) {
|
|
497
494
|
setChatTitle(result.thread.title);
|
|
498
495
|
}
|
|
@@ -524,11 +521,7 @@ export function App({
|
|
|
524
521
|
);
|
|
525
522
|
}
|
|
526
523
|
} else {
|
|
527
|
-
skillLines.push(
|
|
528
|
-
"",
|
|
529
|
-
"Skills:",
|
|
530
|
-
" (none — add .md files to .botholomew/skills/)",
|
|
531
|
-
);
|
|
524
|
+
skillLines.push("", "Skills:", " (none — add .md files to skills/)");
|
|
532
525
|
}
|
|
533
526
|
|
|
534
527
|
const helpMsg: ChatMessage = {
|
|
@@ -730,7 +723,7 @@ export function App({
|
|
|
730
723
|
);
|
|
731
724
|
}
|
|
732
725
|
|
|
733
|
-
const
|
|
726
|
+
const _dbPath = sessionRef.current.dbPath;
|
|
734
727
|
const threadId = sessionRef.current.threadId;
|
|
735
728
|
|
|
736
729
|
return (
|
|
@@ -771,14 +764,14 @@ export function App({
|
|
|
771
764
|
flexDirection="column"
|
|
772
765
|
flexGrow={1}
|
|
773
766
|
>
|
|
774
|
-
<ContextPanel
|
|
767
|
+
<ContextPanel projectDir={projectDir} isActive={activeTab === 3} />
|
|
775
768
|
</Box>
|
|
776
769
|
<Box
|
|
777
770
|
display={activeTab === 4 ? "flex" : "none"}
|
|
778
771
|
flexDirection="column"
|
|
779
772
|
flexGrow={1}
|
|
780
773
|
>
|
|
781
|
-
<TaskPanel
|
|
774
|
+
<TaskPanel projectDir={projectDir} isActive={activeTab === 4} />
|
|
782
775
|
</Box>
|
|
783
776
|
<Box
|
|
784
777
|
display={activeTab === 5 ? "flex" : "none"}
|
|
@@ -786,7 +779,7 @@ export function App({
|
|
|
786
779
|
flexGrow={1}
|
|
787
780
|
>
|
|
788
781
|
<ThreadPanel
|
|
789
|
-
|
|
782
|
+
projectDir={projectDir}
|
|
790
783
|
activeThreadId={threadId}
|
|
791
784
|
isActive={activeTab === 5}
|
|
792
785
|
/>
|
|
@@ -796,14 +789,14 @@ export function App({
|
|
|
796
789
|
flexDirection="column"
|
|
797
790
|
flexGrow={1}
|
|
798
791
|
>
|
|
799
|
-
<SchedulePanel
|
|
792
|
+
<SchedulePanel projectDir={projectDir} isActive={activeTab === 6} />
|
|
800
793
|
</Box>
|
|
801
794
|
<Box
|
|
802
795
|
display={activeTab === 7 ? "flex" : "none"}
|
|
803
796
|
flexDirection="column"
|
|
804
797
|
flexGrow={1}
|
|
805
798
|
>
|
|
806
|
-
<WorkerPanel
|
|
799
|
+
<WorkerPanel projectDir={projectDir} isActive={activeTab === 7} />
|
|
807
800
|
</Box>
|
|
808
801
|
<Box
|
|
809
802
|
display={activeTab === 8 ? "flex" : "none"}
|
|
@@ -1,179 +1,91 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { formatDriveRef } from "../../context/drives.ts";
|
|
4
|
-
import { withDb } from "../../db/connection.ts";
|
|
5
3
|
import {
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
listDriveSummaries,
|
|
12
|
-
searchContextByKeyword,
|
|
13
|
-
} from "../../db/context.ts";
|
|
14
|
-
import { isMarkdownItem, renderMarkdown } from "../markdown.ts";
|
|
4
|
+
type ContextEntry,
|
|
5
|
+
listContextDir,
|
|
6
|
+
readContextFile,
|
|
7
|
+
} from "../../context/store.ts";
|
|
8
|
+
import { isMarkdownPath, renderMarkdown } from "../markdown.ts";
|
|
15
9
|
|
|
16
10
|
interface ContextPanelProps {
|
|
17
|
-
|
|
11
|
+
projectDir: string;
|
|
18
12
|
isActive: boolean;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
|
-
interface DriveEntry {
|
|
22
|
-
type: "drive";
|
|
23
|
-
drive: string;
|
|
24
|
-
count: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface DirEntry {
|
|
28
|
-
type: "directory";
|
|
29
|
-
name: string;
|
|
30
|
-
path: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface FileEntry {
|
|
34
|
-
type: "file";
|
|
35
|
-
item: ContextItem;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type Entry = DriveEntry | DirEntry | FileEntry;
|
|
39
|
-
|
|
40
15
|
const CHROME_LINES = 8;
|
|
41
16
|
|
|
42
17
|
export const ContextPanel = memo(function ContextPanel({
|
|
43
|
-
|
|
18
|
+
projectDir,
|
|
44
19
|
isActive,
|
|
45
20
|
}: ContextPanelProps) {
|
|
46
21
|
const { stdout } = useStdout();
|
|
47
22
|
const termRows = stdout?.rows ?? 24;
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
const [
|
|
51
|
-
const [currentPath, setCurrentPath] = useState("/");
|
|
52
|
-
const [entries, setEntries] = useState<Entry[]>([]);
|
|
24
|
+
const [currentPath, setCurrentPath] = useState("");
|
|
25
|
+
const [entries, setEntries] = useState<ContextEntry[]>([]);
|
|
53
26
|
const [cursor, setCursor] = useState(0);
|
|
54
27
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
55
|
-
const [preview, setPreview] = useState<
|
|
28
|
+
const [preview, setPreview] = useState<{
|
|
29
|
+
entry: ContextEntry;
|
|
30
|
+
content: string;
|
|
31
|
+
} | null>(null);
|
|
56
32
|
const [previewScroll, setPreviewScroll] = useState(0);
|
|
57
|
-
const [searchMode, setSearchMode] = useState(false);
|
|
58
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
59
|
-
const [searchResults, setSearchResults] = useState<ContextItem[] | null>(
|
|
60
|
-
null,
|
|
61
|
-
);
|
|
62
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
63
33
|
|
|
64
34
|
const visibleRows = Math.max(1, termRows - CHROME_LINES);
|
|
65
35
|
|
|
66
36
|
useEffect(() => {
|
|
67
|
-
if (cursor < scrollOffset)
|
|
68
|
-
|
|
69
|
-
} else if (cursor >= scrollOffset + visibleRows) {
|
|
37
|
+
if (cursor < scrollOffset) setScrollOffset(cursor);
|
|
38
|
+
else if (cursor >= scrollOffset + visibleRows) {
|
|
70
39
|
setScrollOffset(cursor - visibleRows + 1);
|
|
71
40
|
}
|
|
72
41
|
}, [cursor, scrollOffset, visibleRows]);
|
|
73
42
|
|
|
74
|
-
const
|
|
75
|
-
async (
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
43
|
+
const refresh = useCallback(
|
|
44
|
+
async (path: string) => {
|
|
45
|
+
try {
|
|
46
|
+
const list = await listContextDir(projectDir, path, {
|
|
47
|
+
recursive: false,
|
|
48
|
+
});
|
|
49
|
+
list.sort((a, b) => {
|
|
50
|
+
if (a.is_directory !== b.is_directory) return a.is_directory ? -1 : 1;
|
|
51
|
+
return a.path.localeCompare(b.path);
|
|
52
|
+
});
|
|
53
|
+
setEntries(list);
|
|
54
|
+
setCursor(0);
|
|
55
|
+
setScrollOffset(0);
|
|
56
|
+
setPreview(null);
|
|
57
|
+
} catch {
|
|
58
|
+
setEntries([]);
|
|
86
59
|
setCursor(0);
|
|
87
60
|
setScrollOffset(0);
|
|
88
61
|
setPreview(null);
|
|
89
|
-
return;
|
|
90
62
|
}
|
|
91
|
-
|
|
92
|
-
const [dirs, files] = await withDb(dbPath, async (conn) => [
|
|
93
|
-
await getDistinctDirectories(conn, drive, path),
|
|
94
|
-
await listContextItemsByPrefix(conn, drive, path, { recursive: false }),
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
const dirEntries: DirEntry[] = dirs.map((d) => ({
|
|
98
|
-
type: "directory",
|
|
99
|
-
name: d,
|
|
100
|
-
path: `${d}/`,
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
const fileEntries: FileEntry[] = files
|
|
104
|
-
.filter((f) => !dirs.some((d) => f.path.startsWith(`${d}/`)))
|
|
105
|
-
.map((f) => ({ type: "file", item: f }));
|
|
106
|
-
|
|
107
|
-
setEntries([...dirEntries, ...fileEntries]);
|
|
108
|
-
setCursor(0);
|
|
109
|
-
setScrollOffset(0);
|
|
110
|
-
setPreview(null);
|
|
111
63
|
},
|
|
112
|
-
[
|
|
64
|
+
[projectDir],
|
|
113
65
|
);
|
|
114
66
|
|
|
115
67
|
useEffect(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
}, [currentDrive, currentPath, loadEntries, searchResults]);
|
|
68
|
+
refresh(currentPath);
|
|
69
|
+
}, [currentPath, refresh]);
|
|
120
70
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
setSearchResults(results);
|
|
131
|
-
setCursor(0);
|
|
132
|
-
setScrollOffset(0);
|
|
133
|
-
setPreview(null);
|
|
134
|
-
},
|
|
135
|
-
[dbPath],
|
|
136
|
-
);
|
|
71
|
+
const previewLines = useMemo(() => {
|
|
72
|
+
if (!preview) return [];
|
|
73
|
+
const body =
|
|
74
|
+
isMarkdownPath(preview.entry.path) && preview.entry.is_textual
|
|
75
|
+
? renderMarkdown(preview.content)
|
|
76
|
+
: preview.content;
|
|
77
|
+
return body.split("\n");
|
|
78
|
+
}, [preview]);
|
|
137
79
|
|
|
138
|
-
const items =
|
|
80
|
+
const items = entries;
|
|
139
81
|
const itemCount = items.length;
|
|
140
82
|
const visibleItems = useMemo(
|
|
141
83
|
() => items.slice(scrollOffset, scrollOffset + visibleRows),
|
|
142
84
|
[items, scrollOffset, visibleRows],
|
|
143
85
|
);
|
|
144
86
|
|
|
145
|
-
const previewLines = useMemo(() => {
|
|
146
|
-
if (!preview?.content) return [];
|
|
147
|
-
const body = isMarkdownItem(preview)
|
|
148
|
-
? renderMarkdown(preview.content)
|
|
149
|
-
: preview.content;
|
|
150
|
-
return body.split("\n");
|
|
151
|
-
}, [preview]);
|
|
152
|
-
|
|
153
87
|
useInput(
|
|
154
88
|
(input, key) => {
|
|
155
|
-
if (searchMode) {
|
|
156
|
-
if (key.return) {
|
|
157
|
-
setSearchMode(false);
|
|
158
|
-
executeSearch(searchQuery);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (key.escape) {
|
|
162
|
-
setSearchMode(false);
|
|
163
|
-
setSearchQuery("");
|
|
164
|
-
setSearchResults(null);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (key.backspace || key.delete) {
|
|
168
|
-
setSearchQuery((q) => q.slice(0, -1));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (input && !key.ctrl && !key.meta) {
|
|
172
|
-
setSearchQuery((q) => q + input);
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
89
|
if (preview) {
|
|
178
90
|
if (key.upArrow) {
|
|
179
91
|
setPreviewScroll((s) => Math.max(0, s - 1));
|
|
@@ -184,58 +96,13 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
184
96
|
setPreviewScroll((s) => Math.min(maxScroll, s + 1));
|
|
185
97
|
return;
|
|
186
98
|
}
|
|
187
|
-
if (key.escape) {
|
|
99
|
+
if (key.escape || input === "q") {
|
|
188
100
|
setPreview(null);
|
|
189
101
|
setPreviewScroll(0);
|
|
190
|
-
return;
|
|
191
102
|
}
|
|
192
103
|
return;
|
|
193
104
|
}
|
|
194
105
|
|
|
195
|
-
if (confirmDelete) {
|
|
196
|
-
if (input === "y" || input === "d") {
|
|
197
|
-
const entry = entries[cursor];
|
|
198
|
-
if (entry) {
|
|
199
|
-
void withDb(dbPath, async (conn) => {
|
|
200
|
-
if (entry.type === "directory" && currentDrive) {
|
|
201
|
-
await deleteContextItemsByPrefix(
|
|
202
|
-
conn,
|
|
203
|
-
currentDrive,
|
|
204
|
-
entry.path,
|
|
205
|
-
);
|
|
206
|
-
} else if (entry.type === "file") {
|
|
207
|
-
await deleteContextItem(conn, entry.item.id);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
setConfirmDelete(false);
|
|
211
|
-
loadEntries(currentDrive, currentPath);
|
|
212
|
-
}
|
|
213
|
-
} else {
|
|
214
|
-
setConfirmDelete(false);
|
|
215
|
-
}
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (input === "d" && itemCount > 0 && searchResults === null) {
|
|
220
|
-
setConfirmDelete(true);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (input === "/") {
|
|
225
|
-
setSearchMode(true);
|
|
226
|
-
setSearchQuery("");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (key.escape) {
|
|
231
|
-
if (searchResults !== null) {
|
|
232
|
-
setSearchResults(null);
|
|
233
|
-
setPreview(null);
|
|
234
|
-
setScrollOffset(0);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
106
|
if (key.upArrow) {
|
|
240
107
|
setCursor((c) => Math.max(0, c - 1));
|
|
241
108
|
return;
|
|
@@ -244,84 +111,31 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
244
111
|
setCursor((c) => Math.min(itemCount - 1, c + 1));
|
|
245
112
|
return;
|
|
246
113
|
}
|
|
247
|
-
|
|
248
114
|
if (key.return) {
|
|
249
|
-
if (searchResults !== null) {
|
|
250
|
-
const item = searchResults[cursor];
|
|
251
|
-
if (item) {
|
|
252
|
-
setPreview(item);
|
|
253
|
-
setPreviewScroll(0);
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
115
|
const entry = entries[cursor];
|
|
258
116
|
if (!entry) return;
|
|
259
|
-
if (entry.
|
|
260
|
-
setCurrentDrive(entry.drive);
|
|
261
|
-
setCurrentPath("/");
|
|
262
|
-
} else if (entry.type === "directory") {
|
|
117
|
+
if (entry.is_directory) {
|
|
263
118
|
setCurrentPath(entry.path);
|
|
264
|
-
|
|
265
|
-
setPreview(entry.item);
|
|
266
|
-
setPreviewScroll(0);
|
|
119
|
+
return;
|
|
267
120
|
}
|
|
121
|
+
if (!entry.is_textual) return;
|
|
122
|
+
readContextFile(projectDir, entry.path).then((content) => {
|
|
123
|
+
setPreview({ entry, content });
|
|
124
|
+
setPreviewScroll(0);
|
|
125
|
+
});
|
|
268
126
|
return;
|
|
269
127
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const parent = parts.length <= 1 ? "/" : `${parts.join("/")}/`;
|
|
276
|
-
setCurrentPath(parent);
|
|
277
|
-
} else if (currentDrive !== null) {
|
|
278
|
-
setCurrentDrive(null);
|
|
279
|
-
}
|
|
128
|
+
if (key.backspace || key.delete || input === "h") {
|
|
129
|
+
if (currentPath === "") return;
|
|
130
|
+
const parts = currentPath.split("/");
|
|
131
|
+
parts.pop();
|
|
132
|
+
setCurrentPath(parts.join("/"));
|
|
280
133
|
}
|
|
134
|
+
if (input === "r") refresh(currentPath);
|
|
281
135
|
},
|
|
282
136
|
{ isActive },
|
|
283
137
|
);
|
|
284
138
|
|
|
285
|
-
if (searchResults !== null && !preview) {
|
|
286
|
-
return (
|
|
287
|
-
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
288
|
-
<Box>
|
|
289
|
-
<Text bold color="cyan">
|
|
290
|
-
Search results for: "{searchQuery}"
|
|
291
|
-
</Text>
|
|
292
|
-
<Text dimColor> ({searchResults.length} matches · esc to clear)</Text>
|
|
293
|
-
</Box>
|
|
294
|
-
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
|
295
|
-
{searchResults.length === 0 && <Text dimColor>No results found</Text>}
|
|
296
|
-
{visibleItems.map((item, vi) => {
|
|
297
|
-
const i = vi + scrollOffset;
|
|
298
|
-
const ci = item as ContextItem;
|
|
299
|
-
const ref = formatDriveRef(ci);
|
|
300
|
-
return (
|
|
301
|
-
<Box key={ci.id}>
|
|
302
|
-
<Text
|
|
303
|
-
backgroundColor={i === cursor ? "#333" : undefined}
|
|
304
|
-
color={i === cursor ? "cyan" : undefined}
|
|
305
|
-
>
|
|
306
|
-
{" "}📄 <Text dimColor>{ref}</Text> — {ci.title}
|
|
307
|
-
<Text dimColor> ({ci.mime_type})</Text>
|
|
308
|
-
</Text>
|
|
309
|
-
</Box>
|
|
310
|
-
);
|
|
311
|
-
})}
|
|
312
|
-
</Box>
|
|
313
|
-
{itemCount > visibleRows && (
|
|
314
|
-
<Box>
|
|
315
|
-
<Text dimColor>
|
|
316
|
-
[{scrollOffset + 1}–
|
|
317
|
-
{Math.min(scrollOffset + visibleRows, itemCount)} of {itemCount}]
|
|
318
|
-
</Text>
|
|
319
|
-
</Box>
|
|
320
|
-
)}
|
|
321
|
-
</Box>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
139
|
if (preview) {
|
|
326
140
|
const visiblePreviewLines = previewLines.slice(
|
|
327
141
|
previewScroll,
|
|
@@ -331,18 +145,14 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
331
145
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
332
146
|
<Box>
|
|
333
147
|
<Text bold color="cyan">
|
|
334
|
-
{
|
|
148
|
+
context/{preview.entry.path}
|
|
335
149
|
</Text>
|
|
336
|
-
<Text dimColor> (esc to go back · ↑↓ to scroll)</Text>
|
|
150
|
+
<Text dimColor> (esc/q to go back · ↑↓ to scroll)</Text>
|
|
337
151
|
</Box>
|
|
338
152
|
<Box marginTop={1} flexDirection="column">
|
|
339
153
|
<Text dimColor>
|
|
340
|
-
|
|
341
|
-
{preview.
|
|
342
|
-
</Text>
|
|
343
|
-
<Text dimColor>
|
|
344
|
-
{preview.indexed_at ? "Indexed" : "Not indexed"} · Updated:{" "}
|
|
345
|
-
{preview.updated_at.toLocaleDateString()}
|
|
154
|
+
{preview.entry.mime_type} · {preview.entry.size} bytes · updated{" "}
|
|
155
|
+
{preview.entry.mtime.toLocaleDateString()}
|
|
346
156
|
</Text>
|
|
347
157
|
</Box>
|
|
348
158
|
<Box
|
|
@@ -351,14 +161,10 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
351
161
|
flexGrow={1}
|
|
352
162
|
overflow="hidden"
|
|
353
163
|
>
|
|
354
|
-
{
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
})
|
|
359
|
-
) : (
|
|
360
|
-
<Text dimColor>(binary or empty content)</Text>
|
|
361
|
-
)}
|
|
164
|
+
{visiblePreviewLines.map((line, i) => {
|
|
165
|
+
const lineNum = previewScroll + i;
|
|
166
|
+
return <Text key={lineNum}>{line || " "}</Text>;
|
|
167
|
+
})}
|
|
362
168
|
</Box>
|
|
363
169
|
{previewLines.length > visibleRows - 2 && (
|
|
364
170
|
<Box>
|
|
@@ -374,9 +180,7 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
374
180
|
}
|
|
375
181
|
|
|
376
182
|
const headerLabel =
|
|
377
|
-
|
|
378
|
-
? "(drives)"
|
|
379
|
-
: formatDriveRef({ drive: currentDrive, path: currentPath });
|
|
183
|
+
currentPath === "" ? "context/" : `context/${currentPath}/`;
|
|
380
184
|
|
|
381
185
|
return (
|
|
382
186
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
@@ -386,69 +190,32 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
386
190
|
</Text>
|
|
387
191
|
<Text dimColor>
|
|
388
192
|
{" "}
|
|
389
|
-
({entries.length}
|
|
193
|
+
({entries.length} entries · ↑↓ select · ⏎ open · backspace up · r
|
|
194
|
+
refresh)
|
|
390
195
|
</Text>
|
|
391
196
|
</Box>
|
|
392
|
-
{searchMode && (
|
|
393
|
-
<Box marginTop={1}>
|
|
394
|
-
<Text color="green">search: </Text>
|
|
395
|
-
<Text>{searchQuery}</Text>
|
|
396
|
-
<Text dimColor>█</Text>
|
|
397
|
-
</Box>
|
|
398
|
-
)}
|
|
399
|
-
{confirmDelete && entries[cursor] && (
|
|
400
|
-
<Box marginTop={1}>
|
|
401
|
-
<Text color="red" bold>
|
|
402
|
-
Delete{" "}
|
|
403
|
-
{entries[cursor].type === "directory"
|
|
404
|
-
? `${(entries[cursor] as DirEntry).name}/ and all contents`
|
|
405
|
-
: entries[cursor].type === "file"
|
|
406
|
-
? (entries[cursor] as FileEntry).item.title
|
|
407
|
-
: "(pick a file first)"}
|
|
408
|
-
? (y/n)
|
|
409
|
-
</Text>
|
|
410
|
-
</Box>
|
|
411
|
-
)}
|
|
412
197
|
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
|
413
|
-
{entries.length === 0 && <Text dimColor>
|
|
414
|
-
{visibleItems.map((
|
|
198
|
+
{entries.length === 0 && <Text dimColor>(empty)</Text>}
|
|
199
|
+
{visibleItems.map((entry, vi) => {
|
|
415
200
|
const i = vi + scrollOffset;
|
|
416
|
-
const entry = raw as Entry;
|
|
417
201
|
const isSelected = i === cursor;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
<Box key={entry.drive}>
|
|
421
|
-
<Text
|
|
422
|
-
backgroundColor={isSelected ? "#333" : undefined}
|
|
423
|
-
color={isSelected ? "cyan" : "magenta"}
|
|
424
|
-
bold={isSelected}
|
|
425
|
-
>
|
|
426
|
-
{" "}🗄 {entry.drive}:/ <Text dimColor>({entry.count})</Text>
|
|
427
|
-
</Text>
|
|
428
|
-
</Box>
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
if (entry.type === "directory") {
|
|
432
|
-
return (
|
|
433
|
-
<Box key={entry.path}>
|
|
434
|
-
<Text
|
|
435
|
-
backgroundColor={isSelected ? "#333" : undefined}
|
|
436
|
-
color={isSelected ? "cyan" : "blue"}
|
|
437
|
-
bold={isSelected}
|
|
438
|
-
>
|
|
439
|
-
{" "}📁 {entry.name}/
|
|
440
|
-
</Text>
|
|
441
|
-
</Box>
|
|
442
|
-
);
|
|
443
|
-
}
|
|
202
|
+
const name = entry.path.split("/").pop() ?? entry.path;
|
|
203
|
+
const icon = entry.is_directory ? "📁" : "📄";
|
|
444
204
|
return (
|
|
445
|
-
<Box key={entry.
|
|
205
|
+
<Box key={entry.path}>
|
|
446
206
|
<Text
|
|
447
207
|
backgroundColor={isSelected ? "#333" : undefined}
|
|
448
|
-
color={
|
|
208
|
+
color={
|
|
209
|
+
isSelected ? "cyan" : entry.is_directory ? "blue" : undefined
|
|
210
|
+
}
|
|
211
|
+
bold={isSelected}
|
|
449
212
|
>
|
|
450
|
-
{" "}
|
|
451
|
-
|
|
213
|
+
{" "}
|
|
214
|
+
{icon} {name}
|
|
215
|
+
{entry.is_directory ? "/" : ""}
|
|
216
|
+
{!entry.is_directory && (
|
|
217
|
+
<Text dimColor> ({entry.mime_type})</Text>
|
|
218
|
+
)}
|
|
452
219
|
</Text>
|
|
453
220
|
</Box>
|
|
454
221
|
);
|