@projectservan8n/cnapse 0.2.1 → 0.5.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.
- package/dist/Setup-Q32JPHGP.js +174 -0
- package/dist/chunk-COKO6V5J.js +50 -0
- package/dist/index.js +1684 -186
- package/package.json +4 -2
- package/src/agents/coder.ts +62 -0
- package/src/agents/computer.ts +61 -0
- package/src/agents/executor.ts +179 -0
- package/src/agents/filer.ts +56 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/router.ts +160 -0
- package/src/agents/shell.ts +67 -0
- package/src/agents/types.ts +80 -0
- package/src/components/App.tsx +222 -124
- package/src/components/Header.tsx +11 -1
- package/src/components/HelpMenu.tsx +144 -0
- package/src/components/ProviderSelector.tsx +176 -0
- package/src/components/Setup.tsx +203 -0
- package/src/components/TaskProgress.tsx +68 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useChat.ts +149 -0
- package/src/hooks/useTasks.ts +63 -0
- package/src/hooks/useTelegram.ts +91 -0
- package/src/hooks/useVision.ts +47 -0
- package/src/index.tsx +3 -50
- package/src/lib/api.ts +2 -2
- package/src/lib/config.ts +21 -0
- package/src/lib/screen.ts +118 -0
- package/src/lib/tasks.ts +483 -0
- package/src/lib/vision.ts +254 -0
- package/src/services/telegram.ts +278 -0
- package/src/tools/clipboard.ts +55 -0
- package/src/tools/computer.ts +454 -0
- package/src/tools/filesystem.ts +272 -0
- package/src/tools/index.ts +35 -0
- package/src/tools/network.ts +204 -0
- package/src/tools/process.ts +194 -0
- package/src/tools/shell.ts +140 -0
- package/src/tools/vision.ts +65 -0
- package/src/types/screenshot-desktop.d.ts +10 -0
package/src/components/App.tsx
CHANGED
|
@@ -1,155 +1,253 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback } 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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
timestamp: Date;
|
|
14
|
-
isStreaming?: boolean;
|
|
15
|
-
}
|
|
7
|
+
import { HelpMenu } from './HelpMenu.js';
|
|
8
|
+
import { ProviderSelector } from './ProviderSelector.js';
|
|
9
|
+
import { getConfig } from '../lib/config.js';
|
|
10
|
+
import { useChat, useVision, useTelegram, useTasks } from '../hooks/index.js';
|
|
11
|
+
|
|
12
|
+
type OverlayType = 'none' | 'help' | 'provider';
|
|
16
13
|
|
|
17
14
|
export function App() {
|
|
18
15
|
const { exit } = useApp();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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',
|
|
24
|
-
timestamp: new Date(),
|
|
25
|
-
},
|
|
26
|
-
]);
|
|
27
|
-
const [input, setInput] = useState('');
|
|
28
|
-
const [isProcessing, setIsProcessing] = useState(false);
|
|
16
|
+
|
|
17
|
+
// UI State
|
|
18
|
+
const [overlay, setOverlay] = useState<OverlayType>('none');
|
|
19
|
+
const [screenWatch, setScreenWatch] = useState(false);
|
|
29
20
|
const [status, setStatus] = useState('Ready');
|
|
30
|
-
const [error, setError] = useState<string | null>(null);
|
|
31
21
|
|
|
22
|
+
// Feature hooks
|
|
23
|
+
const chat = useChat(screenWatch);
|
|
24
|
+
const vision = useVision();
|
|
25
|
+
const telegram = useTelegram((msg) => {
|
|
26
|
+
chat.addSystemMessage(`📱 Telegram [${msg.from}]: ${msg.text}`);
|
|
27
|
+
});
|
|
28
|
+
const tasks = useTasks((task, step) => {
|
|
29
|
+
if (step.status === 'running') {
|
|
30
|
+
setStatus(`Running: ${step.description}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Keyboard shortcuts
|
|
32
35
|
useInput((inputChar, key) => {
|
|
33
|
-
if (
|
|
34
|
-
|
|
36
|
+
if (overlay !== 'none') return;
|
|
37
|
+
|
|
38
|
+
if (key.ctrl && inputChar === 'c') exit();
|
|
39
|
+
if (key.ctrl && inputChar === 'l') chat.clearMessages();
|
|
40
|
+
if (key.ctrl && inputChar === 'h') setOverlay('help');
|
|
41
|
+
if (key.ctrl && inputChar === 'p') setOverlay('provider');
|
|
42
|
+
if (key.ctrl && inputChar === 'w') {
|
|
43
|
+
setScreenWatch(prev => {
|
|
44
|
+
const newState = !prev;
|
|
45
|
+
chat.addSystemMessage(newState
|
|
46
|
+
? '🖥️ Screen watching enabled.'
|
|
47
|
+
: '🖥️ Screen watching disabled.'
|
|
48
|
+
);
|
|
49
|
+
return newState;
|
|
50
|
+
});
|
|
35
51
|
}
|
|
36
|
-
if (key.ctrl && inputChar === '
|
|
37
|
-
|
|
38
|
-
setError(null);
|
|
52
|
+
if (key.ctrl && inputChar === 't') {
|
|
53
|
+
handleTelegramToggle();
|
|
39
54
|
}
|
|
40
55
|
});
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
// Command handlers
|
|
58
|
+
const handleCommand = useCallback(async (cmd: string) => {
|
|
59
|
+
const parts = cmd.slice(1).split(' ');
|
|
60
|
+
const command = parts[0];
|
|
61
|
+
const args = parts.slice(1).join(' ');
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
switch (command) {
|
|
64
|
+
case 'clear':
|
|
65
|
+
chat.clearMessages();
|
|
66
|
+
chat.addSystemMessage('Chat cleared.');
|
|
67
|
+
break;
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
case 'help':
|
|
70
|
+
setOverlay('help');
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'provider':
|
|
74
|
+
case 'model':
|
|
75
|
+
setOverlay('provider');
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'config': {
|
|
79
|
+
const config = getConfig();
|
|
80
|
+
chat.addSystemMessage(
|
|
81
|
+
`⚙️ Configuration:\n` +
|
|
82
|
+
` Provider: ${config.provider}\n` +
|
|
83
|
+
` Model: ${config.model}\n` +
|
|
84
|
+
` Ollama: ${config.ollamaHost}\n\n` +
|
|
85
|
+
`Use /provider to change`
|
|
86
|
+
);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'screen':
|
|
91
|
+
await handleScreenCommand();
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'watch':
|
|
95
|
+
setScreenWatch(prev => {
|
|
96
|
+
const newState = !prev;
|
|
97
|
+
chat.addSystemMessage(newState
|
|
98
|
+
? '🖥️ Screen watching enabled.'
|
|
99
|
+
: '🖥️ Screen watching disabled.'
|
|
100
|
+
);
|
|
101
|
+
return newState;
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'telegram':
|
|
106
|
+
await handleTelegramToggle();
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'task':
|
|
110
|
+
if (args) {
|
|
111
|
+
await handleTaskCommand(args);
|
|
112
|
+
} else {
|
|
113
|
+
chat.addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'memory':
|
|
118
|
+
if (args === 'clear') {
|
|
119
|
+
tasks.clearMemory();
|
|
120
|
+
chat.addSystemMessage('🧠 Task memory cleared.');
|
|
121
|
+
} else {
|
|
122
|
+
const stats = tasks.getMemoryStats();
|
|
123
|
+
chat.addSystemMessage(
|
|
124
|
+
`🧠 Task Memory:\n\n` +
|
|
125
|
+
` Learned patterns: ${stats.patternCount}\n` +
|
|
126
|
+
` Total successful uses: ${stats.totalUses}\n\n` +
|
|
127
|
+
(stats.topPatterns.length > 0
|
|
128
|
+
? ` Top patterns:\n${stats.topPatterns.map(p => ` • ${p}`).join('\n')}\n\n`
|
|
129
|
+
: ' No patterns learned yet.\n\n') +
|
|
130
|
+
`The more you use /task, the smarter it gets!\n` +
|
|
131
|
+
`Use /memory clear to reset.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'quit':
|
|
137
|
+
case 'exit':
|
|
138
|
+
exit();
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
chat.addSystemMessage(`Unknown command: ${command}\nType /help for commands`);
|
|
53
143
|
}
|
|
144
|
+
}, [chat, exit]);
|
|
54
145
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
content: userInput,
|
|
60
|
-
timestamp: new Date(),
|
|
61
|
-
};
|
|
62
|
-
setMessages((prev) => [...prev, userMsg]);
|
|
63
|
-
|
|
64
|
-
// Add placeholder for assistant
|
|
65
|
-
const assistantId = (Date.now() + 1).toString();
|
|
66
|
-
setMessages((prev) => [
|
|
67
|
-
...prev,
|
|
68
|
-
{
|
|
69
|
-
id: assistantId,
|
|
70
|
-
role: 'assistant',
|
|
71
|
-
content: '',
|
|
72
|
-
timestamp: new Date(),
|
|
73
|
-
isStreaming: true,
|
|
74
|
-
},
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
setIsProcessing(true);
|
|
78
|
-
setStatus('Thinking...');
|
|
146
|
+
// Screen command
|
|
147
|
+
const handleScreenCommand = useCallback(async () => {
|
|
148
|
+
chat.addSystemMessage('📸 Analyzing screen...');
|
|
149
|
+
setStatus('Analyzing...');
|
|
79
150
|
|
|
80
151
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
84
|
-
.slice(-10)
|
|
85
|
-
.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
|
86
|
-
|
|
87
|
-
apiMessages.push({ role: 'user', content: userInput });
|
|
88
|
-
|
|
89
|
-
const response = await chat(apiMessages);
|
|
90
|
-
|
|
91
|
-
// Update assistant message with response
|
|
92
|
-
setMessages((prev) =>
|
|
93
|
-
prev.map((m) =>
|
|
94
|
-
m.id === assistantId
|
|
95
|
-
? { ...m, content: response.content || '(no response)', isStreaming: false }
|
|
96
|
-
: m
|
|
97
|
-
)
|
|
98
|
-
);
|
|
152
|
+
const description = await vision.analyze();
|
|
153
|
+
chat.addSystemMessage(`🖥️ Screen:\n\n${description}`);
|
|
99
154
|
} catch (err) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
chat.addSystemMessage(`❌ ${vision.error || 'Vision failed'}`);
|
|
156
|
+
} finally {
|
|
157
|
+
setStatus('Ready');
|
|
158
|
+
}
|
|
159
|
+
}, [chat, vision]);
|
|
160
|
+
|
|
161
|
+
// Telegram toggle
|
|
162
|
+
const handleTelegramToggle = useCallback(async () => {
|
|
163
|
+
if (telegram.isEnabled) {
|
|
164
|
+
await telegram.stop();
|
|
165
|
+
chat.addSystemMessage('📱 Telegram stopped.');
|
|
166
|
+
} else {
|
|
167
|
+
chat.addSystemMessage('📱 Starting Telegram...');
|
|
168
|
+
setStatus('Starting Telegram...');
|
|
169
|
+
try {
|
|
170
|
+
await telegram.start();
|
|
171
|
+
chat.addSystemMessage(
|
|
172
|
+
'📱 Telegram started!\n' +
|
|
173
|
+
'Send /start to your bot to connect.\n' +
|
|
174
|
+
'Commands: /screen, /describe, /run, /status'
|
|
175
|
+
);
|
|
176
|
+
} catch {
|
|
177
|
+
chat.addSystemMessage(`❌ ${telegram.error || 'Telegram failed'}`);
|
|
178
|
+
} finally {
|
|
179
|
+
setStatus('Ready');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}, [chat, telegram]);
|
|
183
|
+
|
|
184
|
+
// Task command
|
|
185
|
+
const handleTaskCommand = useCallback(async (description: string) => {
|
|
186
|
+
chat.addSystemMessage(`📋 Parsing: ${description}`);
|
|
187
|
+
setStatus('Parsing task...');
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const task = await tasks.run(description);
|
|
191
|
+
chat.addSystemMessage(`\n${tasks.format(task)}`);
|
|
192
|
+
chat.addSystemMessage(task.status === 'completed'
|
|
193
|
+
? '✅ Task completed!'
|
|
194
|
+
: '❌ Task failed.'
|
|
109
195
|
);
|
|
196
|
+
} catch {
|
|
197
|
+
chat.addSystemMessage(`❌ ${tasks.error || 'Task failed'}`);
|
|
110
198
|
} finally {
|
|
111
|
-
setIsProcessing(false);
|
|
112
199
|
setStatus('Ready');
|
|
113
200
|
}
|
|
114
|
-
};
|
|
201
|
+
}, [chat, tasks]);
|
|
115
202
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
203
|
+
// Submit handler
|
|
204
|
+
const handleSubmit = useCallback(async (value: string) => {
|
|
205
|
+
if (!value.trim()) return;
|
|
119
206
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
addSystemMessage(
|
|
127
|
-
'Commands:\n /clear - Clear chat history\n /help - Show this help\n\nJust type naturally to chat with the AI!'
|
|
128
|
-
);
|
|
129
|
-
break;
|
|
130
|
-
default:
|
|
131
|
-
addSystemMessage(`Unknown command: ${command}`);
|
|
207
|
+
if (value.startsWith('/')) {
|
|
208
|
+
await handleCommand(value);
|
|
209
|
+
} else {
|
|
210
|
+
setStatus('Thinking...');
|
|
211
|
+
await chat.sendMessage(value);
|
|
212
|
+
setStatus('Ready');
|
|
132
213
|
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
214
|
+
}, [chat, handleCommand]);
|
|
215
|
+
|
|
216
|
+
// Provider selection callback
|
|
217
|
+
const handleProviderSelect = useCallback((provider: string, model: string) => {
|
|
218
|
+
chat.addSystemMessage(`✅ Updated: ${provider} / ${model}`);
|
|
219
|
+
}, [chat]);
|
|
220
|
+
|
|
221
|
+
// Render overlays
|
|
222
|
+
if (overlay === 'help') {
|
|
223
|
+
return (
|
|
224
|
+
<Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
|
|
225
|
+
<HelpMenu
|
|
226
|
+
onClose={() => setOverlay('none')}
|
|
227
|
+
onSelect={(cmd) => { setOverlay('none'); handleCommand(cmd); }}
|
|
228
|
+
/>
|
|
229
|
+
</Box>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (overlay === 'provider') {
|
|
234
|
+
return (
|
|
235
|
+
<Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
|
|
236
|
+
<ProviderSelector
|
|
237
|
+
onClose={() => setOverlay('none')}
|
|
238
|
+
onSelect={handleProviderSelect}
|
|
239
|
+
/>
|
|
240
|
+
</Box>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Main UI
|
|
245
|
+
const visibleMessages = chat.messages.slice(-20);
|
|
246
|
+
const isProcessing = chat.isProcessing || vision.isAnalyzing || tasks.isRunning || telegram.isStarting;
|
|
149
247
|
|
|
150
248
|
return (
|
|
151
249
|
<Box flexDirection="column" height="100%">
|
|
152
|
-
<Header />
|
|
250
|
+
<Header screenWatch={screenWatch} telegramEnabled={telegram.isEnabled} />
|
|
153
251
|
|
|
154
252
|
<Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="gray" padding={1}>
|
|
155
253
|
<Text bold color="gray"> Chat </Text>
|
|
@@ -164,15 +262,15 @@ export function App() {
|
|
|
164
262
|
))}
|
|
165
263
|
</Box>
|
|
166
264
|
|
|
167
|
-
{error && (
|
|
265
|
+
{chat.error && (
|
|
168
266
|
<Box marginY={1}>
|
|
169
|
-
<Text color="red">Error: {error}</Text>
|
|
267
|
+
<Text color="red">Error: {chat.error}</Text>
|
|
170
268
|
</Box>
|
|
171
269
|
)}
|
|
172
270
|
|
|
173
271
|
<ChatInput
|
|
174
|
-
value=
|
|
175
|
-
onChange={
|
|
272
|
+
value=""
|
|
273
|
+
onChange={() => {}}
|
|
176
274
|
onSubmit={handleSubmit}
|
|
177
275
|
isProcessing={isProcessing}
|
|
178
276
|
/>
|
|
@@ -11,7 +11,12 @@ const ASCII_BANNER = `
|
|
|
11
11
|
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝
|
|
12
12
|
`.trim();
|
|
13
13
|
|
|
14
|
-
|
|
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,144 @@
|
|
|
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
|
+
{ command: '/memory', description: 'View/clear learned task patterns', category: 'actions' },
|
|
22
|
+
|
|
23
|
+
// Settings
|
|
24
|
+
{ command: '/config', description: 'Show/edit configuration', category: 'settings' },
|
|
25
|
+
{ command: '/watch', shortcut: 'Ctrl+W', description: 'Toggle screen watching', category: 'settings' },
|
|
26
|
+
{ command: '/model', description: 'Change AI model', category: 'settings' },
|
|
27
|
+
{ command: '/provider', shortcut: 'Ctrl+P', description: 'Change AI provider', category: 'settings' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
interface HelpMenuProps {
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onSelect: (command: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function HelpMenu({ onClose, onSelect }: HelpMenuProps) {
|
|
36
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
37
|
+
const [selectedCategory, setSelectedCategory] = useState<'all' | MenuItem['category']>('all');
|
|
38
|
+
|
|
39
|
+
const filteredItems = selectedCategory === 'all'
|
|
40
|
+
? MENU_ITEMS
|
|
41
|
+
: MENU_ITEMS.filter(item => item.category === selectedCategory);
|
|
42
|
+
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (key.escape) {
|
|
45
|
+
onClose();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (key.upArrow) {
|
|
50
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : filteredItems.length - 1));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (key.downArrow) {
|
|
54
|
+
setSelectedIndex(prev => (prev < filteredItems.length - 1 ? prev + 1 : 0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (key.leftArrow || key.rightArrow) {
|
|
58
|
+
const categories: Array<'all' | MenuItem['category']> = ['all', 'navigation', 'actions', 'settings'];
|
|
59
|
+
const currentIdx = categories.indexOf(selectedCategory);
|
|
60
|
+
if (key.leftArrow) {
|
|
61
|
+
setSelectedCategory(categories[currentIdx > 0 ? currentIdx - 1 : categories.length - 1]!);
|
|
62
|
+
} else {
|
|
63
|
+
setSelectedCategory(categories[currentIdx < categories.length - 1 ? currentIdx + 1 : 0]!);
|
|
64
|
+
}
|
|
65
|
+
setSelectedIndex(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (key.return) {
|
|
69
|
+
const item = filteredItems[selectedIndex];
|
|
70
|
+
if (item) {
|
|
71
|
+
onSelect(item.command);
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const categories: Array<{ key: 'all' | MenuItem['category']; label: string }> = [
|
|
78
|
+
{ key: 'all', label: 'All' },
|
|
79
|
+
{ key: 'navigation', label: 'Navigation' },
|
|
80
|
+
{ key: 'actions', label: 'Actions' },
|
|
81
|
+
{ key: 'settings', label: 'Settings' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box
|
|
86
|
+
flexDirection="column"
|
|
87
|
+
borderStyle="round"
|
|
88
|
+
borderColor="cyan"
|
|
89
|
+
padding={1}
|
|
90
|
+
width={60}
|
|
91
|
+
>
|
|
92
|
+
{/* Header */}
|
|
93
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
94
|
+
<Text bold color="cyan">C-napse Help</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
|
|
97
|
+
{/* Category Tabs */}
|
|
98
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
99
|
+
{categories.map((cat, idx) => (
|
|
100
|
+
<React.Fragment key={cat.key}>
|
|
101
|
+
<Text
|
|
102
|
+
color={selectedCategory === cat.key ? 'cyan' : 'gray'}
|
|
103
|
+
bold={selectedCategory === cat.key}
|
|
104
|
+
>
|
|
105
|
+
{cat.label}
|
|
106
|
+
</Text>
|
|
107
|
+
{idx < categories.length - 1 && <Text color="gray"> │ </Text>}
|
|
108
|
+
</React.Fragment>
|
|
109
|
+
))}
|
|
110
|
+
</Box>
|
|
111
|
+
|
|
112
|
+
<Box marginBottom={1}>
|
|
113
|
+
<Text color="gray" dimColor>Use ←→ to switch tabs, ↑↓ to navigate, Enter to select</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
|
|
116
|
+
{/* Menu Items */}
|
|
117
|
+
<Box flexDirection="column">
|
|
118
|
+
{filteredItems.map((item, index) => (
|
|
119
|
+
<Box key={item.command}>
|
|
120
|
+
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
|
121
|
+
{index === selectedIndex ? '❯ ' : ' '}
|
|
122
|
+
</Text>
|
|
123
|
+
<Box width={12}>
|
|
124
|
+
<Text bold={index === selectedIndex} color={index === selectedIndex ? 'cyan' : 'white'}>
|
|
125
|
+
{item.command}
|
|
126
|
+
</Text>
|
|
127
|
+
</Box>
|
|
128
|
+
{item.shortcut && (
|
|
129
|
+
<Box width={10}>
|
|
130
|
+
<Text color="yellow" dimColor>{item.shortcut}</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
)}
|
|
133
|
+
<Text color="gray">{item.description}</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
))}
|
|
136
|
+
</Box>
|
|
137
|
+
|
|
138
|
+
{/* Footer */}
|
|
139
|
+
<Box marginTop={1} justifyContent="center">
|
|
140
|
+
<Text color="gray" dimColor>Press Esc to close</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
}
|