@google/gemini-cli 0.13.0-nightly.20251102.d7243fb8 → 0.13.0-preview.0

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 (166) hide show
  1. package/dist/google-gemini-cli-0.13.0-nightly.20251031.c89bc30d.tgz +0 -0
  2. package/dist/package.json +3 -3
  3. package/dist/src/commands/mcp/list.test.js +25 -21
  4. package/dist/src/commands/mcp/list.test.js.map +1 -1
  5. package/dist/src/config/config.js +11 -84
  6. package/dist/src/config/config.js.map +1 -1
  7. package/dist/src/config/config.test.js +13 -30
  8. package/dist/src/config/config.test.js.map +1 -1
  9. package/dist/src/config/extension-manager.d.ts +18 -6
  10. package/dist/src/config/extension-manager.js +25 -19
  11. package/dist/src/config/extension-manager.js.map +1 -1
  12. package/dist/src/config/extension.test.js +9 -9
  13. package/dist/src/config/extension.test.js.map +1 -1
  14. package/dist/src/config/extensions/extensionSettings.test.js +39 -43
  15. package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
  16. package/dist/src/config/extensions/github.test.js +133 -165
  17. package/dist/src/config/extensions/github.test.js.map +1 -1
  18. package/dist/src/config/keyBindings.d.ts +3 -0
  19. package/dist/src/config/keyBindings.js +29 -7
  20. package/dist/src/config/keyBindings.js.map +1 -1
  21. package/dist/src/config/keyBindings.test.js +17 -0
  22. package/dist/src/config/keyBindings.test.js.map +1 -1
  23. package/dist/src/config/policy.d.ts +0 -7
  24. package/dist/src/config/policy.js +10 -177
  25. package/dist/src/config/policy.js.map +1 -1
  26. package/dist/src/config/settings.js +1 -0
  27. package/dist/src/config/settings.js.map +1 -1
  28. package/dist/src/config/settingsSchema.d.ts +123 -22
  29. package/dist/src/config/settingsSchema.js +371 -21
  30. package/dist/src/config/settingsSchema.js.map +1 -1
  31. package/dist/src/config/settingsSchema.test.js +40 -1
  32. package/dist/src/config/settingsSchema.test.js.map +1 -1
  33. package/dist/src/gemini.js +15 -5
  34. package/dist/src/gemini.js.map +1 -1
  35. package/dist/src/gemini.test.js +2 -0
  36. package/dist/src/gemini.test.js.map +1 -1
  37. package/dist/src/generated/git-commit.d.ts +2 -2
  38. package/dist/src/generated/git-commit.js +2 -2
  39. package/dist/src/generated/git-commit.js.map +1 -1
  40. package/dist/src/nonInteractiveCli.js +68 -1
  41. package/dist/src/nonInteractiveCli.js.map +1 -1
  42. package/dist/src/services/BuiltinCommandLoader.js +4 -0
  43. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  44. package/dist/src/services/BuiltinCommandLoader.test.js +22 -0
  45. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  46. package/dist/src/services/McpPromptLoader.js +2 -2
  47. package/dist/src/services/McpPromptLoader.js.map +1 -1
  48. package/dist/src/services/McpPromptLoader.test.js +4 -2
  49. package/dist/src/services/McpPromptLoader.test.js.map +1 -1
  50. package/dist/src/test-utils/render.d.ts +2 -1
  51. package/dist/src/test-utils/render.js +3 -2
  52. package/dist/src/test-utils/render.js.map +1 -1
  53. package/dist/src/ui/AppContainer.js +32 -15
  54. package/dist/src/ui/AppContainer.js.map +1 -1
  55. package/dist/src/ui/AppContainer.test.js +160 -0
  56. package/dist/src/ui/AppContainer.test.js.map +1 -1
  57. package/dist/src/ui/commands/mcpCommand.js +14 -14
  58. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  59. package/dist/src/ui/commands/mcpCommand.test.js +4 -0
  60. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
  61. package/dist/src/ui/commands/policiesCommand.d.ts +7 -0
  62. package/dist/src/ui/commands/policiesCommand.js +59 -0
  63. package/dist/src/ui/commands/policiesCommand.js.map +1 -0
  64. package/dist/src/ui/commands/policiesCommand.test.js +83 -0
  65. package/dist/src/ui/commands/policiesCommand.test.js.map +1 -0
  66. package/dist/src/ui/components/Composer.js +1 -1
  67. package/dist/src/ui/components/Composer.js.map +1 -1
  68. package/dist/src/ui/components/Composer.test.js +4 -1
  69. package/dist/src/ui/components/Composer.test.js.map +1 -1
  70. package/dist/src/ui/components/ConfigInitDisplay.js +4 -6
  71. package/dist/src/ui/components/ConfigInitDisplay.js.map +1 -1
  72. package/dist/src/ui/components/InputPrompt.js +22 -2
  73. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  74. package/dist/src/ui/components/InputPrompt.test.js +70 -5
  75. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  76. package/dist/src/ui/components/MainContent.js +15 -4
  77. package/dist/src/ui/components/MainContent.js.map +1 -1
  78. package/dist/src/ui/components/Notifications.js +38 -5
  79. package/dist/src/ui/components/Notifications.js.map +1 -1
  80. package/dist/src/ui/components/SettingsDialog.js +32 -25
  81. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  82. package/dist/src/ui/components/ShellConfirmationDialog.js +1 -1
  83. package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -1
  84. package/dist/src/ui/components/messages/InfoMessage.js +1 -1
  85. package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
  86. package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
  87. package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
  88. package/dist/src/ui/components/messages/WarningMessage.js +2 -2
  89. package/dist/src/ui/components/messages/WarningMessage.js.map +1 -1
  90. package/dist/src/ui/components/shared/text-buffer.d.ts +1 -0
  91. package/dist/src/ui/components/shared/text-buffer.js +23 -0
  92. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  93. package/dist/src/ui/components/shared/text-buffer.test.js +246 -201
  94. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
  95. package/dist/src/ui/contexts/KeypressContext.js +182 -132
  96. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  97. package/dist/src/ui/contexts/KeypressContext.test.js +144 -8
  98. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  99. package/dist/src/ui/contexts/MouseContext.d.ts +21 -0
  100. package/dist/src/ui/contexts/MouseContext.js +89 -0
  101. package/dist/src/ui/contexts/MouseContext.js.map +1 -0
  102. package/dist/src/ui/contexts/MouseContext.test.js +164 -0
  103. package/dist/src/ui/contexts/MouseContext.test.js.map +1 -0
  104. package/dist/src/ui/hooks/slashCommandProcessor.test.js +70 -73
  105. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  106. package/dist/src/ui/hooks/useGeminiStream.test.js +135 -368
  107. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  108. package/dist/src/ui/hooks/useKeypress.test.js +17 -9
  109. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  110. package/dist/src/ui/hooks/useMouse.d.ts +17 -0
  111. package/dist/src/ui/hooks/useMouse.js +27 -0
  112. package/dist/src/ui/hooks/useMouse.js.map +1 -0
  113. package/dist/src/ui/hooks/useMouse.test.d.ts +6 -0
  114. package/dist/src/ui/hooks/useMouse.test.js +57 -0
  115. package/dist/src/ui/hooks/useMouse.test.js.map +1 -0
  116. package/dist/src/ui/hooks/useSelectionList.js +5 -4
  117. package/dist/src/ui/hooks/useSelectionList.js.map +1 -1
  118. package/dist/src/ui/hooks/useSelectionList.test.js +24 -3
  119. package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
  120. package/dist/src/ui/hooks/useToolScheduler.test.js +109 -200
  121. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  122. package/dist/src/ui/keyMatchers.test.js +27 -0
  123. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  124. package/dist/src/ui/themes/no-color.js +1 -0
  125. package/dist/src/ui/themes/no-color.js.map +1 -1
  126. package/dist/src/ui/themes/semantic-tokens.d.ts +1 -0
  127. package/dist/src/ui/themes/semantic-tokens.js +3 -0
  128. package/dist/src/ui/themes/semantic-tokens.js.map +1 -1
  129. package/dist/src/ui/themes/theme.d.ts +1 -0
  130. package/dist/src/ui/themes/theme.js +4 -0
  131. package/dist/src/ui/themes/theme.js.map +1 -1
  132. package/dist/src/ui/utils/InlineMarkdownRenderer.d.ts +1 -0
  133. package/dist/src/ui/utils/InlineMarkdownRenderer.js +11 -10
  134. package/dist/src/ui/utils/InlineMarkdownRenderer.js.map +1 -1
  135. package/dist/src/ui/utils/MarkdownDisplay.js +11 -9
  136. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  137. package/dist/src/ui/utils/input.d.ts +17 -0
  138. package/dist/src/ui/utils/input.js +51 -0
  139. package/dist/src/ui/utils/input.js.map +1 -0
  140. package/dist/src/ui/utils/input.test.d.ts +6 -0
  141. package/dist/src/ui/utils/input.test.js +44 -0
  142. package/dist/src/ui/utils/input.test.js.map +1 -0
  143. package/dist/src/ui/utils/kittyProtocolDetector.js +13 -4
  144. package/dist/src/ui/utils/kittyProtocolDetector.js.map +1 -1
  145. package/dist/src/ui/utils/mouse.d.ts +31 -0
  146. package/dist/src/ui/utils/mouse.js +164 -0
  147. package/dist/src/ui/utils/mouse.js.map +1 -0
  148. package/dist/src/ui/utils/mouse.test.d.ts +6 -0
  149. package/dist/src/ui/utils/mouse.test.js +131 -0
  150. package/dist/src/ui/utils/mouse.test.js.map +1 -0
  151. package/dist/src/utils/events.d.ts +11 -2
  152. package/dist/src/utils/events.js +1 -0
  153. package/dist/src/utils/events.js.map +1 -1
  154. package/dist/src/utils/sandbox.js +16 -18
  155. package/dist/src/utils/sandbox.js.map +1 -1
  156. package/dist/tsconfig.tsbuildinfo +1 -1
  157. package/package.json +4 -4
  158. package/dist/src/config/policy-toml-loader.d.ts +0 -46
  159. package/dist/src/config/policy-toml-loader.js +0 -314
  160. package/dist/src/config/policy-toml-loader.js.map +0 -1
  161. package/dist/src/config/policy-toml-loader.test.js +0 -626
  162. package/dist/src/config/policy-toml-loader.test.js.map +0 -1
  163. package/dist/src/config/policy.test.js +0 -1058
  164. package/dist/src/config/policy.test.js.map +0 -1
  165. /package/dist/src/{config/policy-toml-loader.test.d.ts → ui/commands/policiesCommand.test.d.ts} +0 -0
  166. /package/dist/src/{config/policy.test.d.ts → ui/contexts/MouseContext.test.d.ts} +0 -0
@@ -265,6 +265,49 @@ describe('useGeminiStream', () => {
265
265
  client,
266
266
  };
267
267
  };
268
+ // Helper to create mock tool calls - reduces boilerplate
269
+ const createMockToolCall = (toolName, callId, confirmationType, mockOnConfirm, status = 'awaiting_approval') => ({
270
+ request: {
271
+ callId,
272
+ name: toolName,
273
+ args: {},
274
+ isClientInitiated: false,
275
+ prompt_id: 'prompt-id-1',
276
+ },
277
+ status: status,
278
+ responseSubmittedToGemini: false,
279
+ confirmationDetails: confirmationType === 'edit'
280
+ ? {
281
+ type: 'edit',
282
+ title: 'Confirm Edit',
283
+ onConfirm: mockOnConfirm,
284
+ fileName: 'file.txt',
285
+ filePath: '/test/file.txt',
286
+ fileDiff: 'fake diff',
287
+ originalContent: 'old',
288
+ newContent: 'new',
289
+ }
290
+ : {
291
+ type: 'info',
292
+ title: `${toolName} confirmation`,
293
+ onConfirm: mockOnConfirm,
294
+ prompt: `Execute ${toolName}?`,
295
+ },
296
+ tool: {
297
+ name: toolName,
298
+ displayName: toolName,
299
+ description: `${toolName} description`,
300
+ build: vi.fn(),
301
+ },
302
+ invocation: {
303
+ getDescription: () => 'Mock description',
304
+ },
305
+ });
306
+ // Helper to render hook with default parameters - reduces boilerplate
307
+ const renderHookWithDefaults = (options = {}) => {
308
+ const { shellModeActive = false, onCancelSubmit = () => { }, setShellInputFocused = () => { }, performMemoryRefresh = () => Promise.resolve(), onAuthError = () => { }, onEditorClose = () => { }, setModelSwitched = vi.fn(), modelSwitched = false, } = options;
309
+ return renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, shellModeActive, () => 'vscode', onAuthError, performMemoryRefresh, modelSwitched, setModelSwitched, onEditorClose, onCancelSubmit, setShellInputFocused, 80, 24));
310
+ };
268
311
  it('should not submit tool responses if not all tool calls are completed', () => {
269
312
  const toolCalls = [
270
313
  {
@@ -999,62 +1042,8 @@ describe('useGeminiStream', () => {
999
1042
  it('should auto-approve all pending tool calls when switching to YOLO mode', async () => {
1000
1043
  const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
1001
1044
  const awaitingApprovalToolCalls = [
1002
- {
1003
- request: {
1004
- callId: 'call1',
1005
- name: 'replace',
1006
- args: { old_string: 'old', new_string: 'new' },
1007
- isClientInitiated: false,
1008
- prompt_id: 'prompt-id-1',
1009
- },
1010
- status: 'awaiting_approval',
1011
- responseSubmittedToGemini: false,
1012
- confirmationDetails: {
1013
- type: 'edit',
1014
- title: 'Confirm Edit',
1015
- onConfirm: mockOnConfirm,
1016
- fileName: 'file.txt',
1017
- filePath: '/test/file.txt',
1018
- fileDiff: 'fake diff',
1019
- originalContent: 'old',
1020
- newContent: 'new',
1021
- },
1022
- tool: {
1023
- name: 'replace',
1024
- displayName: 'replace',
1025
- description: 'Replace text',
1026
- build: vi.fn(),
1027
- },
1028
- invocation: {
1029
- getDescription: () => 'Mock description',
1030
- },
1031
- },
1032
- {
1033
- request: {
1034
- callId: 'call2',
1035
- name: 'read_file',
1036
- args: { path: '/test/file.txt' },
1037
- isClientInitiated: false,
1038
- prompt_id: 'prompt-id-1',
1039
- },
1040
- status: 'awaiting_approval',
1041
- responseSubmittedToGemini: false,
1042
- confirmationDetails: {
1043
- type: 'info',
1044
- title: 'Read File',
1045
- onConfirm: mockOnConfirm,
1046
- prompt: 'Read /test/file.txt?',
1047
- },
1048
- tool: {
1049
- name: 'read_file',
1050
- displayName: 'read_file',
1051
- description: 'Read file',
1052
- build: vi.fn(),
1053
- },
1054
- invocation: {
1055
- getDescription: () => 'Mock description',
1056
- },
1057
- },
1045
+ createMockToolCall('replace', 'call1', 'edit', mockOnConfirm),
1046
+ createMockToolCall('read_file', 'call2', 'info', mockOnConfirm),
1058
1047
  ];
1059
1048
  const { result } = renderTestHook(awaitingApprovalToolCalls);
1060
1049
  await act(async () => {
@@ -1062,109 +1051,23 @@ describe('useGeminiStream', () => {
1062
1051
  });
1063
1052
  // Both tool calls should be auto-approved
1064
1053
  expect(mockOnConfirm).toHaveBeenCalledTimes(2);
1065
- expect(mockOnConfirm).toHaveBeenNthCalledWith(1, ToolConfirmationOutcome.ProceedOnce);
1066
- expect(mockOnConfirm).toHaveBeenNthCalledWith(2, ToolConfirmationOutcome.ProceedOnce);
1054
+ expect(mockOnConfirm).toHaveBeenCalledWith(ToolConfirmationOutcome.ProceedOnce);
1067
1055
  });
1068
1056
  it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => {
1069
1057
  const mockOnConfirmReplace = vi.fn().mockResolvedValue(undefined);
1070
1058
  const mockOnConfirmWrite = vi.fn().mockResolvedValue(undefined);
1071
1059
  const mockOnConfirmRead = vi.fn().mockResolvedValue(undefined);
1072
1060
  const awaitingApprovalToolCalls = [
1073
- {
1074
- request: {
1075
- callId: 'call1',
1076
- name: 'replace',
1077
- args: { old_string: 'old', new_string: 'new' },
1078
- isClientInitiated: false,
1079
- prompt_id: 'prompt-id-1',
1080
- },
1081
- status: 'awaiting_approval',
1082
- responseSubmittedToGemini: false,
1083
- confirmationDetails: {
1084
- type: 'edit',
1085
- title: 'Confirm Edit',
1086
- onConfirm: mockOnConfirmReplace,
1087
- fileName: 'file.txt',
1088
- filePath: '/test/file.txt',
1089
- fileDiff: 'fake diff',
1090
- originalContent: 'old',
1091
- newContent: 'new',
1092
- },
1093
- tool: {
1094
- name: 'replace',
1095
- displayName: 'replace',
1096
- description: 'Replace text',
1097
- build: vi.fn(),
1098
- },
1099
- invocation: {
1100
- getDescription: () => 'Mock description',
1101
- },
1102
- },
1103
- {
1104
- request: {
1105
- callId: 'call2',
1106
- name: 'write_file',
1107
- args: { path: '/test/new.txt', content: 'content' },
1108
- isClientInitiated: false,
1109
- prompt_id: 'prompt-id-1',
1110
- },
1111
- status: 'awaiting_approval',
1112
- responseSubmittedToGemini: false,
1113
- confirmationDetails: {
1114
- type: 'edit',
1115
- title: 'Confirm Edit',
1116
- onConfirm: mockOnConfirmWrite,
1117
- fileName: 'new.txt',
1118
- filePath: '/test/new.txt',
1119
- fileDiff: 'fake diff',
1120
- originalContent: null,
1121
- newContent: 'content',
1122
- },
1123
- tool: {
1124
- name: 'write_file',
1125
- displayName: 'write_file',
1126
- description: 'Write file',
1127
- build: vi.fn(),
1128
- },
1129
- invocation: {
1130
- getDescription: () => 'Mock description',
1131
- },
1132
- },
1133
- {
1134
- request: {
1135
- callId: 'call3',
1136
- name: 'read_file',
1137
- args: { path: '/test/file.txt' },
1138
- isClientInitiated: false,
1139
- prompt_id: 'prompt-id-1',
1140
- },
1141
- status: 'awaiting_approval',
1142
- responseSubmittedToGemini: false,
1143
- confirmationDetails: {
1144
- type: 'info',
1145
- title: 'Read File',
1146
- onConfirm: mockOnConfirmRead,
1147
- prompt: 'Read /test/file.txt?',
1148
- },
1149
- tool: {
1150
- name: 'read_file',
1151
- displayName: 'read_file',
1152
- description: 'Read file',
1153
- build: vi.fn(),
1154
- },
1155
- invocation: {
1156
- getDescription: () => 'Mock description',
1157
- },
1158
- },
1061
+ createMockToolCall('replace', 'call1', 'edit', mockOnConfirmReplace),
1062
+ createMockToolCall('write_file', 'call2', 'edit', mockOnConfirmWrite),
1063
+ createMockToolCall('read_file', 'call3', 'info', mockOnConfirmRead),
1159
1064
  ];
1160
1065
  const { result } = renderTestHook(awaitingApprovalToolCalls);
1161
1066
  await act(async () => {
1162
1067
  await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
1163
1068
  });
1164
1069
  // Only replace and write_file should be auto-approved
1165
- expect(mockOnConfirmReplace).toHaveBeenCalledTimes(1);
1166
1070
  expect(mockOnConfirmReplace).toHaveBeenCalledWith(ToolConfirmationOutcome.ProceedOnce);
1167
- expect(mockOnConfirmWrite).toHaveBeenCalledTimes(1);
1168
1071
  expect(mockOnConfirmWrite).toHaveBeenCalledWith(ToolConfirmationOutcome.ProceedOnce);
1169
1072
  // read_file should not be auto-approved
1170
1073
  expect(mockOnConfirmRead).not.toHaveBeenCalled();
@@ -1172,36 +1075,7 @@ describe('useGeminiStream', () => {
1172
1075
  it('should not auto-approve any tools when switching to REQUIRE_CONFIRMATION mode', async () => {
1173
1076
  const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
1174
1077
  const awaitingApprovalToolCalls = [
1175
- {
1176
- request: {
1177
- callId: 'call1',
1178
- name: 'replace',
1179
- args: { old_string: 'old', new_string: 'new' },
1180
- isClientInitiated: false,
1181
- prompt_id: 'prompt-id-1',
1182
- },
1183
- status: 'awaiting_approval',
1184
- responseSubmittedToGemini: false,
1185
- confirmationDetails: {
1186
- type: 'edit',
1187
- title: 'Confirm Edit',
1188
- onConfirm: mockOnConfirm,
1189
- fileName: 'file.txt',
1190
- filePath: '/test/file.txt',
1191
- fileDiff: 'fake diff',
1192
- originalContent: 'old',
1193
- newContent: 'new',
1194
- },
1195
- tool: {
1196
- name: 'replace',
1197
- displayName: 'replace',
1198
- description: 'Replace text',
1199
- build: vi.fn(),
1200
- },
1201
- invocation: {
1202
- getDescription: () => 'Mock description',
1203
- },
1204
- },
1078
+ createMockToolCall('replace', 'call1', 'edit', mockOnConfirm),
1205
1079
  ];
1206
1080
  const { result } = renderTestHook(awaitingApprovalToolCalls);
1207
1081
  await act(async () => {
@@ -1219,74 +1093,16 @@ describe('useGeminiStream', () => {
1219
1093
  .fn()
1220
1094
  .mockRejectedValue(new Error('Approval failed'));
1221
1095
  const awaitingApprovalToolCalls = [
1222
- {
1223
- request: {
1224
- callId: 'call1',
1225
- name: 'replace',
1226
- args: { old_string: 'old', new_string: 'new' },
1227
- isClientInitiated: false,
1228
- prompt_id: 'prompt-id-1',
1229
- },
1230
- status: 'awaiting_approval',
1231
- responseSubmittedToGemini: false,
1232
- confirmationDetails: {
1233
- type: 'edit',
1234
- title: 'Confirm Edit',
1235
- onConfirm: mockOnConfirmSuccess,
1236
- fileName: 'file.txt',
1237
- filePath: '/test/file.txt',
1238
- fileDiff: 'fake diff',
1239
- originalContent: 'old',
1240
- newContent: 'new',
1241
- },
1242
- tool: {
1243
- name: 'replace',
1244
- displayName: 'replace',
1245
- description: 'Replace text',
1246
- build: vi.fn(),
1247
- },
1248
- invocation: {
1249
- getDescription: () => 'Mock description',
1250
- },
1251
- },
1252
- {
1253
- request: {
1254
- callId: 'call2',
1255
- name: 'write_file',
1256
- args: { path: '/test/file.txt', content: 'content' },
1257
- isClientInitiated: false,
1258
- prompt_id: 'prompt-id-1',
1259
- },
1260
- status: 'awaiting_approval',
1261
- responseSubmittedToGemini: false,
1262
- confirmationDetails: {
1263
- type: 'edit',
1264
- title: 'Confirm Edit',
1265
- onConfirm: mockOnConfirmError,
1266
- fileName: 'file.txt',
1267
- filePath: '/test/file.txt',
1268
- fileDiff: 'fake diff',
1269
- originalContent: null,
1270
- newContent: 'content',
1271
- },
1272
- tool: {
1273
- name: 'write_file',
1274
- displayName: 'write_file',
1275
- description: 'Write file',
1276
- build: vi.fn(),
1277
- },
1278
- invocation: {
1279
- getDescription: () => 'Mock description',
1280
- },
1281
- },
1096
+ createMockToolCall('replace', 'call1', 'edit', mockOnConfirmSuccess),
1097
+ createMockToolCall('write_file', 'call2', 'edit', mockOnConfirmError),
1282
1098
  ];
1283
1099
  const { result } = renderTestHook(awaitingApprovalToolCalls);
1284
1100
  await act(async () => {
1285
1101
  await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
1286
1102
  });
1287
1103
  // Both confirmation methods should be called
1288
- expect(mockOnConfirmSuccess).toHaveBeenCalledTimes(1);
1289
- expect(mockOnConfirmError).toHaveBeenCalledTimes(1);
1104
+ expect(mockOnConfirmSuccess).toHaveBeenCalled();
1105
+ expect(mockOnConfirmError).toHaveBeenCalled();
1290
1106
  // Error should be logged
1291
1107
  expect(debuggerSpy).toHaveBeenCalledWith('Failed to auto-approve tool call call2:', expect.any(Error));
1292
1108
  debuggerSpy.mockRestore();
@@ -1456,53 +1272,37 @@ describe('useGeminiStream', () => {
1456
1272
  beforeEach(() => {
1457
1273
  vi.mocked(tokenLimit).mockReturnValue(100);
1458
1274
  });
1459
- it('should add message without suggestion when remaining tokens are > 75% of limit', async () => {
1460
- // Setup mock to return a stream with ContextWindowWillOverflow event
1461
- // Limit is 100, remaining is 80 (> 75)
1462
- mockSendMessageStream.mockReturnValue((async function* () {
1463
- yield {
1464
- type: ServerGeminiEventType.ContextWindowWillOverflow,
1465
- value: {
1466
- estimatedRequestTokenCount: 20,
1467
- remainingTokenCount: 80,
1468
- },
1469
- };
1470
- })());
1471
- const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1472
- // Submit a query
1473
- await act(async () => {
1474
- await result.current.submitQuery('Test overflow');
1475
- });
1476
- // Check that the message was added without suggestion
1477
- await waitFor(() => {
1478
- expect(mockAddItem).toHaveBeenCalledWith({
1479
- type: 'info',
1480
- text: `Sending this message (20 tokens) might exceed the remaining context window limit (80 tokens).`,
1481
- }, expect.any(Number));
1482
- });
1483
- });
1484
- it('should add message with suggestion when remaining tokens are < 75% of limit', async () => {
1485
- // Setup mock to return a stream with ContextWindowWillOverflow event
1486
- // Limit is 100, remaining is 70 (< 75)
1275
+ it.each([
1276
+ {
1277
+ name: 'without suggestion when remaining tokens are > 75% of limit',
1278
+ requestTokens: 20,
1279
+ remainingTokens: 80,
1280
+ expectedMessage: 'Sending this message (20 tokens) might exceed the remaining context window limit (80 tokens).',
1281
+ },
1282
+ {
1283
+ name: 'with suggestion when remaining tokens are < 75% of limit',
1284
+ requestTokens: 30,
1285
+ remainingTokens: 70,
1286
+ expectedMessage: 'Sending this message (30 tokens) might exceed the remaining context window limit (70 tokens). Please try reducing the size of your message or use the `/compress` command to compress the chat history.',
1287
+ },
1288
+ ])('should add message $name', async ({ requestTokens, remainingTokens, expectedMessage }) => {
1487
1289
  mockSendMessageStream.mockReturnValue((async function* () {
1488
1290
  yield {
1489
1291
  type: ServerGeminiEventType.ContextWindowWillOverflow,
1490
1292
  value: {
1491
- estimatedRequestTokenCount: 30,
1492
- remainingTokenCount: 70,
1293
+ estimatedRequestTokenCount: requestTokens,
1294
+ remainingTokenCount: remainingTokens,
1493
1295
  },
1494
1296
  };
1495
1297
  })());
1496
- const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1497
- // Submit a query
1298
+ const { result } = renderHookWithDefaults();
1498
1299
  await act(async () => {
1499
1300
  await result.current.submitQuery('Test overflow');
1500
1301
  });
1501
- // Check that the message was added with suggestion
1502
1302
  await waitFor(() => {
1503
1303
  expect(mockAddItem).toHaveBeenCalledWith({
1504
1304
  type: 'info',
1505
- text: `Sending this message (30 tokens) might exceed the remaining context window limit (70 tokens). Please try reducing the size of your message or use the \`/compress\` command to compress the chat history.`,
1305
+ text: expectedMessage,
1506
1306
  }, expect.any(Number));
1507
1307
  });
1508
1308
  });
@@ -1529,112 +1329,71 @@ describe('useGeminiStream', () => {
1529
1329
  expect(onCancelSubmitSpy).toHaveBeenCalled();
1530
1330
  });
1531
1331
  });
1532
- it('should not add message for STOP finish reason', async () => {
1533
- // Setup mock to return a stream with STOP finish reason
1534
- mockSendMessageStream.mockReturnValue((async function* () {
1535
- yield {
1536
- type: ServerGeminiEventType.Content,
1537
- value: 'Complete response',
1538
- };
1539
- yield {
1540
- type: ServerGeminiEventType.Finished,
1541
- value: { reason: 'STOP', usageMetadata: undefined },
1542
- };
1543
- })());
1544
- const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1545
- // Submit a query
1546
- await act(async () => {
1547
- await result.current.submitQuery('Test normal completion');
1548
- });
1549
- // Wait a bit to ensure no message is added
1550
- await new Promise((resolve) => setTimeout(resolve, 100));
1551
- // Check that no info message was added for STOP
1552
- const infoMessages = mockAddItem.mock.calls.filter((call) => call[0].type === 'info');
1553
- expect(infoMessages).toHaveLength(0);
1554
- });
1555
- it('should not add message for FINISH_REASON_UNSPECIFIED', async () => {
1556
- // Setup mock to return a stream with FINISH_REASON_UNSPECIFIED
1332
+ it.each([
1333
+ {
1334
+ reason: 'STOP',
1335
+ shouldAddMessage: false,
1336
+ },
1337
+ {
1338
+ reason: 'FINISH_REASON_UNSPECIFIED',
1339
+ shouldAddMessage: false,
1340
+ },
1341
+ {
1342
+ reason: 'SAFETY',
1343
+ message: '⚠️ Response stopped due to safety reasons.',
1344
+ },
1345
+ {
1346
+ reason: 'RECITATION',
1347
+ message: '⚠️ Response stopped due to recitation policy.',
1348
+ },
1349
+ {
1350
+ reason: 'LANGUAGE',
1351
+ message: '⚠️ Response stopped due to unsupported language.',
1352
+ },
1353
+ {
1354
+ reason: 'BLOCKLIST',
1355
+ message: '⚠️ Response stopped due to forbidden terms.',
1356
+ },
1357
+ {
1358
+ reason: 'PROHIBITED_CONTENT',
1359
+ message: '⚠️ Response stopped due to prohibited content.',
1360
+ },
1361
+ {
1362
+ reason: 'SPII',
1363
+ message: '⚠️ Response stopped due to sensitive personally identifiable information.',
1364
+ },
1365
+ {
1366
+ reason: 'OTHER',
1367
+ message: '⚠️ Response stopped for other reasons.',
1368
+ },
1369
+ {
1370
+ reason: 'MALFORMED_FUNCTION_CALL',
1371
+ message: '⚠️ Response stopped due to malformed function call.',
1372
+ },
1373
+ {
1374
+ reason: 'IMAGE_SAFETY',
1375
+ message: '⚠️ Response stopped due to image safety violations.',
1376
+ },
1377
+ {
1378
+ reason: 'UNEXPECTED_TOOL_CALL',
1379
+ message: '⚠️ Response stopped due to unexpected tool call.',
1380
+ },
1381
+ ])('should handle $reason finish reason correctly', async ({ reason, shouldAddMessage = true, message }) => {
1557
1382
  mockSendMessageStream.mockReturnValue((async function* () {
1558
1383
  yield {
1559
1384
  type: ServerGeminiEventType.Content,
1560
- value: 'Response with unspecified finish',
1385
+ value: `Response for ${reason}`,
1561
1386
  };
1562
1387
  yield {
1563
1388
  type: ServerGeminiEventType.Finished,
1564
- value: {
1565
- reason: 'FINISH_REASON_UNSPECIFIED',
1566
- usageMetadata: undefined,
1567
- },
1389
+ value: { reason, usageMetadata: undefined },
1568
1390
  };
1569
1391
  })());
1570
- const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1571
- // Submit a query
1392
+ const { result } = renderHookWithDefaults();
1572
1393
  await act(async () => {
1573
- await result.current.submitQuery('Test unspecified finish');
1394
+ await result.current.submitQuery(`Test ${reason}`);
1574
1395
  });
1575
- // Wait a bit to ensure no message is added
1576
- await new Promise((resolve) => setTimeout(resolve, 100));
1577
- // Check that no info message was added
1578
- const infoMessages = mockAddItem.mock.calls.filter((call) => call[0].type === 'info');
1579
- expect(infoMessages).toHaveLength(0);
1580
- });
1581
- it('should add appropriate messages for other finish reasons', async () => {
1582
- const testCases = [
1583
- {
1584
- reason: 'SAFETY',
1585
- message: '⚠️ Response stopped due to safety reasons.',
1586
- },
1587
- {
1588
- reason: 'RECITATION',
1589
- message: '⚠️ Response stopped due to recitation policy.',
1590
- },
1591
- {
1592
- reason: 'LANGUAGE',
1593
- message: '⚠️ Response stopped due to unsupported language.',
1594
- },
1595
- {
1596
- reason: 'BLOCKLIST',
1597
- message: '⚠️ Response stopped due to forbidden terms.',
1598
- },
1599
- {
1600
- reason: 'PROHIBITED_CONTENT',
1601
- message: '⚠️ Response stopped due to prohibited content.',
1602
- },
1603
- {
1604
- reason: 'SPII',
1605
- message: '⚠️ Response stopped due to sensitive personally identifiable information.',
1606
- },
1607
- { reason: 'OTHER', message: '⚠️ Response stopped for other reasons.' },
1608
- {
1609
- reason: 'MALFORMED_FUNCTION_CALL',
1610
- message: '⚠️ Response stopped due to malformed function call.',
1611
- },
1612
- {
1613
- reason: 'IMAGE_SAFETY',
1614
- message: '⚠️ Response stopped due to image safety violations.',
1615
- },
1616
- {
1617
- reason: 'UNEXPECTED_TOOL_CALL',
1618
- message: '⚠️ Response stopped due to unexpected tool call.',
1619
- },
1620
- ];
1621
- for (const { reason, message } of testCases) {
1622
- // Reset mocks for each test case
1623
- mockAddItem.mockClear();
1624
- mockSendMessageStream.mockReturnValue((async function* () {
1625
- yield {
1626
- type: ServerGeminiEventType.Content,
1627
- value: `Response for ${reason}`,
1628
- };
1629
- yield {
1630
- type: ServerGeminiEventType.Finished,
1631
- value: { reason, usageMetadata: undefined },
1632
- };
1633
- })());
1634
- const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, vi.fn(), 80, 24));
1635
- await act(async () => {
1636
- await result.current.submitQuery(`Test ${reason}`);
1637
- });
1396
+ if (shouldAddMessage) {
1638
1397
  await waitFor(() => {
1639
1398
  expect(mockAddItem).toHaveBeenCalledWith({
1640
1399
  type: 'info',
@@ -1642,6 +1401,14 @@ describe('useGeminiStream', () => {
1642
1401
  }, expect.any(Number));
1643
1402
  });
1644
1403
  }
1404
+ else {
1405
+ // Verify state returns to idle without any info messages
1406
+ await waitFor(() => {
1407
+ expect(result.current.streamingState).toBe(StreamingState.Idle);
1408
+ });
1409
+ const infoMessages = mockAddItem.mock.calls.filter((call) => call[0].type === 'info');
1410
+ expect(infoMessages).toHaveLength(0);
1411
+ }
1645
1412
  });
1646
1413
  });
1647
1414
  it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {