@google/gemini-cli 0.3.2 → 0.4.0-preview

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 (198) hide show
  1. package/dist/google-gemini-cli-0.3.1.tgz +0 -0
  2. package/dist/package.json +3 -6
  3. package/dist/src/commands/extensions/enable.js +1 -1
  4. package/dist/src/commands/extensions/enable.js.map +1 -1
  5. package/dist/src/commands/extensions/examples/context/GEMINI.md +8 -0
  6. package/dist/src/commands/extensions/examples/context/gemini-extension.json +5 -0
  7. package/dist/src/commands/extensions/examples/custom-commands/commands/fs/grep-code.toml +6 -0
  8. package/dist/src/commands/extensions/examples/custom-commands/gemini-extension.json +4 -0
  9. package/dist/src/commands/extensions/examples/exclude-tools/gemini-extension.json +5 -0
  10. package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
  11. package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
  12. package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
  13. package/dist/src/commands/extensions/examples/mcp-server/example.ts +60 -0
  14. package/dist/src/commands/extensions/examples/mcp-server/gemini-extension.json +10 -0
  15. package/dist/src/commands/extensions/install.js +35 -7
  16. package/dist/src/commands/extensions/install.js.map +1 -1
  17. package/dist/src/commands/extensions/install.test.js +20 -2
  18. package/dist/src/commands/extensions/install.test.js.map +1 -1
  19. package/dist/src/commands/extensions/link.d.ts +12 -0
  20. package/dist/src/commands/extensions/link.js +37 -0
  21. package/dist/src/commands/extensions/link.js.map +1 -0
  22. package/dist/src/commands/extensions/new.d.ts +7 -0
  23. package/dist/src/commands/extensions/new.js +70 -0
  24. package/dist/src/commands/extensions/new.js.map +1 -0
  25. package/dist/src/commands/extensions/new.test.d.ts +6 -0
  26. package/dist/src/commands/extensions/new.test.js +50 -0
  27. package/dist/src/commands/extensions/new.test.js.map +1 -0
  28. package/dist/src/commands/extensions/update.d.ts +2 -1
  29. package/dist/src/commands/extensions/update.js +36 -15
  30. package/dist/src/commands/extensions/update.js.map +1 -1
  31. package/dist/src/commands/extensions.js +4 -0
  32. package/dist/src/commands/extensions.js.map +1 -1
  33. package/dist/src/commands/mcp/add.js +1 -1
  34. package/dist/src/commands/mcp/add.js.map +1 -1
  35. package/dist/src/commands/mcp/list.js +2 -2
  36. package/dist/src/commands/mcp/list.js.map +1 -1
  37. package/dist/src/commands/mcp/remove.js +1 -1
  38. package/dist/src/commands/mcp/remove.js.map +1 -1
  39. package/dist/src/config/auth.d.ts +1 -1
  40. package/dist/src/config/auth.js +4 -4
  41. package/dist/src/config/auth.js.map +1 -1
  42. package/dist/src/config/auth.test.js +15 -7
  43. package/dist/src/config/auth.test.js.map +1 -1
  44. package/dist/src/config/config.d.ts +4 -1
  45. package/dist/src/config/config.js +32 -18
  46. package/dist/src/config/config.js.map +1 -1
  47. package/dist/src/config/extension.d.ts +7 -4
  48. package/dist/src/config/extension.js +76 -24
  49. package/dist/src/config/extension.js.map +1 -1
  50. package/dist/src/config/keyBindings.d.ts +1 -0
  51. package/dist/src/config/keyBindings.js +6 -25
  52. package/dist/src/config/keyBindings.js.map +1 -1
  53. package/dist/src/config/settings.d.ts +5 -5
  54. package/dist/src/config/settings.js +93 -224
  55. package/dist/src/config/settings.js.map +1 -1
  56. package/dist/src/config/settingsSchema.d.ts +113 -13
  57. package/dist/src/config/settingsSchema.js +112 -13
  58. package/dist/src/config/settingsSchema.js.map +1 -1
  59. package/dist/src/config/trustedFolders.d.ts +12 -2
  60. package/dist/src/config/trustedFolders.js +59 -40
  61. package/dist/src/config/trustedFolders.js.map +1 -1
  62. package/dist/src/config/trustedFolders.test.js +81 -2
  63. package/dist/src/config/trustedFolders.test.js.map +1 -1
  64. package/dist/src/gemini.d.ts +1 -1
  65. package/dist/src/gemini.js +52 -26
  66. package/dist/src/gemini.js.map +1 -1
  67. package/dist/src/gemini.test.js +2 -30
  68. package/dist/src/gemini.test.js.map +1 -1
  69. package/dist/src/generated/git-commit.d.ts +2 -2
  70. package/dist/src/generated/git-commit.js +2 -2
  71. package/dist/src/generated/git-commit.js.map +1 -1
  72. package/dist/src/services/BuiltinCommandLoader.js +1 -1
  73. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  74. package/dist/src/services/BuiltinCommandLoader.test.js +3 -3
  75. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  76. package/dist/src/services/FileCommandLoader.d.ts +2 -0
  77. package/dist/src/services/FileCommandLoader.js +7 -0
  78. package/dist/src/services/FileCommandLoader.js.map +1 -1
  79. package/dist/src/ui/App.js +109 -47
  80. package/dist/src/ui/App.js.map +1 -1
  81. package/dist/src/ui/commands/aboutCommand.js +9 -3
  82. package/dist/src/ui/commands/aboutCommand.js.map +1 -1
  83. package/dist/src/ui/commands/bugCommand.js +9 -4
  84. package/dist/src/ui/commands/bugCommand.js.map +1 -1
  85. package/dist/src/ui/commands/directoryCommand.js +1 -1
  86. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  87. package/dist/src/ui/commands/ideCommand.d.ts +1 -2
  88. package/dist/src/ui/commands/ideCommand.js +16 -7
  89. package/dist/src/ui/commands/ideCommand.js.map +1 -1
  90. package/dist/src/ui/commands/mcpCommand.js +6 -4
  91. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  92. package/dist/src/ui/commands/memoryCommand.js +1 -1
  93. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  94. package/dist/src/ui/components/AuthDialog.js +8 -2
  95. package/dist/src/ui/components/AuthDialog.js.map +1 -1
  96. package/dist/src/ui/components/AuthDialog.test.js +41 -10
  97. package/dist/src/ui/components/AuthDialog.test.js.map +1 -1
  98. package/dist/src/ui/components/FolderTrustDialog.js +3 -1
  99. package/dist/src/ui/components/FolderTrustDialog.js.map +1 -1
  100. package/dist/src/ui/components/FolderTrustDialog.test.js +19 -4
  101. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  102. package/dist/src/ui/components/Footer.d.ts +3 -0
  103. package/dist/src/ui/components/Footer.js +4 -3
  104. package/dist/src/ui/components/Footer.js.map +1 -1
  105. package/dist/src/ui/components/InputPrompt.js +34 -24
  106. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  107. package/dist/src/ui/components/MemoryUsageDisplay.js +1 -1
  108. package/dist/src/ui/components/MemoryUsageDisplay.js.map +1 -1
  109. package/dist/src/ui/components/ProQuotaDialog.d.ts +13 -0
  110. package/dist/src/ui/components/ProQuotaDialog.js +21 -0
  111. package/dist/src/ui/components/ProQuotaDialog.js.map +1 -0
  112. package/dist/src/ui/components/ProQuotaDialog.test.d.ts +6 -0
  113. package/dist/src/ui/components/ProQuotaDialog.test.js +56 -0
  114. package/dist/src/ui/components/ProQuotaDialog.test.js.map +1 -0
  115. package/dist/src/ui/components/SettingsDialog.test.js +2 -2
  116. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  117. package/dist/src/ui/components/messages/ToolConfirmationMessage.js +3 -3
  118. package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
  119. package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js +0 -8
  120. package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js.map +1 -1
  121. package/dist/src/ui/components/shared/MaxSizedBox.js.map +1 -1
  122. package/dist/src/ui/components/shared/text-buffer.js +35 -51
  123. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  124. package/dist/src/ui/contexts/KeypressContext.js +255 -42
  125. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  126. package/dist/src/ui/contexts/KeypressContext.test.js +115 -1
  127. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  128. package/dist/src/ui/hooks/slashCommandProcessor.js +11 -6
  129. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  130. package/dist/src/ui/hooks/useFolderTrust.js +6 -4
  131. package/dist/src/ui/hooks/useFolderTrust.js.map +1 -1
  132. package/dist/src/ui/hooks/useGeminiStream.d.ts +3 -2
  133. package/dist/src/ui/hooks/useGeminiStream.js +31 -3
  134. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  135. package/dist/src/ui/hooks/useIdeTrustListener.d.ts +14 -0
  136. package/dist/src/ui/hooks/useIdeTrustListener.js +39 -0
  137. package/dist/src/ui/hooks/useIdeTrustListener.js.map +1 -0
  138. package/dist/src/ui/hooks/useInputHistoryStore.d.ts +19 -0
  139. package/dist/src/ui/hooks/useInputHistoryStore.js +81 -0
  140. package/dist/src/ui/hooks/useInputHistoryStore.js.map +1 -0
  141. package/dist/src/ui/hooks/useInputHistoryStore.test.d.ts +6 -0
  142. package/dist/src/ui/hooks/useInputHistoryStore.test.js +234 -0
  143. package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -0
  144. package/dist/src/ui/hooks/useLoadingIndicator.d.ts +1 -1
  145. package/dist/src/ui/hooks/useLoadingIndicator.js +2 -2
  146. package/dist/src/ui/hooks/useLoadingIndicator.js.map +1 -1
  147. package/dist/src/ui/hooks/useLoadingIndicator.test.js +2 -2
  148. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  149. package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -1
  150. package/dist/src/ui/hooks/usePhraseCycler.js +11 -8
  151. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  152. package/dist/src/ui/hooks/usePrivacySettings.d.ts +1 -1
  153. package/dist/src/ui/hooks/usePrivacySettings.js +8 -13
  154. package/dist/src/ui/hooks/usePrivacySettings.js.map +1 -1
  155. package/dist/src/ui/hooks/usePrivacySettings.test.js +33 -97
  156. package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
  157. package/dist/src/ui/hooks/useSlashCompletion.js +263 -67
  158. package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
  159. package/dist/src/ui/hooks/useSlashCompletion.test.d.ts +4 -1
  160. package/dist/src/ui/hooks/useSlashCompletion.test.js +452 -59
  161. package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
  162. package/dist/src/ui/hooks/useToolScheduler.test.js +57 -59
  163. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  164. package/dist/src/ui/keyMatchers.test.js +9 -0
  165. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  166. package/dist/src/ui/utils/MarkdownDisplay.test.js +2 -2
  167. package/dist/src/ui/utils/MarkdownDisplay.test.js.map +1 -1
  168. package/dist/src/ui/utils/highlight.d.ts +10 -0
  169. package/dist/src/ui/utils/highlight.js +41 -0
  170. package/dist/src/ui/utils/highlight.js.map +1 -0
  171. package/dist/src/ui/utils/highlight.test.d.ts +6 -0
  172. package/dist/src/ui/utils/highlight.test.js +93 -0
  173. package/dist/src/ui/utils/highlight.test.js.map +1 -0
  174. package/dist/src/ui/utils/platformConstants.d.ts +24 -1
  175. package/dist/src/ui/utils/platformConstants.js +26 -1
  176. package/dist/src/ui/utils/platformConstants.js.map +1 -1
  177. package/dist/src/utils/deepMerge.d.ts +10 -0
  178. package/dist/src/utils/deepMerge.js +58 -0
  179. package/dist/src/utils/deepMerge.js.map +1 -0
  180. package/dist/src/utils/deepMerge.test.d.ts +6 -0
  181. package/dist/src/utils/deepMerge.test.js +143 -0
  182. package/dist/src/utils/deepMerge.test.js.map +1 -0
  183. package/dist/src/utils/envVarResolver.d.ts +39 -0
  184. package/dist/src/utils/envVarResolver.js +97 -0
  185. package/dist/src/utils/envVarResolver.js.map +1 -0
  186. package/dist/src/utils/envVarResolver.test.d.ts +6 -0
  187. package/dist/src/utils/envVarResolver.test.js +221 -0
  188. package/dist/src/utils/envVarResolver.test.js.map +1 -0
  189. package/dist/src/utils/userStartupWarnings.d.ts +1 -1
  190. package/dist/src/utils/userStartupWarnings.js +1 -1
  191. package/dist/src/utils/userStartupWarnings.js.map +1 -1
  192. package/dist/src/validateNonInterActiveAuth.d.ts +2 -1
  193. package/dist/src/validateNonInterActiveAuth.js +11 -2
  194. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  195. package/dist/src/zed-integration/zedIntegration.js +7 -5
  196. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  197. package/dist/tsconfig.tsbuildinfo +1 -1
  198. package/package.json +4 -7
@@ -7,7 +7,105 @@
7
7
  import { describe, it, expect, vi } from 'vitest';
8
8
  import { renderHook, waitFor } from '@testing-library/react';
9
9
  import { useSlashCompletion } from './useSlashCompletion.js';
10
+ import { CommandKind } from '../commands/types.js';
10
11
  import { useState } from 'react';
12
+ function createTestCommand(command) {
13
+ return {
14
+ kind: CommandKind.BUILT_IN, // default for tests
15
+ ...command,
16
+ };
17
+ }
18
+ // Track AsyncFzf constructor calls for cache testing
19
+ let asyncFzfConstructorCalls = 0;
20
+ const resetConstructorCallCount = () => {
21
+ asyncFzfConstructorCalls = 0;
22
+ };
23
+ const getConstructorCallCount = () => asyncFzfConstructorCalls;
24
+ // Centralized fuzzy matching simulation logic
25
+ // Note: This is a simplified reimplementation that may diverge from real fzf behavior.
26
+ // Integration tests in useSlashCompletion.integration.test.ts use the real fzf library
27
+ // to catch any behavioral differences and serve as our "canary in a coal mine."
28
+ function simulateFuzzyMatching(items, query) {
29
+ const results = [];
30
+ if (query) {
31
+ const lowerQuery = query.toLowerCase();
32
+ for (const item of items) {
33
+ const lowerItem = item.toLowerCase();
34
+ // Exact match gets highest score
35
+ if (lowerItem === lowerQuery) {
36
+ results.push({
37
+ item,
38
+ positions: [],
39
+ score: 100,
40
+ start: 0,
41
+ end: item.length,
42
+ });
43
+ continue;
44
+ }
45
+ // Prefix match gets high score
46
+ if (lowerItem.startsWith(lowerQuery)) {
47
+ results.push({
48
+ item,
49
+ positions: [],
50
+ score: 80,
51
+ start: 0,
52
+ end: query.length,
53
+ });
54
+ continue;
55
+ }
56
+ // Fuzzy matching: check if query chars appear in order
57
+ let queryIndex = 0;
58
+ let score = 0;
59
+ for (let i = 0; i < lowerItem.length && queryIndex < lowerQuery.length; i++) {
60
+ if (lowerItem[i] === lowerQuery[queryIndex]) {
61
+ queryIndex++;
62
+ score += 10 - i; // Earlier matches get higher scores
63
+ }
64
+ }
65
+ // If all query characters were found in order, include this item
66
+ if (queryIndex === lowerQuery.length) {
67
+ results.push({
68
+ item,
69
+ positions: [],
70
+ score,
71
+ start: 0,
72
+ end: query.length,
73
+ });
74
+ }
75
+ }
76
+ }
77
+ // Sort by score descending (better matches first)
78
+ results.sort((a, b) => b.score - a.score);
79
+ return Promise.resolve(results);
80
+ }
81
+ // Mock the fzf module to provide a working fuzzy search implementation for tests
82
+ vi.mock('fzf', async () => {
83
+ const actual = await vi.importActual('fzf');
84
+ return {
85
+ ...actual,
86
+ AsyncFzf: vi.fn().mockImplementation((items, _options) => {
87
+ asyncFzfConstructorCalls++;
88
+ return {
89
+ find: vi
90
+ .fn()
91
+ .mockImplementation((query) => simulateFuzzyMatching(items, query)),
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ };
94
+ }),
95
+ };
96
+ });
97
+ // Default mock behavior helper - now uses centralized logic
98
+ const createDefaultAsyncFzfMock = () => (items, _options) => {
99
+ asyncFzfConstructorCalls++;
100
+ return {
101
+ find: vi
102
+ .fn()
103
+ .mockImplementation((query) => simulateFuzzyMatching(items, query)),
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ };
106
+ };
107
+ // Export test utilities
108
+ export { resetConstructorCallCount, getConstructorCallCount, createDefaultAsyncFzfMock, };
11
109
  // Test harness to capture the state from the hook's callbacks.
12
110
  function useTestHarnessForSlashCompletion(enabled, query, slashCommands, commandContext) {
13
111
  const [suggestions, setSuggestions] = useState([]);
@@ -36,19 +134,25 @@ describe('useSlashCompletion', () => {
36
134
  describe('Top-Level Commands', () => {
37
135
  it('should suggest all top-level commands for the root slash', async () => {
38
136
  const slashCommands = [
39
- { name: 'help', altNames: ['?'], description: 'Show help' },
40
- {
137
+ createTestCommand({
138
+ name: 'help',
139
+ altNames: ['?'],
140
+ description: 'Show help',
141
+ }),
142
+ createTestCommand({
41
143
  name: 'stats',
42
144
  altNames: ['usage'],
43
145
  description: 'check session stats. Usage: /stats [model|tools]',
44
- },
45
- { name: 'clear', description: 'Clear the screen' },
46
- {
146
+ }),
147
+ createTestCommand({ name: 'clear', description: 'Clear the screen' }),
148
+ createTestCommand({
47
149
  name: 'memory',
48
150
  description: 'Manage memory',
49
- subCommands: [{ name: 'show', description: 'Show memory' }],
50
- },
51
- { name: 'chat', description: 'Manage chat history' },
151
+ subCommands: [
152
+ createTestCommand({ name: 'show', description: 'Show memory' }),
153
+ ],
154
+ }),
155
+ createTestCommand({ name: 'chat', description: 'Manage chat history' }),
52
156
  ];
53
157
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/', slashCommands, mockCommandContext));
54
158
  expect(result.current.suggestions.length).toBe(slashCommands.length);
@@ -56,65 +160,73 @@ describe('useSlashCompletion', () => {
56
160
  });
57
161
  it('should filter commands based on partial input', async () => {
58
162
  const slashCommands = [
59
- { name: 'memory', description: 'Manage memory' },
163
+ createTestCommand({ name: 'memory', description: 'Manage memory' }),
60
164
  ];
61
165
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/mem', slashCommands, mockCommandContext));
62
- expect(result.current.suggestions).toEqual([
63
- { label: 'memory', value: 'memory', description: 'Manage memory' },
64
- ]);
166
+ await waitFor(() => {
167
+ expect(result.current.suggestions).toEqual([
168
+ { label: 'memory', value: 'memory', description: 'Manage memory' },
169
+ ]);
170
+ });
65
171
  });
66
172
  it('should suggest commands based on partial altNames', async () => {
67
173
  const slashCommands = [
68
- {
174
+ createTestCommand({
69
175
  name: 'stats',
70
176
  altNames: ['usage'],
71
177
  description: 'check session stats. Usage: /stats [model|tools]',
72
- },
178
+ }),
73
179
  ];
74
180
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/usag', slashCommands, mockCommandContext));
75
- expect(result.current.suggestions).toEqual([
76
- {
77
- label: 'stats',
78
- value: 'stats',
79
- description: 'check session stats. Usage: /stats [model|tools]',
80
- },
81
- ]);
181
+ await waitFor(() => {
182
+ expect(result.current.suggestions).toEqual([
183
+ {
184
+ label: 'stats',
185
+ value: 'stats',
186
+ description: 'check session stats. Usage: /stats [model|tools]',
187
+ },
188
+ ]);
189
+ });
82
190
  });
83
191
  it('should NOT provide suggestions for a perfectly typed command that is a leaf node', async () => {
84
192
  const slashCommands = [
85
- { name: 'clear', description: 'Clear the screen', action: vi.fn() },
193
+ createTestCommand({
194
+ name: 'clear',
195
+ description: 'Clear the screen',
196
+ action: vi.fn(),
197
+ }),
86
198
  ];
87
199
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/clear', slashCommands, mockCommandContext));
88
200
  expect(result.current.suggestions).toHaveLength(0);
89
201
  });
90
202
  it.each([['/?'], ['/usage']])('should not suggest commands when altNames is fully typed', async (query) => {
91
203
  const mockSlashCommands = [
92
- {
204
+ createTestCommand({
93
205
  name: 'help',
94
206
  altNames: ['?'],
95
207
  description: 'Show help',
96
208
  action: vi.fn(),
97
- },
98
- {
209
+ }),
210
+ createTestCommand({
99
211
  name: 'stats',
100
212
  altNames: ['usage'],
101
213
  description: 'check session stats. Usage: /stats [model|tools]',
102
214
  action: vi.fn(),
103
- },
215
+ }),
104
216
  ];
105
217
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, query, mockSlashCommands, mockCommandContext));
106
218
  expect(result.current.suggestions).toHaveLength(0);
107
219
  });
108
220
  it('should not provide suggestions for a fully typed command that has no sub-commands or argument completion', async () => {
109
221
  const slashCommands = [
110
- { name: 'clear', description: 'Clear the screen' },
222
+ createTestCommand({ name: 'clear', description: 'Clear the screen' }),
111
223
  ];
112
224
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/clear ', slashCommands, mockCommandContext));
113
225
  expect(result.current.suggestions).toHaveLength(0);
114
226
  });
115
227
  it('should not provide suggestions for an unknown command', async () => {
116
228
  const slashCommands = [
117
- { name: 'help', description: 'Show help' },
229
+ createTestCommand({ name: 'help', description: 'Show help' }),
118
230
  ];
119
231
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/unknown-command', slashCommands, mockCommandContext));
120
232
  expect(result.current.suggestions).toHaveLength(0);
@@ -123,14 +235,14 @@ describe('useSlashCompletion', () => {
123
235
  describe('Sub-Commands', () => {
124
236
  it('should suggest sub-commands for a parent command', async () => {
125
237
  const slashCommands = [
126
- {
238
+ createTestCommand({
127
239
  name: 'memory',
128
240
  description: 'Manage memory',
129
241
  subCommands: [
130
- { name: 'show', description: 'Show memory' },
131
- { name: 'add', description: 'Add to memory' },
242
+ createTestCommand({ name: 'show', description: 'Show memory' }),
243
+ createTestCommand({ name: 'add', description: 'Add to memory' }),
132
244
  ],
133
- },
245
+ }),
134
246
  ];
135
247
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory', slashCommands, mockCommandContext));
136
248
  expect(result.current.suggestions).toHaveLength(2);
@@ -141,14 +253,14 @@ describe('useSlashCompletion', () => {
141
253
  });
142
254
  it('should suggest all sub-commands when the query ends with the parent command and a space', async () => {
143
255
  const slashCommands = [
144
- {
256
+ createTestCommand({
145
257
  name: 'memory',
146
258
  description: 'Manage memory',
147
259
  subCommands: [
148
- { name: 'show', description: 'Show memory' },
149
- { name: 'add', description: 'Add to memory' },
260
+ createTestCommand({ name: 'show', description: 'Show memory' }),
261
+ createTestCommand({ name: 'add', description: 'Add to memory' }),
150
262
  ],
151
- },
263
+ }),
152
264
  ];
153
265
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory ', slashCommands, mockCommandContext));
154
266
  expect(result.current.suggestions).toHaveLength(2);
@@ -159,30 +271,32 @@ describe('useSlashCompletion', () => {
159
271
  });
160
272
  it('should filter sub-commands by prefix', async () => {
161
273
  const slashCommands = [
162
- {
274
+ createTestCommand({
163
275
  name: 'memory',
164
276
  description: 'Manage memory',
165
277
  subCommands: [
166
- { name: 'show', description: 'Show memory' },
167
- { name: 'add', description: 'Add to memory' },
278
+ createTestCommand({ name: 'show', description: 'Show memory' }),
279
+ createTestCommand({ name: 'add', description: 'Add to memory' }),
168
280
  ],
169
- },
281
+ }),
170
282
  ];
171
283
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory a', slashCommands, mockCommandContext));
172
- expect(result.current.suggestions).toEqual([
173
- { label: 'add', value: 'add', description: 'Add to memory' },
174
- ]);
284
+ await waitFor(() => {
285
+ expect(result.current.suggestions).toEqual([
286
+ { label: 'add', value: 'add', description: 'Add to memory' },
287
+ ]);
288
+ });
175
289
  });
176
290
  it('should provide no suggestions for an invalid sub-command', async () => {
177
291
  const slashCommands = [
178
- {
292
+ createTestCommand({
179
293
  name: 'memory',
180
294
  description: 'Manage memory',
181
295
  subCommands: [
182
- { name: 'show', description: 'Show memory' },
183
- { name: 'add', description: 'Add to memory' },
296
+ createTestCommand({ name: 'show', description: 'Show memory' }),
297
+ createTestCommand({ name: 'add', description: 'Add to memory' }),
184
298
  ],
185
- },
299
+ }),
186
300
  ];
187
301
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory dothisnow', slashCommands, mockCommandContext));
188
302
  expect(result.current.suggestions).toHaveLength(0);
@@ -199,17 +313,17 @@ describe('useSlashCompletion', () => {
199
313
  .fn()
200
314
  .mockImplementation(async (_context, partialArg) => availableTags.filter((tag) => tag.startsWith(partialArg)));
201
315
  const slashCommands = [
202
- {
316
+ createTestCommand({
203
317
  name: 'chat',
204
318
  description: 'Manage chat history',
205
319
  subCommands: [
206
- {
320
+ createTestCommand({
207
321
  name: 'resume',
208
322
  description: 'Resume a saved chat',
209
323
  completion: mockCompletionFn,
210
- },
324
+ }),
211
325
  ],
212
- },
326
+ }),
213
327
  ];
214
328
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume my-ch', slashCommands, mockCommandContext));
215
329
  await waitFor(() => {
@@ -227,17 +341,17 @@ describe('useSlashCompletion', () => {
227
341
  .fn()
228
342
  .mockResolvedValue(['my-chat-tag-1', 'my-chat-tag-2', 'my-channel']);
229
343
  const slashCommands = [
230
- {
344
+ createTestCommand({
231
345
  name: 'chat',
232
346
  description: 'Manage chat history',
233
347
  subCommands: [
234
- {
348
+ createTestCommand({
235
349
  name: 'resume',
236
350
  description: 'Resume a saved chat',
237
351
  completion: mockCompletionFn,
238
- },
352
+ }),
239
353
  ],
240
- },
354
+ }),
241
355
  ];
242
356
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume ', slashCommands, mockCommandContext));
243
357
  await waitFor(() => {
@@ -250,17 +364,17 @@ describe('useSlashCompletion', () => {
250
364
  it('should handle completion function that returns null', async () => {
251
365
  const completionFn = vi.fn().mockResolvedValue(null);
252
366
  const slashCommands = [
253
- {
367
+ createTestCommand({
254
368
  name: 'chat',
255
369
  description: 'Manage chat history',
256
370
  subCommands: [
257
- {
371
+ createTestCommand({
258
372
  name: 'resume',
259
373
  description: 'Resume a saved chat',
260
374
  completion: completionFn,
261
- },
375
+ }),
262
376
  ],
263
- },
377
+ }),
264
378
  ];
265
379
  const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume ', slashCommands, mockCommandContext));
266
380
  await waitFor(() => {
@@ -268,5 +382,284 @@ describe('useSlashCompletion', () => {
268
382
  });
269
383
  });
270
384
  });
385
+ describe('Fuzzy Matching', () => {
386
+ const fuzzyTestCommands = [
387
+ createTestCommand({
388
+ name: 'help',
389
+ altNames: ['?'],
390
+ description: 'Show help',
391
+ }),
392
+ createTestCommand({
393
+ name: 'history',
394
+ description: 'Show command history',
395
+ }),
396
+ createTestCommand({ name: 'hello', description: 'Hello world command' }),
397
+ createTestCommand({
398
+ name: 'config',
399
+ altNames: ['configure'],
400
+ description: 'Configure settings',
401
+ }),
402
+ createTestCommand({ name: 'clear', description: 'Clear the screen' }),
403
+ ];
404
+ it('should match commands with fuzzy search for partial queries', async () => {
405
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/he', fuzzyTestCommands, mockCommandContext));
406
+ await waitFor(() => {
407
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
408
+ });
409
+ const labels = result.current.suggestions.map((s) => s.label);
410
+ expect(labels).toEqual(expect.arrayContaining(['help', 'hello']));
411
+ });
412
+ it('should handle case-insensitive fuzzy matching', async () => {
413
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/HeLp', fuzzyTestCommands, mockCommandContext));
414
+ await waitFor(() => {
415
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
416
+ });
417
+ const labels = result.current.suggestions.map((s) => s.label);
418
+ expect(labels).toContain('help');
419
+ });
420
+ it('should provide typo-tolerant matching', async () => {
421
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/hlp', fuzzyTestCommands, mockCommandContext));
422
+ await waitFor(() => {
423
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
424
+ });
425
+ const labels = result.current.suggestions.map((s) => s.label);
426
+ expect(labels).toContain('help');
427
+ });
428
+ it('should match against alternative names with fuzzy search', async () => {
429
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/conf', fuzzyTestCommands, mockCommandContext));
430
+ await waitFor(() => {
431
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
432
+ });
433
+ const labels = result.current.suggestions.map((s) => s.label);
434
+ expect(labels).toContain('config');
435
+ });
436
+ it('should fallback to prefix matching when AsyncFzf find fails', async () => {
437
+ // Mock console.error to avoid noise in test output
438
+ const consoleErrorSpy = vi
439
+ .spyOn(console, 'error')
440
+ .mockImplementation(() => { });
441
+ // Import the mocked AsyncFzf
442
+ const { AsyncFzf } = await import('fzf');
443
+ // Create a failing find method for this specific test
444
+ const mockFind = vi
445
+ .fn()
446
+ .mockRejectedValue(new Error('AsyncFzf find failed'));
447
+ // Mock AsyncFzf to return an instance with failing find
448
+ vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
449
+ finder: vi.fn(),
450
+ find: mockFind,
451
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
452
+ }));
453
+ const testCommands = [
454
+ createTestCommand({ name: 'clear', description: 'Clear the screen' }),
455
+ createTestCommand({
456
+ name: 'config',
457
+ description: 'Configure settings',
458
+ }),
459
+ createTestCommand({ name: 'chat', description: 'Start chat' }),
460
+ ];
461
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/cle', testCommands, mockCommandContext));
462
+ await waitFor(() => {
463
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
464
+ });
465
+ // Should still get suggestions via prefix matching fallback
466
+ const labels = result.current.suggestions.map((s) => s.label);
467
+ expect(labels).toContain('clear');
468
+ expect(labels).not.toContain('config'); // Doesn't start with 'cle'
469
+ expect(labels).not.toContain('chat'); // Doesn't start with 'cle'
470
+ // Verify the error was logged
471
+ await waitFor(() => {
472
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', expect.any(Error));
473
+ });
474
+ consoleErrorSpy.mockRestore();
475
+ // Reset AsyncFzf mock to default behavior for other tests
476
+ vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
477
+ });
478
+ it('should show all commands for empty partial query', async () => {
479
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/', fuzzyTestCommands, mockCommandContext));
480
+ expect(result.current.suggestions.length).toBe(fuzzyTestCommands.length);
481
+ });
482
+ it('should handle AsyncFzf errors gracefully and fallback to prefix matching', async () => {
483
+ // Mock console.error to avoid noise in test output
484
+ const consoleErrorSpy = vi
485
+ .spyOn(console, 'error')
486
+ .mockImplementation(() => { });
487
+ // Import the mocked AsyncFzf
488
+ const { AsyncFzf } = await import('fzf');
489
+ // Create a failing find method for this specific test
490
+ const mockFind = vi
491
+ .fn()
492
+ .mockRejectedValue(new Error('AsyncFzf error in find'));
493
+ // Mock AsyncFzf to return an instance with failing find
494
+ vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
495
+ finder: vi.fn(),
496
+ find: mockFind,
497
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
498
+ }));
499
+ const testCommands = [
500
+ { name: 'test', description: 'Test command' },
501
+ { name: 'temp', description: 'Temporary command' },
502
+ ];
503
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/te', testCommands, mockCommandContext));
504
+ await waitFor(() => {
505
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
506
+ });
507
+ // Should get suggestions via prefix matching fallback
508
+ const labels = result.current.suggestions.map((s) => s.label);
509
+ expect(labels).toEqual(expect.arrayContaining(['test', 'temp']));
510
+ // Verify the error was logged
511
+ await waitFor(() => {
512
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', expect.any(Error));
513
+ });
514
+ consoleErrorSpy.mockRestore();
515
+ // Reset AsyncFzf mock to default behavior for other tests
516
+ vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
517
+ });
518
+ it('should cache AsyncFzf instances for performance', async () => {
519
+ // Reset constructor call count and ensure mock is set up correctly
520
+ resetConstructorCallCount();
521
+ // Import the mocked AsyncFzf
522
+ const { AsyncFzf } = await import('fzf');
523
+ vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
524
+ const { result, rerender } = renderHook(({ query }) => useTestHarnessForSlashCompletion(true, query, fuzzyTestCommands, mockCommandContext), { initialProps: { query: '/he' } });
525
+ await waitFor(() => {
526
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
527
+ });
528
+ const firstResults = result.current.suggestions.map((s) => s.label);
529
+ const callCountAfterFirst = getConstructorCallCount();
530
+ expect(callCountAfterFirst).toBeGreaterThan(0);
531
+ // Rerender with same query - should use cached instance
532
+ rerender({ query: '/he' });
533
+ await waitFor(() => {
534
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
535
+ });
536
+ const secondResults = result.current.suggestions.map((s) => s.label);
537
+ const callCountAfterSecond = getConstructorCallCount();
538
+ // Should have same number of constructor calls (reused cached instance)
539
+ expect(callCountAfterSecond).toBe(callCountAfterFirst);
540
+ expect(secondResults).toEqual(firstResults);
541
+ // Different query should still use same cached instance for same command set
542
+ rerender({ query: '/hel' });
543
+ await waitFor(() => {
544
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
545
+ });
546
+ const thirdCallCount = getConstructorCallCount();
547
+ expect(thirdCallCount).toBe(callCountAfterFirst); // Same constructor call count
548
+ });
549
+ it('should not return duplicate suggestions when query matches both name and altNames', async () => {
550
+ const commandsWithAltNames = [
551
+ createTestCommand({
552
+ name: 'config',
553
+ altNames: ['configure', 'conf'],
554
+ description: 'Configure settings',
555
+ }),
556
+ createTestCommand({
557
+ name: 'help',
558
+ altNames: ['?'],
559
+ description: 'Show help',
560
+ }),
561
+ ];
562
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/con', commandsWithAltNames, mockCommandContext));
563
+ await waitFor(() => {
564
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
565
+ });
566
+ const labels = result.current.suggestions.map((s) => s.label);
567
+ const uniqueLabels = new Set(labels);
568
+ // Should not have duplicates
569
+ expect(labels.length).toBe(uniqueLabels.size);
570
+ expect(labels).toContain('config');
571
+ });
572
+ });
573
+ describe('Race Condition Handling', () => {
574
+ it('should handle rapid input changes without race conditions', async () => {
575
+ const mockDelayedCompletion = vi
576
+ .fn()
577
+ .mockImplementation(async (_context, partialArg) => {
578
+ // Simulate network delay with different delays for different inputs
579
+ const delay = partialArg.includes('slow') ? 200 : 50;
580
+ await new Promise((resolve) => setTimeout(resolve, delay));
581
+ return [`suggestion-for-${partialArg}`];
582
+ });
583
+ const slashCommands = [
584
+ createTestCommand({
585
+ name: 'test',
586
+ description: 'Test command',
587
+ completion: mockDelayedCompletion,
588
+ }),
589
+ ];
590
+ const { result, rerender } = renderHook(({ query }) => useTestHarnessForSlashCompletion(true, query, slashCommands, mockCommandContext), { initialProps: { query: '/test slowquery' } });
591
+ // Quickly change to a faster query
592
+ rerender({ query: '/test fastquery' });
593
+ await waitFor(() => {
594
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
595
+ });
596
+ // Should show suggestions for the latest query only
597
+ const labels = result.current.suggestions.map((s) => s.label);
598
+ expect(labels).toContain('suggestion-for-fastquery');
599
+ expect(labels).not.toContain('suggestion-for-slowquery');
600
+ });
601
+ it('should not update suggestions if component unmounts during async operation', async () => {
602
+ let resolveCompletion;
603
+ const mockCompletion = vi.fn().mockImplementation(async () => new Promise((resolve) => {
604
+ resolveCompletion = resolve;
605
+ }));
606
+ const slashCommands = [
607
+ createTestCommand({
608
+ name: 'test',
609
+ description: 'Test command',
610
+ completion: mockCompletion,
611
+ }),
612
+ ];
613
+ const { unmount } = renderHook(() => useTestHarnessForSlashCompletion(true, '/test query', slashCommands, mockCommandContext));
614
+ // Start the async operation
615
+ await waitFor(() => {
616
+ expect(mockCompletion).toHaveBeenCalled();
617
+ });
618
+ // Unmount before completion resolves
619
+ unmount();
620
+ // Now resolve the completion
621
+ resolveCompletion(['late-suggestion']);
622
+ // Wait a bit to ensure any pending updates would have been processed
623
+ await new Promise((resolve) => setTimeout(resolve, 100));
624
+ // Since the component is unmounted, suggestions should remain empty
625
+ // and no state update errors should occur
626
+ expect(true).toBe(true); // Test passes if no errors are thrown
627
+ });
628
+ });
629
+ describe('Error Logging', () => {
630
+ it('should log errors to the console', async () => {
631
+ // Mock console.error to capture log calls
632
+ const consoleErrorSpy = vi
633
+ .spyOn(console, 'error')
634
+ .mockImplementation(() => { });
635
+ // Import the mocked AsyncFzf
636
+ const { AsyncFzf } = await import('fzf');
637
+ // Create a failing find method with error containing sensitive-looking data
638
+ const sensitiveError = new Error('Database connection failed: user=admin, pass=secret123');
639
+ const mockFind = vi.fn().mockRejectedValue(sensitiveError);
640
+ // Mock AsyncFzf to return an instance with failing find
641
+ vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
642
+ find: mockFind,
643
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
644
+ }));
645
+ const testCommands = [
646
+ createTestCommand({ name: 'test', description: 'Test command' }),
647
+ ];
648
+ const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/test', testCommands, mockCommandContext));
649
+ await waitFor(() => {
650
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
651
+ });
652
+ // Should get fallback suggestions
653
+ const labels = result.current.suggestions.map((s) => s.label);
654
+ expect(labels).toContain('test');
655
+ // Verify error logging occurred
656
+ await waitFor(() => {
657
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', sensitiveError);
658
+ });
659
+ consoleErrorSpy.mockRestore();
660
+ // Reset AsyncFzf mock to default behavior
661
+ vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
662
+ });
663
+ });
271
664
  });
272
665
  //# sourceMappingURL=useSlashCompletion.test.js.map