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.
Files changed (70) hide show
  1. package/README.md +9 -0
  2. package/package.json +3 -1
  3. package/src/chat/agent.ts +87 -23
  4. package/src/chat/session.ts +19 -6
  5. package/src/cli.ts +2 -0
  6. package/src/commands/chat.ts +5 -2
  7. package/src/commands/context.ts +91 -35
  8. package/src/commands/thread.ts +180 -0
  9. package/src/config/schemas.ts +3 -1
  10. package/src/context/embedder.ts +0 -3
  11. package/src/daemon/context.ts +146 -0
  12. package/src/daemon/large-results.ts +100 -0
  13. package/src/daemon/llm.ts +45 -19
  14. package/src/daemon/prompt.ts +1 -6
  15. package/src/daemon/tick.ts +9 -0
  16. package/src/db/sql/4-unique_context_path.sql +1 -0
  17. package/src/db/threads.ts +17 -0
  18. package/src/init/templates.ts +2 -1
  19. package/src/tools/context/read-large-result.ts +33 -0
  20. package/src/tools/context/search.ts +2 -0
  21. package/src/tools/context/update-beliefs.ts +2 -0
  22. package/src/tools/context/update-goals.ts +2 -0
  23. package/src/tools/dir/create.ts +3 -2
  24. package/src/tools/dir/list.ts +2 -1
  25. package/src/tools/dir/size.ts +2 -1
  26. package/src/tools/dir/tree.ts +3 -2
  27. package/src/tools/file/copy.ts +12 -3
  28. package/src/tools/file/count-lines.ts +2 -1
  29. package/src/tools/file/delete.ts +3 -2
  30. package/src/tools/file/edit.ts +3 -2
  31. package/src/tools/file/exists.ts +2 -1
  32. package/src/tools/file/info.ts +2 -0
  33. package/src/tools/file/move.ts +12 -3
  34. package/src/tools/file/read.ts +2 -1
  35. package/src/tools/file/write.ts +5 -4
  36. package/src/tools/mcp/exec.ts +70 -3
  37. package/src/tools/mcp/info.ts +8 -0
  38. package/src/tools/mcp/list-tools.ts +18 -6
  39. package/src/tools/mcp/search.ts +38 -10
  40. package/src/tools/registry.ts +4 -0
  41. package/src/tools/schedule/create.ts +2 -0
  42. package/src/tools/schedule/list.ts +2 -0
  43. package/src/tools/search/grep.ts +3 -2
  44. package/src/tools/search/semantic.ts +2 -0
  45. package/src/tools/task/complete.ts +2 -0
  46. package/src/tools/task/create.ts +17 -4
  47. package/src/tools/task/fail.ts +2 -0
  48. package/src/tools/task/list.ts +2 -0
  49. package/src/tools/task/update.ts +87 -0
  50. package/src/tools/task/view.ts +3 -1
  51. package/src/tools/task/wait.ts +2 -0
  52. package/src/tools/thread/list.ts +2 -0
  53. package/src/tools/thread/view.ts +3 -1
  54. package/src/tools/tool.ts +7 -3
  55. package/src/tui/App.tsx +323 -78
  56. package/src/tui/components/ContextPanel.tsx +415 -0
  57. package/src/tui/components/Divider.tsx +14 -0
  58. package/src/tui/components/HelpPanel.tsx +166 -0
  59. package/src/tui/components/InputBar.tsx +157 -47
  60. package/src/tui/components/Logo.tsx +79 -0
  61. package/src/tui/components/MessageList.tsx +50 -23
  62. package/src/tui/components/QueuePanel.tsx +57 -0
  63. package/src/tui/components/StatusBar.tsx +21 -9
  64. package/src/tui/components/TabBar.tsx +40 -0
  65. package/src/tui/components/TaskPanel.tsx +409 -0
  66. package/src/tui/components/ThreadPanel.tsx +541 -0
  67. package/src/tui/components/ToolCall.tsx +68 -5
  68. package/src/tui/components/ToolPanel.tsx +295 -281
  69. package/src/tui/theme.ts +75 -0
  70. 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({ projectDir, threadId: resumeThreadId }: AppProps) {
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 [toolPanelOpen, setToolPanelOpen] = useState(false);
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: "Type /help for keyboard shortcuts and commands.",
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: \x1b[32mbotholomew chat --thread-id ${threadId}\x1b[0m\n`,
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
- setStreamingText(currentText);
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 = { name, input, running: true };
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: (name, output) => {
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
- "Keyboard shortcuts:",
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 flexDirection="column" padding={1}>
323
- <Text dimColor>Starting chat session...</Text>
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
- <MessageList
331
- messages={messages}
332
- streamingText={streamingText}
333
- isLoading={isLoading}
334
- activeToolCalls={activeToolCalls}
335
- />
336
- {toolPanelOpen && allToolCalls.length > 0 && (
337
- <ToolPanel
338
- toolCalls={allToolCalls}
339
- onClose={() => setToolPanelOpen(false)}
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={toolPanelOpen}
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
  }