@gxp-dev/tools 2.0.12 → 2.0.14
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/commands/init.js +15 -2
- package/bin/lib/tui/App.tsx +69 -49
- package/bin/lib/tui/components/CommandInput.tsx +100 -95
- package/bin/lib/tui/components/LogPanel.tsx +30 -17
- package/bin/lib/tui/components/TabBar.tsx +1 -1
- package/bin/lib/tui/services/ExtensionService.ts +34 -0
- package/bin/lib/utils/ai-scaffold.js +82 -11
- package/browser-extensions/chrome/manifest.json +1 -1
- package/browser-extensions/chrome/popup.html +339 -237
- package/browser-extensions/chrome/popup.js +110 -110
- package/browser-extensions/firefox/manifest.json +2 -1
- package/browser-extensions/firefox/popup.js +31 -12
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +66 -48
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/CommandInput.d.ts.map +1 -1
- package/dist/tui/components/CommandInput.js +87 -89
- package/dist/tui/components/CommandInput.js.map +1 -1
- package/dist/tui/components/LogPanel.d.ts +4 -2
- package/dist/tui/components/LogPanel.d.ts.map +1 -1
- package/dist/tui/components/LogPanel.js +25 -15
- package/dist/tui/components/LogPanel.js.map +1 -1
- package/dist/tui/components/TabBar.js +1 -1
- package/dist/tui/components/TabBar.js.map +1 -1
- package/dist/tui/services/ExtensionService.d.ts.map +1 -1
- package/dist/tui/services/ExtensionService.js +30 -0
- package/dist/tui/services/ExtensionService.js.map +1 -1
- package/package.json +1 -1
- package/scripts/launch-chrome.js +39 -0
package/bin/lib/commands/init.js
CHANGED
|
@@ -340,7 +340,8 @@ async function runInteractiveConfig(projectPath, initialName) {
|
|
|
340
340
|
console.log(" To enable AI scaffolding, set up one of:");
|
|
341
341
|
console.log(" • Claude CLI: npm install -g @anthropic-ai/claude-code && claude login");
|
|
342
342
|
console.log(" • Codex CLI: npm install -g @openai/codex && codex auth");
|
|
343
|
-
console.log(" • Gemini:
|
|
343
|
+
console.log(" • Gemini CLI: npm install -g @google/gemini-cli && gemini");
|
|
344
|
+
console.log(" • Gemini API: export GEMINI_API_KEY=your_key");
|
|
344
345
|
console.log("");
|
|
345
346
|
aiChoice = "skip";
|
|
346
347
|
} else {
|
|
@@ -352,7 +353,19 @@ async function runInteractiveConfig(projectPath, initialName) {
|
|
|
352
353
|
for (const provider of availableProviders) {
|
|
353
354
|
let authInfo = "";
|
|
354
355
|
if (provider.id === "gemini") {
|
|
355
|
-
|
|
356
|
+
switch (provider.method) {
|
|
357
|
+
case "cli":
|
|
358
|
+
authInfo = "logged in";
|
|
359
|
+
break;
|
|
360
|
+
case "api_key":
|
|
361
|
+
authInfo = "via API key";
|
|
362
|
+
break;
|
|
363
|
+
case "gcloud":
|
|
364
|
+
authInfo = "via gcloud";
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
authInfo = "";
|
|
368
|
+
}
|
|
356
369
|
} else {
|
|
357
370
|
authInfo = "logged in";
|
|
358
371
|
}
|
package/bin/lib/tui/App.tsx
CHANGED
|
@@ -137,16 +137,7 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
//
|
|
141
|
-
if (key.tab && services.length > 0) {
|
|
142
|
-
const nextTab = key.shift
|
|
143
|
-
? (activeTab - 1 + services.length) % services.length
|
|
144
|
-
: (activeTab + 1) % services.length;
|
|
145
|
-
setActiveTab(nextTab);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Left/Right arrow to switch tabs
|
|
140
|
+
// Left/Right arrow to switch tabs (Tab is reserved for command autocomplete)
|
|
150
141
|
if (key.leftArrow && services.length > 0) {
|
|
151
142
|
setActiveTab((activeTab - 1 + services.length) % services.length);
|
|
152
143
|
return;
|
|
@@ -156,7 +147,7 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
156
147
|
return;
|
|
157
148
|
}
|
|
158
149
|
|
|
159
|
-
// Ctrl+1-9 or Cmd+1-9 to switch tabs
|
|
150
|
+
// Ctrl+1-9 or Cmd+1-9 to switch tabs directly
|
|
160
151
|
if ((key.ctrl || key.meta) && /^[1-9]$/.test(input)) {
|
|
161
152
|
const tabIndex = parseInt(input) - 1;
|
|
162
153
|
if (tabIndex < services.length) {
|
|
@@ -530,11 +521,24 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
530
521
|
addSystemLog('Scanning source files for GxP configuration...');
|
|
531
522
|
|
|
532
523
|
try {
|
|
533
|
-
// Use
|
|
534
|
-
const path =
|
|
535
|
-
const fs =
|
|
536
|
-
|
|
537
|
-
const
|
|
524
|
+
// Use dynamic imports for ES modules
|
|
525
|
+
const path = await import('path');
|
|
526
|
+
const fs = await import('fs');
|
|
527
|
+
const url = await import('url');
|
|
528
|
+
const { createRequire } = await import('module');
|
|
529
|
+
|
|
530
|
+
// Get the directory of this file and resolve to the utils directory
|
|
531
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
532
|
+
const __dirname = path.dirname(__filename);
|
|
533
|
+
|
|
534
|
+
// The compiled JS is in dist/tui/, utils is in bin/lib/utils/
|
|
535
|
+
// From dist/tui/ we need to go up to package root, then into bin/lib/utils/
|
|
536
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
537
|
+
const utilsPath = path.join(packageRoot, 'bin', 'lib', 'utils', 'extract-config.js');
|
|
538
|
+
|
|
539
|
+
// Create a require function to load CommonJS modules
|
|
540
|
+
const requireCjs = createRequire(import.meta.url);
|
|
541
|
+
const extractConfigUtils = requireCjs(utilsPath) as {
|
|
538
542
|
extractConfigFromSource: (srcDir: string) => ExtractedConfig;
|
|
539
543
|
mergeConfig: (existing: Record<string, unknown>, extracted: ExtractedConfig, options: { overwrite: boolean }) => Record<string, unknown>;
|
|
540
544
|
generateSummary: (config: ExtractedConfig) => string;
|
|
@@ -616,43 +620,59 @@ export default function App({ autoStart, args }: AppProps) {
|
|
|
616
620
|
|
|
617
621
|
const getHelpText = () => `
|
|
618
622
|
Available commands:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
623
|
+
|
|
624
|
+
Development Server:
|
|
625
|
+
/dev Start Vite dev server
|
|
626
|
+
/dev --with-socket Start Vite + Socket.IO
|
|
627
|
+
/dev --with-mock Start Vite + Socket.IO + Mock API
|
|
628
|
+
/dev --no-https Start without SSL
|
|
629
|
+
/dev --no-socket Start without Socket.IO
|
|
630
|
+
/dev --chrome Start + launch Chrome extension
|
|
631
|
+
/dev --firefox Start + launch Firefox extension
|
|
632
|
+
|
|
633
|
+
Socket.IO:
|
|
634
|
+
/socket Start Socket.IO server
|
|
635
|
+
/socket --with-mock Start with Mock API enabled
|
|
636
|
+
/socket send <event> Send a socket event
|
|
637
|
+
/socket list List available events
|
|
638
|
+
/mock Shorthand for /socket --with-mock
|
|
639
|
+
|
|
640
|
+
Browser Extensions:
|
|
641
|
+
/ext chrome Launch Chrome with GxP extension
|
|
642
|
+
/ext firefox Launch Firefox with GxP extension
|
|
643
|
+
|
|
644
|
+
Config Extraction:
|
|
645
|
+
/extract-config Extract GxP config from source
|
|
646
|
+
/extract-config -d Dry run (preview changes)
|
|
647
|
+
/extract-config -o Overwrite existing values
|
|
648
|
+
|
|
649
|
+
AI Assistant:
|
|
650
|
+
/gemini Open Gemini AI chat panel
|
|
651
|
+
/gemini enable Set up Google authentication
|
|
652
|
+
/gemini ask <query> Quick question to AI
|
|
653
|
+
/gemini status Check auth status
|
|
654
|
+
/gemini logout Log out from Gemini
|
|
655
|
+
/gemini clear Clear conversation history
|
|
656
|
+
/ai Alias for /gemini
|
|
657
|
+
|
|
658
|
+
Service Management:
|
|
659
|
+
/stop [service] Stop current or specified service
|
|
660
|
+
/restart [service] Restart a service
|
|
661
|
+
/clear Clear current log panel
|
|
662
|
+
/help Show this help
|
|
663
|
+
/quit Exit application
|
|
646
664
|
|
|
647
665
|
Keyboard shortcuts:
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
Cmd+Up/Down Jump to top/bottom of logs
|
|
666
|
+
←/→ Switch tabs
|
|
667
|
+
Ctrl+1/2/3... Jump to tab directly
|
|
668
|
+
Shift+↑/↓ Scroll logs
|
|
669
|
+
Ctrl+↑/↓ Jump to top/bottom of logs
|
|
653
670
|
Ctrl+L Clear current log
|
|
671
|
+
Ctrl+K Stop current service
|
|
654
672
|
Ctrl+C Exit application
|
|
655
|
-
|
|
673
|
+
Tab Autocomplete command
|
|
674
|
+
↑/↓ Navigate suggestions or command history
|
|
675
|
+
Esc Clear input
|
|
656
676
|
`;
|
|
657
677
|
|
|
658
678
|
// Show Gemini panel
|
|
@@ -1,29 +1,52 @@
|
|
|
1
|
-
import React, { useState, useMemo, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import TextInput from 'ink-text-input';
|
|
4
4
|
|
|
5
|
-
// Command definitions with descriptions
|
|
5
|
+
// Command definitions with descriptions - comprehensive list
|
|
6
6
|
const COMMANDS = [
|
|
7
|
-
|
|
7
|
+
// Dev server commands
|
|
8
|
+
{ cmd: '/dev', args: '', desc: 'Start Vite dev server' },
|
|
8
9
|
{ cmd: '/dev', args: '--with-socket', desc: 'Start Vite + Socket.IO' },
|
|
9
|
-
{ cmd: '/dev', args: '--with-mock', desc: 'Start Vite + Socket.IO + Mock API
|
|
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' },
|
|
10
|
+
{ cmd: '/dev', args: '--with-mock', desc: 'Start Vite + Socket.IO + Mock API' },
|
|
13
11
|
{ cmd: '/dev', args: '--no-https', desc: 'Start Vite without SSL' },
|
|
12
|
+
{ cmd: '/dev', args: '--no-socket', desc: 'Start Vite without Socket.IO' },
|
|
13
|
+
{ cmd: '/dev', args: '--chrome', desc: 'Start Vite + Chrome extension' },
|
|
14
|
+
{ cmd: '/dev', args: '--firefox', desc: 'Start Vite + Firefox extension' },
|
|
15
|
+
|
|
16
|
+
// Socket commands
|
|
14
17
|
{ cmd: '/socket', args: '', desc: 'Start Socket.IO server' },
|
|
15
|
-
{ cmd: '/socket', args: '
|
|
16
|
-
{ cmd: '/socket', args: '
|
|
17
|
-
{ cmd: '/
|
|
18
|
-
{ cmd: '/
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
{ cmd: '/socket', args: '--with-mock', desc: 'Start Socket.IO + Mock API' },
|
|
19
|
+
{ cmd: '/socket', args: 'send <event>', desc: 'Send a socket event' },
|
|
20
|
+
{ cmd: '/socket', args: 'list', desc: 'List available socket events' },
|
|
21
|
+
{ cmd: '/mock', args: '', desc: 'Start Socket.IO + Mock API (shorthand)' },
|
|
22
|
+
|
|
23
|
+
// Browser extension commands
|
|
24
|
+
{ cmd: '/ext', args: 'chrome', desc: 'Launch Chrome with extension' },
|
|
25
|
+
{ cmd: '/ext', args: 'firefox', desc: 'Launch Firefox with extension' },
|
|
26
|
+
|
|
27
|
+
// Service management
|
|
28
|
+
{ cmd: '/stop', args: '[service]', desc: 'Stop current or specified service' },
|
|
29
|
+
{ cmd: '/restart', args: '[service]', desc: 'Restart current or specified service' },
|
|
21
30
|
{ cmd: '/clear', args: '', desc: 'Clear current log panel' },
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
|
|
32
|
+
// Config extraction
|
|
33
|
+
{ cmd: '/extract-config', args: '', desc: 'Extract GxP config from source' },
|
|
34
|
+
{ cmd: '/extract-config', args: '--dry-run', desc: 'Preview config extraction' },
|
|
35
|
+
{ cmd: '/extract-config', args: '--overwrite', desc: 'Overwrite existing config values' },
|
|
36
|
+
|
|
37
|
+
// AI commands
|
|
38
|
+
{ cmd: '/gemini', args: '', desc: 'Open Gemini AI chat panel' },
|
|
39
|
+
{ cmd: '/gemini', args: 'enable', desc: 'Set up Google authentication' },
|
|
24
40
|
{ cmd: '/gemini', args: 'ask <query>', desc: 'Quick AI question' },
|
|
41
|
+
{ cmd: '/gemini', args: 'status', desc: 'Check AI auth status' },
|
|
42
|
+
{ cmd: '/gemini', args: 'logout', desc: 'Log out from Gemini' },
|
|
43
|
+
{ cmd: '/gemini', args: 'clear', desc: 'Clear conversation history' },
|
|
44
|
+
{ cmd: '/ai', args: '', desc: 'Open Gemini AI chat (alias)' },
|
|
45
|
+
|
|
46
|
+
// General
|
|
25
47
|
{ cmd: '/help', args: '', desc: 'Show all commands' },
|
|
26
48
|
{ cmd: '/quit', args: '', desc: 'Exit application' },
|
|
49
|
+
{ cmd: '/exit', args: '', desc: 'Exit application (alias)' },
|
|
27
50
|
];
|
|
28
51
|
|
|
29
52
|
interface ActiveService {
|
|
@@ -39,16 +62,17 @@ interface CommandInputProps {
|
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
export default function CommandInput({ onSubmit, activeService, onSuggestionsChange }: CommandInputProps) {
|
|
42
|
-
const [value, setValue] = useState('');
|
|
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
|
|
65
|
+
const [value, setValue] = useState('');
|
|
45
66
|
const [history, setHistory] = useState<string[]>([]);
|
|
46
67
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
47
68
|
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
69
|
|
|
51
|
-
//
|
|
70
|
+
// Use ref to track if we should skip the next onChange (after programmatic update)
|
|
71
|
+
const skipNextChange = useRef(false);
|
|
72
|
+
// Track previous suggestion count to avoid unnecessary parent updates
|
|
73
|
+
const prevSuggestionCount = useRef(0);
|
|
74
|
+
|
|
75
|
+
// Filter commands based on typed value
|
|
52
76
|
const suggestions = useMemo(() => {
|
|
53
77
|
if (!value.startsWith('/')) return [];
|
|
54
78
|
|
|
@@ -57,13 +81,13 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
57
81
|
const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd;
|
|
58
82
|
return fullCmd.toLowerCase().includes(search) ||
|
|
59
83
|
c.cmd.toLowerCase().startsWith(search);
|
|
60
|
-
});
|
|
84
|
+
}).slice(0, 8); // Limit to 8 suggestions for cleaner UI
|
|
61
85
|
}, [value]);
|
|
62
86
|
|
|
63
87
|
const showSuggestions = value.startsWith('/') && value.length >= 1 && suggestions.length > 0;
|
|
64
88
|
|
|
65
89
|
// Helper to build full command string from suggestion
|
|
66
|
-
const buildFullCommand = (suggestion: typeof COMMANDS[0]): string => {
|
|
90
|
+
const buildFullCommand = useCallback((suggestion: typeof COMMANDS[0]): string => {
|
|
67
91
|
if (suggestion.args) {
|
|
68
92
|
const hasPlaceholder = suggestion.args.includes('<') || suggestion.args.includes('[');
|
|
69
93
|
if (!hasPlaceholder) {
|
|
@@ -71,55 +95,43 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
97
|
return suggestion.cmd;
|
|
74
|
-
};
|
|
98
|
+
}, []);
|
|
75
99
|
|
|
76
|
-
// Notify parent when suggestions change (
|
|
100
|
+
// Notify parent when suggestions change (debounced to reduce flicker)
|
|
77
101
|
useEffect(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
const count = showSuggestions ? suggestions.length + 2 : 0;
|
|
103
|
+
if (count !== prevSuggestionCount.current) {
|
|
104
|
+
prevSuggestionCount.current = count;
|
|
105
|
+
onSuggestionsChange?.(count);
|
|
81
106
|
}
|
|
82
107
|
}, [showSuggestions, suggestions.length, onSuggestionsChange]);
|
|
83
108
|
|
|
109
|
+
// Reset selected suggestion when suggestions change
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (selectedSuggestion >= suggestions.length) {
|
|
112
|
+
setSelectedSuggestion(Math.max(0, suggestions.length - 1));
|
|
113
|
+
}
|
|
114
|
+
}, [suggestions.length, selectedSuggestion]);
|
|
115
|
+
|
|
84
116
|
useInput((input, key) => {
|
|
85
|
-
// Tab to autocomplete selected suggestion
|
|
117
|
+
// Tab to autocomplete selected suggestion
|
|
86
118
|
if (key.tab && showSuggestions && suggestions[selectedSuggestion]) {
|
|
87
119
|
const suggestion = suggestions[selectedSuggestion];
|
|
88
120
|
const fullCmd = buildFullCommand(suggestion);
|
|
89
|
-
|
|
121
|
+
skipNextChange.current = true;
|
|
90
122
|
setValue(fullCmd);
|
|
91
|
-
setDisplayValue(fullCmd);
|
|
92
123
|
setSelectedSuggestion(0);
|
|
93
|
-
setIsNavigating(false);
|
|
94
|
-
// Increment key to force TextInput remount (resets cursor to end)
|
|
95
|
-
setInputKey(k => k + 1);
|
|
96
124
|
return;
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
// Up/Down to navigate suggestions when showing
|
|
100
128
|
if (showSuggestions) {
|
|
101
129
|
if (key.upArrow) {
|
|
102
|
-
|
|
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
|
-
}
|
|
130
|
+
setSelectedSuggestion(prev => Math.max(0, prev - 1));
|
|
111
131
|
return;
|
|
112
132
|
}
|
|
113
133
|
if (key.downArrow) {
|
|
114
|
-
|
|
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
|
-
}
|
|
134
|
+
setSelectedSuggestion(prev => Math.min(suggestions.length - 1, prev + 1));
|
|
123
135
|
return;
|
|
124
136
|
}
|
|
125
137
|
} else {
|
|
@@ -127,81 +139,75 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
127
139
|
if (key.upArrow && history.length > 0) {
|
|
128
140
|
const newIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
129
141
|
setHistoryIndex(newIndex);
|
|
130
|
-
|
|
131
|
-
setValue(
|
|
132
|
-
setDisplayValue(histVal);
|
|
133
|
-
setInputKey(k => k + 1);
|
|
142
|
+
skipNextChange.current = true;
|
|
143
|
+
setValue(history[history.length - 1 - newIndex] || '');
|
|
134
144
|
return;
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
if (key.downArrow) {
|
|
138
148
|
const newIndex = Math.max(historyIndex - 1, -1);
|
|
139
149
|
setHistoryIndex(newIndex);
|
|
150
|
+
skipNextChange.current = true;
|
|
140
151
|
if (newIndex < 0) {
|
|
141
152
|
setValue('');
|
|
142
|
-
setDisplayValue('');
|
|
143
153
|
} else {
|
|
144
|
-
|
|
145
|
-
setValue(histVal);
|
|
146
|
-
setDisplayValue(histVal);
|
|
154
|
+
setValue(history[history.length - 1 - newIndex] || '');
|
|
147
155
|
}
|
|
148
|
-
setInputKey(k => k + 1);
|
|
149
156
|
return;
|
|
150
157
|
}
|
|
151
158
|
}
|
|
159
|
+
|
|
160
|
+
// Escape to clear input or close suggestions
|
|
161
|
+
if (key.escape) {
|
|
162
|
+
setValue('');
|
|
163
|
+
setSelectedSuggestion(0);
|
|
164
|
+
setHistoryIndex(-1);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
152
167
|
});
|
|
153
168
|
|
|
154
|
-
const handleSubmit = (input: string) => {
|
|
169
|
+
const handleSubmit = useCallback((input: string) => {
|
|
155
170
|
if (!input.trim()) return;
|
|
156
171
|
|
|
157
|
-
// Add to history
|
|
158
|
-
|
|
159
|
-
setHistory(prev => [...prev.filter(h => h !== input.trim()), input.trim()]);
|
|
160
|
-
}
|
|
172
|
+
// Add to history (avoid duplicates)
|
|
173
|
+
setHistory(prev => [...prev.filter(h => h !== input.trim()), input.trim()]);
|
|
161
174
|
|
|
162
175
|
// Reset state
|
|
163
176
|
setValue('');
|
|
164
|
-
setDisplayValue('');
|
|
165
177
|
setHistoryIndex(-1);
|
|
166
178
|
setSelectedSuggestion(0);
|
|
167
|
-
setIsNavigating(false);
|
|
168
179
|
|
|
169
180
|
// Call handler
|
|
170
181
|
onSubmit(input);
|
|
171
|
-
};
|
|
182
|
+
}, [onSubmit]);
|
|
172
183
|
|
|
173
184
|
// Handle text input changes
|
|
174
|
-
const handleChange = (v: string) => {
|
|
175
|
-
|
|
185
|
+
const handleChange = useCallback((v: string) => {
|
|
186
|
+
if (skipNextChange.current) {
|
|
187
|
+
skipNextChange.current = false;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
176
190
|
setValue(v);
|
|
177
|
-
setDisplayValue(v);
|
|
178
191
|
setSelectedSuggestion(0);
|
|
179
|
-
|
|
180
|
-
};
|
|
192
|
+
}, []);
|
|
181
193
|
|
|
182
194
|
// Get context-specific hints for current tab
|
|
183
|
-
const
|
|
184
|
-
const
|
|
195
|
+
const hints = useMemo((): string[] => {
|
|
196
|
+
const h: string[] = [];
|
|
185
197
|
|
|
186
198
|
if (activeService) {
|
|
187
199
|
const isRunning = activeService.status === 'running' || activeService.status === 'starting';
|
|
188
|
-
if (isRunning) {
|
|
189
|
-
|
|
190
|
-
hints.push(`/restart to restart`);
|
|
191
|
-
}
|
|
192
|
-
if (activeService.id === 'vite') {
|
|
193
|
-
hints.push('r to refresh browser');
|
|
200
|
+
if (isRunning && activeService.id !== 'system') {
|
|
201
|
+
h.push(`Ctrl+K stop`);
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return hints;
|
|
202
|
-
};
|
|
205
|
+
h.push('Ctrl+L clear');
|
|
206
|
+
h.push('←/→ tabs');
|
|
207
|
+
h.push('Esc cancel');
|
|
203
208
|
|
|
204
|
-
|
|
209
|
+
return h;
|
|
210
|
+
}, [activeService]);
|
|
205
211
|
|
|
206
212
|
return (
|
|
207
213
|
<Box flexDirection="column">
|
|
@@ -239,8 +245,8 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
239
245
|
</Text>
|
|
240
246
|
</Box>
|
|
241
247
|
))}
|
|
242
|
-
<Box paddingX={1}
|
|
243
|
-
<Text dimColor>Tab
|
|
248
|
+
<Box paddingX={1}>
|
|
249
|
+
<Text dimColor>Tab complete · ↑↓ select · Esc cancel</Text>
|
|
244
250
|
</Box>
|
|
245
251
|
</Box>
|
|
246
252
|
)}
|
|
@@ -254,10 +260,9 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
254
260
|
<Text color="cyan" bold>></Text>
|
|
255
261
|
<Text> </Text>
|
|
256
262
|
<TextInput
|
|
257
|
-
|
|
258
|
-
value={displayValue}
|
|
263
|
+
value={value}
|
|
259
264
|
onChange={handleChange}
|
|
260
|
-
onSubmit={() => handleSubmit(
|
|
265
|
+
onSubmit={() => handleSubmit(value.startsWith('/') ? value : '/' + value)}
|
|
261
266
|
placeholder="Type / to run a command..."
|
|
262
267
|
/>
|
|
263
268
|
</Box>
|
|
@@ -265,7 +270,7 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
265
270
|
{/* Hints bar (below input) */}
|
|
266
271
|
<Box paddingX={1} justifyContent="space-between">
|
|
267
272
|
<Box>
|
|
268
|
-
{hints.
|
|
273
|
+
{hints.map((hint, index) => (
|
|
269
274
|
<React.Fragment key={hint}>
|
|
270
275
|
{index > 0 && <Text color="gray"> · </Text>}
|
|
271
276
|
<Text dimColor>{hint}</Text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react';
|
|
2
2
|
import { Box, Text, useStdout, useInput } from 'ink';
|
|
3
3
|
|
|
4
4
|
interface LogPanelProps {
|
|
@@ -7,15 +7,25 @@ interface LogPanelProps {
|
|
|
7
7
|
maxHeight?: number; // Maximum height in rows (from parent container)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Memoized log line component to prevent unnecessary re-renders
|
|
11
|
+
const LogLine = memo(({ log, index }: { log: string; index: number }) => (
|
|
12
|
+
<Text key={index} wrap="wrap">
|
|
13
|
+
{formatLog(log)}
|
|
14
|
+
</Text>
|
|
15
|
+
));
|
|
16
|
+
LogLine.displayName = 'LogLine';
|
|
17
|
+
|
|
18
|
+
function LogPanel({ logs, isActive = true, maxHeight }: LogPanelProps) {
|
|
11
19
|
const { stdout } = useStdout();
|
|
12
20
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
13
21
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
14
22
|
|
|
15
23
|
// Calculate visible lines based on provided maxHeight or terminal height
|
|
16
24
|
// Subtract 2 for padding, 2 for border
|
|
17
|
-
const
|
|
18
|
-
|
|
25
|
+
const maxLines = useMemo(() => {
|
|
26
|
+
const defaultMaxLines = stdout ? Math.max(5, stdout.rows - 10) : 15;
|
|
27
|
+
return maxHeight ? Math.max(3, maxHeight - 4) : defaultMaxLines;
|
|
28
|
+
}, [stdout?.rows, maxHeight]);
|
|
19
29
|
|
|
20
30
|
// Reset scroll when logs are cleared
|
|
21
31
|
useEffect(() => {
|
|
@@ -23,7 +33,7 @@ export default function LogPanel({ logs, isActive = true, maxHeight }: LogPanelP
|
|
|
23
33
|
setScrollOffset(0);
|
|
24
34
|
setAutoScroll(true);
|
|
25
35
|
}
|
|
26
|
-
}, [logs.length
|
|
36
|
+
}, [logs.length]);
|
|
27
37
|
|
|
28
38
|
// Auto-scroll to bottom when new logs arrive (if autoScroll is enabled)
|
|
29
39
|
useEffect(() => {
|
|
@@ -70,15 +80,18 @@ export default function LogPanel({ logs, isActive = true, maxHeight }: LogPanelP
|
|
|
70
80
|
}
|
|
71
81
|
});
|
|
72
82
|
|
|
73
|
-
// Calculate visible logs with scroll offset
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
// Calculate visible logs with scroll offset - memoized
|
|
84
|
+
const { visibleLogs, startIndex, canScrollUp, canScrollDown } = useMemo(() => {
|
|
85
|
+
const totalLogs = logs.length;
|
|
86
|
+
const start = Math.max(0, totalLogs - maxLines - scrollOffset);
|
|
87
|
+
const end = Math.max(0, totalLogs - scrollOffset);
|
|
88
|
+
return {
|
|
89
|
+
visibleLogs: logs.slice(start, end),
|
|
90
|
+
startIndex: start,
|
|
91
|
+
canScrollUp: start > 0,
|
|
92
|
+
canScrollDown: scrollOffset > 0,
|
|
93
|
+
};
|
|
94
|
+
}, [logs, maxLines, scrollOffset]);
|
|
82
95
|
|
|
83
96
|
return (
|
|
84
97
|
<Box flexDirection="column" padding={1} flexGrow={1} overflow="hidden">
|
|
@@ -89,9 +102,7 @@ export default function LogPanel({ logs, isActive = true, maxHeight }: LogPanelP
|
|
|
89
102
|
<Text color="gray" dimColor>No logs yet...</Text>
|
|
90
103
|
) : (
|
|
91
104
|
visibleLogs.map((log, index) => (
|
|
92
|
-
<
|
|
93
|
-
{formatLog(log)}
|
|
94
|
-
</Text>
|
|
105
|
+
<LogLine key={startIndex + index} log={log} index={startIndex + index} />
|
|
95
106
|
))
|
|
96
107
|
)}
|
|
97
108
|
{canScrollDown && (
|
|
@@ -120,3 +131,5 @@ function formatLog(log: string): React.ReactNode {
|
|
|
120
131
|
}
|
|
121
132
|
return <Text>{log}</Text>;
|
|
122
133
|
}
|
|
134
|
+
|
|
135
|
+
export default memo(LogPanel);
|
|
@@ -20,6 +20,37 @@ function getToolkitRoot(): string {
|
|
|
20
20
|
return path.resolve(__dirname, '..', '..', '..');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Generate extension defaults based on environment variables
|
|
25
|
+
* This creates a defaults.json file that the popup.js reads on load
|
|
26
|
+
*/
|
|
27
|
+
function generateExtensionDefaults(extensionPath: string, useHttps: boolean, port: number | string): void {
|
|
28
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
29
|
+
const baseUrl = `${protocol}://localhost:${port}`;
|
|
30
|
+
|
|
31
|
+
const defaults = {
|
|
32
|
+
// Extension should be enabled by default when launched from CLI
|
|
33
|
+
enabled: true,
|
|
34
|
+
// JS redirect URL based on env
|
|
35
|
+
jsRedirectUrl: `${baseUrl}/src/Plugin.vue`,
|
|
36
|
+
// CSS redirect URL (empty by default, uses blank CSS)
|
|
37
|
+
cssRedirectUrl: '',
|
|
38
|
+
// CSS override should be enabled by default
|
|
39
|
+
cssRuleEnabled: true,
|
|
40
|
+
// Return blank CSS by default
|
|
41
|
+
cssReturnBlank: true,
|
|
42
|
+
// Use custom URL pattern by default
|
|
43
|
+
jsUseCustomPattern: true,
|
|
44
|
+
cssUseCustomPattern: true,
|
|
45
|
+
// Cache settings
|
|
46
|
+
clearCacheOnEnable: true,
|
|
47
|
+
disableCacheForRedirects: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const defaultsPath = path.join(extensionPath, 'defaults.json');
|
|
51
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
// Find the extension path (project-local or toolkit built-in)
|
|
24
55
|
function findExtensionPath(browser: BrowserType, cwd: string): string | null {
|
|
25
56
|
// Check local project first
|
|
@@ -64,6 +95,9 @@ export function startExtension(options: ExtensionOptions): void {
|
|
|
64
95
|
return;
|
|
65
96
|
}
|
|
66
97
|
|
|
98
|
+
// Generate defaults.json for the extension based on environment
|
|
99
|
+
generateExtensionDefaults(extensionPath, useHttps, port);
|
|
100
|
+
|
|
67
101
|
if (browser === 'firefox') {
|
|
68
102
|
const config: ServiceConfig = {
|
|
69
103
|
id: serviceId,
|