@google/gemini-cli-core 0.7.0-nightly.20250912.68035591 → 0.7.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 (246) hide show
  1. package/dist/index.d.ts +5 -4
  2. package/dist/index.js +5 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/code_assist/converter.d.ts +1 -0
  5. package/dist/src/code_assist/converter.js +1 -0
  6. package/dist/src/code_assist/converter.js.map +1 -1
  7. package/dist/src/code_assist/converter.test.js +10 -0
  8. package/dist/src/code_assist/converter.test.js.map +1 -1
  9. package/dist/src/code_assist/oauth-credential-storage.d.ts +5 -7
  10. package/dist/src/code_assist/oauth-credential-storage.js +5 -8
  11. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -1
  12. package/dist/src/code_assist/oauth-credential-storage.test.js +35 -33
  13. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
  14. package/dist/src/code_assist/oauth2.js +28 -2
  15. package/dist/src/code_assist/oauth2.js.map +1 -1
  16. package/dist/src/code_assist/oauth2.test.js +674 -536
  17. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  18. package/dist/src/config/config.d.ts +19 -0
  19. package/dist/src/config/config.js +48 -6
  20. package/dist/src/config/config.js.map +1 -1
  21. package/dist/src/config/config.test.js +93 -1
  22. package/dist/src/config/config.test.js.map +1 -1
  23. package/dist/src/config/models.d.ts +1 -0
  24. package/dist/src/config/models.js +1 -0
  25. package/dist/src/config/models.js.map +1 -1
  26. package/dist/src/core/baseLlmClient.d.ts +1 -0
  27. package/dist/src/core/baseLlmClient.js +24 -0
  28. package/dist/src/core/baseLlmClient.js.map +1 -1
  29. package/dist/src/core/baseLlmClient.test.js +63 -0
  30. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  31. package/dist/src/core/client.d.ts +3 -4
  32. package/dist/src/core/client.js +62 -145
  33. package/dist/src/core/client.js.map +1 -1
  34. package/dist/src/core/client.test.js +119 -202
  35. package/dist/src/core/client.test.js.map +1 -1
  36. package/dist/src/core/coreToolScheduler.test.js +9 -0
  37. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  38. package/dist/src/core/geminiChat.d.ts +16 -11
  39. package/dist/src/core/geminiChat.js +124 -150
  40. package/dist/src/core/geminiChat.js.map +1 -1
  41. package/dist/src/core/geminiChat.test.js +342 -204
  42. package/dist/src/core/geminiChat.test.js.map +1 -1
  43. package/dist/src/core/loggingContentGenerator.js +5 -5
  44. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  45. package/dist/src/core/nonInteractiveToolExecutor.test.js +1 -0
  46. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  47. package/dist/src/generated/git-commit.d.ts +2 -2
  48. package/dist/src/generated/git-commit.js +2 -2
  49. package/dist/src/generated/git-commit.js.map +1 -1
  50. package/dist/src/ide/constants.d.ts +1 -0
  51. package/dist/src/ide/constants.js +1 -0
  52. package/dist/src/ide/constants.js.map +1 -1
  53. package/dist/src/ide/detect-ide.d.ts +44 -14
  54. package/dist/src/ide/detect-ide.js +29 -69
  55. package/dist/src/ide/detect-ide.js.map +1 -1
  56. package/dist/src/ide/detect-ide.test.js +29 -46
  57. package/dist/src/ide/detect-ide.test.js.map +1 -1
  58. package/dist/src/ide/ide-client.d.ts +28 -17
  59. package/dist/src/ide/ide-client.js +125 -57
  60. package/dist/src/ide/ide-client.js.map +1 -1
  61. package/dist/src/ide/ide-client.test.js +44 -10
  62. package/dist/src/ide/ide-client.test.js.map +1 -1
  63. package/dist/src/ide/ide-installer.d.ts +2 -2
  64. package/dist/src/ide/ide-installer.js +15 -11
  65. package/dist/src/ide/ide-installer.js.map +1 -1
  66. package/dist/src/ide/ide-installer.test.js +30 -12
  67. package/dist/src/ide/ide-installer.test.js.map +1 -1
  68. package/dist/src/ide/ideContext.d.ts +0 -93
  69. package/dist/src/ide/ideContext.js +0 -45
  70. package/dist/src/ide/ideContext.js.map +1 -1
  71. package/dist/src/ide/types.d.ts +141 -0
  72. package/dist/src/ide/types.js +73 -0
  73. package/dist/src/ide/types.js.map +1 -1
  74. package/dist/src/index.d.ts +4 -2
  75. package/dist/src/index.js +4 -2
  76. package/dist/src/index.js.map +1 -1
  77. package/dist/src/mcp/oauth-provider.d.ts +4 -1
  78. package/dist/src/mcp/oauth-provider.js +32 -26
  79. package/dist/src/mcp/oauth-provider.js.map +1 -1
  80. package/dist/src/mcp/oauth-token-storage.d.ts +2 -0
  81. package/dist/src/mcp/oauth-token-storage.js +25 -0
  82. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  83. package/dist/src/mcp/oauth-token-storage.test.js +251 -160
  84. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  85. package/dist/src/mcp/token-storage/index.d.ts +11 -0
  86. package/dist/src/mcp/token-storage/index.js +12 -0
  87. package/dist/src/mcp/token-storage/index.js.map +1 -0
  88. package/dist/src/policy/policy-engine.js +11 -2
  89. package/dist/src/policy/policy-engine.js.map +1 -1
  90. package/dist/src/policy/policy-engine.test.js +45 -0
  91. package/dist/src/policy/policy-engine.test.js.map +1 -1
  92. package/dist/src/routing/modelRouterService.js +37 -3
  93. package/dist/src/routing/modelRouterService.js.map +1 -1
  94. package/dist/src/routing/modelRouterService.test.js +37 -11
  95. package/dist/src/routing/modelRouterService.test.js.map +1 -1
  96. package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
  97. package/dist/src/routing/strategies/classifierStrategy.js +173 -0
  98. package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
  99. package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
  100. package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
  101. package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
  102. package/dist/src/routing/strategies/compositeStrategy.js +4 -3
  103. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -1
  104. package/dist/src/routing/strategies/overrideStrategy.js +13 -12
  105. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -1
  106. package/dist/src/routing/strategies/overrideStrategy.test.js +3 -2
  107. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -1
  108. package/dist/src/services/chatRecordingService.d.ts +2 -1
  109. package/dist/src/services/chatRecordingService.js +3 -3
  110. package/dist/src/services/chatRecordingService.js.map +1 -1
  111. package/dist/src/services/chatRecordingService.test.js +8 -3
  112. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  113. package/dist/src/services/gitService.js +9 -12
  114. package/dist/src/services/gitService.js.map +1 -1
  115. package/dist/src/services/gitService.test.js +10 -20
  116. package/dist/src/services/gitService.test.js.map +1 -1
  117. package/dist/src/services/loopDetectionService.js +23 -18
  118. package/dist/src/services/loopDetectionService.js.map +1 -1
  119. package/dist/src/services/loopDetectionService.test.js +27 -13
  120. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  121. package/dist/src/services/shellExecutionService.js +30 -15
  122. package/dist/src/services/shellExecutionService.js.map +1 -1
  123. package/dist/src/services/shellExecutionService.test.js +43 -11
  124. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  125. package/dist/src/telemetry/activity-detector.d.ts +41 -0
  126. package/dist/src/telemetry/activity-detector.js +61 -0
  127. package/dist/src/telemetry/activity-detector.js.map +1 -0
  128. package/dist/src/telemetry/activity-detector.test.d.ts +6 -0
  129. package/dist/src/telemetry/activity-detector.test.js +136 -0
  130. package/dist/src/telemetry/activity-detector.test.js.map +1 -0
  131. package/dist/src/telemetry/activity-types.d.ts +19 -0
  132. package/dist/src/telemetry/activity-types.js +21 -0
  133. package/dist/src/telemetry/activity-types.js.map +1 -0
  134. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +14 -2
  135. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +109 -3
  136. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  137. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +63 -5
  138. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  139. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +12 -1
  140. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +27 -0
  141. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  142. package/dist/src/telemetry/config.d.ts +31 -0
  143. package/dist/src/telemetry/config.js +76 -0
  144. package/dist/src/telemetry/config.js.map +1 -0
  145. package/dist/src/telemetry/config.test.d.ts +6 -0
  146. package/dist/src/telemetry/config.test.js +124 -0
  147. package/dist/src/telemetry/config.test.js.map +1 -0
  148. package/dist/src/telemetry/constants.d.ts +9 -0
  149. package/dist/src/telemetry/constants.js +9 -0
  150. package/dist/src/telemetry/constants.js.map +1 -1
  151. package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
  152. package/dist/src/telemetry/gcp-exporters.js +117 -0
  153. package/dist/src/telemetry/gcp-exporters.js.map +1 -0
  154. package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
  155. package/dist/src/telemetry/gcp-exporters.test.js +318 -0
  156. package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
  157. package/dist/src/telemetry/index.d.ts +5 -1
  158. package/dist/src/telemetry/index.js +5 -1
  159. package/dist/src/telemetry/index.js.map +1 -1
  160. package/dist/src/telemetry/loggers.d.ts +8 -1
  161. package/dist/src/telemetry/loggers.js +111 -2
  162. package/dist/src/telemetry/loggers.js.map +1 -1
  163. package/dist/src/telemetry/loggers.test.js +207 -4
  164. package/dist/src/telemetry/loggers.test.js.map +1 -1
  165. package/dist/src/telemetry/metrics.d.ts +3 -0
  166. package/dist/src/telemetry/metrics.js +43 -1
  167. package/dist/src/telemetry/metrics.js.map +1 -1
  168. package/dist/src/telemetry/metrics.test.js +42 -0
  169. package/dist/src/telemetry/metrics.test.js.map +1 -1
  170. package/dist/src/telemetry/sdk.js +20 -2
  171. package/dist/src/telemetry/sdk.js.map +1 -1
  172. package/dist/src/telemetry/sdk.test.js +108 -0
  173. package/dist/src/telemetry/sdk.test.js.map +1 -1
  174. package/dist/src/telemetry/types.d.ts +50 -3
  175. package/dist/src/telemetry/types.js +91 -6
  176. package/dist/src/telemetry/types.js.map +1 -1
  177. package/dist/src/telemetry/uiTelemetry.d.ts +1 -1
  178. package/dist/src/telemetry/uiTelemetry.js +2 -3
  179. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  180. package/dist/src/telemetry/uiTelemetry.test.js +11 -11
  181. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  182. package/dist/src/tools/edit.js +4 -2
  183. package/dist/src/tools/edit.js.map +1 -1
  184. package/dist/src/tools/edit.test.js +77 -1
  185. package/dist/src/tools/edit.test.js.map +1 -1
  186. package/dist/src/tools/message-bus-integration.test.d.ts +6 -0
  187. package/dist/src/tools/message-bus-integration.test.js +183 -0
  188. package/dist/src/tools/message-bus-integration.test.js.map +1 -0
  189. package/dist/src/tools/shell.js +8 -11
  190. package/dist/src/tools/shell.js.map +1 -1
  191. package/dist/src/tools/shell.test.js +33 -37
  192. package/dist/src/tools/shell.test.js.map +1 -1
  193. package/dist/src/tools/smart-edit.d.ts +0 -1
  194. package/dist/src/tools/smart-edit.js +3 -15
  195. package/dist/src/tools/smart-edit.js.map +1 -1
  196. package/dist/src/tools/smart-edit.test.js +16 -1
  197. package/dist/src/tools/smart-edit.test.js.map +1 -1
  198. package/dist/src/tools/tool-registry.js +1 -0
  199. package/dist/src/tools/tool-registry.js.map +1 -1
  200. package/dist/src/tools/tools.d.ts +13 -4
  201. package/dist/src/tools/tools.js +101 -3
  202. package/dist/src/tools/tools.js.map +1 -1
  203. package/dist/src/tools/write-file.js +2 -2
  204. package/dist/src/tools/write-file.js.map +1 -1
  205. package/dist/src/tools/write-file.test.js +16 -10
  206. package/dist/src/tools/write-file.test.js.map +1 -1
  207. package/dist/src/tools/write-todos.d.ts +25 -0
  208. package/dist/src/tools/write-todos.js +150 -0
  209. package/dist/src/tools/write-todos.js.map +1 -0
  210. package/dist/src/tools/write-todos.test.d.ts +6 -0
  211. package/dist/src/tools/write-todos.test.js +89 -0
  212. package/dist/src/tools/write-todos.test.js.map +1 -0
  213. package/dist/src/utils/editCorrector.d.ts +7 -6
  214. package/dist/src/utils/editCorrector.js +61 -18
  215. package/dist/src/utils/editCorrector.js.map +1 -1
  216. package/dist/src/utils/editCorrector.test.js +30 -79
  217. package/dist/src/utils/editCorrector.test.js.map +1 -1
  218. package/dist/src/utils/editor.js +31 -44
  219. package/dist/src/utils/editor.js.map +1 -1
  220. package/dist/src/utils/editor.test.js +61 -75
  221. package/dist/src/utils/editor.test.js.map +1 -1
  222. package/dist/src/utils/errorParsing.js +2 -2
  223. package/dist/src/utils/errorParsing.js.map +1 -1
  224. package/dist/src/utils/errorParsing.test.js +7 -7
  225. package/dist/src/utils/errorParsing.test.js.map +1 -1
  226. package/dist/src/utils/fileUtils.test.js +17 -8
  227. package/dist/src/utils/fileUtils.test.js.map +1 -1
  228. package/dist/src/utils/memoryDiscovery.test.js +12 -6
  229. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  230. package/dist/src/utils/nextSpeakerChecker.d.ts +2 -2
  231. package/dist/src/utils/nextSpeakerChecker.js +8 -2
  232. package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
  233. package/dist/src/utils/nextSpeakerChecker.test.js +40 -33
  234. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  235. package/dist/src/utils/shell-utils.d.ts +5 -0
  236. package/dist/src/utils/shell-utils.js +23 -0
  237. package/dist/src/utils/shell-utils.js.map +1 -1
  238. package/dist/src/utils/textUtils.d.ts +5 -0
  239. package/dist/src/utils/textUtils.js +14 -0
  240. package/dist/src/utils/textUtils.js.map +1 -1
  241. package/dist/src/utils/textUtils.test.d.ts +6 -0
  242. package/dist/src/utils/textUtils.test.js +59 -0
  243. package/dist/src/utils/textUtils.test.js.map +1 -0
  244. package/dist/tsconfig.tsbuildinfo +1 -1
  245. package/package.json +5 -1
  246. package/dist/google-gemini-cli-core-0.6.0-nightly.tgz +0 -0
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
- import { findIndexAfterFraction, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
7
+ import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
8
  import { AuthType, } from './contentGenerator.js';
9
9
  import {} from './geminiChat.js';
10
10
  import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
@@ -15,6 +15,7 @@ import { setSimulate429 } from '../utils/testUtils.js';
15
15
  import { tokenLimit } from './tokenLimits.js';
16
16
  import { ideContextStore } from '../ide/ideContext.js';
17
17
  import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
+ import { uiTelemetryService } from '../telemetry/uiTelemetry.js';
18
19
  // Mock fs module to prevent actual file system operations during tests
19
20
  const mockFileSystem = new Map();
20
21
  vi.mock('node:fs', () => {
@@ -76,6 +77,11 @@ vi.mock('../telemetry/index.js', () => ({
76
77
  logApiError: vi.fn(),
77
78
  }));
78
79
  vi.mock('../ide/ideContext.js');
80
+ vi.mock('../telemetry/uiTelemetry.js', () => ({
81
+ uiTelemetryService: {
82
+ setLastPromptTokenCount: vi.fn(),
83
+ },
84
+ }));
79
85
  /**
80
86
  * Array.fromAsync ponyfill, which will be available in es 2024.
81
87
  *
@@ -89,41 +95,59 @@ async function fromAsync(promise) {
89
95
  return results;
90
96
  }
91
97
  describe('findIndexAfterFraction', () => {
92
- const history = [
93
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
94
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
95
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
96
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
97
- { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
98
- ];
99
- // Total length: 333
100
98
  it('should throw an error for non-positive numbers', () => {
101
- expect(() => findIndexAfterFraction(history, 0)).toThrow('Fraction must be between 0 and 1');
99
+ expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
102
100
  });
103
101
  it('should throw an error for a fraction greater than or equal to 1', () => {
104
- expect(() => findIndexAfterFraction(history, 1)).toThrow('Fraction must be between 0 and 1');
102
+ expect(() => findCompressSplitPoint([], 1)).toThrow('Fraction must be between 0 and 1');
103
+ });
104
+ it('should handle an empty history', () => {
105
+ expect(findCompressSplitPoint([], 0.5)).toBe(0);
105
106
  });
106
107
  it('should handle a fraction in the middle', () => {
107
- // 333 * 0.5 = 166.5
108
- // 0: 66
109
- // 1: 66 + 68 = 134
110
- // 2: 134 + 66 = 200
111
- // 200 >= 166.5, so index is 3
112
- expect(findIndexAfterFraction(history, 0.5)).toBe(3);
108
+ const history = [
109
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
110
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
111
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
112
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
113
+ { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
114
+ ];
115
+ expect(findCompressSplitPoint(history, 0.5)).toBe(2);
113
116
  });
114
- it('should handle a fraction that results in the last index', () => {
115
- // 333 * 0.9 = 299.7
116
- // ...
117
- // 3: 200 + 68 = 268
118
- // 4: 268 + 65 = 333
119
- // 333 >= 299.7, so index is 5
120
- expect(findIndexAfterFraction(history, 0.9)).toBe(5);
117
+ it('should handle a fraction of last index', () => {
118
+ const history = [
119
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
120
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
121
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
122
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
123
+ { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
124
+ ];
125
+ expect(findCompressSplitPoint(history, 0.9)).toBe(4);
121
126
  });
122
- it('should handle an empty history', () => {
123
- expect(findIndexAfterFraction([], 0.5)).toBe(0);
127
+ it('should handle a fraction of after last index', () => {
128
+ const history = [
129
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (24%%)
130
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (50%)
131
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (74%)
132
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (100%)
133
+ ];
134
+ expect(findCompressSplitPoint(history, 0.8)).toBe(4);
135
+ });
136
+ it('should return earlier splitpoint if no valid ones are after threshhold', () => {
137
+ const history = [
138
+ { role: 'user', parts: [{ text: 'This is the first message.' }] },
139
+ { role: 'model', parts: [{ text: 'This is the second message.' }] },
140
+ { role: 'user', parts: [{ text: 'This is the third message.' }] },
141
+ { role: 'model', parts: [{ functionCall: {} }] },
142
+ ];
143
+ // Can't return 4 because the previous item has a function call.
144
+ expect(findCompressSplitPoint(history, 0.99)).toBe(2);
124
145
  });
125
146
  it('should handle a history with only one item', () => {
126
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
147
+ const historyWithEmptyParts = [
148
+ { role: 'user', parts: [{ text: 'Message 1' }] },
149
+ ];
150
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
127
151
  });
128
152
  it('should handle history with weird parts', () => {
129
153
  const historyWithEmptyParts = [
@@ -131,7 +155,7 @@ describe('findIndexAfterFraction', () => {
131
155
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
132
156
  { role: 'user', parts: [{ text: 'Message 2' }] },
133
157
  ];
134
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
158
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(2);
135
159
  });
136
160
  });
137
161
  describe('isThinkingSupported', () => {
@@ -168,6 +192,7 @@ describe('Gemini Client (client.ts)', () => {
168
192
  let mockGenerateContentFn;
169
193
  beforeEach(async () => {
170
194
  vi.resetAllMocks();
195
+ vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
171
196
  mockGenerateContentFn = vi.fn().mockResolvedValue({
172
197
  candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
173
198
  });
@@ -177,7 +202,6 @@ describe('Gemini Client (client.ts)', () => {
177
202
  generateContent: mockGenerateContentFn,
178
203
  generateContentStream: vi.fn(),
179
204
  countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
180
- embedContent: vi.fn(),
181
205
  batchEmbedContents: vi.fn(),
182
206
  };
183
207
  // Because the GeminiClient constructor kicks off an async process (startChat)
@@ -229,11 +253,18 @@ describe('Gemini Client (client.ts)', () => {
229
253
  getChatCompression: vi.fn().mockReturnValue(undefined),
230
254
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
231
255
  getUseSmartEdit: vi.fn().mockReturnValue(false),
256
+ getUseModelRouter: vi.fn().mockReturnValue(false),
232
257
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
233
258
  storage: {
234
259
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
235
260
  },
236
261
  getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
262
+ getBaseLlmClient: vi.fn().mockReturnValue({
263
+ generateJson: vi.fn().mockResolvedValue({
264
+ next_speaker: 'user',
265
+ reasoning: 'test',
266
+ }),
267
+ }),
237
268
  };
238
269
  client = new GeminiClient(mockConfig);
239
270
  await client.initialize();
@@ -242,129 +273,6 @@ describe('Gemini Client (client.ts)', () => {
242
273
  afterEach(() => {
243
274
  vi.restoreAllMocks();
244
275
  });
245
- describe('generateEmbedding', () => {
246
- const texts = ['hello world', 'goodbye world'];
247
- const testEmbeddingModel = 'test-embedding-model';
248
- it('should call embedContent with correct parameters and return embeddings', async () => {
249
- const mockEmbeddings = [
250
- [0.1, 0.2, 0.3],
251
- [0.4, 0.5, 0.6],
252
- ];
253
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
254
- embeddings: [
255
- { values: mockEmbeddings[0] },
256
- { values: mockEmbeddings[1] },
257
- ],
258
- });
259
- const result = await client.generateEmbedding(texts);
260
- expect(mockContentGenerator.embedContent).toHaveBeenCalledTimes(1);
261
- expect(mockContentGenerator.embedContent).toHaveBeenCalledWith({
262
- model: testEmbeddingModel,
263
- contents: texts,
264
- });
265
- expect(result).toEqual(mockEmbeddings);
266
- });
267
- it('should return an empty array if an empty array is passed', async () => {
268
- const result = await client.generateEmbedding([]);
269
- expect(result).toEqual([]);
270
- expect(mockContentGenerator.embedContent).not.toHaveBeenCalled();
271
- });
272
- it('should throw an error if API response has no embeddings array', async () => {
273
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({});
274
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
275
- });
276
- it('should throw an error if API response has an empty embeddings array', async () => {
277
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
278
- embeddings: [],
279
- });
280
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
281
- });
282
- it('should throw an error if API returns a mismatched number of embeddings', async () => {
283
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
284
- embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
285
- });
286
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
287
- });
288
- it('should throw an error if any embedding has nullish values', async () => {
289
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
290
- embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
291
- });
292
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
293
- });
294
- it('should throw an error if any embedding has an empty values array', async () => {
295
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
296
- embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
297
- });
298
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
299
- });
300
- it('should propagate errors from the API call', async () => {
301
- vi.mocked(mockContentGenerator.embedContent).mockRejectedValue(new Error('API Failure'));
302
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
303
- });
304
- });
305
- describe('generateJson', () => {
306
- it('should call generateContent with the correct parameters', async () => {
307
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
308
- const schema = { type: 'string' };
309
- const abortSignal = new AbortController().signal;
310
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
311
- totalTokens: 1,
312
- });
313
- await client.generateJson(contents, schema, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
314
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
315
- model: DEFAULT_GEMINI_FLASH_MODEL,
316
- config: {
317
- abortSignal,
318
- systemInstruction: getCoreSystemPrompt(''),
319
- temperature: 0,
320
- topP: 1,
321
- responseJsonSchema: schema,
322
- responseMimeType: 'application/json',
323
- },
324
- contents,
325
- }, 'test-session-id');
326
- });
327
- it('should allow overriding model and config', async () => {
328
- const contents = [
329
- { role: 'user', parts: [{ text: 'hello' }] },
330
- ];
331
- const schema = { type: 'string' };
332
- const abortSignal = new AbortController().signal;
333
- const customModel = 'custom-json-model';
334
- const customConfig = { temperature: 0.9, topK: 20 };
335
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
336
- totalTokens: 1,
337
- });
338
- await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
339
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
340
- model: customModel,
341
- config: {
342
- abortSignal,
343
- systemInstruction: getCoreSystemPrompt(''),
344
- temperature: 0.9,
345
- topP: 1, // from default
346
- topK: 20,
347
- responseJsonSchema: schema,
348
- responseMimeType: 'application/json',
349
- },
350
- contents,
351
- }, 'test-session-id');
352
- });
353
- it('should use the Flash model when fallback mode is active', async () => {
354
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
355
- const schema = { type: 'string' };
356
- const abortSignal = new AbortController().signal;
357
- const requestedModel = 'gemini-2.5-pro'; // A non-flash model
358
- // Mock config to be in fallback mode
359
- // We access the mock via the client instance which holds the mocked config
360
- vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
361
- await client.generateJson(contents, schema, abortSignal, requestedModel);
362
- // Assert that the Flash model was used, not the requested model
363
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(expect.objectContaining({
364
- model: DEFAULT_GEMINI_FLASH_MODEL,
365
- }), 'test-session-id');
366
- });
367
- });
368
276
  describe('addHistory', () => {
369
277
  it('should call chat.addHistory with the provided content', async () => {
370
278
  const mockChat = {
@@ -434,7 +342,7 @@ describe('Gemini Client (client.ts)', () => {
434
342
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
435
343
  totalTokens: 1000,
436
344
  });
437
- await client.tryCompressChat('prompt-id-4'); // Fails
345
+ await client.tryCompressChat('prompt-id-4', false); // Fails
438
346
  const result = await client.tryCompressChat('prompt-id-4', true);
439
347
  expect(result).toEqual({
440
348
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -447,16 +355,18 @@ describe('Gemini Client (client.ts)', () => {
447
355
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
448
356
  totalTokens: 1000,
449
357
  });
450
- const result = await client.tryCompressChat('prompt-id-4', true);
358
+ const result = await client.tryCompressChat('prompt-id-4', false);
451
359
  expect(result).toEqual({
452
360
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
453
361
  newTokenCount: 5000,
454
362
  originalTokenCount: 1000,
455
363
  });
364
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(5000);
365
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
456
366
  });
457
367
  it('does not manipulate the source chat', async () => {
458
368
  const { client, mockChat } = setup();
459
- await client.tryCompressChat('prompt-id-4', true);
369
+ await client.tryCompressChat('prompt-id-4', false);
460
370
  expect(client['chat']).toBe(mockChat); // a new chat session was not created
461
371
  });
462
372
  it('restores the history back to the original', async () => {
@@ -472,14 +382,14 @@ describe('Gemini Client (client.ts)', () => {
472
382
  const { client } = setup({
473
383
  chatHistory: originalHistory,
474
384
  });
475
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
385
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
476
386
  expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
477
387
  expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
478
388
  });
479
389
  it('will not attempt to compress context after a failure', async () => {
480
390
  const { client } = setup();
481
- await client.tryCompressChat('prompt-id-4');
482
- const result = await client.tryCompressChat('prompt-id-5');
391
+ await client.tryCompressChat('prompt-id-4', false);
392
+ const result = await client.tryCompressChat('prompt-id-5', false);
483
393
  // it counts tokens for {original, compressed} and then never again
484
394
  expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
485
395
  expect(result).toEqual({
@@ -499,7 +409,7 @@ describe('Gemini Client (client.ts)', () => {
499
409
  totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
500
410
  });
501
411
  const initialChat = client.getChat();
502
- const result = await client.tryCompressChat('prompt-id-2');
412
+ const result = await client.tryCompressChat('prompt-id-2', false);
503
413
  const newChat = client.getChat();
504
414
  expect(tokenLimit).toHaveBeenCalled();
505
415
  expect(result).toEqual({
@@ -536,11 +446,13 @@ describe('Gemini Client (client.ts)', () => {
536
446
  },
537
447
  ],
538
448
  });
539
- await client.tryCompressChat('prompt-id-3');
449
+ await client.tryCompressChat('prompt-id-3', false);
540
450
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
541
451
  tokens_before: originalTokenCount,
542
452
  tokens_after: newTokenCount,
543
453
  }));
454
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
455
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
544
456
  });
545
457
  it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
546
458
  const MOCKED_TOKEN_LIMIT = 1000;
@@ -569,7 +481,7 @@ describe('Gemini Client (client.ts)', () => {
569
481
  ],
570
482
  });
571
483
  const initialChat = client.getChat();
572
- const result = await client.tryCompressChat('prompt-id-3');
484
+ const result = await client.tryCompressChat('prompt-id-3', false);
573
485
  const newChat = client.getChat();
574
486
  expect(tokenLimit).toHaveBeenCalled();
575
487
  expect(mockGenerateContentFn).toHaveBeenCalled();
@@ -620,7 +532,7 @@ describe('Gemini Client (client.ts)', () => {
620
532
  ],
621
533
  });
622
534
  const initialChat = client.getChat();
623
- const result = await client.tryCompressChat('prompt-id-3');
535
+ const result = await client.tryCompressChat('prompt-id-3', false);
624
536
  const newChat = client.getChat();
625
537
  expect(tokenLimit).toHaveBeenCalled();
626
538
  expect(mockGenerateContentFn).toHaveBeenCalled();
@@ -660,7 +572,7 @@ describe('Gemini Client (client.ts)', () => {
660
572
  ],
661
573
  });
662
574
  const initialChat = client.getChat();
663
- const result = await client.tryCompressChat('prompt-id-1', true); // force = true
575
+ const result = await client.tryCompressChat('prompt-id-1', false); // force = true
664
576
  const newChat = client.getChat();
665
577
  expect(mockGenerateContentFn).toHaveBeenCalled();
666
578
  expect(result).toEqual({
@@ -671,45 +583,6 @@ describe('Gemini Client (client.ts)', () => {
671
583
  // Assert that the chat was reset
672
584
  expect(newChat).not.toBe(initialChat);
673
585
  });
674
- it('should use current model from config for token counting after sendMessage', async () => {
675
- const initialModel = mockConfig.getModel();
676
- // mock the model has been changed between calls of `countTokens`
677
- const firstCurrentModel = initialModel + '-changed-1';
678
- const secondCurrentModel = initialModel + '-changed-2';
679
- vi.mocked(mockConfig.getModel)
680
- .mockReturnValueOnce(firstCurrentModel)
681
- .mockReturnValueOnce(secondCurrentModel);
682
- vi.mocked(mockContentGenerator.countTokens)
683
- .mockResolvedValueOnce({ totalTokens: 100000 })
684
- .mockResolvedValueOnce({ totalTokens: 5000 });
685
- const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
686
- const mockChatHistory = [
687
- { role: 'user', parts: [{ text: 'Long conversation' }] },
688
- { role: 'model', parts: [{ text: 'Long response' }] },
689
- ];
690
- const mockChat = {
691
- getHistory: vi.fn().mockReturnValue(mockChatHistory),
692
- setHistory: vi.fn(),
693
- sendMessage: mockSendMessage,
694
- };
695
- client['chat'] = mockChat;
696
- client['startChat'] = vi.fn().mockResolvedValue(mockChat);
697
- const result = await client.tryCompressChat('prompt-id-4', true);
698
- expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
699
- expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
700
- model: firstCurrentModel,
701
- contents: mockChatHistory,
702
- });
703
- expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
704
- model: secondCurrentModel,
705
- contents: expect.any(Array),
706
- });
707
- expect(result).toEqual({
708
- compressionStatus: CompressionStatus.COMPRESSED,
709
- originalTokenCount: 100000,
710
- newTokenCount: 5000,
711
- });
712
- });
713
586
  });
714
587
  describe('sendMessageStream', () => {
715
588
  it('emits a compression event when the context was automatically compressed', async () => {
@@ -785,6 +658,11 @@ describe('Gemini Client (client.ts)', () => {
785
658
  },
786
659
  });
787
660
  vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
661
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
662
+ originalTokenCount: 0,
663
+ newTokenCount: 0,
664
+ compressionStatus: CompressionStatus.COMPRESSED,
665
+ });
788
666
  mockTurnRunFn.mockReturnValue((async function* () {
789
667
  yield { type: 'content', value: 'Hello' };
790
668
  })());
@@ -870,6 +748,11 @@ ${JSON.stringify({
870
748
  },
871
749
  });
872
750
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
751
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
752
+ originalTokenCount: 0,
753
+ newTokenCount: 0,
754
+ compressionStatus: CompressionStatus.COMPRESSED,
755
+ });
873
756
  const mockStream = (async function* () {
874
757
  yield { type: 'content', value: 'Hello' };
875
758
  })();
@@ -925,6 +808,11 @@ ${JSON.stringify({
925
808
  },
926
809
  });
927
810
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
811
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
812
+ originalTokenCount: 0,
813
+ newTokenCount: 0,
814
+ compressionStatus: CompressionStatus.COMPRESSED,
815
+ });
928
816
  const mockStream = (async function* () {
929
817
  yield { type: 'content', value: 'Hello' };
930
818
  })();
@@ -1750,6 +1638,35 @@ ${JSON.stringify({
1750
1638
  // Assert
1751
1639
  expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1752
1640
  });
1641
+ it('should abort linked signal when loop is detected', async () => {
1642
+ // Arrange
1643
+ vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
1644
+ vi.spyOn(client['loopDetector'], 'addAndCheck')
1645
+ .mockReturnValueOnce(false)
1646
+ .mockReturnValueOnce(true);
1647
+ let capturedSignal;
1648
+ mockTurnRunFn.mockImplementation((model, request, signal) => {
1649
+ capturedSignal = signal;
1650
+ return (async function* () {
1651
+ yield { type: 'content', value: 'First event' };
1652
+ yield { type: 'content', value: 'Second event' };
1653
+ })();
1654
+ });
1655
+ const mockChat = {
1656
+ addHistory: vi.fn(),
1657
+ getHistory: vi.fn().mockReturnValue([]),
1658
+ };
1659
+ client['chat'] = mockChat;
1660
+ // Act
1661
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
1662
+ const events = [];
1663
+ for await (const event of stream) {
1664
+ events.push(event);
1665
+ }
1666
+ // Assert
1667
+ expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
1668
+ expect(capturedSignal.aborted).toBe(true);
1669
+ });
1753
1670
  });
1754
1671
  describe('generateContent', () => {
1755
1672
  it('should call generateContent with the correct parameters', async () => {