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,409 @@
|
|
|
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
|
+
deleteTask,
|
|
6
|
+
listTasks,
|
|
7
|
+
TASK_PRIORITIES,
|
|
8
|
+
TASK_STATUSES,
|
|
9
|
+
type Task,
|
|
10
|
+
} from "../../db/tasks.ts";
|
|
11
|
+
import { ansi, theme } from "../theme.ts";
|
|
12
|
+
|
|
13
|
+
interface TaskPanelProps {
|
|
14
|
+
conn: DbConnection;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SIDEBAR_WIDTH = 42;
|
|
19
|
+
const PAGE_SCROLL_LINES = 10;
|
|
20
|
+
|
|
21
|
+
const STATUS_ICONS: Record<Task["status"], string> = {
|
|
22
|
+
pending: "○",
|
|
23
|
+
in_progress: "●",
|
|
24
|
+
waiting: "◌",
|
|
25
|
+
failed: "✖",
|
|
26
|
+
complete: "✔",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const STATUS_COLORS: Record<Task["status"], string> = {
|
|
30
|
+
pending: theme.muted,
|
|
31
|
+
in_progress: theme.accent,
|
|
32
|
+
waiting: theme.info,
|
|
33
|
+
failed: theme.error,
|
|
34
|
+
complete: theme.success,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const STATUS_ANSI: Record<Task["status"], string> = {
|
|
38
|
+
pending: ansi.muted,
|
|
39
|
+
in_progress: ansi.accent,
|
|
40
|
+
waiting: ansi.info,
|
|
41
|
+
failed: ansi.error,
|
|
42
|
+
complete: ansi.success,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PRIORITY_LABELS: Record<Task["priority"], string> = {
|
|
46
|
+
high: "HI",
|
|
47
|
+
medium: "MD",
|
|
48
|
+
low: "LO",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const PRIORITY_COLORS: Record<Task["priority"], string> = {
|
|
52
|
+
high: theme.error,
|
|
53
|
+
medium: theme.accent,
|
|
54
|
+
low: theme.muted,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const PRIORITY_ANSI: Record<Task["priority"], string> = {
|
|
58
|
+
high: ansi.error,
|
|
59
|
+
medium: ansi.accent,
|
|
60
|
+
low: ansi.muted,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function buildTaskDetailAnsi(task: Task): string {
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
|
|
66
|
+
lines.push(`${ansi.bold}${ansi.info}${task.name}${ansi.reset}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
|
|
69
|
+
const statusAnsi = STATUS_ANSI[task.status];
|
|
70
|
+
lines.push(
|
|
71
|
+
`${ansi.bold}${ansi.primary}Status${ansi.reset} ${statusAnsi}${STATUS_ICONS[task.status]} ${task.status}${ansi.reset}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const priorityAnsi = PRIORITY_ANSI[task.priority];
|
|
75
|
+
lines.push(
|
|
76
|
+
`${ansi.bold}${ansi.primary}Priority${ansi.reset} ${priorityAnsi}${task.priority}${ansi.reset}`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
lines.push(
|
|
80
|
+
`${ansi.bold}${ansi.primary}Claimed${ansi.reset} ${task.claimed_by ? task.claimed_by : `${ansi.dim}(unclaimed)${ansi.reset}`}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const created = task.created_at.toLocaleString([], {
|
|
84
|
+
month: "short",
|
|
85
|
+
day: "numeric",
|
|
86
|
+
hour: "2-digit",
|
|
87
|
+
minute: "2-digit",
|
|
88
|
+
});
|
|
89
|
+
const updated = task.updated_at.toLocaleString([], {
|
|
90
|
+
month: "short",
|
|
91
|
+
day: "numeric",
|
|
92
|
+
hour: "2-digit",
|
|
93
|
+
minute: "2-digit",
|
|
94
|
+
});
|
|
95
|
+
lines.push(
|
|
96
|
+
`${ansi.bold}${ansi.primary}Created${ansi.reset} ${ansi.dim}${created}${ansi.reset}`,
|
|
97
|
+
);
|
|
98
|
+
lines.push(
|
|
99
|
+
`${ansi.bold}${ansi.primary}Updated${ansi.reset} ${ansi.dim}${updated}${ansi.reset}`,
|
|
100
|
+
);
|
|
101
|
+
lines.push("");
|
|
102
|
+
|
|
103
|
+
if (task.description) {
|
|
104
|
+
lines.push(`${ansi.bold}${ansi.primary}Description${ansi.reset}`);
|
|
105
|
+
lines.push(task.description);
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (task.status === "waiting" && task.waiting_reason) {
|
|
110
|
+
lines.push(`${ansi.bold}${ansi.primary}Waiting Reason${ansi.reset}`);
|
|
111
|
+
lines.push(`${ansi.accent}${task.waiting_reason}${ansi.reset}`);
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (task.blocked_by.length > 0) {
|
|
116
|
+
lines.push(`${ansi.bold}${ansi.primary}Blocked By${ansi.reset}`);
|
|
117
|
+
for (const id of task.blocked_by) {
|
|
118
|
+
lines.push(` ${ansi.dim}• ${id}${ansi.reset}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (task.context_ids.length > 0) {
|
|
124
|
+
lines.push(`${ansi.bold}${ansi.primary}Context IDs${ansi.reset}`);
|
|
125
|
+
for (const id of task.context_ids) {
|
|
126
|
+
lines.push(` ${ansi.dim}• ${id}${ansi.reset}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Cycle through filter options: null -> ...values -> null
|
|
134
|
+
function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
|
|
135
|
+
if (current === null) return values[0] ?? null;
|
|
136
|
+
const idx = values.indexOf(current);
|
|
137
|
+
if (idx === -1 || idx === values.length - 1) return null;
|
|
138
|
+
return values[idx + 1] ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const TaskPanel = memo(function TaskPanel({
|
|
142
|
+
conn,
|
|
143
|
+
isActive,
|
|
144
|
+
}: TaskPanelProps) {
|
|
145
|
+
const { stdout } = useStdout();
|
|
146
|
+
const termRows = stdout?.rows ?? 24;
|
|
147
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
148
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
149
|
+
const [detailScroll, setDetailScroll] = useState(0);
|
|
150
|
+
const [statusFilter, setStatusFilter] = useState<Task["status"] | null>(null);
|
|
151
|
+
const [priorityFilter, setPriorityFilter] = useState<Task["priority"] | null>(
|
|
152
|
+
null,
|
|
153
|
+
);
|
|
154
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
155
|
+
|
|
156
|
+
// Fetch tasks on mount, filter change, and every 5 seconds
|
|
157
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshTick triggers manual refresh
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
let mounted = true;
|
|
160
|
+
|
|
161
|
+
const refresh = async () => {
|
|
162
|
+
const filters: {
|
|
163
|
+
status?: Task["status"];
|
|
164
|
+
priority?: Task["priority"];
|
|
165
|
+
} = {};
|
|
166
|
+
if (statusFilter) filters.status = statusFilter;
|
|
167
|
+
if (priorityFilter) filters.priority = priorityFilter;
|
|
168
|
+
const result = await listTasks(conn, filters);
|
|
169
|
+
if (mounted) {
|
|
170
|
+
setTasks(result);
|
|
171
|
+
setSelectedIndex((prev) =>
|
|
172
|
+
Math.min(prev, Math.max(0, result.length - 1)),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
refresh();
|
|
178
|
+
const interval = setInterval(refresh, 5000);
|
|
179
|
+
return () => {
|
|
180
|
+
mounted = false;
|
|
181
|
+
clearInterval(interval);
|
|
182
|
+
};
|
|
183
|
+
}, [conn, statusFilter, priorityFilter, refreshTick]);
|
|
184
|
+
|
|
185
|
+
const selectedTask = tasks[selectedIndex];
|
|
186
|
+
|
|
187
|
+
const renderedDetail = useMemo(() => {
|
|
188
|
+
if (!selectedTask) return "";
|
|
189
|
+
return buildTaskDetailAnsi(selectedTask);
|
|
190
|
+
}, [selectedTask]);
|
|
191
|
+
|
|
192
|
+
const detailLines = useMemo(
|
|
193
|
+
() => renderedDetail.split("\n"),
|
|
194
|
+
[renderedDetail],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const visibleRows = Math.max(1, termRows - 6);
|
|
198
|
+
const maxDetailScroll = Math.max(0, detailLines.length - visibleRows);
|
|
199
|
+
const sidebarScrollOffset = Math.max(
|
|
200
|
+
0,
|
|
201
|
+
Math.min(
|
|
202
|
+
selectedIndex - Math.floor(visibleRows / 2),
|
|
203
|
+
tasks.length - visibleRows,
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Reset detail scroll when selection changes
|
|
208
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: selectedIndex is the intentional trigger
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setDetailScroll(0);
|
|
211
|
+
}, [selectedIndex]);
|
|
212
|
+
|
|
213
|
+
const forceRefresh = useCallback(() => {
|
|
214
|
+
setRefreshTick((t) => t + 1);
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
useInput(
|
|
218
|
+
(input, key) => {
|
|
219
|
+
if (key.upArrow) {
|
|
220
|
+
if (key.shift) {
|
|
221
|
+
setDetailScroll((s) => Math.max(0, s - 1));
|
|
222
|
+
} else {
|
|
223
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (key.downArrow) {
|
|
228
|
+
if (key.shift) {
|
|
229
|
+
setDetailScroll((s) => Math.min(maxDetailScroll, s + 1));
|
|
230
|
+
} else {
|
|
231
|
+
setSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (input === "j") {
|
|
237
|
+
setDetailScroll((s) => Math.min(maxDetailScroll, s + 1));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (input === "k") {
|
|
241
|
+
setDetailScroll((s) => Math.max(0, s - 1));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (input === "J") {
|
|
245
|
+
setDetailScroll((s) =>
|
|
246
|
+
Math.min(maxDetailScroll, s + PAGE_SCROLL_LINES),
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (input === "K") {
|
|
251
|
+
setDetailScroll((s) => Math.max(0, s - PAGE_SCROLL_LINES));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (input === "g") {
|
|
255
|
+
setDetailScroll(0);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (input === "G") {
|
|
259
|
+
setDetailScroll(maxDetailScroll);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (input === "f") {
|
|
264
|
+
setStatusFilter((f) => cycleFilter(f, TASK_STATUSES));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (input === "p") {
|
|
268
|
+
setPriorityFilter((f) => cycleFilter(f, TASK_PRIORITIES));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (input === "d" && selectedTask) {
|
|
272
|
+
deleteTask(conn, selectedTask.id).then(() => {
|
|
273
|
+
forceRefresh();
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (input === "r") {
|
|
278
|
+
forceRefresh();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{ isActive },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (tasks.length === 0) {
|
|
286
|
+
return (
|
|
287
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
288
|
+
<Text dimColor>
|
|
289
|
+
{statusFilter || priorityFilter
|
|
290
|
+
? "No tasks match the current filters. Press f/p to change filters."
|
|
291
|
+
: "No tasks found. Tasks will appear here as they are created."}
|
|
292
|
+
</Text>
|
|
293
|
+
{(statusFilter || priorityFilter) && (
|
|
294
|
+
<Box marginTop={1}>
|
|
295
|
+
{statusFilter && (
|
|
296
|
+
<Text color={theme.info}>status: {statusFilter}</Text>
|
|
297
|
+
)}
|
|
298
|
+
{statusFilter && priorityFilter && <Text dimColor> · </Text>}
|
|
299
|
+
{priorityFilter && (
|
|
300
|
+
<Text color={theme.accent}>priority: {priorityFilter}</Text>
|
|
301
|
+
)}
|
|
302
|
+
</Box>
|
|
303
|
+
)}
|
|
304
|
+
</Box>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sidebarVisible = tasks.slice(
|
|
309
|
+
sidebarScrollOffset,
|
|
310
|
+
sidebarScrollOffset + visibleRows,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const detailVisible = detailLines.slice(
|
|
314
|
+
detailScroll,
|
|
315
|
+
detailScroll + visibleRows,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<Box flexGrow={1} height={visibleRows + 1} overflow="hidden">
|
|
320
|
+
{/* Left sidebar: task list */}
|
|
321
|
+
<Box
|
|
322
|
+
flexDirection="column"
|
|
323
|
+
width={SIDEBAR_WIDTH}
|
|
324
|
+
height={visibleRows + 1}
|
|
325
|
+
borderStyle="single"
|
|
326
|
+
borderColor={theme.muted}
|
|
327
|
+
borderRight
|
|
328
|
+
borderTop={false}
|
|
329
|
+
borderBottom={false}
|
|
330
|
+
borderLeft={false}
|
|
331
|
+
overflow="hidden"
|
|
332
|
+
>
|
|
333
|
+
<Box paddingX={1} gap={1}>
|
|
334
|
+
<Text bold dimColor>
|
|
335
|
+
Tasks ({tasks.length})
|
|
336
|
+
</Text>
|
|
337
|
+
{statusFilter && <Text color={theme.info}>[{statusFilter}]</Text>}
|
|
338
|
+
{priorityFilter && (
|
|
339
|
+
<Text color={theme.accent}>[{priorityFilter}]</Text>
|
|
340
|
+
)}
|
|
341
|
+
</Box>
|
|
342
|
+
{sidebarVisible.map((task, vi) => {
|
|
343
|
+
const i = vi + sidebarScrollOffset;
|
|
344
|
+
const isSelected = i === selectedIndex;
|
|
345
|
+
const icon = STATUS_ICONS[task.status];
|
|
346
|
+
const priorityLabel = PRIORITY_LABELS[task.priority];
|
|
347
|
+
const maxName = SIDEBAR_WIDTH - 11; // icon + priority + padding
|
|
348
|
+
const nameDisplay =
|
|
349
|
+
task.name.length > maxName
|
|
350
|
+
? `${task.name.slice(0, maxName - 1)}…`
|
|
351
|
+
: task.name;
|
|
352
|
+
return (
|
|
353
|
+
<Box key={task.id} paddingX={1}>
|
|
354
|
+
<Text
|
|
355
|
+
backgroundColor={isSelected ? theme.selectionBg : undefined}
|
|
356
|
+
bold={isSelected}
|
|
357
|
+
color={isSelected ? theme.info : undefined}
|
|
358
|
+
wrap="truncate-end"
|
|
359
|
+
>
|
|
360
|
+
{isSelected ? "▸" : " "}{" "}
|
|
361
|
+
<Text color={STATUS_COLORS[task.status]} bold={false}>
|
|
362
|
+
{icon}
|
|
363
|
+
</Text>{" "}
|
|
364
|
+
{nameDisplay}
|
|
365
|
+
<Text
|
|
366
|
+
color={PRIORITY_COLORS[task.priority]}
|
|
367
|
+
dimColor={task.priority === "low"}
|
|
368
|
+
>
|
|
369
|
+
{" "}
|
|
370
|
+
{priorityLabel}
|
|
371
|
+
</Text>
|
|
372
|
+
</Text>
|
|
373
|
+
</Box>
|
|
374
|
+
);
|
|
375
|
+
})}
|
|
376
|
+
</Box>
|
|
377
|
+
|
|
378
|
+
{/* Right detail pane */}
|
|
379
|
+
<Box
|
|
380
|
+
flexDirection="column"
|
|
381
|
+
flexGrow={1}
|
|
382
|
+
height={visibleRows + 1}
|
|
383
|
+
paddingX={1}
|
|
384
|
+
overflow="hidden"
|
|
385
|
+
>
|
|
386
|
+
{detailVisible.map((line, i) => {
|
|
387
|
+
const lineNum = detailScroll + i;
|
|
388
|
+
return <Text key={lineNum}>{line || " "}</Text>;
|
|
389
|
+
})}
|
|
390
|
+
{detailLines.length > visibleRows && (
|
|
391
|
+
<Box>
|
|
392
|
+
<Text dimColor>
|
|
393
|
+
f filter · p priority · ↑↓ select · j/k scroll · d delete · r
|
|
394
|
+
refresh · [{detailScroll + 1}–
|
|
395
|
+
{Math.min(detailScroll + visibleRows, detailLines.length)} of{" "}
|
|
396
|
+
{detailLines.length}]
|
|
397
|
+
</Text>
|
|
398
|
+
</Box>
|
|
399
|
+
)}
|
|
400
|
+
{detailLines.length <= visibleRows && <Box flexGrow={1} />}
|
|
401
|
+
{detailLines.length <= visibleRows && (
|
|
402
|
+
<Text dimColor>
|
|
403
|
+
f filter · p priority · ↑↓ select · d delete · r refresh
|
|
404
|
+
</Text>
|
|
405
|
+
)}
|
|
406
|
+
</Box>
|
|
407
|
+
</Box>
|
|
408
|
+
);
|
|
409
|
+
});
|