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/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..564c9c63fef0 100644 --- a/vscode/src/chat/agentic/CodyToolProvider.ts +++ b/vscode/src/chat/agentic/CodyToolProvider.ts @@ -62,7 +62,7 @@ export class ToolFactory { 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[] } @@ -90,8 +90,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 { @@ -151,7 +159,7 @@ export namespace CodyToolProvider { } export function setupOpenCtxProviderListener(): void { - if (!openCtx.controller) { + if (!openCtx.controller || !factory) { console.error('OpenCtx controller not available') } if (openCtxSubscription || !openCtx.controller) { @@ -161,7 +169,7 @@ export namespace CodyToolProvider { openCtxSubscription = openCtx.controller .metaChanges({}, {}) .pipe(map(providers => providers.filter(p => !!p.mentions).map(openCtxProviderMetadata))) - .subscribe(providerMeta => factory.createOpenCtxTools(providerMeta)) + .subscribe(providerMeta => factory?.createOpenCtxTools(providerMeta)) } function initializeRegistry(): void { 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/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 }) => >