@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.
- package/bin/lib/tui/App.tsx +74 -49
- package/bin/lib/tui/components/AIPanel.tsx +180 -0
- package/bin/lib/tui/components/CommandInput.tsx +120 -60
- 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 +70 -46
- 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 +84 -42
- 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);
|
|
@@ -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
|
-
|
|
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
|
|
393
|
+
const handleAICommand = async (cmdArgs: string[]) => {
|
|
389
394
|
const subCommand = cmdArgs[0];
|
|
390
395
|
|
|
391
396
|
switch (subCommand) {
|
|
392
|
-
case '
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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: /
|
|
445
|
+
addSystemLog('Usage: /ai ask <your question>');
|
|
422
446
|
return;
|
|
423
447
|
}
|
|
424
|
-
if (!
|
|
425
|
-
addSystemLog(
|
|
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
|
-
|
|
452
|
+
const providerName = aiService.getProviderInfo()?.name || 'AI';
|
|
453
|
+
addSystemLog(`Asking ${providerName}: ${question}`);
|
|
429
454
|
try {
|
|
430
|
-
|
|
431
|
-
const response = await
|
|
432
|
-
addSystemLog(
|
|
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
|
-
|
|
464
|
+
aiService.clearConversation();
|
|
440
465
|
addSystemLog('Conversation history cleared.');
|
|
441
466
|
break;
|
|
442
467
|
|
|
468
|
+
case 'chat':
|
|
443
469
|
default:
|
|
444
|
-
//
|
|
445
|
-
if (!
|
|
446
|
-
addSystemLog(
|
|
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
|
-
|
|
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
|
-
/
|
|
654
|
-
/
|
|
655
|
-
/
|
|
656
|
-
/
|
|
657
|
-
/
|
|
658
|
-
/
|
|
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
|
|
682
|
-
if (
|
|
706
|
+
// Show AI panel
|
|
707
|
+
if (showAIPanel) {
|
|
683
708
|
return (
|
|
684
|
-
<
|
|
685
|
-
onClose={() =>
|
|
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={
|
|
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}>> </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
|
+
}
|