botholomew 0.16.2 → 0.16.4
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 +5 -0
- package/package.json +1 -1
- package/src/context/fetcher.ts +2 -2
- package/src/context/markdown-converter.ts +2 -2
- package/src/tui/App.tsx +111 -748
- package/src/tui/components/MessageList.tsx +0 -1
- package/src/tui/components/TabBar.tsx +1 -1
- package/src/tui/components/TabPanels.tsx +108 -0
- package/src/tui/handleSubmit.ts +206 -0
- package/src/tui/hooks/useAppKeybindings.ts +166 -0
- package/src/tui/hooks/useCaptureTabCycle.ts +28 -0
- package/src/tui/hooks/useChatSession.ts +151 -0
- package/src/tui/hooks/useChatTitlePolling.ts +36 -0
- package/src/tui/hooks/useMessageQueue.ts +254 -0
- package/src/tui/hooks/useTerminalRows.ts +20 -0
- package/src/tui/keys.ts +24 -0
- package/src/tui/messages.ts +11 -0
- package/src/worker/fake-llm.ts +60 -0
- package/src/worker/fake-mcp.ts +134 -0
package/src/tui/App.tsx
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
import { Box, Static, Text
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
abortActiveStream,
|
|
5
|
-
type ChatSession,
|
|
6
|
-
clearChatSession,
|
|
7
|
-
endChatSession,
|
|
8
|
-
sendMessage,
|
|
9
|
-
startChatSession,
|
|
10
|
-
} from "../chat/session.ts";
|
|
11
|
-
import type { ContextUsage } from "../chat/usage.ts";
|
|
1
|
+
import { Box, Static, Text } from "ink";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
3
|
import {
|
|
13
4
|
BUILTIN_SLASH_COMMANDS,
|
|
14
|
-
handleSlashCommand,
|
|
15
5
|
type SlashCommand,
|
|
16
6
|
} from "../skills/commands.ts";
|
|
17
|
-
import { getThread } from "../threads/store.ts";
|
|
18
|
-
import { ContextPanel } from "./components/ContextPanel.tsx";
|
|
19
|
-
import { HelpPanel } from "./components/HelpPanel.tsx";
|
|
20
7
|
import { InputBar } from "./components/InputBar.tsx";
|
|
21
8
|
import { AnimatedLogo } from "./components/Logo.tsx";
|
|
22
9
|
import {
|
|
@@ -25,18 +12,19 @@ import {
|
|
|
25
12
|
MessageList,
|
|
26
13
|
} from "./components/MessageList.tsx";
|
|
27
14
|
import { QueuePanel } from "./components/QueuePanel.tsx";
|
|
28
|
-
import { SchedulePanel } from "./components/SchedulePanel.tsx";
|
|
29
15
|
import { StatusBar } from "./components/StatusBar.tsx";
|
|
30
16
|
import { TabBar, type TabId } from "./components/TabBar.tsx";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
17
|
+
import { TabPanels } from "./components/TabPanels.tsx";
|
|
18
|
+
import { useChatSubmit } from "./handleSubmit.ts";
|
|
19
|
+
import { useAppKeybindings } from "./hooks/useAppKeybindings.ts";
|
|
20
|
+
import { useCaptureTabCycle } from "./hooks/useCaptureTabCycle.ts";
|
|
21
|
+
import { useChatSession } from "./hooks/useChatSession.ts";
|
|
22
|
+
import { useChatTitlePolling } from "./hooks/useChatTitlePolling.ts";
|
|
23
|
+
import { useMessageQueue } from "./hooks/useMessageQueue.ts";
|
|
24
|
+
import { useTerminalRows } from "./hooks/useTerminalRows.ts";
|
|
36
25
|
import { IdleProvider, useIdle } from "./idle.tsx";
|
|
37
|
-
import {
|
|
38
|
-
import { buildSlashCommands
|
|
39
|
-
import { ansi } from "./theme.ts";
|
|
26
|
+
import { FOOTER_RESERVE } from "./messages.ts";
|
|
27
|
+
import { buildSlashCommands } from "./slashCompletion.ts";
|
|
40
28
|
|
|
41
29
|
interface AppProps {
|
|
42
30
|
projectDir: string;
|
|
@@ -45,42 +33,6 @@ interface AppProps {
|
|
|
45
33
|
idleTimeoutMs: number;
|
|
46
34
|
}
|
|
47
35
|
|
|
48
|
-
let nextMsgId = 0;
|
|
49
|
-
function msgId(): string {
|
|
50
|
-
return `msg-${++nextMsgId}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Conservative line reservation for the bottom chrome — StatusBar (1) +
|
|
54
|
-
// bordered InputBar (3) + multiline hint (1) + TabBar (1) + slack for the
|
|
55
|
-
// SlashCommandPopup or QueuePanel (~4). The chat-tab body's `maxHeight` and
|
|
56
|
-
// the panel boxes' `height` both subtract this from `rows` so the dynamic
|
|
57
|
-
// frame's total output stays strictly below the viewport — see the comment
|
|
58
|
-
// on the `rows` state in `AppInner` for why that matters.
|
|
59
|
-
const FOOTER_RESERVE = 10;
|
|
60
|
-
|
|
61
|
-
// Tab routing: Ctrl+<letter> jumps to a tab. Chosen for memorability — first
|
|
62
|
-
// available letter that doesn't collide with other Ctrl bindings (Ctrl+C exit,
|
|
63
|
-
// Ctrl+J/K/X/E queue ops on Chat).
|
|
64
|
-
//
|
|
65
|
-
// Help is bound to Ctrl+G rather than Ctrl+H because most terminals deliver
|
|
66
|
-
// Ctrl+H as ASCII 0x08 (backspace). Bonus: macOS Terminal.app and several
|
|
67
|
-
// other terminals map Ctrl+/ to BEL (0x07), the same byte as Ctrl+G — so this
|
|
68
|
-
// binding also catches the Ctrl+/ keystroke on those terminals "for free".
|
|
69
|
-
// We also accept "/" and "_" as fallbacks for terminals that deliver Ctrl+/
|
|
70
|
-
// as 0x1F or as the literal "/" with ctrl=true (Kitty keyboard protocol).
|
|
71
|
-
const TAB_BY_CTRL_KEY: Record<string, TabId> = {
|
|
72
|
-
a: 1, // ch[a]t
|
|
73
|
-
o: 2, // t[o]ols
|
|
74
|
-
n: 3, // co[n]text
|
|
75
|
-
t: 4, // [t]asks
|
|
76
|
-
e: 5, // thr[e]ads
|
|
77
|
-
s: 6, // [s]chedules
|
|
78
|
-
w: 7, // [w]orkers
|
|
79
|
-
g: 8, // help (also catches Ctrl+/ on terminals that map it to BEL)
|
|
80
|
-
"/": 8, // help (Kitty keyboard protocol)
|
|
81
|
-
_: 8, // help (terminals that send Ctrl+/ as 0x1F)
|
|
82
|
-
};
|
|
83
|
-
|
|
84
36
|
export function App({
|
|
85
37
|
projectDir,
|
|
86
38
|
threadId: resumeThreadId,
|
|
@@ -109,25 +61,9 @@ function AppInner({
|
|
|
109
61
|
threadId: resumeThreadId,
|
|
110
62
|
initialPrompt,
|
|
111
63
|
}: AppInnerProps) {
|
|
112
|
-
const { exit } = useApp();
|
|
113
|
-
const { stdout } = useStdout();
|
|
114
64
|
const { markActivity } = useIdle();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// → `ansiEscapes.clearTerminal`) whenever the dynamic frame is overflowing
|
|
118
|
-
// or transitions out of fullscreen — so as long as the rendered output
|
|
119
|
-
// height stays < `rows` on every render, scrollback is preserved. The
|
|
120
|
-
// chat-tab body and the seven panel boxes use this value to set explicit
|
|
121
|
-
// height/maxHeight constraints.
|
|
122
|
-
const [rows, setRows] = useState(stdout?.rows ?? 24);
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!stdout) return;
|
|
125
|
-
const onResize = () => setRows(stdout.rows ?? 24);
|
|
126
|
-
stdout.on("resize", onResize);
|
|
127
|
-
return () => {
|
|
128
|
-
stdout.off("resize", onResize);
|
|
129
|
-
};
|
|
130
|
-
}, [stdout]);
|
|
65
|
+
const rows = useTerminalRows();
|
|
66
|
+
|
|
131
67
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
132
68
|
const [messagesEpoch, setMessagesEpoch] = useState(0);
|
|
133
69
|
// `clearing` gates new submissions while /clear's async work is in flight.
|
|
@@ -136,429 +72,49 @@ function AppInner({
|
|
|
136
72
|
// overwrites the user bubble it added — the message disappears.
|
|
137
73
|
const [clearing, setClearing] = useState(false);
|
|
138
74
|
const clearingRef = useRef(false);
|
|
139
|
-
const [usage, setUsage] = useState<ContextUsage | null>(null);
|
|
140
75
|
const [inputValue, setInputValue] = useState("");
|
|
141
76
|
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
|
142
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
143
|
-
const [streamingText, setStreamingText] = useState("");
|
|
144
|
-
const [activeToolCalls, setActiveToolCalls] = useState<ToolCallData[]>([]);
|
|
145
|
-
const [streamStartedAt, setStreamStartedAt] = useState<Date | null>(null);
|
|
146
|
-
const [preparingTool, setPreparingTool] = useState<{
|
|
147
|
-
id: string;
|
|
148
|
-
name: string;
|
|
149
|
-
} | null>(null);
|
|
150
|
-
const [ready, setReady] = useState(false);
|
|
151
|
-
const skipSplash = !!(resumeThreadId || initialPrompt);
|
|
152
|
-
const [splashDone, setSplashDone] = useState(skipSplash);
|
|
153
77
|
const [error, setError] = useState<string | null>(null);
|
|
154
|
-
const sessionRef = useRef<ChatSession | null>(null);
|
|
155
|
-
const shuttingDownRef = useRef(false);
|
|
156
78
|
const [activeTab, setActiveTab] = useState<TabId>(1);
|
|
157
79
|
const [workerRunning, setWorkerRunning] = useState(false);
|
|
158
|
-
const [chatTitle, setChatTitle] = useState<string | undefined>(undefined);
|
|
159
|
-
const queueRef = useRef<Array<{ display: string; content: string }>>([]);
|
|
160
|
-
const processingRef = useRef(false);
|
|
161
|
-
const [queuedMessages, setQueuedMessages] = useState<string[]>([]);
|
|
162
|
-
const [selectedQueueIndex, setSelectedQueueIndex] = useState(0);
|
|
163
|
-
|
|
164
|
-
const syncQueue = useCallback(() => {
|
|
165
|
-
const snapshot = queueRef.current.map((e) => e.display);
|
|
166
|
-
setQueuedMessages(snapshot);
|
|
167
|
-
setSelectedQueueIndex((prev) =>
|
|
168
|
-
snapshot.length === 0 ? 0 : Math.min(prev, snapshot.length - 1),
|
|
169
|
-
);
|
|
170
|
-
}, []);
|
|
171
|
-
|
|
172
|
-
// Initialize session
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
let cancelled = false;
|
|
175
|
-
|
|
176
|
-
startChatSession(projectDir, resumeThreadId)
|
|
177
|
-
.then(async (session) => {
|
|
178
|
-
if (cancelled) {
|
|
179
|
-
endChatSession(session);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
sessionRef.current = session;
|
|
183
|
-
|
|
184
|
-
if (resumeThreadId) {
|
|
185
|
-
// Always hydrate on resume so the Tools tab and chat history
|
|
186
|
-
// pick up prior tool_use/tool_result rows from the CSV — even if
|
|
187
|
-
// the thread has no plain message-kind interactions yet.
|
|
188
|
-
const threadData = await getThread(
|
|
189
|
-
session.projectDir,
|
|
190
|
-
session.threadId,
|
|
191
|
-
);
|
|
192
|
-
if (threadData) {
|
|
193
|
-
setMessages(
|
|
194
|
-
restoreMessagesFromInteractions(threadData.interactions),
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
setMessages((prev) => [
|
|
200
|
-
...prev,
|
|
201
|
-
{
|
|
202
|
-
id: msgId(),
|
|
203
|
-
role: "system" as const,
|
|
204
|
-
content:
|
|
205
|
-
"Switch panels with Ctrl+<letter> (^a chat · ^o tools · ^n context · ^t tasks · ^r threads · ^s schedules · ^w workers) — `?` for help. Type /help for commands.",
|
|
206
|
-
timestamp: new Date(),
|
|
207
|
-
},
|
|
208
|
-
]);
|
|
209
|
-
|
|
210
|
-
setReady(true);
|
|
211
|
-
})
|
|
212
|
-
.catch((err) => {
|
|
213
|
-
setError(`Failed to start session: ${err}`);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
return () => {
|
|
217
|
-
cancelled = true;
|
|
218
|
-
// Fire-and-forget safety net: only triggers when unmount happens via a
|
|
219
|
-
// path that didn't go through performShutdown (which nulls sessionRef
|
|
220
|
-
// first). React doesn't await unmount cleanups, so the goodbye lands
|
|
221
|
-
// before mcpx finishes closing — that's fine for non-Ctrl-C paths.
|
|
222
|
-
if (sessionRef.current) {
|
|
223
|
-
const session = sessionRef.current;
|
|
224
|
-
const threadId = session.threadId;
|
|
225
|
-
abortActiveStream(session);
|
|
226
|
-
void endChatSession(session);
|
|
227
|
-
process.stderr.write(
|
|
228
|
-
`\nThread: ${threadId}\nResume with: ${ansi.success}botholomew chat --thread-id ${threadId}${ansi.reset}\nBye!\n`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
}, [projectDir, resumeThreadId]);
|
|
233
|
-
|
|
234
|
-
const performShutdown = useCallback(async () => {
|
|
235
|
-
if (shuttingDownRef.current) {
|
|
236
|
-
// Second Ctrl-C while cleanup is in flight — give the user an escape
|
|
237
|
-
// hatch. 130 = standard SIGINT exit code.
|
|
238
|
-
process.exit(130);
|
|
239
|
-
}
|
|
240
|
-
shuttingDownRef.current = true;
|
|
241
|
-
|
|
242
|
-
const session = sessionRef.current;
|
|
243
|
-
// Null the ref so the useEffect cleanup that runs on Ink unmount becomes
|
|
244
|
-
// a no-op — otherwise it would double-print the goodbye and double-close
|
|
245
|
-
// the mcpx client.
|
|
246
|
-
sessionRef.current = null;
|
|
247
|
-
|
|
248
|
-
if (session) {
|
|
249
|
-
const threadId = session.threadId;
|
|
250
|
-
abortActiveStream(session);
|
|
251
|
-
try {
|
|
252
|
-
await endChatSession(session);
|
|
253
|
-
} catch {
|
|
254
|
-
// Best-effort: the user pressed Ctrl-C, surfacing a stack trace here
|
|
255
|
-
// would just hide the goodbye line.
|
|
256
|
-
}
|
|
257
|
-
process.stderr.write(
|
|
258
|
-
`\nThread: ${threadId}\nResume with: ${ansi.success}botholomew chat --thread-id ${threadId}${ansi.reset}\nBye!\n`,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
exit();
|
|
262
|
-
}, [exit]);
|
|
263
|
-
|
|
264
|
-
// Minimum splash screen duration
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
const timer = setTimeout(() => setSplashDone(true), 2000);
|
|
267
|
-
return () => clearTimeout(timer);
|
|
268
|
-
}, []);
|
|
269
|
-
|
|
270
|
-
// Capture-mode tab auto-cycle. Under VHS/ttyd the Tab key doesn't reliably
|
|
271
|
-
// reach Ink, so a docs tape can't drive the tab tour by keystroke. When
|
|
272
|
-
// BOTHOLOMEW_CAPTURE_TAB_CYCLE is set, schedule timers that walk through
|
|
273
|
-
// every tab so a single recording can show all panels.
|
|
274
|
-
//
|
|
275
|
-
// Format: "dwellMs" or "dwellMs:startDelayMs". The optional start delay
|
|
276
|
-
// lets a tape finish a streamed chat reply before the cycle kicks in.
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
const spec = process.env.BOTHOLOMEW_CAPTURE_TAB_CYCLE;
|
|
279
|
-
if (!spec) return;
|
|
280
|
-
const [dwellRaw, delayRaw] = spec.split(":");
|
|
281
|
-
const dwellMs = Number.parseInt(dwellRaw ?? "", 10) || 2500;
|
|
282
|
-
const startDelayMs = Number.parseInt(delayRaw ?? "", 10) || 0;
|
|
283
|
-
const sequence: TabId[] = [2, 3, 4, 5, 6, 7, 8, 1];
|
|
284
|
-
const timers = sequence.map((tab, i) =>
|
|
285
|
-
setTimeout(() => setActiveTab(tab), startDelayMs + dwellMs * (i + 1)),
|
|
286
|
-
);
|
|
287
|
-
return () => {
|
|
288
|
-
for (const t of timers) clearTimeout(t);
|
|
289
|
-
};
|
|
290
|
-
}, []);
|
|
291
|
-
|
|
292
|
-
// Stable ref for App-level input handler — same pattern as InputBar to
|
|
293
|
-
// prevent Ink's useInput from re-registering stdin listeners on every render.
|
|
294
|
-
const activeTabRef = useRef(activeTab);
|
|
295
|
-
const queuedMessagesRef = useRef(queuedMessages);
|
|
296
|
-
const selectedQueueIndexRef = useRef(selectedQueueIndex);
|
|
297
|
-
activeTabRef.current = activeTab;
|
|
298
|
-
queuedMessagesRef.current = queuedMessages;
|
|
299
|
-
selectedQueueIndexRef.current = selectedQueueIndex;
|
|
300
|
-
|
|
301
|
-
const slashCommandsRef = useRef<SlashCommand[]>([]);
|
|
302
|
-
const inputValueRef = useRef("");
|
|
303
80
|
|
|
304
81
|
const markActivityRef = useRef(markActivity);
|
|
305
82
|
markActivityRef.current = markActivity;
|
|
306
83
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
setActiveTab(tabForKey);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const tab = activeTabRef.current;
|
|
350
|
-
|
|
351
|
-
// Esc on Chat tab while a turn is in flight: steer / interrupt.
|
|
352
|
-
// Calls MessageStream.abort() at the SDK layer; tools already running
|
|
353
|
-
// finish normally, but no further LLM turn is started.
|
|
354
|
-
if (key.escape && tab === 1 && processingRef.current) {
|
|
355
|
-
const session = sessionRef.current;
|
|
356
|
-
if (session) {
|
|
357
|
-
abortActiveStream(session);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Queue manipulation keybindings (only when queue has items on Chat tab)
|
|
363
|
-
const queue = queuedMessagesRef.current;
|
|
364
|
-
if (tab === 1 && queue.length > 0 && key.ctrl) {
|
|
365
|
-
if (input === "j") {
|
|
366
|
-
setSelectedQueueIndex((i) => Math.min(i + 1, queue.length - 1));
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (input === "k") {
|
|
370
|
-
setSelectedQueueIndex((i) => Math.max(i - 1, 0));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (input === "x") {
|
|
374
|
-
queueRef.current.splice(selectedQueueIndexRef.current, 1);
|
|
375
|
-
syncQueue();
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (input === "e") {
|
|
379
|
-
const [msg] = queueRef.current.splice(
|
|
380
|
-
selectedQueueIndexRef.current,
|
|
381
|
-
1,
|
|
382
|
-
);
|
|
383
|
-
syncQueue();
|
|
384
|
-
if (msg) {
|
|
385
|
-
setInputValue(msg.display);
|
|
386
|
-
}
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (tab !== 1) {
|
|
392
|
-
// Escape returns to chat
|
|
393
|
-
if (key.escape) {
|
|
394
|
-
setActiveTab(1);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
[performShutdown, syncQueue],
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
useInput(stableAppHandler);
|
|
403
|
-
|
|
404
|
-
const processQueue = useCallback(async () => {
|
|
405
|
-
if (processingRef.current || !sessionRef.current) return;
|
|
406
|
-
processingRef.current = true;
|
|
407
|
-
|
|
408
|
-
while (queueRef.current.length > 0) {
|
|
409
|
-
const entry = queueRef.current.shift();
|
|
410
|
-
syncQueue();
|
|
411
|
-
if (!entry) break;
|
|
412
|
-
setIsLoading(true);
|
|
413
|
-
setStreamingText("");
|
|
414
|
-
setActiveToolCalls([]);
|
|
415
|
-
setPreparingTool(null);
|
|
416
|
-
setStreamStartedAt(new Date());
|
|
417
|
-
|
|
418
|
-
const userMsg: ChatMessage = {
|
|
419
|
-
id: msgId(),
|
|
420
|
-
role: "user",
|
|
421
|
-
content: entry.display,
|
|
422
|
-
timestamp: new Date(),
|
|
423
|
-
};
|
|
424
|
-
setMessages((prev) => [...prev, userMsg]);
|
|
425
|
-
|
|
426
|
-
let pendingToolCalls: ToolCallData[] = [];
|
|
427
|
-
let currentText = "";
|
|
428
|
-
|
|
429
|
-
const finalizeSegment = () => {
|
|
430
|
-
if (currentText || pendingToolCalls.length > 0) {
|
|
431
|
-
const assistantMsg: ChatMessage = {
|
|
432
|
-
id: msgId(),
|
|
433
|
-
role: "assistant",
|
|
434
|
-
content: currentText,
|
|
435
|
-
timestamp: new Date(),
|
|
436
|
-
toolCalls:
|
|
437
|
-
pendingToolCalls.length > 0 ? [...pendingToolCalls] : undefined,
|
|
438
|
-
};
|
|
439
|
-
setMessages((prev) => [...prev, assistantMsg]);
|
|
440
|
-
currentText = "";
|
|
441
|
-
pendingToolCalls = [];
|
|
442
|
-
setStreamingText("");
|
|
443
|
-
setActiveToolCalls([]);
|
|
444
|
-
setStreamStartedAt(new Date());
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
let lastStreamFlush = 0;
|
|
449
|
-
try {
|
|
450
|
-
await sendMessage(sessionRef.current, entry.content, {
|
|
451
|
-
onToken: (token) => {
|
|
452
|
-
currentText += token;
|
|
453
|
-
const now = Date.now();
|
|
454
|
-
if (now - lastStreamFlush >= 50) {
|
|
455
|
-
setStreamingText(currentText);
|
|
456
|
-
lastStreamFlush = now;
|
|
457
|
-
markActivityRef.current();
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
onToolPreparing: (id, name) => {
|
|
461
|
-
markActivityRef.current();
|
|
462
|
-
setPreparingTool({ id, name });
|
|
463
|
-
},
|
|
464
|
-
onToolStart: (id, name, input) => {
|
|
465
|
-
markActivityRef.current();
|
|
466
|
-
if (currentText) {
|
|
467
|
-
finalizeSegment();
|
|
468
|
-
}
|
|
469
|
-
const tc: ToolCallData = {
|
|
470
|
-
id,
|
|
471
|
-
name,
|
|
472
|
-
input,
|
|
473
|
-
running: true,
|
|
474
|
-
timestamp: new Date(),
|
|
475
|
-
};
|
|
476
|
-
pendingToolCalls = [...pendingToolCalls, tc];
|
|
477
|
-
setActiveToolCalls(pendingToolCalls);
|
|
478
|
-
setPreparingTool(null);
|
|
479
|
-
},
|
|
480
|
-
onToolEnd: (id, _name, output, isError, meta) => {
|
|
481
|
-
markActivityRef.current();
|
|
482
|
-
// Replace the matched entry with a new object so its identity
|
|
483
|
-
// changes (memoized ToolCall children rely on this); other entries
|
|
484
|
-
// keep their reference and skip re-render.
|
|
485
|
-
pendingToolCalls = pendingToolCalls.map((t) =>
|
|
486
|
-
t.id === id
|
|
487
|
-
? {
|
|
488
|
-
...t,
|
|
489
|
-
running: false,
|
|
490
|
-
output,
|
|
491
|
-
isError,
|
|
492
|
-
...(meta?.largeResult
|
|
493
|
-
? { largeResult: meta.largeResult }
|
|
494
|
-
: {}),
|
|
495
|
-
}
|
|
496
|
-
: t,
|
|
497
|
-
);
|
|
498
|
-
setActiveToolCalls(pendingToolCalls);
|
|
499
|
-
},
|
|
500
|
-
onToolNotify: (id, message) => {
|
|
501
|
-
markActivityRef.current();
|
|
502
|
-
let touched = false;
|
|
503
|
-
pendingToolCalls = pendingToolCalls.map((t) => {
|
|
504
|
-
if (t.id !== id) return t;
|
|
505
|
-
touched = true;
|
|
506
|
-
return { ...t, notes: [...(t.notes ?? []), message] };
|
|
507
|
-
});
|
|
508
|
-
if (touched) setActiveToolCalls(pendingToolCalls);
|
|
509
|
-
},
|
|
510
|
-
onUsage: (info) => {
|
|
511
|
-
setUsage(info);
|
|
512
|
-
},
|
|
513
|
-
takeInjections: () => {
|
|
514
|
-
// Drain queued messages into the running turn so the agent sees
|
|
515
|
-
// them on the next LLM call instead of after the whole tool loop.
|
|
516
|
-
// Finalize the in-flight assistant segment first so the new user
|
|
517
|
-
// bubbles render in the right order in the chat view.
|
|
518
|
-
if (queueRef.current.length === 0) return [];
|
|
519
|
-
if (currentText || pendingToolCalls.length > 0) {
|
|
520
|
-
finalizeSegment();
|
|
521
|
-
}
|
|
522
|
-
const drained = queueRef.current.splice(0);
|
|
523
|
-
syncQueue();
|
|
524
|
-
for (const e of drained) {
|
|
525
|
-
const userMsg: ChatMessage = {
|
|
526
|
-
id: msgId(),
|
|
527
|
-
role: "user",
|
|
528
|
-
content: e.display,
|
|
529
|
-
timestamp: new Date(),
|
|
530
|
-
};
|
|
531
|
-
setMessages((prev) => [...prev, userMsg]);
|
|
532
|
-
}
|
|
533
|
-
return drained.map((e) => e.content);
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
if (sessionRef.current?.aborted) {
|
|
538
|
-
currentText += currentText
|
|
539
|
-
? "\n\n_(steered — response interrupted)_"
|
|
540
|
-
: "_(steered — no response)_";
|
|
541
|
-
}
|
|
542
|
-
finalizeSegment();
|
|
543
|
-
} catch (err) {
|
|
544
|
-
const errorMsg: ChatMessage = {
|
|
545
|
-
id: msgId(),
|
|
546
|
-
role: "system",
|
|
547
|
-
content: `Error: ${err}`,
|
|
548
|
-
timestamp: new Date(),
|
|
549
|
-
};
|
|
550
|
-
setMessages((prev) => [...prev, errorMsg]);
|
|
551
|
-
} finally {
|
|
552
|
-
setStreamingText("");
|
|
553
|
-
setActiveToolCalls([]);
|
|
554
|
-
setPreparingTool(null);
|
|
555
|
-
setStreamStartedAt(null);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
setIsLoading(false);
|
|
560
|
-
processingRef.current = false;
|
|
561
|
-
}, [syncQueue]);
|
|
84
|
+
const { sessionRef, ready, splashDone, performShutdown } = useChatSession({
|
|
85
|
+
projectDir,
|
|
86
|
+
resumeThreadId,
|
|
87
|
+
initialPrompt,
|
|
88
|
+
setMessages,
|
|
89
|
+
setError,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const queue = useMessageQueue({
|
|
93
|
+
sessionRef,
|
|
94
|
+
setMessages,
|
|
95
|
+
markActivityRef,
|
|
96
|
+
});
|
|
97
|
+
const {
|
|
98
|
+
queueRef,
|
|
99
|
+
processingRef,
|
|
100
|
+
queuedMessages,
|
|
101
|
+
selectedQueueIndex,
|
|
102
|
+
setSelectedQueueIndex,
|
|
103
|
+
syncQueue,
|
|
104
|
+
processQueue,
|
|
105
|
+
isLoading,
|
|
106
|
+
streamingText,
|
|
107
|
+
activeToolCalls,
|
|
108
|
+
preparingTool,
|
|
109
|
+
streamStartedAt,
|
|
110
|
+
usage,
|
|
111
|
+
setUsage,
|
|
112
|
+
clearStreamingState,
|
|
113
|
+
} = queue;
|
|
114
|
+
|
|
115
|
+
const { chatTitle, setChatTitle } = useChatTitlePolling(ready, sessionRef);
|
|
116
|
+
|
|
117
|
+
useCaptureTabCycle(setActiveTab);
|
|
562
118
|
|
|
563
119
|
// Auto-submit initial prompt once session is ready
|
|
564
120
|
const initialPromptSent = useRef(false);
|
|
@@ -573,184 +129,7 @@ function AppInner({
|
|
|
573
129
|
setInputHistory((prev) => [...prev, initialPrompt]);
|
|
574
130
|
processQueue();
|
|
575
131
|
}
|
|
576
|
-
}, [ready, initialPrompt, processQueue, syncQueue]);
|
|
577
|
-
|
|
578
|
-
// Poll for chat thread title updates
|
|
579
|
-
useEffect(() => {
|
|
580
|
-
if (!ready || !sessionRef.current) return;
|
|
581
|
-
let mounted = true;
|
|
582
|
-
|
|
583
|
-
const refreshTitle = async () => {
|
|
584
|
-
const session = sessionRef.current;
|
|
585
|
-
if (!session) return;
|
|
586
|
-
const result = await getThread(session.projectDir, session.threadId);
|
|
587
|
-
if (mounted && result?.thread.title) {
|
|
588
|
-
setChatTitle(result.thread.title);
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
refreshTitle();
|
|
593
|
-
const interval = setInterval(refreshTitle, 5000);
|
|
594
|
-
return () => {
|
|
595
|
-
mounted = false;
|
|
596
|
-
clearInterval(interval);
|
|
597
|
-
};
|
|
598
|
-
}, [ready]);
|
|
599
|
-
|
|
600
|
-
const handleSubmit = useCallback(
|
|
601
|
-
async (text: string) => {
|
|
602
|
-
const trimmed = text.trim();
|
|
603
|
-
if (!trimmed || !sessionRef.current) return;
|
|
604
|
-
// /clear is mid-flight: don't queue against the old thread id.
|
|
605
|
-
if (clearingRef.current) return;
|
|
606
|
-
|
|
607
|
-
setInputValue("");
|
|
608
|
-
|
|
609
|
-
if (trimmed === "/help") {
|
|
610
|
-
const skills = sessionRef.current.skills;
|
|
611
|
-
const lines: string[] = [
|
|
612
|
-
"For the full keyboard reference, switch to the Help tab (`Ctrl+g`) — this message lists chat commands only.",
|
|
613
|
-
"",
|
|
614
|
-
"Slash commands:",
|
|
615
|
-
" /help Show this message",
|
|
616
|
-
" /skills List available skills",
|
|
617
|
-
" /clear End current thread and start a new one",
|
|
618
|
-
" /exit End the chat session",
|
|
619
|
-
];
|
|
620
|
-
if (skills.size > 0) {
|
|
621
|
-
lines.push("", "Skills:");
|
|
622
|
-
for (const [skillName, skill] of skills) {
|
|
623
|
-
lines.push(
|
|
624
|
-
` /${skillName.padEnd(14)} ${skill.description || "(no description)"}`,
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
lines.push("", "Skills:", " (none — add .md files to skills/)");
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const helpMsg: ChatMessage = {
|
|
632
|
-
id: msgId(),
|
|
633
|
-
role: "system",
|
|
634
|
-
content: lines.join("\n"),
|
|
635
|
-
timestamp: new Date(),
|
|
636
|
-
};
|
|
637
|
-
setMessages((prev) => [...prev, helpMsg]);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (trimmed.startsWith("/")) {
|
|
642
|
-
const skills = sessionRef.current.skills;
|
|
643
|
-
const handled = handleSlashCommand(trimmed, {
|
|
644
|
-
skills,
|
|
645
|
-
addSystemMessage: (content) => {
|
|
646
|
-
const msg: ChatMessage = {
|
|
647
|
-
id: msgId(),
|
|
648
|
-
role: "system",
|
|
649
|
-
content,
|
|
650
|
-
timestamp: new Date(),
|
|
651
|
-
};
|
|
652
|
-
setMessages((prev) => [...prev, msg]);
|
|
653
|
-
},
|
|
654
|
-
queueUserMessage: (content, opts) => {
|
|
655
|
-
setInputHistory((prev) => [...prev, trimmed]);
|
|
656
|
-
queueRef.current.push({
|
|
657
|
-
display: opts?.display ?? content,
|
|
658
|
-
content,
|
|
659
|
-
});
|
|
660
|
-
syncQueue();
|
|
661
|
-
processQueue();
|
|
662
|
-
},
|
|
663
|
-
exit: () => void performShutdown(),
|
|
664
|
-
clearChat: () => {
|
|
665
|
-
const session = sessionRef.current;
|
|
666
|
-
if (!session) return;
|
|
667
|
-
// Drain any queued messages so they don't leak into the new thread.
|
|
668
|
-
queueRef.current.length = 0;
|
|
669
|
-
syncQueue();
|
|
670
|
-
// Abort any in-flight stream synchronously so its callbacks stop
|
|
671
|
-
// firing before we reset UI state. clearChatSession also calls
|
|
672
|
-
// this, but doing it here lets us start the wait-for-quiesce
|
|
673
|
-
// poll below immediately rather than waiting on the
|
|
674
|
-
// createThread/endThread round trip first.
|
|
675
|
-
abortActiveStream(session);
|
|
676
|
-
// Block new submissions until the new thread id is in place —
|
|
677
|
-
// otherwise the user's first post-/clear message races the
|
|
678
|
-
// async createThread, runs against the old thread id, and is
|
|
679
|
-
// then wiped by setMessages([sys]) below.
|
|
680
|
-
clearingRef.current = true;
|
|
681
|
-
setClearing(true);
|
|
682
|
-
void (async () => {
|
|
683
|
-
// Wait for any in-flight processQueue iteration to finish so
|
|
684
|
-
// its trailing `finalizeSegment` can't race our state reset
|
|
685
|
-
// and re-add the previous thread's assistant message after
|
|
686
|
-
// the UI has been cleared. (Issue #190.)
|
|
687
|
-
while (processingRef.current) {
|
|
688
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
689
|
-
}
|
|
690
|
-
try {
|
|
691
|
-
const { previousThreadId, newThreadId } =
|
|
692
|
-
await clearChatSession(session);
|
|
693
|
-
// Ink's <Static> writes messages to terminal scrollback and
|
|
694
|
-
// can't un-write them, so setMessages alone leaves the old
|
|
695
|
-
// lines visible. Clear the terminal (including scrollback)
|
|
696
|
-
// and bump the epoch key on <Static> to force a fresh mount.
|
|
697
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
698
|
-
setMessages([
|
|
699
|
-
{
|
|
700
|
-
id: msgId(),
|
|
701
|
-
role: "system",
|
|
702
|
-
content: `Started a new chat thread (${newThreadId}). Previous thread saved — resume with: botholomew chat --thread-id ${previousThreadId}`,
|
|
703
|
-
timestamp: new Date(),
|
|
704
|
-
},
|
|
705
|
-
]);
|
|
706
|
-
setMessagesEpoch((n) => n + 1);
|
|
707
|
-
setChatTitle(undefined);
|
|
708
|
-
setStreamingText("");
|
|
709
|
-
setActiveToolCalls([]);
|
|
710
|
-
setPreparingTool(null);
|
|
711
|
-
setStreamStartedAt(null);
|
|
712
|
-
setUsage(null);
|
|
713
|
-
} catch (err) {
|
|
714
|
-
setMessages((prev) => [
|
|
715
|
-
...prev,
|
|
716
|
-
{
|
|
717
|
-
id: msgId(),
|
|
718
|
-
role: "system",
|
|
719
|
-
content: `Failed to clear chat: ${err}`,
|
|
720
|
-
timestamp: new Date(),
|
|
721
|
-
},
|
|
722
|
-
]);
|
|
723
|
-
} finally {
|
|
724
|
-
clearingRef.current = false;
|
|
725
|
-
setClearing(false);
|
|
726
|
-
}
|
|
727
|
-
})();
|
|
728
|
-
},
|
|
729
|
-
});
|
|
730
|
-
if (handled) return;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
setInputHistory((prev) => [...prev, trimmed]);
|
|
734
|
-
queueRef.current.push({ display: trimmed, content: trimmed });
|
|
735
|
-
syncQueue();
|
|
736
|
-
processQueue();
|
|
737
|
-
},
|
|
738
|
-
[performShutdown, processQueue, syncQueue],
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
const sessionDbPath = sessionRef.current?.dbPath;
|
|
742
|
-
const inputBarHeader = useMemo(
|
|
743
|
-
() =>
|
|
744
|
-
sessionDbPath ? (
|
|
745
|
-
<StatusBar
|
|
746
|
-
projectDir={projectDir}
|
|
747
|
-
dbPath={sessionDbPath}
|
|
748
|
-
chatTitle={chatTitle}
|
|
749
|
-
onWorkerStatusChange={setWorkerRunning}
|
|
750
|
-
/>
|
|
751
|
-
) : null,
|
|
752
|
-
[projectDir, sessionDbPath, chatTitle],
|
|
753
|
-
);
|
|
132
|
+
}, [ready, initialPrompt, processQueue, syncQueue, queueRef]);
|
|
754
133
|
|
|
755
134
|
const sessionSkills = ready ? sessionRef.current?.skills : undefined;
|
|
756
135
|
const slashCommands = useMemo<SlashCommand[]>(() => {
|
|
@@ -767,9 +146,60 @@ function AppInner({
|
|
|
767
146
|
return buildSlashCommands(BUILTIN_SLASH_COMMANDS, skillList);
|
|
768
147
|
}, [sessionSkills]);
|
|
769
148
|
|
|
149
|
+
const slashCommandsRef = useRef<SlashCommand[]>([]);
|
|
150
|
+
const inputValueRef = useRef("");
|
|
770
151
|
slashCommandsRef.current = slashCommands;
|
|
771
152
|
inputValueRef.current = inputValue;
|
|
772
153
|
|
|
154
|
+
useAppKeybindings({
|
|
155
|
+
activeTab,
|
|
156
|
+
setActiveTab,
|
|
157
|
+
performShutdown,
|
|
158
|
+
sessionRef,
|
|
159
|
+
processingRef,
|
|
160
|
+
queueRef,
|
|
161
|
+
queuedMessages,
|
|
162
|
+
selectedQueueIndex,
|
|
163
|
+
setSelectedQueueIndex,
|
|
164
|
+
setInputValue,
|
|
165
|
+
syncQueue,
|
|
166
|
+
slashCommandsRef,
|
|
167
|
+
inputValueRef,
|
|
168
|
+
markActivityRef,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const handleSubmit = useChatSubmit({
|
|
172
|
+
sessionRef,
|
|
173
|
+
queueRef,
|
|
174
|
+
processingRef,
|
|
175
|
+
clearingRef,
|
|
176
|
+
syncQueue,
|
|
177
|
+
processQueue,
|
|
178
|
+
performShutdown,
|
|
179
|
+
clearStreamingState,
|
|
180
|
+
setMessages,
|
|
181
|
+
setInputValue,
|
|
182
|
+
setInputHistory,
|
|
183
|
+
setMessagesEpoch,
|
|
184
|
+
setChatTitle,
|
|
185
|
+
setClearing,
|
|
186
|
+
setUsage,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const sessionDbPath = sessionRef.current?.dbPath;
|
|
190
|
+
const inputBarHeader = useMemo(
|
|
191
|
+
() =>
|
|
192
|
+
sessionDbPath ? (
|
|
193
|
+
<StatusBar
|
|
194
|
+
projectDir={projectDir}
|
|
195
|
+
dbPath={sessionDbPath}
|
|
196
|
+
chatTitle={chatTitle}
|
|
197
|
+
onWorkerStatusChange={setWorkerRunning}
|
|
198
|
+
/>
|
|
199
|
+
) : null,
|
|
200
|
+
[projectDir, sessionDbPath, chatTitle],
|
|
201
|
+
);
|
|
202
|
+
|
|
773
203
|
const allToolCalls = useMemo(
|
|
774
204
|
() => messages.flatMap((m) => m.toolCalls ?? []),
|
|
775
205
|
[messages],
|
|
@@ -797,9 +227,7 @@ function AppInner({
|
|
|
797
227
|
);
|
|
798
228
|
}
|
|
799
229
|
|
|
800
|
-
const _dbPath = sessionRef.current.dbPath;
|
|
801
230
|
const threadId = sessionRef.current.threadId;
|
|
802
|
-
|
|
803
231
|
const panelHeight = Math.max(1, rows - FOOTER_RESERVE);
|
|
804
232
|
const onChatTab = activeTab === 1;
|
|
805
233
|
|
|
@@ -826,22 +254,13 @@ function AppInner({
|
|
|
826
254
|
{(msg) => <MessageBubble key={msg.id} message={msg} />}
|
|
827
255
|
</Static>
|
|
828
256
|
|
|
829
|
-
{/*
|
|
830
|
-
remount cycles. display="none" hides inactive panels from
|
|
831
|
-
layout without destroying them.
|
|
832
|
-
|
|
833
|
-
Chat tab: `maxHeight={panelHeight}` (not `height`) so the box
|
|
257
|
+
{/* Chat tab body: `maxHeight={panelHeight}` (not `height`) so the box
|
|
834
258
|
shrinks to its content when streaming is short or absent. When
|
|
835
259
|
streaming overflows, the box stops at `panelHeight`;
|
|
836
260
|
`justifyContent="flex-end"` + `overflow="hidden"` clip the *top*
|
|
837
261
|
so the most-recent tokens stay visible above the input bar.
|
|
838
262
|
The frame stays strictly below `rows`, so Ink never wipes
|
|
839
|
-
scrollback during a turn.
|
|
840
|
-
|
|
841
|
-
Other tabs: `flexGrow={1}` fills the root (which is pinned to
|
|
842
|
-
`rows` on those tabs) minus the footer's actual height, so the
|
|
843
|
-
panel always reaches the top of the viewport — no scrollback
|
|
844
|
-
leak above the panel regardless of footer height. */}
|
|
263
|
+
scrollback during a turn. */}
|
|
845
264
|
<Box
|
|
846
265
|
display={onChatTab ? "flex" : "none"}
|
|
847
266
|
flexDirection="column"
|
|
@@ -857,71 +276,15 @@ function AppInner({
|
|
|
857
276
|
streamStartedAt={streamStartedAt}
|
|
858
277
|
/>
|
|
859
278
|
</Box>
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
display={activeTab === 3 ? "flex" : "none"}
|
|
870
|
-
flexDirection="column"
|
|
871
|
-
flexGrow={1}
|
|
872
|
-
overflow="hidden"
|
|
873
|
-
>
|
|
874
|
-
<ContextPanel projectDir={projectDir} isActive={activeTab === 3} />
|
|
875
|
-
</Box>
|
|
876
|
-
<Box
|
|
877
|
-
display={activeTab === 4 ? "flex" : "none"}
|
|
878
|
-
flexDirection="column"
|
|
879
|
-
flexGrow={1}
|
|
880
|
-
overflow="hidden"
|
|
881
|
-
>
|
|
882
|
-
<TaskPanel projectDir={projectDir} isActive={activeTab === 4} />
|
|
883
|
-
</Box>
|
|
884
|
-
<Box
|
|
885
|
-
display={activeTab === 5 ? "flex" : "none"}
|
|
886
|
-
flexDirection="column"
|
|
887
|
-
flexGrow={1}
|
|
888
|
-
overflow="hidden"
|
|
889
|
-
>
|
|
890
|
-
<ThreadPanel
|
|
891
|
-
projectDir={projectDir}
|
|
892
|
-
activeThreadId={threadId}
|
|
893
|
-
isActive={activeTab === 5}
|
|
894
|
-
/>
|
|
895
|
-
</Box>
|
|
896
|
-
<Box
|
|
897
|
-
display={activeTab === 6 ? "flex" : "none"}
|
|
898
|
-
flexDirection="column"
|
|
899
|
-
flexGrow={1}
|
|
900
|
-
overflow="hidden"
|
|
901
|
-
>
|
|
902
|
-
<SchedulePanel projectDir={projectDir} isActive={activeTab === 6} />
|
|
903
|
-
</Box>
|
|
904
|
-
<Box
|
|
905
|
-
display={activeTab === 7 ? "flex" : "none"}
|
|
906
|
-
flexDirection="column"
|
|
907
|
-
flexGrow={1}
|
|
908
|
-
overflow="hidden"
|
|
909
|
-
>
|
|
910
|
-
<WorkerPanel projectDir={projectDir} isActive={activeTab === 7} />
|
|
911
|
-
</Box>
|
|
912
|
-
<Box
|
|
913
|
-
display={activeTab === 8 ? "flex" : "none"}
|
|
914
|
-
flexDirection="column"
|
|
915
|
-
flexGrow={1}
|
|
916
|
-
overflow="hidden"
|
|
917
|
-
>
|
|
918
|
-
<HelpPanel
|
|
919
|
-
projectDir={projectDir}
|
|
920
|
-
threadId={threadId}
|
|
921
|
-
workerRunning={workerRunning}
|
|
922
|
-
usage={usage}
|
|
923
|
-
/>
|
|
924
|
-
</Box>
|
|
279
|
+
|
|
280
|
+
<TabPanels
|
|
281
|
+
activeTab={activeTab}
|
|
282
|
+
projectDir={projectDir}
|
|
283
|
+
threadId={threadId}
|
|
284
|
+
allToolCalls={allToolCalls}
|
|
285
|
+
workerRunning={workerRunning}
|
|
286
|
+
usage={usage}
|
|
287
|
+
/>
|
|
925
288
|
|
|
926
289
|
{/* Queued messages (only on Chat tab) */}
|
|
927
290
|
{activeTab === 1 && queuedMessages.length > 0 && (
|