@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.
Files changed (145) hide show
  1. package/.github/workflows/npm-publish.yml +48 -0
  2. package/CLAUDE.md +400 -0
  3. package/README.md +247 -0
  4. package/REFACTOR_PLAN.md +194 -0
  5. package/bin/gx-devtools.js +87 -0
  6. package/bin/lib/cli.js +251 -0
  7. package/bin/lib/commands/assets.js +337 -0
  8. package/bin/lib/commands/build.js +259 -0
  9. package/bin/lib/commands/datastore.js +433 -0
  10. package/bin/lib/commands/dev.js +328 -0
  11. package/bin/lib/commands/extensions.js +298 -0
  12. package/bin/lib/commands/index.js +35 -0
  13. package/bin/lib/commands/init.js +307 -0
  14. package/bin/lib/commands/publish.js +189 -0
  15. package/bin/lib/commands/socket.js +158 -0
  16. package/bin/lib/commands/ssl.js +47 -0
  17. package/bin/lib/constants.js +120 -0
  18. package/bin/lib/tui/App.tsx +600 -0
  19. package/bin/lib/tui/components/CommandInput.tsx +278 -0
  20. package/bin/lib/tui/components/GeminiPanel.tsx +161 -0
  21. package/bin/lib/tui/components/Header.tsx +27 -0
  22. package/bin/lib/tui/components/LogPanel.tsx +122 -0
  23. package/bin/lib/tui/components/TabBar.tsx +56 -0
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +80 -0
  25. package/bin/lib/tui/index.tsx +63 -0
  26. package/bin/lib/tui/services/ExtensionService.ts +122 -0
  27. package/bin/lib/tui/services/GeminiService.ts +395 -0
  28. package/bin/lib/tui/services/ServiceManager.ts +336 -0
  29. package/bin/lib/tui/services/SocketService.ts +204 -0
  30. package/bin/lib/tui/services/ViteService.ts +107 -0
  31. package/bin/lib/tui/services/index.ts +13 -0
  32. package/bin/lib/utils/files.js +180 -0
  33. package/bin/lib/utils/index.js +17 -0
  34. package/bin/lib/utils/paths.js +138 -0
  35. package/bin/lib/utils/prompts.js +71 -0
  36. package/bin/lib/utils/ssl.js +233 -0
  37. package/browser-extensions/README.md +1 -0
  38. package/browser-extensions/chrome/background.js +857 -0
  39. package/browser-extensions/chrome/content.js +51 -0
  40. package/browser-extensions/chrome/devtools.html +9 -0
  41. package/browser-extensions/chrome/devtools.js +23 -0
  42. package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
  43. package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
  44. package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
  45. package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
  46. package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
  47. package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
  48. package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
  49. package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
  50. package/browser-extensions/chrome/inspector.js +1087 -0
  51. package/browser-extensions/chrome/manifest.json +70 -0
  52. package/browser-extensions/chrome/panel.html +638 -0
  53. package/browser-extensions/chrome/panel.js +862 -0
  54. package/browser-extensions/chrome/popup.html +399 -0
  55. package/browser-extensions/chrome/popup.js +515 -0
  56. package/browser-extensions/chrome/rules.json +1 -0
  57. package/browser-extensions/chrome/test-chrome.html +145 -0
  58. package/browser-extensions/chrome/test-mixed-content.html +190 -0
  59. package/browser-extensions/chrome/test-uri-pattern.html +199 -0
  60. package/browser-extensions/firefox/README.md +134 -0
  61. package/browser-extensions/firefox/background.js +804 -0
  62. package/browser-extensions/firefox/content.js +120 -0
  63. package/browser-extensions/firefox/debug-errors.html +229 -0
  64. package/browser-extensions/firefox/debug-https.html +113 -0
  65. package/browser-extensions/firefox/devtools.html +9 -0
  66. package/browser-extensions/firefox/devtools.js +24 -0
  67. package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
  68. package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
  69. package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
  70. package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
  71. package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
  72. package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
  73. package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
  74. package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
  75. package/browser-extensions/firefox/inspector.js +1087 -0
  76. package/browser-extensions/firefox/manifest.json +67 -0
  77. package/browser-extensions/firefox/panel.html +638 -0
  78. package/browser-extensions/firefox/panel.js +862 -0
  79. package/browser-extensions/firefox/popup.html +525 -0
  80. package/browser-extensions/firefox/popup.js +536 -0
  81. package/browser-extensions/firefox/test-gramercy.html +126 -0
  82. package/browser-extensions/firefox/test-imports.html +58 -0
  83. package/browser-extensions/firefox/test-masking.html +147 -0
  84. package/browser-extensions/firefox/test-uri-pattern.html +199 -0
  85. package/docs/DOCUSAURUS_IMPORT.md +378 -0
  86. package/docs/_category_.json +8 -0
  87. package/docs/app-manifest.md +272 -0
  88. package/docs/building-for-platform.md +315 -0
  89. package/docs/dev-tools.md +291 -0
  90. package/docs/getting-started.md +180 -0
  91. package/docs/gxp-store.md +305 -0
  92. package/docs/index.md +44 -0
  93. package/package.json +77 -0
  94. package/runtime/PortalContainer.vue +326 -0
  95. package/runtime/dev-tools/DevToolsModal.vue +217 -0
  96. package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
  97. package/runtime/dev-tools/MockDataEditor.vue +621 -0
  98. package/runtime/dev-tools/SocketSimulator.vue +562 -0
  99. package/runtime/dev-tools/StoreInspector.vue +644 -0
  100. package/runtime/dev-tools/index.js +6 -0
  101. package/runtime/gxpStringsPlugin.js +428 -0
  102. package/runtime/index.html +22 -0
  103. package/runtime/main.js +32 -0
  104. package/runtime/mock-api/auth-middleware.js +97 -0
  105. package/runtime/mock-api/image-generator.js +221 -0
  106. package/runtime/mock-api/index.js +197 -0
  107. package/runtime/mock-api/response-generator.js +394 -0
  108. package/runtime/mock-api/route-generator.js +323 -0
  109. package/runtime/mock-api/socket-triggers.js +371 -0
  110. package/runtime/mock-api/spec-loader.js +300 -0
  111. package/runtime/server.js +180 -0
  112. package/runtime/stores/gxpPortalConfigStore.js +554 -0
  113. package/runtime/stores/index.js +6 -0
  114. package/runtime/vite-inspector-plugin.js +749 -0
  115. package/runtime/vite-source-tracker-plugin.js +232 -0
  116. package/runtime/vite.config.js +402 -0
  117. package/scripts/launch-chrome.js +90 -0
  118. package/scripts/pack-chrome.js +91 -0
  119. package/socket-events/AiSessionMessageCreated.json +18 -0
  120. package/socket-events/SocialStreamPostCreated.json +24 -0
  121. package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
  122. package/template/README.md +332 -0
  123. package/template/app-manifest.json +32 -0
  124. package/template/dev-assets/images/avatar-placeholder.png +0 -0
  125. package/template/dev-assets/images/background-placeholder.jpg +0 -0
  126. package/template/dev-assets/images/banner-placeholder.jpg +0 -0
  127. package/template/dev-assets/images/icon-placeholder.png +0 -0
  128. package/template/dev-assets/images/logo-placeholder.png +0 -0
  129. package/template/dev-assets/images/product-placeholder.jpg +0 -0
  130. package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
  131. package/template/env.example +51 -0
  132. package/template/gitignore +53 -0
  133. package/template/index.html +22 -0
  134. package/template/main.js +28 -0
  135. package/template/src/DemoPage.vue +459 -0
  136. package/template/src/Plugin.vue +38 -0
  137. package/template/src/stores/index.js +9 -0
  138. package/template/src/stores/test-data.json +173 -0
  139. package/template/theme-layouts/AdditionalStyling.css +0 -0
  140. package/template/theme-layouts/PrivateLayout.vue +39 -0
  141. package/template/theme-layouts/PublicLayout.vue +39 -0
  142. package/template/theme-layouts/SystemLayout.vue +39 -0
  143. package/template/vite.config.js +333 -0
  144. package/tsconfig.tui.json +21 -0
  145. 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>&gt;</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">&gt; </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
+ }