@projectservan8n/cnapse 0.2.1 → 0.4.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.
@@ -1,10 +1,15 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useRef } from 'react';
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { Header } from './Header.js';
4
4
  import { ChatMessage } from './ChatMessage.js';
5
5
  import { ChatInput } from './ChatInput.js';
6
6
  import { StatusBar } from './StatusBar.js';
7
+ import { HelpMenu } from './HelpMenu.js';
7
8
  import { chat, Message } from '../lib/api.js';
9
+ import { getScreenDescription } from '../lib/screen.js';
10
+ import { describeScreen } from '../lib/vision.js';
11
+ import { getTelegramBot, TelegramMessage } from '../services/telegram.js';
12
+ import { parseTask, executeTask, formatTask, Task } from '../lib/tasks.js';
8
13
 
9
14
  interface ChatMsg {
10
15
  id: string;
@@ -20,7 +25,7 @@ export function App() {
20
25
  {
21
26
  id: '0',
22
27
  role: 'system',
23
- content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n /clear - Clear chat\n /help - Show help',
28
+ content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n Ctrl+W - Toggle screen watch\n /clear - Clear chat\n /help - Show help',
24
29
  timestamp: new Date(),
25
30
  },
26
31
  ]);
@@ -28,8 +33,37 @@ export function App() {
28
33
  const [isProcessing, setIsProcessing] = useState(false);
29
34
  const [status, setStatus] = useState('Ready');
30
35
  const [error, setError] = useState<string | null>(null);
36
+ const [screenWatch, setScreenWatch] = useState(false);
37
+ const [showHelpMenu, setShowHelpMenu] = useState(false);
38
+ const [telegramEnabled, setTelegramEnabled] = useState(false);
39
+ const screenContextRef = useRef<string | null>(null);
40
+
41
+ // Screen watching effect
42
+ useEffect(() => {
43
+ if (!screenWatch) {
44
+ screenContextRef.current = null;
45
+ return;
46
+ }
47
+
48
+ const checkScreen = async () => {
49
+ const desc = await getScreenDescription();
50
+ if (desc) {
51
+ screenContextRef.current = desc;
52
+ }
53
+ };
54
+
55
+ checkScreen();
56
+ const interval = setInterval(checkScreen, 5000); // Check every 5 seconds
57
+
58
+ return () => clearInterval(interval);
59
+ }, [screenWatch]);
31
60
 
32
61
  useInput((inputChar, key) => {
62
+ // If help menu is open, don't process other shortcuts
63
+ if (showHelpMenu) {
64
+ return;
65
+ }
66
+
33
67
  if (key.ctrl && inputChar === 'c') {
34
68
  exit();
35
69
  }
@@ -37,6 +71,31 @@ export function App() {
37
71
  setMessages([messages[0]!]); // Keep welcome message
38
72
  setError(null);
39
73
  }
74
+ if (key.ctrl && inputChar === 'w') {
75
+ setScreenWatch((prev) => {
76
+ const newState = !prev;
77
+ addSystemMessage(
78
+ newState
79
+ ? '🖥️ Screen watching enabled. AI will have context of your screen.'
80
+ : '🖥️ Screen watching disabled.'
81
+ );
82
+ return newState;
83
+ });
84
+ }
85
+ if (key.ctrl && inputChar === 'h') {
86
+ setShowHelpMenu(true);
87
+ }
88
+ if (key.ctrl && inputChar === 't') {
89
+ setTelegramEnabled((prev) => {
90
+ const newState = !prev;
91
+ addSystemMessage(
92
+ newState
93
+ ? '📱 Telegram bot enabled.'
94
+ : '📱 Telegram bot disabled.'
95
+ );
96
+ return newState;
97
+ });
98
+ }
40
99
  });
41
100
 
42
101
  const handleSubmit = async (value: string) => {
@@ -84,7 +143,13 @@ export function App() {
84
143
  .slice(-10)
85
144
  .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
86
145
 
87
- apiMessages.push({ role: 'user', content: userInput });
146
+ // Add screen context if watching
147
+ let finalInput = userInput;
148
+ if (screenWatch && screenContextRef.current) {
149
+ finalInput = `[Screen context: ${screenContextRef.current}]\n\n${userInput}`;
150
+ }
151
+
152
+ apiMessages.push({ role: 'user', content: finalInput });
88
153
 
89
154
  const response = await chat(apiMessages);
90
155
 
@@ -116,6 +181,7 @@ export function App() {
116
181
  const handleCommand = (cmd: string) => {
117
182
  const parts = cmd.slice(1).split(' ');
118
183
  const command = parts[0];
184
+ const args = parts.slice(1).join(' ');
119
185
 
120
186
  switch (command) {
121
187
  case 'clear':
@@ -123,12 +189,151 @@ export function App() {
123
189
  addSystemMessage('Chat cleared.');
124
190
  break;
125
191
  case 'help':
126
- addSystemMessage(
127
- 'Commands:\n /clear - Clear chat history\n /help - Show this help\n\nJust type naturally to chat with the AI!'
128
- );
192
+ setShowHelpMenu(true);
193
+ break;
194
+ case 'watch':
195
+ setScreenWatch((prev) => {
196
+ const newState = !prev;
197
+ addSystemMessage(
198
+ newState
199
+ ? '🖥️ Screen watching enabled.'
200
+ : '🖥️ Screen watching disabled.'
201
+ );
202
+ return newState;
203
+ });
204
+ break;
205
+ case 'telegram':
206
+ handleTelegramToggle();
207
+ break;
208
+ case 'screen':
209
+ handleScreenCommand();
210
+ break;
211
+ case 'task':
212
+ if (args) {
213
+ handleTaskCommand(args);
214
+ } else {
215
+ addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
216
+ }
217
+ break;
218
+ case 'config':
219
+ addSystemMessage('⚙️ Configuration:\n Provider: Use cnapse config\n Model: Use cnapse config set model <name>');
220
+ break;
221
+ case 'model':
222
+ addSystemMessage('🤖 Model selection coming soon.\nUse: cnapse config set model <name>');
223
+ break;
224
+ case 'provider':
225
+ addSystemMessage('🔌 Provider selection coming soon.\nUse: cnapse config set provider <name>');
226
+ break;
227
+ case 'quit':
228
+ case 'exit':
229
+ exit();
129
230
  break;
130
231
  default:
131
- addSystemMessage(`Unknown command: ${command}`);
232
+ addSystemMessage(`Unknown command: ${command}\nType /help to see available commands.`);
233
+ }
234
+ };
235
+
236
+ const handleHelpMenuSelect = (command: string) => {
237
+ // Execute the selected command
238
+ handleCommand(command);
239
+ };
240
+
241
+ const handleScreenCommand = async () => {
242
+ addSystemMessage('📸 Taking screenshot and analyzing...');
243
+ setStatus('Analyzing screen...');
244
+ setIsProcessing(true);
245
+
246
+ try {
247
+ const result = await describeScreen();
248
+ addSystemMessage(`🖥️ Screen Analysis:\n\n${result.description}`);
249
+ } catch (err) {
250
+ const errorMsg = err instanceof Error ? err.message : 'Vision analysis failed';
251
+ addSystemMessage(`❌ Screen capture failed: ${errorMsg}`);
252
+ } finally {
253
+ setIsProcessing(false);
254
+ setStatus('Ready');
255
+ }
256
+ };
257
+
258
+ const handleTaskCommand = async (taskDescription: string) => {
259
+ addSystemMessage(`📋 Parsing task: ${taskDescription}`);
260
+ setStatus('Parsing task...');
261
+ setIsProcessing(true);
262
+
263
+ try {
264
+ // Parse the task into steps
265
+ const task = await parseTask(taskDescription);
266
+ addSystemMessage(`📋 Task planned (${task.steps.length} steps):\n${formatTask(task)}`);
267
+
268
+ // Execute the task
269
+ addSystemMessage('🚀 Executing task...');
270
+ setStatus('Executing task...');
271
+
272
+ await executeTask(task, (updatedTask, currentStep) => {
273
+ // Update progress
274
+ if (currentStep.status === 'running') {
275
+ setStatus(`Running: ${currentStep.description}`);
276
+ }
277
+ });
278
+
279
+ // Show final result
280
+ addSystemMessage(`\n${formatTask(task)}`);
281
+
282
+ if (task.status === 'completed') {
283
+ addSystemMessage('✅ Task completed successfully!');
284
+ } else {
285
+ addSystemMessage('❌ Task failed. Check the steps above for errors.');
286
+ }
287
+ } catch (err) {
288
+ const errorMsg = err instanceof Error ? err.message : 'Task failed';
289
+ addSystemMessage(`❌ Task error: ${errorMsg}`);
290
+ } finally {
291
+ setIsProcessing(false);
292
+ setStatus('Ready');
293
+ }
294
+ };
295
+
296
+ const handleTelegramToggle = async () => {
297
+ const bot = getTelegramBot();
298
+
299
+ if (telegramEnabled) {
300
+ // Stop the bot
301
+ try {
302
+ await bot.stop();
303
+ setTelegramEnabled(false);
304
+ addSystemMessage('📱 Telegram bot stopped.');
305
+ } catch (err) {
306
+ const errorMsg = err instanceof Error ? err.message : 'Failed to stop bot';
307
+ addSystemMessage(`❌ Error stopping bot: ${errorMsg}`);
308
+ }
309
+ } else {
310
+ // Start the bot
311
+ addSystemMessage('📱 Starting Telegram bot...');
312
+ setStatus('Starting Telegram...');
313
+
314
+ try {
315
+ // Setup event handlers
316
+ bot.on('message', (msg: TelegramMessage) => {
317
+ addSystemMessage(`📱 Telegram [${msg.from}]: ${msg.text}`);
318
+ });
319
+
320
+ bot.on('error', (error: Error) => {
321
+ addSystemMessage(`📱 Telegram error: ${error.message}`);
322
+ });
323
+
324
+ await bot.start();
325
+ setTelegramEnabled(true);
326
+ addSystemMessage(
327
+ '📱 Telegram bot started!\n\n' +
328
+ 'Open Telegram and send /start to your bot to connect.\n' +
329
+ 'Commands: /screen, /describe, /run <cmd>, /status'
330
+ );
331
+ } catch (err) {
332
+ const errorMsg = err instanceof Error ? err.message : 'Failed to start bot';
333
+ addSystemMessage(`❌ Telegram error: ${errorMsg}`);
334
+ } finally {
335
+ setStatus('Ready');
336
+ }
132
337
  }
133
338
  };
134
339
 
@@ -147,9 +352,21 @@ export function App() {
147
352
  // Only show last N messages that fit
148
353
  const visibleMessages = messages.slice(-20);
149
354
 
355
+ // If help menu is open, show it as overlay
356
+ if (showHelpMenu) {
357
+ return (
358
+ <Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
359
+ <HelpMenu
360
+ onClose={() => setShowHelpMenu(false)}
361
+ onSelect={handleHelpMenuSelect}
362
+ />
363
+ </Box>
364
+ );
365
+ }
366
+
150
367
  return (
151
368
  <Box flexDirection="column" height="100%">
152
- <Header />
369
+ <Header screenWatch={screenWatch} telegramEnabled={telegramEnabled} />
153
370
 
154
371
  <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="gray" padding={1}>
155
372
  <Text bold color="gray"> Chat </Text>
@@ -11,7 +11,12 @@ const ASCII_BANNER = `
11
11
  ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝
12
12
  `.trim();
13
13
 
14
- export function Header() {
14
+ interface HeaderProps {
15
+ screenWatch?: boolean;
16
+ telegramEnabled?: boolean;
17
+ }
18
+
19
+ export function Header({ screenWatch = false, telegramEnabled = false }: HeaderProps) {
15
20
  const config = getConfig();
16
21
 
17
22
  return (
@@ -20,8 +25,13 @@ export function Header() {
20
25
  <Box justifyContent="center">
21
26
  <Text color="gray">
22
27
  {config.provider} │ {config.model}
28
+ {screenWatch && <Text color="yellow"> │ 🖥️ Screen</Text>}
29
+ {telegramEnabled && <Text color="blue"> │ 📱 Telegram</Text>}
23
30
  </Text>
24
31
  </Box>
32
+ <Box justifyContent="center">
33
+ <Text color="gray" dimColor>Press Ctrl+H for help</Text>
34
+ </Box>
25
35
  </Box>
26
36
  );
27
37
  }
@@ -0,0 +1,143 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ interface MenuItem {
5
+ command: string;
6
+ shortcut?: string;
7
+ description: string;
8
+ category: 'navigation' | 'actions' | 'settings';
9
+ }
10
+
11
+ const MENU_ITEMS: MenuItem[] = [
12
+ // Navigation
13
+ { command: '/help', shortcut: 'Ctrl+H', description: 'Show this help menu', category: 'navigation' },
14
+ { command: '/clear', shortcut: 'Ctrl+L', description: 'Clear chat history', category: 'navigation' },
15
+ { command: '/quit', shortcut: 'Ctrl+C', description: 'Exit C-napse', category: 'navigation' },
16
+
17
+ // Actions
18
+ { command: '/screen', shortcut: 'Ctrl+S', description: 'Take screenshot and describe', category: 'actions' },
19
+ { command: '/task', description: 'Run multi-step task', category: 'actions' },
20
+ { command: '/telegram', shortcut: 'Ctrl+T', description: 'Toggle Telegram bot', category: 'actions' },
21
+
22
+ // Settings
23
+ { command: '/config', description: 'Show/edit configuration', category: 'settings' },
24
+ { command: '/watch', shortcut: 'Ctrl+W', description: 'Toggle screen watching', category: 'settings' },
25
+ { command: '/model', description: 'Change AI model', category: 'settings' },
26
+ { command: '/provider', description: 'Change AI provider', category: 'settings' },
27
+ ];
28
+
29
+ interface HelpMenuProps {
30
+ onClose: () => void;
31
+ onSelect: (command: string) => void;
32
+ }
33
+
34
+ export function HelpMenu({ onClose, onSelect }: HelpMenuProps) {
35
+ const [selectedIndex, setSelectedIndex] = useState(0);
36
+ const [selectedCategory, setSelectedCategory] = useState<'all' | MenuItem['category']>('all');
37
+
38
+ const filteredItems = selectedCategory === 'all'
39
+ ? MENU_ITEMS
40
+ : MENU_ITEMS.filter(item => item.category === selectedCategory);
41
+
42
+ useInput((input, key) => {
43
+ if (key.escape) {
44
+ onClose();
45
+ return;
46
+ }
47
+
48
+ if (key.upArrow) {
49
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : filteredItems.length - 1));
50
+ }
51
+
52
+ if (key.downArrow) {
53
+ setSelectedIndex(prev => (prev < filteredItems.length - 1 ? prev + 1 : 0));
54
+ }
55
+
56
+ if (key.leftArrow || key.rightArrow) {
57
+ const categories: Array<'all' | MenuItem['category']> = ['all', 'navigation', 'actions', 'settings'];
58
+ const currentIdx = categories.indexOf(selectedCategory);
59
+ if (key.leftArrow) {
60
+ setSelectedCategory(categories[currentIdx > 0 ? currentIdx - 1 : categories.length - 1]!);
61
+ } else {
62
+ setSelectedCategory(categories[currentIdx < categories.length - 1 ? currentIdx + 1 : 0]!);
63
+ }
64
+ setSelectedIndex(0);
65
+ }
66
+
67
+ if (key.return) {
68
+ const item = filteredItems[selectedIndex];
69
+ if (item) {
70
+ onSelect(item.command);
71
+ onClose();
72
+ }
73
+ }
74
+ });
75
+
76
+ const categories: Array<{ key: 'all' | MenuItem['category']; label: string }> = [
77
+ { key: 'all', label: 'All' },
78
+ { key: 'navigation', label: 'Navigation' },
79
+ { key: 'actions', label: 'Actions' },
80
+ { key: 'settings', label: 'Settings' },
81
+ ];
82
+
83
+ return (
84
+ <Box
85
+ flexDirection="column"
86
+ borderStyle="round"
87
+ borderColor="cyan"
88
+ padding={1}
89
+ width={60}
90
+ >
91
+ {/* Header */}
92
+ <Box justifyContent="center" marginBottom={1}>
93
+ <Text bold color="cyan">C-napse Help</Text>
94
+ </Box>
95
+
96
+ {/* Category Tabs */}
97
+ <Box justifyContent="center" marginBottom={1}>
98
+ {categories.map((cat, idx) => (
99
+ <React.Fragment key={cat.key}>
100
+ <Text
101
+ color={selectedCategory === cat.key ? 'cyan' : 'gray'}
102
+ bold={selectedCategory === cat.key}
103
+ >
104
+ {cat.label}
105
+ </Text>
106
+ {idx < categories.length - 1 && <Text color="gray"> │ </Text>}
107
+ </React.Fragment>
108
+ ))}
109
+ </Box>
110
+
111
+ <Box marginBottom={1}>
112
+ <Text color="gray" dimColor>Use ←→ to switch tabs, ↑↓ to navigate, Enter to select</Text>
113
+ </Box>
114
+
115
+ {/* Menu Items */}
116
+ <Box flexDirection="column">
117
+ {filteredItems.map((item, index) => (
118
+ <Box key={item.command}>
119
+ <Text color={index === selectedIndex ? 'cyan' : 'white'}>
120
+ {index === selectedIndex ? '❯ ' : ' '}
121
+ </Text>
122
+ <Box width={12}>
123
+ <Text bold={index === selectedIndex} color={index === selectedIndex ? 'cyan' : 'white'}>
124
+ {item.command}
125
+ </Text>
126
+ </Box>
127
+ {item.shortcut && (
128
+ <Box width={10}>
129
+ <Text color="yellow" dimColor>{item.shortcut}</Text>
130
+ </Box>
131
+ )}
132
+ <Text color="gray">{item.description}</Text>
133
+ </Box>
134
+ ))}
135
+ </Box>
136
+
137
+ {/* Footer */}
138
+ <Box marginTop={1} justifyContent="center">
139
+ <Text color="gray" dimColor>Press Esc to close</Text>
140
+ </Box>
141
+ </Box>
142
+ );
143
+ }
@@ -0,0 +1,203 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput, useApp } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { setApiKey, setProvider, setModel, getConfig } from '../lib/config.js';
5
+
6
+ type Step = 'provider' | 'apiKey' | 'model' | 'done';
7
+
8
+ const PROVIDERS = [
9
+ { id: 'ollama', name: 'Ollama', desc: 'Local AI (free, no API key needed)' },
10
+ { id: 'openrouter', name: 'OpenRouter', desc: 'Many models, pay per use' },
11
+ { id: 'anthropic', name: 'Anthropic', desc: 'Claude models' },
12
+ { id: 'openai', name: 'OpenAI', desc: 'GPT models' },
13
+ ] as const;
14
+
15
+ const DEFAULT_MODELS: Record<string, string[]> = {
16
+ ollama: ['qwen2.5:0.5b', 'qwen2.5:7b', 'llama3.2:3b', 'codellama:7b'],
17
+ openrouter: [
18
+ 'qwen/qwen-2.5-coder-32b-instruct',
19
+ 'anthropic/claude-3.5-sonnet',
20
+ 'openai/gpt-4o',
21
+ 'google/gemini-pro-1.5',
22
+ ],
23
+ anthropic: [
24
+ 'claude-3-5-sonnet-20241022',
25
+ 'claude-3-opus-20240229',
26
+ 'claude-3-haiku-20240307',
27
+ ],
28
+ openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
29
+ };
30
+
31
+ export function Setup() {
32
+ const { exit } = useApp();
33
+ const [step, setStep] = useState<Step>('provider');
34
+ const [selectedProvider, setSelectedProvider] = useState(0);
35
+ const [selectedModel, setSelectedModel] = useState(0);
36
+ const [apiKey, setApiKeyInput] = useState('');
37
+ const [provider, setProviderState] = useState<string>('ollama');
38
+
39
+ useInput((input, key) => {
40
+ if (key.escape) {
41
+ exit();
42
+ return;
43
+ }
44
+
45
+ if (step === 'provider') {
46
+ if (key.upArrow) {
47
+ setSelectedProvider((prev) => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
48
+ } else if (key.downArrow) {
49
+ setSelectedProvider((prev) => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
50
+ } else if (key.return) {
51
+ const selected = PROVIDERS[selectedProvider]!;
52
+ setProviderState(selected.id);
53
+ setProvider(selected.id as any);
54
+
55
+ if (selected.id === 'ollama') {
56
+ setStep('model');
57
+ } else {
58
+ setStep('apiKey');
59
+ }
60
+ }
61
+ } else if (step === 'model') {
62
+ const models = DEFAULT_MODELS[provider] || [];
63
+ if (key.upArrow) {
64
+ setSelectedModel((prev) => (prev > 0 ? prev - 1 : models.length - 1));
65
+ } else if (key.downArrow) {
66
+ setSelectedModel((prev) => (prev < models.length - 1 ? prev + 1 : 0));
67
+ } else if (key.return) {
68
+ const model = models[selectedModel]!;
69
+ setModel(model);
70
+ setStep('done');
71
+ setTimeout(() => exit(), 1500);
72
+ }
73
+ }
74
+ });
75
+
76
+ const handleApiKeySubmit = (value: string) => {
77
+ if (value.trim()) {
78
+ setApiKey(provider as any, value.trim());
79
+ setApiKeyInput(value);
80
+ setStep('model');
81
+ }
82
+ };
83
+
84
+ return (
85
+ <Box flexDirection="column" padding={1}>
86
+ <Box marginBottom={1}>
87
+ <Text bold color="cyan">
88
+ 🚀 C-napse Setup
89
+ </Text>
90
+ </Box>
91
+
92
+ {step === 'provider' && (
93
+ <Box flexDirection="column">
94
+ <Text bold>Select AI Provider:</Text>
95
+ <Text color="gray" dimColor>
96
+ (Use ↑↓ arrows, Enter to select)
97
+ </Text>
98
+ <Box marginTop={1} flexDirection="column">
99
+ {PROVIDERS.map((p, i) => (
100
+ <Box key={p.id}>
101
+ <Text color={i === selectedProvider ? 'cyan' : 'white'}>
102
+ {i === selectedProvider ? '❯ ' : ' '}
103
+ <Text bold={i === selectedProvider}>{p.name}</Text>
104
+ <Text color="gray"> - {p.desc}</Text>
105
+ </Text>
106
+ </Box>
107
+ ))}
108
+ </Box>
109
+ </Box>
110
+ )}
111
+
112
+ {step === 'apiKey' && (
113
+ <Box flexDirection="column">
114
+ <Text>
115
+ <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
116
+ </Text>
117
+ <Box marginTop={1} flexDirection="column">
118
+ <Text bold>Enter your {provider} API key:</Text>
119
+ <Text color="gray" dimColor>
120
+ (Paste with Ctrl+V or right-click, then Enter)
121
+ </Text>
122
+ <Box marginTop={1}>
123
+ <Text color="cyan">❯ </Text>
124
+ <TextInput
125
+ value={apiKey}
126
+ onChange={setApiKeyInput}
127
+ onSubmit={handleApiKeySubmit}
128
+ mask="*"
129
+ />
130
+ </Box>
131
+ </Box>
132
+ </Box>
133
+ )}
134
+
135
+ {step === 'model' && (
136
+ <Box flexDirection="column">
137
+ <Text>
138
+ <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
139
+ </Text>
140
+ {provider !== 'ollama' && (
141
+ <Text>
142
+ <Text color="green">✓</Text> API Key: <Text bold>saved</Text>
143
+ </Text>
144
+ )}
145
+ <Box marginTop={1} flexDirection="column">
146
+ <Text bold>Select Model:</Text>
147
+ <Text color="gray" dimColor>
148
+ (Use ↑↓ arrows, Enter to select)
149
+ </Text>
150
+ <Box marginTop={1} flexDirection="column">
151
+ {(DEFAULT_MODELS[provider] || []).map((model, i) => (
152
+ <Box key={model}>
153
+ <Text color={i === selectedModel ? 'cyan' : 'white'}>
154
+ {i === selectedModel ? '❯ ' : ' '}
155
+ <Text bold={i === selectedModel}>{model}</Text>
156
+ </Text>
157
+ </Box>
158
+ ))}
159
+ </Box>
160
+ </Box>
161
+ </Box>
162
+ )}
163
+
164
+ {step === 'done' && (
165
+ <Box flexDirection="column">
166
+ <Text>
167
+ <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
168
+ </Text>
169
+ {provider !== 'ollama' && (
170
+ <Text>
171
+ <Text color="green">✓</Text> API Key: <Text bold>saved</Text>
172
+ </Text>
173
+ )}
174
+ <Text>
175
+ <Text color="green">✓</Text> Model:{' '}
176
+ <Text bold>{DEFAULT_MODELS[provider]?.[selectedModel]}</Text>
177
+ </Text>
178
+ <Box marginTop={1}>
179
+ <Text color="green" bold>
180
+ ✅ Setup complete! Run `cnapse` to start chatting.
181
+ </Text>
182
+ </Box>
183
+ {provider === 'ollama' && (
184
+ <Box marginTop={1} flexDirection="column">
185
+ <Text color="yellow">
186
+ Note: Make sure the model is downloaded. Run:
187
+ </Text>
188
+ <Text color="cyan">
189
+ {' '}ollama pull {DEFAULT_MODELS[provider]?.[selectedModel]}
190
+ </Text>
191
+ </Box>
192
+ )}
193
+ </Box>
194
+ )}
195
+
196
+ <Box marginTop={2}>
197
+ <Text color="gray" dimColor>
198
+ Press Esc to cancel
199
+ </Text>
200
+ </Box>
201
+ </Box>
202
+ );
203
+ }