@gxp-dev/tools 2.0.16 → 2.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/tui/App.tsx +74 -49
- package/bin/lib/tui/components/AIPanel.tsx +180 -0
- package/bin/lib/tui/components/CommandInput.tsx +120 -60
- package/bin/lib/tui/services/AIService.ts +509 -0
- package/bin/lib/tui/services/index.ts +11 -0
- package/browser-extensions/chrome/background.js +16 -0
- package/browser-extensions/chrome/popup.html +63 -19
- package/browser-extensions/chrome/popup.js +47 -9
- package/browser-extensions/firefox/popup.html +67 -14
- package/browser-extensions/firefox/popup.js +45 -9
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +70 -46
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/AIPanel.d.ts +7 -0
- package/dist/tui/components/AIPanel.d.ts.map +1 -0
- package/dist/tui/components/AIPanel.js +116 -0
- package/dist/tui/components/AIPanel.js.map +1 -0
- package/dist/tui/components/CommandInput.d.ts.map +1 -1
- package/dist/tui/components/CommandInput.js +84 -42
- package/dist/tui/components/CommandInput.js.map +1 -1
- package/dist/tui/services/AIService.d.ts +48 -0
- package/dist/tui/services/AIService.d.ts.map +1 -0
- package/dist/tui/services/AIService.js +429 -0
- package/dist/tui/services/AIService.js.map +1 -0
- package/dist/tui/services/index.d.ts +1 -0
- package/dist/tui/services/index.d.ts.map +1 -1
- package/dist/tui/services/index.js +1 -0
- package/dist/tui/services/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -35,13 +35,14 @@ const COMMANDS = [
|
|
|
35
35
|
{ cmd: '/extract-config', args: '--overwrite', desc: 'Overwrite existing config values' },
|
|
36
36
|
|
|
37
37
|
// AI commands
|
|
38
|
-
{ cmd: '/
|
|
39
|
-
{ cmd: '/
|
|
40
|
-
{ cmd: '/
|
|
41
|
-
{ cmd: '/
|
|
42
|
-
{ cmd: '/
|
|
43
|
-
{ cmd: '/
|
|
44
|
-
{ cmd: '/ai', args: '', desc: '
|
|
38
|
+
{ cmd: '/ai', args: '', desc: 'Open AI chat with current provider' },
|
|
39
|
+
{ cmd: '/ai', args: 'model', desc: 'Show available AI providers' },
|
|
40
|
+
{ cmd: '/ai', args: 'model claude', desc: 'Switch to Claude AI' },
|
|
41
|
+
{ cmd: '/ai', args: 'model codex', desc: 'Switch to Codex AI' },
|
|
42
|
+
{ cmd: '/ai', args: 'model gemini', desc: 'Switch to Gemini AI' },
|
|
43
|
+
{ cmd: '/ai', args: 'ask <query>', desc: 'Quick AI question' },
|
|
44
|
+
{ cmd: '/ai', args: 'status', desc: 'Check provider availability' },
|
|
45
|
+
{ cmd: '/ai', args: 'clear', desc: 'Clear conversation history' },
|
|
45
46
|
|
|
46
47
|
// General
|
|
47
48
|
{ cmd: '/help', args: '', desc: 'Show all commands' },
|
|
@@ -63,28 +64,53 @@ interface CommandInputProps {
|
|
|
63
64
|
|
|
64
65
|
export default function CommandInput({ onSubmit, activeService, onSuggestionsChange }: CommandInputProps) {
|
|
65
66
|
const [value, setValue] = useState('');
|
|
67
|
+
const [filterValue, setFilterValue] = useState(''); // What we filter suggestions by (doesn't change when arrowing)
|
|
66
68
|
const [history, setHistory] = useState<string[]>([]);
|
|
67
69
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
68
70
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
|
71
|
+
const [isNavigating, setIsNavigating] = useState(false); // True when user is arrowing through suggestions
|
|
69
72
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
72
|
-
// Track previous suggestion count to avoid unnecessary parent updates
|
|
73
|
-
const prevSuggestionCount = useRef(0);
|
|
73
|
+
// Track if suggestions are currently shown to avoid flicker
|
|
74
|
+
const prevShowSuggestions = useRef(false);
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
+
// Maximum visible suggestions in the dropdown
|
|
77
|
+
const MAX_VISIBLE = 8;
|
|
78
|
+
|
|
79
|
+
// Filter commands based on filterValue (stays stable while navigating with arrows)
|
|
76
80
|
const suggestions = useMemo(() => {
|
|
77
|
-
if (!
|
|
81
|
+
if (!filterValue.startsWith('/')) return [];
|
|
78
82
|
|
|
79
|
-
const search =
|
|
83
|
+
const search = filterValue.toLowerCase();
|
|
80
84
|
return COMMANDS.filter(c => {
|
|
81
85
|
const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd;
|
|
82
86
|
return fullCmd.toLowerCase().includes(search) ||
|
|
83
87
|
c.cmd.toLowerCase().startsWith(search);
|
|
84
|
-
})
|
|
85
|
-
}, [
|
|
88
|
+
});
|
|
89
|
+
}, [filterValue]);
|
|
90
|
+
|
|
91
|
+
// Calculate visible window of suggestions (scrolls to keep selection visible)
|
|
92
|
+
const { visibleSuggestions, startIndex } = useMemo(() => {
|
|
93
|
+
if (suggestions.length <= MAX_VISIBLE) {
|
|
94
|
+
return { visibleSuggestions: suggestions, startIndex: 0 };
|
|
95
|
+
}
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
// Calculate window to keep selected item visible
|
|
98
|
+
let start = 0;
|
|
99
|
+
if (selectedSuggestion >= MAX_VISIBLE) {
|
|
100
|
+
// Selected item is beyond initial window, scroll down
|
|
101
|
+
start = selectedSuggestion - MAX_VISIBLE + 1;
|
|
102
|
+
}
|
|
103
|
+
// Ensure we don't go past the end
|
|
104
|
+
start = Math.min(start, suggestions.length - MAX_VISIBLE);
|
|
105
|
+
start = Math.max(0, start);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
visibleSuggestions: suggestions.slice(start, start + MAX_VISIBLE),
|
|
109
|
+
startIndex: start,
|
|
110
|
+
};
|
|
111
|
+
}, [suggestions, selectedSuggestion]);
|
|
112
|
+
|
|
113
|
+
const showSuggestions = filterValue.startsWith('/') && filterValue.length >= 1 && suggestions.length > 0;
|
|
88
114
|
|
|
89
115
|
// Helper to build full command string from suggestion
|
|
90
116
|
const buildFullCommand = useCallback((suggestion: typeof COMMANDS[0]): string => {
|
|
@@ -97,11 +123,13 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
97
123
|
return suggestion.cmd;
|
|
98
124
|
}, []);
|
|
99
125
|
|
|
100
|
-
// Notify parent when suggestions
|
|
126
|
+
// Notify parent when suggestions visibility changes (not on every count change)
|
|
127
|
+
// This reduces flicker by only updating when suggestions appear/disappear
|
|
101
128
|
useEffect(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
if (showSuggestions !== prevShowSuggestions.current) {
|
|
130
|
+
prevShowSuggestions.current = showSuggestions;
|
|
131
|
+
const visibleCount = Math.min(suggestions.length, MAX_VISIBLE);
|
|
132
|
+
const count = showSuggestions ? visibleCount + 4 : 0; // +4 for borders, scroll indicators, hint line
|
|
105
133
|
onSuggestionsChange?.(count);
|
|
106
134
|
}
|
|
107
135
|
}, [showSuggestions, suggestions.length, onSuggestionsChange]);
|
|
@@ -114,31 +142,42 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
114
142
|
}, [suggestions.length, selectedSuggestion]);
|
|
115
143
|
|
|
116
144
|
useInput((input, key) => {
|
|
117
|
-
// Tab to autocomplete selected suggestion
|
|
145
|
+
// Tab to autocomplete selected suggestion and commit it
|
|
118
146
|
if (key.tab && showSuggestions && suggestions[selectedSuggestion]) {
|
|
119
147
|
const suggestion = suggestions[selectedSuggestion];
|
|
120
148
|
const fullCmd = buildFullCommand(suggestion);
|
|
121
|
-
skipNextChange.current = true;
|
|
122
149
|
setValue(fullCmd);
|
|
150
|
+
setFilterValue(fullCmd); // Commit the selection to filter
|
|
123
151
|
setSelectedSuggestion(0);
|
|
152
|
+
setIsNavigating(false);
|
|
124
153
|
return;
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
// Up/Down to navigate suggestions when showing (circular navigation)
|
|
128
157
|
if (showSuggestions) {
|
|
129
158
|
if (key.upArrow) {
|
|
159
|
+
setIsNavigating(true);
|
|
130
160
|
setSelectedSuggestion(prev => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
const newIndex = prev <= 0 ? suggestions.length - 1 : prev - 1;
|
|
162
|
+
// Update display value to show selected command
|
|
163
|
+
const suggestion = suggestions[newIndex];
|
|
164
|
+
if (suggestion) {
|
|
165
|
+
setValue(buildFullCommand(suggestion));
|
|
166
|
+
}
|
|
167
|
+
return newIndex;
|
|
134
168
|
});
|
|
135
169
|
return;
|
|
136
170
|
}
|
|
137
171
|
if (key.downArrow) {
|
|
172
|
+
setIsNavigating(true);
|
|
138
173
|
setSelectedSuggestion(prev => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
const newIndex = prev >= suggestions.length - 1 ? 0 : prev + 1;
|
|
175
|
+
// Update display value to show selected command
|
|
176
|
+
const suggestion = suggestions[newIndex];
|
|
177
|
+
if (suggestion) {
|
|
178
|
+
setValue(buildFullCommand(suggestion));
|
|
179
|
+
}
|
|
180
|
+
return newIndex;
|
|
142
181
|
});
|
|
143
182
|
return;
|
|
144
183
|
}
|
|
@@ -147,19 +186,22 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
147
186
|
if (key.upArrow && history.length > 0) {
|
|
148
187
|
const newIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
149
188
|
setHistoryIndex(newIndex);
|
|
150
|
-
|
|
151
|
-
setValue(
|
|
189
|
+
const historyValue = history[history.length - 1 - newIndex] || '';
|
|
190
|
+
setValue(historyValue);
|
|
191
|
+
setFilterValue(historyValue);
|
|
152
192
|
return;
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
if (key.downArrow) {
|
|
156
196
|
const newIndex = Math.max(historyIndex - 1, -1);
|
|
157
197
|
setHistoryIndex(newIndex);
|
|
158
|
-
skipNextChange.current = true;
|
|
159
198
|
if (newIndex < 0) {
|
|
160
199
|
setValue('');
|
|
200
|
+
setFilterValue('');
|
|
161
201
|
} else {
|
|
162
|
-
|
|
202
|
+
const historyValue = history[history.length - 1 - newIndex] || '';
|
|
203
|
+
setValue(historyValue);
|
|
204
|
+
setFilterValue(historyValue);
|
|
163
205
|
}
|
|
164
206
|
return;
|
|
165
207
|
}
|
|
@@ -168,8 +210,10 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
168
210
|
// Escape to clear input or close suggestions
|
|
169
211
|
if (key.escape) {
|
|
170
212
|
setValue('');
|
|
213
|
+
setFilterValue('');
|
|
171
214
|
setSelectedSuggestion(0);
|
|
172
215
|
setHistoryIndex(-1);
|
|
216
|
+
setIsNavigating(false);
|
|
173
217
|
return;
|
|
174
218
|
}
|
|
175
219
|
});
|
|
@@ -182,21 +226,21 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
182
226
|
|
|
183
227
|
// Reset state
|
|
184
228
|
setValue('');
|
|
229
|
+
setFilterValue('');
|
|
185
230
|
setHistoryIndex(-1);
|
|
186
231
|
setSelectedSuggestion(0);
|
|
232
|
+
setIsNavigating(false);
|
|
187
233
|
|
|
188
234
|
// Call handler
|
|
189
235
|
onSubmit(input);
|
|
190
236
|
}, [onSubmit]);
|
|
191
237
|
|
|
192
|
-
// Handle text input changes
|
|
238
|
+
// Handle text input changes - updates both value and filter
|
|
193
239
|
const handleChange = useCallback((v: string) => {
|
|
194
|
-
if (skipNextChange.current) {
|
|
195
|
-
skipNextChange.current = false;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
240
|
setValue(v);
|
|
241
|
+
setFilterValue(v); // When user types, update filter to match
|
|
199
242
|
setSelectedSuggestion(0);
|
|
243
|
+
setIsNavigating(false);
|
|
200
244
|
}, []);
|
|
201
245
|
|
|
202
246
|
// Get context-specific hints for current tab
|
|
@@ -227,32 +271,48 @@ export default function CommandInput({ onSubmit, activeService, onSuggestionsCha
|
|
|
227
271
|
borderColor="gray"
|
|
228
272
|
marginBottom={0}
|
|
229
273
|
>
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
274
|
+
{/* Scroll up indicator */}
|
|
275
|
+
{startIndex > 0 && (
|
|
276
|
+
<Box paddingX={1}>
|
|
277
|
+
<Text color="gray">↑ {startIndex} more above</Text>
|
|
278
|
+
</Box>
|
|
279
|
+
)}
|
|
280
|
+
{visibleSuggestions.map((suggestion, visibleIndex) => {
|
|
281
|
+
const actualIndex = startIndex + visibleIndex;
|
|
282
|
+
const isSelected = actualIndex === selectedSuggestion;
|
|
283
|
+
return (
|
|
284
|
+
<Box key={`${suggestion.cmd}-${suggestion.args}-${actualIndex}`} paddingX={1}>
|
|
285
|
+
<Text
|
|
286
|
+
backgroundColor={isSelected ? 'blue' : undefined}
|
|
287
|
+
color={isSelected ? 'white' : 'cyan'}
|
|
288
|
+
bold={isSelected}
|
|
289
|
+
>
|
|
290
|
+
{suggestion.cmd}
|
|
291
|
+
</Text>
|
|
292
|
+
{suggestion.args && (
|
|
293
|
+
<Text
|
|
294
|
+
color={isSelected ? 'white' : 'gray'}
|
|
295
|
+
backgroundColor={isSelected ? 'blue' : undefined}
|
|
296
|
+
>
|
|
297
|
+
{' '}{suggestion.args}
|
|
298
|
+
</Text>
|
|
299
|
+
)}
|
|
300
|
+
<Text color="gray"> - </Text>
|
|
240
301
|
<Text
|
|
241
|
-
color={
|
|
242
|
-
|
|
302
|
+
color={isSelected ? 'white' : 'gray'}
|
|
303
|
+
dimColor={!isSelected}
|
|
243
304
|
>
|
|
244
|
-
{
|
|
305
|
+
{suggestion.desc}
|
|
245
306
|
</Text>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
</Text>
|
|
307
|
+
</Box>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
{/* Scroll down indicator */}
|
|
311
|
+
{startIndex + MAX_VISIBLE < suggestions.length && (
|
|
312
|
+
<Box paddingX={1}>
|
|
313
|
+
<Text color="gray">↓ {suggestions.length - startIndex - MAX_VISIBLE} more below</Text>
|
|
254
314
|
</Box>
|
|
255
|
-
)
|
|
315
|
+
)}
|
|
256
316
|
<Box paddingX={1}>
|
|
257
317
|
<Text dimColor>Tab complete · ↑↓ select · Esc cancel</Text>
|
|
258
318
|
</Box>
|