@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,600 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
3
+ import WelcomeScreen from './components/WelcomeScreen.js';
4
+ import Header from './components/Header.js';
5
+ import TabBar from './components/TabBar.js';
6
+ import LogPanel from './components/LogPanel.js';
7
+ import CommandInput from './components/CommandInput.js';
8
+ import GeminiPanel from './components/GeminiPanel.js';
9
+ import {
10
+ serviceManager,
11
+ startVite,
12
+ stopVite,
13
+ startSocket,
14
+ stopSocket,
15
+ startExtension,
16
+ stopExtension,
17
+ listSocketEvents,
18
+ sendSocketEvent,
19
+ ServiceStatus,
20
+ BrowserType,
21
+ geminiService,
22
+ isAuthenticated,
23
+ clearAuthTokens,
24
+ } from './services/index.js';
25
+
26
+ export interface Service {
27
+ id: string;
28
+ name: string;
29
+ status: ServiceStatus;
30
+ logs: string[];
31
+ }
32
+
33
+ export interface AppProps {
34
+ autoStart?: string[];
35
+ args?: Record<string, unknown>;
36
+ }
37
+
38
+ export default function App({ autoStart, args }: AppProps) {
39
+ const { exit } = useApp();
40
+ const { stdout } = useStdout();
41
+ const [showGemini, setShowGemini] = useState(false);
42
+ const [services, setServices] = useState<Service[]>([]);
43
+ const [activeTab, setActiveTab] = useState(0);
44
+ const [suggestionRows, setSuggestionRows] = useState(0);
45
+
46
+ // Get terminal height for full screen
47
+ const terminalHeight = stdout?.rows || 24;
48
+
49
+ // Sync services from ServiceManager
50
+ const syncServices = useCallback(() => {
51
+ const managerServices = serviceManager.getAllServices();
52
+ setServices(managerServices.map(s => ({
53
+ id: s.id,
54
+ name: s.name,
55
+ status: s.status,
56
+ logs: s.logs,
57
+ })));
58
+ }, []);
59
+
60
+ // Set up ServiceManager event listeners
61
+ useEffect(() => {
62
+ const onLog = (id: string, message: string) => {
63
+ setServices(prev => prev.map(s =>
64
+ s.id === id ? { ...s, logs: [...s.logs, message] } : s
65
+ ));
66
+ };
67
+
68
+ const onStatusChange = (id: string, status: ServiceStatus) => {
69
+ setServices(prev => prev.map(s =>
70
+ s.id === id ? { ...s, status } : s
71
+ ));
72
+ };
73
+
74
+ const onLogsCleared = (id: string) => {
75
+ setServices(prev => prev.map(s =>
76
+ s.id === id ? { ...s, logs: [] } : s
77
+ ));
78
+ };
79
+
80
+ serviceManager.on('log', onLog);
81
+ serviceManager.on('statusChange', onStatusChange);
82
+ serviceManager.on('logsCleared', onLogsCleared);
83
+
84
+ return () => {
85
+ serviceManager.off('log', onLog);
86
+ serviceManager.off('statusChange', onStatusChange);
87
+ serviceManager.off('logsCleared', onLogsCleared);
88
+ };
89
+ }, []);
90
+
91
+ // Cleanup on exit
92
+ useEffect(() => {
93
+ const cleanup = () => {
94
+ serviceManager.forceStopAll();
95
+ };
96
+
97
+ process.on('SIGINT', cleanup);
98
+ process.on('SIGTERM', cleanup);
99
+
100
+ return () => {
101
+ cleanup();
102
+ process.off('SIGINT', cleanup);
103
+ process.off('SIGTERM', cleanup);
104
+ };
105
+ }, []);
106
+
107
+ // Handle keyboard shortcuts
108
+ useInput((input, key) => {
109
+ // Ctrl+C to exit
110
+ if (key.ctrl && input === 'c') {
111
+ serviceManager.forceStopAll();
112
+ exit();
113
+ return;
114
+ }
115
+
116
+ // Ctrl+L to clear current log
117
+ if (key.ctrl && input === 'l') {
118
+ if (services[activeTab]) {
119
+ serviceManager.clearLogs(services[activeTab].id);
120
+ }
121
+ return;
122
+ }
123
+
124
+ // Ctrl+K to stop current service
125
+ if (key.ctrl && input === 'k') {
126
+ if (services[activeTab] && services[activeTab].id !== 'system') {
127
+ stopService(services[activeTab].id);
128
+ }
129
+ return;
130
+ }
131
+
132
+ // Tab to cycle through tabs (when not in input)
133
+ if (key.tab && services.length > 0) {
134
+ const nextTab = key.shift
135
+ ? (activeTab - 1 + services.length) % services.length
136
+ : (activeTab + 1) % services.length;
137
+ setActiveTab(nextTab);
138
+ return;
139
+ }
140
+
141
+ // Left/Right arrow to switch tabs
142
+ if (key.leftArrow && services.length > 0) {
143
+ setActiveTab((activeTab - 1 + services.length) % services.length);
144
+ return;
145
+ }
146
+ if (key.rightArrow && services.length > 0) {
147
+ setActiveTab((activeTab + 1) % services.length);
148
+ return;
149
+ }
150
+
151
+ // Ctrl+1-9 or Cmd+1-9 to switch tabs (for compatibility)
152
+ if ((key.ctrl || key.meta) && /^[1-9]$/.test(input)) {
153
+ const tabIndex = parseInt(input) - 1;
154
+ if (tabIndex < services.length) {
155
+ setActiveTab(tabIndex);
156
+ }
157
+ return;
158
+ }
159
+ });
160
+
161
+ // Handle auto-start commands
162
+ useEffect(() => {
163
+ if (autoStart?.length) {
164
+ setTimeout(() => {
165
+ autoStart.forEach(cmd => {
166
+ handleCommand(`/${cmd}`);
167
+ });
168
+ }, 100);
169
+ }
170
+ }, []);
171
+
172
+ const handleCommand = (input: string) => {
173
+ const trimmed = input.trim();
174
+ if (!trimmed.startsWith('/')) return;
175
+
176
+ const parts = trimmed.slice(1).split(' ');
177
+ const command = parts[0];
178
+ const cmdArgs = parts.slice(1);
179
+
180
+ switch (command) {
181
+ case 'help':
182
+ addSystemLog(getHelpText());
183
+ break;
184
+
185
+ case 'dev':
186
+ startDevServer(cmdArgs);
187
+ break;
188
+
189
+ case 'socket':
190
+ if (cmdArgs[0] === 'send') {
191
+ handleSocketSend(cmdArgs.slice(1));
192
+ } else if (cmdArgs[0] === 'list') {
193
+ handleSocketList();
194
+ } else {
195
+ const socketWithMock = cmdArgs.includes('--with-mock') || args?.withMock === true;
196
+ startSocketServer(socketWithMock);
197
+ }
198
+ break;
199
+
200
+ case 'mock':
201
+ // Shorthand for /socket --with-mock
202
+ startSocketServer(true);
203
+ break;
204
+
205
+ case 'ext':
206
+ const browser = cmdArgs[0] || 'chrome';
207
+ launchExtension(browser);
208
+ break;
209
+
210
+ case 'stop':
211
+ stopService(cmdArgs[0]);
212
+ break;
213
+
214
+ case 'restart':
215
+ restartService(cmdArgs[0]);
216
+ break;
217
+
218
+ case 'clear':
219
+ if (services[activeTab]) {
220
+ serviceManager.clearLogs(services[activeTab].id);
221
+ }
222
+ break;
223
+
224
+ case 'quit':
225
+ case 'exit':
226
+ serviceManager.forceStopAll();
227
+ exit();
228
+ break;
229
+
230
+ case 'gemini':
231
+ case 'ai':
232
+ handleGeminiCommand(cmdArgs);
233
+ break;
234
+
235
+ default:
236
+ addSystemLog(`Unknown command: ${command}. Type /help for available commands.`);
237
+ }
238
+ };
239
+
240
+ const startDevServer = (cmdArgs: string[]) => {
241
+ const noHttps = cmdArgs.includes('--no-https') || args?.noHttps === true;
242
+ const noSocket = cmdArgs.includes('--no-socket') || args?.noSocket === true;
243
+ const withSocket = cmdArgs.includes('--with-socket') || args?.withSocket === true;
244
+ const withMock = cmdArgs.includes('--with-mock') || args?.withMock === true;
245
+ const withFirefox = cmdArgs.includes('--firefox') || args?.firefox === true;
246
+ const withChrome = cmdArgs.includes('--chrome') || args?.chrome === true;
247
+
248
+ // Determine port from env or default
249
+ const port = process.env.NODE_PORT || 3060;
250
+ const useHttps = !noHttps;
251
+
252
+ // Check SOCKET_IO_ENABLED env var (default to socket if enabled, unless --no-socket)
253
+ const socketEnabled = process.env.SOCKET_IO_ENABLED === 'true';
254
+ // Mock API requires socket server
255
+ const shouldStartSocket = !noSocket && (withSocket || socketEnabled || withMock);
256
+
257
+ // Check if already running
258
+ if (serviceManager.isRunning('vite')) {
259
+ addSystemLog('Vite dev server is already running.');
260
+ // Switch to vite tab
261
+ const viteIdx = services.findIndex(s => s.id === 'vite');
262
+ if (viteIdx >= 0) setActiveTab(viteIdx);
263
+ return;
264
+ }
265
+
266
+ startVite({ noHttps });
267
+
268
+ // Also start socket server based on flags/env (with mock if requested)
269
+ if (shouldStartSocket && !serviceManager.isRunning('socket')) {
270
+ startSocket({ withMock });
271
+ }
272
+
273
+ // Launch browser extensions if requested (pass URL options)
274
+ if (withFirefox && !serviceManager.isRunning('ext-firefox')) {
275
+ startExtension({ browser: 'firefox', useHttps, port });
276
+ }
277
+ if (withChrome && !serviceManager.isRunning('ext-chrome')) {
278
+ startExtension({ browser: 'chrome', useHttps, port });
279
+ }
280
+
281
+ // Sync and switch to the new vite tab
282
+ const updatedServices = serviceManager.getAllServices();
283
+ const viteIdx = updatedServices.findIndex(s => s.id === 'vite');
284
+ setServices(updatedServices.map(s => ({
285
+ id: s.id,
286
+ name: s.name,
287
+ status: s.status,
288
+ logs: s.logs,
289
+ })));
290
+ setActiveTab(viteIdx >= 0 ? viteIdx : Math.max(0, updatedServices.length - 1));
291
+ };
292
+
293
+ const startSocketServer = (withMock: boolean = false) => {
294
+ if (serviceManager.isRunning('socket')) {
295
+ addSystemLog('Socket.IO server is already running.');
296
+ const socketIdx = services.findIndex(s => s.id === 'socket');
297
+ if (socketIdx >= 0) setActiveTab(socketIdx);
298
+ return;
299
+ }
300
+
301
+ startSocket({ withMock });
302
+
303
+ // Sync and switch to the new socket tab
304
+ const updatedServices = serviceManager.getAllServices();
305
+ const socketIdx = updatedServices.findIndex(s => s.id === 'socket');
306
+ setServices(updatedServices.map(s => ({
307
+ id: s.id,
308
+ name: s.name,
309
+ status: s.status,
310
+ logs: s.logs,
311
+ })));
312
+ setActiveTab(socketIdx >= 0 ? socketIdx : Math.max(0, updatedServices.length - 1));
313
+ };
314
+
315
+ const launchExtension = (browser: string) => {
316
+ const browserType = browser.toLowerCase() as BrowserType;
317
+ if (browserType !== 'chrome' && browserType !== 'firefox') {
318
+ addSystemLog(`Invalid browser: ${browser}. Use 'chrome' or 'firefox'.`);
319
+ return;
320
+ }
321
+
322
+ const serviceId = `ext-${browserType}`;
323
+ if (serviceManager.isRunning(serviceId)) {
324
+ addSystemLog(`${browser} extension is already running.`);
325
+ const extIdx = services.findIndex(s => s.id === serviceId);
326
+ if (extIdx >= 0) setActiveTab(extIdx);
327
+ return;
328
+ }
329
+
330
+ startExtension({ browser: browserType });
331
+
332
+ // Sync and switch to the new extension tab
333
+ const updatedServices = serviceManager.getAllServices();
334
+ const extIdx = updatedServices.findIndex(s => s.id === serviceId);
335
+ setServices(updatedServices.map(s => ({
336
+ id: s.id,
337
+ name: s.name,
338
+ status: s.status,
339
+ logs: s.logs,
340
+ })));
341
+ setActiveTab(extIdx >= 0 ? extIdx : Math.max(0, updatedServices.length - 1));
342
+ };
343
+
344
+ const stopService = (serviceId?: string) => {
345
+ const targetId = serviceId || services[activeTab]?.id;
346
+ if (!targetId) {
347
+ addSystemLog('No service specified. Usage: /stop <service-id>');
348
+ return;
349
+ }
350
+
351
+ if (targetId === 'vite') {
352
+ stopVite();
353
+ } else if (targetId === 'socket') {
354
+ stopSocket();
355
+ } else if (targetId.startsWith('ext-')) {
356
+ const browser = targetId.replace('ext-', '') as BrowserType;
357
+ stopExtension(browser);
358
+ } else {
359
+ serviceManager.stop(targetId);
360
+ }
361
+
362
+ syncServices();
363
+ };
364
+
365
+ const restartService = (serviceId?: string) => {
366
+ const targetId = serviceId || services[activeTab]?.id;
367
+ if (!targetId) {
368
+ addSystemLog('No service to restart. Usage: /restart [service-id]');
369
+ return;
370
+ }
371
+
372
+ if (targetId === 'system') {
373
+ addSystemLog('Cannot restart the system service.');
374
+ return;
375
+ }
376
+
377
+ const success = serviceManager.restart(targetId);
378
+ if (!success) {
379
+ addSystemLog(`Cannot restart "${targetId}". Service config not found.`);
380
+ }
381
+ syncServices();
382
+ };
383
+
384
+ const handleGeminiCommand = async (cmdArgs: string[]) => {
385
+ const subCommand = cmdArgs[0];
386
+
387
+ switch (subCommand) {
388
+ case 'enable':
389
+ addSystemLog('Starting Google OAuth flow...');
390
+ addSystemLog('A browser window will open for authentication.');
391
+ const result = await geminiService.startOAuthFlow();
392
+ if (result.success) {
393
+ addSystemLog(`✅ ${result.message}`);
394
+ } else {
395
+ addSystemLog(`❌ ${result.message}`);
396
+ }
397
+ break;
398
+
399
+ case 'logout':
400
+ case 'disable':
401
+ clearAuthTokens();
402
+ addSystemLog('Logged out from Gemini AI.');
403
+ break;
404
+
405
+ case 'status':
406
+ if (isAuthenticated()) {
407
+ addSystemLog('✅ Gemini AI is authenticated and ready.');
408
+ } else {
409
+ addSystemLog('❌ Not authenticated. Run /gemini enable to set up.');
410
+ }
411
+ break;
412
+
413
+ case 'ask':
414
+ // Quick question without opening panel
415
+ const question = cmdArgs.slice(1).join(' ');
416
+ if (!question) {
417
+ addSystemLog('Usage: /gemini ask <your question>');
418
+ return;
419
+ }
420
+ if (!isAuthenticated()) {
421
+ addSystemLog('Not authenticated. Run /gemini enable first.');
422
+ return;
423
+ }
424
+ addSystemLog(`Asking Gemini: ${question}`);
425
+ try {
426
+ geminiService.loadProjectContext(process.cwd());
427
+ const response = await geminiService.sendMessage(question);
428
+ addSystemLog(`Gemini: ${response}`);
429
+ } catch (err) {
430
+ addSystemLog(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
431
+ }
432
+ break;
433
+
434
+ case 'clear':
435
+ geminiService.clearConversation();
436
+ addSystemLog('Conversation history cleared.');
437
+ break;
438
+
439
+ default:
440
+ // No subcommand = open chat panel
441
+ if (!isAuthenticated()) {
442
+ addSystemLog('Not authenticated. Run /gemini enable to set up Google authentication.');
443
+ return;
444
+ }
445
+ setShowGemini(true);
446
+ }
447
+ };
448
+
449
+ const addSystemLog = (message: string) => {
450
+ // Find or create system service for general messages
451
+ let systemService = services.find(s => s.id === 'system');
452
+ if (!systemService) {
453
+ const newService: Service = {
454
+ id: 'system',
455
+ name: 'System',
456
+ status: 'running',
457
+ logs: [message],
458
+ };
459
+ setServices(prev => {
460
+ const updated = [...prev, newService];
461
+ return updated;
462
+ });
463
+ if (services.length === 0) setActiveTab(0);
464
+ } else {
465
+ setServices(prev => prev.map(s =>
466
+ s.id === 'system' ? { ...s, logs: [...s.logs, message] } : s
467
+ ));
468
+ }
469
+
470
+ // Switch to system tab
471
+ setTimeout(() => {
472
+ const sysIdx = services.findIndex(s => s.id === 'system');
473
+ if (sysIdx >= 0) setActiveTab(sysIdx);
474
+ }, 50);
475
+ };
476
+
477
+ const handleSocketSend = async (eventArgs: string[]) => {
478
+ if (!eventArgs.length) {
479
+ addSystemLog('Usage: /socket send <event-name> [identifier]');
480
+ return;
481
+ }
482
+
483
+ const eventName = eventArgs[0];
484
+ const identifier = eventArgs[1];
485
+
486
+ addSystemLog(`Sending socket event: ${eventName}...`);
487
+
488
+ const result = await sendSocketEvent(eventName, identifier);
489
+ if (result.success) {
490
+ addSystemLog(`✅ ${result.message}`);
491
+ } else {
492
+ addSystemLog(`❌ ${result.message}`);
493
+ }
494
+ };
495
+
496
+ const handleSocketList = () => {
497
+ const events = listSocketEvents();
498
+ if (events.length === 0) {
499
+ addSystemLog('No socket events found. Check your socket-events directory.');
500
+ return;
501
+ }
502
+
503
+ let message = 'Available socket events:\n';
504
+ for (const event of events) {
505
+ message += `\n ${event.name}\n`;
506
+ message += ` Event: ${event.event}\n`;
507
+ message += ` Channel: ${event.channel}`;
508
+ }
509
+ message += '\n\nUsage: /socket send <event-name> [identifier]';
510
+ addSystemLog(message);
511
+ };
512
+
513
+ const getHelpText = () => `
514
+ Available commands:
515
+ /dev Start Vite (+ Socket if SOCKET_IO_ENABLED=true)
516
+ /dev --with-socket Start Vite + Socket.IO together
517
+ /dev --with-mock Start Vite + Socket.IO + Mock API server
518
+ /dev --no-socket Start Vite only (skip Socket.IO)
519
+ /dev --no-https Start Vite without SSL
520
+ /dev --firefox Start Vite + Firefox extension
521
+ /dev --chrome Start Vite + Chrome extension
522
+ /socket Start Socket.IO server
523
+ /socket --with-mock Start Socket.IO with Mock API enabled
524
+ /socket send <event> Send socket event
525
+ /socket list List available events
526
+ /mock Start Socket.IO + Mock API (shorthand)
527
+ /ext chrome Launch Chrome extension
528
+ /ext firefox Launch Firefox extension
529
+ /stop [service] Stop a running service
530
+ /restart [service] Restart a service
531
+ /clear Clear current log panel
532
+ /gemini Open Gemini AI chat panel
533
+ /gemini enable Set up Google authentication
534
+ /gemini ask <query> Quick question to Gemini
535
+ /gemini status Check authentication status
536
+ /gemini logout Log out from Gemini
537
+ /help Show this help message
538
+ /quit Exit the application
539
+
540
+ Keyboard shortcuts:
541
+ Tab / Shift+Tab Cycle through tabs
542
+ Left/Right Switch tabs
543
+ Cmd+1/2/3... Jump to tab (Mac)
544
+ Shift+Up/Down Scroll logs
545
+ Cmd+Up/Down Jump to top/bottom of logs
546
+ Ctrl+L Clear current log
547
+ Ctrl+C Exit application
548
+ Up/Down Command history (in input)
549
+ `;
550
+
551
+ // Show Gemini panel
552
+ if (showGemini) {
553
+ return (
554
+ <GeminiPanel
555
+ onClose={() => setShowGemini(false)}
556
+ onLog={addSystemLog}
557
+ />
558
+ );
559
+ }
560
+
561
+ const currentService = services[activeTab];
562
+
563
+ // Calculate log panel height to make room for suggestions
564
+ // Fixed elements: Header (3), TabBar (1), Log border (2), Input (3), Hints (1) = 10 rows minimum
565
+ const fixedRows = 10;
566
+ const availableForLog = terminalHeight - fixedRows - suggestionRows;
567
+ const logPanelHeight = Math.max(3, availableForLog); // Minimum 3 rows for log panel
568
+
569
+ return (
570
+ <Box flexDirection="column" height={terminalHeight}>
571
+ <Header projectName={process.cwd().split('/').pop() || 'gxdev'} />
572
+
573
+ {services.length > 0 && (
574
+ <TabBar
575
+ services={services}
576
+ activeTab={activeTab}
577
+ onTabChange={setActiveTab}
578
+ />
579
+ )}
580
+
581
+ <Box height={logPanelHeight} flexDirection="column" borderStyle="single" borderColor="gray" overflow="hidden">
582
+ {currentService ? (
583
+ <LogPanel logs={currentService.logs} maxHeight={logPanelHeight} />
584
+ ) : (
585
+ <WelcomeScreen />
586
+ )}
587
+ </Box>
588
+
589
+ <CommandInput
590
+ onSubmit={handleCommand}
591
+ activeService={currentService ? {
592
+ id: currentService.id,
593
+ name: currentService.name,
594
+ status: currentService.status
595
+ } : null}
596
+ onSuggestionsChange={setSuggestionRows}
597
+ />
598
+ </Box>
599
+ );
600
+ }