botholomew 0.3.0 → 0.3.2
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 +9 -0
- package/package.json +3 -1
- package/src/chat/agent.ts +87 -23
- package/src/chat/session.ts +19 -6
- package/src/cli.ts +2 -0
- package/src/commands/chat.ts +5 -2
- package/src/commands/context.ts +91 -35
- package/src/commands/thread.ts +180 -0
- package/src/config/schemas.ts +3 -1
- package/src/context/embedder.ts +0 -3
- package/src/daemon/context.ts +146 -0
- package/src/daemon/large-results.ts +100 -0
- package/src/daemon/llm.ts +45 -19
- package/src/daemon/prompt.ts +1 -6
- package/src/daemon/tick.ts +9 -0
- package/src/db/sql/4-unique_context_path.sql +1 -0
- package/src/db/threads.ts +17 -0
- package/src/init/templates.ts +2 -1
- package/src/tools/context/read-large-result.ts +33 -0
- package/src/tools/context/search.ts +2 -0
- package/src/tools/context/update-beliefs.ts +2 -0
- package/src/tools/context/update-goals.ts +2 -0
- package/src/tools/dir/create.ts +3 -2
- package/src/tools/dir/list.ts +2 -1
- package/src/tools/dir/size.ts +2 -1
- package/src/tools/dir/tree.ts +3 -2
- package/src/tools/file/copy.ts +12 -3
- package/src/tools/file/count-lines.ts +2 -1
- package/src/tools/file/delete.ts +3 -2
- package/src/tools/file/edit.ts +3 -2
- package/src/tools/file/exists.ts +2 -1
- package/src/tools/file/info.ts +2 -0
- package/src/tools/file/move.ts +12 -3
- package/src/tools/file/read.ts +2 -1
- package/src/tools/file/write.ts +5 -4
- package/src/tools/mcp/exec.ts +70 -3
- package/src/tools/mcp/info.ts +8 -0
- package/src/tools/mcp/list-tools.ts +18 -6
- package/src/tools/mcp/search.ts +38 -10
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/search/grep.ts +3 -2
- package/src/tools/search/semantic.ts +2 -0
- package/src/tools/task/complete.ts +2 -0
- package/src/tools/task/create.ts +17 -4
- package/src/tools/task/fail.ts +2 -0
- package/src/tools/task/list.ts +2 -0
- package/src/tools/task/update.ts +87 -0
- package/src/tools/task/view.ts +3 -1
- package/src/tools/task/wait.ts +2 -0
- package/src/tools/thread/list.ts +2 -0
- package/src/tools/thread/view.ts +3 -1
- package/src/tools/tool.ts +7 -3
- package/src/tui/App.tsx +323 -78
- package/src/tui/components/ContextPanel.tsx +415 -0
- package/src/tui/components/Divider.tsx +14 -0
- package/src/tui/components/HelpPanel.tsx +166 -0
- package/src/tui/components/InputBar.tsx +157 -47
- package/src/tui/components/Logo.tsx +79 -0
- package/src/tui/components/MessageList.tsx +50 -23
- package/src/tui/components/QueuePanel.tsx +57 -0
- package/src/tui/components/StatusBar.tsx +21 -9
- package/src/tui/components/TabBar.tsx +40 -0
- package/src/tui/components/TaskPanel.tsx +409 -0
- package/src/tui/components/ThreadPanel.tsx +541 -0
- package/src/tui/components/ToolCall.tsx +68 -5
- package/src/tui/components/ToolPanel.tsx +295 -281
- package/src/tui/theme.ts +75 -0
- package/src/utils/title.ts +47 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import type { DbConnection } from "../../db/connection.ts";
|
|
4
|
+
import {
|
|
5
|
+
type ContextItem,
|
|
6
|
+
deleteContextItem,
|
|
7
|
+
deleteContextItemsByPrefix,
|
|
8
|
+
getDistinctDirectories,
|
|
9
|
+
listContextItemsByPrefix,
|
|
10
|
+
searchContextByKeyword,
|
|
11
|
+
} from "../../db/context.ts";
|
|
12
|
+
|
|
13
|
+
interface ContextPanelProps {
|
|
14
|
+
conn: DbConnection;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DirEntry {
|
|
19
|
+
type: "directory";
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FileEntry {
|
|
25
|
+
type: "file";
|
|
26
|
+
item: ContextItem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Entry = DirEntry | FileEntry;
|
|
30
|
+
|
|
31
|
+
// Reserve lines for header, search bar, padding, tab bar, status/input bar
|
|
32
|
+
const CHROME_LINES = 8;
|
|
33
|
+
|
|
34
|
+
export const ContextPanel = memo(function ContextPanel({
|
|
35
|
+
conn,
|
|
36
|
+
isActive,
|
|
37
|
+
}: ContextPanelProps) {
|
|
38
|
+
const { stdout } = useStdout();
|
|
39
|
+
const termRows = stdout?.rows ?? 24;
|
|
40
|
+
|
|
41
|
+
const [currentPath, setCurrentPath] = useState("/");
|
|
42
|
+
const [entries, setEntries] = useState<Entry[]>([]);
|
|
43
|
+
const [cursor, setCursor] = useState(0);
|
|
44
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
45
|
+
const [preview, setPreview] = useState<ContextItem | null>(null);
|
|
46
|
+
const [previewScroll, setPreviewScroll] = useState(0);
|
|
47
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
48
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
49
|
+
const [searchResults, setSearchResults] = useState<ContextItem[] | null>(
|
|
50
|
+
null,
|
|
51
|
+
);
|
|
52
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
53
|
+
|
|
54
|
+
const visibleRows = Math.max(1, termRows - CHROME_LINES);
|
|
55
|
+
|
|
56
|
+
// Keep cursor in view by adjusting scroll offset
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (cursor < scrollOffset) {
|
|
59
|
+
setScrollOffset(cursor);
|
|
60
|
+
} else if (cursor >= scrollOffset + visibleRows) {
|
|
61
|
+
setScrollOffset(cursor - visibleRows + 1);
|
|
62
|
+
}
|
|
63
|
+
}, [cursor, scrollOffset, visibleRows]);
|
|
64
|
+
|
|
65
|
+
const loadEntries = useCallback(
|
|
66
|
+
async (path: string) => {
|
|
67
|
+
const dirs = await getDistinctDirectories(conn, path);
|
|
68
|
+
const files = await listContextItemsByPrefix(conn, path, {
|
|
69
|
+
recursive: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const dirEntries: DirEntry[] = dirs.map((d) => ({
|
|
73
|
+
type: "directory",
|
|
74
|
+
name: d,
|
|
75
|
+
path: `${d}/`,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const fileEntries: FileEntry[] = files
|
|
79
|
+
.filter((f) => !dirs.some((d) => f.context_path.startsWith(`${d}/`)))
|
|
80
|
+
.map((f) => ({ type: "file", item: f }));
|
|
81
|
+
|
|
82
|
+
setEntries([...dirEntries, ...fileEntries]);
|
|
83
|
+
setCursor(0);
|
|
84
|
+
setScrollOffset(0);
|
|
85
|
+
setPreview(null);
|
|
86
|
+
},
|
|
87
|
+
[conn],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (searchResults === null) {
|
|
92
|
+
loadEntries(currentPath);
|
|
93
|
+
}
|
|
94
|
+
}, [currentPath, loadEntries, searchResults]);
|
|
95
|
+
|
|
96
|
+
const executeSearch = useCallback(
|
|
97
|
+
async (query: string) => {
|
|
98
|
+
if (!query.trim()) {
|
|
99
|
+
setSearchResults(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const results = await searchContextByKeyword(conn, query.trim(), 50);
|
|
103
|
+
setSearchResults(results);
|
|
104
|
+
setCursor(0);
|
|
105
|
+
setScrollOffset(0);
|
|
106
|
+
setPreview(null);
|
|
107
|
+
},
|
|
108
|
+
[conn],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Compute the items list and visible window for the current view
|
|
112
|
+
const items = searchResults ?? entries;
|
|
113
|
+
const itemCount = items.length;
|
|
114
|
+
const visibleItems = useMemo(
|
|
115
|
+
() => items.slice(scrollOffset, scrollOffset + visibleRows),
|
|
116
|
+
[items, scrollOffset, visibleRows],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Preview content split into lines for scrolling
|
|
120
|
+
const previewLines = useMemo(() => {
|
|
121
|
+
if (!preview?.content) return [];
|
|
122
|
+
return preview.content.split("\n");
|
|
123
|
+
}, [preview]);
|
|
124
|
+
|
|
125
|
+
useInput(
|
|
126
|
+
(input, key) => {
|
|
127
|
+
// Search mode: capture text input
|
|
128
|
+
if (searchMode) {
|
|
129
|
+
if (key.return) {
|
|
130
|
+
setSearchMode(false);
|
|
131
|
+
executeSearch(searchQuery);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (key.escape) {
|
|
135
|
+
setSearchMode(false);
|
|
136
|
+
setSearchQuery("");
|
|
137
|
+
setSearchResults(null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (key.backspace || key.delete) {
|
|
141
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (input && !key.ctrl && !key.meta) {
|
|
145
|
+
setSearchQuery((q) => q + input);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Preview mode: scroll content
|
|
151
|
+
if (preview) {
|
|
152
|
+
if (key.upArrow) {
|
|
153
|
+
setPreviewScroll((s) => Math.max(0, s - 1));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (key.downArrow) {
|
|
157
|
+
const maxScroll = Math.max(0, previewLines.length - visibleRows + 2);
|
|
158
|
+
setPreviewScroll((s) => Math.min(maxScroll, s + 1));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (key.escape) {
|
|
162
|
+
setPreview(null);
|
|
163
|
+
setPreviewScroll(0);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Delete confirmation mode
|
|
170
|
+
if (confirmDelete) {
|
|
171
|
+
if (input === "y" || input === "d") {
|
|
172
|
+
const entry = entries[cursor];
|
|
173
|
+
if (entry) {
|
|
174
|
+
if (entry.type === "directory") {
|
|
175
|
+
deleteContextItemsByPrefix(conn, entry.path);
|
|
176
|
+
} else {
|
|
177
|
+
deleteContextItem(conn, entry.item.id);
|
|
178
|
+
}
|
|
179
|
+
setConfirmDelete(false);
|
|
180
|
+
loadEntries(currentPath);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
setConfirmDelete(false);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Normal navigation
|
|
189
|
+
if (input === "d" && itemCount > 0 && searchResults === null) {
|
|
190
|
+
setConfirmDelete(true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (input === "/") {
|
|
195
|
+
setSearchMode(true);
|
|
196
|
+
setSearchQuery("");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (key.escape) {
|
|
201
|
+
if (searchResults !== null) {
|
|
202
|
+
setSearchResults(null);
|
|
203
|
+
setPreview(null);
|
|
204
|
+
setScrollOffset(0);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (key.upArrow) {
|
|
210
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (key.downArrow) {
|
|
214
|
+
setCursor((c) => Math.min(itemCount - 1, c + 1));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (key.return) {
|
|
219
|
+
if (searchResults !== null) {
|
|
220
|
+
const item = searchResults[cursor];
|
|
221
|
+
if (item) {
|
|
222
|
+
setPreview(item);
|
|
223
|
+
setPreviewScroll(0);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const entry = entries[cursor];
|
|
228
|
+
if (!entry) return;
|
|
229
|
+
if (entry.type === "directory") {
|
|
230
|
+
setCurrentPath(entry.path);
|
|
231
|
+
} else {
|
|
232
|
+
setPreview(entry.item);
|
|
233
|
+
setPreviewScroll(0);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (key.backspace || key.delete) {
|
|
239
|
+
if (currentPath !== "/") {
|
|
240
|
+
const parts = currentPath.replace(/\/$/, "").split("/");
|
|
241
|
+
parts.pop();
|
|
242
|
+
const parent = parts.length <= 1 ? "/" : `${parts.join("/")}/`;
|
|
243
|
+
setCurrentPath(parent);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{ isActive },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Render search results view
|
|
251
|
+
if (searchResults !== null && !preview) {
|
|
252
|
+
return (
|
|
253
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
254
|
+
<Box>
|
|
255
|
+
<Text bold color="cyan">
|
|
256
|
+
Search results for: "{searchQuery}"
|
|
257
|
+
</Text>
|
|
258
|
+
<Text dimColor> ({searchResults.length} matches · esc to clear)</Text>
|
|
259
|
+
</Box>
|
|
260
|
+
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
|
261
|
+
{searchResults.length === 0 && <Text dimColor>No results found</Text>}
|
|
262
|
+
{visibleItems.map((item, vi) => {
|
|
263
|
+
const i = vi + scrollOffset;
|
|
264
|
+
const ci = item as ContextItem;
|
|
265
|
+
return (
|
|
266
|
+
<Box key={ci.id}>
|
|
267
|
+
<Text
|
|
268
|
+
backgroundColor={i === cursor ? "#333" : undefined}
|
|
269
|
+
color={i === cursor ? "cyan" : undefined}
|
|
270
|
+
>
|
|
271
|
+
{" "}📄 {ci.context_path}
|
|
272
|
+
<Text dimColor> ({ci.mime_type})</Text>
|
|
273
|
+
</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</Box>
|
|
278
|
+
{itemCount > visibleRows && (
|
|
279
|
+
<Box>
|
|
280
|
+
<Text dimColor>
|
|
281
|
+
[{scrollOffset + 1}–
|
|
282
|
+
{Math.min(scrollOffset + visibleRows, itemCount)} of {itemCount}]
|
|
283
|
+
</Text>
|
|
284
|
+
</Box>
|
|
285
|
+
)}
|
|
286
|
+
</Box>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Render file preview with scrolling
|
|
291
|
+
if (preview) {
|
|
292
|
+
const visiblePreviewLines = previewLines.slice(
|
|
293
|
+
previewScroll,
|
|
294
|
+
previewScroll + visibleRows - 2,
|
|
295
|
+
);
|
|
296
|
+
return (
|
|
297
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
298
|
+
<Box>
|
|
299
|
+
<Text bold color="cyan">
|
|
300
|
+
{preview.context_path}
|
|
301
|
+
</Text>
|
|
302
|
+
<Text dimColor> (esc to go back · ↑↓ to scroll)</Text>
|
|
303
|
+
</Box>
|
|
304
|
+
<Box marginTop={1} flexDirection="column">
|
|
305
|
+
<Text dimColor>
|
|
306
|
+
Type: {preview.mime_type} · Title: {preview.title}
|
|
307
|
+
{preview.description ? ` · ${preview.description}` : ""}
|
|
308
|
+
</Text>
|
|
309
|
+
<Text dimColor>
|
|
310
|
+
Source: {preview.source_path ?? "n/a"} ·{" "}
|
|
311
|
+
{preview.indexed_at ? "Indexed" : "Not indexed"} · Updated:{" "}
|
|
312
|
+
{preview.updated_at.toLocaleDateString()}
|
|
313
|
+
</Text>
|
|
314
|
+
</Box>
|
|
315
|
+
<Box
|
|
316
|
+
marginTop={1}
|
|
317
|
+
flexDirection="column"
|
|
318
|
+
flexGrow={1}
|
|
319
|
+
overflow="hidden"
|
|
320
|
+
>
|
|
321
|
+
{preview.content ? (
|
|
322
|
+
visiblePreviewLines.map((line, i) => {
|
|
323
|
+
const lineNum = previewScroll + i;
|
|
324
|
+
return <Text key={lineNum}>{line || " "}</Text>;
|
|
325
|
+
})
|
|
326
|
+
) : (
|
|
327
|
+
<Text dimColor>(binary or empty content)</Text>
|
|
328
|
+
)}
|
|
329
|
+
</Box>
|
|
330
|
+
{previewLines.length > visibleRows - 2 && (
|
|
331
|
+
<Box>
|
|
332
|
+
<Text dimColor>
|
|
333
|
+
[line {previewScroll + 1}–
|
|
334
|
+
{Math.min(previewScroll + visibleRows - 2, previewLines.length)}{" "}
|
|
335
|
+
of {previewLines.length}]
|
|
336
|
+
</Text>
|
|
337
|
+
</Box>
|
|
338
|
+
)}
|
|
339
|
+
</Box>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Render directory listing with scroll window
|
|
344
|
+
return (
|
|
345
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
346
|
+
<Box>
|
|
347
|
+
<Text bold color="cyan">
|
|
348
|
+
{currentPath}
|
|
349
|
+
</Text>
|
|
350
|
+
<Text dimColor>
|
|
351
|
+
{" "}
|
|
352
|
+
({entries.length} items · / search · d delete · backspace up)
|
|
353
|
+
</Text>
|
|
354
|
+
</Box>
|
|
355
|
+
{searchMode && (
|
|
356
|
+
<Box marginTop={1}>
|
|
357
|
+
<Text color="green">search: </Text>
|
|
358
|
+
<Text>{searchQuery}</Text>
|
|
359
|
+
<Text dimColor>█</Text>
|
|
360
|
+
</Box>
|
|
361
|
+
)}
|
|
362
|
+
{confirmDelete && entries[cursor] && (
|
|
363
|
+
<Box marginTop={1}>
|
|
364
|
+
<Text color="red" bold>
|
|
365
|
+
Delete{" "}
|
|
366
|
+
{entries[cursor].type === "directory"
|
|
367
|
+
? `${entries[cursor].name}/ and all contents`
|
|
368
|
+
: (entries[cursor] as FileEntry).item.title}
|
|
369
|
+
? (y/n)
|
|
370
|
+
</Text>
|
|
371
|
+
</Box>
|
|
372
|
+
)}
|
|
373
|
+
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
|
374
|
+
{entries.length === 0 && <Text dimColor>No context items found</Text>}
|
|
375
|
+
{visibleItems.map((raw, vi) => {
|
|
376
|
+
const i = vi + scrollOffset;
|
|
377
|
+
const entry = raw as Entry;
|
|
378
|
+
const isSelected = i === cursor;
|
|
379
|
+
if (entry.type === "directory") {
|
|
380
|
+
return (
|
|
381
|
+
<Box key={entry.path}>
|
|
382
|
+
<Text
|
|
383
|
+
backgroundColor={isSelected ? "#333" : undefined}
|
|
384
|
+
color={isSelected ? "cyan" : "blue"}
|
|
385
|
+
bold={isSelected}
|
|
386
|
+
>
|
|
387
|
+
{" "}📁 {entry.name}/
|
|
388
|
+
</Text>
|
|
389
|
+
</Box>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return (
|
|
393
|
+
<Box key={entry.item.id}>
|
|
394
|
+
<Text
|
|
395
|
+
backgroundColor={isSelected ? "#333" : undefined}
|
|
396
|
+
color={isSelected ? "cyan" : undefined}
|
|
397
|
+
>
|
|
398
|
+
{" "}📄 {entry.item.title}
|
|
399
|
+
<Text dimColor> ({entry.item.mime_type})</Text>
|
|
400
|
+
</Text>
|
|
401
|
+
</Box>
|
|
402
|
+
);
|
|
403
|
+
})}
|
|
404
|
+
</Box>
|
|
405
|
+
{itemCount > visibleRows && (
|
|
406
|
+
<Box>
|
|
407
|
+
<Text dimColor>
|
|
408
|
+
[{scrollOffset + 1}–
|
|
409
|
+
{Math.min(scrollOffset + visibleRows, itemCount)} of {itemCount}]
|
|
410
|
+
</Text>
|
|
411
|
+
</Box>
|
|
412
|
+
)}
|
|
413
|
+
</Box>
|
|
414
|
+
);
|
|
415
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Text, useStdout } from "ink";
|
|
2
|
+
import { theme } from "../theme.ts";
|
|
3
|
+
|
|
4
|
+
interface DividerProps {
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Divider({ isLoading }: DividerProps) {
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const cols = stdout?.columns ?? 80;
|
|
11
|
+
const line = "─".repeat(cols);
|
|
12
|
+
|
|
13
|
+
return <Text color={isLoading ? theme.accent : theme.muted}>{line}</Text>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
|
|
4
|
+
interface HelpPanelProps {
|
|
5
|
+
projectDir: string;
|
|
6
|
+
threadId: string;
|
|
7
|
+
daemonRunning: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HelpPanel = memo(function HelpPanel({
|
|
11
|
+
projectDir,
|
|
12
|
+
threadId,
|
|
13
|
+
daemonRunning,
|
|
14
|
+
}: HelpPanelProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
17
|
+
<Box marginTop={1} flexDirection="column">
|
|
18
|
+
<Text bold color="cyan">
|
|
19
|
+
Navigation
|
|
20
|
+
</Text>
|
|
21
|
+
<Text>
|
|
22
|
+
{" "}Tab{" "}Cycle between tabs
|
|
23
|
+
</Text>
|
|
24
|
+
<Text>
|
|
25
|
+
{" "}1-6{" "}Jump to tab (non-chat tabs)
|
|
26
|
+
</Text>
|
|
27
|
+
<Text>
|
|
28
|
+
{" "}Escape{" "}Return to Chat tab
|
|
29
|
+
</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
|
|
32
|
+
<Box marginTop={1} flexDirection="column">
|
|
33
|
+
<Text bold color="cyan">
|
|
34
|
+
Chat (Tab 1)
|
|
35
|
+
</Text>
|
|
36
|
+
<Text>
|
|
37
|
+
{" "}Enter{" "}Send message
|
|
38
|
+
</Text>
|
|
39
|
+
<Text>
|
|
40
|
+
{" "}⌥+Enter{" "}Insert newline
|
|
41
|
+
</Text>
|
|
42
|
+
<Text>
|
|
43
|
+
{" "}↑/↓{" "}Browse input history
|
|
44
|
+
</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
|
|
47
|
+
<Box marginTop={1} flexDirection="column">
|
|
48
|
+
<Text bold color="cyan">
|
|
49
|
+
Tools (Tab 2)
|
|
50
|
+
</Text>
|
|
51
|
+
<Text>
|
|
52
|
+
{" "}↑/↓{" "}Select tool call
|
|
53
|
+
</Text>
|
|
54
|
+
<Text>
|
|
55
|
+
{" "}Shift+↑/↓{" "}Scroll detail pane
|
|
56
|
+
</Text>
|
|
57
|
+
<Text>
|
|
58
|
+
{" "}j / k{" "}Scroll detail pane
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
|
|
62
|
+
<Box marginTop={1} flexDirection="column">
|
|
63
|
+
<Text bold color="cyan">
|
|
64
|
+
Context (Tab 3)
|
|
65
|
+
</Text>
|
|
66
|
+
<Text>
|
|
67
|
+
{" "}↑/↓{" "}Navigate items
|
|
68
|
+
</Text>
|
|
69
|
+
<Text>
|
|
70
|
+
{" "}Enter{" "}Expand directory / preview file
|
|
71
|
+
</Text>
|
|
72
|
+
<Text>
|
|
73
|
+
{" "}Backspace{" "}Go up one directory
|
|
74
|
+
</Text>
|
|
75
|
+
<Text>
|
|
76
|
+
{" "}/{" "}Search context
|
|
77
|
+
</Text>
|
|
78
|
+
<Text>
|
|
79
|
+
{" "}d{" "}Delete selected item (with confirmation)
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
|
|
83
|
+
<Box marginTop={1} flexDirection="column">
|
|
84
|
+
<Text bold color="cyan">
|
|
85
|
+
Tasks (Tab 4)
|
|
86
|
+
</Text>
|
|
87
|
+
<Text>
|
|
88
|
+
{" "}↑/↓{" "}Navigate task list
|
|
89
|
+
</Text>
|
|
90
|
+
<Text>
|
|
91
|
+
{" "}Shift+↑/↓{" "}Scroll detail pane
|
|
92
|
+
</Text>
|
|
93
|
+
<Text>
|
|
94
|
+
{" "}j / k{" "}Scroll detail pane
|
|
95
|
+
</Text>
|
|
96
|
+
<Text>
|
|
97
|
+
{" "}f{" "}Cycle status filter
|
|
98
|
+
</Text>
|
|
99
|
+
<Text>
|
|
100
|
+
{" "}p{" "}Cycle priority filter
|
|
101
|
+
</Text>
|
|
102
|
+
<Text>
|
|
103
|
+
{" "}r{" "}Refresh tasks
|
|
104
|
+
</Text>
|
|
105
|
+
</Box>
|
|
106
|
+
|
|
107
|
+
<Box marginTop={1} flexDirection="column">
|
|
108
|
+
<Text bold color="cyan">
|
|
109
|
+
Threads (Tab 5)
|
|
110
|
+
</Text>
|
|
111
|
+
<Text>
|
|
112
|
+
{" "}↑/↓{" "}Navigate thread list
|
|
113
|
+
</Text>
|
|
114
|
+
<Text>
|
|
115
|
+
{" "}Shift+↑/↓{" "}Scroll detail pane
|
|
116
|
+
</Text>
|
|
117
|
+
<Text>
|
|
118
|
+
{" "}j / k{" "}Scroll detail pane
|
|
119
|
+
</Text>
|
|
120
|
+
<Text>
|
|
121
|
+
{" "}f{" "}Cycle type filter
|
|
122
|
+
</Text>
|
|
123
|
+
<Text>
|
|
124
|
+
{" "}d{" "}Delete thread (with confirmation)
|
|
125
|
+
</Text>
|
|
126
|
+
<Text>
|
|
127
|
+
{" "}r{" "}Refresh threads
|
|
128
|
+
</Text>
|
|
129
|
+
</Box>
|
|
130
|
+
|
|
131
|
+
<Box marginTop={1} flexDirection="column">
|
|
132
|
+
<Text bold color="cyan">
|
|
133
|
+
Commands
|
|
134
|
+
</Text>
|
|
135
|
+
<Text>
|
|
136
|
+
{" "}/help{" "}Show help in chat
|
|
137
|
+
</Text>
|
|
138
|
+
<Text>
|
|
139
|
+
{" "}/quit, /exit{" "}End the chat session
|
|
140
|
+
</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
|
|
143
|
+
<Box marginTop={1} flexDirection="column">
|
|
144
|
+
<Text bold color="cyan">
|
|
145
|
+
System Info
|
|
146
|
+
</Text>
|
|
147
|
+
<Text>
|
|
148
|
+
{" "}Project{" "}
|
|
149
|
+
{projectDir}
|
|
150
|
+
</Text>
|
|
151
|
+
<Text>
|
|
152
|
+
{" "}Thread{" "}
|
|
153
|
+
{threadId}
|
|
154
|
+
</Text>
|
|
155
|
+
<Text>
|
|
156
|
+
{" "}Daemon{" "}
|
|
157
|
+
{daemonRunning ? (
|
|
158
|
+
<Text color="green">running</Text>
|
|
159
|
+
) : (
|
|
160
|
+
<Text color="red">off</Text>
|
|
161
|
+
)}
|
|
162
|
+
</Text>
|
|
163
|
+
</Box>
|
|
164
|
+
</Box>
|
|
165
|
+
);
|
|
166
|
+
});
|