@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/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +112 -134
- 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 +216 -401
- 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,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
|
-
//
|
|
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
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setInput('');
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
169
|
+
// Initial Prompt
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (initialPrompt) handleSubmit(initialPrompt);
|
|
172
|
+
}, []);
|
|
221
173
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
247
202
|
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
271
|
+
// Finalize
|
|
272
|
+
if (response.content) {
|
|
273
|
+
const cleanContent = response.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
322
274
|
if (cleanContent) {
|
|
323
|
-
|
|
275
|
+
setMessages((prev) => [...prev, { role: 'assistant', content: cleanContent }]);
|
|
324
276
|
}
|
|
325
|
-
|
|
277
|
+
}
|
|
326
278
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
{
|
|
395
|
-
{
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
};
|