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
|
@@ -1,328 +1,342 @@
|
|
|
1
|
-
import { Box, Text, useInput } from "ink";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
import
|
|
1
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
2
|
+
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { ansi, theme } from "../theme.ts";
|
|
4
|
+
import { resolveToolDisplay, type ToolCallData } from "./ToolCall.tsx";
|
|
4
5
|
|
|
5
6
|
interface ToolPanelProps {
|
|
6
7
|
toolCalls: ToolCallData[];
|
|
7
|
-
|
|
8
|
+
isActive: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
interface TreeRow {
|
|
12
|
-
depth: number;
|
|
13
|
-
key: string;
|
|
14
|
-
value: string | null; // null = expandable parent
|
|
15
|
-
path: string;
|
|
16
|
-
hasChildren: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function flattenJson(
|
|
20
|
-
obj: unknown,
|
|
21
|
-
parentPath: string,
|
|
22
|
-
depth: number,
|
|
23
|
-
expanded: Set<string>,
|
|
24
|
-
): TreeRow[] {
|
|
25
|
-
const rows: TreeRow[] = [];
|
|
11
|
+
const SIDEBAR_WIDTH = 42;
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
/** Try to parse a string as JSON; returns the parsed value or undefined on failure */
|
|
14
|
+
function tryParseJson(str: string): unknown | undefined {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(str);
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
29
19
|
}
|
|
20
|
+
}
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
depth,
|
|
49
|
-
key: `[${i}]`,
|
|
50
|
-
value: formatValue(child),
|
|
51
|
-
path,
|
|
52
|
-
hasChildren: false,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} else if (typeof obj === "object") {
|
|
57
|
-
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
58
|
-
const path = parentPath ? `${parentPath}.${k}` : k;
|
|
59
|
-
if (typeof v === "object" && v !== null) {
|
|
60
|
-
const childCount = Array.isArray(v) ? v.length : Object.keys(v).length;
|
|
61
|
-
const preview = Array.isArray(v)
|
|
62
|
-
? `[${childCount} items]`
|
|
63
|
-
: `{${childCount} keys}`;
|
|
64
|
-
rows.push({
|
|
65
|
-
depth,
|
|
66
|
-
key: k,
|
|
67
|
-
value: expanded.has(path) ? null : preview,
|
|
68
|
-
path,
|
|
69
|
-
hasChildren: true,
|
|
70
|
-
});
|
|
71
|
-
if (expanded.has(path)) {
|
|
72
|
-
rows.push(...flattenJson(v, path, depth + 1, expanded));
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
rows.push({
|
|
76
|
-
depth,
|
|
77
|
-
key: k,
|
|
78
|
-
value: formatValue(v),
|
|
79
|
-
path,
|
|
80
|
-
hasChildren: false,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
22
|
+
/** Colorize a JSON string with ANSI codes */
|
|
23
|
+
function colorizeJson(str: string): string {
|
|
24
|
+
const parsed = tryParseJson(str);
|
|
25
|
+
if (parsed === undefined) return str;
|
|
26
|
+
return colorizeValue(parsed, 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function colorizeValue(value: unknown, indent: number): string {
|
|
30
|
+
if (value === null) return `${ansi.toolName}null${ansi.reset}`;
|
|
31
|
+
if (typeof value === "boolean")
|
|
32
|
+
return `${ansi.toolName}${value ? "true" : "false"}${ansi.reset}`;
|
|
33
|
+
if (typeof value === "number") return `${ansi.accent}${value}${ansi.reset}`;
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
// Try to unwrap stringified JSON (common in tool results)
|
|
36
|
+
const inner = tryParseJson(value);
|
|
37
|
+
if (inner !== undefined && typeof inner === "object" && inner !== null) {
|
|
38
|
+
return colorizeValue(inner, indent);
|
|
83
39
|
}
|
|
40
|
+
const escaped = JSON.stringify(value);
|
|
41
|
+
return `${ansi.success}${escaped}${ansi.reset}`;
|
|
84
42
|
}
|
|
85
43
|
|
|
86
|
-
|
|
87
|
-
|
|
44
|
+
const pad = " ".repeat(indent);
|
|
45
|
+
const innerPad = " ".repeat(indent + 1);
|
|
88
46
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return `"${
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
if (value.length === 0) return "[]";
|
|
49
|
+
const items = value.map(
|
|
50
|
+
(v) => `${innerPad}${colorizeValue(v, indent + 1)}`,
|
|
51
|
+
);
|
|
52
|
+
return `[\n${items.join(",\n")}\n${pad}]`;
|
|
95
53
|
}
|
|
96
|
-
return String(v);
|
|
97
|
-
}
|
|
98
54
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
55
|
+
if (typeof value === "object") {
|
|
56
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
57
|
+
if (entries.length === 0) return "{}";
|
|
58
|
+
const lines = entries.map(
|
|
59
|
+
([k, v]) =>
|
|
60
|
+
`${innerPad}${ansi.info}${JSON.stringify(k)}${ansi.reset}: ${colorizeValue(v, indent + 1)}`,
|
|
61
|
+
);
|
|
62
|
+
return `{\n${lines.join(",\n")}\n${pad}}`;
|
|
104
63
|
}
|
|
64
|
+
|
|
65
|
+
return String(value);
|
|
105
66
|
}
|
|
106
67
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
68
|
+
function buildDetailAnsi(tool: ToolCallData): string {
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
|
|
71
|
+
const time = tool.timestamp.toLocaleTimeString([], {
|
|
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("");
|
|
90
|
+
|
|
91
|
+
lines.push(`${ansi.bold}${ansi.primary}Input${ansi.reset}`);
|
|
92
|
+
lines.push(colorizeJson(displayInput));
|
|
93
|
+
lines.push("");
|
|
94
|
+
|
|
95
|
+
if (tool.output) {
|
|
96
|
+
if (tool.isError) {
|
|
97
|
+
lines.push(`${ansi.bold}${ansi.error}Error${ansi.reset}`);
|
|
98
|
+
lines.push(`${ansi.error}${colorizeJson(tool.output)}${ansi.reset}`);
|
|
99
|
+
} else {
|
|
100
|
+
lines.push(`${ansi.bold}${ansi.primary}Output${ansi.reset}`);
|
|
101
|
+
if (tool.largeResult) {
|
|
102
|
+
lines.push(
|
|
103
|
+
`${ansi.accent}Paginated for LLM: ${tool.largeResult.chars.toLocaleString()} chars, ${tool.largeResult.pages} page(s) — stored as ${tool.largeResult.id}${ansi.reset}`,
|
|
138
104
|
);
|
|
105
|
+
}
|
|
106
|
+
lines.push(colorizeJson(tool.output));
|
|
139
107
|
}
|
|
140
|
-
|
|
141
|
-
|
|
108
|
+
} else if (!tool.running) {
|
|
109
|
+
lines.push(`${ansi.bold}${ansi.primary}Output${ansi.reset}`);
|
|
110
|
+
lines.push(`${ansi.dim}(no output)${ansi.reset}`);
|
|
111
|
+
}
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
onClose();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
148
115
|
|
|
149
|
-
|
|
150
|
-
setTab((t) => (t === "input" ? "output" : "input"));
|
|
151
|
-
setCursor(0);
|
|
152
|
-
setExpanded(new Set());
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
116
|
+
const PAGE_SCROLL_LINES = 10;
|
|
155
117
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
setCursor(0);
|
|
165
|
-
setExpanded(new Set());
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
118
|
+
export const ToolPanel = memo(function ToolPanel({
|
|
119
|
+
toolCalls,
|
|
120
|
+
isActive,
|
|
121
|
+
}: ToolPanelProps) {
|
|
122
|
+
const { stdout } = useStdout();
|
|
123
|
+
const termRows = stdout?.rows ?? 24;
|
|
124
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
125
|
+
const [detailScroll, setDetailScroll] = useState(0);
|
|
168
126
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (key.downArrow) {
|
|
174
|
-
setCursor((c) => Math.min(rows.length - 1, c + 1));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
127
|
+
// Reverse-chronological order (most recent first)
|
|
128
|
+
const reversedCalls = useMemo(() => [...toolCalls].reverse(), [toolCalls]);
|
|
177
129
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const next = new Set(prev);
|
|
183
|
-
if (next.has(row.path)) {
|
|
184
|
-
for (const p of next) {
|
|
185
|
-
if (
|
|
186
|
-
p === row.path ||
|
|
187
|
-
p.startsWith(`${row.path}.`) ||
|
|
188
|
-
p.startsWith(`${row.path}[`)
|
|
189
|
-
) {
|
|
190
|
-
next.delete(p);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
next.add(row.path);
|
|
195
|
-
}
|
|
196
|
-
return next;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
return;
|
|
130
|
+
// Keep selection in bounds when new calls arrive
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (selectedIndex >= reversedCalls.length && reversedCalls.length > 0) {
|
|
133
|
+
setSelectedIndex(reversedCalls.length - 1);
|
|
200
134
|
}
|
|
135
|
+
}, [reversedCalls.length, selectedIndex]);
|
|
136
|
+
|
|
137
|
+
const selectedTool = reversedCalls[selectedIndex];
|
|
138
|
+
|
|
139
|
+
const renderedDetail = useMemo(() => {
|
|
140
|
+
if (!selectedTool) return "";
|
|
141
|
+
return buildDetailAnsi(selectedTool);
|
|
142
|
+
}, [selectedTool]);
|
|
201
143
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
144
|
+
const detailLines = useMemo(
|
|
145
|
+
() => renderedDetail.split("\n"),
|
|
146
|
+
[renderedDetail],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Visible area for sidebar and detail
|
|
150
|
+
const visibleRows = Math.max(1, termRows - 6); // chrome: tab bar, divider, status, input, borders
|
|
151
|
+
const maxDetailScroll = Math.max(0, detailLines.length - visibleRows);
|
|
152
|
+
const sidebarScrollOffset = Math.max(
|
|
153
|
+
0,
|
|
154
|
+
Math.min(
|
|
155
|
+
selectedIndex - Math.floor(visibleRows / 2),
|
|
156
|
+
reversedCalls.length - visibleRows,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Reset detail scroll when selection changes
|
|
161
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: selectedIndex is the intentional trigger
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
setDetailScroll(0);
|
|
164
|
+
}, [selectedIndex]);
|
|
165
|
+
|
|
166
|
+
useInput(
|
|
167
|
+
(input, key) => {
|
|
168
|
+
if (key.upArrow) {
|
|
169
|
+
if (key.shift) {
|
|
170
|
+
// Shift+up scrolls detail
|
|
171
|
+
setDetailScroll((s) => Math.max(0, s - 1));
|
|
172
|
+
} else {
|
|
173
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
223
174
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
175
|
+
return;
|
|
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
|
+
}
|
|
229
185
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
195
|
|
|
236
|
-
|
|
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
|
+
}
|
|
237
207
|
|
|
238
|
-
|
|
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
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{ isActive },
|
|
219
|
+
);
|
|
239
220
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
flexDirection="column"
|
|
243
|
-
borderStyle="round"
|
|
244
|
-
borderColor="cyan"
|
|
245
|
-
paddingX={1}
|
|
246
|
-
height={16}
|
|
247
|
-
>
|
|
248
|
-
{/* Header */}
|
|
249
|
-
<Box justifyContent="space-between">
|
|
250
|
-
<Box>
|
|
251
|
-
<Text bold color="cyan">
|
|
252
|
-
🔍 Tool Inspector
|
|
253
|
-
</Text>
|
|
254
|
-
<Text dimColor>
|
|
255
|
-
{" "}
|
|
256
|
-
({selectedTool + 1}/{toolCalls.length})
|
|
257
|
-
</Text>
|
|
258
|
-
</Box>
|
|
221
|
+
if (reversedCalls.length === 0) {
|
|
222
|
+
return (
|
|
223
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
259
224
|
<Text dimColor>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
</Text>
|
|
263
|
-
</Box>
|
|
264
|
-
|
|
265
|
-
{/* Tool name */}
|
|
266
|
-
<Box>
|
|
267
|
-
<Text bold color="magenta">
|
|
268
|
-
{tool.name}
|
|
225
|
+
No tool calls to inspect yet. Tool calls will appear here as the agent
|
|
226
|
+
uses them.
|
|
269
227
|
</Text>
|
|
270
|
-
{tool.running && <Text color="yellow"> ⟳ running</Text>}
|
|
271
228
|
</Box>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
272
231
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
dimColor={tab !== "input"}
|
|
279
|
-
>
|
|
280
|
-
{tab === "input" ? "▸ " : " "}Input
|
|
281
|
-
</Text>
|
|
282
|
-
<Text
|
|
283
|
-
bold={tab === "output"}
|
|
284
|
-
color={tab === "output" ? "green" : undefined}
|
|
285
|
-
dimColor={tab !== "output" && !hasOutput}
|
|
286
|
-
>
|
|
287
|
-
{tab === "output" ? "▸ " : " "}Output{!hasOutput ? " (none)" : ""}
|
|
288
|
-
</Text>
|
|
289
|
-
</Box>
|
|
232
|
+
// Sidebar visible window
|
|
233
|
+
const sidebarVisible = reversedCalls.slice(
|
|
234
|
+
sidebarScrollOffset,
|
|
235
|
+
sidebarScrollOffset + visibleRows,
|
|
236
|
+
);
|
|
290
237
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const indent = " ".repeat(row.depth);
|
|
297
|
-
const arrow = row.hasChildren
|
|
298
|
-
? expanded.has(row.path)
|
|
299
|
-
? "▾ "
|
|
300
|
-
: "▸ "
|
|
301
|
-
: " ";
|
|
238
|
+
// Detail visible window
|
|
239
|
+
const detailVisible = detailLines.slice(
|
|
240
|
+
detailScroll,
|
|
241
|
+
detailScroll + visibleRows,
|
|
242
|
+
);
|
|
302
243
|
|
|
244
|
+
return (
|
|
245
|
+
<Box flexGrow={1} height={visibleRows + 1} overflow="hidden">
|
|
246
|
+
{/* Left sidebar: tool call list */}
|
|
247
|
+
<Box
|
|
248
|
+
flexDirection="column"
|
|
249
|
+
width={SIDEBAR_WIDTH}
|
|
250
|
+
height={visibleRows + 1}
|
|
251
|
+
borderStyle="single"
|
|
252
|
+
borderColor={theme.muted}
|
|
253
|
+
borderRight
|
|
254
|
+
borderTop={false}
|
|
255
|
+
borderBottom={false}
|
|
256
|
+
borderLeft={false}
|
|
257
|
+
overflow="hidden"
|
|
258
|
+
>
|
|
259
|
+
<Box paddingX={1}>
|
|
260
|
+
<Text bold dimColor>
|
|
261
|
+
Tool Calls ({reversedCalls.length})
|
|
262
|
+
</Text>
|
|
263
|
+
</Box>
|
|
264
|
+
{sidebarVisible.map((tc, vi) => {
|
|
265
|
+
const i = vi + sidebarScrollOffset;
|
|
266
|
+
const isSelected = i === selectedIndex;
|
|
267
|
+
const icon = tc.running ? "⟳" : tc.isError ? "✘" : "✔";
|
|
268
|
+
const time = tc.timestamp.toLocaleTimeString([], {
|
|
269
|
+
hour: "2-digit",
|
|
270
|
+
minute: "2-digit",
|
|
271
|
+
});
|
|
272
|
+
const { displayName } = resolveToolDisplay(tc.name, tc.input);
|
|
273
|
+
const maxName = SIDEBAR_WIDTH - 12; // icon + time + padding
|
|
274
|
+
const nameDisplay =
|
|
275
|
+
displayName.length > maxName
|
|
276
|
+
? `${displayName.slice(0, maxName - 1)}…`
|
|
277
|
+
: displayName;
|
|
303
278
|
return (
|
|
304
|
-
<Box key={
|
|
279
|
+
<Box key={tc.id} paddingX={1}>
|
|
305
280
|
<Text
|
|
306
|
-
backgroundColor={isSelected ?
|
|
307
|
-
|
|
281
|
+
backgroundColor={isSelected ? theme.selectionBg : undefined}
|
|
282
|
+
bold={isSelected}
|
|
283
|
+
color={
|
|
284
|
+
isSelected
|
|
285
|
+
? theme.info
|
|
286
|
+
: tc.running
|
|
287
|
+
? theme.accent
|
|
288
|
+
: undefined
|
|
289
|
+
}
|
|
290
|
+
wrap="truncate-end"
|
|
308
291
|
>
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
292
|
+
{isSelected ? "▸" : " "}{" "}
|
|
293
|
+
<Text
|
|
294
|
+
color={
|
|
295
|
+
tc.running
|
|
296
|
+
? theme.accent
|
|
297
|
+
: tc.isError
|
|
298
|
+
? theme.error
|
|
299
|
+
: theme.muted
|
|
300
|
+
}
|
|
301
|
+
bold={false}
|
|
302
|
+
>
|
|
303
|
+
{icon}
|
|
304
|
+
</Text>{" "}
|
|
305
|
+
{nameDisplay}
|
|
306
|
+
<Text dimColor> {time}</Text>
|
|
321
307
|
</Text>
|
|
322
308
|
</Box>
|
|
323
309
|
);
|
|
324
310
|
})}
|
|
325
311
|
</Box>
|
|
312
|
+
|
|
313
|
+
{/* Right detail pane */}
|
|
314
|
+
<Box
|
|
315
|
+
flexDirection="column"
|
|
316
|
+
flexGrow={1}
|
|
317
|
+
height={visibleRows + 1}
|
|
318
|
+
paddingX={1}
|
|
319
|
+
overflow="hidden"
|
|
320
|
+
>
|
|
321
|
+
{detailVisible.map((line, i) => {
|
|
322
|
+
const lineNum = detailScroll + i;
|
|
323
|
+
return <Text key={lineNum}>{line || " "}</Text>;
|
|
324
|
+
})}
|
|
325
|
+
{detailLines.length > visibleRows && (
|
|
326
|
+
<Box>
|
|
327
|
+
<Text dimColor>
|
|
328
|
+
↑↓ select · j/k scroll · J/K page · g/G top/bottom · [
|
|
329
|
+
{detailScroll + 1}–
|
|
330
|
+
{Math.min(detailScroll + visibleRows, detailLines.length)} of{" "}
|
|
331
|
+
{detailLines.length}]
|
|
332
|
+
</Text>
|
|
333
|
+
</Box>
|
|
334
|
+
)}
|
|
335
|
+
{detailLines.length <= visibleRows && <Box flexGrow={1} />}
|
|
336
|
+
{detailLines.length <= visibleRows && (
|
|
337
|
+
<Text dimColor>↑↓ select tool calls</Text>
|
|
338
|
+
)}
|
|
339
|
+
</Box>
|
|
326
340
|
</Box>
|
|
327
341
|
);
|
|
328
|
-
}
|
|
342
|
+
});
|