botholomew 0.14.2 → 0.15.1
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 +2 -0
- package/package.json +3 -2
- package/src/chat/session.ts +4 -0
- package/src/commands/chat.ts +18 -6
- package/src/config/schemas.ts +2 -0
- package/src/context/fetcher.ts +1 -1
- package/src/tui/App.tsx +110 -86
- package/src/tui/components/ContextPanel.tsx +325 -151
- package/src/tui/components/HelpPanel.tsx +42 -43
- package/src/tui/components/InputBar.tsx +6 -4
- package/src/tui/components/Logo.tsx +15 -5
- package/src/tui/components/SchedulePanel.tsx +98 -97
- package/src/tui/components/Scrollbar.tsx +73 -0
- package/src/tui/components/SleepProgress.tsx +5 -2
- package/src/tui/components/StatusBar.tsx +9 -6
- package/src/tui/components/TabBar.tsx +13 -13
- package/src/tui/components/TaskPanel.tsx +86 -95
- package/src/tui/components/ThreadPanel.tsx +133 -120
- package/src/tui/components/ToolPanel.tsx +84 -85
- package/src/tui/components/WorkerPanel.tsx +77 -77
- package/src/tui/idle.tsx +68 -0
- package/src/tui/listDetailKeys.ts +124 -0
- package/src/tui/useLatestRef.ts +18 -0
- package/src/utils/frontmatter.ts +1 -1
- package/src/worker/prompt.ts +10 -1
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
listThreads,
|
|
10
10
|
type Thread,
|
|
11
11
|
} from "../../threads/store.ts";
|
|
12
|
+
import {
|
|
13
|
+
detailPaneBorderProps,
|
|
14
|
+
type FocusState,
|
|
15
|
+
handleListDetailKey,
|
|
16
|
+
} from "../listDetailKeys.ts";
|
|
12
17
|
import { ansi, theme } from "../theme.ts";
|
|
18
|
+
import { useLatestRef } from "../useLatestRef.ts";
|
|
19
|
+
import { Scrollbar } from "./Scrollbar.tsx";
|
|
13
20
|
|
|
14
21
|
interface ThreadPanelProps {
|
|
15
22
|
projectDir: string;
|
|
@@ -40,11 +47,6 @@ const TYPE_COLORS: Record<Thread["type"], string> = {
|
|
|
40
47
|
chat_session: theme.info,
|
|
41
48
|
};
|
|
42
49
|
|
|
43
|
-
const TYPE_ANSI: Record<Thread["type"], string> = {
|
|
44
|
-
worker_tick: ansi.accent,
|
|
45
|
-
chat_session: ansi.info,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
50
|
const ROLE_ANSI: Record<string, string> = {
|
|
49
51
|
user: ansi.success,
|
|
50
52
|
assistant: ansi.info,
|
|
@@ -76,44 +78,18 @@ function formatDate(d: Date): string {
|
|
|
76
78
|
function buildThreadDetailAnsi(
|
|
77
79
|
thread: Thread,
|
|
78
80
|
interactions: Interaction[],
|
|
79
|
-
|
|
81
|
+
_isActiveThread: boolean,
|
|
80
82
|
): string {
|
|
81
83
|
const lines: string[] = [];
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
`${ansi.bold}${ansi.italic}${ansi.info}${thread.title || "(untitled)"}${ansi.reset}`,
|
|
85
|
-
);
|
|
86
|
-
lines.push("");
|
|
87
|
-
|
|
88
|
-
const typeAnsi = TYPE_ANSI[thread.type];
|
|
89
|
-
lines.push(
|
|
90
|
-
`${ansi.bold}${ansi.primary}Type${ansi.reset} ${typeAnsi}${TYPE_ICONS[thread.type]} ${TYPE_LABELS[thread.type]}${ansi.reset}`,
|
|
91
|
-
);
|
|
92
|
-
|
|
85
|
+
// Body only — title/type/timing live in the panel header.
|
|
93
86
|
if (thread.task_id) {
|
|
94
87
|
lines.push(
|
|
95
88
|
`${ansi.bold}${ansi.primary}Task${ansi.reset} ${ansi.dim}${thread.task_id}${ansi.reset}`,
|
|
96
89
|
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
lines.push(
|
|
100
|
-
`${ansi.bold}${ansi.primary}Started${ansi.reset} ${ansi.dim}${formatDate(thread.started_at)}${ansi.reset}`,
|
|
101
|
-
);
|
|
102
|
-
lines.push(
|
|
103
|
-
`${ansi.bold}${ansi.primary}Ended${ansi.reset} ${thread.ended_at ? `${ansi.dim}${formatDate(thread.ended_at)}${ansi.reset}` : `${ansi.success}ongoing${ansi.reset}`}`,
|
|
104
|
-
);
|
|
105
|
-
lines.push(
|
|
106
|
-
`${ansi.bold}${ansi.primary}Duration${ansi.reset} ${ansi.dim}${formatDuration(thread.started_at, thread.ended_at)}${ansi.reset}`,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
if (isActiveThread) {
|
|
110
90
|
lines.push("");
|
|
111
|
-
lines.push(
|
|
112
|
-
`${ansi.bold}${ansi.success}★ Current session thread${ansi.reset}`,
|
|
113
|
-
);
|
|
114
91
|
}
|
|
115
92
|
|
|
116
|
-
lines.push("");
|
|
117
93
|
lines.push(
|
|
118
94
|
`${ansi.bold}${ansi.primary}Interactions${ansi.reset} ${interactions.length} total`,
|
|
119
95
|
);
|
|
@@ -189,6 +165,7 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
189
165
|
const [threads, setThreads] = useState<Thread[]>([]);
|
|
190
166
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
191
167
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
168
|
+
const [focus, setFocus] = useState<FocusState>("list");
|
|
192
169
|
const [typeFilter, setTypeFilter] = useState<Thread["type"] | null>(null);
|
|
193
170
|
const [refreshTick, setRefreshTick] = useState(0);
|
|
194
171
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
@@ -346,10 +323,21 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
346
323
|
setRefreshTick((t) => t + 1);
|
|
347
324
|
}, []);
|
|
348
325
|
|
|
326
|
+
// Mirror state into refs to dodge Ink's stale-closure bug.
|
|
327
|
+
const itemCountRef = useLatestRef(filteredThreads.length);
|
|
328
|
+
const maxDetailScrollRef = useLatestRef(maxDetailScroll);
|
|
329
|
+
const selectedThreadRef = useLatestRef(selectedThread);
|
|
330
|
+
const selectedDetailRef = useLatestRef(selectedDetail);
|
|
331
|
+
const searchingRef = useLatestRef(searching);
|
|
332
|
+
const confirmDeleteRef = useLatestRef(confirmDelete);
|
|
333
|
+
const isActiveSelectedRef = useLatestRef(isActiveSelected);
|
|
334
|
+
const followingRef = useLatestRef(following);
|
|
335
|
+
const focusRef = useLatestRef(focus);
|
|
336
|
+
|
|
349
337
|
useInput(
|
|
350
338
|
(input, key) => {
|
|
351
339
|
// Search mode: capture typed characters
|
|
352
|
-
if (
|
|
340
|
+
if (searchingRef.current) {
|
|
353
341
|
if (key.escape) {
|
|
354
342
|
setSearching(false);
|
|
355
343
|
setSearchQuery("");
|
|
@@ -373,10 +361,11 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
373
361
|
}
|
|
374
362
|
|
|
375
363
|
// Delete confirmation mode
|
|
376
|
-
if (
|
|
364
|
+
if (confirmDeleteRef.current) {
|
|
377
365
|
if (input === "y" || input === "d") {
|
|
378
|
-
|
|
379
|
-
|
|
366
|
+
const t = selectedThreadRef.current;
|
|
367
|
+
if (t && !isActiveSelectedRef.current) {
|
|
368
|
+
deleteThread(projectDir, t.id).then(() => {
|
|
380
369
|
forceRefresh();
|
|
381
370
|
});
|
|
382
371
|
}
|
|
@@ -387,47 +376,17 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
387
376
|
return;
|
|
388
377
|
}
|
|
389
378
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
} else {
|
|
402
|
-
setSelectedIndex((i) => Math.min(filteredThreads.length - 1, i + 1));
|
|
403
|
-
}
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (input === "j") {
|
|
408
|
-
setDetailScroll((s) => Math.min(maxDetailScroll, s + 1));
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (input === "k") {
|
|
412
|
-
setDetailScroll((s) => Math.max(0, s - 1));
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (input === "J") {
|
|
416
|
-
setDetailScroll((s) =>
|
|
417
|
-
Math.min(maxDetailScroll, s + PAGE_SCROLL_LINES),
|
|
418
|
-
);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (input === "K") {
|
|
422
|
-
setDetailScroll((s) => Math.max(0, s - PAGE_SCROLL_LINES));
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
if (input === "g") {
|
|
426
|
-
setDetailScroll(0);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (input === "G") {
|
|
430
|
-
setDetailScroll(maxDetailScroll);
|
|
379
|
+
if (
|
|
380
|
+
handleListDetailKey(input, key, {
|
|
381
|
+
focusRef,
|
|
382
|
+
setFocus,
|
|
383
|
+
itemCountRef,
|
|
384
|
+
maxDetailScrollRef,
|
|
385
|
+
setSelectedIndex,
|
|
386
|
+
setDetailScroll,
|
|
387
|
+
pageScrollLines: PAGE_SCROLL_LINES,
|
|
388
|
+
})
|
|
389
|
+
) {
|
|
431
390
|
return;
|
|
432
391
|
}
|
|
433
392
|
|
|
@@ -435,8 +394,8 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
435
394
|
setTypeFilter((f) => cycleFilter(f, THREAD_TYPES));
|
|
436
395
|
return;
|
|
437
396
|
}
|
|
438
|
-
if (input === "d" &&
|
|
439
|
-
if (
|
|
397
|
+
if (input === "d" && selectedThreadRef.current) {
|
|
398
|
+
if (isActiveSelectedRef.current) return; // Can't delete active thread
|
|
440
399
|
setConfirmDelete(true);
|
|
441
400
|
return;
|
|
442
401
|
}
|
|
@@ -449,14 +408,17 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
449
408
|
setSearchQuery("");
|
|
450
409
|
return;
|
|
451
410
|
}
|
|
452
|
-
if (input === "w"
|
|
453
|
-
|
|
411
|
+
if (input === "w") {
|
|
412
|
+
const t = selectedThreadRef.current;
|
|
413
|
+
if (!t) return;
|
|
414
|
+
if (followingRef.current) {
|
|
454
415
|
setFollowing(false);
|
|
455
|
-
} else if (!
|
|
456
|
-
const maxSeq =
|
|
416
|
+
} else if (!t.ended_at) {
|
|
417
|
+
const maxSeq =
|
|
418
|
+
selectedDetailRef.current?.interactions.at(-1)?.sequence ?? 0;
|
|
457
419
|
lastSeenSequenceRef.current = maxSeq;
|
|
458
420
|
setFollowing(true);
|
|
459
|
-
setDetailScroll(
|
|
421
|
+
setDetailScroll(maxDetailScrollRef.current);
|
|
460
422
|
}
|
|
461
423
|
return;
|
|
462
424
|
}
|
|
@@ -583,46 +545,97 @@ export const ThreadPanel = memo(function ThreadPanel({
|
|
|
583
545
|
flexGrow={1}
|
|
584
546
|
height={visibleRows + 1}
|
|
585
547
|
paddingX={1}
|
|
548
|
+
{...detailPaneBorderProps(focus)}
|
|
586
549
|
overflow="hidden"
|
|
587
550
|
>
|
|
588
|
-
{
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
<Box>
|
|
594
|
-
{following && (
|
|
595
|
-
<Text color={theme.success} bold>
|
|
596
|
-
{" "}
|
|
597
|
-
FOLLOWING{" "}
|
|
598
|
-
</Text>
|
|
599
|
-
)}
|
|
600
|
-
<Text dimColor>
|
|
601
|
-
s search · f filter · ↑↓ select · j/k scroll · d delete ·
|
|
602
|
-
{selectedThread && !selectedThread.ended_at ? " w follow ·" : ""}{" "}
|
|
603
|
-
r refresh · [{detailScroll + 1}–
|
|
604
|
-
{Math.min(detailScroll + visibleRows, detailLines.length)} of{" "}
|
|
605
|
-
{detailLines.length}]
|
|
606
|
-
</Text>
|
|
607
|
-
</Box>
|
|
551
|
+
{selectedThread && (
|
|
552
|
+
<ThreadDetailHeader
|
|
553
|
+
thread={selectedThread}
|
|
554
|
+
isActiveThread={isActiveSelected}
|
|
555
|
+
/>
|
|
608
556
|
)}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
{
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
s search · f filter · ↑↓ select · d delete ·
|
|
620
|
-
{selectedThread && !selectedThread.ended_at ? " w follow ·" : ""}{" "}
|
|
621
|
-
r refresh
|
|
622
|
-
</Text>
|
|
557
|
+
<Box flexDirection="row" flexGrow={1} overflow="hidden">
|
|
558
|
+
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
559
|
+
{detailVisible.map((line, i) => {
|
|
560
|
+
const lineNum = detailScroll + i;
|
|
561
|
+
return (
|
|
562
|
+
<Text key={lineNum} wrap="truncate-end">
|
|
563
|
+
{line || " "}
|
|
564
|
+
</Text>
|
|
565
|
+
);
|
|
566
|
+
})}
|
|
623
567
|
</Box>
|
|
624
|
-
|
|
568
|
+
<Scrollbar
|
|
569
|
+
total={detailLines.length}
|
|
570
|
+
visible={visibleRows - 3}
|
|
571
|
+
offset={detailScroll}
|
|
572
|
+
height={visibleRows - 3}
|
|
573
|
+
focused={focus === "detail"}
|
|
574
|
+
/>
|
|
575
|
+
</Box>
|
|
576
|
+
<Box>
|
|
577
|
+
{following && (
|
|
578
|
+
<Text color={theme.success} bold>
|
|
579
|
+
{" "}
|
|
580
|
+
FOLLOWING{" "}
|
|
581
|
+
</Text>
|
|
582
|
+
)}
|
|
583
|
+
<Text dimColor>
|
|
584
|
+
{focus === "detail"
|
|
585
|
+
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
586
|
+
: `↑↓ select · → enter detail · s search · f filter · d delete${selectedThread && !selectedThread.ended_at ? " · w follow" : ""} · r refresh`}
|
|
587
|
+
</Text>
|
|
588
|
+
</Box>
|
|
625
589
|
</Box>
|
|
626
590
|
</Box>
|
|
627
591
|
);
|
|
628
592
|
});
|
|
593
|
+
|
|
594
|
+
function ThreadDetailHeader({
|
|
595
|
+
thread,
|
|
596
|
+
isActiveThread,
|
|
597
|
+
}: {
|
|
598
|
+
thread: Thread;
|
|
599
|
+
isActiveThread: boolean;
|
|
600
|
+
}) {
|
|
601
|
+
return (
|
|
602
|
+
<Box flexDirection="column">
|
|
603
|
+
<Box>
|
|
604
|
+
<Text wrap="truncate-end">
|
|
605
|
+
<Text bold italic color={theme.info}>
|
|
606
|
+
{thread.title || "(untitled)"}
|
|
607
|
+
</Text>
|
|
608
|
+
{isActiveThread && (
|
|
609
|
+
<Text bold color={theme.success}>
|
|
610
|
+
{" ★"}
|
|
611
|
+
</Text>
|
|
612
|
+
)}
|
|
613
|
+
</Text>
|
|
614
|
+
</Box>
|
|
615
|
+
<Box>
|
|
616
|
+
<Text wrap="truncate-end">
|
|
617
|
+
<Text color={TYPE_COLORS[thread.type]}>
|
|
618
|
+
{TYPE_ICONS[thread.type]} {TYPE_LABELS[thread.type]}
|
|
619
|
+
</Text>
|
|
620
|
+
<Text dimColor>
|
|
621
|
+
{" · started "}
|
|
622
|
+
{formatDate(thread.started_at)}
|
|
623
|
+
{" · "}
|
|
624
|
+
</Text>
|
|
625
|
+
{thread.ended_at ? (
|
|
626
|
+
<Text dimColor>ended {formatDate(thread.ended_at)}</Text>
|
|
627
|
+
) : (
|
|
628
|
+
<Text color={theme.success}>ongoing</Text>
|
|
629
|
+
)}
|
|
630
|
+
<Text dimColor>
|
|
631
|
+
{" · "}
|
|
632
|
+
{formatDuration(thread.started_at, thread.ended_at)}
|
|
633
|
+
</Text>
|
|
634
|
+
</Text>
|
|
635
|
+
</Box>
|
|
636
|
+
<Box>
|
|
637
|
+
<Text dimColor>{"─".repeat(2)}</Text>
|
|
638
|
+
</Box>
|
|
639
|
+
</Box>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
2
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
detailPaneBorderProps,
|
|
5
|
+
type FocusState,
|
|
6
|
+
handleListDetailKey,
|
|
7
|
+
} from "../listDetailKeys.ts";
|
|
3
8
|
import { ansi, theme } from "../theme.ts";
|
|
9
|
+
import { useLatestRef } from "../useLatestRef.ts";
|
|
10
|
+
import { Scrollbar } from "./Scrollbar.tsx";
|
|
4
11
|
import { resolveToolDisplay, type ToolCallData } from "./ToolCall.tsx";
|
|
5
12
|
|
|
6
13
|
interface ToolPanelProps {
|
|
@@ -68,26 +75,9 @@ function colorizeValue(value: unknown, indent: number): string {
|
|
|
68
75
|
function buildDetailAnsi(tool: ToolCallData): string {
|
|
69
76
|
const lines: string[] = [];
|
|
70
77
|
|
|
71
|
-
const
|
|
72
|
-
hour: "2-digit",
|
|
73
|
-
minute: "2-digit",
|
|
74
|
-
second: "2-digit",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const { displayName, displayInput } = resolveToolDisplay(
|
|
78
|
-
tool.name,
|
|
79
|
-
tool.input,
|
|
80
|
-
);
|
|
81
|
-
lines.push(`${ansi.bold}${ansi.info}${displayName}${ansi.reset}`);
|
|
82
|
-
if (tool.name === "mcp_exec") {
|
|
83
|
-
lines.push(`${ansi.dim}via mcp_exec${ansi.reset}`);
|
|
84
|
-
}
|
|
85
|
-
lines.push(`${ansi.dim}Time: ${time}${ansi.reset}`);
|
|
86
|
-
if (tool.running) {
|
|
87
|
-
lines.push(`${ansi.accent}⟳ running${ansi.reset}`);
|
|
88
|
-
}
|
|
89
|
-
lines.push("");
|
|
78
|
+
const { displayInput } = resolveToolDisplay(tool.name, tool.input);
|
|
90
79
|
|
|
80
|
+
// Body only — name/server/status/time live in the panel header now.
|
|
91
81
|
lines.push(`${ansi.bold}${ansi.primary}Input${ansi.reset}`);
|
|
92
82
|
lines.push(colorizeJson(displayInput));
|
|
93
83
|
lines.push("");
|
|
@@ -123,6 +113,7 @@ export const ToolPanel = memo(function ToolPanel({
|
|
|
123
113
|
const termRows = stdout?.rows ?? 24;
|
|
124
114
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
125
115
|
const [detailScroll, setDetailScroll] = useState(0);
|
|
116
|
+
const [focus, setFocus] = useState<FocusState>("list");
|
|
126
117
|
|
|
127
118
|
// Reverse-chronological order (most recent first)
|
|
128
119
|
const reversedCalls = useMemo(() => [...toolCalls].reverse(), [toolCalls]);
|
|
@@ -163,57 +154,21 @@ export const ToolPanel = memo(function ToolPanel({
|
|
|
163
154
|
setDetailScroll(0);
|
|
164
155
|
}, [selectedIndex]);
|
|
165
156
|
|
|
157
|
+
const itemCountRef = useLatestRef(reversedCalls.length);
|
|
158
|
+
const maxDetailScrollRef = useLatestRef(maxDetailScroll);
|
|
159
|
+
const focusRef = useLatestRef(focus);
|
|
160
|
+
|
|
166
161
|
useInput(
|
|
167
162
|
(input, key) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
if (key.downArrow) {
|
|
178
|
-
if (key.shift) {
|
|
179
|
-
setDetailScroll((s) => Math.min(maxDetailScroll, s + 1));
|
|
180
|
-
} else {
|
|
181
|
-
setSelectedIndex((i) => Math.min(reversedCalls.length - 1, i + 1));
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// j/k vim-style for detail scrolling (single line)
|
|
187
|
-
if (input === "j") {
|
|
188
|
-
setDetailScroll((s) => Math.min(maxDetailScroll, s + 1));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (input === "k") {
|
|
192
|
-
setDetailScroll((s) => Math.max(0, s - 1));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// J/K for page scrolling (hold shift or caps)
|
|
197
|
-
if (input === "J") {
|
|
198
|
-
setDetailScroll((s) =>
|
|
199
|
-
Math.min(maxDetailScroll, s + PAGE_SCROLL_LINES),
|
|
200
|
-
);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (input === "K") {
|
|
204
|
-
setDetailScroll((s) => Math.max(0, s - PAGE_SCROLL_LINES));
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// g/G for top/bottom
|
|
209
|
-
if (input === "g") {
|
|
210
|
-
setDetailScroll(0);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (input === "G") {
|
|
214
|
-
setDetailScroll(maxDetailScroll);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
163
|
+
handleListDetailKey(input, key, {
|
|
164
|
+
focusRef,
|
|
165
|
+
setFocus,
|
|
166
|
+
itemCountRef,
|
|
167
|
+
maxDetailScrollRef,
|
|
168
|
+
setSelectedIndex,
|
|
169
|
+
setDetailScroll,
|
|
170
|
+
pageScrollLines: PAGE_SCROLL_LINES,
|
|
171
|
+
});
|
|
217
172
|
},
|
|
218
173
|
{ isActive },
|
|
219
174
|
);
|
|
@@ -316,27 +271,71 @@ export const ToolPanel = memo(function ToolPanel({
|
|
|
316
271
|
flexGrow={1}
|
|
317
272
|
height={visibleRows + 1}
|
|
318
273
|
paddingX={1}
|
|
274
|
+
{...detailPaneBorderProps(focus)}
|
|
319
275
|
overflow="hidden"
|
|
320
276
|
>
|
|
321
|
-
{
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
</Text>
|
|
277
|
+
{selectedTool && <ToolDetailHeader tool={selectedTool} />}
|
|
278
|
+
<Box flexDirection="row" flexGrow={1} overflow="hidden">
|
|
279
|
+
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
280
|
+
{detailVisible.map((line, i) => {
|
|
281
|
+
const lineNum = detailScroll + i;
|
|
282
|
+
return (
|
|
283
|
+
<Text key={lineNum} wrap="truncate-end">
|
|
284
|
+
{line || " "}
|
|
285
|
+
</Text>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
333
288
|
</Box>
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
289
|
+
<Scrollbar
|
|
290
|
+
total={detailLines.length}
|
|
291
|
+
visible={visibleRows - 3}
|
|
292
|
+
offset={detailScroll}
|
|
293
|
+
height={visibleRows - 3}
|
|
294
|
+
focused={focus === "detail"}
|
|
295
|
+
/>
|
|
296
|
+
</Box>
|
|
297
|
+
<Text dimColor>
|
|
298
|
+
{focus === "detail"
|
|
299
|
+
? "↑↓ scroll · ⇧↑↓ page · g/G top/bot · ← back to list"
|
|
300
|
+
: "↑↓ select · → enter detail"}
|
|
301
|
+
</Text>
|
|
339
302
|
</Box>
|
|
340
303
|
</Box>
|
|
341
304
|
);
|
|
342
305
|
});
|
|
306
|
+
|
|
307
|
+
function ToolDetailHeader({ tool }: { tool: ToolCallData }) {
|
|
308
|
+
const { displayName } = resolveToolDisplay(tool.name, tool.input);
|
|
309
|
+
const time = tool.timestamp.toLocaleTimeString([], {
|
|
310
|
+
hour: "2-digit",
|
|
311
|
+
minute: "2-digit",
|
|
312
|
+
second: "2-digit",
|
|
313
|
+
});
|
|
314
|
+
const isMcp = tool.name === "mcp_exec";
|
|
315
|
+
const status = tool.running
|
|
316
|
+
? { color: theme.accent, label: "⟳ running" }
|
|
317
|
+
: tool.isError
|
|
318
|
+
? { color: theme.error, label: "✘ error" }
|
|
319
|
+
: tool.output
|
|
320
|
+
? { color: theme.success, label: "✔ done" }
|
|
321
|
+
: { color: theme.muted, label: "— no output" };
|
|
322
|
+
return (
|
|
323
|
+
<Box flexDirection="column">
|
|
324
|
+
<Box>
|
|
325
|
+
<Text bold color={theme.info} wrap="truncate-end">
|
|
326
|
+
{displayName}
|
|
327
|
+
</Text>
|
|
328
|
+
</Box>
|
|
329
|
+
<Box>
|
|
330
|
+
<Text wrap="truncate-end">
|
|
331
|
+
<Text dimColor>{isMcp ? "mcp_exec · " : ""}</Text>
|
|
332
|
+
<Text color={status.color}>{status.label}</Text>
|
|
333
|
+
<Text dimColor> · {time}</Text>
|
|
334
|
+
</Text>
|
|
335
|
+
</Box>
|
|
336
|
+
<Box>
|
|
337
|
+
<Text dimColor>{"─".repeat(2)}</Text>
|
|
338
|
+
</Box>
|
|
339
|
+
</Box>
|
|
340
|
+
);
|
|
341
|
+
}
|