camo-cli 2.0.1

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 (44) hide show
  1. package/README.md +184 -0
  2. package/dist/agent.js +977 -0
  3. package/dist/art.js +33 -0
  4. package/dist/components/App.js +71 -0
  5. package/dist/components/Chat.js +509 -0
  6. package/dist/components/HITLConfirmation.js +89 -0
  7. package/dist/components/ModelSelector.js +100 -0
  8. package/dist/components/SetupScreen.js +43 -0
  9. package/dist/config/constants.js +58 -0
  10. package/dist/config/prompts.js +98 -0
  11. package/dist/config/store.js +5 -0
  12. package/dist/core/AgentLoop.js +159 -0
  13. package/dist/hooks/useAutocomplete.js +52 -0
  14. package/dist/hooks/useKeyboard.js +73 -0
  15. package/dist/index.js +31 -0
  16. package/dist/mcp.js +95 -0
  17. package/dist/memory/MemoryManager.js +228 -0
  18. package/dist/providers/index.js +85 -0
  19. package/dist/providers/registry.js +121 -0
  20. package/dist/providers/types.js +5 -0
  21. package/dist/theme.js +45 -0
  22. package/dist/tools/FileTools.js +88 -0
  23. package/dist/tools/MemoryTools.js +53 -0
  24. package/dist/tools/SearchTools.js +45 -0
  25. package/dist/tools/ShellTools.js +40 -0
  26. package/dist/tools/TaskTools.js +52 -0
  27. package/dist/tools/ToolDefinitions.js +102 -0
  28. package/dist/tools/ToolRegistry.js +30 -0
  29. package/dist/types/Agent.js +6 -0
  30. package/dist/types/ink.js +1 -0
  31. package/dist/types/message.js +1 -0
  32. package/dist/types/ui.js +1 -0
  33. package/dist/utils/CriticAgent.js +88 -0
  34. package/dist/utils/DecisionLogger.js +156 -0
  35. package/dist/utils/MessageHistory.js +55 -0
  36. package/dist/utils/PermissionManager.js +253 -0
  37. package/dist/utils/SessionManager.js +180 -0
  38. package/dist/utils/TaskState.js +108 -0
  39. package/dist/utils/debug.js +35 -0
  40. package/dist/utils/execAsync.js +3 -0
  41. package/dist/utils/retry.js +50 -0
  42. package/dist/utils/tokenCounter.js +24 -0
  43. package/dist/utils/uiFormatter.js +106 -0
  44. package/package.json +92 -0
package/dist/art.js ADDED
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ export function getEliteHeaderArt() {
3
+ const c = {
4
+ g1: chalk.hex('#4ade80'), // Skin Light (Bright Green)
5
+ g2: chalk.hex('#22c55e'), // Skin Mid
6
+ g3: chalk.hex('#166534'), // Skin Dark (Shadows)
7
+ eye: chalk.hex('#fbbf24'), // Eye (Amber)
8
+ pupil: chalk.hex('#000000'), // Pupil
9
+ br1: chalk.hex('#a0522d'), // Branch Light
10
+ br2: chalk.hex('#8b4513'), // Branch Mid
11
+ leaf: chalk.hex('#86efac'), // Leaf
12
+ dim: chalk.dim,
13
+ };
14
+ const lines = [
15
+ ``,
16
+ ` ${c.g1('▄▄')}`,
17
+ ` ${c.g1('████')}${c.g2('▄')}`,
18
+ ` ${c.g2('▄')}${c.g1('██████')}${c.g2('▄')}`,
19
+ ` ${c.g2('████████')}${c.g1('█')}${c.g2('▄')} ${c.leaf('▄▄')}`,
20
+ ` ${c.g1('▄▄▄▄')} ${c.g2('▐██')}${c.eye('███')}${c.pupil('█')}${c.eye('█')}${c.g2('██▌')} ${c.leaf('████')}`,
21
+ ` ${c.g1('▄██████')}${c.g2('▄')} ${c.g2('▀██████▀')} ${c.leaf('▄██████')}`,
22
+ ` ${c.g1('▐██▀ ▀██▌')} ${c.leaf('▀▀')}${c.br1('__')}${c.leaf('▀▀')}`,
23
+ ` ${c.g2('██')} ${c.g2('▀')} ${c.br1('.-´')}`,
24
+ ` ${c.g2('▐█▄')} ${c.g3('▄▄▄')}${c.g2('██████▄')}${c.g3('▄')} ${c.br1('.-´')}`,
25
+ ` ${c.g2('▀██▄▄▄▄██████████████')}${c.g3('██▄')} ${c.br1('.-´')}`,
26
+ ` ${c.g3('▀▀▀▀▀▀')} ${c.g3('▐████')}${c.g2('████')}${c.br2('██▄´')}`,
27
+ ` ${c.br1('================')}${c.g3('██')}${c.g2('██')}${c.g3('█')}${c.br1('====')}${c.br2('▀▀')}${c.br1('======')}`,
28
+ ``,
29
+ ` ${chalk.bold.hex('#4ade80')('🦎 CAMO CLI')} ${chalk.dim('v2.1')} ${chalk.dim('• Adaptive Terminal Intelligence')}`,
30
+ ``
31
+ ];
32
+ return lines.join('\n');
33
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { Box, useApp } from 'ink';
3
+ import { SetupScreen } from './SetupScreen.js';
4
+ import { ChatInterface } from './Chat.js';
5
+ import { config } from '../config/store.js';
6
+ // View States
7
+ var ViewState;
8
+ (function (ViewState) {
9
+ ViewState["CHAT_VIEW"] = "CHAT_VIEW";
10
+ ViewState["SETUP_VIEW"] = "SETUP_VIEW";
11
+ ViewState["HITL_VIEW"] = "HITL_VIEW";
12
+ })(ViewState || (ViewState = {}));
13
+ export const App = ({ initialInput, verbose }) => {
14
+ const { exit } = useApp();
15
+ // Determine initial view based on API key presence
16
+ const hasApiKey = Boolean(config.get('googleApiKey'));
17
+ // State
18
+ const [view, setView] = useState(hasApiKey ? ViewState.CHAT_VIEW : ViewState.SETUP_VIEW);
19
+ const [apiKey, setApiKey] = useState(config.get('googleApiKey') || null);
20
+ const [hitlRequest, setHitlRequest] = useState(null);
21
+ // HITL Resolver logic
22
+ const hitlResolveRef = useRef(null);
23
+ const createHITLRequest = useCallback((request) => {
24
+ return new Promise((resolve) => {
25
+ hitlResolveRef.current = resolve;
26
+ setHitlRequest(request);
27
+ setView(ViewState.HITL_VIEW);
28
+ });
29
+ }, []);
30
+ const resolveHITL = useCallback((approved, scope) => {
31
+ if (hitlResolveRef.current) {
32
+ const resolver = hitlResolveRef.current;
33
+ hitlResolveRef.current = null;
34
+ setHitlRequest(null);
35
+ setView(ViewState.CHAT_VIEW);
36
+ resolver({ approved, scope });
37
+ }
38
+ }, []);
39
+ // Initialize Agent Context only once
40
+ useEffect(() => {
41
+ import('../agent.js').then(module => {
42
+ module.initializeAgentContext();
43
+ });
44
+ // Cleanup on unmount
45
+ return () => {
46
+ import('../agent.js').then(module => {
47
+ module.cleanupAgent();
48
+ });
49
+ };
50
+ }, []);
51
+ const handleCommand = useCallback((cmd) => {
52
+ // App-level interception removed for /model so Chat.tsx can handle it
53
+ if (cmd.trim() === '/exit') {
54
+ exit();
55
+ return true;
56
+ }
57
+ return false;
58
+ }, [exit]);
59
+ const handleSetupComplete = useCallback(() => {
60
+ const key = config.get('googleApiKey');
61
+ setApiKey(key);
62
+ if (key) {
63
+ setView(ViewState.CHAT_VIEW);
64
+ }
65
+ }, []);
66
+ return (React.createElement(Box, { flexDirection: "column", height: "100%" },
67
+ view === ViewState.SETUP_VIEW && (React.createElement(Box, { flexDirection: "column", justifyContent: "center", alignItems: "center", flexGrow: 1, marginBottom: 1 },
68
+ React.createElement(SetupScreen, { onComplete: handleSetupComplete }))),
69
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column" },
70
+ React.createElement(ChatInterface, { active: view === ViewState.CHAT_VIEW, onCommand: handleCommand, apiKey: apiKey, verbose: verbose, onRequestHITL: createHITLRequest, hitlRequest: view === ViewState.HITL_VIEW ? hitlRequest : null, onResolveHITL: view === ViewState.HITL_VIEW ? resolveHITL : undefined }))));
71
+ };
@@ -0,0 +1,509 @@
1
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { Box, Text, useInput, useApp } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { HITLConfirmation } from './HITLConfirmation.js';
5
+ import { getContextStats, formatTokenUsage } from '../utils/tokenCounter.js';
6
+ import fs from 'fs/promises';
7
+ import { config } from '../config/store.js';
8
+ export const ChatInterface = ({ active, onCommand, apiKey, verbose, onRequestHITL, hitlRequest, onResolveHITL }) => {
9
+ const [input, setInput] = useState('');
10
+ const [messages, setMessages] = useState([]);
11
+ const [isProcessing, setIsProcessing] = useState(false);
12
+ const [status, setStatus] = useState('');
13
+ const [mode, setMode] = useState('manual');
14
+ // Command history
15
+ const [commandHistory, setCommandHistory] = useState([]);
16
+ const [historyIndex, setHistoryIndex] = useState(-1);
17
+ // Ctrl+C tracking
18
+ const lastCtrlCRef = useRef(0);
19
+ const { exit } = useApp();
20
+ // Refs
21
+ const messagesRef = useRef([]);
22
+ const pendingChunksRef = useRef('');
23
+ const assistantIdRef = useRef(null);
24
+ const updateIntervalRef = useRef(null);
25
+ // Thinking animation
26
+ const [dots, setDots] = useState('');
27
+ useEffect(() => {
28
+ let interval;
29
+ if (isProcessing && status === 'thinking...') {
30
+ interval = setInterval(() => {
31
+ setDots(prev => prev.length >= 3 ? '' : prev + '.');
32
+ }, 500);
33
+ }
34
+ else {
35
+ setDots('');
36
+ }
37
+ return () => clearInterval(interval);
38
+ }, [isProcessing, status]);
39
+ const COMMANDS = useMemo(() => [
40
+ { name: '/context', description: 'Show token usage' },
41
+ { name: '/report', description: 'Show decision report' },
42
+ { name: '/model', description: 'Change AI model' },
43
+ { name: '/provider', description: 'Manage API provider and keys' },
44
+ { name: '/help', description: 'Show help' },
45
+ { name: '/exit', description: 'Exit CAMO' }
46
+ ], []);
47
+ // Suggestions state
48
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
49
+ const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
50
+ const [suggestionScrollTop, setSuggestionScrollTop] = useState(0);
51
+ const suggestions = useMemo(() => {
52
+ // Special case for /model
53
+ if (input.startsWith('/model ')) {
54
+ const search = input.replace('/model ', '').toLowerCase();
55
+ const currentModel = config.get('selectedModel') || 'gemini-2-flash';
56
+ const models = [
57
+ { name: 'gemini-3-flash', description: 'Am schnellsten' },
58
+ { name: 'gemini-3-pro', description: 'Am besten' },
59
+ { name: 'gemini-2-flash', description: 'Schnell & stabil' },
60
+ { name: 'gemini-2-pro', description: 'Fortgeschritten & komplex' }
61
+ ];
62
+ return models
63
+ .filter(m => m.name.toLowerCase().includes(search))
64
+ .map(m => ({
65
+ name: m.name,
66
+ description: m.description
67
+ }));
68
+ }
69
+ return COMMANDS.filter(cmd => cmd.name.toLowerCase().startsWith(input.toLowerCase()));
70
+ }, [input, COMMANDS]);
71
+ const showSuggestions = input.startsWith('/') && suggestions.length > 0 && !suggestionsDismissed;
72
+ // Reset suggestions
73
+ useEffect(() => {
74
+ setSuggestionIndex(0);
75
+ setSuggestionScrollTop(0);
76
+ setSuggestionsDismissed(false);
77
+ }, [input]);
78
+ // Keyboard handling
79
+ useInput((inputChar, key) => {
80
+ if (!active)
81
+ return;
82
+ if (key.ctrl && inputChar === 'c') {
83
+ const now = Date.now();
84
+ if (now - lastCtrlCRef.current < 500) {
85
+ exit();
86
+ }
87
+ else {
88
+ setInput('');
89
+ setHistoryIndex(-1);
90
+ lastCtrlCRef.current = now;
91
+ }
92
+ return;
93
+ }
94
+ if (showSuggestions) {
95
+ if (key.escape) {
96
+ setSuggestionsDismissed(true);
97
+ return;
98
+ }
99
+ if (key.upArrow) {
100
+ const newIndex = suggestionIndex > 0 ? suggestionIndex - 1 : suggestions.length - 1;
101
+ setSuggestionIndex(newIndex);
102
+ if (newIndex < suggestionScrollTop) {
103
+ setSuggestionScrollTop(newIndex);
104
+ }
105
+ else if (newIndex >= suggestionScrollTop + 4) {
106
+ setSuggestionScrollTop(newIndex - 3);
107
+ }
108
+ return;
109
+ }
110
+ if (key.downArrow) {
111
+ const newIndex = suggestionIndex < suggestions.length - 1 ? suggestionIndex + 1 : 0;
112
+ setSuggestionIndex(newIndex);
113
+ if (newIndex >= suggestionScrollTop + 4) {
114
+ setSuggestionScrollTop(newIndex - 3);
115
+ }
116
+ else if (newIndex < suggestionScrollTop) {
117
+ setSuggestionScrollTop(newIndex);
118
+ }
119
+ return;
120
+ }
121
+ if (key.tab || key.return) {
122
+ if (suggestionIndex >= 0 && suggestionIndex < suggestions.length) {
123
+ const selected = suggestions[suggestionIndex];
124
+ if (input.startsWith('/model ')) {
125
+ setInput(`/model ${selected.name}`);
126
+ }
127
+ else {
128
+ // If completing a command, add space so arguments can be typed
129
+ setInput(selected.name + ' ');
130
+ }
131
+ setSuggestionIndex(0);
132
+ setSuggestionsDismissed(true);
133
+ }
134
+ else if (suggestions.length > 0) {
135
+ const selected = suggestions[0];
136
+ if (input.startsWith('/model ')) {
137
+ setInput(`/model ${selected.name}`);
138
+ }
139
+ else {
140
+ setInput(selected.name + ' ');
141
+ }
142
+ setSuggestionIndex(0);
143
+ setSuggestionsDismissed(true);
144
+ }
145
+ return;
146
+ }
147
+ }
148
+ if (!showSuggestions) {
149
+ if (key.upArrow && commandHistory.length > 0 && !isProcessing) {
150
+ const newIndex = historyIndex < commandHistory.length - 1 ? historyIndex + 1 : historyIndex;
151
+ setHistoryIndex(newIndex);
152
+ setInput(commandHistory[commandHistory.length - 1 - newIndex] || '');
153
+ return;
154
+ }
155
+ if (key.downArrow && historyIndex > 0) {
156
+ const newIndex = historyIndex - 1;
157
+ setHistoryIndex(newIndex);
158
+ setInput(commandHistory[commandHistory.length - 1 - newIndex] || '');
159
+ return;
160
+ }
161
+ }
162
+ });
163
+ // Initialize mode
164
+ useEffect(() => {
165
+ import('../utils/PermissionManager.js').then(({ PermissionManager }) => {
166
+ setMode(PermissionManager.getInstance().getMode());
167
+ });
168
+ }, []);
169
+ const handleCommand = useCallback(async (value) => {
170
+ const [cmd, ...args] = value.slice(1).split(' ');
171
+ if (cmd === 'context') {
172
+ const stats = getContextStats(messages.map(m => ({ role: m.role, content: m.content })));
173
+ const systemMsg = {
174
+ id: Date.now().toString(),
175
+ role: 'system',
176
+ content: `⏺ Context: ${formatTokenUsage(stats)}`,
177
+ timestamp: Date.now()
178
+ };
179
+ setMessages(prev => [...prev, systemMsg]);
180
+ return true;
181
+ }
182
+ if (cmd === 'report') {
183
+ const { getSessionReport } = await import('../agent.js');
184
+ const report = await getSessionReport();
185
+ const systemMsg = {
186
+ id: Date.now().toString(),
187
+ role: 'system',
188
+ content: `⏺ ${report}`,
189
+ timestamp: Date.now()
190
+ };
191
+ setMessages(prev => [...prev, systemMsg]);
192
+ return true;
193
+ }
194
+ // Handle /model (show current)
195
+ if (cmd === 'model' && args.length === 0) {
196
+ const current = config.get('selectedModel') || 'gemini-2-flash';
197
+ const systemMsg = {
198
+ id: Date.now().toString(),
199
+ role: 'system',
200
+ content: `⏺ Current: **${current}**`,
201
+ timestamp: Date.now()
202
+ };
203
+ setMessages(prev => [...prev, systemMsg]);
204
+ return true;
205
+ }
206
+ // Handle /model [name]
207
+ if (cmd === 'model' && args.length > 0) {
208
+ const targetModel = args[0];
209
+ if (['gemini-3-flash', 'gemini-3-pro', 'gemini-2-flash', 'gemini-2-pro'].includes(targetModel)) {
210
+ config.set('selectedModel', targetModel);
211
+ const systemMsg = {
212
+ id: Date.now().toString(),
213
+ role: 'system',
214
+ content: `⏺ Model set to **${targetModel}**`,
215
+ timestamp: Date.now()
216
+ };
217
+ setMessages(prev => [...prev, systemMsg]);
218
+ }
219
+ else {
220
+ const systemMsg = {
221
+ id: Date.now().toString(),
222
+ role: 'system',
223
+ content: `⏺ Invalid model. Available: gemini-3-flash, gemini-3-pro, gemini-2-flash, gemini-2-pro`,
224
+ timestamp: Date.now()
225
+ };
226
+ setMessages(prev => [...prev, systemMsg]);
227
+ }
228
+ return true;
229
+ }
230
+ // Handle /provider (show info)
231
+ if (cmd === 'provider' && (args.length === 0 || args[0] !== 'key')) {
232
+ const googleKey = config.get('googleApiKey');
233
+ const maskedKey = googleKey ? `...${googleKey.slice(-4)}` : 'Not set';
234
+ const providerInfo = [
235
+ '⏺ **Provider Management**',
236
+ '',
237
+ '**Current Provider:** Google',
238
+ `**API Key:** ${maskedKey}`,
239
+ '',
240
+ '**Commands:**',
241
+ '• `/provider key [new-key]` - Update Google API key',
242
+ '• `/provider` - Show this info'
243
+ ].join('\n');
244
+ const systemMsg = {
245
+ id: Date.now().toString(),
246
+ role: 'system',
247
+ content: providerInfo,
248
+ timestamp: Date.now()
249
+ };
250
+ setMessages(prev => [...prev, systemMsg]);
251
+ return true;
252
+ }
253
+ // Handle /provider key [new-key]
254
+ if (cmd === 'provider' && args.length >= 2 && args[0] === 'key') {
255
+ const newKey = args.slice(1).join(' ');
256
+ config.set('googleApiKey', newKey);
257
+ const maskedNew = `...${newKey.slice(-4)}`;
258
+ const systemMsg = {
259
+ id: Date.now().toString(),
260
+ role: 'system',
261
+ content: `⏺ Google API key updated to ${maskedNew}`,
262
+ timestamp: Date.now()
263
+ };
264
+ setMessages(prev => [...prev, systemMsg]);
265
+ return true;
266
+ }
267
+ if (value.trim() === '/help' || value.trim() === '/') {
268
+ const helpText = `⏺ Commands:
269
+ /context - Show token usage
270
+ /report - Show decision report
271
+ /model - Change Model (Gemini 3)
272
+ /exit - Exit CAMO
273
+ /help - Show this help`;
274
+ const systemMsg = {
275
+ id: Date.now().toString(),
276
+ role: 'system',
277
+ content: helpText,
278
+ timestamp: Date.now()
279
+ };
280
+ setMessages(prev => [...prev, systemMsg]);
281
+ return true;
282
+ }
283
+ if (onCommand(value)) {
284
+ return true;
285
+ }
286
+ return false;
287
+ }, [config, messages, onCommand]);
288
+ const handleSubmit = useCallback(async (value) => {
289
+ if (!value.trim())
290
+ return;
291
+ if (value.startsWith('/')) {
292
+ if (await handleCommand(value)) {
293
+ setInput('');
294
+ return;
295
+ }
296
+ }
297
+ setCommandHistory(prev => [...prev, value]);
298
+ setHistoryIndex(-1);
299
+ const lines = value.split('\n');
300
+ const isPaste = lines.length > 10;
301
+ const displayContent = isPaste ? `[Pasted Text: ${lines.length} lines]` : value;
302
+ const userMsg = {
303
+ id: Date.now().toString(),
304
+ role: 'user',
305
+ content: value,
306
+ displayContent: displayContent,
307
+ timestamp: Date.now(),
308
+ };
309
+ const currentMessages = [...messagesRef.current, userMsg];
310
+ setIsProcessing(true);
311
+ setInput('');
312
+ setMessages(prev => [...prev, userMsg]);
313
+ messagesRef.current = currentMessages;
314
+ try {
315
+ const { runEliteAgent } = await import('../agent.js');
316
+ await fs.appendFile('debug_chat.log', `[${new Date().toISOString()}] Starting agent for: ${value}\n`);
317
+ setStatus('thinking...');
318
+ const assistantId = (Date.now() + 1).toString();
319
+ assistantIdRef.current = assistantId;
320
+ pendingChunksRef.current = '';
321
+ const assistantPlaceholder = {
322
+ id: assistantId,
323
+ role: 'assistant',
324
+ content: '',
325
+ timestamp: Date.now()
326
+ };
327
+ setMessages(prev => [...prev, assistantPlaceholder]);
328
+ messagesRef.current = [...messagesRef.current, assistantPlaceholder];
329
+ updateIntervalRef.current = setInterval(() => {
330
+ if (pendingChunksRef.current && assistantIdRef.current) {
331
+ const id = assistantIdRef.current;
332
+ const chunks = pendingChunksRef.current;
333
+ pendingChunksRef.current = '';
334
+ setMessages(prev => prev.map(m => m.id === id ? { ...m, content: m.content + chunks } : m));
335
+ messagesRef.current = messagesRef.current.map(m => m.id === id ? { ...m, content: m.content + chunks } : m);
336
+ }
337
+ }, 100);
338
+ await runEliteAgent(value, currentMessages, {
339
+ onChunk: (chunk) => {
340
+ if (status === 'thinking...')
341
+ setStatus('');
342
+ fs.appendFile('debug_chat.log', `[CHUNK] ${chunk}\n`).catch(() => { });
343
+ pendingChunksRef.current += chunk;
344
+ },
345
+ onHITL: onRequestHITL
346
+ });
347
+ // Clear interval before final flush
348
+ if (updateIntervalRef.current) {
349
+ clearInterval(updateIntervalRef.current);
350
+ updateIntervalRef.current = null;
351
+ }
352
+ // Final flush of any remaining chunks
353
+ if (pendingChunksRef.current && assistantIdRef.current) {
354
+ const id = assistantIdRef.current;
355
+ const pendingContent = pendingChunksRef.current;
356
+ await fs.appendFile('debug_chat.log', `[FINAL FLUSH] Content length: ${pendingContent.length}\n`).catch(() => { });
357
+ setMessages(prev => {
358
+ const updated = prev.map(m => m.id === id ? { ...m, content: m.content + pendingContent } : m);
359
+ fs.appendFile('debug_chat.log', `[FINAL FLUSH] Updated message count: ${updated.length}\n`).catch(() => { });
360
+ return updated;
361
+ });
362
+ messagesRef.current = messagesRef.current.map(m => m.id === id ? { ...m, content: m.content + pendingContent } : m);
363
+ pendingChunksRef.current = '';
364
+ }
365
+ await fs.appendFile('debug_chat.log', `[FLUSH] ${messagesRef.current[messagesRef.current.length - 1]?.content || 'NO CONTENT'}\n`).catch(() => { });
366
+ }
367
+ catch (e) {
368
+ await fs.appendFile('debug_chat.log', `[ERROR] ${e.message}\n`);
369
+ setStatus(`Error: ${e.message}`);
370
+ }
371
+ finally {
372
+ if (updateIntervalRef.current) {
373
+ clearInterval(updateIntervalRef.current);
374
+ updateIntervalRef.current = null;
375
+ }
376
+ assistantIdRef.current = null;
377
+ setIsProcessing(false);
378
+ setStatus(prev => prev.startsWith('Error') ? prev : '');
379
+ }
380
+ }, [handleCommand, onRequestHITL]);
381
+ const handleInputChange = useCallback((value) => {
382
+ setInput(value);
383
+ }, []);
384
+ const renderMessageContent = useCallback((content) => {
385
+ const lines = content.trim().split('\n');
386
+ let bufferLineCount = 0;
387
+ let isTextBlock = false;
388
+ return lines.map((line, i) => {
389
+ if (line.includes('[LOOP_STATUS]'))
390
+ return null;
391
+ if (line.includes('[done]'))
392
+ return null;
393
+ if (line.includes('[TOOL_STATUS]')) {
394
+ const text = line.replace(/\[TOOL_STATUS\]|\[\/TOOL_STATUS\]/g, '');
395
+ isTextBlock = false;
396
+ return React.createElement(Text, { key: i, color: "gray", dimColor: true },
397
+ " \u21B3 ",
398
+ text);
399
+ }
400
+ if (line.includes('[BUFFER_LINE]')) {
401
+ const text = line.replace(/\[BUFFER_LINE\]|\[\/BUFFER_LINE\]/g, '');
402
+ bufferLineCount++;
403
+ isTextBlock = false;
404
+ if (bufferLineCount <= 3) {
405
+ return React.createElement(Text, { key: i, color: "gray", dimColor: true },
406
+ " \u21B3 ",
407
+ text.slice(0, 70));
408
+ }
409
+ return null;
410
+ }
411
+ if (line.includes('[PLAN_DELTA]')) {
412
+ const delta = line.replace(/\[PLAN_DELTA\]|\[\/PLAN_DELTA\]/g, '');
413
+ isTextBlock = false;
414
+ return React.createElement(Text, { key: i, color: "cyan" },
415
+ "\u23FA ",
416
+ delta);
417
+ }
418
+ if (line.includes('[INTERNAL_THOUGHT]')) {
419
+ const thought = line.replace(/\[INTERNAL_THOUGHT\]|\[\/INTERNAL_THOUGHT\]/g, '');
420
+ isTextBlock = false;
421
+ return React.createElement(Text, { key: i, color: "gray", dimColor: true, italic: true },
422
+ " \uD83D\uDCAD ",
423
+ thought.slice(0, 60),
424
+ "...");
425
+ }
426
+ if (line.startsWith('⏺')) {
427
+ const textWithoutBullet = line.slice(1).trim();
428
+ isTextBlock = false;
429
+ return (React.createElement(Box, { key: i },
430
+ React.createElement(Text, { color: "green" }, "\u23FA "),
431
+ React.createElement(Text, { color: "white" }, textWithoutBullet)));
432
+ }
433
+ // Regular text
434
+ if (line.trim() && !line.includes('↳')) {
435
+ // Filter only specific internal tags
436
+ const hiddenTags = [
437
+ '[LOOP_STATUS]',
438
+ '[TOOL_STATUS]',
439
+ '[BUFFER_LINE]',
440
+ '[PLAN_DELTA]',
441
+ '[INTERNAL_THOUGHT]',
442
+ '[done]',
443
+ '[TASK_COMPLETE]',
444
+ '[ASK_USER]',
445
+ '[PLAN_READY]'
446
+ ];
447
+ // If line starts with a known hidden tag, skip it
448
+ if (hiddenTags.some(tag => line.startsWith(tag)))
449
+ return null;
450
+ if (!isTextBlock) {
451
+ // Start of new text block - Always use large bullet
452
+ isTextBlock = true;
453
+ return (React.createElement(Box, { key: i },
454
+ React.createElement(Text, { color: "white" }, "\u2022 "),
455
+ React.createElement(Text, { color: "white" }, line)));
456
+ }
457
+ else {
458
+ // Continuation line - no bullet, just indent
459
+ return (React.createElement(Box, { key: i, paddingLeft: 2 },
460
+ React.createElement(Text, { color: "white" }, line)));
461
+ }
462
+ }
463
+ return null;
464
+ }).filter(Boolean);
465
+ }, []);
466
+ return (React.createElement(Box, { flexDirection: "column" },
467
+ React.createElement(Box, { flexDirection: "column" }, messages.map((msg, i) => (React.createElement(Box, { key: msg.id, paddingBottom: msg.role === 'assistant' ? 1 : 0, flexDirection: "column" }, msg.role === 'user' ? (React.createElement(Text, { color: "gray" },
468
+ "> ",
469
+ msg.displayContent || msg.content)) : (React.createElement(Box, { flexDirection: "column" }, renderMessageContent(msg.content))))))),
470
+ React.createElement(Box, { flexDirection: "column" },
471
+ hitlRequest && onResolveHITL ? (React.createElement(HITLConfirmation, { request: hitlRequest, onResolve: onResolveHITL })) : (React.createElement(Box, { borderStyle: "single", borderColor: active ? "white" : "gray", flexDirection: "column", paddingX: 1 },
472
+ (showSuggestions || input.trim() === '?') && (React.createElement(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginBottom: 0 }, input.trim() === '?' ? (React.createElement(React.Fragment, null,
473
+ React.createElement(Text, { color: "white", bold: true }, "Shortcuts"),
474
+ React.createElement(Box, { flexDirection: "column", marginLeft: 1 },
475
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
476
+ React.createElement(Text, { color: "gray" }, "Double Ctrl+C"),
477
+ React.createElement(Text, { color: "white" }, "Exit session")),
478
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
479
+ React.createElement(Text, { color: "gray" }, "Shift+Enter"),
480
+ React.createElement(Text, { color: "white" }, "New line")),
481
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
482
+ React.createElement(Text, { color: "gray" }, "Tab"),
483
+ React.createElement(Text, { color: "white" }, "Autocomplete")),
484
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
485
+ React.createElement(Text, { color: "gray" }, "Ctrl+C"),
486
+ React.createElement(Text, { color: "white" }, "Clear input")),
487
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
488
+ React.createElement(Text, { color: "gray" }, "\u2191/\u2193"),
489
+ React.createElement(Text, { color: "white" }, "History")),
490
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
491
+ React.createElement(Text, { color: "gray" }, "/"),
492
+ React.createElement(Text, { color: "white" }, "Commands"))))) : (suggestions.slice(suggestionScrollTop, suggestionScrollTop + 4).map((cmd, i) => {
493
+ const realIndex = suggestionScrollTop + i;
494
+ const isSelected = realIndex === suggestionIndex;
495
+ return (React.createElement(Box, { key: cmd.name, flexDirection: "row", justifyContent: "space-between" },
496
+ React.createElement(Box, null,
497
+ React.createElement(Text, { color: isSelected ? 'white' : 'gray' },
498
+ isSelected ? '> ' : ' ',
499
+ cmd.name)),
500
+ React.createElement(Box, { marginLeft: 2 },
501
+ React.createElement(Text, { color: "gray", dimColor: true }, cmd.description))));
502
+ })))),
503
+ (isProcessing || status.startsWith('Error')) && status && (React.createElement(Text, { color: status.startsWith('Error') ? 'red' : 'gray', dimColor: true }, status === 'thinking...' ? `Thinking ${dots}` : status)),
504
+ React.createElement(Box, null,
505
+ React.createElement(Text, { color: active ? "white" : "gray" }, "> "),
506
+ active ? (React.createElement(TextInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, placeholder: "Type a message..." })) : (React.createElement(Text, { color: "gray" }, input))))),
507
+ !hitlRequest && active && (React.createElement(Box, { marginTop: 0, paddingLeft: 1 },
508
+ React.createElement(Text, { color: "gray", dimColor: true }, "? for shortcuts"))))));
509
+ };