bimmo-cli 2.2.11 → 3.0.0

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 (3) hide show
  1. package/bin/bimmo +8 -1
  2. package/package.json +12 -3
  3. package/src/interface.js +300 -207
package/bin/bimmo CHANGED
@@ -6,11 +6,18 @@ process.removeAllListeners('warning');
6
6
  import { program } from 'commander';
7
7
  import { startInteractive } from '../src/interface.js';
8
8
  import { configure } from '../src/config.js';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
9
16
 
10
17
  program
11
18
  .name('bimmo')
12
19
  .description('bimmo — Sua IA universal no terminal (verde & lavanda)')
13
- .version('1.2.4');
20
+ .version(pkg.version);
14
21
 
15
22
  program
16
23
  .command('config')
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.11",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time robusto, Diffs e Contexto Inteligente.",
3
+ "version": "3.0.0",
4
+ "description": "🌿 Plataforma de IA universal profissional com interface React (Ink). Agentes, Swarms, Autocomplete real-time e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
8
8
  "type": "module",
9
9
  "keywords": [
10
- "ai", "cli", "openai", "anthropic", "gemini", "grok", "deepseek", "openrouter", "zai", "agent", "swarm", "terminal"
10
+ "ai", "cli", "ink", "react", "openai", "anthropic", "gemini", "agent", "swarm", "terminal"
11
11
  ],
12
12
  "author": "Judah",
13
13
  "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/JudahAragao/bimmo-cli.git"
17
+ },
14
18
  "files": [
15
19
  "bin/",
16
20
  "src/",
@@ -30,6 +34,10 @@
30
34
  "diff": "^7.0.0",
31
35
  "figlet": "^1.7.0",
32
36
  "fuzzy": "^0.1.3",
37
+ "ink": "^5.1.0",
38
+ "ink-divider": "^3.0.0",
39
+ "ink-spinner": "^5.0.0",
40
+ "ink-text-input": "^6.0.0",
33
41
  "inquirer": "^9.3.8",
34
42
  "marked": "^14.0.0",
35
43
  "marked-terminal": "^7.0.0",
@@ -37,6 +45,7 @@
37
45
  "ollama": "^0.5.12",
38
46
  "openai": "^4.82.0",
39
47
  "ora": "^8.1.1",
48
+ "react": "^18.2.0",
40
49
  "zod": "^3.24.1"
41
50
  }
42
51
  }
package/src/interface.js CHANGED
@@ -1,26 +1,22 @@
1
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
2
+ import { render, Box, Text, useInput, useApp } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import Spinner from 'ink-spinner';
1
5
  import chalk from 'chalk';
2
6
  import figlet from 'figlet';
3
7
  import { marked } from 'marked';
4
8
  import TerminalRenderer from 'marked-terminal';
5
- import ora from 'ora';
6
9
  import fs from 'fs';
7
10
  import path from 'path';
8
- import mime from 'mime-types';
9
- import readline from 'readline';
10
11
  import { fileURLToPath } from 'url';
11
- import { stripVTControlCharacters } from 'util';
12
12
 
13
- import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
13
+ import { getConfig, updateActiveModel, switchProfile } from './config.js';
14
14
  import { createProvider } from './providers/factory.js';
15
15
  import { getProjectContext } from './project-context.js';
16
- import { SwarmOrchestrator } from './orchestrator.js';
17
16
  import { editState } from './agent.js';
17
+ import { SwarmOrchestrator } from './orchestrator.js';
18
18
 
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = path.dirname(__filename);
21
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
22
- const version = pkg.version;
23
-
19
+ // Configuração do renderizador Markdown para o terminal
24
20
  marked.use(new TerminalRenderer({
25
21
  heading: chalk.hex('#c084fc').bold,
26
22
  code: chalk.hex('#00ff9d'),
@@ -29,239 +25,336 @@ marked.use(new TerminalRenderer({
29
25
  html: () => '',
30
26
  }));
31
27
 
32
- const green = chalk.hex('#00ff9d');
33
- const lavender = chalk.hex('#c084fc');
34
- const gray = chalk.gray;
35
- const bold = chalk.bold;
36
- const yellow = chalk.yellow;
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
31
+ const version = pkg.version;
37
32
 
38
- let currentMode = 'chat';
39
- let activePersona = null;
40
- let exitCounter = 0;
41
- let exitTimer = null;
33
+ const green = '#00ff9d';
34
+ const lavender = '#c084fc';
35
+ const gray = '#6272a4';
36
+ const yellow = '#f1fa8c';
37
+ const red = '#ff5555';
38
+ const cyan = '#8be9fd';
42
39
 
43
- function getFilesForPreview(partialPath) {
44
- try {
45
- let p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
46
- let searchDir = '.';
47
- let filter = '';
40
+ const BimmoApp = ({ initialConfig }) => {
41
+ const { exit } = useApp();
42
+ const [config, setConfig] = useState(initialConfig);
43
+ const [mode, setMode] = useState('chat');
44
+ const [activePersona, setActivePersona] = useState(null);
45
+ const [messages, setMessages] = useState([]);
46
+ const [input, setInput] = useState('');
47
+ const [isThinking, setIsThinking] = useState(false);
48
+ const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
49
+ const [exitCounter, setExitCounter] = useState(0);
50
+ const [provider, setProvider] = useState(() => createProvider(initialConfig));
48
51
 
49
- if (p.includes('/')) {
50
- const lastSlash = p.lastIndexOf('/');
51
- searchDir = p.substring(0, lastSlash) || '.';
52
- filter = p.substring(lastSlash + 1);
53
- } else {
54
- searchDir = '.';
55
- filter = p;
56
- }
52
+ // Inicializa com o contexto do projeto
53
+ useEffect(() => {
54
+ const ctx = getProjectContext();
55
+ setMessages([{ role: 'system', content: ctx }]);
56
+
57
+ // Welcome message
58
+ setMessages(prev => [...prev, {
59
+ role: 'assistant',
60
+ content: `Olá! Sou o **bimmo v${version}**. Como posso ajudar hoje?\n\nDigite \`/help\` para ver os comandos disponíveis.`
61
+ }]);
62
+ }, []);
57
63
 
58
- const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
59
- if (!fs.existsSync(absoluteSearchDir)) return [];
64
+ // Lógica de Autocomplete em tempo real para @arquivos
65
+ const filePreview = useMemo(() => {
66
+ if (!input.includes('@')) return [];
67
+ const words = input.split(' ');
68
+ const lastWord = words[words.length - 1];
69
+ if (!lastWord.startsWith('@')) return [];
60
70
 
61
- const files = fs.readdirSync(absoluteSearchDir);
62
- return files
63
- .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
64
- .map(f => {
65
- const rel = path.join(searchDir === '.' ? '' : searchDir, f);
66
- const isDir = fs.statSync(path.join(absoluteSearchDir, f)).isDirectory();
67
- return { name: rel, isDir };
68
- });
69
- } catch (e) { return []; }
70
- }
71
+ try {
72
+ const p = lastWord.slice(1);
73
+ const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
74
+ const filter = p.includes('/') ? p.substring(p.lastIndexOf('/') + 1) : p;
75
+ const absDir = path.resolve(process.cwd(), dir);
76
+ if (!fs.existsSync(absDir)) return [];
71
77
 
72
- function cleanAIResponse(text) {
73
- if (!text) return "";
74
- return text
75
- .replace(/<br\s*\/?>/gi, '\n')
76
- .replace(/<p>/gi, '')
77
- .replace(/<\/p>/gi, '\n\n')
78
- .replace(/<[^>]*>?/gm, '')
79
- .trim();
80
- }
78
+ return fs.readdirSync(absDir)
79
+ .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
80
+ .slice(0, 5)
81
+ .map(f => ({
82
+ name: f,
83
+ isDir: fs.statSync(path.join(absDir, f)).isDirectory(),
84
+ rel: path.join(dir === '.' ? '' : dir, f)
85
+ }));
86
+ } catch (e) { return []; }
87
+ }, [input]);
81
88
 
82
- export async function startInteractive() {
83
- let config = getConfig();
84
- if (!config.provider || !config.apiKey) {
85
- console.log(lavender(figlet.textSync('bimmo')));
86
- await configure(); return startInteractive();
87
- }
89
+ const handleSubmit = async (val) => {
90
+ const rawInput = val.trim();
91
+ if (!rawInput) return;
92
+ setInput('');
88
93
 
89
- let provider = createProvider(config);
90
- const orchestrator = new SwarmOrchestrator(config);
91
- let messages = [];
94
+ const lowerInput = rawInput.toLowerCase();
95
+ const parts = rawInput.split(' ');
96
+ const cmd = parts[0].toLowerCase();
92
97
 
93
- const resetMessages = () => {
94
- messages = [{ role: 'system', content: getProjectContext() }];
95
- if (activePersona) {
96
- const agent = (config.agents || {})[activePersona];
97
- if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
98
+ // COMANDOS INTERNOS
99
+ if (cmd === '/exit' || cmd === 'sair') exit();
100
+
101
+ if (cmd === '/clear') {
102
+ const ctx = getProjectContext();
103
+ setMessages([{ role: 'system', content: ctx }, { role: 'assistant', content: 'Chat limpo.' }]);
104
+ return;
98
105
  }
99
- };
100
- resetMessages();
101
106
 
102
- console.clear();
103
- console.log(lavender(figlet.textSync('bimmo')));
104
- console.log(lavender(` v${version} `.padStart(60, '')));
105
- console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
106
- console.log(green(` Modelo: ${bold(config.model)}`));
107
- console.log(lavender('─'.repeat(60)) + '\n');
107
+ if (cmd === '/chat') { setMode('chat'); return; }
108
+ if (cmd === '/plan') { setMode('plan'); return; }
109
+ if (cmd === '/edit') {
110
+ setMode('edit');
111
+ editState.autoAccept = parts[1] === 'auto';
112
+ return;
113
+ }
108
114
 
109
- const rl = readline.createInterface({
110
- input: process.stdin,
111
- output: process.stdout,
112
- terminal: true,
113
- completer: (line) => {
114
- const words = line.split(' ');
115
- const lastWord = words[words.length - 1];
116
- if (lastWord.startsWith('@')) {
117
- const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
118
- return [hits, lastWord];
115
+ if (cmd === '/model') {
116
+ const newModel = parts[1];
117
+ if (newModel) {
118
+ updateActiveModel(newModel);
119
+ const newCfg = getConfig();
120
+ setConfig(newCfg);
121
+ setProvider(createProvider(newCfg));
122
+ setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
119
123
  }
120
- return [[], line];
124
+ return;
121
125
  }
122
- });
123
126
 
124
- let currentPreviewLines = 0;
125
-
126
- const clearPreview = () => {
127
- if (currentPreviewLines > 0) {
128
- // Move o cursor para o início da primeira linha de preview
129
- readline.moveCursor(process.stdout, 0, 1);
130
- for (let i = 0; i < currentPreviewLines; i++) {
131
- readline.clearLine(process.stdout, 0);
132
- readline.moveCursor(process.stdout, 0, 1);
127
+ if (cmd === '/switch') {
128
+ const profile = parts[1];
129
+ if (switchProfile(profile)) {
130
+ const newCfg = getConfig();
131
+ setConfig(newCfg);
132
+ setProvider(createProvider(newCfg));
133
+ setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
134
+ } else {
135
+ setMessages(prev => [...prev, { role: 'system', content: `Perfil "${profile}" não encontrado.` }]);
133
136
  }
134
- // Volta o cursor para a posição original
135
- readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
136
- currentPreviewLines = 0;
137
+ return;
137
138
  }
138
- };
139
139
 
140
- const showPreview = () => {
141
- const words = rl.line.split(' ');
142
- const lastWord = words[words.length - 1];
143
-
144
- if (lastWord.startsWith('@')) {
145
- const files = getFilesForPreview(lastWord);
146
- if (files.length > 0) {
147
- clearPreview();
148
- const displayFiles = files.slice(0, 10);
149
-
150
- // Move cursor para o final da linha atual e pula linha
151
- process.stdout.write('\n');
152
- displayFiles.forEach(f => {
153
- process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
154
- });
155
- currentPreviewLines = displayFiles.length;
156
-
157
- // Retorna o cursor para a posição exata de escrita
158
- readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
159
- const promptWidth = stripVTControlCharacters(rl.getPrompt()).length;
160
- readline.cursorTo(process.stdout, (promptWidth + rl.line.length) % (process.stdout.columns || 80));
140
+ if (cmd === '/use') {
141
+ const agentName = parts[1];
142
+ const agents = config.agents || {};
143
+ if (agentName === 'normal') {
144
+ setActivePersona(null);
145
+ setMessages(prev => [...prev, { role: 'system', content: 'Modo normal ativado.' }]);
146
+ } else if (agents[agentName]) {
147
+ const agent = agents[agentName];
148
+ setActivePersona(agentName);
149
+ setMode(agent.mode || 'chat');
150
+ if (agent.profile && agent.profile !== config.activeProfile) {
151
+ switchProfile(agent.profile);
152
+ const newCfg = getConfig();
153
+ setConfig(newCfg);
154
+ setProvider(createProvider(newCfg));
155
+ }
156
+ setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" ativo.` }]);
161
157
  } else {
162
- clearPreview();
158
+ setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" não encontrado.` }]);
163
159
  }
164
- } else {
165
- clearPreview();
160
+ return;
166
161
  }
167
- };
168
-
169
- const displayPrompt = () => {
170
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
171
- let modeLabel = `[${currentMode.toUpperCase()}]`;
172
- if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
173
-
174
- console.log(`${gray(`📁 ${process.cwd()}`)}`);
175
- rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
176
- rl.prompt();
177
- };
178
162
 
179
- // Escuta teclas de forma segura
180
- rl.on('line', async (input) => {
181
- clearPreview();
182
- const rawInput = input.trim();
183
- if (rawInput === '') { displayPrompt(); return; }
163
+ if (cmd === '/swarm') {
164
+ const swarmType = parts[1];
165
+ const orchestrator = new SwarmOrchestrator(config);
166
+ setIsThinking(true);
167
+ setThinkingMessage('Enxame em ação...');
184
168
 
185
- const cmd = rawInput.toLowerCase();
186
- if (cmd === '/exit' || cmd === 'sair') process.exit(0);
187
- if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
188
- if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
189
- if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
190
- if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
191
- if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
192
-
193
- if (cmd === '/init') {
194
- console.log(chalk.cyan('\n🚀 Gerando .bimmorc.json...\n'));
195
- const initPrompt = `Analise o projeto e crie o .bimmorc.json estruturado. Use write_file.`;
196
- const spinner = ora({ text: lavender(`bimmo pensando...`), color: 'red' }).start();
197
169
  try {
198
- const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
199
- spinner.stop();
200
- console.log(marked.parse(cleanAIResponse(res)));
201
- } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
202
- resetMessages(); displayPrompt(); return;
170
+ let response;
171
+ if (swarmType === 'seq') {
172
+ const agents = parts[2].split(',');
173
+ const goal = parts.slice(3).join(' ');
174
+ response = await orchestrator.runSequential(agents, goal);
175
+ } else if (swarmType === 'run') {
176
+ const manager = parts[2];
177
+ const workers = parts[3].split(',');
178
+ const goal = parts.slice(4).join(' ');
179
+ response = await orchestrator.runHierarchical(manager, workers, goal);
180
+ }
181
+ setMessages(prev => [...prev, { role: 'user', content: rawInput }, { role: 'assistant', content: response }]);
182
+ } catch (err) {
183
+ setMessages(prev => [...prev, { role: 'system', content: `Erro no enxame: ${err.message}` }]);
184
+ } finally {
185
+ setIsThinking(false);
186
+ setThinkingMessage('bimmo pensando...');
187
+ }
188
+ return;
203
189
  }
204
190
 
205
- if (cmd === '/config') {
206
- rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
207
- displayPrompt(); return;
191
+ if (cmd === '/help') {
192
+ const helpText = `
193
+ **Comandos Disponíveis:**
194
+ \`/chat\` | \`/plan\` | \`/edit [auto|manual]\` - Muda o modo
195
+ \`/switch [perfil]\` - Alterna perfis
196
+ \`/model [modelo]\` - Altera o modelo
197
+ \`/use [agente|normal]\` - Ativa um agente
198
+ \`/swarm seq [agente1,agente2] [objetivo]\` - Enxame sequencial
199
+ \`/swarm run [líder] [worker1,worker2] [objetivo]\` - Enxame hierárquico
200
+ \`/clear\` - Limpa o chat
201
+ \`/exit\` - Encerra o bimmo
202
+ \`@arquivo\` - Inclui conteúdo de arquivo
203
+ `;
204
+ setMessages(prev => [...prev, { role: 'assistant', content: helpText }]);
205
+ return;
208
206
  }
209
207
 
210
- // PROCESSAMENTO IA
211
- const controller = new AbortController();
212
- const abortHandler = () => controller.abort();
213
- process.on('SIGINT', abortHandler);
214
-
215
- let modeInstr = "";
216
- if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
217
- else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
218
-
219
- const processedContent = [];
220
- const words = rawInput.split(' ');
221
- for (const word of words) {
222
- if (word.startsWith('@')) {
223
- const filePath = word.slice(1);
224
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
225
- processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
226
- } else { processedContent.push({ type: 'text', text: word }); }
227
- } else { processedContent.push({ type: 'text', text: word }); }
208
+ // ENVIO PARA IA
209
+ setIsThinking(true);
210
+
211
+ let processedInput = rawInput;
212
+ const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
213
+ if (fileMatches) {
214
+ for (const match of fileMatches) {
215
+ const filePath = match.slice(1);
216
+ try {
217
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
218
+ const content = fs.readFileSync(filePath, 'utf-8');
219
+ processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
220
+ }
221
+ } catch (e) {}
222
+ }
228
223
  }
229
224
 
230
- messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
231
- const spinner = ora({ text: lavender(`bimmo pensando...`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
225
+ const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
226
+ const newMessages = [...messages, userMsg];
227
+ setMessages(newMessages);
232
228
 
233
229
  try {
234
- let responseText = await provider.sendMessage(messages, { signal: controller.signal });
235
- spinner.stop();
236
- console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
237
- console.log(lavender('─'.repeat(50)));
238
- process.stdout.write(marked.parse(cleanAIResponse(responseText)));
239
- console.log(gray('\n' + ''.repeat(50)));
240
- messages.push({ role: 'assistant', content: responseText });
230
+ let finalMessages = newMessages;
231
+ if (activePersona && config.agents[activePersona]) {
232
+ const agent = config.agents[activePersona];
233
+ finalMessages = [
234
+ { role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` },
235
+ ...newMessages.filter(m => m.role !== 'system')
236
+ ];
237
+ }
238
+
239
+ const response = await provider.sendMessage(finalMessages);
240
+ setMessages(prev => [...prev, { role: 'assistant', content: response }]);
241
241
  } catch (err) {
242
- spinner.stop();
243
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
244
- else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
242
+ setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
245
243
  } finally {
246
- process.removeListener('SIGINT', abortHandler);
247
- displayPrompt();
244
+ setIsThinking(false);
248
245
  }
249
- });
246
+ };
250
247
 
251
- // Listener de tecla para o preview
252
- process.stdin.on('keypress', (s, key) => {
253
- if (key && (key.name === 'return' || key.name === 'enter')) return;
254
- setImmediate(() => showPreview());
248
+ useInput((input, key) => {
249
+ if (key.ctrl && input === 'c') {
250
+ if (isThinking) {
251
+ setIsThinking(false);
252
+ } else {
253
+ if (exitCounter === 0) {
254
+ setExitCounter(1);
255
+ setTimeout(() => setExitCounter(0), 2000);
256
+ } else {
257
+ exit();
258
+ }
259
+ }
260
+ }
261
+
262
+ if (key.tab && filePreview.length > 0) {
263
+ const words = input.split(' ');
264
+ words[words.length - 1] = `@${filePreview[0].rel}${filePreview[0].isDir ? '/' : ''}`;
265
+ setInput(words.join(' '));
266
+ }
255
267
  });
256
268
 
257
- rl.on('SIGINT', () => {
258
- if (exitCounter === 0) {
259
- exitCounter++;
260
- process.stdout.write(`\n${gray('(Pressione novamente para sair)')}\n`);
261
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
262
- displayPrompt();
263
- } else { process.exit(0); }
264
- });
269
+ return (
270
+ <Box flexDirection="column" paddingX={1} minHeight={10}>
271
+ {/* HEADER */}
272
+ <Box flexDirection="column" marginBottom={1}>
273
+ <Text color={lavender}>{figlet.textSync('bimmo', { font: 'small' })}</Text>
274
+ <Box borderStyle="single" borderColor={lavender} paddingX={1} justifyContent="space-between">
275
+ <Text color={green} bold>v{version}</Text>
276
+ <Box>
277
+ <Text color={gray}>{config.activeProfile || 'Default'} </Text>
278
+ <Text color={lavender}>•</Text>
279
+ <Text color={gray}> {config.model}</Text>
280
+ </Box>
281
+ </Box>
282
+ </Box>
283
+
284
+ {/* MENSAGENS */}
285
+ <Box flexDirection="column" flexGrow={1}>
286
+ {messages.filter(m => m.role !== 'system').slice(-10).map((m, i) => (
287
+ <Box key={i} flexDirection="column" marginBottom={1}>
288
+ <Box>
289
+ <Text bold color={m.role === 'user' ? green : lavender}>
290
+ {m.role === 'user' ? '› Você' : '› bimmo'}
291
+ </Text>
292
+ {m.role === 'system' && <Text color={yellow}> [SISTEMA]</Text>}
293
+ </Box>
294
+ <Box paddingLeft={2}>
295
+ <Text>
296
+ {m.role === 'assistant'
297
+ ? marked.parse(m.content).trim()
298
+ : (m.displayContent || m.content)}
299
+ </Text>
300
+ </Box>
301
+ </Box>
302
+ ))}
303
+ </Box>
265
304
 
266
- displayPrompt();
305
+ {/* STATUS / THINKING */}
306
+ {isThinking && (
307
+ <Box marginBottom={1}>
308
+ <Text color={lavender}>
309
+ <Spinner type="dots" /> <Text italic>{thinkingMessage}</Text>
310
+ </Text>
311
+ </Box>
312
+ )}
313
+
314
+ {/* AUTOCOMPLETE PREVIEW */}
315
+ {filePreview.length > 0 && (
316
+ <Box flexDirection="column" borderStyle="round" borderColor={gray} paddingX={1} marginBottom={1}>
317
+ <Text color={gray} dimColor italic>Sugestões (TAB para completar):</Text>
318
+ {filePreview.map((f, i) => (
319
+ <Text key={i} color={i === 0 ? green : gray}>
320
+ {f.isDir ? '📁' : '📄'} {f.rel}{f.isDir ? '/' : ''}
321
+ </Text>
322
+ ))}
323
+ </Box>
324
+ )}
325
+
326
+ {/* PROMPT */}
327
+ <Box borderStyle="round" borderColor={isThinking ? gray : lavender} paddingX={1}>
328
+ <Text bold color={mode === 'edit' ? red : mode === 'plan' ? cyan : lavender}>
329
+ {activePersona ? `[${activePersona.toUpperCase()}] ` : ''}
330
+ [{mode.toUpperCase()}] ›{' '}
331
+ </Text>
332
+ <TextInput
333
+ value={input}
334
+ onChange={setInput}
335
+ onSubmit={handleSubmit}
336
+ placeholder="Como posso ajudar hoje?"
337
+ />
338
+ </Box>
339
+
340
+ {/* FOOTER */}
341
+ <Box marginTop={1} justifyContent="space-between" paddingX={1}>
342
+ <Text color={gray} dimColor>📁 {path.relative(process.env.HOME || '', process.cwd())}</Text>
343
+ {exitCounter === 1 && <Text color={yellow} bold> Pressione Ctrl+C novamente para sair</Text>}
344
+ <Box>
345
+ <Text color={gray} dimColor italic>↑↓ para histórico (em breve) • /help para comandos</Text>
346
+ </Box>
347
+ </Box>
348
+ </Box>
349
+ );
350
+ };
351
+
352
+ export async function startInteractive() {
353
+ const config = getConfig();
354
+ if (!config.provider || !config.apiKey) {
355
+ console.log(chalk.yellow('Provedor não configurado. Execute "bimmo config" primeiro.'));
356
+ process.exit(0);
357
+ }
358
+ process.stdout.write('\x1Bc');
359
+ render(<BimmoApp initialConfig={config} />);
267
360
  }