botholomew 0.8.10 → 0.9.4
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/package.json +1 -1
- package/src/chat/agent.ts +5 -3
- package/src/commands/context.ts +223 -373
- package/src/commands/tools.ts +100 -11
- package/src/context/describer.ts +3 -118
- package/src/context/drives.ts +110 -0
- package/src/context/fetcher.ts +11 -1
- package/src/context/ingest.ts +13 -10
- package/src/context/refresh.ts +39 -24
- package/src/context/url-utils.ts +0 -23
- package/src/db/context.ts +195 -119
- package/src/db/embeddings.ts +35 -16
- package/src/db/sql/13-drive-paths.sql +49 -0
- package/src/tools/context/list-drives.ts +36 -0
- package/src/tools/context/refresh.ts +41 -23
- package/src/tools/context/search.ts +8 -3
- package/src/tools/dir/create.ts +14 -11
- package/src/tools/dir/size.ts +3 -2
- package/src/tools/dir/tree.ts +57 -17
- package/src/tools/file/copy.ts +14 -8
- package/src/tools/file/count-lines.ts +6 -3
- package/src/tools/file/delete.ts +12 -5
- package/src/tools/file/edit.ts +5 -3
- package/src/tools/file/exists.ts +25 -3
- package/src/tools/file/info.ts +90 -18
- package/src/tools/file/move.ts +15 -16
- package/src/tools/file/read.ts +79 -5
- package/src/tools/file/write.ts +29 -12
- package/src/tools/registry.ts +2 -2
- package/src/tools/search/grep.ts +44 -11
- package/src/tools/search/semantic.ts +7 -3
- package/src/tui/components/ContextPanel.tsx +73 -35
- package/src/tui/markdown.ts +2 -3
- package/src/worker/prompt.ts +3 -2
- package/src/tools/dir/list.ts +0 -89
package/src/tools/search/grep.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
|
+
import {
|
|
4
|
+
listContextItems,
|
|
5
|
+
listContextItemsByPrefix,
|
|
6
|
+
} from "../../db/context.ts";
|
|
3
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
8
|
|
|
5
9
|
const GrepMatchSchema = z.object({
|
|
10
|
+
ref: z.string(),
|
|
11
|
+
drive: z.string(),
|
|
6
12
|
path: z.string(),
|
|
7
13
|
line: z.number(),
|
|
8
14
|
content: z.string(),
|
|
@@ -11,14 +17,20 @@ const GrepMatchSchema = z.object({
|
|
|
11
17
|
|
|
12
18
|
const inputSchema = z.object({
|
|
13
19
|
pattern: z.string().describe("Regex pattern to search for"),
|
|
20
|
+
drive: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Restrict search to a single drive (defaults to all drives)"),
|
|
14
24
|
path: z
|
|
15
25
|
.string()
|
|
16
26
|
.optional()
|
|
17
|
-
.describe(
|
|
27
|
+
.describe(
|
|
28
|
+
"Directory to search under within the drive (defaults to /). Requires `drive`.",
|
|
29
|
+
),
|
|
18
30
|
glob: z
|
|
19
31
|
.string()
|
|
20
32
|
.optional()
|
|
21
|
-
.describe("Only search files
|
|
33
|
+
.describe("Only search files whose basename matches this glob pattern"),
|
|
22
34
|
ignore_case: z.boolean().optional().describe("Case-insensitive search"),
|
|
23
35
|
context: z
|
|
24
36
|
.number()
|
|
@@ -33,20 +45,39 @@ const inputSchema = z.object({
|
|
|
33
45
|
const outputSchema = z.object({
|
|
34
46
|
matches: z.array(GrepMatchSchema),
|
|
35
47
|
is_error: z.boolean(),
|
|
48
|
+
error_type: z.string().optional(),
|
|
49
|
+
message: z.string().optional(),
|
|
36
50
|
});
|
|
37
51
|
|
|
38
52
|
export const searchGrepTool = {
|
|
39
53
|
name: "search_grep",
|
|
40
|
-
description:
|
|
41
|
-
"Search file contents by regex pattern in the virtual filesystem.",
|
|
54
|
+
description: "Search file contents by regex pattern across context drives.",
|
|
42
55
|
group: "search",
|
|
43
56
|
inputSchema,
|
|
44
57
|
outputSchema,
|
|
45
58
|
execute: async (input, ctx) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
// `path` scopes to a directory within a single drive; requiring `drive`
|
|
60
|
+
// alongside prevents a silent full-DB scan when only `path` is passed.
|
|
61
|
+
if (input.path && !input.drive) {
|
|
62
|
+
return {
|
|
63
|
+
matches: [],
|
|
64
|
+
is_error: true,
|
|
65
|
+
error_type: "invalid_arguments",
|
|
66
|
+
message:
|
|
67
|
+
"`path` requires `drive` — use context_list_drives to see which drives exist, then pass `drive` alongside `path`.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const items = input.drive
|
|
72
|
+
? await listContextItemsByPrefix(
|
|
73
|
+
ctx.conn,
|
|
74
|
+
input.drive,
|
|
75
|
+
input.path ?? "/",
|
|
76
|
+
{
|
|
77
|
+
recursive: true,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
: await listContextItems(ctx.conn);
|
|
50
81
|
|
|
51
82
|
const flags = input.ignore_case ? "gi" : "g";
|
|
52
83
|
const regex = new RegExp(input.pattern, flags);
|
|
@@ -60,7 +91,7 @@ export const searchGrepTool = {
|
|
|
60
91
|
if (item.content == null) continue;
|
|
61
92
|
|
|
62
93
|
if (globRegex) {
|
|
63
|
-
const filename = item.
|
|
94
|
+
const filename = item.path.split("/").pop() ?? "";
|
|
64
95
|
if (!globRegex.test(filename)) continue;
|
|
65
96
|
}
|
|
66
97
|
|
|
@@ -72,7 +103,9 @@ export const searchGrepTool = {
|
|
|
72
103
|
const start = Math.max(0, i - contextLines);
|
|
73
104
|
const end = Math.min(lines.length, i + contextLines + 1);
|
|
74
105
|
matches.push({
|
|
75
|
-
|
|
106
|
+
ref: formatDriveRef(item),
|
|
107
|
+
drive: item.drive,
|
|
108
|
+
path: item.path,
|
|
76
109
|
line: i + 1,
|
|
77
110
|
content: line,
|
|
78
111
|
context_lines: lines.slice(start, end),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
2
3
|
import { embedSingle } from "../../context/embedder.ts";
|
|
3
4
|
import { hybridSearch } from "../../db/embeddings.ts";
|
|
4
5
|
import type { ToolDefinition } from "../tool.ts";
|
|
@@ -19,7 +20,7 @@ const inputSchema = z.object({
|
|
|
19
20
|
const outputSchema = z.object({
|
|
20
21
|
results: z.array(
|
|
21
22
|
z.object({
|
|
22
|
-
|
|
23
|
+
ref: z.string(),
|
|
23
24
|
title: z.string(),
|
|
24
25
|
score: z.number(),
|
|
25
26
|
snippet: z.string(),
|
|
@@ -31,7 +32,7 @@ const outputSchema = z.object({
|
|
|
31
32
|
export const searchSemanticTool = {
|
|
32
33
|
name: "search_semantic",
|
|
33
34
|
description:
|
|
34
|
-
"Semantic search over indexed
|
|
35
|
+
"Semantic search over indexed context using vector embeddings. Finds conceptually related content, not just keyword matches.",
|
|
35
36
|
group: "search",
|
|
36
37
|
inputSchema,
|
|
37
38
|
outputSchema,
|
|
@@ -53,7 +54,10 @@ export const searchSemanticTool = {
|
|
|
53
54
|
return {
|
|
54
55
|
results: filtered
|
|
55
56
|
.map((r) => ({
|
|
56
|
-
|
|
57
|
+
ref:
|
|
58
|
+
r.drive && r.path
|
|
59
|
+
? formatDriveRef({ drive: r.drive, path: r.path })
|
|
60
|
+
: r.context_item_id,
|
|
57
61
|
title: r.title,
|
|
58
62
|
score: Math.round(r.score * 1000) / 1000,
|
|
59
63
|
snippet: (r.chunk_content || "").slice(0, 300),
|
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
3
4
|
import { withDb } from "../../db/connection.ts";
|
|
4
5
|
import {
|
|
5
6
|
type ContextItem,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
deleteContextItemsByPrefix,
|
|
8
9
|
getDistinctDirectories,
|
|
9
10
|
listContextItemsByPrefix,
|
|
11
|
+
listDriveSummaries,
|
|
10
12
|
searchContextByKeyword,
|
|
11
13
|
} from "../../db/context.ts";
|
|
12
14
|
import { isMarkdownItem, renderMarkdown } from "../markdown.ts";
|
|
@@ -16,6 +18,12 @@ interface ContextPanelProps {
|
|
|
16
18
|
isActive: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
interface DriveEntry {
|
|
22
|
+
type: "drive";
|
|
23
|
+
drive: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
interface DirEntry {
|
|
20
28
|
type: "directory";
|
|
21
29
|
name: string;
|
|
@@ -27,9 +35,8 @@ interface FileEntry {
|
|
|
27
35
|
item: ContextItem;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
type Entry = DirEntry | FileEntry;
|
|
38
|
+
type Entry = DriveEntry | DirEntry | FileEntry;
|
|
31
39
|
|
|
32
|
-
// Reserve lines for header, search bar, padding, tab bar, status/input bar
|
|
33
40
|
const CHROME_LINES = 8;
|
|
34
41
|
|
|
35
42
|
export const ContextPanel = memo(function ContextPanel({
|
|
@@ -39,6 +46,8 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
39
46
|
const { stdout } = useStdout();
|
|
40
47
|
const termRows = stdout?.rows ?? 24;
|
|
41
48
|
|
|
49
|
+
// currentDrive === null means we're at the "pick a drive" level.
|
|
50
|
+
const [currentDrive, setCurrentDrive] = useState<string | null>(null);
|
|
42
51
|
const [currentPath, setCurrentPath] = useState("/");
|
|
43
52
|
const [entries, setEntries] = useState<Entry[]>([]);
|
|
44
53
|
const [cursor, setCursor] = useState(0);
|
|
@@ -54,7 +63,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
54
63
|
|
|
55
64
|
const visibleRows = Math.max(1, termRows - CHROME_LINES);
|
|
56
65
|
|
|
57
|
-
// Keep cursor in view by adjusting scroll offset
|
|
58
66
|
useEffect(() => {
|
|
59
67
|
if (cursor < scrollOffset) {
|
|
60
68
|
setScrollOffset(cursor);
|
|
@@ -64,10 +72,26 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
64
72
|
}, [cursor, scrollOffset, visibleRows]);
|
|
65
73
|
|
|
66
74
|
const loadEntries = useCallback(
|
|
67
|
-
async (path: string) => {
|
|
75
|
+
async (drive: string | null, path: string) => {
|
|
76
|
+
if (drive === null) {
|
|
77
|
+
const summaries = await withDb(dbPath, (conn) =>
|
|
78
|
+
listDriveSummaries(conn),
|
|
79
|
+
);
|
|
80
|
+
const driveEntries: DriveEntry[] = summaries.map((s) => ({
|
|
81
|
+
type: "drive",
|
|
82
|
+
drive: s.drive,
|
|
83
|
+
count: s.count,
|
|
84
|
+
}));
|
|
85
|
+
setEntries(driveEntries);
|
|
86
|
+
setCursor(0);
|
|
87
|
+
setScrollOffset(0);
|
|
88
|
+
setPreview(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
const [dirs, files] = await withDb(dbPath, async (conn) => [
|
|
69
|
-
await getDistinctDirectories(conn, path),
|
|
70
|
-
await listContextItemsByPrefix(conn, path, { recursive: false }),
|
|
93
|
+
await getDistinctDirectories(conn, drive, path),
|
|
94
|
+
await listContextItemsByPrefix(conn, drive, path, { recursive: false }),
|
|
71
95
|
]);
|
|
72
96
|
|
|
73
97
|
const dirEntries: DirEntry[] = dirs.map((d) => ({
|
|
@@ -77,7 +101,7 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
77
101
|
}));
|
|
78
102
|
|
|
79
103
|
const fileEntries: FileEntry[] = files
|
|
80
|
-
.filter((f) => !dirs.some((d) => f.
|
|
104
|
+
.filter((f) => !dirs.some((d) => f.path.startsWith(`${d}/`)))
|
|
81
105
|
.map((f) => ({ type: "file", item: f }));
|
|
82
106
|
|
|
83
107
|
setEntries([...dirEntries, ...fileEntries]);
|
|
@@ -90,9 +114,9 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
90
114
|
|
|
91
115
|
useEffect(() => {
|
|
92
116
|
if (searchResults === null) {
|
|
93
|
-
loadEntries(currentPath);
|
|
117
|
+
loadEntries(currentDrive, currentPath);
|
|
94
118
|
}
|
|
95
|
-
}, [currentPath, loadEntries, searchResults]);
|
|
119
|
+
}, [currentDrive, currentPath, loadEntries, searchResults]);
|
|
96
120
|
|
|
97
121
|
const executeSearch = useCallback(
|
|
98
122
|
async (query: string) => {
|
|
@@ -111,7 +135,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
111
135
|
[dbPath],
|
|
112
136
|
);
|
|
113
137
|
|
|
114
|
-
// Compute the items list and visible window for the current view
|
|
115
138
|
const items = searchResults ?? entries;
|
|
116
139
|
const itemCount = items.length;
|
|
117
140
|
const visibleItems = useMemo(
|
|
@@ -119,9 +142,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
119
142
|
[items, scrollOffset, visibleRows],
|
|
120
143
|
);
|
|
121
144
|
|
|
122
|
-
// Preview content split into lines for scrolling. Markdown files are
|
|
123
|
-
// rendered through Bun.markdown.ansi so headers/emphasis/code display
|
|
124
|
-
// with ANSI formatting in the terminal.
|
|
125
145
|
const previewLines = useMemo(() => {
|
|
126
146
|
if (!preview?.content) return [];
|
|
127
147
|
const body = isMarkdownItem(preview)
|
|
@@ -132,7 +152,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
132
152
|
|
|
133
153
|
useInput(
|
|
134
154
|
(input, key) => {
|
|
135
|
-
// Search mode: capture text input
|
|
136
155
|
if (searchMode) {
|
|
137
156
|
if (key.return) {
|
|
138
157
|
setSearchMode(false);
|
|
@@ -155,7 +174,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
155
174
|
return;
|
|
156
175
|
}
|
|
157
176
|
|
|
158
|
-
// Preview mode: scroll content
|
|
159
177
|
if (preview) {
|
|
160
178
|
if (key.upArrow) {
|
|
161
179
|
setPreviewScroll((s) => Math.max(0, s - 1));
|
|
@@ -174,20 +192,23 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
174
192
|
return;
|
|
175
193
|
}
|
|
176
194
|
|
|
177
|
-
// Delete confirmation mode
|
|
178
195
|
if (confirmDelete) {
|
|
179
196
|
if (input === "y" || input === "d") {
|
|
180
197
|
const entry = entries[cursor];
|
|
181
198
|
if (entry) {
|
|
182
199
|
void withDb(dbPath, async (conn) => {
|
|
183
|
-
if (entry.type === "directory") {
|
|
184
|
-
await deleteContextItemsByPrefix(
|
|
185
|
-
|
|
200
|
+
if (entry.type === "directory" && currentDrive) {
|
|
201
|
+
await deleteContextItemsByPrefix(
|
|
202
|
+
conn,
|
|
203
|
+
currentDrive,
|
|
204
|
+
entry.path,
|
|
205
|
+
);
|
|
206
|
+
} else if (entry.type === "file") {
|
|
186
207
|
await deleteContextItem(conn, entry.item.id);
|
|
187
208
|
}
|
|
188
209
|
});
|
|
189
210
|
setConfirmDelete(false);
|
|
190
|
-
loadEntries(currentPath);
|
|
211
|
+
loadEntries(currentDrive, currentPath);
|
|
191
212
|
}
|
|
192
213
|
} else {
|
|
193
214
|
setConfirmDelete(false);
|
|
@@ -195,7 +216,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
195
216
|
return;
|
|
196
217
|
}
|
|
197
218
|
|
|
198
|
-
// Normal navigation
|
|
199
219
|
if (input === "d" && itemCount > 0 && searchResults === null) {
|
|
200
220
|
setConfirmDelete(true);
|
|
201
221
|
return;
|
|
@@ -236,7 +256,10 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
236
256
|
}
|
|
237
257
|
const entry = entries[cursor];
|
|
238
258
|
if (!entry) return;
|
|
239
|
-
if (entry.type === "
|
|
259
|
+
if (entry.type === "drive") {
|
|
260
|
+
setCurrentDrive(entry.drive);
|
|
261
|
+
setCurrentPath("/");
|
|
262
|
+
} else if (entry.type === "directory") {
|
|
240
263
|
setCurrentPath(entry.path);
|
|
241
264
|
} else {
|
|
242
265
|
setPreview(entry.item);
|
|
@@ -251,13 +274,14 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
251
274
|
parts.pop();
|
|
252
275
|
const parent = parts.length <= 1 ? "/" : `${parts.join("/")}/`;
|
|
253
276
|
setCurrentPath(parent);
|
|
277
|
+
} else if (currentDrive !== null) {
|
|
278
|
+
setCurrentDrive(null);
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
},
|
|
257
282
|
{ isActive },
|
|
258
283
|
);
|
|
259
284
|
|
|
260
|
-
// Render search results view
|
|
261
285
|
if (searchResults !== null && !preview) {
|
|
262
286
|
return (
|
|
263
287
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
@@ -272,17 +296,14 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
272
296
|
{visibleItems.map((item, vi) => {
|
|
273
297
|
const i = vi + scrollOffset;
|
|
274
298
|
const ci = item as ContextItem;
|
|
275
|
-
const
|
|
276
|
-
const dir =
|
|
277
|
-
slashIdx >= 0 ? ci.context_path.slice(0, slashIdx + 1) : "";
|
|
299
|
+
const ref = formatDriveRef(ci);
|
|
278
300
|
return (
|
|
279
301
|
<Box key={ci.id}>
|
|
280
302
|
<Text
|
|
281
303
|
backgroundColor={i === cursor ? "#333" : undefined}
|
|
282
304
|
color={i === cursor ? "cyan" : undefined}
|
|
283
305
|
>
|
|
284
|
-
{" "}📄 <Text dimColor>{
|
|
285
|
-
{ci.title}
|
|
306
|
+
{" "}📄 <Text dimColor>{ref}</Text> — {ci.title}
|
|
286
307
|
<Text dimColor> ({ci.mime_type})</Text>
|
|
287
308
|
</Text>
|
|
288
309
|
</Box>
|
|
@@ -301,7 +322,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
301
322
|
);
|
|
302
323
|
}
|
|
303
324
|
|
|
304
|
-
// Render file preview with scrolling
|
|
305
325
|
if (preview) {
|
|
306
326
|
const visiblePreviewLines = previewLines.slice(
|
|
307
327
|
previewScroll,
|
|
@@ -311,7 +331,7 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
311
331
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
312
332
|
<Box>
|
|
313
333
|
<Text bold color="cyan">
|
|
314
|
-
{preview
|
|
334
|
+
{formatDriveRef(preview)}
|
|
315
335
|
</Text>
|
|
316
336
|
<Text dimColor> (esc to go back · ↑↓ to scroll)</Text>
|
|
317
337
|
</Box>
|
|
@@ -321,7 +341,6 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
321
341
|
{preview.description ? ` · ${preview.description}` : ""}
|
|
322
342
|
</Text>
|
|
323
343
|
<Text dimColor>
|
|
324
|
-
Source: {preview.source_path ?? "n/a"} ·{" "}
|
|
325
344
|
{preview.indexed_at ? "Indexed" : "Not indexed"} · Updated:{" "}
|
|
326
345
|
{preview.updated_at.toLocaleDateString()}
|
|
327
346
|
</Text>
|
|
@@ -354,12 +373,16 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
354
373
|
);
|
|
355
374
|
}
|
|
356
375
|
|
|
357
|
-
|
|
376
|
+
const headerLabel =
|
|
377
|
+
currentDrive === null
|
|
378
|
+
? "(drives)"
|
|
379
|
+
: formatDriveRef({ drive: currentDrive, path: currentPath });
|
|
380
|
+
|
|
358
381
|
return (
|
|
359
382
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
360
383
|
<Box>
|
|
361
384
|
<Text bold color="cyan">
|
|
362
|
-
{
|
|
385
|
+
{headerLabel}
|
|
363
386
|
</Text>
|
|
364
387
|
<Text dimColor>
|
|
365
388
|
{" "}
|
|
@@ -378,8 +401,10 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
378
401
|
<Text color="red" bold>
|
|
379
402
|
Delete{" "}
|
|
380
403
|
{entries[cursor].type === "directory"
|
|
381
|
-
? `${entries[cursor].name}/ and all contents`
|
|
382
|
-
:
|
|
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)"}
|
|
383
408
|
? (y/n)
|
|
384
409
|
</Text>
|
|
385
410
|
</Box>
|
|
@@ -390,6 +415,19 @@ export const ContextPanel = memo(function ContextPanel({
|
|
|
390
415
|
const i = vi + scrollOffset;
|
|
391
416
|
const entry = raw as Entry;
|
|
392
417
|
const isSelected = i === cursor;
|
|
418
|
+
if (entry.type === "drive") {
|
|
419
|
+
return (
|
|
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
|
+
}
|
|
393
431
|
if (entry.type === "directory") {
|
|
394
432
|
return (
|
|
395
433
|
<Box key={entry.path}>
|
package/src/tui/markdown.ts
CHANGED
|
@@ -6,10 +6,9 @@ export function renderMarkdown(text: string): string {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function isMarkdownItem(
|
|
9
|
-
item: Pick<ContextItem, "mime_type" | "
|
|
9
|
+
item: Pick<ContextItem, "mime_type" | "path">,
|
|
10
10
|
): boolean {
|
|
11
11
|
if (item.mime_type === "text/markdown") return true;
|
|
12
|
-
if (item.
|
|
13
|
-
if (item.context_path.toLowerCase().endsWith(".md")) return true;
|
|
12
|
+
if (item.path.toLowerCase().endsWith(".md")) return true;
|
|
14
13
|
return false;
|
|
15
14
|
}
|
package/src/worker/prompt.ts
CHANGED
|
@@ -107,8 +107,9 @@ export async function buildSystemPrompt(
|
|
|
107
107
|
if (results.length > 0) {
|
|
108
108
|
prompt += "## Relevant Context\n";
|
|
109
109
|
for (const r of results) {
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const ref =
|
|
111
|
+
r.drive && r.path ? `${r.drive}:${r.path}` : r.context_item_id;
|
|
112
|
+
prompt += `### ${r.title} (${ref})\n`;
|
|
112
113
|
if (r.chunk_content) {
|
|
113
114
|
prompt += `${r.chunk_content.slice(0, 1000)}\n`;
|
|
114
115
|
}
|
package/src/tools/dir/list.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
getDistinctDirectories,
|
|
4
|
-
listContextItemsByPrefix,
|
|
5
|
-
} from "../../db/context.ts";
|
|
6
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
7
|
-
|
|
8
|
-
const DirEntrySchema = z.object({
|
|
9
|
-
name: z.string(),
|
|
10
|
-
type: z.enum(["file", "directory"]),
|
|
11
|
-
size: z.number(),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const inputSchema = z.object({
|
|
15
|
-
path: z.string().optional().describe("Directory path (defaults to /)"),
|
|
16
|
-
recursive: z
|
|
17
|
-
.boolean()
|
|
18
|
-
.optional()
|
|
19
|
-
.default(true)
|
|
20
|
-
.describe("Include contents of subdirectories (defaults to true)"),
|
|
21
|
-
limit: z
|
|
22
|
-
.number()
|
|
23
|
-
.optional()
|
|
24
|
-
.default(100)
|
|
25
|
-
.describe("Maximum number of entries to return (defaults to 100)"),
|
|
26
|
-
offset: z
|
|
27
|
-
.number()
|
|
28
|
-
.optional()
|
|
29
|
-
.default(0)
|
|
30
|
-
.describe("Number of entries to skip (defaults to 0)"),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const outputSchema = z.object({
|
|
34
|
-
entries: z.array(DirEntrySchema),
|
|
35
|
-
total: z.number(),
|
|
36
|
-
is_error: z.boolean(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export const contextListDirTool = {
|
|
40
|
-
name: "context_list_dir",
|
|
41
|
-
description:
|
|
42
|
-
"[[ bash equivalent command: ls ]] List directory contents in context.",
|
|
43
|
-
group: "context",
|
|
44
|
-
inputSchema,
|
|
45
|
-
outputSchema,
|
|
46
|
-
execute: async (input, ctx) => {
|
|
47
|
-
const path = input.path ?? "/";
|
|
48
|
-
const recursive = input.recursive ?? true;
|
|
49
|
-
const limit = input.limit ?? 100;
|
|
50
|
-
const offset = input.offset ?? 0;
|
|
51
|
-
const normalizedPath = path.endsWith("/") ? path : `${path}/`;
|
|
52
|
-
|
|
53
|
-
const allItems = await listContextItemsByPrefix(ctx.conn, path, {
|
|
54
|
-
recursive,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const entries: z.infer<typeof DirEntrySchema>[] = allItems.map((item) => ({
|
|
58
|
-
name: recursive
|
|
59
|
-
? item.context_path
|
|
60
|
-
: item.context_path.slice(normalizedPath.length),
|
|
61
|
-
type:
|
|
62
|
-
item.mime_type === "inode/directory"
|
|
63
|
-
? ("directory" as const)
|
|
64
|
-
: ("file" as const),
|
|
65
|
-
size: item.content?.length ?? 0,
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
|
-
// Add subdirectories (if not recursive, show immediate child dirs)
|
|
69
|
-
if (!recursive) {
|
|
70
|
-
const dirs = await getDistinctDirectories(ctx.conn, path);
|
|
71
|
-
for (const dir of dirs) {
|
|
72
|
-
const name = dir.slice(normalizedPath.length);
|
|
73
|
-
if (!entries.some((e) => e.name === name)) {
|
|
74
|
-
entries.push({ name, type: "directory", size: 0 });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
entries.sort((a, b) => {
|
|
80
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
81
|
-
return a.name.localeCompare(b.name);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const total = entries.length;
|
|
85
|
-
const paginated = entries.slice(offset, offset + limit);
|
|
86
|
-
|
|
87
|
-
return { entries: paginated, total, is_error: false };
|
|
88
|
-
},
|
|
89
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|