@vybestack/llxprt-code 0.1.23 → 0.2.2-nightly.250908.7b895396

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 (164) hide show
  1. package/README.md +21 -17
  2. package/dist/package.json +4 -3
  3. package/dist/src/auth/anthropic-oauth-provider.d.ts +50 -3
  4. package/dist/src/auth/anthropic-oauth-provider.js +287 -63
  5. package/dist/src/auth/anthropic-oauth-provider.js.map +1 -1
  6. package/dist/src/auth/gemini-oauth-provider.d.ts +34 -3
  7. package/dist/src/auth/gemini-oauth-provider.js +308 -14
  8. package/dist/src/auth/gemini-oauth-provider.js.map +1 -1
  9. package/dist/src/auth/migration.d.ts +26 -0
  10. package/dist/src/auth/migration.js +54 -0
  11. package/dist/src/auth/migration.js.map +1 -0
  12. package/dist/src/auth/oauth-manager.d.ts +24 -0
  13. package/dist/src/auth/oauth-manager.js +179 -14
  14. package/dist/src/auth/oauth-manager.js.map +1 -1
  15. package/dist/src/auth/oauth-manager.spec.js +10 -8
  16. package/dist/src/auth/oauth-manager.spec.js.map +1 -1
  17. package/dist/src/auth/qwen-oauth-provider.d.ts +59 -3
  18. package/dist/src/auth/qwen-oauth-provider.js +263 -41
  19. package/dist/src/auth/qwen-oauth-provider.js.map +1 -1
  20. package/dist/src/config/config.js +11 -10
  21. package/dist/src/config/config.js.map +1 -1
  22. package/dist/src/config/extension.d.ts +1 -1
  23. package/dist/src/config/extension.js +1 -1
  24. package/dist/src/gemini.js +33 -11
  25. package/dist/src/gemini.js.map +1 -1
  26. package/dist/src/generated/git-commit.d.ts +1 -1
  27. package/dist/src/generated/git-commit.js +1 -1
  28. package/dist/src/integration-tests/base-url-behavior.integration.test.js +4 -1
  29. package/dist/src/integration-tests/base-url-behavior.integration.test.js.map +1 -1
  30. package/dist/src/integration-tests/compression-settings-apply.integration.test.js +85 -332
  31. package/dist/src/integration-tests/compression-settings-apply.integration.test.js.map +1 -1
  32. package/dist/src/integration-tests/ephemeral-settings.integration.test.js +5 -16
  33. package/dist/src/integration-tests/ephemeral-settings.integration.test.js.map +1 -1
  34. package/dist/src/integration-tests/model-params-isolation.integration.test.js +8 -2
  35. package/dist/src/integration-tests/model-params-isolation.integration.test.js.map +1 -1
  36. package/dist/src/integration-tests/modelParams.integration.test.js +4 -1
  37. package/dist/src/integration-tests/modelParams.integration.test.js.map +1 -1
  38. package/dist/src/integration-tests/provider-switching.integration.test.js +4 -1
  39. package/dist/src/integration-tests/provider-switching.integration.test.js.map +1 -1
  40. package/dist/src/integration-tests/retry-settings.integration.test.d.ts +6 -0
  41. package/dist/src/integration-tests/retry-settings.integration.test.js +56 -0
  42. package/dist/src/integration-tests/retry-settings.integration.test.js.map +1 -0
  43. package/dist/src/providers/index.d.ts +1 -1
  44. package/dist/src/providers/logging/LoggingProviderWrapper.test.js +58 -47
  45. package/dist/src/providers/logging/LoggingProviderWrapper.test.js.map +1 -1
  46. package/dist/src/providers/logging/multi-provider-logging.integration.test.js +130 -68
  47. package/dist/src/providers/logging/multi-provider-logging.integration.test.js.map +1 -1
  48. package/dist/src/providers/logging/performance.test.js +39 -16
  49. package/dist/src/providers/logging/performance.test.js.map +1 -1
  50. package/dist/src/providers/provider-gemini-switching.test.js +4 -1
  51. package/dist/src/providers/provider-gemini-switching.test.js.map +1 -1
  52. package/dist/src/providers/provider-switching.integration.test.js +12 -3
  53. package/dist/src/providers/provider-switching.integration.test.js.map +1 -1
  54. package/dist/src/providers/providerConfigUtils.js +2 -4
  55. package/dist/src/providers/providerConfigUtils.js.map +1 -1
  56. package/dist/src/providers/providerManagerInstance.js +24 -49
  57. package/dist/src/providers/providerManagerInstance.js.map +1 -1
  58. package/dist/src/services/BuiltinCommandLoader.js +4 -0
  59. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  60. package/dist/src/storage/ConversationStorage.test.js +10 -7
  61. package/dist/src/storage/ConversationStorage.test.js.map +1 -1
  62. package/dist/src/test-utils/mockCommandContext.js +2 -0
  63. package/dist/src/test-utils/mockCommandContext.js.map +1 -1
  64. package/dist/src/ui/App.e2e.test.d.ts +6 -0
  65. package/dist/src/ui/App.e2e.test.js +37 -0
  66. package/dist/src/ui/App.e2e.test.js.map +1 -0
  67. package/dist/src/ui/App.js +51 -4
  68. package/dist/src/ui/App.js.map +1 -1
  69. package/dist/src/ui/commands/authCommand.d.ts +9 -1
  70. package/dist/src/ui/commands/authCommand.js +109 -31
  71. package/dist/src/ui/commands/authCommand.js.map +1 -1
  72. package/dist/src/ui/commands/clearCommand.js +2 -0
  73. package/dist/src/ui/commands/clearCommand.js.map +1 -1
  74. package/dist/src/ui/commands/ideCommand.js +26 -0
  75. package/dist/src/ui/commands/ideCommand.js.map +1 -1
  76. package/dist/src/ui/commands/keyCommand.js +16 -54
  77. package/dist/src/ui/commands/keyCommand.js.map +1 -1
  78. package/dist/src/ui/commands/keyCommand.test.js +3 -3
  79. package/dist/src/ui/commands/keyCommand.test.js.map +1 -1
  80. package/dist/src/ui/commands/keyfileCommand.js +42 -32
  81. package/dist/src/ui/commands/keyfileCommand.js.map +1 -1
  82. package/dist/src/ui/commands/logoutCommand.d.ts +7 -0
  83. package/dist/src/ui/commands/logoutCommand.js +70 -0
  84. package/dist/src/ui/commands/logoutCommand.js.map +1 -0
  85. package/dist/src/ui/commands/modelCommand.js +16 -0
  86. package/dist/src/ui/commands/modelCommand.js.map +1 -1
  87. package/dist/src/ui/commands/profileCommand.js +31 -25
  88. package/dist/src/ui/commands/profileCommand.js.map +1 -1
  89. package/dist/src/ui/commands/profileCommand.test.js +15 -16
  90. package/dist/src/ui/commands/profileCommand.test.js.map +1 -1
  91. package/dist/src/ui/commands/providerCommand.js +10 -7
  92. package/dist/src/ui/commands/providerCommand.js.map +1 -1
  93. package/dist/src/ui/commands/setCommand.js +43 -19
  94. package/dist/src/ui/commands/setCommand.js.map +1 -1
  95. package/dist/src/ui/commands/setCommand.test.js +5 -7
  96. package/dist/src/ui/commands/setCommand.test.js.map +1 -1
  97. package/dist/src/ui/commands/setupGithubCommand.d.ts +2 -0
  98. package/dist/src/ui/commands/setupGithubCommand.js +123 -23
  99. package/dist/src/ui/commands/setupGithubCommand.js.map +1 -1
  100. package/dist/src/ui/commands/setupGithubCommand.test.js +94 -1
  101. package/dist/src/ui/commands/setupGithubCommand.test.js.map +1 -1
  102. package/dist/src/ui/commands/statusCommand.d.ts +7 -0
  103. package/dist/src/ui/commands/statusCommand.js +71 -0
  104. package/dist/src/ui/commands/statusCommand.js.map +1 -0
  105. package/dist/src/ui/commands/types.d.ts +1 -0
  106. package/dist/src/ui/commands/types.js.map +1 -1
  107. package/dist/src/ui/components/ContextIndicator.ui.test.js +19 -46
  108. package/dist/src/ui/components/ContextIndicator.ui.test.js.map +1 -1
  109. package/dist/src/ui/components/Footer.d.ts +1 -1
  110. package/dist/src/ui/components/Footer.js +7 -7
  111. package/dist/src/ui/components/Footer.js.map +1 -1
  112. package/dist/src/ui/components/Footer.responsive.test.js +28 -25
  113. package/dist/src/ui/components/Footer.responsive.test.js.map +1 -1
  114. package/dist/src/ui/components/OAuthCodeDialog.d.ts +5 -0
  115. package/dist/src/ui/components/OAuthCodeDialog.js +31 -1
  116. package/dist/src/ui/components/OAuthCodeDialog.js.map +1 -1
  117. package/dist/src/ui/components/OAuthCodeDialog.test.d.ts +6 -0
  118. package/dist/src/ui/components/OAuthCodeDialog.test.js +60 -0
  119. package/dist/src/ui/components/OAuthCodeDialog.test.js.map +1 -0
  120. package/dist/src/ui/contexts/KeypressContext.js +23 -1
  121. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  122. package/dist/src/ui/contexts/KeypressContext.test.js +58 -2
  123. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  124. package/dist/src/ui/contexts/SessionContext.d.ts +2 -0
  125. package/dist/src/ui/contexts/SessionContext.js +9 -1
  126. package/dist/src/ui/contexts/SessionContext.js.map +1 -1
  127. package/dist/src/ui/contexts/TodoContext.js +2 -2
  128. package/dist/src/ui/contexts/TodoContext.js.map +1 -1
  129. package/dist/src/ui/hooks/atCommandProcessor.js +0 -2
  130. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  131. package/dist/src/ui/hooks/atCommandProcessor.test.js +21 -6
  132. package/dist/src/ui/hooks/atCommandProcessor.test.js.map +1 -1
  133. package/dist/src/ui/hooks/slashCommandProcessor.js +2 -0
  134. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  135. package/dist/src/ui/hooks/useAuthCommand.js +6 -5
  136. package/dist/src/ui/hooks/useAuthCommand.js.map +1 -1
  137. package/dist/src/ui/hooks/useGeminiStream.js +47 -63
  138. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  139. package/dist/src/ui/hooks/useProviderDialog.js +2 -5
  140. package/dist/src/ui/hooks/useProviderDialog.js.map +1 -1
  141. package/dist/src/ui/hooks/useToolScheduler.test.js +60 -24
  142. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  143. package/dist/src/ui/utils/platformConstants.d.ts +2 -0
  144. package/dist/src/ui/utils/platformConstants.js +2 -0
  145. package/dist/src/ui/utils/platformConstants.js.map +1 -1
  146. package/dist/src/ui/utils/secureInputHandler.js +18 -7
  147. package/dist/src/ui/utils/secureInputHandler.js.map +1 -1
  148. package/dist/src/ui/utils/secureInputHandler.test.js +20 -0
  149. package/dist/src/ui/utils/secureInputHandler.test.js.map +1 -1
  150. package/dist/src/utils/privacy/ConversationDataRedactor.d.ts +3 -3
  151. package/dist/src/utils/privacy/ConversationDataRedactor.js +16 -12
  152. package/dist/src/utils/privacy/ConversationDataRedactor.js.map +1 -1
  153. package/dist/src/utils/privacy/ConversationDataRedactor.test.js +115 -72
  154. package/dist/src/utils/privacy/ConversationDataRedactor.test.js.map +1 -1
  155. package/dist/src/validateNonInterActiveAuth.js +8 -17
  156. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  157. package/dist/src/zed-integration/schema.d.ts +194 -91
  158. package/dist/src/zed-integration/schema.js +7 -1
  159. package/dist/src/zed-integration/schema.js.map +1 -1
  160. package/dist/src/zed-integration/zedIntegration.d.ts +1 -3
  161. package/dist/src/zed-integration/zedIntegration.js +454 -198
  162. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  163. package/dist/tsconfig.tsbuildinfo +1 -1
  164. package/package.json +4 -3
@@ -3,7 +3,7 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { AuthType, logToolCall, convertToFunctionResponse, ToolConfirmationOutcome, clearCachedCredentialFile, isNodeError, getErrorMessage, isWithinRoot, getErrorStatus, MCPServerConfig, DiscoveredMCPTool, } from '@vybestack/llxprt-code-core';
6
+ import { AuthType, logToolCall, convertToFunctionResponse, ToolConfirmationOutcome, clearCachedCredentialFile, isNodeError, getErrorMessage, isWithinRoot, getErrorStatus, DiscoveredMCPTool, DebugLogger, getFunctionCalls, EmojiFilter, } from '@vybestack/llxprt-code-core';
7
7
  import * as acp from './acp.js';
8
8
  import { AcpFileSystemService } from './fileSystemService.js';
9
9
  import { Readable, Writable } from 'node:stream';
@@ -11,32 +11,43 @@ import { SettingScope } from '../config/settings.js';
11
11
  import * as fs from 'fs/promises';
12
12
  import * as path from 'path';
13
13
  import { z } from 'zod';
14
+ import os from 'os';
14
15
  import { randomUUID } from 'crypto';
15
- import { loadCliConfig } from '../config/config.js';
16
- export async function runZedIntegration(config, settings, extensions, argv) {
16
+ export async function runZedIntegration(config, settings) {
17
+ const logger = new DebugLogger('llxprt:zed-integration');
18
+ logger.debug(() => 'Starting Zed integration');
17
19
  const stdout = Writable.toWeb(process.stdout);
18
20
  const stdin = Readable.toWeb(process.stdin);
19
- // Stdout is used to send messages to the client, so console.log/console.info
20
- // messages to stderr so that they don't interfere with ACP.
21
- console.log = console.error;
22
- console.info = console.error;
23
- console.debug = console.error;
24
- new acp.AgentSideConnection((client) => new GeminiAgent(config, settings, extensions, argv, client), stdout, stdin);
21
+ logger.debug(() => 'Streams created');
22
+ try {
23
+ new acp.AgentSideConnection((client) => {
24
+ logger.debug(() => 'Creating GeminiAgent');
25
+ return new GeminiAgent(config, settings, client);
26
+ }, stdout, stdin);
27
+ logger.debug(() => 'AgentSideConnection created successfully');
28
+ }
29
+ catch (e) {
30
+ logger.debug(() => `ERROR: Failed to create AgentSideConnection: ${e}`);
31
+ throw e;
32
+ }
33
+ logger.debug(() => 'Zed integration ready, waiting for messages');
34
+ // Keep the process alive - the Connection's #receive method will handle messages
35
+ await new Promise(() => {
36
+ // This promise never resolves, keeping the process alive
37
+ });
25
38
  }
26
39
  class GeminiAgent {
27
40
  config;
28
41
  settings;
29
- extensions;
30
- argv;
31
42
  client;
32
43
  sessions = new Map();
33
44
  clientCapabilities;
34
- constructor(config, settings, extensions, argv, client) {
45
+ logger;
46
+ constructor(config, settings, client) {
35
47
  this.config = config;
36
48
  this.settings = settings;
37
- this.extensions = extensions;
38
- this.argv = argv;
39
49
  this.client = client;
50
+ this.logger = new DebugLogger('llxprt:zed-integration');
40
51
  }
41
52
  async initialize(args) {
42
53
  this.clientCapabilities = args.clientCapabilities;
@@ -62,6 +73,11 @@ class GeminiAgent {
62
73
  authMethods,
63
74
  agentCapabilities: {
64
75
  loadSession: false,
76
+ promptCapabilities: {
77
+ image: true,
78
+ audio: true,
79
+ embeddedContext: true,
80
+ },
65
81
  },
66
82
  };
67
83
  }
@@ -71,47 +87,176 @@ class GeminiAgent {
71
87
  await this.config.refreshAuth(method);
72
88
  this.settings.setValue(SettingScope.User, 'selectedAuthType', method);
73
89
  }
74
- async newSession({ cwd, mcpServers, }) {
75
- const sessionId = randomUUID();
76
- const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
77
- let isAuthenticated = false;
78
- if (this.settings.merged.selectedAuthType) {
90
+ async newSession({ cwd: _cwd, mcpServers: _mcpServers, }) {
91
+ try {
92
+ const sessionId = randomUUID();
93
+ // Use the existing config that was passed to runZedIntegration
94
+ const sessionConfig = this.config;
95
+ this.logger.debug(() => `newSession - creating session ${sessionId}`);
96
+ if (this.clientCapabilities?.fs) {
97
+ const acpFileSystemService = new AcpFileSystemService(this.client, sessionId, this.clientCapabilities.fs, sessionConfig.getFileSystemService());
98
+ sessionConfig.setFileSystemService(acpFileSystemService);
99
+ }
100
+ // Try to get the client and check if it's properly initialized
101
+ let geminiClient = sessionConfig.getGeminiClient();
102
+ const hasContentGeneratorConfig = sessionConfig.getContentGeneratorConfig() !== undefined;
103
+ this.logger.debug(() => `GeminiClient exists: ${!!geminiClient}, ContentGeneratorConfig exists: ${hasContentGeneratorConfig}`);
104
+ if (!geminiClient || !hasContentGeneratorConfig) {
105
+ this.logger.debug(() => 'GeminiClient not available - attempting auto-authentication');
106
+ // Auto-authenticate based on available configuration
107
+ const providerManager = sessionConfig.getProviderManager();
108
+ // Debug provider state
109
+ if (providerManager) {
110
+ this.logger.debug(() => `ProviderManager exists: ${providerManager.hasActiveProvider() ? 'has active provider' : 'no active provider'}`);
111
+ this.logger.debug(() => `Active provider name: ${providerManager.getActiveProviderName() || 'none'}`);
112
+ }
113
+ else {
114
+ this.logger.debug(() => 'No ProviderManager available');
115
+ }
116
+ // Check for provider from config (loaded from profile or CLI)
117
+ const configProvider = sessionConfig.getProvider();
118
+ if (configProvider && providerManager) {
119
+ this.logger.debug(() => `Config has provider: ${configProvider}`);
120
+ // Ensure provider is activated
121
+ if (!providerManager.hasActiveProvider()) {
122
+ this.logger.debug(() => `Activating provider: ${configProvider}`);
123
+ await providerManager.setActiveProvider(configProvider);
124
+ // Apply ephemeral settings from profile to the provider
125
+ const activeProvider = providerManager.getActiveProvider();
126
+ if (activeProvider) {
127
+ const authKey = sessionConfig.getEphemeralSetting('auth-key');
128
+ const authKeyfile = sessionConfig.getEphemeralSetting('auth-keyfile');
129
+ const baseUrl = sessionConfig.getEphemeralSetting('base-url');
130
+ // Apply auth settings from profile
131
+ if (authKey && activeProvider.setApiKey) {
132
+ this.logger.debug(() => 'Setting API key from profile');
133
+ activeProvider.setApiKey(authKey);
134
+ }
135
+ else if (authKeyfile && activeProvider.setApiKey) {
136
+ // Load API key from file
137
+ try {
138
+ const apiKey = (await fs.readFile(authKeyfile.replace(/^~/, os.homedir()), 'utf-8')).trim();
139
+ if (apiKey) {
140
+ this.logger.debug(() => 'Setting API key from keyfile');
141
+ activeProvider.setApiKey(apiKey);
142
+ }
143
+ }
144
+ catch (error) {
145
+ this.logger.debug(() => `ERROR: Failed to load keyfile ${authKeyfile}: ${error}`);
146
+ }
147
+ }
148
+ // Apply base URL if specified
149
+ if (baseUrl && baseUrl !== 'none' && activeProvider.setBaseUrl) {
150
+ this.logger.debug(() => `Setting base URL: ${baseUrl}`);
151
+ activeProvider.setBaseUrl(baseUrl);
152
+ }
153
+ // Apply profile model params if loaded
154
+ const configWithProfile = sessionConfig;
155
+ if (configWithProfile._profileModelParams &&
156
+ 'setModelParams' in activeProvider &&
157
+ activeProvider.setModelParams) {
158
+ this.logger.debug(() => 'Setting model params from profile');
159
+ activeProvider.setModelParams(configWithProfile._profileModelParams);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (providerManager && providerManager.hasActiveProvider()) {
165
+ // Use provider-based auth if a provider is configured
166
+ this.logger.debug(() => `Auto-authenticating with provider: ${providerManager.getActiveProviderName()}`);
167
+ // Ensure provider manager is set on config before refreshAuth
168
+ // This is crucial for createContentGeneratorConfig to include the provider manager
169
+ if (!sessionConfig.getProviderManager()) {
170
+ this.logger.debug(() => 'Setting provider manager on config');
171
+ sessionConfig.providerManager = providerManager;
172
+ // Ensure serverToolsProvider (Gemini) has config set BEFORE refreshAuth
173
+ // This is critical for web search to work properly
174
+ const serverToolsProvider = providerManager.getServerToolsProvider();
175
+ if (serverToolsProvider &&
176
+ serverToolsProvider.name === 'gemini' &&
177
+ serverToolsProvider.setConfig) {
178
+ this.logger.debug(() => 'Setting config on serverToolsProvider for web search (before auth)');
179
+ serverToolsProvider.setConfig(sessionConfig);
180
+ }
181
+ }
182
+ await sessionConfig.refreshAuth(AuthType.USE_PROVIDER);
183
+ // After refreshAuth, verify ContentGeneratorConfig was created with provider manager
184
+ const contentGenConfig = sessionConfig.getContentGeneratorConfig();
185
+ if (contentGenConfig && !contentGenConfig.providerManager) {
186
+ this.logger.debug(() => 'Adding provider manager to ContentGeneratorConfig');
187
+ contentGenConfig.providerManager = providerManager;
188
+ }
189
+ }
190
+ else if (process.env.GEMINI_API_KEY) {
191
+ // Use API key if available
192
+ this.logger.debug(() => 'Auto-authenticating with GEMINI_API_KEY');
193
+ await sessionConfig.refreshAuth(AuthType.USE_GEMINI);
194
+ }
195
+ else {
196
+ // Try OAuth as last resort (this might open a browser)
197
+ this.logger.debug(() => 'Auto-authenticating with OAuth');
198
+ await sessionConfig.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
199
+ }
200
+ geminiClient = sessionConfig.getGeminiClient();
201
+ if (!geminiClient) {
202
+ throw new Error('Failed to authenticate. Please ensure valid credentials are available.');
203
+ }
204
+ }
205
+ this.logger.debug(() => 'Successfully obtained GeminiClient');
206
+ // Verify ContentGeneratorConfig was created properly
207
+ let contentGenConfig;
79
208
  try {
80
- await config.refreshAuth(this.settings.merged.selectedAuthType);
81
- isAuthenticated = true;
209
+ contentGenConfig = sessionConfig.getContentGeneratorConfig();
210
+ this.logger.debug(() => `ContentGeneratorConfig exists: ${!!contentGenConfig}`);
211
+ if (contentGenConfig) {
212
+ this.logger.debug(() => `ContentGeneratorConfig has providerManager: ${!!contentGenConfig.providerManager}`);
213
+ this.logger.debug(() => `ContentGeneratorConfig authType: ${contentGenConfig.authType}`);
214
+ }
82
215
  }
83
- catch (e) {
84
- console.error(`Authentication failed: ${e}`);
216
+ catch (error) {
217
+ this.logger.debug(() => `Failed to get ContentGeneratorConfig: ${error}`);
218
+ throw new Error('Content generator config not created after authentication. Please check your credentials.');
85
219
  }
86
- }
87
- if (!isAuthenticated) {
88
- throw acp.RequestError.authRequired();
89
- }
90
- if (this.clientCapabilities?.fs) {
91
- const acpFileSystemService = new AcpFileSystemService(this.client, sessionId, this.clientCapabilities.fs, config.getFileSystemService());
92
- config.setFileSystemService(acpFileSystemService);
93
- }
94
- const geminiClient = config.getGeminiClient();
95
- const chat = await geminiClient.startChat();
96
- const session = new Session(sessionId, chat, config, this.client);
97
- this.sessions.set(sessionId, session);
98
- return {
99
- sessionId,
100
- };
101
- }
102
- async newSessionConfig(sessionId, cwd, mcpServers) {
103
- const mergedMcpServers = { ...this.settings.merged.mcpServers };
104
- for (const { command, args, env: rawEnv, name } of mcpServers) {
105
- const env = {};
106
- for (const { name: envName, value } of rawEnv) {
107
- env[envName] = value;
220
+ if (!contentGenConfig) {
221
+ throw new Error('Content generator config not created after authentication.');
108
222
  }
109
- mergedMcpServers[name] = new MCPServerConfig(command, args, env, cwd);
223
+ let chat;
224
+ try {
225
+ chat = await geminiClient.startChat();
226
+ }
227
+ catch (error) {
228
+ this.logger.debug(() => `Error starting chat: ${error}`);
229
+ // If startChat fails due to missing config, try to authenticate now
230
+ if (error instanceof Error &&
231
+ error.message.includes('Content generator config')) {
232
+ this.logger.debug(() => 'Attempting late authentication due to missing config');
233
+ const providerManager = sessionConfig.getProviderManager();
234
+ if (providerManager && providerManager.hasActiveProvider()) {
235
+ await sessionConfig.refreshAuth(AuthType.USE_PROVIDER);
236
+ }
237
+ else if (process.env.GEMINI_API_KEY) {
238
+ await sessionConfig.refreshAuth(AuthType.USE_GEMINI);
239
+ }
240
+ else {
241
+ await sessionConfig.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
242
+ }
243
+ // Try again after auth
244
+ chat = await geminiClient.startChat();
245
+ }
246
+ else {
247
+ throw error;
248
+ }
249
+ }
250
+ const session = new Session(sessionId, chat, sessionConfig, this.client);
251
+ this.sessions.set(sessionId, session);
252
+ return {
253
+ sessionId,
254
+ };
255
+ }
256
+ catch (error) {
257
+ this.logger.debug(() => `ERROR in newSession: ${error}`);
258
+ throw error;
110
259
  }
111
- const settings = { ...this.settings.merged, mcpServers: mergedMcpServers };
112
- const config = await loadCliConfig(settings, this.extensions, sessionId, this.argv, cwd);
113
- await config.initialize();
114
- return config;
115
260
  }
116
261
  async cancel(params) {
117
262
  const session = this.sessions.get(params.sessionId);
@@ -134,11 +279,16 @@ class Session {
134
279
  config;
135
280
  client;
136
281
  pendingPrompt = null;
282
+ emojiFilter;
137
283
  constructor(id, chat, config, client) {
138
284
  this.id = id;
139
285
  this.chat = chat;
140
286
  this.config = config;
141
287
  this.client = client;
288
+ // Initialize emoji filter from settings
289
+ const emojiFilterMode = this.config.getEphemeralSetting('emojifilter') || 'auto';
290
+ const filterConfig = { mode: emojiFilterMode };
291
+ this.emojiFilter = new EmojiFilter(filterConfig);
142
292
  }
143
293
  async cancelPendingPrompt() {
144
294
  if (!this.pendingPrompt) {
@@ -156,10 +306,6 @@ class Session {
156
306
  const parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
157
307
  let nextMessage = { role: 'user', parts };
158
308
  while (nextMessage !== null) {
159
- if (pendingSend.signal.aborted) {
160
- chat.addHistory(nextMessage);
161
- return { stopReason: 'cancelled' };
162
- }
163
309
  const functionCalls = [];
164
310
  try {
165
311
  const responseStream = await chat.sendMessageStream({
@@ -171,7 +317,9 @@ class Session {
171
317
  nextMessage = null;
172
318
  for await (const resp of responseStream) {
173
319
  if (pendingSend.signal.aborted) {
174
- return { stopReason: 'cancelled' };
320
+ // Let the stream processing complete naturally to handle cancellation properly
321
+ // Don't return early here - let the tool pipeline handle cleanup
322
+ break;
175
323
  }
176
324
  if (resp.candidates && resp.candidates.length > 0) {
177
325
  const candidate = resp.candidates[0];
@@ -179,9 +327,27 @@ class Session {
179
327
  if (!part.text) {
180
328
  continue;
181
329
  }
330
+ // Filter the content through emoji filter
331
+ const filterResult = this.emojiFilter.filterStreamChunk(part.text);
332
+ if (filterResult.blocked) {
333
+ // In error mode: inject error feedback to model for retry
334
+ this.sendUpdate({
335
+ sessionUpdate: 'agent_message_chunk',
336
+ content: {
337
+ type: 'text',
338
+ text: '[Error: Response blocked due to emoji detection]',
339
+ },
340
+ });
341
+ // Add system feedback to be sent with next tool response
342
+ // This could be done by queueing feedback similar to TUI implementation
343
+ continue;
344
+ }
345
+ const filteredText = typeof filterResult.filtered === 'string'
346
+ ? filterResult.filtered
347
+ : '';
182
348
  const content = {
183
349
  type: 'text',
184
- text: part.text,
350
+ text: filteredText,
185
351
  };
186
352
  this.sendUpdate({
187
353
  sessionUpdate: part.thought
@@ -191,8 +357,10 @@ class Session {
191
357
  });
192
358
  }
193
359
  }
194
- if (resp.functionCalls) {
195
- functionCalls.push(...resp.functionCalls);
360
+ // Extract function calls from the response using the proper utility
361
+ const respFunctionCalls = getFunctionCalls(resp);
362
+ if (respFunctionCalls && respFunctionCalls.length > 0) {
363
+ functionCalls.push(...respFunctionCalls);
196
364
  }
197
365
  }
198
366
  }
@@ -200,11 +368,31 @@ class Session {
200
368
  if (getErrorStatus(error) === 429) {
201
369
  throw new acp.RequestError(429, 'Rate limit exceeded. Try again later.');
202
370
  }
203
- throw error;
371
+ // If this is an abort error due to cancellation, handle it gracefully
372
+ if (pendingSend.signal.aborted &&
373
+ isNodeError(error) &&
374
+ error.name === 'AbortError') {
375
+ // Don't throw - let the cancellation be handled below
376
+ }
377
+ else {
378
+ throw error;
379
+ }
380
+ }
381
+ // Check for cancellation after stream processing but before tool execution
382
+ if (pendingSend.signal.aborted) {
383
+ // Return cancellation without adding to conversation history
384
+ // The conversation state should remain clean for proper context handling
385
+ return { stopReason: 'cancelled' };
204
386
  }
205
387
  if (functionCalls.length > 0) {
206
388
  const toolResponseParts = [];
207
389
  for (const fc of functionCalls) {
390
+ // Check for cancellation before each tool execution
391
+ if (pendingSend.signal.aborted) {
392
+ // Return cancellation without polluting conversation history
393
+ // Tool execution cancellation should be handled by the tool execution system
394
+ return { stopReason: 'cancelled' };
395
+ }
208
396
  const response = await this.runTool(pendingSend.signal, promptId, fc);
209
397
  const parts = Array.isArray(response) ? response : [response];
210
398
  for (const part of parts) {
@@ -216,7 +404,14 @@ class Session {
216
404
  }
217
405
  }
218
406
  }
219
- nextMessage = { role: 'user', parts: toolResponseParts };
407
+ // For multiple tool responses, send them all together as the TUI does
408
+ // This ensures proper conversation history structure for providers like Anthropic
409
+ if (toolResponseParts.length > 0) {
410
+ nextMessage = { role: 'user', parts: toolResponseParts };
411
+ }
412
+ else {
413
+ nextMessage = null;
414
+ }
220
415
  }
221
416
  }
222
417
  return { stopReason: 'end_turn' };
@@ -247,7 +442,15 @@ class Session {
247
442
  ? 'mcp'
248
443
  : 'native',
249
444
  });
445
+ // Return paired function call and function response for proper conversation history
250
446
  return [
447
+ {
448
+ functionCall: {
449
+ id: callId,
450
+ name: fc.name ?? '',
451
+ args,
452
+ },
453
+ },
251
454
  {
252
455
  functionResponse: {
253
456
  id: callId,
@@ -265,64 +468,64 @@ class Session {
265
468
  if (!tool) {
266
469
  return errorResponse(new Error(`Tool "${fc.name}" not found in registry.`));
267
470
  }
268
- const invocation = tool.build(args);
269
- const confirmationDetails = await invocation.shouldConfirmExecute(abortSignal);
270
- if (confirmationDetails) {
271
- const content = [];
272
- if (confirmationDetails.type === 'edit') {
273
- content.push({
274
- type: 'diff',
275
- path: confirmationDetails.fileName,
276
- oldText: confirmationDetails.originalContent,
277
- newText: confirmationDetails.newContent,
278
- });
471
+ try {
472
+ const invocation = tool.build(args);
473
+ const confirmationDetails = await invocation.shouldConfirmExecute(abortSignal);
474
+ if (confirmationDetails) {
475
+ const content = [];
476
+ if (confirmationDetails.type === 'edit') {
477
+ content.push({
478
+ type: 'diff',
479
+ path: confirmationDetails.fileName,
480
+ oldText: confirmationDetails.originalContent,
481
+ newText: confirmationDetails.newContent,
482
+ });
483
+ }
484
+ const params = {
485
+ sessionId: this.id,
486
+ options: toPermissionOptions(confirmationDetails),
487
+ toolCall: {
488
+ toolCallId: callId,
489
+ status: 'pending',
490
+ title: invocation.getDescription(),
491
+ content,
492
+ locations: invocation.toolLocations(),
493
+ kind: tool.kind,
494
+ },
495
+ };
496
+ const output = await this.client.requestPermission(params);
497
+ const outcome = output.outcome.outcome === 'cancelled'
498
+ ? ToolConfirmationOutcome.Cancel
499
+ : z
500
+ .nativeEnum(ToolConfirmationOutcome)
501
+ .parse(output.outcome.optionId);
502
+ await confirmationDetails.onConfirm(outcome);
503
+ switch (outcome) {
504
+ case ToolConfirmationOutcome.Cancel:
505
+ return errorResponse(new Error(`Tool "${fc.name}" was canceled by the user.`));
506
+ case ToolConfirmationOutcome.ProceedOnce:
507
+ case ToolConfirmationOutcome.ProceedAlways:
508
+ case ToolConfirmationOutcome.ProceedAlwaysServer:
509
+ case ToolConfirmationOutcome.ProceedAlwaysTool:
510
+ case ToolConfirmationOutcome.ModifyWithEditor:
511
+ break;
512
+ default: {
513
+ const resultOutcome = outcome;
514
+ throw new Error(`Unexpected: ${resultOutcome}`);
515
+ }
516
+ }
279
517
  }
280
- const params = {
281
- sessionId: this.id,
282
- options: toPermissionOptions(confirmationDetails),
283
- toolCall: {
518
+ else {
519
+ await this.sendUpdate({
520
+ sessionUpdate: 'tool_call',
284
521
  toolCallId: callId,
285
- status: 'pending',
522
+ status: 'in_progress',
286
523
  title: invocation.getDescription(),
287
- content,
524
+ content: [],
288
525
  locations: invocation.toolLocations(),
289
526
  kind: tool.kind,
290
- },
291
- };
292
- const output = await this.client.requestPermission(params);
293
- const outcome = output.outcome.outcome === 'cancelled'
294
- ? ToolConfirmationOutcome.Cancel
295
- : z
296
- .nativeEnum(ToolConfirmationOutcome)
297
- .parse(output.outcome.optionId);
298
- await confirmationDetails.onConfirm(outcome);
299
- switch (outcome) {
300
- case ToolConfirmationOutcome.Cancel:
301
- return errorResponse(new Error(`Tool "${fc.name}" was canceled by the user.`));
302
- case ToolConfirmationOutcome.ProceedOnce:
303
- case ToolConfirmationOutcome.ProceedAlways:
304
- case ToolConfirmationOutcome.ProceedAlwaysServer:
305
- case ToolConfirmationOutcome.ProceedAlwaysTool:
306
- case ToolConfirmationOutcome.ModifyWithEditor:
307
- break;
308
- default: {
309
- const resultOutcome = outcome;
310
- throw new Error(`Unexpected: ${resultOutcome}`);
311
- }
527
+ });
312
528
  }
313
- }
314
- else {
315
- await this.sendUpdate({
316
- sessionUpdate: 'tool_call',
317
- toolCallId: callId,
318
- status: 'in_progress',
319
- title: invocation.getDescription(),
320
- content: [],
321
- locations: invocation.toolLocations(),
322
- kind: tool.kind,
323
- });
324
- }
325
- try {
326
529
  const toolResult = await invocation.execute(abortSignal);
327
530
  const content = toToolCallContent(toolResult);
328
531
  await this.sendUpdate({
@@ -344,7 +547,21 @@ class Session {
344
547
  ? 'mcp'
345
548
  : 'native',
346
549
  });
347
- return convertToFunctionResponse(fc.name, callId, toolResult.llmContent);
550
+ // Return paired function call and function response like the TUI does
551
+ // This ensures proper conversation history for providers that need it (like Anthropic)
552
+ const functionResponseParts = convertToFunctionResponse(fc.name, callId, toolResult.llmContent);
553
+ return [
554
+ {
555
+ functionCall: {
556
+ id: callId,
557
+ name: fc.name,
558
+ args,
559
+ },
560
+ },
561
+ ...(Array.isArray(functionResponseParts)
562
+ ? functionResponseParts
563
+ : [functionResponseParts]),
564
+ ];
348
565
  }
349
566
  catch (e) {
350
567
  const error = e instanceof Error ? e : new Error(String(e));
@@ -360,41 +577,53 @@ class Session {
360
577
  }
361
578
  }
362
579
  async #resolvePrompt(message, abortSignal) {
580
+ const FILE_URI_SCHEME = 'file://';
581
+ const embeddedContext = [];
363
582
  const parts = message.map((part) => {
364
583
  switch (part.type) {
365
584
  case 'text':
366
585
  return { text: part.text };
367
- case 'resource_link':
586
+ case 'image':
587
+ case 'audio':
368
588
  return {
369
- fileData: {
370
- mimeData: part.mimeType,
371
- name: part.name,
372
- fileUri: part.uri,
589
+ inlineData: {
590
+ mimeType: part.mimeType,
591
+ data: part.data,
373
592
  },
374
593
  };
594
+ case 'resource_link': {
595
+ if (part.uri.startsWith(FILE_URI_SCHEME)) {
596
+ return {
597
+ fileData: {
598
+ mimeData: part.mimeType,
599
+ name: part.name,
600
+ fileUri: part.uri.slice(FILE_URI_SCHEME.length),
601
+ },
602
+ };
603
+ }
604
+ else {
605
+ return { text: `@${part.uri}` };
606
+ }
607
+ }
375
608
  case 'resource': {
376
- return {
377
- fileData: {
378
- mimeData: part.resource.mimeType,
379
- name: part.resource.uri,
380
- fileUri: part.resource.uri,
381
- },
382
- };
609
+ embeddedContext.push(part.resource);
610
+ return { text: `@${part.resource.uri}` };
383
611
  }
384
612
  default: {
385
- throw new Error(`Unexpected chunk type: '${part.type}'`);
613
+ const unreachable = part;
614
+ throw new Error(`Unexpected chunk type: '${unreachable}'`);
386
615
  }
387
616
  }
388
617
  });
389
618
  const atPathCommandParts = parts.filter((part) => 'fileData' in part);
390
- if (atPathCommandParts.length === 0) {
619
+ if (atPathCommandParts.length === 0 && embeddedContext.length === 0) {
391
620
  return parts;
392
621
  }
622
+ const atPathToResolvedSpecMap = new Map();
393
623
  // Get centralized file discovery service
394
624
  const fileDiscovery = this.config.getFileService();
395
625
  const respectGitIgnore = this.config.getFileFilteringRespectGitIgnore();
396
626
  const pathSpecsToRead = [];
397
- const atPathToResolvedSpecMap = new Map();
398
627
  const contentLabelsForDisplay = [];
399
628
  const ignoredPaths = [];
400
629
  const toolRegistry = this.config.getToolRegistry();
@@ -527,91 +756,115 @@ class Session {
527
756
  const ignoreType = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
528
757
  this.debug(`Ignored ${ignoredPaths.length} ${ignoreType} files: ${ignoredPaths.join(', ')}`);
529
758
  }
530
- // Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
531
- if (pathSpecsToRead.length === 0) {
759
+ const processedQueryParts = [{ text: initialQueryText }];
760
+ if (pathSpecsToRead.length === 0 && embeddedContext.length === 0) {
761
+ // Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
532
762
  console.warn('No valid file paths found in @ commands to read.');
533
763
  return [{ text: initialQueryText }];
534
764
  }
535
- const processedQueryParts = [{ text: initialQueryText }];
536
- const toolArgs = {
537
- paths: pathSpecsToRead,
538
- respectGitIgnore, // Use configuration setting
539
- };
540
- const callId = `${readManyFilesTool.name}-${Date.now()}`;
541
- try {
542
- const invocation = readManyFilesTool.build(toolArgs);
543
- await this.sendUpdate({
544
- sessionUpdate: 'tool_call',
545
- toolCallId: callId,
546
- status: 'in_progress',
547
- title: invocation.getDescription(),
548
- content: [],
549
- locations: invocation.toolLocations(),
550
- kind: readManyFilesTool.kind,
551
- });
552
- const result = await invocation.execute(abortSignal);
553
- const content = toToolCallContent(result) || {
554
- type: 'content',
555
- content: {
556
- type: 'text',
557
- text: `Successfully read: ${contentLabelsForDisplay.join(', ')}`,
558
- },
765
+ if (pathSpecsToRead.length > 0) {
766
+ const toolArgs = {
767
+ paths: pathSpecsToRead,
768
+ respectGitIgnore, // Use configuration setting
559
769
  };
560
- await this.sendUpdate({
561
- sessionUpdate: 'tool_call_update',
562
- toolCallId: callId,
563
- status: 'completed',
564
- content: content ? [content] : [],
565
- });
566
- if (Array.isArray(result.llmContent)) {
567
- const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
568
- processedQueryParts.push({
569
- text: '\n--- Content from referenced files ---',
770
+ const callId = `${readManyFilesTool.name}-${Date.now()}`;
771
+ try {
772
+ const invocation = readManyFilesTool.build(toolArgs);
773
+ await this.sendUpdate({
774
+ sessionUpdate: 'tool_call',
775
+ toolCallId: callId,
776
+ status: 'in_progress',
777
+ title: invocation.getDescription(),
778
+ content: [],
779
+ locations: invocation.toolLocations(),
780
+ kind: readManyFilesTool.kind,
570
781
  });
571
- for (const part of result.llmContent) {
572
- if (typeof part === 'string') {
573
- const match = fileContentRegex.exec(part);
574
- if (match) {
575
- const filePathSpecInContent = match[1]; // This is a resolved pathSpec
576
- const fileActualContent = match[2].trim();
577
- processedQueryParts.push({
578
- text: `\nContent from @${filePathSpecInContent}:\n`,
579
- });
580
- processedQueryParts.push({ text: fileActualContent });
782
+ const result = await invocation.execute(abortSignal);
783
+ const content = toToolCallContent(result) || {
784
+ type: 'content',
785
+ content: {
786
+ type: 'text',
787
+ text: `Successfully read: ${contentLabelsForDisplay.join(', ')}`,
788
+ },
789
+ };
790
+ await this.sendUpdate({
791
+ sessionUpdate: 'tool_call_update',
792
+ toolCallId: callId,
793
+ status: 'completed',
794
+ content: content ? [content] : [],
795
+ });
796
+ if (Array.isArray(result.llmContent)) {
797
+ const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
798
+ processedQueryParts.push({
799
+ text: '\n--- Content from referenced files ---',
800
+ });
801
+ for (const part of result.llmContent) {
802
+ if (typeof part === 'string') {
803
+ const match = fileContentRegex.exec(part);
804
+ if (match) {
805
+ const filePathSpecInContent = match[1]; // This is a resolved pathSpec
806
+ const fileActualContent = match[2].trim();
807
+ processedQueryParts.push({
808
+ text: `\nContent from @${filePathSpecInContent}:\n`,
809
+ });
810
+ processedQueryParts.push({ text: fileActualContent });
811
+ }
812
+ else {
813
+ processedQueryParts.push({ text: part });
814
+ }
581
815
  }
582
816
  else {
583
- processedQueryParts.push({ text: part });
817
+ // part is a Part object.
818
+ processedQueryParts.push(part);
584
819
  }
585
820
  }
586
- else {
587
- // part is a Part object.
588
- processedQueryParts.push(part);
589
- }
590
821
  }
591
- processedQueryParts.push({ text: '\n--- End of content ---' });
822
+ else {
823
+ console.warn('read_many_files tool returned no content or empty content.');
824
+ }
592
825
  }
593
- else {
594
- console.warn('read_many_files tool returned no content or empty content.');
826
+ catch (error) {
827
+ await this.sendUpdate({
828
+ sessionUpdate: 'tool_call_update',
829
+ toolCallId: callId,
830
+ status: 'failed',
831
+ content: [
832
+ {
833
+ type: 'content',
834
+ content: {
835
+ type: 'text',
836
+ text: `Error reading files (${contentLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
837
+ },
838
+ },
839
+ ],
840
+ });
841
+ throw error;
595
842
  }
596
- return processedQueryParts;
597
843
  }
598
- catch (error) {
599
- await this.sendUpdate({
600
- sessionUpdate: 'tool_call_update',
601
- toolCallId: callId,
602
- status: 'failed',
603
- content: [
604
- {
605
- type: 'content',
606
- content: {
607
- type: 'text',
608
- text: `Error reading files (${contentLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
609
- },
610
- },
611
- ],
844
+ if (embeddedContext.length > 0) {
845
+ processedQueryParts.push({
846
+ text: '\n--- Content from referenced context ---',
612
847
  });
613
- throw error;
848
+ for (const contextPart of embeddedContext) {
849
+ processedQueryParts.push({
850
+ text: `\nContent from @${contextPart.uri}:\n`,
851
+ });
852
+ if ('text' in contextPart) {
853
+ processedQueryParts.push({
854
+ text: contextPart.text,
855
+ });
856
+ }
857
+ else {
858
+ processedQueryParts.push({
859
+ inlineData: {
860
+ mimeType: contextPart.mimeType ?? 'application/octet-stream',
861
+ data: contextPart.blob,
862
+ },
863
+ });
864
+ }
865
+ }
614
866
  }
867
+ return processedQueryParts;
615
868
  }
616
869
  debug(msg) {
617
870
  if (this.config.getDebugMode()) {
@@ -620,6 +873,9 @@ class Session {
620
873
  }
621
874
  }
622
875
  function toToolCallContent(toolResult) {
876
+ if (toolResult.error?.message) {
877
+ throw new Error(toolResult.error.message);
878
+ }
623
879
  if (toolResult.returnDisplay) {
624
880
  if (typeof toolResult.returnDisplay === 'string') {
625
881
  return {