@gxp-dev/tools 2.0.16 → 2.0.18

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.
@@ -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 GeminiPanel from './components/GeminiPanel.js';
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
- geminiService,
22
- isAuthenticated,
23
- clearAuthTokens,
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 [showGemini, setShowGemini] = useState(false);
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);
@@ -54,6 +55,11 @@ export default function App({ autoStart, args }: AppProps) {
54
55
  // Get terminal height for full screen
55
56
  const terminalHeight = stdout?.rows || 24;
56
57
 
58
+ // Stable callback for suggestion row changes to prevent unnecessary re-renders
59
+ const handleSuggestionsChange = useCallback((count: number) => {
60
+ setSuggestionRows(count);
61
+ }, []);
62
+
57
63
  // Sync services from ServiceManager
58
64
  const syncServices = useCallback(() => {
59
65
  const managerServices = serviceManager.getAllServices();
@@ -226,9 +232,8 @@ export default function App({ autoStart, args }: AppProps) {
226
232
  exit();
227
233
  break;
228
234
 
229
- case 'gemini':
230
235
  case 'ai':
231
- handleGeminiCommand(cmdArgs);
236
+ handleAICommand(cmdArgs);
232
237
  break;
233
238
 
234
239
  case 'extract-config':
@@ -385,68 +390,89 @@ export default function App({ autoStart, args }: AppProps) {
385
390
  syncServices();
386
391
  };
387
392
 
388
- const handleGeminiCommand = async (cmdArgs: string[]) => {
393
+ const handleAICommand = async (cmdArgs: string[]) => {
389
394
  const subCommand = cmdArgs[0];
390
395
 
391
396
  switch (subCommand) {
392
- case 'enable':
393
- addSystemLog('Starting Google OAuth flow...');
394
- addSystemLog('A browser window will open for authentication.');
395
- const result = await geminiService.startOAuthFlow();
396
- if (result.success) {
397
- addSystemLog(`✅ ${result.message}`);
397
+ case 'model':
398
+ // Set or show current AI provider
399
+ const providerArg = cmdArgs[1] as AIProvider | undefined;
400
+ if (providerArg) {
401
+ const result = aiService.setProvider(providerArg);
402
+ if (result.success) {
403
+ addSystemLog(`✅ ${result.message}`);
404
+ } else {
405
+ addSystemLog(`❌ ${result.message}`);
406
+ }
398
407
  } else {
399
- addSystemLog(`❌ ${result.message}`);
408
+ // Show current provider and available providers
409
+ const current = aiService.getProviderInfo();
410
+ const providers = getAvailableProviders();
411
+ let message = `Current AI provider: ${current ? getProviderStatus(current) : 'None'}\n\nAvailable providers:`;
412
+ for (const p of providers) {
413
+ const status = p.available ? getProviderStatus(p) : `${p.name} (not available)`;
414
+ const marker = p.id === current?.id ? ' ← current' : '';
415
+ message += `\n ${p.id}: ${status}${marker}`;
416
+ if (!p.available && p.reason) {
417
+ message += `\n ${p.reason}`;
418
+ }
419
+ }
420
+ message += '\n\nUsage: /ai model <claude|codex|gemini>';
421
+ addSystemLog(message);
400
422
  }
401
423
  break;
402
424
 
403
- case 'logout':
404
- case 'disable':
405
- clearAuthTokens();
406
- addSystemLog('Logged out from Gemini AI.');
407
- break;
408
-
409
425
  case 'status':
410
- if (isAuthenticated()) {
411
- addSystemLog('✅ Gemini AI is authenticated and ready.');
412
- } else {
413
- addSystemLog('❌ Not authenticated. Run /gemini enable to set up.');
426
+ // Show detailed status of all providers
427
+ const providers = getAvailableProviders();
428
+ const currentProvider = aiService.getProvider();
429
+ let statusMsg = 'AI Provider Status:\n';
430
+ for (const p of providers) {
431
+ const icon = p.available ? '✅' : '❌';
432
+ const current = p.id === currentProvider ? ' (current)' : '';
433
+ statusMsg += `\n ${icon} ${getProviderStatus(p)}${current}`;
434
+ if (!p.available && p.reason) {
435
+ statusMsg += `\n ${p.reason}`;
436
+ }
414
437
  }
438
+ addSystemLog(statusMsg);
415
439
  break;
416
440
 
417
441
  case 'ask':
418
442
  // Quick question without opening panel
419
443
  const question = cmdArgs.slice(1).join(' ');
420
444
  if (!question) {
421
- addSystemLog('Usage: /gemini ask <your question>');
445
+ addSystemLog('Usage: /ai ask <your question>');
422
446
  return;
423
447
  }
424
- if (!isAuthenticated()) {
425
- addSystemLog('Not authenticated. Run /gemini enable first.');
448
+ if (!aiService.isAvailable()) {
449
+ addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
426
450
  return;
427
451
  }
428
- addSystemLog(`Asking Gemini: ${question}`);
452
+ const providerName = aiService.getProviderInfo()?.name || 'AI';
453
+ addSystemLog(`Asking ${providerName}: ${question}`);
429
454
  try {
430
- geminiService.loadProjectContext(process.cwd());
431
- const response = await geminiService.sendMessage(question);
432
- addSystemLog(`Gemini: ${response}`);
455
+ aiService.loadProjectContext(process.cwd());
456
+ const response = await aiService.sendMessage(question);
457
+ addSystemLog(`${providerName}: ${response}`);
433
458
  } catch (err) {
434
459
  addSystemLog(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
435
460
  }
436
461
  break;
437
462
 
438
463
  case 'clear':
439
- geminiService.clearConversation();
464
+ aiService.clearConversation();
440
465
  addSystemLog('Conversation history cleared.');
441
466
  break;
442
467
 
468
+ case 'chat':
443
469
  default:
444
- // No subcommand = open chat panel
445
- if (!isAuthenticated()) {
446
- addSystemLog('Not authenticated. Run /gemini enable to set up Google authentication.');
470
+ // Open AI chat panel
471
+ if (!aiService.isAvailable()) {
472
+ addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
447
473
  return;
448
474
  }
449
- setShowGemini(true);
475
+ setShowAIPanel(true);
450
476
  }
451
477
  };
452
478
 
@@ -650,13 +676,12 @@ Available commands:
650
676
  /extract-config -o Overwrite existing values
651
677
 
652
678
  AI Assistant:
653
- /gemini Open Gemini AI chat panel
654
- /gemini enable Set up Google authentication
655
- /gemini ask <query> Quick question to AI
656
- /gemini status Check auth status
657
- /gemini logout Log out from Gemini
658
- /gemini clear Clear conversation history
659
- /ai Alias for /gemini
679
+ /ai Open AI chat with current provider
680
+ /ai model Show available AI providers
681
+ /ai model <name> Switch to claude, codex, or gemini
682
+ /ai ask <query> Quick question to AI
683
+ /ai status Check provider availability
684
+ /ai clear Clear conversation history
660
685
 
661
686
  Service Management:
662
687
  /stop [service] Stop current or specified service
@@ -678,11 +703,11 @@ Keyboard shortcuts:
678
703
  Esc Clear input
679
704
  `;
680
705
 
681
- // Show Gemini panel
682
- if (showGemini) {
706
+ // Show AI panel
707
+ if (showAIPanel) {
683
708
  return (
684
- <GeminiPanel
685
- onClose={() => setShowGemini(false)}
709
+ <AIPanel
710
+ onClose={() => setShowAIPanel(false)}
686
711
  onLog={addSystemLog}
687
712
  />
688
713
  );
@@ -723,7 +748,7 @@ Keyboard shortcuts:
723
748
  name: currentService.name,
724
749
  status: currentService.status
725
750
  } : null}
726
- onSuggestionsChange={setSuggestionRows}
751
+ onSuggestionsChange={handleSuggestionsChange}
727
752
  />
728
753
  </Box>
729
754
  );
@@ -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}>&gt; </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
+ }