@gxp-dev/tools 2.0.5
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/.github/workflows/npm-publish.yml +48 -0
- package/CLAUDE.md +400 -0
- package/README.md +247 -0
- package/REFACTOR_PLAN.md +194 -0
- package/bin/gx-devtools.js +87 -0
- package/bin/lib/cli.js +251 -0
- package/bin/lib/commands/assets.js +337 -0
- package/bin/lib/commands/build.js +259 -0
- package/bin/lib/commands/datastore.js +433 -0
- package/bin/lib/commands/dev.js +328 -0
- package/bin/lib/commands/extensions.js +298 -0
- package/bin/lib/commands/index.js +35 -0
- package/bin/lib/commands/init.js +307 -0
- package/bin/lib/commands/publish.js +189 -0
- package/bin/lib/commands/socket.js +158 -0
- package/bin/lib/commands/ssl.js +47 -0
- package/bin/lib/constants.js +120 -0
- package/bin/lib/tui/App.tsx +600 -0
- package/bin/lib/tui/components/CommandInput.tsx +278 -0
- package/bin/lib/tui/components/GeminiPanel.tsx +161 -0
- package/bin/lib/tui/components/Header.tsx +27 -0
- package/bin/lib/tui/components/LogPanel.tsx +122 -0
- package/bin/lib/tui/components/TabBar.tsx +56 -0
- package/bin/lib/tui/components/WelcomeScreen.tsx +80 -0
- package/bin/lib/tui/index.tsx +63 -0
- package/bin/lib/tui/services/ExtensionService.ts +122 -0
- package/bin/lib/tui/services/GeminiService.ts +395 -0
- package/bin/lib/tui/services/ServiceManager.ts +336 -0
- package/bin/lib/tui/services/SocketService.ts +204 -0
- package/bin/lib/tui/services/ViteService.ts +107 -0
- package/bin/lib/tui/services/index.ts +13 -0
- package/bin/lib/utils/files.js +180 -0
- package/bin/lib/utils/index.js +17 -0
- package/bin/lib/utils/paths.js +138 -0
- package/bin/lib/utils/prompts.js +71 -0
- package/bin/lib/utils/ssl.js +233 -0
- package/browser-extensions/README.md +1 -0
- package/browser-extensions/chrome/background.js +857 -0
- package/browser-extensions/chrome/content.js +51 -0
- package/browser-extensions/chrome/devtools.html +9 -0
- package/browser-extensions/chrome/devtools.js +23 -0
- package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
- package/browser-extensions/chrome/inspector.js +1087 -0
- package/browser-extensions/chrome/manifest.json +70 -0
- package/browser-extensions/chrome/panel.html +638 -0
- package/browser-extensions/chrome/panel.js +862 -0
- package/browser-extensions/chrome/popup.html +399 -0
- package/browser-extensions/chrome/popup.js +515 -0
- package/browser-extensions/chrome/rules.json +1 -0
- package/browser-extensions/chrome/test-chrome.html +145 -0
- package/browser-extensions/chrome/test-mixed-content.html +190 -0
- package/browser-extensions/chrome/test-uri-pattern.html +199 -0
- package/browser-extensions/firefox/README.md +134 -0
- package/browser-extensions/firefox/background.js +804 -0
- package/browser-extensions/firefox/content.js +120 -0
- package/browser-extensions/firefox/debug-errors.html +229 -0
- package/browser-extensions/firefox/debug-https.html +113 -0
- package/browser-extensions/firefox/devtools.html +9 -0
- package/browser-extensions/firefox/devtools.js +24 -0
- package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
- package/browser-extensions/firefox/inspector.js +1087 -0
- package/browser-extensions/firefox/manifest.json +67 -0
- package/browser-extensions/firefox/panel.html +638 -0
- package/browser-extensions/firefox/panel.js +862 -0
- package/browser-extensions/firefox/popup.html +525 -0
- package/browser-extensions/firefox/popup.js +536 -0
- package/browser-extensions/firefox/test-gramercy.html +126 -0
- package/browser-extensions/firefox/test-imports.html +58 -0
- package/browser-extensions/firefox/test-masking.html +147 -0
- package/browser-extensions/firefox/test-uri-pattern.html +199 -0
- package/docs/DOCUSAURUS_IMPORT.md +378 -0
- package/docs/_category_.json +8 -0
- package/docs/app-manifest.md +272 -0
- package/docs/building-for-platform.md +315 -0
- package/docs/dev-tools.md +291 -0
- package/docs/getting-started.md +180 -0
- package/docs/gxp-store.md +305 -0
- package/docs/index.md +44 -0
- package/package.json +77 -0
- package/runtime/PortalContainer.vue +326 -0
- package/runtime/dev-tools/DevToolsModal.vue +217 -0
- package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
- package/runtime/dev-tools/MockDataEditor.vue +621 -0
- package/runtime/dev-tools/SocketSimulator.vue +562 -0
- package/runtime/dev-tools/StoreInspector.vue +644 -0
- package/runtime/dev-tools/index.js +6 -0
- package/runtime/gxpStringsPlugin.js +428 -0
- package/runtime/index.html +22 -0
- package/runtime/main.js +32 -0
- package/runtime/mock-api/auth-middleware.js +97 -0
- package/runtime/mock-api/image-generator.js +221 -0
- package/runtime/mock-api/index.js +197 -0
- package/runtime/mock-api/response-generator.js +394 -0
- package/runtime/mock-api/route-generator.js +323 -0
- package/runtime/mock-api/socket-triggers.js +371 -0
- package/runtime/mock-api/spec-loader.js +300 -0
- package/runtime/server.js +180 -0
- package/runtime/stores/gxpPortalConfigStore.js +554 -0
- package/runtime/stores/index.js +6 -0
- package/runtime/vite-inspector-plugin.js +749 -0
- package/runtime/vite-source-tracker-plugin.js +232 -0
- package/runtime/vite.config.js +402 -0
- package/scripts/launch-chrome.js +90 -0
- package/scripts/pack-chrome.js +91 -0
- package/socket-events/AiSessionMessageCreated.json +18 -0
- package/socket-events/SocialStreamPostCreated.json +24 -0
- package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
- package/template/README.md +332 -0
- package/template/app-manifest.json +32 -0
- package/template/dev-assets/images/avatar-placeholder.png +0 -0
- package/template/dev-assets/images/background-placeholder.jpg +0 -0
- package/template/dev-assets/images/banner-placeholder.jpg +0 -0
- package/template/dev-assets/images/icon-placeholder.png +0 -0
- package/template/dev-assets/images/logo-placeholder.png +0 -0
- package/template/dev-assets/images/product-placeholder.jpg +0 -0
- package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
- package/template/env.example +51 -0
- package/template/gitignore +53 -0
- package/template/index.html +22 -0
- package/template/main.js +28 -0
- package/template/src/DemoPage.vue +459 -0
- package/template/src/Plugin.vue +38 -0
- package/template/src/stores/index.js +9 -0
- package/template/src/stores/test-data.json +173 -0
- package/template/theme-layouts/AdditionalStyling.css +0 -0
- package/template/theme-layouts/PrivateLayout.vue +39 -0
- package/template/theme-layouts/PublicLayout.vue +39 -0
- package/template/theme-layouts/SystemLayout.vue +39 -0
- package/template/vite.config.js +333 -0
- package/tsconfig.tui.json +21 -0
- package/vite.config.js +164 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
|
|
5
|
+
// Command definitions with descriptions
|
|
6
|
+
const COMMANDS = [
|
|
7
|
+
{ cmd: '/dev', args: '', desc: 'Start Vite (+ Socket if SOCKET_IO_ENABLED)' },
|
|
8
|
+
{ cmd: '/dev', args: '--with-socket', desc: 'Start Vite + Socket.IO' },
|
|
9
|
+
{ cmd: '/dev', args: '--with-mock', desc: 'Start Vite + Socket.IO + Mock API server' },
|
|
10
|
+
{ cmd: '/dev', args: '--firefox', desc: 'Start Vite + Firefox extension' },
|
|
11
|
+
{ cmd: '/dev', args: '--chrome', desc: 'Start Vite + Chrome extension' },
|
|
12
|
+
{ cmd: '/dev', args: '--no-socket', desc: 'Start Vite without Socket.IO' },
|
|
13
|
+
{ cmd: '/dev', args: '--no-https', desc: 'Start Vite without SSL' },
|
|
14
|
+
{ cmd: '/socket', args: '', desc: 'Start Socket.IO server' },
|
|
15
|
+
{ cmd: '/socket', args: 'send <event>', desc: 'Send socket event' },
|
|
16
|
+
{ cmd: '/socket', args: 'list', desc: 'List available events' },
|
|
17
|
+
{ cmd: '/ext', args: 'chrome', desc: 'Launch Chrome extension' },
|
|
18
|
+
{ cmd: '/ext', args: 'firefox', desc: 'Launch Firefox extension' },
|
|
19
|
+
{ cmd: '/stop', args: '[service]', desc: 'Stop current/specified service' },
|
|
20
|
+
{ cmd: '/restart', args: '[service]', desc: 'Restart current/specified service' },
|
|
21
|
+
{ cmd: '/clear', args: '', desc: 'Clear current log panel' },
|
|
22
|
+
{ cmd: '/gemini', args: '', desc: 'Open Gemini AI chat' },
|
|
23
|
+
{ cmd: '/gemini', args: 'enable', desc: 'Set up Google auth' },
|
|
24
|
+
{ cmd: '/gemini', args: 'ask <query>', desc: 'Quick AI question' },
|
|
25
|
+
{ cmd: '/help', args: '', desc: 'Show all commands' },
|
|
26
|
+
{ cmd: '/quit', args: '', desc: 'Exit application' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
interface ActiveService {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
status: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CommandInputProps {
|
|
36
|
+
onSubmit: (command: string) => void;
|
|
37
|
+
activeService?: ActiveService | null;
|
|
38
|
+
onSuggestionsChange?: (count: number) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function CommandInput({ onSubmit, activeService, onSuggestionsChange }: CommandInputProps) {
|
|
42
|
+
const [value, setValue] = useState(''); // The actual typed value (for filtering)
|
|
43
|
+
const [displayValue, setDisplayValue] = useState(''); // What's shown in input (may differ when navigating)
|
|
44
|
+
const [isNavigating, setIsNavigating] = useState(false); // Track if user is arrowing through suggestions
|
|
45
|
+
const [history, setHistory] = useState<string[]>([]);
|
|
46
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
47
|
+
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
|
48
|
+
// Key to force TextInput remount when value is set programmatically (resets cursor)
|
|
49
|
+
const [inputKey, setInputKey] = useState(0);
|
|
50
|
+
|
|
51
|
+
// Filter commands based on the TYPED value (not display value)
|
|
52
|
+
const suggestions = useMemo(() => {
|
|
53
|
+
if (!value.startsWith('/')) return [];
|
|
54
|
+
|
|
55
|
+
const search = value.toLowerCase();
|
|
56
|
+
return COMMANDS.filter(c => {
|
|
57
|
+
const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd;
|
|
58
|
+
return fullCmd.toLowerCase().includes(search) ||
|
|
59
|
+
c.cmd.toLowerCase().startsWith(search);
|
|
60
|
+
});
|
|
61
|
+
}, [value]);
|
|
62
|
+
|
|
63
|
+
const showSuggestions = value.startsWith('/') && value.length >= 1 && suggestions.length > 0;
|
|
64
|
+
|
|
65
|
+
// Helper to build full command string from suggestion
|
|
66
|
+
const buildFullCommand = (suggestion: typeof COMMANDS[0]): string => {
|
|
67
|
+
if (suggestion.args) {
|
|
68
|
+
const hasPlaceholder = suggestion.args.includes('<') || suggestion.args.includes('[');
|
|
69
|
+
if (!hasPlaceholder) {
|
|
70
|
+
return `${suggestion.cmd} ${suggestion.args}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return suggestion.cmd;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Notify parent when suggestions change (for layout adjustment)
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (onSuggestionsChange) {
|
|
79
|
+
// +2 for border and help text row
|
|
80
|
+
onSuggestionsChange(showSuggestions ? suggestions.length + 2 : 0);
|
|
81
|
+
}
|
|
82
|
+
}, [showSuggestions, suggestions.length, onSuggestionsChange]);
|
|
83
|
+
|
|
84
|
+
useInput((input, key) => {
|
|
85
|
+
// Tab to autocomplete selected suggestion (commits the selection)
|
|
86
|
+
if (key.tab && showSuggestions && suggestions[selectedSuggestion]) {
|
|
87
|
+
const suggestion = suggestions[selectedSuggestion];
|
|
88
|
+
const fullCmd = buildFullCommand(suggestion);
|
|
89
|
+
// Commit to both value and displayValue
|
|
90
|
+
setValue(fullCmd);
|
|
91
|
+
setDisplayValue(fullCmd);
|
|
92
|
+
setSelectedSuggestion(0);
|
|
93
|
+
setIsNavigating(false);
|
|
94
|
+
// Increment key to force TextInput remount (resets cursor to end)
|
|
95
|
+
setInputKey(k => k + 1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Up/Down to navigate suggestions when showing
|
|
100
|
+
if (showSuggestions) {
|
|
101
|
+
if (key.upArrow) {
|
|
102
|
+
const newIndex = Math.max(0, selectedSuggestion - 1);
|
|
103
|
+
setSelectedSuggestion(newIndex);
|
|
104
|
+
setIsNavigating(true);
|
|
105
|
+
// Update display value to show highlighted command
|
|
106
|
+
const suggestion = suggestions[newIndex];
|
|
107
|
+
if (suggestion) {
|
|
108
|
+
setDisplayValue(buildFullCommand(suggestion));
|
|
109
|
+
setInputKey(k => k + 1);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.downArrow) {
|
|
114
|
+
const newIndex = Math.min(suggestions.length - 1, selectedSuggestion + 1);
|
|
115
|
+
setSelectedSuggestion(newIndex);
|
|
116
|
+
setIsNavigating(true);
|
|
117
|
+
// Update display value to show highlighted command
|
|
118
|
+
const suggestion = suggestions[newIndex];
|
|
119
|
+
if (suggestion) {
|
|
120
|
+
setDisplayValue(buildFullCommand(suggestion));
|
|
121
|
+
setInputKey(k => k + 1);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// History navigation when not showing suggestions
|
|
127
|
+
if (key.upArrow && history.length > 0) {
|
|
128
|
+
const newIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
129
|
+
setHistoryIndex(newIndex);
|
|
130
|
+
const histVal = history[history.length - 1 - newIndex] || '';
|
|
131
|
+
setValue(histVal);
|
|
132
|
+
setDisplayValue(histVal);
|
|
133
|
+
setInputKey(k => k + 1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (key.downArrow) {
|
|
138
|
+
const newIndex = Math.max(historyIndex - 1, -1);
|
|
139
|
+
setHistoryIndex(newIndex);
|
|
140
|
+
if (newIndex < 0) {
|
|
141
|
+
setValue('');
|
|
142
|
+
setDisplayValue('');
|
|
143
|
+
} else {
|
|
144
|
+
const histVal = history[history.length - 1 - newIndex] || '';
|
|
145
|
+
setValue(histVal);
|
|
146
|
+
setDisplayValue(histVal);
|
|
147
|
+
}
|
|
148
|
+
setInputKey(k => k + 1);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const handleSubmit = (input: string) => {
|
|
155
|
+
if (!input.trim()) return;
|
|
156
|
+
|
|
157
|
+
// Add to history
|
|
158
|
+
if (input.trim()) {
|
|
159
|
+
setHistory(prev => [...prev.filter(h => h !== input.trim()), input.trim()]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reset state
|
|
163
|
+
setValue('');
|
|
164
|
+
setDisplayValue('');
|
|
165
|
+
setHistoryIndex(-1);
|
|
166
|
+
setSelectedSuggestion(0);
|
|
167
|
+
setIsNavigating(false);
|
|
168
|
+
|
|
169
|
+
// Call handler
|
|
170
|
+
onSubmit(input);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Handle text input changes
|
|
174
|
+
const handleChange = (v: string) => {
|
|
175
|
+
// User typed something, reset navigation state
|
|
176
|
+
setValue(v);
|
|
177
|
+
setDisplayValue(v);
|
|
178
|
+
setSelectedSuggestion(0);
|
|
179
|
+
setIsNavigating(false);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Get context-specific hints for current tab
|
|
183
|
+
const getHints = (): string[] => {
|
|
184
|
+
const hints: string[] = [];
|
|
185
|
+
|
|
186
|
+
if (activeService) {
|
|
187
|
+
const isRunning = activeService.status === 'running' || activeService.status === 'starting';
|
|
188
|
+
if (isRunning) {
|
|
189
|
+
hints.push(`Ctrl+K stop ${activeService.name}`);
|
|
190
|
+
hints.push(`/restart to restart`);
|
|
191
|
+
}
|
|
192
|
+
if (activeService.id === 'vite') {
|
|
193
|
+
hints.push('r to refresh browser');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
hints.push('Ctrl+L clear logs');
|
|
198
|
+
hints.push('Tab/Arrow switch tabs');
|
|
199
|
+
hints.push('Ctrl+C quit');
|
|
200
|
+
|
|
201
|
+
return hints;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const hints = getHints();
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Box flexDirection="column">
|
|
208
|
+
{/* Suggestions dropdown (above input) */}
|
|
209
|
+
{showSuggestions && (
|
|
210
|
+
<Box
|
|
211
|
+
flexDirection="column"
|
|
212
|
+
borderStyle="round"
|
|
213
|
+
borderColor="gray"
|
|
214
|
+
marginBottom={0}
|
|
215
|
+
>
|
|
216
|
+
{suggestions.map((suggestion, index) => (
|
|
217
|
+
<Box key={`${suggestion.cmd}-${suggestion.args}-${index}`} paddingX={1}>
|
|
218
|
+
<Text
|
|
219
|
+
backgroundColor={index === selectedSuggestion ? 'blue' : undefined}
|
|
220
|
+
color={index === selectedSuggestion ? 'white' : 'cyan'}
|
|
221
|
+
bold={index === selectedSuggestion}
|
|
222
|
+
>
|
|
223
|
+
{suggestion.cmd}
|
|
224
|
+
</Text>
|
|
225
|
+
{suggestion.args && (
|
|
226
|
+
<Text
|
|
227
|
+
color={index === selectedSuggestion ? 'white' : 'gray'}
|
|
228
|
+
backgroundColor={index === selectedSuggestion ? 'blue' : undefined}
|
|
229
|
+
>
|
|
230
|
+
{' '}{suggestion.args}
|
|
231
|
+
</Text>
|
|
232
|
+
)}
|
|
233
|
+
<Text color="gray"> - </Text>
|
|
234
|
+
<Text
|
|
235
|
+
color={index === selectedSuggestion ? 'white' : 'gray'}
|
|
236
|
+
dimColor={index !== selectedSuggestion}
|
|
237
|
+
>
|
|
238
|
+
{suggestion.desc}
|
|
239
|
+
</Text>
|
|
240
|
+
</Box>
|
|
241
|
+
))}
|
|
242
|
+
<Box paddingX={1} borderStyle={undefined}>
|
|
243
|
+
<Text dimColor>Tab to complete · Up/Down to select</Text>
|
|
244
|
+
</Box>
|
|
245
|
+
</Box>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Input box */}
|
|
249
|
+
<Box
|
|
250
|
+
borderStyle="single"
|
|
251
|
+
borderColor="cyan"
|
|
252
|
+
paddingX={1}
|
|
253
|
+
>
|
|
254
|
+
<Text color="cyan" bold>></Text>
|
|
255
|
+
<Text> </Text>
|
|
256
|
+
<TextInput
|
|
257
|
+
key={inputKey}
|
|
258
|
+
value={displayValue}
|
|
259
|
+
onChange={handleChange}
|
|
260
|
+
onSubmit={() => handleSubmit(displayValue.startsWith('/') ? displayValue : '/' + displayValue)}
|
|
261
|
+
placeholder="Type / to run a command..."
|
|
262
|
+
/>
|
|
263
|
+
</Box>
|
|
264
|
+
|
|
265
|
+
{/* Hints bar (below input) */}
|
|
266
|
+
<Box paddingX={1} justifyContent="space-between">
|
|
267
|
+
<Box>
|
|
268
|
+
{hints.slice(0, 4).map((hint, index) => (
|
|
269
|
+
<React.Fragment key={hint}>
|
|
270
|
+
{index > 0 && <Text color="gray"> · </Text>}
|
|
271
|
+
<Text dimColor>{hint}</Text>
|
|
272
|
+
</React.Fragment>
|
|
273
|
+
))}
|
|
274
|
+
</Box>
|
|
275
|
+
</Box>
|
|
276
|
+
</Box>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
geminiService,
|
|
6
|
+
isAuthenticated,
|
|
7
|
+
loadGeminiConfig,
|
|
8
|
+
} from '../services/index.js';
|
|
9
|
+
|
|
10
|
+
interface GeminiPanelProps {
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onLog: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Message {
|
|
16
|
+
role: 'user' | 'assistant' | 'system';
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function GeminiPanel({ onClose, onLog }: GeminiPanelProps) {
|
|
21
|
+
const { stdout } = useStdout();
|
|
22
|
+
const [input, setInput] = useState('');
|
|
23
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
26
|
+
|
|
27
|
+
// Calculate available height for messages
|
|
28
|
+
const maxMessageLines = stdout ? Math.max(5, stdout.rows - 8) : 10;
|
|
29
|
+
|
|
30
|
+
// Check authentication on mount
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isAuthenticated()) {
|
|
33
|
+
setMessages([{
|
|
34
|
+
role: 'system',
|
|
35
|
+
content: 'Not authenticated. Run /gemini enable to set up Google authentication.',
|
|
36
|
+
}]);
|
|
37
|
+
} else {
|
|
38
|
+
const config = loadGeminiConfig();
|
|
39
|
+
setMessages([{
|
|
40
|
+
role: 'system',
|
|
41
|
+
content: `Gemini AI ready. ${config.projectContext ? 'Project context loaded.' : ''}\nType your message and press Enter. Press Escape to close.`,
|
|
42
|
+
}]);
|
|
43
|
+
// Load project context
|
|
44
|
+
geminiService.loadProjectContext(process.cwd());
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Handle keyboard input
|
|
49
|
+
useInput((char, key) => {
|
|
50
|
+
if (key.escape) {
|
|
51
|
+
onClose();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Scroll with Shift+Up/Down
|
|
56
|
+
if (key.shift && key.upArrow) {
|
|
57
|
+
setScrollOffset(prev => Math.min(prev + 1, Math.max(0, messages.length - maxMessageLines)));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.shift && key.downArrow) {
|
|
61
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const handleSubmit = async (value: string) => {
|
|
67
|
+
if (!value.trim() || isLoading) return;
|
|
68
|
+
|
|
69
|
+
const userMessage = value.trim();
|
|
70
|
+
setInput('');
|
|
71
|
+
|
|
72
|
+
// Add user message
|
|
73
|
+
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await geminiService.sendMessage(userMessage);
|
|
78
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
|
79
|
+
setScrollOffset(0); // Auto-scroll to bottom
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
82
|
+
setMessages(prev => [...prev, {
|
|
83
|
+
role: 'system',
|
|
84
|
+
content: `Error: ${errorMessage}`,
|
|
85
|
+
}]);
|
|
86
|
+
onLog(`Gemini error: ${errorMessage}`);
|
|
87
|
+
} finally {
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Render messages with scrolling
|
|
93
|
+
const renderMessages = () => {
|
|
94
|
+
const start = Math.max(0, messages.length - maxMessageLines - scrollOffset);
|
|
95
|
+
const end = messages.length - scrollOffset;
|
|
96
|
+
const visibleMessages = messages.slice(start, end);
|
|
97
|
+
|
|
98
|
+
return visibleMessages.map((msg, idx) => {
|
|
99
|
+
let color: string;
|
|
100
|
+
let prefix: string;
|
|
101
|
+
|
|
102
|
+
switch (msg.role) {
|
|
103
|
+
case 'user':
|
|
104
|
+
color = 'cyan';
|
|
105
|
+
prefix = 'You: ';
|
|
106
|
+
break;
|
|
107
|
+
case 'assistant':
|
|
108
|
+
color = 'green';
|
|
109
|
+
prefix = 'Gemini: ';
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
color = 'yellow';
|
|
113
|
+
prefix = '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Box key={start + idx} flexDirection="column" marginBottom={1}>
|
|
118
|
+
<Text color={color} bold>{prefix}</Text>
|
|
119
|
+
<Text wrap="wrap">{msg.content}</Text>
|
|
120
|
+
</Box>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Box flexDirection="column" height="100%" borderStyle="double" borderColor="magenta">
|
|
127
|
+
<Box paddingX={1} justifyContent="space-between">
|
|
128
|
+
<Text bold color="magenta">Gemini AI Assistant</Text>
|
|
129
|
+
<Text color="gray" dimColor>Esc to close</Text>
|
|
130
|
+
</Box>
|
|
131
|
+
|
|
132
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
133
|
+
{messages.length > maxMessageLines + scrollOffset && (
|
|
134
|
+
<Text color="gray" dimColor>
|
|
135
|
+
... {messages.length - maxMessageLines - scrollOffset} earlier messages
|
|
136
|
+
</Text>
|
|
137
|
+
)}
|
|
138
|
+
{renderMessages()}
|
|
139
|
+
{scrollOffset > 0 && (
|
|
140
|
+
<Text color="gray" dimColor>... {scrollOffset} newer messages below</Text>
|
|
141
|
+
)}
|
|
142
|
+
</Box>
|
|
143
|
+
|
|
144
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
|
145
|
+
{isLoading ? (
|
|
146
|
+
<Text color="yellow">Thinking...</Text>
|
|
147
|
+
) : (
|
|
148
|
+
<Box>
|
|
149
|
+
<Text color="magenta">> </Text>
|
|
150
|
+
<TextInput
|
|
151
|
+
value={input}
|
|
152
|
+
onChange={setInput}
|
|
153
|
+
onSubmit={handleSubmit}
|
|
154
|
+
placeholder="Ask Gemini something..."
|
|
155
|
+
/>
|
|
156
|
+
</Box>
|
|
157
|
+
)}
|
|
158
|
+
</Box>
|
|
159
|
+
</Box>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface HeaderProps {
|
|
5
|
+
projectName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Header({ projectName }: HeaderProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Box
|
|
11
|
+
borderStyle="single"
|
|
12
|
+
borderColor="blue"
|
|
13
|
+
paddingX={1}
|
|
14
|
+
justifyContent="space-between"
|
|
15
|
+
>
|
|
16
|
+
<Box>
|
|
17
|
+
<Text color="blue" bold>GxP</Text>
|
|
18
|
+
<Text color="white"> DevStudio</Text>
|
|
19
|
+
<Text color="gray"> - </Text>
|
|
20
|
+
<Text color="cyan">{projectName}</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
<Box>
|
|
23
|
+
<Text color="gray">Ctrl+C to quit</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
</Box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useStdout, useInput } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface LogPanelProps {
|
|
5
|
+
logs: string[];
|
|
6
|
+
isActive?: boolean;
|
|
7
|
+
maxHeight?: number; // Maximum height in rows (from parent container)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function LogPanel({ logs, isActive = true, maxHeight }: LogPanelProps) {
|
|
11
|
+
const { stdout } = useStdout();
|
|
12
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
13
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
14
|
+
|
|
15
|
+
// Calculate visible lines based on provided maxHeight or terminal height
|
|
16
|
+
// Subtract 2 for padding, 2 for border
|
|
17
|
+
const defaultMaxLines = stdout ? Math.max(5, stdout.rows - 10) : 15;
|
|
18
|
+
const maxLines = maxHeight ? Math.max(3, maxHeight - 4) : defaultMaxLines;
|
|
19
|
+
|
|
20
|
+
// Reset scroll when logs are cleared
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (logs.length === 0) {
|
|
23
|
+
setScrollOffset(0);
|
|
24
|
+
setAutoScroll(true);
|
|
25
|
+
}
|
|
26
|
+
}, [logs.length === 0]);
|
|
27
|
+
|
|
28
|
+
// Auto-scroll to bottom when new logs arrive (if autoScroll is enabled)
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (autoScroll) {
|
|
31
|
+
setScrollOffset(0);
|
|
32
|
+
}
|
|
33
|
+
}, [logs.length, autoScroll]);
|
|
34
|
+
|
|
35
|
+
// Handle keyboard input for scrolling
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (!isActive) return;
|
|
38
|
+
|
|
39
|
+
// Page Up - scroll up by half a page
|
|
40
|
+
if (key.pageUp || (key.shift && key.upArrow)) {
|
|
41
|
+
const maxOffset = Math.max(0, logs.length - maxLines);
|
|
42
|
+
setScrollOffset(prev => Math.min(prev + Math.floor(maxLines / 2), maxOffset));
|
|
43
|
+
setAutoScroll(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Page Down - scroll down by half a page
|
|
48
|
+
if (key.pageDown || (key.shift && key.downArrow)) {
|
|
49
|
+
setScrollOffset(prev => {
|
|
50
|
+
const newOffset = Math.max(prev - Math.floor(maxLines / 2), 0);
|
|
51
|
+
if (newOffset === 0) setAutoScroll(true);
|
|
52
|
+
return newOffset;
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Home - scroll to top
|
|
58
|
+
if (key.meta && key.upArrow) {
|
|
59
|
+
const maxOffset = Math.max(0, logs.length - maxLines);
|
|
60
|
+
setScrollOffset(maxOffset);
|
|
61
|
+
setAutoScroll(false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// End - scroll to bottom
|
|
66
|
+
if (key.meta && key.downArrow) {
|
|
67
|
+
setScrollOffset(0);
|
|
68
|
+
setAutoScroll(true);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Calculate visible logs with scroll offset
|
|
74
|
+
const totalLogs = logs.length;
|
|
75
|
+
const startIndex = Math.max(0, totalLogs - maxLines - scrollOffset);
|
|
76
|
+
const endIndex = Math.max(0, totalLogs - scrollOffset);
|
|
77
|
+
const visibleLogs = logs.slice(startIndex, endIndex);
|
|
78
|
+
|
|
79
|
+
// Show scroll indicator
|
|
80
|
+
const canScrollUp = startIndex > 0;
|
|
81
|
+
const canScrollDown = scrollOffset > 0;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Box flexDirection="column" padding={1} flexGrow={1} overflow="hidden">
|
|
85
|
+
{canScrollUp && (
|
|
86
|
+
<Text color="gray" dimColor>↑ {startIndex} more lines (Shift+↑ to scroll)</Text>
|
|
87
|
+
)}
|
|
88
|
+
{visibleLogs.length === 0 ? (
|
|
89
|
+
<Text color="gray" dimColor>No logs yet...</Text>
|
|
90
|
+
) : (
|
|
91
|
+
visibleLogs.map((log, index) => (
|
|
92
|
+
<Text key={startIndex + index} wrap="wrap">
|
|
93
|
+
{formatLog(log)}
|
|
94
|
+
</Text>
|
|
95
|
+
))
|
|
96
|
+
)}
|
|
97
|
+
{canScrollDown && (
|
|
98
|
+
<Text color="gray" dimColor>↓ {scrollOffset} more lines (Shift+↓ to scroll)</Text>
|
|
99
|
+
)}
|
|
100
|
+
</Box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatLog(log: string): React.ReactNode {
|
|
105
|
+
// Color code different log types
|
|
106
|
+
if (log.startsWith('[VITE]') || log.includes('VITE')) {
|
|
107
|
+
return <Text color="cyan">{log}</Text>;
|
|
108
|
+
}
|
|
109
|
+
if (log.startsWith('[SOCKET]') || log.includes('Socket')) {
|
|
110
|
+
return <Text color="green">{log}</Text>;
|
|
111
|
+
}
|
|
112
|
+
if (log.includes('error') || log.includes('Error') || log.includes('ERROR')) {
|
|
113
|
+
return <Text color="red">{log}</Text>;
|
|
114
|
+
}
|
|
115
|
+
if (log.includes('warning') || log.includes('Warning') || log.includes('WARN')) {
|
|
116
|
+
return <Text color="yellow">{log}</Text>;
|
|
117
|
+
}
|
|
118
|
+
if (log.includes('Starting') || log.includes('started')) {
|
|
119
|
+
return <Text color="blue">{log}</Text>;
|
|
120
|
+
}
|
|
121
|
+
return <Text>{log}</Text>;
|
|
122
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import type { Service } from '../App.js';
|
|
4
|
+
|
|
5
|
+
interface TabBarProps {
|
|
6
|
+
services: Service[];
|
|
7
|
+
activeTab: number;
|
|
8
|
+
onTabChange: (index: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STATUS_COLORS: Record<Service['status'], string> = {
|
|
12
|
+
stopped: 'gray',
|
|
13
|
+
starting: 'yellow',
|
|
14
|
+
running: 'green',
|
|
15
|
+
error: 'red',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const STATUS_ICONS: Record<Service['status'], string> = {
|
|
19
|
+
stopped: '○',
|
|
20
|
+
starting: '◐',
|
|
21
|
+
running: '●',
|
|
22
|
+
error: '✖',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function TabBar({ services, activeTab, onTabChange }: TabBarProps) {
|
|
26
|
+
return (
|
|
27
|
+
<Box paddingX={1} gap={2} justifyContent="space-between">
|
|
28
|
+
<Box gap={2}>
|
|
29
|
+
{services.map((service, index) => {
|
|
30
|
+
const isActive = index === activeTab;
|
|
31
|
+
const statusColor = STATUS_COLORS[service.status];
|
|
32
|
+
const statusIcon = STATUS_ICONS[service.status];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Box key={service.id}>
|
|
36
|
+
<Text
|
|
37
|
+
color={isActive ? 'white' : 'gray'}
|
|
38
|
+
backgroundColor={isActive ? 'blue' : undefined}
|
|
39
|
+
bold={isActive}
|
|
40
|
+
>
|
|
41
|
+
{' '}
|
|
42
|
+
<Text color={statusColor}>{statusIcon}</Text>
|
|
43
|
+
{' '}
|
|
44
|
+
{service.name}
|
|
45
|
+
{' '}
|
|
46
|
+
</Text>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</Box>
|
|
51
|
+
<Box>
|
|
52
|
+
<Text color="gray">Tab/←→ switch tabs</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
}
|