@gxp-dev/tools 2.0.63 → 2.0.65

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 (182) hide show
  1. package/README.md +32 -31
  2. package/bin/gx-devtools.js +74 -54
  3. package/bin/lib/cli.js +23 -21
  4. package/bin/lib/commands/add-dependency.js +366 -325
  5. package/bin/lib/commands/assets.js +137 -139
  6. package/bin/lib/commands/build.js +169 -174
  7. package/bin/lib/commands/datastore.js +181 -183
  8. package/bin/lib/commands/dev.js +127 -131
  9. package/bin/lib/commands/extensions.js +147 -149
  10. package/bin/lib/commands/extract-config.js +73 -67
  11. package/bin/lib/commands/index.js +12 -12
  12. package/bin/lib/commands/init.js +342 -240
  13. package/bin/lib/commands/publish.js +69 -75
  14. package/bin/lib/commands/socket.js +69 -69
  15. package/bin/lib/commands/ssl.js +14 -14
  16. package/bin/lib/constants.js +10 -24
  17. package/bin/lib/tui/App.tsx +761 -705
  18. package/bin/lib/tui/components/AIPanel.tsx +191 -171
  19. package/bin/lib/tui/components/CommandInput.tsx +394 -343
  20. package/bin/lib/tui/components/GeminiPanel.tsx +175 -151
  21. package/bin/lib/tui/components/Header.tsx +23 -21
  22. package/bin/lib/tui/components/LogPanel.tsx +244 -220
  23. package/bin/lib/tui/components/TabBar.tsx +50 -48
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +126 -71
  25. package/bin/lib/tui/index.tsx +37 -39
  26. package/bin/lib/tui/services/AIService.ts +518 -462
  27. package/bin/lib/tui/services/ExtensionService.ts +140 -129
  28. package/bin/lib/tui/services/GeminiService.ts +367 -337
  29. package/bin/lib/tui/services/ServiceManager.ts +344 -322
  30. package/bin/lib/tui/services/SocketService.ts +168 -168
  31. package/bin/lib/tui/services/ViteService.ts +88 -88
  32. package/bin/lib/tui/services/index.ts +47 -22
  33. package/bin/lib/utils/ai-scaffold.js +291 -280
  34. package/bin/lib/utils/extract-config.js +157 -140
  35. package/bin/lib/utils/files.js +82 -86
  36. package/bin/lib/utils/index.js +7 -7
  37. package/bin/lib/utils/paths.js +34 -34
  38. package/bin/lib/utils/prompts.js +194 -169
  39. package/bin/lib/utils/ssl.js +79 -81
  40. package/browser-extensions/README.md +0 -1
  41. package/browser-extensions/chrome/background.js +244 -237
  42. package/browser-extensions/chrome/content.js +32 -29
  43. package/browser-extensions/chrome/devtools.html +7 -7
  44. package/browser-extensions/chrome/devtools.js +19 -19
  45. package/browser-extensions/chrome/inspector.js +802 -767
  46. package/browser-extensions/chrome/manifest.json +71 -63
  47. package/browser-extensions/chrome/panel.html +674 -636
  48. package/browser-extensions/chrome/panel.js +722 -712
  49. package/browser-extensions/chrome/popup.html +586 -543
  50. package/browser-extensions/chrome/popup.js +282 -244
  51. package/browser-extensions/chrome/rules.json +1 -1
  52. package/browser-extensions/chrome/test-chrome.html +216 -136
  53. package/browser-extensions/chrome/test-mixed-content.html +284 -189
  54. package/browser-extensions/chrome/test-uri-pattern.html +221 -198
  55. package/browser-extensions/firefox/README.md +9 -6
  56. package/browser-extensions/firefox/background.js +221 -218
  57. package/browser-extensions/firefox/content.js +55 -52
  58. package/browser-extensions/firefox/debug-errors.html +386 -228
  59. package/browser-extensions/firefox/debug-https.html +153 -105
  60. package/browser-extensions/firefox/devtools.html +7 -7
  61. package/browser-extensions/firefox/devtools.js +23 -20
  62. package/browser-extensions/firefox/inspector.js +802 -767
  63. package/browser-extensions/firefox/manifest.json +68 -68
  64. package/browser-extensions/firefox/panel.html +674 -636
  65. package/browser-extensions/firefox/panel.js +722 -712
  66. package/browser-extensions/firefox/popup.html +572 -535
  67. package/browser-extensions/firefox/popup.js +281 -236
  68. package/browser-extensions/firefox/test-gramercy.html +170 -125
  69. package/browser-extensions/firefox/test-imports.html +59 -55
  70. package/browser-extensions/firefox/test-masking.html +231 -140
  71. package/browser-extensions/firefox/test-uri-pattern.html +221 -198
  72. package/dist/tui/App.d.ts +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +154 -150
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/components/AIPanel.d.ts.map +1 -1
  77. package/dist/tui/components/AIPanel.js +42 -35
  78. package/dist/tui/components/AIPanel.js.map +1 -1
  79. package/dist/tui/components/CommandInput.d.ts +1 -1
  80. package/dist/tui/components/CommandInput.d.ts.map +1 -1
  81. package/dist/tui/components/CommandInput.js +92 -62
  82. package/dist/tui/components/CommandInput.js.map +1 -1
  83. package/dist/tui/components/GeminiPanel.d.ts.map +1 -1
  84. package/dist/tui/components/GeminiPanel.js +37 -30
  85. package/dist/tui/components/GeminiPanel.js.map +1 -1
  86. package/dist/tui/components/Header.d.ts.map +1 -1
  87. package/dist/tui/components/Header.js +1 -1
  88. package/dist/tui/components/Header.js.map +1 -1
  89. package/dist/tui/components/LogPanel.d.ts +1 -1
  90. package/dist/tui/components/LogPanel.d.ts.map +1 -1
  91. package/dist/tui/components/LogPanel.js +26 -24
  92. package/dist/tui/components/LogPanel.js.map +1 -1
  93. package/dist/tui/components/TabBar.d.ts +2 -2
  94. package/dist/tui/components/TabBar.d.ts.map +1 -1
  95. package/dist/tui/components/TabBar.js +11 -11
  96. package/dist/tui/components/TabBar.js.map +1 -1
  97. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -1
  98. package/dist/tui/components/WelcomeScreen.js +6 -6
  99. package/dist/tui/components/WelcomeScreen.js.map +1 -1
  100. package/dist/tui/index.d.ts.map +1 -1
  101. package/dist/tui/index.js +8 -8
  102. package/dist/tui/index.js.map +1 -1
  103. package/dist/tui/services/AIService.d.ts +2 -2
  104. package/dist/tui/services/AIService.d.ts.map +1 -1
  105. package/dist/tui/services/AIService.js +165 -125
  106. package/dist/tui/services/AIService.js.map +1 -1
  107. package/dist/tui/services/ExtensionService.d.ts +1 -1
  108. package/dist/tui/services/ExtensionService.d.ts.map +1 -1
  109. package/dist/tui/services/ExtensionService.js +33 -26
  110. package/dist/tui/services/ExtensionService.js.map +1 -1
  111. package/dist/tui/services/GeminiService.d.ts +1 -1
  112. package/dist/tui/services/GeminiService.d.ts.map +1 -1
  113. package/dist/tui/services/GeminiService.js +87 -76
  114. package/dist/tui/services/GeminiService.js.map +1 -1
  115. package/dist/tui/services/ServiceManager.d.ts +3 -3
  116. package/dist/tui/services/ServiceManager.d.ts.map +1 -1
  117. package/dist/tui/services/ServiceManager.js +72 -58
  118. package/dist/tui/services/ServiceManager.js.map +1 -1
  119. package/dist/tui/services/SocketService.d.ts.map +1 -1
  120. package/dist/tui/services/SocketService.js +32 -32
  121. package/dist/tui/services/SocketService.js.map +1 -1
  122. package/dist/tui/services/ViteService.d.ts.map +1 -1
  123. package/dist/tui/services/ViteService.js +26 -28
  124. package/dist/tui/services/ViteService.js.map +1 -1
  125. package/dist/tui/services/index.d.ts +6 -6
  126. package/dist/tui/services/index.d.ts.map +1 -1
  127. package/dist/tui/services/index.js +6 -6
  128. package/dist/tui/services/index.js.map +1 -1
  129. package/mcp/gxp-api-server.js +83 -81
  130. package/package.json +109 -93
  131. package/runtime/PortalContainer.vue +258 -234
  132. package/runtime/dev-tools/DevToolsModal.vue +153 -155
  133. package/runtime/dev-tools/LayoutSwitcher.vue +144 -140
  134. package/runtime/dev-tools/MockDataEditor.vue +456 -433
  135. package/runtime/dev-tools/SocketSimulator.vue +379 -371
  136. package/runtime/dev-tools/StoreInspector.vue +517 -455
  137. package/runtime/dev-tools/index.js +5 -5
  138. package/runtime/fallback-layouts/PrivateLayout.vue +2 -2
  139. package/runtime/fallback-layouts/PublicLayout.vue +2 -2
  140. package/runtime/fallback-layouts/SystemLayout.vue +2 -2
  141. package/runtime/gxpStringsPlugin.js +159 -134
  142. package/runtime/index.html +17 -19
  143. package/runtime/main.js +24 -22
  144. package/runtime/mock-api/auth-middleware.js +15 -15
  145. package/runtime/mock-api/image-generator.js +46 -46
  146. package/runtime/mock-api/index.js +55 -55
  147. package/runtime/mock-api/response-generator.js +116 -105
  148. package/runtime/mock-api/route-generator.js +107 -84
  149. package/runtime/mock-api/socket-triggers.js +94 -93
  150. package/runtime/mock-api/spec-loader.js +79 -80
  151. package/runtime/package.json +3 -0
  152. package/runtime/server.js +68 -68
  153. package/runtime/stores/gxpPortalConfigStore.js +204 -186
  154. package/runtime/stores/index.js +2 -2
  155. package/runtime/vite-inspector-plugin.js +858 -707
  156. package/runtime/vite-source-tracker-plugin.js +132 -113
  157. package/runtime/vite.config.js +191 -139
  158. package/scripts/launch-chrome.js +41 -41
  159. package/scripts/pack-chrome.js +38 -39
  160. package/socket-events/AiSessionMessageCreated.json +17 -17
  161. package/socket-events/SocialStreamPostCreated.json +23 -23
  162. package/socket-events/SocialStreamPostVariantCompleted.json +22 -22
  163. package/template/.claude/agents/gxp-developer.md +100 -99
  164. package/template/.claude/settings.json +7 -7
  165. package/template/AGENTS.md +30 -23
  166. package/template/GEMINI.md +20 -20
  167. package/template/README.md +70 -53
  168. package/template/app-manifest.json +2 -4
  169. package/template/configuration.json +10 -10
  170. package/template/default-styling.css +1 -1
  171. package/template/index.html +18 -20
  172. package/template/main.js +24 -22
  173. package/template/src/DemoPage.vue +415 -362
  174. package/template/src/Plugin.vue +76 -85
  175. package/template/src/stores/index.js +3 -3
  176. package/template/src/stores/test-data.json +164 -172
  177. package/template/theme-layouts/AdditionalStyling.css +50 -50
  178. package/template/theme-layouts/PrivateLayout.vue +8 -12
  179. package/template/theme-layouts/PublicLayout.vue +8 -12
  180. package/template/theme-layouts/SystemLayout.vue +8 -12
  181. package/template/vite.extend.js +45 -0
  182. package/template/vite.config.js +0 -409
@@ -1,354 +1,405 @@
1
- import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import TextInput from 'ink-text-input';
1
+ import React, { useState, useMemo, useEffect, useCallback, useRef } from "react"
2
+ import { Box, Text, useInput } from "ink"
3
+ import TextInput from "ink-text-input"
4
4
 
5
5
  // Command definitions with descriptions - comprehensive list
6
6
  const COMMANDS = [
7
- // Dev server commands
8
- { cmd: '/dev', args: '', desc: 'Start Vite + Socket.IO servers' },
9
- { cmd: '/dev', args: '--with-mock', desc: 'Start with Mock API enabled' },
10
- { cmd: '/dev', args: '--no-https', desc: 'Start without SSL' },
11
- { cmd: '/dev', args: '--no-socket', desc: 'Start without Socket.IO' },
12
- { cmd: '/dev', args: '--chrome', desc: 'Start + launch Chrome extension' },
13
- { cmd: '/dev', args: '--firefox', desc: 'Start + launch Firefox extension' },
14
-
15
- // Socket commands
16
- { cmd: '/socket', args: '', desc: 'Start Socket.IO server' },
17
- { cmd: '/socket', args: '--with-mock', desc: 'Start Socket.IO + Mock API' },
18
- { cmd: '/socket', args: 'send <event>', desc: 'Send a socket event' },
19
- { cmd: '/socket', args: 'list', desc: 'List available socket events' },
20
- { cmd: '/mock', args: '', desc: 'Start Socket.IO + Mock API (shorthand)' },
21
-
22
- // Browser extension commands
23
- { cmd: '/ext', args: 'chrome', desc: 'Launch Chrome with extension' },
24
- { cmd: '/ext', args: 'firefox', desc: 'Launch Firefox with extension' },
25
-
26
- // Service management
27
- { cmd: '/stop', args: '[service]', desc: 'Stop current or specified service' },
28
- { cmd: '/restart', args: '[service]', desc: 'Restart current or specified service' },
29
- { cmd: '/clear', args: '', desc: 'Clear current log panel' },
30
-
31
- // Config extraction
32
- { cmd: '/extract-config', args: '', desc: 'Extract GxP config from source' },
33
- { cmd: '/extract-config', args: '--dry-run', desc: 'Preview config extraction' },
34
- { cmd: '/extract-config', args: '--overwrite', desc: 'Overwrite existing config values' },
35
-
36
- // Dependency management
37
- { cmd: '/add-dependency', args: '', desc: 'Add API dependency wizard (develop)' },
38
- { cmd: '/add-dependency', args: '--env local', desc: 'Add dependency from local API' },
39
-
40
- // AI commands
41
- { cmd: '/ai', args: '', desc: 'Open AI chat with current provider' },
42
- { cmd: '/ai', args: 'model', desc: 'Show available AI providers' },
43
- { cmd: '/ai', args: 'model claude', desc: 'Switch to Claude AI' },
44
- { cmd: '/ai', args: 'model codex', desc: 'Switch to Codex AI' },
45
- { cmd: '/ai', args: 'model gemini', desc: 'Switch to Gemini AI' },
46
- { cmd: '/ai', args: 'ask <query>', desc: 'Quick AI question' },
47
- { cmd: '/ai', args: 'status', desc: 'Check provider availability' },
48
- { cmd: '/ai', args: 'clear', desc: 'Clear conversation history' },
49
-
50
- // General
51
- { cmd: '/help', args: '', desc: 'Show all commands' },
52
- { cmd: '/quit', args: '', desc: 'Exit application' },
53
- { cmd: '/exit', args: '', desc: 'Exit application (alias)' },
54
- ];
7
+ // Dev server commands
8
+ { cmd: "/dev", args: "", desc: "Start Vite + Socket.IO servers" },
9
+ { cmd: "/dev", args: "--with-mock", desc: "Start with Mock API enabled" },
10
+ { cmd: "/dev", args: "--no-https", desc: "Start without SSL" },
11
+ { cmd: "/dev", args: "--no-socket", desc: "Start without Socket.IO" },
12
+ { cmd: "/dev", args: "--chrome", desc: "Start + launch Chrome extension" },
13
+ { cmd: "/dev", args: "--firefox", desc: "Start + launch Firefox extension" },
14
+
15
+ // Socket commands
16
+ { cmd: "/socket", args: "", desc: "Start Socket.IO server" },
17
+ { cmd: "/socket", args: "--with-mock", desc: "Start Socket.IO + Mock API" },
18
+ { cmd: "/socket", args: "send <event>", desc: "Send a socket event" },
19
+ { cmd: "/socket", args: "list", desc: "List available socket events" },
20
+ { cmd: "/mock", args: "", desc: "Start Socket.IO + Mock API (shorthand)" },
21
+
22
+ // Browser extension commands
23
+ { cmd: "/ext", args: "chrome", desc: "Launch Chrome with extension" },
24
+ { cmd: "/ext", args: "firefox", desc: "Launch Firefox with extension" },
25
+
26
+ // Service management
27
+ {
28
+ cmd: "/stop",
29
+ args: "[service]",
30
+ desc: "Stop current or specified service",
31
+ },
32
+ {
33
+ cmd: "/restart",
34
+ args: "[service]",
35
+ desc: "Restart current or specified service",
36
+ },
37
+ { cmd: "/clear", args: "", desc: "Clear current log panel" },
38
+
39
+ // Config extraction
40
+ { cmd: "/extract-config", args: "", desc: "Extract GxP config from source" },
41
+ {
42
+ cmd: "/extract-config",
43
+ args: "--dry-run",
44
+ desc: "Preview config extraction",
45
+ },
46
+ {
47
+ cmd: "/extract-config",
48
+ args: "--overwrite",
49
+ desc: "Overwrite existing config values",
50
+ },
51
+
52
+ // Dependency management
53
+ {
54
+ cmd: "/add-dependency",
55
+ args: "",
56
+ desc: "Add API dependency wizard (develop)",
57
+ },
58
+ {
59
+ cmd: "/add-dependency",
60
+ args: "--env local",
61
+ desc: "Add dependency from local API",
62
+ },
63
+
64
+ // AI commands
65
+ { cmd: "/ai", args: "", desc: "Open AI chat with current provider" },
66
+ { cmd: "/ai", args: "model", desc: "Show available AI providers" },
67
+ { cmd: "/ai", args: "model claude", desc: "Switch to Claude AI" },
68
+ { cmd: "/ai", args: "model codex", desc: "Switch to Codex AI" },
69
+ { cmd: "/ai", args: "model gemini", desc: "Switch to Gemini AI" },
70
+ { cmd: "/ai", args: "ask <query>", desc: "Quick AI question" },
71
+ { cmd: "/ai", args: "status", desc: "Check provider availability" },
72
+ { cmd: "/ai", args: "clear", desc: "Clear conversation history" },
73
+
74
+ // General
75
+ { cmd: "/help", args: "", desc: "Show all commands" },
76
+ { cmd: "/quit", args: "", desc: "Exit application" },
77
+ { cmd: "/exit", args: "", desc: "Exit application (alias)" },
78
+ ]
55
79
 
56
80
  interface ActiveService {
57
- id: string;
58
- name: string;
59
- status: string;
81
+ id: string
82
+ name: string
83
+ status: string
60
84
  }
61
85
 
62
86
  interface CommandInputProps {
63
- onSubmit: (command: string) => void;
64
- activeService?: ActiveService | null;
65
- onSuggestionsChange?: (count: number) => void;
87
+ onSubmit: (command: string) => void
88
+ activeService?: ActiveService | null
89
+ onSuggestionsChange?: (count: number) => void
66
90
  }
67
91
 
68
- export default function CommandInput({ onSubmit, activeService, onSuggestionsChange }: CommandInputProps) {
69
- const [value, setValue] = useState('');
70
- const [filterValue, setFilterValue] = useState(''); // What we filter suggestions by (doesn't change when arrowing)
71
- const [history, setHistory] = useState<string[]>([]);
72
- const [historyIndex, setHistoryIndex] = useState(-1);
73
- const [selectedSuggestion, setSelectedSuggestion] = useState(0);
74
- const [isNavigating, setIsNavigating] = useState(false); // True when user is arrowing through suggestions
75
-
76
- // Track if suggestions are currently shown to avoid flicker
77
- const prevShowSuggestions = useRef(false);
78
-
79
- // Maximum visible suggestions in the dropdown
80
- const MAX_VISIBLE = 8;
81
-
82
- // Filter commands based on filterValue (stays stable while navigating with arrows)
83
- const suggestions = useMemo(() => {
84
- if (!filterValue.startsWith('/')) return [];
85
-
86
- const search = filterValue.toLowerCase();
87
- return COMMANDS.filter(c => {
88
- const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd;
89
- return fullCmd.toLowerCase().includes(search) ||
90
- c.cmd.toLowerCase().startsWith(search);
91
- });
92
- }, [filterValue]);
93
-
94
- // Calculate visible window of suggestions (scrolls to keep selection visible)
95
- const { visibleSuggestions, startIndex } = useMemo(() => {
96
- if (suggestions.length <= MAX_VISIBLE) {
97
- return { visibleSuggestions: suggestions, startIndex: 0 };
98
- }
99
-
100
- // Calculate window to keep selected item visible
101
- let start = 0;
102
- if (selectedSuggestion >= MAX_VISIBLE) {
103
- // Selected item is beyond initial window, scroll down
104
- start = selectedSuggestion - MAX_VISIBLE + 1;
105
- }
106
- // Ensure we don't go past the end
107
- start = Math.min(start, suggestions.length - MAX_VISIBLE);
108
- start = Math.max(0, start);
109
-
110
- return {
111
- visibleSuggestions: suggestions.slice(start, start + MAX_VISIBLE),
112
- startIndex: start,
113
- };
114
- }, [suggestions, selectedSuggestion]);
115
-
116
- const showSuggestions = filterValue.startsWith('/') && filterValue.length >= 1 && suggestions.length > 0;
117
-
118
- // Helper to build full command string from suggestion
119
- const buildFullCommand = useCallback((suggestion: typeof COMMANDS[0]): string => {
120
- if (suggestion.args) {
121
- const hasPlaceholder = suggestion.args.includes('<') || suggestion.args.includes('[');
122
- if (!hasPlaceholder) {
123
- return `${suggestion.cmd} ${suggestion.args}`;
124
- }
125
- }
126
- return suggestion.cmd;
127
- }, []);
128
-
129
- // Notify parent when suggestions visibility changes (not on every count change)
130
- // This reduces flicker by only updating when suggestions appear/disappear
131
- useEffect(() => {
132
- if (showSuggestions !== prevShowSuggestions.current) {
133
- prevShowSuggestions.current = showSuggestions;
134
- const visibleCount = Math.min(suggestions.length, MAX_VISIBLE);
135
- const count = showSuggestions ? visibleCount + 4 : 0; // +4 for borders, scroll indicators, hint line
136
- onSuggestionsChange?.(count);
137
- }
138
- }, [showSuggestions, suggestions.length, onSuggestionsChange]);
139
-
140
- // Reset selected suggestion when suggestions change
141
- useEffect(() => {
142
- if (selectedSuggestion >= suggestions.length) {
143
- setSelectedSuggestion(Math.max(0, suggestions.length - 1));
144
- }
145
- }, [suggestions.length, selectedSuggestion]);
146
-
147
- useInput((input, key) => {
148
- // Tab to autocomplete selected suggestion and commit it
149
- if (key.tab && showSuggestions && suggestions[selectedSuggestion]) {
150
- const suggestion = suggestions[selectedSuggestion];
151
- const fullCmd = buildFullCommand(suggestion);
152
- setValue(fullCmd);
153
- setFilterValue(fullCmd); // Commit the selection to filter
154
- setSelectedSuggestion(0);
155
- setIsNavigating(false);
156
- return;
157
- }
158
-
159
- // Up/Down to navigate suggestions when showing (circular navigation)
160
- if (showSuggestions) {
161
- if (key.upArrow) {
162
- setIsNavigating(true);
163
- setSelectedSuggestion(prev => {
164
- const newIndex = prev <= 0 ? suggestions.length - 1 : prev - 1;
165
- // Update display value to show selected command
166
- const suggestion = suggestions[newIndex];
167
- if (suggestion) {
168
- setValue(buildFullCommand(suggestion));
169
- }
170
- return newIndex;
171
- });
172
- return;
173
- }
174
- if (key.downArrow) {
175
- setIsNavigating(true);
176
- setSelectedSuggestion(prev => {
177
- const newIndex = prev >= suggestions.length - 1 ? 0 : prev + 1;
178
- // Update display value to show selected command
179
- const suggestion = suggestions[newIndex];
180
- if (suggestion) {
181
- setValue(buildFullCommand(suggestion));
182
- }
183
- return newIndex;
184
- });
185
- return;
186
- }
187
- } else {
188
- // History navigation when not showing suggestions
189
- if (key.upArrow && history.length > 0) {
190
- const newIndex = Math.min(historyIndex + 1, history.length - 1);
191
- setHistoryIndex(newIndex);
192
- const historyValue = history[history.length - 1 - newIndex] || '';
193
- setValue(historyValue);
194
- setFilterValue(historyValue);
195
- return;
196
- }
197
-
198
- if (key.downArrow) {
199
- const newIndex = Math.max(historyIndex - 1, -1);
200
- setHistoryIndex(newIndex);
201
- if (newIndex < 0) {
202
- setValue('');
203
- setFilterValue('');
204
- } else {
205
- const historyValue = history[history.length - 1 - newIndex] || '';
206
- setValue(historyValue);
207
- setFilterValue(historyValue);
208
- }
209
- return;
210
- }
211
- }
212
-
213
- // Escape to clear input or close suggestions
214
- if (key.escape) {
215
- setValue('');
216
- setFilterValue('');
217
- setSelectedSuggestion(0);
218
- setHistoryIndex(-1);
219
- setIsNavigating(false);
220
- return;
221
- }
222
- });
223
-
224
- const handleSubmit = useCallback((input: string) => {
225
- if (!input.trim()) return;
226
-
227
- // Add to history (avoid duplicates)
228
- setHistory(prev => [...prev.filter(h => h !== input.trim()), input.trim()]);
229
-
230
- // Reset state
231
- setValue('');
232
- setFilterValue('');
233
- setHistoryIndex(-1);
234
- setSelectedSuggestion(0);
235
- setIsNavigating(false);
236
-
237
- // Call handler
238
- onSubmit(input);
239
- }, [onSubmit]);
240
-
241
- // Handle text input changes - updates both value and filter
242
- const handleChange = useCallback((v: string) => {
243
- setValue(v);
244
- setFilterValue(v); // When user types, update filter to match
245
- setSelectedSuggestion(0);
246
- setIsNavigating(false);
247
- }, []);
248
-
249
- // Get context-specific hints for current tab
250
- const hints = useMemo((): string[] => {
251
- const h: string[] = [];
252
-
253
- if (activeService) {
254
- const isRunning = activeService.status === 'running' || activeService.status === 'starting';
255
- if (isRunning && activeService.id !== 'system') {
256
- h.push(`Ctrl+K stop`);
257
- }
258
- }
259
-
260
- h.push('Ctrl+L clear');
261
- h.push('←/→ tabs');
262
- h.push('Esc cancel');
263
-
264
- return h;
265
- }, [activeService]);
266
-
267
- return (
268
- <Box flexDirection="column">
269
- {/* Suggestions dropdown (above input) */}
270
- {showSuggestions && (
271
- <Box
272
- flexDirection="column"
273
- borderStyle="round"
274
- borderColor="gray"
275
- marginBottom={0}
276
- >
277
- {/* Scroll up indicator */}
278
- {startIndex > 0 && (
279
- <Box paddingX={1}>
280
- <Text color="gray">↑ {startIndex} more above</Text>
281
- </Box>
282
- )}
283
- {visibleSuggestions.map((suggestion, visibleIndex) => {
284
- const actualIndex = startIndex + visibleIndex;
285
- const isSelected = actualIndex === selectedSuggestion;
286
- return (
287
- <Box key={`${suggestion.cmd}-${suggestion.args}-${actualIndex}`} paddingX={1}>
288
- <Text
289
- backgroundColor={isSelected ? 'blue' : undefined}
290
- color={isSelected ? 'white' : 'cyan'}
291
- bold={isSelected}
292
- >
293
- {suggestion.cmd}
294
- </Text>
295
- {suggestion.args && (
296
- <Text
297
- color={isSelected ? 'white' : 'gray'}
298
- backgroundColor={isSelected ? 'blue' : undefined}
299
- >
300
- {' '}{suggestion.args}
301
- </Text>
302
- )}
303
- <Text color="gray"> - </Text>
304
- <Text
305
- color={isSelected ? 'white' : 'gray'}
306
- dimColor={!isSelected}
307
- >
308
- {suggestion.desc}
309
- </Text>
310
- </Box>
311
- );
312
- })}
313
- {/* Scroll down indicator */}
314
- {startIndex + MAX_VISIBLE < suggestions.length && (
315
- <Box paddingX={1}>
316
- <Text color="gray">↓ {suggestions.length - startIndex - MAX_VISIBLE} more below</Text>
317
- </Box>
318
- )}
319
- <Box paddingX={1}>
320
- <Text dimColor>Tab complete · ↑↓ select · Esc cancel</Text>
321
- </Box>
322
- </Box>
323
- )}
324
-
325
- {/* Input box */}
326
- <Box
327
- borderStyle="single"
328
- borderColor="cyan"
329
- paddingX={1}
330
- >
331
- <Text color="cyan" bold>&gt;</Text>
332
- <Text> </Text>
333
- <TextInput
334
- value={value}
335
- onChange={handleChange}
336
- onSubmit={() => handleSubmit(value.startsWith('/') ? value : '/' + value)}
337
- placeholder="Type / to run a command..."
338
- />
339
- </Box>
340
-
341
- {/* Hints bar (below input) */}
342
- <Box paddingX={1} justifyContent="space-between">
343
- <Box>
344
- {hints.map((hint, index) => (
345
- <React.Fragment key={hint}>
346
- {index > 0 && <Text color="gray"> · </Text>}
347
- <Text dimColor>{hint}</Text>
348
- </React.Fragment>
349
- ))}
350
- </Box>
351
- </Box>
352
- </Box>
353
- );
92
+ export default function CommandInput({
93
+ onSubmit,
94
+ activeService,
95
+ onSuggestionsChange,
96
+ }: CommandInputProps) {
97
+ const [value, setValue] = useState("")
98
+ const [filterValue, setFilterValue] = useState("") // What we filter suggestions by (doesn't change when arrowing)
99
+ const [history, setHistory] = useState<string[]>([])
100
+ const [historyIndex, setHistoryIndex] = useState(-1)
101
+ const [selectedSuggestion, setSelectedSuggestion] = useState(0)
102
+ const [isNavigating, setIsNavigating] = useState(false) // True when user is arrowing through suggestions
103
+
104
+ // Track if suggestions are currently shown to avoid flicker
105
+ const prevShowSuggestions = useRef(false)
106
+
107
+ // Maximum visible suggestions in the dropdown
108
+ const MAX_VISIBLE = 8
109
+
110
+ // Filter commands based on filterValue (stays stable while navigating with arrows)
111
+ const suggestions = useMemo(() => {
112
+ if (!filterValue.startsWith("/")) return []
113
+
114
+ const search = filterValue.toLowerCase()
115
+ return COMMANDS.filter((c) => {
116
+ const fullCmd = c.args ? `${c.cmd} ${c.args}` : c.cmd
117
+ return (
118
+ fullCmd.toLowerCase().includes(search) ||
119
+ c.cmd.toLowerCase().startsWith(search)
120
+ )
121
+ })
122
+ }, [filterValue])
123
+
124
+ // Calculate visible window of suggestions (scrolls to keep selection visible)
125
+ const { visibleSuggestions, startIndex } = useMemo(() => {
126
+ if (suggestions.length <= MAX_VISIBLE) {
127
+ return { visibleSuggestions: suggestions, startIndex: 0 }
128
+ }
129
+
130
+ // Calculate window to keep selected item visible
131
+ let start = 0
132
+ if (selectedSuggestion >= MAX_VISIBLE) {
133
+ // Selected item is beyond initial window, scroll down
134
+ start = selectedSuggestion - MAX_VISIBLE + 1
135
+ }
136
+ // Ensure we don't go past the end
137
+ start = Math.min(start, suggestions.length - MAX_VISIBLE)
138
+ start = Math.max(0, start)
139
+
140
+ return {
141
+ visibleSuggestions: suggestions.slice(start, start + MAX_VISIBLE),
142
+ startIndex: start,
143
+ }
144
+ }, [suggestions, selectedSuggestion])
145
+
146
+ const showSuggestions =
147
+ filterValue.startsWith("/") &&
148
+ filterValue.length >= 1 &&
149
+ suggestions.length > 0
150
+
151
+ // Helper to build full command string from suggestion
152
+ const buildFullCommand = useCallback(
153
+ (suggestion: (typeof COMMANDS)[0]): string => {
154
+ if (suggestion.args) {
155
+ const hasPlaceholder =
156
+ suggestion.args.includes("<") || suggestion.args.includes("[")
157
+ if (!hasPlaceholder) {
158
+ return `${suggestion.cmd} ${suggestion.args}`
159
+ }
160
+ }
161
+ return suggestion.cmd
162
+ },
163
+ [],
164
+ )
165
+
166
+ // Notify parent when suggestions visibility changes (not on every count change)
167
+ // This reduces flicker by only updating when suggestions appear/disappear
168
+ useEffect(() => {
169
+ if (showSuggestions !== prevShowSuggestions.current) {
170
+ prevShowSuggestions.current = showSuggestions
171
+ const visibleCount = Math.min(suggestions.length, MAX_VISIBLE)
172
+ const count = showSuggestions ? visibleCount + 4 : 0 // +4 for borders, scroll indicators, hint line
173
+ onSuggestionsChange?.(count)
174
+ }
175
+ }, [showSuggestions, suggestions.length, onSuggestionsChange])
176
+
177
+ // Reset selected suggestion when suggestions change
178
+ useEffect(() => {
179
+ if (selectedSuggestion >= suggestions.length) {
180
+ setSelectedSuggestion(Math.max(0, suggestions.length - 1))
181
+ }
182
+ }, [suggestions.length, selectedSuggestion])
183
+
184
+ useInput((input, key) => {
185
+ // Tab to autocomplete selected suggestion and commit it
186
+ if (key.tab && showSuggestions && suggestions[selectedSuggestion]) {
187
+ const suggestion = suggestions[selectedSuggestion]
188
+ const fullCmd = buildFullCommand(suggestion)
189
+ setValue(fullCmd)
190
+ setFilterValue(fullCmd) // Commit the selection to filter
191
+ setSelectedSuggestion(0)
192
+ setIsNavigating(false)
193
+ return
194
+ }
195
+
196
+ // Up/Down to navigate suggestions when showing (circular navigation)
197
+ if (showSuggestions) {
198
+ if (key.upArrow) {
199
+ setIsNavigating(true)
200
+ setSelectedSuggestion((prev) => {
201
+ const newIndex = prev <= 0 ? suggestions.length - 1 : prev - 1
202
+ // Update display value to show selected command
203
+ const suggestion = suggestions[newIndex]
204
+ if (suggestion) {
205
+ setValue(buildFullCommand(suggestion))
206
+ }
207
+ return newIndex
208
+ })
209
+ return
210
+ }
211
+ if (key.downArrow) {
212
+ setIsNavigating(true)
213
+ setSelectedSuggestion((prev) => {
214
+ const newIndex = prev >= suggestions.length - 1 ? 0 : prev + 1
215
+ // Update display value to show selected command
216
+ const suggestion = suggestions[newIndex]
217
+ if (suggestion) {
218
+ setValue(buildFullCommand(suggestion))
219
+ }
220
+ return newIndex
221
+ })
222
+ return
223
+ }
224
+ } else {
225
+ // History navigation when not showing suggestions
226
+ if (key.upArrow && history.length > 0) {
227
+ const newIndex = Math.min(historyIndex + 1, history.length - 1)
228
+ setHistoryIndex(newIndex)
229
+ const historyValue = history[history.length - 1 - newIndex] || ""
230
+ setValue(historyValue)
231
+ setFilterValue(historyValue)
232
+ return
233
+ }
234
+
235
+ if (key.downArrow) {
236
+ const newIndex = Math.max(historyIndex - 1, -1)
237
+ setHistoryIndex(newIndex)
238
+ if (newIndex < 0) {
239
+ setValue("")
240
+ setFilterValue("")
241
+ } else {
242
+ const historyValue = history[history.length - 1 - newIndex] || ""
243
+ setValue(historyValue)
244
+ setFilterValue(historyValue)
245
+ }
246
+ return
247
+ }
248
+ }
249
+
250
+ // Escape to clear input or close suggestions
251
+ if (key.escape) {
252
+ setValue("")
253
+ setFilterValue("")
254
+ setSelectedSuggestion(0)
255
+ setHistoryIndex(-1)
256
+ setIsNavigating(false)
257
+ return
258
+ }
259
+ })
260
+
261
+ const handleSubmit = useCallback(
262
+ (input: string) => {
263
+ if (!input.trim()) return
264
+
265
+ // Add to history (avoid duplicates)
266
+ setHistory((prev) => [
267
+ ...prev.filter((h) => h !== input.trim()),
268
+ input.trim(),
269
+ ])
270
+
271
+ // Reset state
272
+ setValue("")
273
+ setFilterValue("")
274
+ setHistoryIndex(-1)
275
+ setSelectedSuggestion(0)
276
+ setIsNavigating(false)
277
+
278
+ // Call handler
279
+ onSubmit(input)
280
+ },
281
+ [onSubmit],
282
+ )
283
+
284
+ // Handle text input changes - updates both value and filter
285
+ const handleChange = useCallback((v: string) => {
286
+ setValue(v)
287
+ setFilterValue(v) // When user types, update filter to match
288
+ setSelectedSuggestion(0)
289
+ setIsNavigating(false)
290
+ }, [])
291
+
292
+ // Get context-specific hints for current tab
293
+ const hints = useMemo((): string[] => {
294
+ const h: string[] = []
295
+
296
+ if (activeService) {
297
+ const isRunning =
298
+ activeService.status === "running" ||
299
+ activeService.status === "starting"
300
+ if (isRunning && activeService.id !== "system") {
301
+ h.push(`Ctrl+K stop`)
302
+ }
303
+ }
304
+
305
+ h.push("Ctrl+L clear")
306
+ h.push("←/→ tabs")
307
+ h.push("Esc cancel")
308
+
309
+ return h
310
+ }, [activeService])
311
+
312
+ return (
313
+ <Box flexDirection="column">
314
+ {/* Suggestions dropdown (above input) */}
315
+ {showSuggestions && (
316
+ <Box
317
+ flexDirection="column"
318
+ borderStyle="round"
319
+ borderColor="gray"
320
+ marginBottom={0}
321
+ >
322
+ {/* Scroll up indicator */}
323
+ {startIndex > 0 && (
324
+ <Box paddingX={1}>
325
+ <Text color="gray">↑ {startIndex} more above</Text>
326
+ </Box>
327
+ )}
328
+ {visibleSuggestions.map((suggestion, visibleIndex) => {
329
+ const actualIndex = startIndex + visibleIndex
330
+ const isSelected = actualIndex === selectedSuggestion
331
+ return (
332
+ <Box
333
+ key={`${suggestion.cmd}-${suggestion.args}-${actualIndex}`}
334
+ paddingX={1}
335
+ >
336
+ <Text
337
+ backgroundColor={isSelected ? "blue" : undefined}
338
+ color={isSelected ? "white" : "cyan"}
339
+ bold={isSelected}
340
+ >
341
+ {suggestion.cmd}
342
+ </Text>
343
+ {suggestion.args && (
344
+ <Text
345
+ color={isSelected ? "white" : "gray"}
346
+ backgroundColor={isSelected ? "blue" : undefined}
347
+ >
348
+ {" "}
349
+ {suggestion.args}
350
+ </Text>
351
+ )}
352
+ <Text color="gray"> - </Text>
353
+ <Text
354
+ color={isSelected ? "white" : "gray"}
355
+ dimColor={!isSelected}
356
+ >
357
+ {suggestion.desc}
358
+ </Text>
359
+ </Box>
360
+ )
361
+ })}
362
+ {/* Scroll down indicator */}
363
+ {startIndex + MAX_VISIBLE < suggestions.length && (
364
+ <Box paddingX={1}>
365
+ <Text color="gray">
366
+ {suggestions.length - startIndex - MAX_VISIBLE} more below
367
+ </Text>
368
+ </Box>
369
+ )}
370
+ <Box paddingX={1}>
371
+ <Text dimColor>Tab complete · ↑↓ select · Esc cancel</Text>
372
+ </Box>
373
+ </Box>
374
+ )}
375
+
376
+ {/* Input box */}
377
+ <Box borderStyle="single" borderColor="cyan" paddingX={1}>
378
+ <Text color="cyan" bold>
379
+ &gt;
380
+ </Text>
381
+ <Text> </Text>
382
+ <TextInput
383
+ value={value}
384
+ onChange={handleChange}
385
+ onSubmit={() =>
386
+ handleSubmit(value.startsWith("/") ? value : "/" + value)
387
+ }
388
+ placeholder="Type / to run a command..."
389
+ />
390
+ </Box>
391
+
392
+ {/* Hints bar (below input) */}
393
+ <Box paddingX={1} justifyContent="space-between">
394
+ <Box>
395
+ {hints.map((hint, index) => (
396
+ <React.Fragment key={hint}>
397
+ {index > 0 && <Text color="gray"> · </Text>}
398
+ <Text dimColor>{hint}</Text>
399
+ </React.Fragment>
400
+ ))}
401
+ </Box>
402
+ </Box>
403
+ </Box>
404
+ )
354
405
  }