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.
Files changed (104) hide show
  1. package/README.md +91 -68
  2. package/package.json +3 -3
  3. package/src/chat/agent.ts +42 -82
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +177 -926
  7. package/src/commands/db.ts +9 -13
  8. package/src/commands/init.ts +4 -1
  9. package/src/commands/nuke.ts +57 -90
  10. package/src/commands/schedule.ts +103 -124
  11. package/src/commands/skill.ts +2 -2
  12. package/src/commands/task.ts +86 -95
  13. package/src/commands/thread.ts +107 -112
  14. package/src/commands/worker.ts +88 -88
  15. package/src/constants.ts +93 -16
  16. package/src/context/capabilities.ts +10 -10
  17. package/src/context/fetcher.ts +9 -10
  18. package/src/context/reindex.ts +189 -0
  19. package/src/context/store.ts +630 -0
  20. package/src/db/doctor.ts +1 -8
  21. package/src/db/embeddings.ts +227 -175
  22. package/src/db/sql/19-disk_backed_index.sql +36 -0
  23. package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
  24. package/src/fs/atomic.ts +217 -0
  25. package/src/fs/compat.ts +86 -0
  26. package/src/fs/sandbox.ts +279 -0
  27. package/src/init/index.ts +69 -52
  28. package/src/init/templates.ts +1 -1
  29. package/src/mcpx/client.ts +1 -1
  30. package/src/schedules/schema.ts +19 -0
  31. package/src/schedules/store.ts +296 -0
  32. package/src/skills/commands.ts +1 -3
  33. package/src/tasks/schema.ts +47 -0
  34. package/src/tasks/store.ts +486 -0
  35. package/src/threads/store.ts +559 -0
  36. package/src/tools/capabilities/refresh.ts +42 -21
  37. package/src/tools/context/pipe.ts +15 -71
  38. package/src/tools/context/update-beliefs.ts +3 -3
  39. package/src/tools/context/update-goals.ts +3 -3
  40. package/src/tools/dir/create.ts +26 -23
  41. package/src/tools/dir/size.ts +46 -17
  42. package/src/tools/dir/tree.ts +73 -279
  43. package/src/tools/file/copy.ts +50 -24
  44. package/src/tools/file/count-lines.ts +34 -10
  45. package/src/tools/file/delete.ts +44 -23
  46. package/src/tools/file/edit.ts +39 -14
  47. package/src/tools/file/exists.ts +12 -26
  48. package/src/tools/file/info.ts +25 -85
  49. package/src/tools/file/move.ts +39 -24
  50. package/src/tools/file/read.ts +32 -80
  51. package/src/tools/file/write.ts +14 -91
  52. package/src/tools/registry.ts +3 -7
  53. package/src/tools/schedule/create.ts +2 -2
  54. package/src/tools/schedule/list.ts +7 -3
  55. package/src/tools/search/fuse.ts +12 -33
  56. package/src/tools/search/index.ts +36 -43
  57. package/src/tools/search/regexp.ts +29 -17
  58. package/src/tools/search/semantic.ts +137 -51
  59. package/src/tools/skill/delete.ts +1 -1
  60. package/src/tools/skill/list.ts +1 -1
  61. package/src/tools/skill/write.ts +1 -1
  62. package/src/tools/task/create.ts +41 -16
  63. package/src/tools/task/delete.ts +3 -3
  64. package/src/tools/task/list.ts +6 -3
  65. package/src/tools/task/update.ts +31 -9
  66. package/src/tools/task/view.ts +6 -6
  67. package/src/tools/thread/list.ts +2 -2
  68. package/src/tools/thread/search.ts +208 -0
  69. package/src/tools/thread/view.ts +50 -5
  70. package/src/tools/worker/spawn.ts +28 -14
  71. package/src/tui/App.tsx +12 -19
  72. package/src/tui/components/ContextPanel.tsx +83 -316
  73. package/src/tui/components/SchedulePanel.tsx +34 -48
  74. package/src/tui/components/StatusBar.tsx +15 -15
  75. package/src/tui/components/TaskPanel.tsx +34 -38
  76. package/src/tui/components/ThreadPanel.tsx +29 -38
  77. package/src/tui/components/WorkerPanel.tsx +21 -19
  78. package/src/tui/markdown.ts +2 -8
  79. package/src/types/file-imports.d.ts +9 -0
  80. package/src/utils/title.ts +5 -7
  81. package/src/utils/v7-date.ts +47 -0
  82. package/src/worker/heartbeat.ts +46 -24
  83. package/src/worker/index.ts +13 -15
  84. package/src/worker/llm.ts +30 -37
  85. package/src/worker/prompt.ts +19 -41
  86. package/src/worker/schedules.ts +48 -69
  87. package/src/worker/spawn.ts +11 -11
  88. package/src/worker/tick.ts +39 -43
  89. package/src/workers/store.ts +247 -0
  90. package/src/commands/tools.ts +0 -367
  91. package/src/context/describer.ts +0 -140
  92. package/src/context/drives.ts +0 -110
  93. package/src/context/ingest.ts +0 -162
  94. package/src/context/refresh.ts +0 -183
  95. package/src/db/context.ts +0 -637
  96. package/src/db/daemon-state.ts +0 -6
  97. package/src/db/reembed.ts +0 -113
  98. package/src/db/schedules.ts +0 -213
  99. package/src/db/tasks.ts +0 -347
  100. package/src/db/threads.ts +0 -276
  101. package/src/db/workers.ts +0 -212
  102. package/src/tools/context/list-drives.ts +0 -36
  103. package/src/tools/context/refresh.ts +0 -165
  104. package/src/tools/context/search.ts +0 -54
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 withDb(session.dbPath, (conn) =>
176
- getThread(conn, session.threadId),
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 withDb(session.dbPath, (conn) =>
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 dbPath = sessionRef.current.dbPath;
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 dbPath={dbPath} isActive={activeTab === 3} />
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 dbPath={dbPath} isActive={activeTab === 4} />
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
- dbPath={dbPath}
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 dbPath={dbPath} isActive={activeTab === 6} />
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 dbPath={dbPath} isActive={activeTab === 7} />
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 ContextItem,
7
- deleteContextItem,
8
- deleteContextItemsByPrefix,
9
- getDistinctDirectories,
10
- listContextItemsByPrefix,
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
- dbPath: string;
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
- dbPath,
18
+ projectDir,
44
19
  isActive,
45
20
  }: ContextPanelProps) {
46
21
  const { stdout } = useStdout();
47
22
  const termRows = stdout?.rows ?? 24;
48
23
 
49
- // currentDrive === null means we're at the "pick a drive" level.
50
- const [currentDrive, setCurrentDrive] = useState<string | null>(null);
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<ContextItem | null>(null);
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
- setScrollOffset(cursor);
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 loadEntries = useCallback(
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);
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
- [dbPath],
64
+ [projectDir],
113
65
  );
114
66
 
115
67
  useEffect(() => {
116
- if (searchResults === null) {
117
- loadEntries(currentDrive, currentPath);
118
- }
119
- }, [currentDrive, currentPath, loadEntries, searchResults]);
68
+ refresh(currentPath);
69
+ }, [currentPath, refresh]);
120
70
 
121
- const executeSearch = useCallback(
122
- async (query: string) => {
123
- if (!query.trim()) {
124
- setSearchResults(null);
125
- return;
126
- }
127
- const results = await withDb(dbPath, (conn) =>
128
- searchContextByKeyword(conn, query.trim(), 50),
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 = searchResults ?? entries;
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.type === "drive") {
260
- setCurrentDrive(entry.drive);
261
- setCurrentPath("/");
262
- } else if (entry.type === "directory") {
117
+ if (entry.is_directory) {
263
118
  setCurrentPath(entry.path);
264
- } else {
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
- if (key.backspace || key.delete) {
272
- if (currentPath !== "/") {
273
- const parts = currentPath.replace(/\/$/, "").split("/");
274
- parts.pop();
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: &quot;{searchQuery}&quot;
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
- {formatDriveRef(preview)}
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
- Type: {preview.mime_type} · Title: {preview.title}
341
- {preview.description ? ` · ${preview.description}` : ""}
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
- {preview.content ? (
355
- visiblePreviewLines.map((line, i) => {
356
- const lineNum = previewScroll + i;
357
- return <Text key={lineNum}>{line || " "}</Text>;
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
- currentDrive === null
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} items · / search · d delete · backspace up)
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>No context items found</Text>}
414
- {visibleItems.map((raw, vi) => {
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
- 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
- }
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.item.id}>
205
+ <Box key={entry.path}>
446
206
  <Text
447
207
  backgroundColor={isSelected ? "#333" : undefined}
448
- color={isSelected ? "cyan" : undefined}
208
+ color={
209
+ isSelected ? "cyan" : entry.is_directory ? "blue" : undefined
210
+ }
211
+ bold={isSelected}
449
212
  >
450
- {" "}📄 {entry.item.title}
451
- <Text dimColor> ({entry.item.mime_type})</Text>
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
  );