@gxp-dev/tools 2.0.15 → 2.0.17
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/bin/lib/tui/App.tsx +68 -48
- package/bin/lib/tui/components/AIPanel.tsx +180 -0
- package/bin/lib/tui/components/CommandInput.tsx +87 -36
- package/bin/lib/tui/services/AIService.ts +509 -0
- package/bin/lib/tui/services/index.ts +11 -0
- package/browser-extensions/chrome/background.js +16 -0
- package/browser-extensions/chrome/popup.html +63 -19
- package/browser-extensions/chrome/popup.js +47 -9
- package/browser-extensions/firefox/popup.html +67 -14
- package/browser-extensions/firefox/popup.js +45 -9
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +65 -45
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/AIPanel.d.ts +7 -0
- package/dist/tui/components/AIPanel.d.ts.map +1 -0
- package/dist/tui/components/AIPanel.js +116 -0
- package/dist/tui/components/AIPanel.js.map +1 -0
- package/dist/tui/components/CommandInput.d.ts.map +1 -1
- package/dist/tui/components/CommandInput.js +53 -16
- package/dist/tui/components/CommandInput.js.map +1 -1
- package/dist/tui/services/AIService.d.ts +48 -0
- package/dist/tui/services/AIService.d.ts.map +1 -0
- package/dist/tui/services/AIService.js +429 -0
- package/dist/tui/services/AIService.js.map +1 -0
- package/dist/tui/services/index.d.ts +1 -0
- package/dist/tui/services/index.d.ts.map +1 -1
- package/dist/tui/services/index.js +1 -0
- package/dist/tui/services/index.js.map +1 -1
- package/package.json +1 -1
package/bin/lib/tui/App.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import Header from './components/Header.js';
|
|
|
5
5
|
import TabBar from './components/TabBar.js';
|
|
6
6
|
import LogPanel from './components/LogPanel.js';
|
|
7
7
|
import CommandInput from './components/CommandInput.js';
|
|
8
|
-
import
|
|
8
|
+
import AIPanel from './components/AIPanel.js';
|
|
9
9
|
import {
|
|
10
10
|
serviceManager,
|
|
11
11
|
startVite,
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
sendSocketEvent,
|
|
19
19
|
ServiceStatus,
|
|
20
20
|
BrowserType,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
aiService,
|
|
22
|
+
getAvailableProviders,
|
|
23
|
+
getProviderStatus,
|
|
24
|
+
AIProvider,
|
|
24
25
|
} from './services/index.js';
|
|
25
26
|
|
|
26
27
|
export interface Service {
|
|
@@ -46,7 +47,7 @@ export interface AppProps {
|
|
|
46
47
|
export default function App({ autoStart, args }: AppProps) {
|
|
47
48
|
const { exit } = useApp();
|
|
48
49
|
const { stdout } = useStdout();
|
|
49
|
-
const [
|
|
50
|
+
const [showAIPanel, setShowAIPanel] = useState(false);
|
|
50
51
|
const [services, setServices] = useState<Service[]>([]);
|
|
51
52
|
const [activeTab, setActiveTab] = useState(0);
|
|
52
53
|
const [suggestionRows, setSuggestionRows] = useState(0);
|
|
@@ -226,9 +227,8 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
226
227
|
exit();
|
|
227
228
|
break;
|
|
228
229
|
|
|
229
|
-
case 'gemini':
|
|
230
230
|
case 'ai':
|
|
231
|
-
|
|
231
|
+
handleAICommand(cmdArgs);
|
|
232
232
|
break;
|
|
233
233
|
|
|
234
234
|
case 'extract-config':
|
|
@@ -385,68 +385,89 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
385
385
|
syncServices();
|
|
386
386
|
};
|
|
387
387
|
|
|
388
|
-
const
|
|
388
|
+
const handleAICommand = async (cmdArgs: string[]) => {
|
|
389
389
|
const subCommand = cmdArgs[0];
|
|
390
390
|
|
|
391
391
|
switch (subCommand) {
|
|
392
|
-
case '
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
392
|
+
case 'model':
|
|
393
|
+
// Set or show current AI provider
|
|
394
|
+
const providerArg = cmdArgs[1] as AIProvider | undefined;
|
|
395
|
+
if (providerArg) {
|
|
396
|
+
const result = aiService.setProvider(providerArg);
|
|
397
|
+
if (result.success) {
|
|
398
|
+
addSystemLog(`✅ ${result.message}`);
|
|
399
|
+
} else {
|
|
400
|
+
addSystemLog(`❌ ${result.message}`);
|
|
401
|
+
}
|
|
398
402
|
} else {
|
|
399
|
-
|
|
403
|
+
// Show current provider and available providers
|
|
404
|
+
const current = aiService.getProviderInfo();
|
|
405
|
+
const providers = getAvailableProviders();
|
|
406
|
+
let message = `Current AI provider: ${current ? getProviderStatus(current) : 'None'}\n\nAvailable providers:`;
|
|
407
|
+
for (const p of providers) {
|
|
408
|
+
const status = p.available ? getProviderStatus(p) : `${p.name} (not available)`;
|
|
409
|
+
const marker = p.id === current?.id ? ' ← current' : '';
|
|
410
|
+
message += `\n ${p.id}: ${status}${marker}`;
|
|
411
|
+
if (!p.available && p.reason) {
|
|
412
|
+
message += `\n ${p.reason}`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
message += '\n\nUsage: /ai model <claude|codex|gemini>';
|
|
416
|
+
addSystemLog(message);
|
|
400
417
|
}
|
|
401
418
|
break;
|
|
402
419
|
|
|
403
|
-
case 'logout':
|
|
404
|
-
case 'disable':
|
|
405
|
-
clearAuthTokens();
|
|
406
|
-
addSystemLog('Logged out from Gemini AI.');
|
|
407
|
-
break;
|
|
408
|
-
|
|
409
420
|
case 'status':
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
421
|
+
// Show detailed status of all providers
|
|
422
|
+
const providers = getAvailableProviders();
|
|
423
|
+
const currentProvider = aiService.getProvider();
|
|
424
|
+
let statusMsg = 'AI Provider Status:\n';
|
|
425
|
+
for (const p of providers) {
|
|
426
|
+
const icon = p.available ? '✅' : '❌';
|
|
427
|
+
const current = p.id === currentProvider ? ' (current)' : '';
|
|
428
|
+
statusMsg += `\n ${icon} ${getProviderStatus(p)}${current}`;
|
|
429
|
+
if (!p.available && p.reason) {
|
|
430
|
+
statusMsg += `\n ${p.reason}`;
|
|
431
|
+
}
|
|
414
432
|
}
|
|
433
|
+
addSystemLog(statusMsg);
|
|
415
434
|
break;
|
|
416
435
|
|
|
417
436
|
case 'ask':
|
|
418
437
|
// Quick question without opening panel
|
|
419
438
|
const question = cmdArgs.slice(1).join(' ');
|
|
420
439
|
if (!question) {
|
|
421
|
-
addSystemLog('Usage: /
|
|
440
|
+
addSystemLog('Usage: /ai ask <your question>');
|
|
422
441
|
return;
|
|
423
442
|
}
|
|
424
|
-
if (!
|
|
425
|
-
addSystemLog(
|
|
443
|
+
if (!aiService.isAvailable()) {
|
|
444
|
+
addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
|
|
426
445
|
return;
|
|
427
446
|
}
|
|
428
|
-
|
|
447
|
+
const providerName = aiService.getProviderInfo()?.name || 'AI';
|
|
448
|
+
addSystemLog(`Asking ${providerName}: ${question}`);
|
|
429
449
|
try {
|
|
430
|
-
|
|
431
|
-
const response = await
|
|
432
|
-
addSystemLog(
|
|
450
|
+
aiService.loadProjectContext(process.cwd());
|
|
451
|
+
const response = await aiService.sendMessage(question);
|
|
452
|
+
addSystemLog(`${providerName}: ${response}`);
|
|
433
453
|
} catch (err) {
|
|
434
454
|
addSystemLog(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
435
455
|
}
|
|
436
456
|
break;
|
|
437
457
|
|
|
438
458
|
case 'clear':
|
|
439
|
-
|
|
459
|
+
aiService.clearConversation();
|
|
440
460
|
addSystemLog('Conversation history cleared.');
|
|
441
461
|
break;
|
|
442
462
|
|
|
463
|
+
case 'chat':
|
|
443
464
|
default:
|
|
444
|
-
//
|
|
445
|
-
if (!
|
|
446
|
-
addSystemLog(
|
|
465
|
+
// Open AI chat panel
|
|
466
|
+
if (!aiService.isAvailable()) {
|
|
467
|
+
addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
|
|
447
468
|
return;
|
|
448
469
|
}
|
|
449
|
-
|
|
470
|
+
setShowAIPanel(true);
|
|
450
471
|
}
|
|
451
472
|
};
|
|
452
473
|
|
|
@@ -650,13 +671,12 @@ Available commands:
|
|
|
650
671
|
/extract-config -o Overwrite existing values
|
|
651
672
|
|
|
652
673
|
AI Assistant:
|
|
653
|
-
/
|
|
654
|
-
/
|
|
655
|
-
/
|
|
656
|
-
/
|
|
657
|
-
/
|
|
658
|
-
/
|
|
659
|
-
/ai Alias for /gemini
|
|
674
|
+
/ai Open AI chat with current provider
|
|
675
|
+
/ai model Show available AI providers
|
|
676
|
+
/ai model <name> Switch to claude, codex, or gemini
|
|
677
|
+
/ai ask <query> Quick question to AI
|
|
678
|
+
/ai status Check provider availability
|
|
679
|
+
/ai clear Clear conversation history
|
|
660
680
|
|
|
661
681
|
Service Management:
|
|
662
682
|
/stop [service] Stop current or specified service
|
|
@@ -678,11 +698,11 @@ Keyboard shortcuts:
|
|
|
678
698
|
Esc Clear input
|
|
679
699
|
`;
|
|
680
700
|
|
|
681
|
-
// Show
|
|
682
|
-
if (
|
|
701
|
+
// Show AI panel
|
|
702
|
+
if (showAIPanel) {
|
|
683
703
|
return (
|
|
684
|
-
<
|
|
685
|
-
onClose={() =>
|
|
704
|
+
<AIPanel
|
|
705
|
+
onClose={() => setShowAIPanel(false)}
|
|
686
706
|
onLog={addSystemLog}
|
|
687
707
|
/>
|
|
688
708
|
);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import {
|
|
5
|
+
aiService,
|
|
6
|
+
getProviderStatus,
|
|
7
|
+
} from '../services/index.js';
|
|
8
|
+
|
|
9
|
+
interface AIPanelProps {
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onLog: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Message {
|
|
15
|
+
role: 'user' | 'assistant' | 'system';
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function AIPanel({ onClose, onLog }: AIPanelProps) {
|
|
20
|
+
const { stdout } = useStdout();
|
|
21
|
+
const [input, setInput] = useState('');
|
|
22
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
23
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
24
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
25
|
+
|
|
26
|
+
// Calculate available height for messages
|
|
27
|
+
const maxMessageLines = stdout ? Math.max(5, stdout.rows - 8) : 10;
|
|
28
|
+
|
|
29
|
+
// Get provider info for display
|
|
30
|
+
const providerInfo = aiService.getProviderInfo();
|
|
31
|
+
const providerName = providerInfo?.name || 'AI';
|
|
32
|
+
|
|
33
|
+
// Check provider availability on mount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!aiService.isAvailable()) {
|
|
36
|
+
setMessages([{
|
|
37
|
+
role: 'system',
|
|
38
|
+
content: `${providerName} is not available. Run /ai model to select a different provider, or install the required CLI.`,
|
|
39
|
+
}]);
|
|
40
|
+
} else {
|
|
41
|
+
const status = getProviderStatus(providerInfo!);
|
|
42
|
+
setMessages([{
|
|
43
|
+
role: 'system',
|
|
44
|
+
content: `${status} ready.\nType your message and press Enter. Press Escape to close.`,
|
|
45
|
+
}]);
|
|
46
|
+
// Load project context
|
|
47
|
+
aiService.loadProjectContext(process.cwd());
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Handle keyboard input
|
|
52
|
+
useInput((char, key) => {
|
|
53
|
+
if (key.escape) {
|
|
54
|
+
onClose();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Scroll with Shift+Up/Down
|
|
59
|
+
if (key.shift && key.upArrow) {
|
|
60
|
+
setScrollOffset(prev => Math.min(prev + 1, Math.max(0, messages.length - maxMessageLines)));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.shift && key.downArrow) {
|
|
64
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const handleSubmit = async (value: string) => {
|
|
70
|
+
if (!value.trim() || isLoading) return;
|
|
71
|
+
|
|
72
|
+
const userMessage = value.trim();
|
|
73
|
+
setInput('');
|
|
74
|
+
|
|
75
|
+
// Add user message
|
|
76
|
+
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
|
|
77
|
+
setIsLoading(true);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await aiService.sendMessage(userMessage);
|
|
81
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
|
82
|
+
setScrollOffset(0); // Auto-scroll to bottom
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
85
|
+
setMessages(prev => [...prev, {
|
|
86
|
+
role: 'system',
|
|
87
|
+
content: `Error: ${errorMessage}`,
|
|
88
|
+
}]);
|
|
89
|
+
onLog(`${providerName} error: ${errorMessage}`);
|
|
90
|
+
} finally {
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Render messages with scrolling
|
|
96
|
+
const renderMessages = () => {
|
|
97
|
+
const start = Math.max(0, messages.length - maxMessageLines - scrollOffset);
|
|
98
|
+
const end = messages.length - scrollOffset;
|
|
99
|
+
const visibleMessages = messages.slice(start, end);
|
|
100
|
+
|
|
101
|
+
return visibleMessages.map((msg, idx) => {
|
|
102
|
+
let color: string;
|
|
103
|
+
let prefix: string;
|
|
104
|
+
|
|
105
|
+
switch (msg.role) {
|
|
106
|
+
case 'user':
|
|
107
|
+
color = 'cyan';
|
|
108
|
+
prefix = 'You: ';
|
|
109
|
+
break;
|
|
110
|
+
case 'assistant':
|
|
111
|
+
color = 'green';
|
|
112
|
+
prefix = `${providerName}: `;
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
color = 'yellow';
|
|
116
|
+
prefix = '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Box key={start + idx} flexDirection="column" marginBottom={1}>
|
|
121
|
+
<Text color={color} bold>{prefix}</Text>
|
|
122
|
+
<Text wrap="wrap">{msg.content}</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Get border color based on provider
|
|
129
|
+
const getBorderColor = (): string => {
|
|
130
|
+
switch (aiService.getProvider()) {
|
|
131
|
+
case 'claude':
|
|
132
|
+
return 'magenta';
|
|
133
|
+
case 'codex':
|
|
134
|
+
return 'green';
|
|
135
|
+
case 'gemini':
|
|
136
|
+
return 'blue';
|
|
137
|
+
default:
|
|
138
|
+
return 'gray';
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const borderColor = getBorderColor();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Box flexDirection="column" height="100%" borderStyle="double" borderColor={borderColor}>
|
|
146
|
+
<Box paddingX={1} justifyContent="space-between">
|
|
147
|
+
<Text bold color={borderColor}>{providerName} AI Assistant</Text>
|
|
148
|
+
<Text color="gray" dimColor>Esc to close · /ai model to switch</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
|
|
151
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
152
|
+
{messages.length > maxMessageLines + scrollOffset && (
|
|
153
|
+
<Text color="gray" dimColor>
|
|
154
|
+
... {messages.length - maxMessageLines - scrollOffset} earlier messages
|
|
155
|
+
</Text>
|
|
156
|
+
)}
|
|
157
|
+
{renderMessages()}
|
|
158
|
+
{scrollOffset > 0 && (
|
|
159
|
+
<Text color="gray" dimColor>... {scrollOffset} newer messages below</Text>
|
|
160
|
+
)}
|
|
161
|
+
</Box>
|
|
162
|
+
|
|
163
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
|
164
|
+
{isLoading ? (
|
|
165
|
+
<Text color="yellow">Thinking...</Text>
|
|
166
|
+
) : (
|
|
167
|
+
<Box>
|
|
168
|
+
<Text color={borderColor}>> </Text>
|
|
169
|
+
<TextInput
|
|
170
|
+
value={input}
|
|
171
|
+
onChange={setInput}
|
|
172
|
+
onSubmit={handleSubmit}
|
|
173
|
+
placeholder={`Ask ${providerName} something...`}
|
|
174
|
+
/>
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
</Box>
|
|
178
|
+
</Box>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -35,13 +35,14 @@ const COMMANDS = [
|
|
|
35
35
|
{ cmd: '/extract-config', args: '--overwrite', desc: 'Overwrite existing config values' },
|
|
36
36
|
|
|
37
37
|
// AI commands
|
|
38
|
-
{ cmd: '/
|
|
39
|
-
{ cmd: '/
|
|
40
|
-
{ cmd: '/
|
|
41
|
-
{ cmd: '/
|
|
42
|
-
{ cmd: '/
|
|
43
|
-
{ cmd: '/
|
|
44
|
-
{ cmd: '/ai', args: '', desc: '
|
|
38
|
+
{ cmd: '/ai', args: '', desc: 'Open AI chat with current provider' },
|
|
39
|
+
{ cmd: '/ai', args: 'model', desc: 'Show available AI providers' },
|
|
40
|
+
{ cmd: '/ai', args: 'model claude', desc: 'Switch to Claude AI' },
|
|
41
|
+
{ cmd: '/ai', args: 'model codex', desc: 'Switch to Codex AI' },
|
|
42
|
+
{ cmd: '/ai', args: 'model gemini', desc: 'Switch to Gemini AI' },
|
|
43
|
+
{ cmd: '/ai', args: 'ask <query>', desc: 'Quick AI question' },
|
|
44
|
+
{ cmd: '/ai', args: 'status', desc: 'Check provider availability' },
|
|
45
|
+
{ cmd: '/ai', args: 'clear', desc: 'Clear conversation history' },
|
|
45
46
|
|
|
46
47
|
// General
|
|
47
48
|
{ cmd: '/help', args: '', desc: 'Show all commands' },
|
|
@@ -72,7 +73,10 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
72
73
|
// Track previous suggestion count to avoid unnecessary parent updates
|
|
73
74
|
const prevSuggestionCount = useRef(0);
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
+
// Maximum visible suggestions in the dropdown
|
|
77
|
+
const MAX_VISIBLE = 8;
|
|
78
|
+
|
|
79
|
+
// Filter commands based on typed value (no limit - we'll handle display separately)
|
|
76
80
|
const suggestions = useMemo(() => {
|
|
77
81
|
if (!value.startsWith('/')) return [];
|
|
78
82
|
|
|
@@ -81,9 +85,31 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
81
85
|
const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd;
|
|
82
86
|
return fullCmd.toLowerCase().includes(search) ||
|
|
83
87
|
c.cmd.toLowerCase().startsWith(search);
|
|
84
|
-
})
|
|
88
|
+
});
|
|
85
89
|
}, [value]);
|
|
86
90
|
|
|
91
|
+
// Calculate visible window of suggestions (scrolls to keep selection visible)
|
|
92
|
+
const { visibleSuggestions, startIndex } = useMemo(() => {
|
|
93
|
+
if (suggestions.length <= MAX_VISIBLE) {
|
|
94
|
+
return { visibleSuggestions: suggestions, startIndex: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate window to keep selected item visible
|
|
98
|
+
let start = 0;
|
|
99
|
+
if (selectedSuggestion >= MAX_VISIBLE) {
|
|
100
|
+
// Selected item is beyond initial window, scroll down
|
|
101
|
+
start = selectedSuggestion - MAX_VISIBLE + 1;
|
|
102
|
+
}
|
|
103
|
+
// Ensure we don't go past the end
|
|
104
|
+
start = Math.min(start, suggestions.length - MAX_VISIBLE);
|
|
105
|
+
start = Math.max(0, start);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
visibleSuggestions: suggestions.slice(start, start + MAX_VISIBLE),
|
|
109
|
+
startIndex: start,
|
|
110
|
+
};
|
|
111
|
+
}, [suggestions, selectedSuggestion]);
|
|
112
|
+
|
|
87
113
|
const showSuggestions = value.startsWith('/') && value.length >= 1 && suggestions.length > 0;
|
|
88
114
|
|
|
89
115
|
// Helper to build full command string from suggestion
|
|
@@ -97,9 +123,10 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
97
123
|
return suggestion.cmd;
|
|
98
124
|
}, []);
|
|
99
125
|
|
|
100
|
-
// Notify parent when suggestions change (
|
|
126
|
+
// Notify parent when suggestions change (use visible count, not total)
|
|
101
127
|
useEffect(() => {
|
|
102
|
-
const
|
|
128
|
+
const visibleCount = Math.min(suggestions.length, MAX_VISIBLE);
|
|
129
|
+
const count = showSuggestions ? visibleCount + 2 : 0; // +2 for border and hint line
|
|
103
130
|
if (count !== prevSuggestionCount.current) {
|
|
104
131
|
prevSuggestionCount.current = count;
|
|
105
132
|
onSuggestionsChange?.(count);
|
|
@@ -124,14 +151,22 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
124
151
|
return;
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
// Up/Down to navigate suggestions when showing
|
|
154
|
+
// Up/Down to navigate suggestions when showing (circular navigation)
|
|
128
155
|
if (showSuggestions) {
|
|
129
156
|
if (key.upArrow) {
|
|
130
|
-
setSelectedSuggestion(prev =>
|
|
157
|
+
setSelectedSuggestion(prev => {
|
|
158
|
+
// Wrap to bottom when at top
|
|
159
|
+
if (prev <= 0) return suggestions.length - 1;
|
|
160
|
+
return prev - 1;
|
|
161
|
+
});
|
|
131
162
|
return;
|
|
132
163
|
}
|
|
133
164
|
if (key.downArrow) {
|
|
134
|
-
setSelectedSuggestion(prev =>
|
|
165
|
+
setSelectedSuggestion(prev => {
|
|
166
|
+
// Wrap to top when at bottom
|
|
167
|
+
if (prev >= suggestions.length - 1) return 0;
|
|
168
|
+
return prev + 1;
|
|
169
|
+
});
|
|
135
170
|
return;
|
|
136
171
|
}
|
|
137
172
|
} else {
|
|
@@ -219,32 +254,48 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
219
254
|
borderColor="gray"
|
|
220
255
|
marginBottom={0}
|
|
221
256
|
>
|
|
222
|
-
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
257
|
+
{/* Scroll up indicator */}
|
|
258
|
+
{startIndex > 0 && (
|
|
259
|
+
<Box paddingX={1}>
|
|
260
|
+
<Text color="gray">↑ {startIndex} more above</Text>
|
|
261
|
+
</Box>
|
|
262
|
+
)}
|
|
263
|
+
{visibleSuggestions.map((suggestion, visibleIndex) => {
|
|
264
|
+
const actualIndex = startIndex + visibleIndex;
|
|
265
|
+
const isSelected = actualIndex === selectedSuggestion;
|
|
266
|
+
return (
|
|
267
|
+
<Box key={`${suggestion.cmd}-${suggestion.args}-${actualIndex}`} paddingX={1}>
|
|
232
268
|
<Text
|
|
233
|
-
|
|
234
|
-
|
|
269
|
+
backgroundColor={isSelected ? 'blue' : undefined}
|
|
270
|
+
color={isSelected ? 'white' : 'cyan'}
|
|
271
|
+
bold={isSelected}
|
|
235
272
|
>
|
|
236
|
-
{
|
|
273
|
+
{suggestion.cmd}
|
|
237
274
|
</Text>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
{suggestion.args && (
|
|
276
|
+
<Text
|
|
277
|
+
color={isSelected ? 'white' : 'gray'}
|
|
278
|
+
backgroundColor={isSelected ? 'blue' : undefined}
|
|
279
|
+
>
|
|
280
|
+
{' '}{suggestion.args}
|
|
281
|
+
</Text>
|
|
282
|
+
)}
|
|
283
|
+
<Text color="gray"> - </Text>
|
|
284
|
+
<Text
|
|
285
|
+
color={isSelected ? 'white' : 'gray'}
|
|
286
|
+
dimColor={!isSelected}
|
|
287
|
+
>
|
|
288
|
+
{suggestion.desc}
|
|
289
|
+
</Text>
|
|
290
|
+
</Box>
|
|
291
|
+
);
|
|
292
|
+
})}
|
|
293
|
+
{/* Scroll down indicator */}
|
|
294
|
+
{startIndex + MAX_VISIBLE < suggestions.length && (
|
|
295
|
+
<Box paddingX={1}>
|
|
296
|
+
<Text color="gray">↓ {suggestions.length - startIndex - MAX_VISIBLE} more below</Text>
|
|
246
297
|
</Box>
|
|
247
|
-
)
|
|
298
|
+
)}
|
|
248
299
|
<Box paddingX={1}>
|
|
249
300
|
<Text dimColor>Tab complete · ↑↓ select · Esc cancel</Text>
|
|
250
301
|
</Box>
|