diff --git a/lib/shared/src/configuration.ts b/lib/shared/src/configuration.ts index 58c94a5ccb41..d761a83bf520 100644 --- a/lib/shared/src/configuration.ts +++ b/lib/shared/src/configuration.ts @@ -466,12 +466,20 @@ export interface FireworksCodeCompletionParams { export interface AgentToolboxSettings { /** - * The agent ID that user has currently enabled. + * The agent that user has currently enabled. */ - agent?: string + agent?: { + /** + * The name of the agent that user has currently enabled. E.g. "deep-cody" + */ + name?: string + } /** * Whether the user has enabled terminal context. * Defaulted to undefined if shell context is not enabled by site admin via feature flag. */ - shell?: boolean + shell?: { + enabled: boolean + error?: string + } } diff --git a/lib/shared/src/models/sync.ts b/lib/shared/src/models/sync.ts index 75027956a7cc..bd60b670e9f9 100644 --- a/lib/shared/src/models/sync.ts +++ b/lib/shared/src/models/sync.ts @@ -205,47 +205,52 @@ export function syncModels({ featureFlagProvider.evaluatedFeatureFlag( FeatureFlag.CodyEarlyAccess ), + featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.DeepCody), featureFlagProvider.evaluatedFeatureFlag( FeatureFlag.CodyChatDefaultToClaude35Haiku ), enableToolCody ).pipe( - switchMap(([hasEarlyAccess, defaultToHaiku]) => { - // TODO(sqs): remove waitlist from localStorage when user has access - const isOnWaitlist = config.clientState.waitlist_o1 - if (isDotComUser && (hasEarlyAccess || isOnWaitlist)) { - data.primaryModels = data.primaryModels.map(model => { - if (model.tags.includes(ModelTag.Waitlist)) { - const newTags = model.tags.filter( - tag => tag !== ModelTag.Waitlist - ) - newTags.push( - hasEarlyAccess - ? ModelTag.EarlyAccess - : ModelTag.OnWaitlist - ) - return { ...model, tags: newTags } + switchMap( + ([hasEarlyAccess, isDeepCodyEnabled, defaultToHaiku]) => { + // TODO(sqs): remove waitlist from localStorage when user has access + const isOnWaitlist = config.clientState.waitlist_o1 + if (isDotComUser && (hasEarlyAccess || isOnWaitlist)) { + data.primaryModels = data.primaryModels.map( + model => { + if (model.tags.includes(ModelTag.Waitlist)) { + const newTags = model.tags.filter( + tag => tag !== ModelTag.Waitlist + ) + newTags.push( + hasEarlyAccess + ? ModelTag.EarlyAccess + : ModelTag.OnWaitlist + ) + return { ...model, tags: newTags } + } + return model + } + ) + } + if (isDeepCodyEnabled && enableToolCody) { + data.primaryModels.push( + createModelFromServerModel(TOOL_CODY_MODEL) + ) + } + // set the default model to Haiku for free users + if (isDotComUser && isCodyFreeUser && defaultToHaiku) { + const haikuModel = data.primaryModels.find(m => + m.id.includes('claude-3-5-haiku') + ) + if (haikuModel) { + data.preferences!.defaults.chat = haikuModel.id } - return model - }) - } - if (enableToolCody) { - data.primaryModels.push( - createModelFromServerModel(TOOL_CODY_MODEL) - ) - } - // set the default model to Haiku for free users - if (isDotComUser && isCodyFreeUser && defaultToHaiku) { - const haikuModel = data.primaryModels.find(m => - m.id.includes('claude-3-5-haiku') - ) - if (haikuModel) { - data.preferences!.defaults.chat = haikuModel.id } - } - return Observable.of(data) - }) + return Observable.of(data) + } + ) ) }) ) diff --git a/vscode/src/chat/agentic/CodyTool.test.ts b/vscode/src/chat/agentic/CodyTool.test.ts index 8786cf7e348b..2aaa8b78aeeb 100644 --- a/vscode/src/chat/agentic/CodyTool.test.ts +++ b/vscode/src/chat/agentic/CodyTool.test.ts @@ -209,7 +209,7 @@ describe('CodyTool', () => { }) it('should register all default tools based on toolbox settings', () => { - const mockedToolboxSettings = { agent: 'mock-agent', shell: true } + const mockedToolboxSettings = { agent: { name: 'deep-cody' }, shell: { enabled: true } } vi.spyOn(toolboxManager, 'getSettings').mockReturnValue(mockedToolboxSettings) const localStorageData: { [key: string]: unknown } = {} mockLocalStorage({ @@ -226,7 +226,7 @@ describe('CodyTool', () => { expect(tools.some(t => t.config.title.includes('Terminal'))).toBeTruthy() // Disable shell and check if terminal tool is removed. - mockedToolboxSettings.shell = false + mockedToolboxSettings.shell.enabled = false vi.spyOn(toolboxManager, 'getSettings').mockReturnValue(mockedToolboxSettings) const newTools = CodyToolProvider.getTools() expect(newTools.some(t => t.config.title.includes('Terminal'))).toBeFalsy() diff --git a/vscode/src/chat/agentic/CodyTool.ts b/vscode/src/chat/agentic/CodyTool.ts index 9059959f1f91..1f18196bf9fb 100644 --- a/vscode/src/chat/agentic/CodyTool.ts +++ b/vscode/src/chat/agentic/CodyTool.ts @@ -69,17 +69,17 @@ export abstract class CodyTool { */ protected parse(): string[] { const { subTag } = this.config.tags - const regex = new RegExp(`<${subTag}>(.+?)`, 's') - const parsed = (this.unprocessedText.match(new RegExp(regex, 'g')) || []) - .map(match => regex.exec(match)?.[1].trim()) - .filter(Boolean) as string[] - const filtered = parsed.filter(q => !this.performedQueries.has(q)) - // Store the search query to avoid running the same query again. - for (const query of filtered) { + const regex = new RegExp(`<${subTag}>(.+?)`, 'gs') + // Use matchAll for more efficient iteration and destructuring + const newQueries = [...this.unprocessedText.matchAll(regex)] + .map(([, group]) => group?.trim()) + .filter(query => query && !this.performedQueries.has(query)) + // Add all new queries to the set at once + for (const query of newQueries) { this.performedQueries.add(query) } this.reset() - return filtered + return newQueries } /** * The raw text input stream. diff --git a/vscode/src/chat/agentic/CodyToolProvider.test.ts b/vscode/src/chat/agentic/CodyToolProvider.test.ts index 419c8d167449..0152b3d9ee5c 100644 --- a/vscode/src/chat/agentic/CodyToolProvider.test.ts +++ b/vscode/src/chat/agentic/CodyToolProvider.test.ts @@ -89,13 +89,19 @@ describe('CodyToolProvider', () => { }) it('should not include CLI tool if shell is disabled', () => { - vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ agent: 'deep-cody', shell: false }) + vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ + agent: { name: 'deep-cody' }, + shell: { enabled: false }, + }) const tools = CodyToolProvider.getTools() expect(tools.some(tool => tool.config.title === 'Terminal')).toBe(false) }) it('should include CLI tool if shell is enabled', () => { - vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ agent: 'deep-cody', shell: true }) + vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ + agent: { name: 'deep-cody' }, + shell: { enabled: true }, + }) const tools = CodyToolProvider.getTools() expect(tools.some(tool => tool.config.title === 'Terminal')).toBe(true) }) diff --git a/vscode/src/chat/agentic/CodyToolProvider.ts b/vscode/src/chat/agentic/CodyToolProvider.ts index 167742236ad9..e939aaccc031 100644 --- a/vscode/src/chat/agentic/CodyToolProvider.ts +++ b/vscode/src/chat/agentic/CodyToolProvider.ts @@ -13,6 +13,8 @@ import { type CodyTool, type CodyToolConfig, OpenCtxTool, TOOL_CONFIGS } from '. import { toolboxManager } from './ToolboxManager' import { OPENCTX_TOOL_CONFIG } from './config' +type Retriever = Pick + /** * Interface for tool execution status callbacks. * Used to track and report tool execution progress. @@ -29,7 +31,7 @@ export interface ToolStatusCallback { */ export interface ToolConfiguration extends CodyToolConfig { name: string - createInstance: (config: CodyToolConfig, ...args: any[]) => CodyTool + createInstance: (config: CodyToolConfig, retriever?: Retriever) => CodyTool } /** @@ -44,30 +46,30 @@ export interface ToolConfiguration extends CodyToolConfig { export class ToolFactory { private tools: Map = new Map() - constructor(private contextRetriever: Pick) {} + constructor(private contextRetriever: Retriever) {} public register(toolConfig: ToolConfiguration): void { this.tools.set(toolConfig.name, toolConfig) } - public createTool(name: string, ...args: any[]): CodyTool | undefined { + public createTool(name: string, retriever?: Retriever): CodyTool | undefined { const config = this.tools.get(name) if (!config) { return undefined } - const instance = config.createInstance(config, ...args) + const instance = config.createInstance(config, retriever) return instance } public getInstances(): CodyTool[] { // Create fresh instances of all registered tools return Array.from(this.tools.entries()) - .filter(([name]) => name !== 'CliTool' || toolboxManager.getSettings()?.shell) + .filter(([name]) => name !== 'CliTool' || toolboxManager.getSettings()?.shell?.enabled) .map(([_, config]) => config.createInstance(config, this.contextRetriever)) .filter(isDefined) as CodyTool[] } - public createDefaultTools(contextRetriever?: Pick): CodyTool[] { + public createDefaultTools(contextRetriever?: Retriever): CodyTool[] { return Object.entries(TOOL_CONFIGS) .map(([name]) => this.createTool(name, contextRetriever)) .filter(isDefined) as CodyTool[] @@ -90,8 +92,16 @@ export class ToolFactory { private generateToolName(provider: ContextMentionProviderMetadata): string { const suffix = provider.id.includes('modelcontextprotocol') ? 'MCP' : '' - const title = provider.title.replace(' ', '').split('/').at(-1) - return 'TOOL' + title?.toUpperCase().replace(/[^a-zA-Z0-9]/g, '') + suffix + return ( + 'TOOL' + + provider.title + .split('/') + .pop() + ?.replace(/\s+/g, '') + ?.toUpperCase() + ?.replace(/[^A-Z0-9]/g, '') + + suffix + ) } private getToolConfig(provider: ContextMentionProviderMetadata): CodyToolConfig { @@ -140,7 +150,7 @@ export namespace CodyToolProvider { export let factory: ToolFactory let openCtxSubscription: Unsubscribable | undefined - export function initialize(contextRetriever: Pick): void { + export function initialize(contextRetriever: Retriever): void { factory = new ToolFactory(contextRetriever) initializeRegistry() } @@ -151,17 +161,12 @@ export namespace CodyToolProvider { } export function setupOpenCtxProviderListener(): void { - if (!openCtx.controller) { - console.error('OpenCtx controller not available') + if (!openCtxSubscription && factory && openCtx.controller) { + openCtxSubscription = openCtx.controller + .metaChanges({}, {}) + .pipe(map(providers => providers.filter(p => !!p.mentions).map(openCtxProviderMetadata))) + .subscribe(providerMeta => factory?.createOpenCtxTools(providerMeta)) } - if (openCtxSubscription || !openCtx.controller) { - return - } - - openCtxSubscription = openCtx.controller - .metaChanges({}, {}) - .pipe(map(providers => providers.filter(p => !!p.mentions).map(openCtxProviderMetadata))) - .subscribe(providerMeta => factory.createOpenCtxTools(providerMeta)) } function initializeRegistry(): void { @@ -170,7 +175,12 @@ export namespace CodyToolProvider { name, ...tool.prototype.config, createInstance: useContextRetriever - ? (_, contextRetriever) => new tool(contextRetriever) + ? (_, contextRetriever) => { + if (!contextRetriever) { + throw new Error(`Context retriever required for ${name}`) + } + return new tool(contextRetriever) + } : () => new tool(), }) } diff --git a/vscode/src/chat/agentic/ToolboxManager.ts b/vscode/src/chat/agentic/ToolboxManager.ts index cb03acb1fcb3..4defa5506517 100644 --- a/vscode/src/chat/agentic/ToolboxManager.ts +++ b/vscode/src/chat/agentic/ToolboxManager.ts @@ -32,7 +32,7 @@ type StoredToolboxSettings = { * NOTE: This is a Singleton class. */ class ToolboxManager { - private static readonly STORAGE_KEY = 'CODY_CHATAGENTS_TOOLBOX_SETTINGS' + private static readonly STORAGE_KEY = 'CODYAGENT_TOOLBOX_SETTINGS' private static instance?: ToolboxManager private constructor() { @@ -51,8 +51,8 @@ class ToolboxManager { private getStoredUserSettings(): StoredToolboxSettings { return ( localStorage.get(ToolboxManager.STORAGE_KEY) ?? { - agent: undefined, - shell: this.shellConfig.user, + agent: this.isEnabled ? 'deep-cody' : undefined, + shell: false, } ) } @@ -62,20 +62,31 @@ class ToolboxManager { return null } const { agent, shell } = this.getStoredUserSettings() + const isShellEnabled = this.shellConfig.instance && this.shellConfig.client ? shell : undefined return { - agent, + agent: { name: agent }, // Only show shell option if it's supported by instance and client. - shell: this.shellConfig.instance && this.shellConfig.client ? shell : undefined, + shell: { enabled: isShellEnabled ?? false }, } } - public async updateToolboxSettings(settings: AgentToolboxSettings): Promise { + public async updateSettings(settings: AgentToolboxSettings): Promise { logDebug('ToolboxManager', 'Updating toolbox settings', { verbose: settings }) - await localStorage.set(ToolboxManager.STORAGE_KEY, settings) + await localStorage.set(ToolboxManager.STORAGE_KEY, { + agent: settings.agent?.name, + shell: settings.shell?.enabled ?? false, + }) this.changeNotifications.next() } - - public readonly settings: Observable = combineLatest( + /** + * Returns a real-time Observable stream of toolbox settings that updates when any of the following changes: + * - Feature flags + * - User subscription + * - Available models + * - Manual settings updates + * Use this when you need to react to settings changes over time. + */ + public readonly observable: Observable = combineLatest( featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.DeepCody), featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.ContextAgentDefaultChatModel), featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.DeepCodyShellContext), diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 5ebbcd49f3de..0b344324444f 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -663,7 +663,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv editorState, intent: detectedIntent, manuallySelectedIntent: manuallySelectedIntent ? detectedIntent : undefined, - agent: toolboxManager.getSettings()?.agent, + agent: toolboxManager.getSettings()?.agent?.name, }) this.postViewTranscript({ speaker: 'assistant' }) @@ -1544,10 +1544,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv userProductSubscription.pipe( map(value => (value === pendingOperation ? null : value)) ), - toolboxSettings: () => toolboxManager.settings, + toolboxSettings: () => toolboxManager.observable, updateToolboxSettings: settings => { return promiseFactoryToObservable(async () => { - await toolboxManager.updateToolboxSettings(settings) + await toolboxManager.updateSettings(settings) }) }, } diff --git a/vscode/src/chat/chat-view/handlers/DeepCodyHandler.ts b/vscode/src/chat/chat-view/handlers/DeepCodyHandler.ts index 8d320083affe..f5f26f1f0c81 100644 --- a/vscode/src/chat/chat-view/handlers/DeepCodyHandler.ts +++ b/vscode/src/chat/chat-view/handlers/DeepCodyHandler.ts @@ -6,7 +6,6 @@ import { featureFlagProvider, storeLastValue, } from '@sourcegraph/cody-shared' -import { CodyToolProvider } from '../../agentic/CodyToolProvider' import { DeepCodyAgent } from '../../agentic/DeepCody' import { DeepCodyRateLimiter } from '../../agentic/DeepCodyRateLimiter' import type { ChatBuilder } from '../ChatBuilder' @@ -34,8 +33,6 @@ export class DeepCodyHandler extends ChatHandler implements AgentHandler { error?: Error abort?: boolean }> { - CodyToolProvider.initialize(this.contextRetriever) - // NOTE: Skip query rewrite for deep-cody as the agent will reviewed and rewrite the query. const skipQueryRewrite = true const baseContextResult = await super.computeContext( diff --git a/vscode/src/main.ts b/vscode/src/main.ts index c751632a17d8..1130f3fe0e80 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -49,6 +49,7 @@ import { showSignInMenu, showSignOutMenu, tokenCallbackHandler } from './auth/au import { AutoeditsProvider } from './autoedits/autoedits-provider' import { registerAutoEditTestRenderCommand } from './autoedits/renderer/mock-renderer' import type { MessageProviderOptions } from './chat/MessageProvider' +import { CodyToolProvider } from './chat/agentic/CodyToolProvider' import { ChatsController, CodyChatEditorViewType } from './chat/chat-view/ChatsController' import { ContextRetriever } from './chat/chat-view/ContextRetriever' import type { ChatIntentAPIClient } from './chat/context/chatIntentAPIClient' @@ -261,6 +262,8 @@ const register = async ( ) disposables.push(chatsController) + CodyToolProvider.initialize(contextRetriever) + disposables.push( subscriptionDisposable( exposeOpenCtxClient(context, platform.createOpenCtxController).subscribe({}) diff --git a/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/ToolboxButton.tsx b/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/ToolboxButton.tsx index 6329184a990a..f44bc5a25268 100644 --- a/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/ToolboxButton.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/ToolboxButton.tsx @@ -79,13 +79,15 @@ export const ToolboxButton: FC = memo(({ settings, api }) =>

Self-Reflection Context Agent

setSettingsForm({ ...settingsForm, - agent: settingsForm.agent - ? undefined - : 'deep-cody', + agent: { + name: settingsForm.agent?.name + ? undefined + : 'deep-cody', // TODO: update name when finalized. + }, }) } /> @@ -93,36 +95,32 @@ export const ToolboxButton: FC = memo(({ settings, api }) =>
{ToolboxOptionText.agentic}
- {settings.shell !== undefined && ( - <> -
-

- Terminal Context Agent -

- - setSettingsForm({ - ...settingsForm, - shell: - !!settingsForm.agent && - !settingsForm.shell, - }) - } - /> -
-
- Enable with caution as mistakes are possible. -
-
- {ToolboxOptionText.terminal} -
- - )} +
+

Terminal Context Agent

+ + setSettingsForm({ + ...settingsForm, + shell: { + enabled: + !!settingsForm.agent?.name && + !settingsForm.shell?.enabled, + }, + }) + } + /> +
+
+ Enable with caution as mistakes are possible. +
+
+ {ToolboxOptionText.terminal} +
@@ -153,7 +151,7 @@ export const ToolboxButton: FC = memo(({ settings, api }) => >