@bluehawks/cli 1.0.37 → 1.0.39

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/src/cli/app.tsx CHANGED
@@ -3,88 +3,18 @@
3
3
  */
4
4
 
5
5
  import React, { useState, useCallback, useEffect } from 'react';
6
- import { Text, Box, useInput, useApp } from 'ink';
7
- import Spinner from 'ink-spinner';
8
- import TextInput from 'ink-text-input';
6
+ import { useInput, useApp } from 'ink';
9
7
  import { APIClient } from '../core/api/client.js';
10
8
  import { Orchestrator } from '../core/agents/orchestrator.js';
11
9
  import { ToolExecutor, toolRegistry, registerAllTools } from '../core/tools/index.js';
12
10
  import { SessionManager } from '../core/session/manager.js';
13
11
  import { commandRegistry, type CommandContext } from './commands/index.js';
14
- import { CLI_NAME, CLI_VERSION, COLORS } from '../config/constants.js';
15
12
  import { hooksManager } from '../core/hooks/index.js';
16
13
  import type { SessionStartInput, StopInput } from '../core/hooks/types.js';
17
- import * as path from 'path';
18
- import * as os from 'os';
19
-
20
- // UI Components
21
- const Branding = () => (
22
- <Box flexDirection="column" marginY={1}>
23
- <Text color="#3B82F6">{"██████╗ ██╗ ██╗ ██╗███████╗██╗ ██╗ █████╗ ██╗ ██╗██╗ ██╗███████╗"}</Text>
24
- <Text color="#6366F1">{"██╔══██╗██║ ██║ ██║██╔════╝██║ ██║██╔══██╗██║ ██║██║ ██╔╝██╔════╝"}</Text>
25
- <Text color="#8B5CF6">{"██████╔╝██║ ██║ ██║█████╗ ███████║███████║██║ █╗ ██║█████╔╝ ███████╗"}</Text>
26
- <Text color="#A855F7">{"██╔══██╗██║ ██║ ██║██╔══╝ ██╔══██║██╔══██║██║███╗██║██╔═██╗ ╚════██║"}</Text>
27
- <Text color="#D946EF">{"██████╔╝███████╗╚██████╔╝███████╗██║ ██║██║ ██║╚███╔███╔╝██║ ██╗███████║"}</Text>
28
- <Text color="#EC4899">{"╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝"}</Text>
29
- </Box>
30
- );
31
-
32
- const HeaderBox: React.FC<{ version: string; model: string; projectPath: string }> = ({ version, model, projectPath }) => {
33
- const relativePath = projectPath.startsWith(os.homedir())
34
- ? '~/' + path.relative(os.homedir(), projectPath)
35
- : projectPath;
36
14
 
37
- return (
38
- <Box borderStyle="round" borderColor="#3B82F6" paddingX={2} paddingY={0} width={80} marginBottom={1}>
39
- <Box flexDirection="column">
40
- <Box>
41
- <Text bold color="#3B82F6">{">_ "}{CLI_NAME} </Text>
42
- <Text color="gray">(v{version})</Text>
43
- </Box>
44
- <Box marginTop={0}>
45
- <Text color="gray">Model: </Text>
46
- <Text color="white">{model}</Text>
47
- </Box>
48
- <Box>
49
- <Text color="gray">Path: </Text>
50
- <Text color="white">{relativePath}</Text>
51
- </Box>
52
- </Box>
53
- </Box>
54
- );
55
- };
56
-
57
- const Tips = () => {
58
- const tipsList = [
59
- "Start a fresh idea with /clear or /new; the previous session stays available in history.",
60
- "Use /yolo to auto-approve all tool executions for maximum speed.",
61
- "Need help? Type /help to see all available commands and shortcuts.",
62
- "Bluehawks can read your codebase, run tests, and even commit changes.",
63
- "Working on a specific repository? Bluehawks understands your local context automatically."
64
- ];
65
- const [tip] = useState(() => tipsList[Math.floor(Math.random() * tipsList.length)]);
66
-
67
- return (
68
- <Box marginTop={1} marginBottom={1}>
69
- <Text color="gray">Tips: {tip}</Text>
70
- </Box>
71
- );
72
- };
73
-
74
- const StatusBar: React.FC<{ isYoloMode: boolean }> = ({ isYoloMode }) => (
75
- <Box marginTop={1} paddingX={1} borderStyle="single" borderTop={true} borderBottom={false} borderLeft={false} borderRight={false} borderColor="gray">
76
- <Box flexGrow={1}>
77
- <Text color="gray">Type </Text>
78
- <Text color="cyan" bold>/help</Text>
79
- <Text color="gray"> for commands. </Text>
80
- </Box>
81
- {isYoloMode && (
82
- <Box>
83
- <Text color="#F59E0B" bold>⚡ YOLO MODE ACTIVE </Text>
84
- </Box>
85
- )}
86
- </Box>
87
- );
15
+ // UI Components & Types
16
+ import { Layout } from './components/Layout.js';
17
+ import { SystemEvent, ToolActivity } from './components/types.js';
88
18
 
89
19
  interface AppProps {
90
20
  initialPrompt?: string;
@@ -102,6 +32,8 @@ interface MessageDisplay {
102
32
  export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = false, onExit }) => {
103
33
  const { exit } = useApp();
104
34
  const exitCalledRef = React.useRef(false);
35
+
36
+ // Core State
105
37
  const [input, setInput] = useState('');
106
38
  const [messages, setMessages] = useState<MessageDisplay[]>([]);
107
39
  const [isProcessing, setIsProcessing] = useState(false);
@@ -114,7 +46,35 @@ export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = fals
114
46
  } | null>(null);
115
47
  const [isYoloMode, setIsYoloMode] = useState(yoloMode);
116
48
 
117
- // Initialize components
49
+ // Dashboard State
50
+ const [systemEvents, setSystemEvents] = useState<SystemEvent[]>([]);
51
+ const [toolHistory, setToolHistory] = useState<ToolActivity[]>([]);
52
+ const [tps, setTps] = useState(0);
53
+ const [contextUsage, setContextUsage] = useState(0);
54
+ const [totalTokens, setTotalTokens] = useState(0);
55
+
56
+ // Initialize Helpers
57
+ const addSystemEvent = useCallback((message: string, type: SystemEvent['type'] = 'info') => {
58
+ const event: SystemEvent = {
59
+ id: Math.random().toString(36).substring(7),
60
+ timestamp: new Date().toISOString(),
61
+ message,
62
+ type,
63
+ };
64
+ setSystemEvents(prev => [...prev, event]);
65
+ }, []);
66
+
67
+ const addToolActivity = useCallback((toolName: string, status: ToolActivity['status']) => {
68
+ const activity: ToolActivity = {
69
+ id: Math.random().toString(36).substring(7),
70
+ toolName,
71
+ status,
72
+ timestamp: Date.now(),
73
+ };
74
+ setToolHistory(prev => [...prev.slice(-24), activity]); // Keep last 25
75
+ }, []);
76
+
77
+ // Initialize Managers
118
78
  const [apiClient] = useState(() => new APIClient({ apiKey }));
119
79
  const [toolExecutor] = useState(() => {
120
80
  const executor = new ToolExecutor({
@@ -134,38 +94,46 @@ export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = fals
134
94
  () => new SessionManager(process.cwd(), apiClient.currentModel)
135
95
  );
136
96
 
137
- // Set up approval handler and lifecycle hooks
97
+ // Lifecycle: Init & System Events
138
98
  useEffect(() => {
139
- toolExecutor.setApprovalHandler(async (toolName, args) => {
140
- if (isYoloMode) return true;
99
+ addSystemEvent('System initialized. Neural interface active.', 'success');
141
100
 
101
+ // Initialize approval handler
102
+ toolExecutor.setApprovalHandler(async (toolName, args) => {
103
+ if (isYoloMode) {
104
+ addSystemEvent(`Auto-approving tool: ${toolName}`, 'warning');
105
+ return true;
106
+ }
107
+ addSystemEvent(`Requesting approval for: ${toolName}`, 'warning');
142
108
  return new Promise<boolean>((resolve) => {
143
109
  setPendingApproval({ toolName, args, resolve });
144
110
  });
145
111
  });
146
112
 
147
- // Initialize orchestrator and trigger SessionStart hook
148
- orchestrator.initialize().then(async () => {
149
- const hookContext: SessionStartInput = {
150
- sessionId: sessionManager.getSessionId(),
151
- projectPath: process.cwd(),
152
- model: apiClient.currentModel,
153
- timestamp: new Date().toISOString(),
154
- cwd: process.cwd(),
155
- };
156
- await hooksManager.execute('SessionStart', hookContext);
157
- }).catch((err) => {
158
- setMessages((prev) => [...prev, { role: 'error', content: `Init error: ${err}` }]);
159
- });
113
+ const init = async () => {
114
+ try {
115
+ const hookContext: SessionStartInput = {
116
+ sessionId: sessionManager.getSessionId(),
117
+ projectPath: process.cwd(),
118
+ model: apiClient.currentModel,
119
+ timestamp: new Date().toISOString(),
120
+ cwd: process.cwd(),
121
+ };
122
+ await hooksManager.execute('SessionStart', hookContext);
123
+ addSystemEvent(`Session started: ${sessionManager.getSessionId()}`, 'info');
124
+ } catch (err) {
125
+ const appId = String(err);
126
+ addSystemEvent(`Init Error: ${appId}`, 'error');
127
+ }
128
+ };
129
+ init();
160
130
 
161
- // Cleanup: trigger Stop hook and auto-save session on exit
162
131
  return () => {
163
132
  if (!exitCalledRef.current && onExit) {
164
133
  exitCalledRef.current = true;
165
134
  const stats = sessionManager.getStats();
166
135
  onExit(stats, sessionManager.getSessionId());
167
136
  }
168
-
169
137
  const cleanup = async () => {
170
138
  const stats = sessionManager.getStats();
171
139
  const stopContext: StopInput = {
@@ -181,168 +149,157 @@ export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = fals
181
149
  };
182
150
  cleanup().catch(console.error);
183
151
  };
184
- }, [toolExecutor, isYoloMode, orchestrator, sessionManager, apiClient, onExit]);
152
+ }, [toolExecutor, isYoloMode, orchestrator, sessionManager, apiClient, onExit, addSystemEvent]);
185
153
 
186
- // Handle initial prompt
154
+ // TPS Simulation (Mock for now, can be real later)
187
155
  useEffect(() => {
188
- if (initialPrompt) {
189
- handleSubmit(initialPrompt);
156
+ let interval: NodeJS.Timeout;
157
+ if (isProcessing) {
158
+ interval = setInterval(() => {
159
+ setTps(Math.random() * 15 + 30); // Random 30-45 t/s
160
+ }, 500);
161
+ } else {
162
+ setTps(0);
190
163
  }
191
- }, []);
192
-
193
- const handleSubmit = useCallback(
194
- async (value: string) => {
195
- const trimmed = value.trim();
196
- if (!trimmed || isProcessing) return;
197
-
198
- // Check for slash commands
199
- if (commandRegistry.isCommand(trimmed)) {
200
- const context: CommandContext = {
201
- sessionManager,
202
- orchestrator,
203
- toolRegistry,
204
- onExit: () => {
205
- if (!exitCalledRef.current && onExit) {
206
- exitCalledRef.current = true;
207
- const stats = sessionManager.getStats();
208
- onExit(stats, sessionManager.getSessionId());
209
- }
210
- exit();
211
- },
212
- };
164
+ return () => {
165
+ if (interval) clearInterval(interval);
166
+ };
167
+ }, [isProcessing]);
213
168
 
214
- const result = await commandRegistry.execute(trimmed, context);
215
- if (result) {
216
- setMessages((prev) => [...prev, { role: 'system', content: result }]);
217
- }
218
- setInput('');
219
- return;
220
- }
169
+ // Initial Prompt
170
+ useEffect(() => {
171
+ if (initialPrompt) handleSubmit(initialPrompt);
172
+ }, []);
221
173
 
222
- // Check for YOLO toggle
223
- if (trimmed.toLowerCase() === '/yolo') {
224
- setIsYoloMode((prev) => {
225
- const newValue = !prev;
226
- toolExecutor.setApprovalMode(newValue ? 'never' : 'unsafe-only');
227
- setMessages((prev) => [
228
- ...prev,
229
- {
230
- role: 'system',
231
- content: newValue
232
- ? '⚡ YOLO mode enabled.'
233
- : '🛡️ YOLO mode disabled.',
234
- },
235
- ]);
236
- return newValue;
237
- });
238
- setInput('');
239
- return;
174
+ // Main Handler
175
+ const handleSubmit = useCallback(async (value: string) => {
176
+ const trimmed = value.trim();
177
+ if (!trimmed || isProcessing) return;
178
+
179
+ // Commands
180
+ if (commandRegistry.isCommand(trimmed)) {
181
+ addSystemEvent(`Executing command: ${trimmed}`, 'info');
182
+ const context: CommandContext = {
183
+ sessionManager,
184
+ orchestrator,
185
+ toolRegistry,
186
+ onExit: () => {
187
+ if (!exitCalledRef.current && onExit) {
188
+ exitCalledRef.current = true;
189
+ const stats = sessionManager.getStats();
190
+ onExit(stats, sessionManager.getSessionId());
191
+ }
192
+ exit();
193
+ },
194
+ };
195
+ const result = await commandRegistry.execute(trimmed, context);
196
+ if (result) {
197
+ setMessages((prev) => [...prev, { role: 'system', content: result }]);
240
198
  }
241
-
242
- // Add user message
243
- setMessages((prev) => [...prev, { role: 'user', content: trimmed }]);
244
199
  setInput('');
245
- setIsProcessing(true);
246
- setStreamingContent('');
200
+ return;
201
+ }
247
202
 
248
- let thinkBuffer = ''; // Buffer for tracking think blocks
249
- let inThinkBlock = false;
203
+ // Yolo Toggle
204
+ if (trimmed.toLowerCase() === '/yolo') {
205
+ setIsYoloMode((prev) => {
206
+ const newValue = !prev;
207
+ toolExecutor.setApprovalMode(newValue ? 'never' : 'unsafe-only');
208
+ addSystemEvent(`YOLO Mode ${newValue ? 'ENABLED' : 'DISABLED'}`, newValue ? 'warning' : 'info');
209
+ return newValue;
210
+ });
211
+ setInput('');
212
+ return;
213
+ }
250
214
 
251
- try {
252
- const response = await orchestrator.chat(trimmed, [], {
253
- onChunk: (chunk) => {
254
- // Filter out <think>...</think> blocks for cleaner output
255
- const fullText = thinkBuffer + chunk;
256
- thinkBuffer = '';
257
-
258
- let output = '';
259
- let i = 0;
260
- while (i < fullText.length) {
261
- if (!inThinkBlock) {
262
- const thinkStart = fullText.indexOf('<think>', i);
263
- if (thinkStart === -1) {
264
- output += fullText.substring(i);
265
- break;
266
- } else {
267
- output += fullText.substring(i, thinkStart);
268
- inThinkBlock = true;
269
- i = thinkStart + 7;
270
- }
215
+ // Chat Loop
216
+ setMessages((prev) => [...prev, { role: 'user', content: trimmed }]);
217
+ setInput('');
218
+ setIsProcessing(true);
219
+ setStreamingContent('');
220
+ addSystemEvent('Processing user intput...', 'info');
221
+
222
+ let thinkBuffer = '';
223
+ let inThinkBlock = false;
224
+
225
+ try {
226
+ const response = await orchestrator.chat(trimmed, [], {
227
+ onChunk: (chunk) => {
228
+ // Think Tag Filtering
229
+ const fullText = thinkBuffer + chunk;
230
+ thinkBuffer = '';
231
+ let output = '';
232
+ let i = 0;
233
+ while (i < fullText.length) {
234
+ if (!inThinkBlock) {
235
+ const thinkStart = fullText.indexOf('<think>', i);
236
+ if (thinkStart === -1) {
237
+ output += fullText.substring(i);
238
+ break;
271
239
  } else {
272
- const thinkEnd = fullText.indexOf('</think>', i);
273
- if (thinkEnd === -1) {
274
- // Think block continues, buffer the rest
275
- thinkBuffer = fullText.substring(i);
276
- break;
277
- } else {
278
- inThinkBlock = false;
279
- i = thinkEnd + 8;
280
- }
240
+ output += fullText.substring(i, thinkStart);
241
+ inThinkBlock = true;
242
+ i = thinkStart + 7;
243
+ }
244
+ } else {
245
+ const thinkEnd = fullText.indexOf('</think>', i);
246
+ if (thinkEnd === -1) {
247
+ thinkBuffer = fullText.substring(i);
248
+ break;
249
+ } else {
250
+ inThinkBlock = false;
251
+ i = thinkEnd + 8;
281
252
  }
282
253
  }
283
-
284
- // Only update streaming content if we have actual output
285
- if (output) {
286
- setStreamingContent((prev) => prev + output);
287
- }
288
- },
289
- onToolStart: (name, args?: Record<string, unknown>) => {
290
- setCurrentTool(name);
291
- setMessages((prev) => [
292
- ...prev,
293
- {
294
- role: 'tool',
295
- content: JSON.stringify({ type: 'tool_start', name, args })
296
- },
297
- ]);
298
- },
299
- onToolEnd: (name, result) => {
300
- setCurrentTool(null);
301
- setMessages((prev) => [
302
- ...prev,
303
- {
304
- role: 'tool',
305
- content: JSON.stringify({ type: 'tool_end', name, result })
306
- },
307
- ]);
308
- },
309
- });
310
-
311
- // Add final response
312
- if (response.content) {
313
- const cleanContent = response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
314
- if (cleanContent) {
315
- setMessages((prev) => [...prev, { role: 'assistant', content: cleanContent }]);
316
254
  }
255
+ if (output) setStreamingContent((prev) => prev + output);
256
+ },
257
+ onToolStart: (name: string, args?: any) => {
258
+ setCurrentTool(name);
259
+ setMessages(prev => [...prev, { role: 'tool', content: JSON.stringify({ type: 'tool_start', name, args }) }]);
260
+ addToolActivity(name, 'running');
261
+ addSystemEvent(`Tool Start: ${name}`, 'info');
262
+ },
263
+ onToolEnd: (name, result) => {
264
+ setCurrentTool(null);
265
+ setMessages(prev => [...prev, { role: 'tool', content: JSON.stringify({ type: 'tool_end', name, result }) }]);
266
+ addToolActivity(name, 'success'); // Assume success for visualization
267
+ addSystemEvent(`Tool Finished: ${name}`, 'success');
317
268
  }
269
+ });
318
270
 
319
- // Update session
320
- const cleanContent = response.content ? response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim() : '';
321
- sessionManager.addMessage({ role: 'user', content: trimmed });
271
+ // Finalize
272
+ if (response.content) {
273
+ const cleanContent = response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
322
274
  if (cleanContent) {
323
- sessionManager.addMessage({ role: 'assistant', content: cleanContent });
275
+ setMessages((prev) => [...prev, { role: 'assistant', content: cleanContent }]);
324
276
  }
325
- response.toolsUsed.forEach((tool) => sessionManager.addToolUsed(tool));
277
+ }
326
278
 
327
- // Record metrics
328
- sessionManager.addApiTime(response.apiTime);
329
- sessionManager.addToolTime(response.toolTime);
330
- if (response.usage) {
331
- sessionManager.addUsage(apiClient.currentModel, response.usage);
332
- }
333
- for (let i = 0; i < response.successfulToolCalls; i++) sessionManager.recordToolCall(true);
334
- for (let i = 0; i < response.failedToolCalls; i++) sessionManager.recordToolCall(false);
335
- } catch (error) {
336
- const errorMessage = error instanceof Error ? error.message : String(error);
337
- setMessages((prev) => [...prev, { role: 'error', content: `Error: ${errorMessage}` }]);
338
- } finally {
339
- setIsProcessing(false);
340
- setStreamingContent('');
341
- setCurrentTool(null);
279
+ // Update Session & Stats
280
+ const cleanContent = response.content ? response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim() : '';
281
+ sessionManager.addMessage({ role: 'user', content: trimmed });
282
+ if (cleanContent) sessionManager.addMessage({ role: 'assistant', content: cleanContent });
283
+
284
+ // Simulating Token Counts for visualization
285
+ if (response.usage) {
286
+ setTotalTokens(prev => prev + response.usage.total_tokens);
287
+ setContextUsage(Math.min(1, (response.usage.total_tokens / 128000))); // Usage vs 128k context
342
288
  }
343
- },
344
- [isProcessing, orchestrator, sessionManager, exit, toolExecutor, isYoloMode]
345
- );
289
+
290
+ addSystemEvent('Response received.', 'success');
291
+
292
+ } catch (error) {
293
+ const errorMessage = error instanceof Error ? error.message : String(error);
294
+ setMessages((prev) => [...prev, { role: 'error', content: `Error: ${errorMessage}` }]);
295
+ addSystemEvent(`Error: ${errorMessage}`, 'error');
296
+ } finally {
297
+ setIsProcessing(false);
298
+ setStreamingContent('');
299
+ setCurrentTool(null);
300
+ }
301
+
302
+ }, [isProcessing, orchestrator, sessionManager, exit, toolExecutor, isYoloMode, addSystemEvent, addToolActivity]);
346
303
 
347
304
  // Handle approval input
348
305
  useInput(
@@ -351,180 +308,38 @@ export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = fals
351
308
  if (input.toLowerCase() === 'y' || key.return) {
352
309
  pendingApproval.resolve(true);
353
310
  setPendingApproval(null);
311
+ addSystemEvent('Action Approved by User', 'success');
354
312
  } else if (input.toLowerCase() === 'n' || key.escape) {
355
313
  pendingApproval.resolve(false);
356
314
  setPendingApproval(null);
315
+ addSystemEvent('Action Denied by User', 'error');
357
316
  }
358
317
  }
359
318
  },
360
319
  { isActive: pendingApproval !== null }
361
320
  );
362
321
 
363
- const getRoleColor = (role: MessageDisplay['role']): string => {
364
- switch (role) {
365
- case 'user':
366
- return COLORS.primary;
367
- case 'assistant':
368
- return COLORS.success;
369
- case 'tool':
370
- return COLORS.info;
371
- case 'system':
372
- return COLORS.warning;
373
- case 'error':
374
- return COLORS.error;
375
- default:
376
- return COLORS.muted;
377
- }
378
- };
379
-
380
322
  return (
381
- <Box flexDirection="column" paddingX={2} paddingY={1}>
382
- {/* Branding & Header */}
383
- <Box flexDirection="row" alignItems="center" marginBottom={1}>
384
- <Branding />
385
- <Box marginLeft={4}>
386
- <HeaderBox
387
- version={CLI_VERSION}
388
- model={apiClient.currentModel}
389
- projectPath={process.cwd()}
390
- />
391
- </Box>
392
- </Box>
393
-
394
- {/* Tips only shown on start */}
395
- {messages.length === 0 && <Tips />}
396
-
397
- {/* Messages */}
398
- <Box flexDirection="column" flexGrow={1} marginBottom={1}>
399
- {messages.slice(-30).map((msg, i) => {
400
- // Custom rendering for Tool messages (Boxed UI)
401
- if (msg.role === 'tool') {
402
- try {
403
- const content = JSON.parse(msg.content);
404
- if (content.type === 'tool_start') {
405
- // Smart JSON formatting: one line if short, indented if long
406
- const jsonStr = JSON.stringify(content.args);
407
- const displayArgs = jsonStr.length < 80 ? jsonStr : JSON.stringify(content.args, null, 2);
408
-
409
- return (
410
- <Box key={i} flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={1} marginY={0} marginBottom={1} alignSelf="flex-start">
411
- <Box>
412
- <Text color="magenta" bold>⚡ TOOL CALL: </Text>
413
- <Text color="white" bold>{content.name}</Text>
414
- </Box>
415
- <Box marginLeft={1}>
416
- <Text color="gray">{displayArgs}</Text>
417
- </Box>
418
- </Box>
419
- );
420
- } else if (content.type === 'tool_end') {
421
- return (
422
- <Box key={i} flexDirection="column" borderStyle="round" borderColor="green" paddingX={1} marginBottom={1} alignSelf="flex-start">
423
- <Box>
424
- <Text color="green" bold>✅ TOOL RESULT: </Text>
425
- <Text color="white" bold>{content.name}</Text>
426
- </Box>
427
- <Box marginLeft={1}>
428
- <Text color="gray">{content.result ? content.result.substring(0, 200) + (content.result.length > 200 ? '...' : '') : 'Completed'}</Text>
429
- </Box>
430
- </Box>
431
- );
432
- }
433
- } catch {
434
- // Fallback for legacy plain text tool messages
435
- return (
436
- <Box key={i} marginBottom={1}>
437
- <Text color="gray">🔧 {msg.content}</Text>
438
- </Box>
439
- );
440
- }
441
- }
442
-
443
- // Standard User/Assistant/System messages
444
- return (
445
- <Box key={i} marginBottom={1} flexDirection="column">
446
- <Box>
447
- <Text bold color={getRoleColor(msg.role)}>
448
- {msg.role === 'user' ? '👤 YOU ' : msg.role === 'assistant' ? '🦅 BLUEHAWKS AI ' : 'ℹ️ SYSTEM '}
449
- </Text>
450
- </Box>
451
- <Box marginLeft={2}>
452
- <Text color="white">
453
- {(msg.role === 'user' || msg.role === 'assistant') ? '🔹 ' : ''}{msg.content}
454
- </Text>
455
- </Box>
456
- </Box>
457
- );
458
- })}
459
-
460
- {/* Streaming content */}
461
- {streamingContent && (
462
- <Box marginBottom={1} flexDirection="column">
463
- <Box>
464
- <Text bold color={COLORS.success}>🦅 BLUEHAWKS AI </Text>
465
- </Box>
466
- <Box marginLeft={2}>
467
- <Text color="white">🔹 {streamingContent}</Text>
468
- </Box>
469
- </Box>
470
- )}
471
-
472
- {/* Current tool - Only show if NO persistent start message was added yet (prevent partial dupes) */}
473
- {currentTool && !messages.some(m => m.role === 'tool' && m.content.includes(currentTool) && m.content.includes('tool_start')) && (
474
- <Box flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={2} paddingY={1} marginY={1}>
475
- <Box>
476
- <Spinner type="dots" />
477
- <Text color="magenta" bold> ⚡ TOOL CALL: </Text>
478
- <Text color="white" bold>{currentTool}</Text>
479
- </Box>
480
- </Box>
481
- )}
482
-
483
- {/* Approval prompt */}
484
- {pendingApproval && (
485
- <Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1} marginY={1}>
486
- <Text color={COLORS.warning} bold>
487
- ⚠️ ACTION REQUIRED: Tool Approval
488
- </Text>
489
- <Box marginY={1} paddingX={1} borderStyle="single" borderColor="gray">
490
- <Text color="white" bold>{pendingApproval.toolName}</Text>
491
- <Text color="gray">
492
- {"\n"}Args: {JSON.stringify(pendingApproval.args, null, 2).substring(0, 500)}
493
- </Text>
494
- </Box>
495
- <Box>
496
- <Text>Press </Text>
497
- <Text color="green" bold>Y</Text>
498
- <Text> to approve, </Text>
499
- <Text color="red" bold>N</Text>
500
- <Text> to deny</Text>
501
- </Box>
502
- </Box>
503
- )}
504
- </Box>
505
-
506
- {/* Input Area */}
507
- {!pendingApproval && (
508
- <Box flexDirection="column">
509
- <Box borderStyle="single" borderTop={true} borderBottom={false} borderLeft={false} borderRight={false} borderColor="gray" paddingTop={1}>
510
- <Text color={COLORS.primary} bold>❯ </Text>
511
- {isProcessing ? (
512
- <Box>
513
- <Spinner type="dots" />
514
- <Text color={COLORS.muted}> Agent is thinking...</Text>
515
- </Box>
516
- ) : (
517
- <TextInput
518
- value={input}
519
- onChange={setInput}
520
- onSubmit={handleSubmit}
521
- placeholder="What's on your mind?"
522
- />
523
- )}
524
- </Box>
525
- <StatusBar isYoloMode={isYoloMode} />
526
- </Box>
527
- )}
528
- </Box>
323
+ <Layout
324
+ // Header
325
+ systemEvents={systemEvents}
326
+ tps={tps}
327
+
328
+ // Sidebar
329
+ model={apiClient.currentModel}
330
+ contextUsage={contextUsage}
331
+ totalTokens={totalTokens}
332
+ toolHistory={toolHistory}
333
+
334
+ // Chat
335
+ messages={messages}
336
+ streamingContent={streamingContent}
337
+ currentTool={currentTool}
338
+ isProcessing={isProcessing}
339
+ input={input}
340
+ setInput={setInput}
341
+ onSubmit={handleSubmit}
342
+ pendingApproval={pendingApproval}
343
+ />
529
344
  );
530
345
  };