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
package/src/tui/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useApp } from "ink";
|
|
1
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
type ChatSession,
|
|
@@ -6,17 +6,27 @@ import {
|
|
|
6
6
|
sendMessage,
|
|
7
7
|
startChatSession,
|
|
8
8
|
} from "../chat/session.ts";
|
|
9
|
+
import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../daemon/large-results.ts";
|
|
9
10
|
import type { Interaction } from "../db/threads.ts";
|
|
10
11
|
import { getThread } from "../db/threads.ts";
|
|
12
|
+
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
13
|
+
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
11
14
|
import { InputBar } from "./components/InputBar.tsx";
|
|
15
|
+
import { AnimatedLogo } from "./components/Logo.tsx";
|
|
12
16
|
import { type ChatMessage, MessageList } from "./components/MessageList.tsx";
|
|
17
|
+
import { QueuePanel } from "./components/QueuePanel.tsx";
|
|
13
18
|
import { StatusBar } from "./components/StatusBar.tsx";
|
|
19
|
+
import { TabBar, type TabId } from "./components/TabBar.tsx";
|
|
20
|
+
import { TaskPanel } from "./components/TaskPanel.tsx";
|
|
21
|
+
import { ThreadPanel } from "./components/ThreadPanel.tsx";
|
|
14
22
|
import type { ToolCallData } from "./components/ToolCall.tsx";
|
|
15
23
|
import { ToolPanel } from "./components/ToolPanel.tsx";
|
|
24
|
+
import { ansi } from "./theme.ts";
|
|
16
25
|
|
|
17
26
|
interface AppProps {
|
|
18
27
|
projectDir: string;
|
|
19
28
|
threadId?: string;
|
|
29
|
+
initialPrompt?: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
let nextMsgId = 0;
|
|
@@ -24,24 +34,45 @@ function msgId(): string {
|
|
|
24
34
|
return `msg-${++nextMsgId}`;
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
function detectToolError(output: string | undefined): boolean {
|
|
38
|
+
if (!output) return false;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(output);
|
|
41
|
+
if (typeof parsed === "object" && parsed?.is_error === true) return true;
|
|
42
|
+
} catch {
|
|
43
|
+
/* not JSON */
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
function restoreMessagesFromInteractions(
|
|
28
49
|
interactions: Interaction[],
|
|
29
50
|
): ChatMessage[] {
|
|
30
51
|
const result: ChatMessage[] = [];
|
|
31
52
|
let pendingTools: ToolCallData[] = [];
|
|
32
53
|
|
|
54
|
+
let restoredIdx = 0;
|
|
33
55
|
for (const ix of interactions) {
|
|
34
56
|
if (ix.kind === "tool_use") {
|
|
35
57
|
pendingTools.push({
|
|
58
|
+
id: `restored-${restoredIdx++}`,
|
|
36
59
|
name: ix.tool_name ?? "unknown",
|
|
37
60
|
input: ix.tool_input ?? "{}",
|
|
38
61
|
running: false,
|
|
62
|
+
timestamp: ix.created_at,
|
|
39
63
|
});
|
|
40
64
|
} else if (ix.kind === "tool_result") {
|
|
41
|
-
// Attach output to the matching pending tool call
|
|
42
65
|
const tc = pendingTools.find((t) => t.name === ix.tool_name && !t.output);
|
|
43
66
|
if (tc) {
|
|
44
67
|
tc.output = ix.content;
|
|
68
|
+
tc.isError = detectToolError(ix.content);
|
|
69
|
+
if (ix.content.length > MAX_INLINE_CHARS) {
|
|
70
|
+
tc.largeResult = {
|
|
71
|
+
id: "(restored)",
|
|
72
|
+
chars: ix.content.length,
|
|
73
|
+
pages: Math.ceil(ix.content.length / PAGE_SIZE_CHARS),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
45
76
|
}
|
|
46
77
|
} else if (ix.kind === "message" && ix.role === "user") {
|
|
47
78
|
result.push({
|
|
@@ -62,7 +93,6 @@ function restoreMessagesFromInteractions(
|
|
|
62
93
|
}
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
// If there are leftover tool calls with no following assistant message
|
|
66
96
|
if (pendingTools.length > 0) {
|
|
67
97
|
result.push({
|
|
68
98
|
id: msgId(),
|
|
@@ -76,7 +106,11 @@ function restoreMessagesFromInteractions(
|
|
|
76
106
|
return result;
|
|
77
107
|
}
|
|
78
108
|
|
|
79
|
-
export function App({
|
|
109
|
+
export function App({
|
|
110
|
+
projectDir,
|
|
111
|
+
threadId: resumeThreadId,
|
|
112
|
+
initialPrompt,
|
|
113
|
+
}: AppProps) {
|
|
80
114
|
const { exit } = useApp();
|
|
81
115
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
82
116
|
const [inputValue, setInputValue] = useState("");
|
|
@@ -85,11 +119,25 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
85
119
|
const [streamingText, setStreamingText] = useState("");
|
|
86
120
|
const [activeToolCalls, setActiveToolCalls] = useState<ToolCallData[]>([]);
|
|
87
121
|
const [ready, setReady] = useState(false);
|
|
122
|
+
const skipSplash = !!(resumeThreadId || initialPrompt);
|
|
123
|
+
const [splashDone, setSplashDone] = useState(skipSplash);
|
|
88
124
|
const [error, setError] = useState<string | null>(null);
|
|
89
125
|
const sessionRef = useRef<ChatSession | null>(null);
|
|
90
|
-
const [
|
|
126
|
+
const [activeTab, setActiveTab] = useState<TabId>(1);
|
|
127
|
+
const [daemonRunning, setDaemonRunning] = useState(false);
|
|
128
|
+
const [chatTitle, setChatTitle] = useState<string | undefined>(undefined);
|
|
91
129
|
const queueRef = useRef<string[]>([]);
|
|
92
130
|
const processingRef = useRef(false);
|
|
131
|
+
const [queuedMessages, setQueuedMessages] = useState<string[]>([]);
|
|
132
|
+
const [selectedQueueIndex, setSelectedQueueIndex] = useState(0);
|
|
133
|
+
|
|
134
|
+
const syncQueue = useCallback(() => {
|
|
135
|
+
const snapshot = [...queueRef.current];
|
|
136
|
+
setQueuedMessages(snapshot);
|
|
137
|
+
setSelectedQueueIndex((prev) =>
|
|
138
|
+
snapshot.length === 0 ? 0 : Math.min(prev, snapshot.length - 1),
|
|
139
|
+
);
|
|
140
|
+
}, []);
|
|
93
141
|
|
|
94
142
|
// Initialize session
|
|
95
143
|
useEffect(() => {
|
|
@@ -103,7 +151,6 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
103
151
|
}
|
|
104
152
|
sessionRef.current = session;
|
|
105
153
|
|
|
106
|
-
// If resuming, populate the message list from DB interactions
|
|
107
154
|
if (session.messages.length > 0) {
|
|
108
155
|
const threadData = await getThread(session.conn, session.threadId);
|
|
109
156
|
if (threadData) {
|
|
@@ -113,13 +160,13 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
// Show startup hint
|
|
117
163
|
setMessages((prev) => [
|
|
118
164
|
...prev,
|
|
119
165
|
{
|
|
120
166
|
id: msgId(),
|
|
121
167
|
role: "system" as const,
|
|
122
|
-
content:
|
|
168
|
+
content:
|
|
169
|
+
"Press Tab to switch between panels. Type /help for commands.",
|
|
123
170
|
timestamp: new Date(),
|
|
124
171
|
},
|
|
125
172
|
]);
|
|
@@ -136,24 +183,103 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
136
183
|
const threadId = sessionRef.current.threadId;
|
|
137
184
|
endChatSession(sessionRef.current);
|
|
138
185
|
process.stderr.write(
|
|
139
|
-
`\nThread: ${threadId}\nResume with:
|
|
186
|
+
`\nThread: ${threadId}\nResume with: ${ansi.success}botholomew chat --thread-id ${threadId}${ansi.reset}\n`,
|
|
140
187
|
);
|
|
141
188
|
}
|
|
142
189
|
};
|
|
143
190
|
}, [projectDir, resumeThreadId]);
|
|
144
191
|
|
|
192
|
+
// Minimum splash screen duration
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const timer = setTimeout(() => setSplashDone(true), 2000);
|
|
195
|
+
return () => clearTimeout(timer);
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
// Stable ref for App-level input handler — same pattern as InputBar to
|
|
199
|
+
// prevent Ink's useInput from re-registering stdin listeners on every render.
|
|
200
|
+
const activeTabRef = useRef(activeTab);
|
|
201
|
+
const queuedMessagesRef = useRef(queuedMessages);
|
|
202
|
+
const selectedQueueIndexRef = useRef(selectedQueueIndex);
|
|
203
|
+
activeTabRef.current = activeTab;
|
|
204
|
+
queuedMessagesRef.current = queuedMessages;
|
|
205
|
+
selectedQueueIndexRef.current = selectedQueueIndex;
|
|
206
|
+
|
|
207
|
+
const stableAppHandler = useCallback(
|
|
208
|
+
// biome-ignore lint/suspicious/noExplicitAny: Ink's Key type is not exported
|
|
209
|
+
(input: string, key: any) => {
|
|
210
|
+
// Ctrl+C exits
|
|
211
|
+
if (input === "c" && key.ctrl) {
|
|
212
|
+
exit();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Tab key cycles tabs — always active (InputBar ignores tab)
|
|
217
|
+
if (key.tab && !key.shift) {
|
|
218
|
+
setActiveTab((t) => ((t % 6) + 1) as TabId);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Queue manipulation keybindings (only when queue has items on Chat tab)
|
|
223
|
+
const tab = activeTabRef.current;
|
|
224
|
+
const queue = queuedMessagesRef.current;
|
|
225
|
+
if (tab === 1 && queue.length > 0 && key.ctrl) {
|
|
226
|
+
if (input === "j") {
|
|
227
|
+
setSelectedQueueIndex((i) => Math.min(i + 1, queue.length - 1));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (input === "k") {
|
|
231
|
+
setSelectedQueueIndex((i) => Math.max(i - 1, 0));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (input === "x") {
|
|
235
|
+
queueRef.current.splice(selectedQueueIndexRef.current, 1);
|
|
236
|
+
syncQueue();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (input === "e") {
|
|
240
|
+
const [msg] = queueRef.current.splice(
|
|
241
|
+
selectedQueueIndexRef.current,
|
|
242
|
+
1,
|
|
243
|
+
);
|
|
244
|
+
syncQueue();
|
|
245
|
+
if (msg) {
|
|
246
|
+
setInputValue(msg);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (tab !== 1) {
|
|
253
|
+
// Number keys jump to tab on non-chat tabs
|
|
254
|
+
const num = Number.parseInt(input, 10);
|
|
255
|
+
if (num >= 1 && num <= 6) {
|
|
256
|
+
setActiveTab(num as TabId);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Escape returns to chat
|
|
260
|
+
if (key.escape) {
|
|
261
|
+
setActiveTab(1);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[exit, syncQueue],
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
useInput(stableAppHandler);
|
|
270
|
+
|
|
145
271
|
const processQueue = useCallback(async () => {
|
|
146
272
|
if (processingRef.current || !sessionRef.current) return;
|
|
147
273
|
processingRef.current = true;
|
|
148
274
|
|
|
149
275
|
while (queueRef.current.length > 0) {
|
|
150
276
|
const trimmed = queueRef.current.shift();
|
|
277
|
+
syncQueue();
|
|
151
278
|
if (!trimmed) break;
|
|
152
279
|
setIsLoading(true);
|
|
153
280
|
setStreamingText("");
|
|
154
281
|
setActiveToolCalls([]);
|
|
155
282
|
|
|
156
|
-
// Add user message
|
|
157
283
|
const userMsg: ChatMessage = {
|
|
158
284
|
id: msgId(),
|
|
159
285
|
role: "user",
|
|
@@ -162,7 +288,6 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
162
288
|
};
|
|
163
289
|
setMessages((prev) => [...prev, userMsg]);
|
|
164
290
|
|
|
165
|
-
// Collect tool calls for the current segment
|
|
166
291
|
let pendingToolCalls: ToolCallData[] = [];
|
|
167
292
|
let currentText = "";
|
|
168
293
|
|
|
@@ -184,27 +309,40 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
184
309
|
}
|
|
185
310
|
};
|
|
186
311
|
|
|
312
|
+
let lastStreamFlush = 0;
|
|
187
313
|
try {
|
|
188
314
|
await sendMessage(sessionRef.current, trimmed, {
|
|
189
315
|
onToken: (token) => {
|
|
190
316
|
currentText += token;
|
|
191
|
-
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
if (now - lastStreamFlush >= 50) {
|
|
319
|
+
setStreamingText(currentText);
|
|
320
|
+
lastStreamFlush = now;
|
|
321
|
+
}
|
|
192
322
|
},
|
|
193
|
-
onToolStart: (name, input) => {
|
|
323
|
+
onToolStart: (id, name, input) => {
|
|
194
324
|
if (currentText) {
|
|
195
325
|
finalizeSegment();
|
|
196
326
|
}
|
|
197
|
-
const tc: ToolCallData = {
|
|
327
|
+
const tc: ToolCallData = {
|
|
328
|
+
id,
|
|
329
|
+
name,
|
|
330
|
+
input,
|
|
331
|
+
running: true,
|
|
332
|
+
timestamp: new Date(),
|
|
333
|
+
};
|
|
198
334
|
pendingToolCalls.push(tc);
|
|
199
335
|
setActiveToolCalls([...pendingToolCalls]);
|
|
200
336
|
},
|
|
201
|
-
onToolEnd: (
|
|
202
|
-
const tc = pendingToolCalls.find(
|
|
203
|
-
(t) => t.name === name && t.running,
|
|
204
|
-
);
|
|
337
|
+
onToolEnd: (id, _name, output, isError, meta) => {
|
|
338
|
+
const tc = pendingToolCalls.find((t) => t.id === id);
|
|
205
339
|
if (tc) {
|
|
206
340
|
tc.running = false;
|
|
207
341
|
tc.output = output;
|
|
342
|
+
tc.isError = isError;
|
|
343
|
+
if (meta?.largeResult) {
|
|
344
|
+
tc.largeResult = meta.largeResult;
|
|
345
|
+
}
|
|
208
346
|
}
|
|
209
347
|
setActiveToolCalls([...pendingToolCalls]);
|
|
210
348
|
},
|
|
@@ -227,7 +365,41 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
227
365
|
|
|
228
366
|
setIsLoading(false);
|
|
229
367
|
processingRef.current = false;
|
|
230
|
-
}, []);
|
|
368
|
+
}, [syncQueue]);
|
|
369
|
+
|
|
370
|
+
// Auto-submit initial prompt once session is ready
|
|
371
|
+
const initialPromptSent = useRef(false);
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (ready && initialPrompt && !initialPromptSent.current) {
|
|
374
|
+
initialPromptSent.current = true;
|
|
375
|
+
queueRef.current.push(initialPrompt);
|
|
376
|
+
syncQueue();
|
|
377
|
+
setInputHistory((prev) => [...prev, initialPrompt]);
|
|
378
|
+
processQueue();
|
|
379
|
+
}
|
|
380
|
+
}, [ready, initialPrompt, processQueue, syncQueue]);
|
|
381
|
+
|
|
382
|
+
// Poll for chat thread title updates
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (!ready || !sessionRef.current) return;
|
|
385
|
+
let mounted = true;
|
|
386
|
+
|
|
387
|
+
const refreshTitle = async () => {
|
|
388
|
+
const session = sessionRef.current;
|
|
389
|
+
if (!session) return;
|
|
390
|
+
const result = await getThread(session.conn, session.threadId);
|
|
391
|
+
if (mounted && result?.thread.title) {
|
|
392
|
+
setChatTitle(result.thread.title);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
refreshTitle();
|
|
397
|
+
const interval = setInterval(refreshTitle, 5000);
|
|
398
|
+
return () => {
|
|
399
|
+
mounted = false;
|
|
400
|
+
clearInterval(interval);
|
|
401
|
+
};
|
|
402
|
+
}, [ready]);
|
|
231
403
|
|
|
232
404
|
const handleSubmit = useCallback(
|
|
233
405
|
async (text: string) => {
|
|
@@ -236,20 +408,51 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
236
408
|
|
|
237
409
|
setInputValue("");
|
|
238
410
|
|
|
239
|
-
// Handle /help
|
|
240
411
|
if (trimmed === "/help") {
|
|
241
412
|
const helpMsg: ChatMessage = {
|
|
242
413
|
id: msgId(),
|
|
243
414
|
role: "system",
|
|
244
415
|
content: [
|
|
245
|
-
"
|
|
416
|
+
"Navigation:",
|
|
417
|
+
" Tab Cycle between panels",
|
|
418
|
+
" 1-6 Jump to panel (when not in Chat)",
|
|
419
|
+
" Escape Return to Chat",
|
|
420
|
+
"",
|
|
421
|
+
"Chat (Tab 1):",
|
|
246
422
|
" Enter Send message",
|
|
247
423
|
" ⌥+Enter Insert newline",
|
|
248
424
|
" ↑/↓ Browse input history",
|
|
249
425
|
"",
|
|
426
|
+
"Tools (Tab 2):",
|
|
427
|
+
" ↑/↓ Select tool call",
|
|
428
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
429
|
+
" j/k Scroll detail pane",
|
|
430
|
+
"",
|
|
431
|
+
"Context (Tab 3):",
|
|
432
|
+
" ↑/↓ Navigate items",
|
|
433
|
+
" Enter Expand directory / preview file",
|
|
434
|
+
" Backspace Go up one directory",
|
|
435
|
+
" / Search context",
|
|
436
|
+
" d Delete selected item",
|
|
437
|
+
"",
|
|
438
|
+
"Tasks (Tab 4):",
|
|
439
|
+
" ↑/↓ Navigate task list",
|
|
440
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
441
|
+
" j/k Scroll detail pane",
|
|
442
|
+
" f Cycle status filter",
|
|
443
|
+
" p Cycle priority filter",
|
|
444
|
+
" r Refresh tasks",
|
|
445
|
+
"",
|
|
446
|
+
"Threads (Tab 5):",
|
|
447
|
+
" ↑/↓ Navigate thread list",
|
|
448
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
449
|
+
" j/k Scroll detail pane",
|
|
450
|
+
" f Cycle type filter",
|
|
451
|
+
" d Delete thread (with confirmation)",
|
|
452
|
+
" r Refresh threads",
|
|
453
|
+
"",
|
|
250
454
|
"Commands:",
|
|
251
455
|
" /help Show this help",
|
|
252
|
-
" /tools Toggle tool call inspector",
|
|
253
456
|
" /quit, /exit End the chat session",
|
|
254
457
|
].join("\n"),
|
|
255
458
|
timestamp: new Date(),
|
|
@@ -258,52 +461,33 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
258
461
|
return;
|
|
259
462
|
}
|
|
260
463
|
|
|
261
|
-
// Handle /tools
|
|
262
|
-
if (trimmed === "/tools") {
|
|
263
|
-
setMessages((prev) => {
|
|
264
|
-
const hasTools = prev.some(
|
|
265
|
-
(m) => m.toolCalls && m.toolCalls.length > 0,
|
|
266
|
-
);
|
|
267
|
-
if (!hasTools) {
|
|
268
|
-
return [
|
|
269
|
-
...prev,
|
|
270
|
-
{
|
|
271
|
-
id: msgId(),
|
|
272
|
-
role: "system" as const,
|
|
273
|
-
content: "No tool calls to inspect yet.",
|
|
274
|
-
timestamp: new Date(),
|
|
275
|
-
},
|
|
276
|
-
];
|
|
277
|
-
}
|
|
278
|
-
// Use setTimeout to toggle panel after state update
|
|
279
|
-
setTimeout(() => setToolPanelOpen((p) => !p), 0);
|
|
280
|
-
return prev;
|
|
281
|
-
});
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Handle /quit
|
|
286
464
|
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
287
|
-
if (sessionRef.current) {
|
|
288
|
-
const threadId = sessionRef.current.threadId;
|
|
289
|
-
await endChatSession(sessionRef.current);
|
|
290
|
-
sessionRef.current = null;
|
|
291
|
-
process.stderr.write(
|
|
292
|
-
`\nThread: ${threadId}\nResume with: \x1b[32mbotholomew chat --thread-id ${threadId}\x1b[0m\n`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
465
|
exit();
|
|
296
466
|
return;
|
|
297
467
|
}
|
|
298
468
|
|
|
299
469
|
setInputHistory((prev) => [...prev, trimmed]);
|
|
300
470
|
queueRef.current.push(trimmed);
|
|
471
|
+
syncQueue();
|
|
301
472
|
processQueue();
|
|
302
473
|
},
|
|
303
|
-
[exit, processQueue],
|
|
474
|
+
[exit, processQueue, syncQueue],
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const sessionConn = sessionRef.current?.conn;
|
|
478
|
+
const inputBarHeader = useMemo(
|
|
479
|
+
() =>
|
|
480
|
+
sessionConn ? (
|
|
481
|
+
<StatusBar
|
|
482
|
+
projectDir={projectDir}
|
|
483
|
+
conn={sessionConn}
|
|
484
|
+
chatTitle={chatTitle}
|
|
485
|
+
onDaemonStatusChange={setDaemonRunning}
|
|
486
|
+
/>
|
|
487
|
+
) : null,
|
|
488
|
+
[projectDir, sessionConn, chatTitle],
|
|
304
489
|
);
|
|
305
490
|
|
|
306
|
-
// Collect all tool calls from messages for the panel
|
|
307
491
|
const allToolCalls = useMemo(
|
|
308
492
|
() => messages.flatMap((m) => m.toolCalls ?? []),
|
|
309
493
|
[messages],
|
|
@@ -317,42 +501,103 @@ export function App({ projectDir, threadId: resumeThreadId }: AppProps) {
|
|
|
317
501
|
);
|
|
318
502
|
}
|
|
319
503
|
|
|
320
|
-
if (!ready || !sessionRef.current) {
|
|
504
|
+
if (!ready || !splashDone || !sessionRef.current) {
|
|
321
505
|
return (
|
|
322
|
-
<Box
|
|
323
|
-
|
|
506
|
+
<Box
|
|
507
|
+
flexDirection="column"
|
|
508
|
+
padding={1}
|
|
509
|
+
alignItems="center"
|
|
510
|
+
justifyContent="center"
|
|
511
|
+
height="100%"
|
|
512
|
+
>
|
|
513
|
+
<AnimatedLogo />
|
|
324
514
|
</Box>
|
|
325
515
|
);
|
|
326
516
|
}
|
|
327
517
|
|
|
518
|
+
const conn = sessionRef.current.conn;
|
|
519
|
+
const threadId = sessionRef.current.threadId;
|
|
520
|
+
|
|
328
521
|
return (
|
|
329
522
|
<Box flexDirection="column" height="100%">
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
523
|
+
{/* Tab content area — all panels stay mounted to avoid expensive
|
|
524
|
+
remount cycles (especially <Static> in MessageList re-rendering
|
|
525
|
+
the entire history). display="none" hides inactive panels from
|
|
526
|
+
layout without destroying them. */}
|
|
527
|
+
<Box
|
|
528
|
+
display={activeTab === 1 ? "flex" : "none"}
|
|
529
|
+
flexDirection="column"
|
|
530
|
+
flexGrow={1}
|
|
531
|
+
>
|
|
532
|
+
<MessageList
|
|
533
|
+
messages={messages}
|
|
534
|
+
streamingText={streamingText}
|
|
535
|
+
isLoading={isLoading}
|
|
536
|
+
activeToolCalls={activeToolCalls}
|
|
537
|
+
/>
|
|
538
|
+
</Box>
|
|
539
|
+
<Box
|
|
540
|
+
display={activeTab === 2 ? "flex" : "none"}
|
|
541
|
+
flexDirection="column"
|
|
542
|
+
flexGrow={1}
|
|
543
|
+
>
|
|
544
|
+
<ToolPanel toolCalls={allToolCalls} isActive={activeTab === 2} />
|
|
545
|
+
</Box>
|
|
546
|
+
<Box
|
|
547
|
+
display={activeTab === 3 ? "flex" : "none"}
|
|
548
|
+
flexDirection="column"
|
|
549
|
+
flexGrow={1}
|
|
550
|
+
>
|
|
551
|
+
<ContextPanel conn={conn} isActive={activeTab === 3} />
|
|
552
|
+
</Box>
|
|
553
|
+
<Box
|
|
554
|
+
display={activeTab === 4 ? "flex" : "none"}
|
|
555
|
+
flexDirection="column"
|
|
556
|
+
flexGrow={1}
|
|
557
|
+
>
|
|
558
|
+
<TaskPanel conn={conn} isActive={activeTab === 4} />
|
|
559
|
+
</Box>
|
|
560
|
+
<Box
|
|
561
|
+
display={activeTab === 5 ? "flex" : "none"}
|
|
562
|
+
flexDirection="column"
|
|
563
|
+
flexGrow={1}
|
|
564
|
+
>
|
|
565
|
+
<ThreadPanel
|
|
566
|
+
conn={conn}
|
|
567
|
+
activeThreadId={threadId}
|
|
568
|
+
isActive={activeTab === 5}
|
|
569
|
+
/>
|
|
570
|
+
</Box>
|
|
571
|
+
<Box
|
|
572
|
+
display={activeTab === 6 ? "flex" : "none"}
|
|
573
|
+
flexDirection="column"
|
|
574
|
+
flexGrow={1}
|
|
575
|
+
>
|
|
576
|
+
<HelpPanel
|
|
577
|
+
projectDir={projectDir}
|
|
578
|
+
threadId={threadId}
|
|
579
|
+
daemonRunning={daemonRunning}
|
|
580
|
+
/>
|
|
581
|
+
</Box>
|
|
582
|
+
|
|
583
|
+
{/* Queued messages (only on Chat tab) */}
|
|
584
|
+
{activeTab === 1 && queuedMessages.length > 0 && (
|
|
585
|
+
<QueuePanel
|
|
586
|
+
messages={queuedMessages}
|
|
587
|
+
selectedIndex={selectedQueueIndex}
|
|
340
588
|
/>
|
|
341
589
|
)}
|
|
590
|
+
|
|
591
|
+
{/* Bottom bar: StatusBar + InputBar (input only on Chat tab) + TabBar */}
|
|
342
592
|
<InputBar
|
|
343
593
|
value={inputValue}
|
|
344
594
|
onChange={setInputValue}
|
|
345
595
|
onSubmit={handleSubmit}
|
|
346
|
-
disabled={
|
|
596
|
+
disabled={activeTab !== 1}
|
|
347
597
|
history={inputHistory}
|
|
348
|
-
header={
|
|
349
|
-
<StatusBar
|
|
350
|
-
projectDir={projectDir}
|
|
351
|
-
conn={sessionRef.current.conn}
|
|
352
|
-
isLoading={isLoading}
|
|
353
|
-
/>
|
|
354
|
-
}
|
|
598
|
+
header={inputBarHeader}
|
|
355
599
|
/>
|
|
600
|
+
<TabBar activeTab={activeTab} />
|
|
356
601
|
</Box>
|
|
357
602
|
);
|
|
358
603
|
}
|