@bluehawks/cli 1.0.36 → 1.0.38
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/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +149 -129
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/ChatArea.d.ts +21 -0
- package/dist/cli/components/ChatArea.d.ts.map +1 -0
- package/dist/cli/components/ChatArea.js +37 -0
- package/dist/cli/components/ChatArea.js.map +1 -0
- package/dist/cli/components/Header.d.ts +7 -0
- package/dist/cli/components/Header.d.ts.map +1 -0
- package/dist/cli/components/Header.js +29 -0
- package/dist/cli/components/Header.js.map +1 -0
- package/dist/cli/components/Layout.d.ts +21 -0
- package/dist/cli/components/Layout.d.ts.map +1 -0
- package/dist/cli/components/Layout.js +9 -0
- package/dist/cli/components/Layout.js.map +1 -0
- package/dist/cli/components/Sidebar.d.ts +9 -0
- package/dist/cli/components/Sidebar.d.ts.map +1 -0
- package/dist/cli/components/Sidebar.js +24 -0
- package/dist/cli/components/Sidebar.js.map +1 -0
- package/dist/cli/components/types.d.ts +29 -0
- package/dist/cli/components/types.d.ts.map +1 -0
- package/dist/cli/components/types.js +5 -0
- package/dist/cli/components/types.js.map +1 -0
- package/dist/config/constants.d.ts +1 -1
- package/dist/config/constants.js +1 -1
- package/package.json +1 -1
- package/src/cli/app.tsx +221 -364
- package/src/cli/components/ChatArea.tsx +127 -0
- package/src/cli/components/Header.tsx +86 -0
- package/src/cli/components/Layout.tsx +60 -0
- package/src/cli/components/Sidebar.tsx +68 -0
- package/src/cli/components/types.ts +32 -0
- package/src/config/constants.ts +1 -1
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 {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
97
|
+
// Lifecycle: Init & System Events
|
|
138
98
|
useEffect(() => {
|
|
139
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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,126 +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
|
-
//
|
|
154
|
+
// TPS Simulation (Mock for now, can be real later)
|
|
187
155
|
useEffect(() => {
|
|
188
|
-
|
|
189
|
-
|
|
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
|
}
|
|
164
|
+
return () => {
|
|
165
|
+
if (interval) clearInterval(interval);
|
|
166
|
+
};
|
|
167
|
+
}, [isProcessing]);
|
|
168
|
+
|
|
169
|
+
// Initial Prompt
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (initialPrompt) handleSubmit(initialPrompt);
|
|
191
172
|
}, []);
|
|
192
173
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 }]);
|
|
198
|
+
}
|
|
199
|
+
setInput('');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
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
|
+
}
|
|
214
|
+
|
|
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;
|
|
239
|
+
} else {
|
|
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;
|
|
252
|
+
}
|
|
209
253
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
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');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
271
|
+
// Finalize
|
|
272
|
+
if (response.content) {
|
|
273
|
+
const cleanContent = response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
274
|
+
if (cleanContent) {
|
|
275
|
+
setMessages((prev) => [...prev, { role: 'assistant', content: cleanContent }]);
|
|
217
276
|
}
|
|
218
|
-
setInput('');
|
|
219
|
-
return;
|
|
220
277
|
}
|
|
221
278
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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;
|
|
240
|
-
}
|
|
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 });
|
|
241
283
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
288
|
+
}
|
|
247
289
|
|
|
248
|
-
|
|
249
|
-
const response = await orchestrator.chat(trimmed, [], {
|
|
250
|
-
onChunk: (chunk) => {
|
|
251
|
-
setStreamingContent((prev) => prev + chunk);
|
|
252
|
-
},
|
|
253
|
-
onToolStart: (name, args?: Record<string, unknown>) => {
|
|
254
|
-
setCurrentTool(name);
|
|
255
|
-
setMessages((prev) => [
|
|
256
|
-
...prev,
|
|
257
|
-
{
|
|
258
|
-
role: 'tool',
|
|
259
|
-
content: JSON.stringify({ type: 'tool_start', name, args })
|
|
260
|
-
},
|
|
261
|
-
]);
|
|
262
|
-
},
|
|
263
|
-
onToolEnd: (name, result) => {
|
|
264
|
-
setCurrentTool(null);
|
|
265
|
-
setMessages((prev) => [
|
|
266
|
-
...prev,
|
|
267
|
-
{
|
|
268
|
-
role: 'tool',
|
|
269
|
-
content: JSON.stringify({ type: 'tool_end', name, result })
|
|
270
|
-
},
|
|
271
|
-
]);
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Add final response
|
|
276
|
-
if (response.content) {
|
|
277
|
-
setMessages((prev) => [...prev, { role: 'assistant', content: response.content }]);
|
|
278
|
-
}
|
|
290
|
+
addSystemEvent('Response received.', 'success');
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
301
|
|
|
285
|
-
|
|
286
|
-
sessionManager.addApiTime(response.apiTime);
|
|
287
|
-
sessionManager.addToolTime(response.toolTime);
|
|
288
|
-
if (response.usage) {
|
|
289
|
-
sessionManager.addUsage(apiClient.currentModel, response.usage);
|
|
290
|
-
}
|
|
291
|
-
for (let i = 0; i < response.successfulToolCalls; i++) sessionManager.recordToolCall(true);
|
|
292
|
-
for (let i = 0; i < response.failedToolCalls; i++) sessionManager.recordToolCall(false);
|
|
293
|
-
} catch (error) {
|
|
294
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
295
|
-
setMessages((prev) => [...prev, { role: 'error', content: `Error: ${errorMessage}` }]);
|
|
296
|
-
} finally {
|
|
297
|
-
setIsProcessing(false);
|
|
298
|
-
setStreamingContent('');
|
|
299
|
-
setCurrentTool(null);
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
[isProcessing, orchestrator, sessionManager, exit, toolExecutor, isYoloMode]
|
|
303
|
-
);
|
|
302
|
+
}, [isProcessing, orchestrator, sessionManager, exit, toolExecutor, isYoloMode, addSystemEvent, addToolActivity]);
|
|
304
303
|
|
|
305
304
|
// Handle approval input
|
|
306
305
|
useInput(
|
|
@@ -309,180 +308,38 @@ export const App: React.FC<AppProps> = ({ initialPrompt, apiKey, yoloMode = fals
|
|
|
309
308
|
if (input.toLowerCase() === 'y' || key.return) {
|
|
310
309
|
pendingApproval.resolve(true);
|
|
311
310
|
setPendingApproval(null);
|
|
311
|
+
addSystemEvent('Action Approved by User', 'success');
|
|
312
312
|
} else if (input.toLowerCase() === 'n' || key.escape) {
|
|
313
313
|
pendingApproval.resolve(false);
|
|
314
314
|
setPendingApproval(null);
|
|
315
|
+
addSystemEvent('Action Denied by User', 'error');
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
},
|
|
318
319
|
{ isActive: pendingApproval !== null }
|
|
319
320
|
);
|
|
320
321
|
|
|
321
|
-
const getRoleColor = (role: MessageDisplay['role']): string => {
|
|
322
|
-
switch (role) {
|
|
323
|
-
case 'user':
|
|
324
|
-
return COLORS.primary;
|
|
325
|
-
case 'assistant':
|
|
326
|
-
return COLORS.success;
|
|
327
|
-
case 'tool':
|
|
328
|
-
return COLORS.info;
|
|
329
|
-
case 'system':
|
|
330
|
-
return COLORS.warning;
|
|
331
|
-
case 'error':
|
|
332
|
-
return COLORS.error;
|
|
333
|
-
default:
|
|
334
|
-
return COLORS.muted;
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
322
|
return (
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
{
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const content = JSON.parse(msg.content);
|
|
362
|
-
if (content.type === 'tool_start') {
|
|
363
|
-
// Smart JSON formatting: one line if short, indented if long
|
|
364
|
-
const jsonStr = JSON.stringify(content.args);
|
|
365
|
-
const displayArgs = jsonStr.length < 80 ? jsonStr : JSON.stringify(content.args, null, 2);
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<Box key={i} flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={1} marginY={0} marginBottom={1} alignSelf="flex-start">
|
|
369
|
-
<Box>
|
|
370
|
-
<Text color="magenta" bold>⚡ TOOL CALL: </Text>
|
|
371
|
-
<Text color="white" bold>{content.name}</Text>
|
|
372
|
-
</Box>
|
|
373
|
-
<Box marginLeft={1}>
|
|
374
|
-
<Text color="gray">{displayArgs}</Text>
|
|
375
|
-
</Box>
|
|
376
|
-
</Box>
|
|
377
|
-
);
|
|
378
|
-
} else if (content.type === 'tool_end') {
|
|
379
|
-
return (
|
|
380
|
-
<Box key={i} flexDirection="column" borderStyle="round" borderColor="green" paddingX={1} marginBottom={1} alignSelf="flex-start">
|
|
381
|
-
<Box>
|
|
382
|
-
<Text color="green" bold>✅ TOOL RESULT: </Text>
|
|
383
|
-
<Text color="white" bold>{content.name}</Text>
|
|
384
|
-
</Box>
|
|
385
|
-
<Box marginLeft={1}>
|
|
386
|
-
<Text color="gray">{content.result ? content.result.substring(0, 200) + (content.result.length > 200 ? '...' : '') : 'Completed'}</Text>
|
|
387
|
-
</Box>
|
|
388
|
-
</Box>
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
} catch {
|
|
392
|
-
// Fallback for legacy plain text tool messages
|
|
393
|
-
return (
|
|
394
|
-
<Box key={i} marginBottom={1}>
|
|
395
|
-
<Text color="gray">🔧 {msg.content}</Text>
|
|
396
|
-
</Box>
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Standard User/Assistant/System messages
|
|
402
|
-
return (
|
|
403
|
-
<Box key={i} marginBottom={1} flexDirection="column">
|
|
404
|
-
<Box>
|
|
405
|
-
<Text bold color={getRoleColor(msg.role)}>
|
|
406
|
-
{msg.role === 'user' ? '👤 YOU ' : msg.role === 'assistant' ? '🦅 BLUEHAWKS AI ' : 'ℹ️ SYSTEM '}
|
|
407
|
-
</Text>
|
|
408
|
-
</Box>
|
|
409
|
-
<Box marginLeft={2}>
|
|
410
|
-
<Text color="white">
|
|
411
|
-
{(msg.role === 'user' || msg.role === 'assistant') ? '🔹 ' : ''}{msg.content}
|
|
412
|
-
</Text>
|
|
413
|
-
</Box>
|
|
414
|
-
</Box>
|
|
415
|
-
);
|
|
416
|
-
})}
|
|
417
|
-
|
|
418
|
-
{/* Streaming content */}
|
|
419
|
-
{streamingContent && (
|
|
420
|
-
<Box marginBottom={1} flexDirection="column">
|
|
421
|
-
<Box>
|
|
422
|
-
<Text bold color={COLORS.success}>🦅 BLUEHAWKS AI </Text>
|
|
423
|
-
</Box>
|
|
424
|
-
<Box marginLeft={2}>
|
|
425
|
-
<Text color="white">🔹 {streamingContent}</Text>
|
|
426
|
-
</Box>
|
|
427
|
-
</Box>
|
|
428
|
-
)}
|
|
429
|
-
|
|
430
|
-
{/* Current tool - Only show if NO persistent start message was added yet (prevent partial dupes) */}
|
|
431
|
-
{currentTool && !messages.some(m => m.role === 'tool' && m.content.includes(currentTool) && m.content.includes('tool_start')) && (
|
|
432
|
-
<Box flexDirection="column" borderStyle="round" borderColor="magenta" paddingX={2} paddingY={1} marginY={1}>
|
|
433
|
-
<Box>
|
|
434
|
-
<Spinner type="dots" />
|
|
435
|
-
<Text color="magenta" bold> ⚡ TOOL CALL: </Text>
|
|
436
|
-
<Text color="white" bold>{currentTool}</Text>
|
|
437
|
-
</Box>
|
|
438
|
-
</Box>
|
|
439
|
-
)}
|
|
440
|
-
|
|
441
|
-
{/* Approval prompt */}
|
|
442
|
-
{pendingApproval && (
|
|
443
|
-
<Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1} marginY={1}>
|
|
444
|
-
<Text color={COLORS.warning} bold>
|
|
445
|
-
⚠️ ACTION REQUIRED: Tool Approval
|
|
446
|
-
</Text>
|
|
447
|
-
<Box marginY={1} paddingX={1} borderStyle="single" borderColor="gray">
|
|
448
|
-
<Text color="white" bold>{pendingApproval.toolName}</Text>
|
|
449
|
-
<Text color="gray">
|
|
450
|
-
{"\n"}Args: {JSON.stringify(pendingApproval.args, null, 2).substring(0, 500)}
|
|
451
|
-
</Text>
|
|
452
|
-
</Box>
|
|
453
|
-
<Box>
|
|
454
|
-
<Text>Press </Text>
|
|
455
|
-
<Text color="green" bold>Y</Text>
|
|
456
|
-
<Text> to approve, </Text>
|
|
457
|
-
<Text color="red" bold>N</Text>
|
|
458
|
-
<Text> to deny</Text>
|
|
459
|
-
</Box>
|
|
460
|
-
</Box>
|
|
461
|
-
)}
|
|
462
|
-
</Box>
|
|
463
|
-
|
|
464
|
-
{/* Input Area */}
|
|
465
|
-
{!pendingApproval && (
|
|
466
|
-
<Box flexDirection="column">
|
|
467
|
-
<Box borderStyle="single" borderTop={true} borderBottom={false} borderLeft={false} borderRight={false} borderColor="gray" paddingTop={1}>
|
|
468
|
-
<Text color={COLORS.primary} bold>❯ </Text>
|
|
469
|
-
{isProcessing ? (
|
|
470
|
-
<Box>
|
|
471
|
-
<Spinner type="dots" />
|
|
472
|
-
<Text color={COLORS.muted}> Agent is thinking...</Text>
|
|
473
|
-
</Box>
|
|
474
|
-
) : (
|
|
475
|
-
<TextInput
|
|
476
|
-
value={input}
|
|
477
|
-
onChange={setInput}
|
|
478
|
-
onSubmit={handleSubmit}
|
|
479
|
-
placeholder="What's on your mind?"
|
|
480
|
-
/>
|
|
481
|
-
)}
|
|
482
|
-
</Box>
|
|
483
|
-
<StatusBar isYoloMode={isYoloMode} />
|
|
484
|
-
</Box>
|
|
485
|
-
)}
|
|
486
|
-
</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
|
+
/>
|
|
487
344
|
);
|
|
488
345
|
};
|