@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.
@@ -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: '/gemini', args: '', desc: 'Open Gemini AI chat panel' },
39
- { cmd: '/gemini', args: 'enable', desc: 'Set up Google authentication' },
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)' },
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
- // 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);
73
+ // Track if suggestions are currently shown to avoid flicker
74
+ const prevShowSuggestions = useRef(false);
74
75
 
75
- // Filter commands based on typed value
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 (!value.startsWith('/')) return [];
81
+ if (!filterValue.startsWith('/')) return [];
78
82
 
79
- const search = value.toLowerCase();
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
- }).slice(0, 8); // Limit to 8 suggestions for cleaner UI
85
- }, [value]);
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
- const showSuggestions = value.startsWith('/') && value.length >= 1 && suggestions.length > 0;
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 change (debounced to reduce flicker)
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
- const count = showSuggestions ? suggestions.length + 2 : 0;
103
- if (count !== prevSuggestionCount.current) {
104
- prevSuggestionCount.current = count;
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
- // Wrap to bottom when at top
132
- if (prev <= 0) return suggestions.length - 1;
133
- return prev - 1;
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
- // Wrap to top when at bottom
140
- if (prev >= suggestions.length - 1) return 0;
141
- return prev + 1;
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
- skipNextChange.current = true;
151
- setValue(history[history.length - 1 - newIndex] || '');
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
- setValue(history[history.length - 1 - newIndex] || '');
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
- {suggestions.map((suggestion, index) => (
231
- <Box key={`${suggestion.cmd}-${suggestion.args}-${index}`} paddingX={1}>
232
- <Text
233
- backgroundColor={index === selectedSuggestion ? 'blue' : undefined}
234
- color={index === selectedSuggestion ? 'white' : 'cyan'}
235
- bold={index === selectedSuggestion}
236
- >
237
- {suggestion.cmd}
238
- </Text>
239
- {suggestion.args && (
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={index === selectedSuggestion ? 'white' : 'gray'}
242
- backgroundColor={index === selectedSuggestion ? 'blue' : undefined}
302
+ color={isSelected ? 'white' : 'gray'}
303
+ dimColor={!isSelected}
243
304
  >
244
- {' '}{suggestion.args}
305
+ {suggestion.desc}
245
306
  </Text>
246
- )}
247
- <Text color="gray"> - </Text>
248
- <Text
249
- color={index === selectedSuggestion ? 'white' : 'gray'}
250
- dimColor={index !== selectedSuggestion}
251
- >
252
- {suggestion.desc}
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>