From 1977dc941e7c16635bca815145c5a66d7ed85d5a Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:27:36 -0300 Subject: [PATCH 1/2] fix(cli): reload MCP config after project selection --- .../__tests__/utils/project-picker.test.ts | 73 ++++++++++++++++++- cli/src/index.tsx | 19 +++-- cli/src/utils/project-picker.ts | 22 ++++++ 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/cli/src/__tests__/utils/project-picker.test.ts b/cli/src/__tests__/utils/project-picker.test.ts index d0bd4fa48a..201a68a08f 100644 --- a/cli/src/__tests__/utils/project-picker.test.ts +++ b/cli/src/__tests__/utils/project-picker.test.ts @@ -1,8 +1,24 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' +import os from 'os' import path from 'path' import { describe, test, expect } from 'bun:test' -import { shouldShowProjectPicker } from '../../utils/project-picker' +import { + getProjectRoot, + setProjectRoot, + tryGetProjectRoot, +} from '../../project-files' +import { + __resetLocalAgentRegistryForTests, + getLoadedMCPServers, + initializeAgentRegistry, + loadAgentDefinitions, +} from '../../utils/local-agent-registry' +import { + activateProject, + shouldShowProjectPicker, +} from '../../utils/project-picker' describe('cli/utils/project-picker', () => { test('returns true when start cwd is home directory', () => { @@ -36,4 +52,59 @@ describe('cli/utils/project-picker', () => { expect(shouldShowProjectPicker(siblingDir, homeDir)).toBe(false) }) + + test('reloads MCP servers after selecting a project', async () => { + const originalCwd = process.cwd() + const originalProjectRoot = tryGetProjectRoot() + const tempDir = mkdtempSync(path.join(os.tmpdir(), 'freebuff-project-')) + const launchDir = path.join(tempDir, 'launch') + const projectDir = path.join(tempDir, 'project') + const agentsDir = path.join(projectDir, '.agents') + + mkdirSync(launchDir) + mkdirSync(agentsDir, { recursive: true }) + writeFileSync( + path.join(agentsDir, 'mcp.json'), + JSON.stringify({ + mcpServers: { + projectPickerServer: { + command: 'node', + args: ['server.js'], + }, + }, + }), + ) + + try { + process.chdir(launchDir) + setProjectRoot(launchDir) + __resetLocalAgentRegistryForTests() + await initializeAgentRegistry() + + expect(getLoadedMCPServers().projectPickerServer).toBeUndefined() + + await activateProject(projectDir) + + expect(process.cwd()).toBe(projectDir) + expect(getProjectRoot()).toBe(projectDir) + expect(getLoadedMCPServers().projectPickerServer).toMatchObject({ + command: 'node', + args: ['server.js'], + }) + + const baseAgent = loadAgentDefinitions().find((definition) => + definition.id.startsWith('base'), + ) + expect(baseAgent).toBeDefined() + expect(baseAgent?.mcpServers?.projectPickerServer).toMatchObject({ + command: 'node', + args: ['server.js'], + }) + } finally { + process.chdir(originalCwd) + setProjectRoot(originalProjectRoot ?? originalCwd) + __resetLocalAgentRegistryForTests() + rmSync(tempDir, { recursive: true, force: true }) + } + }) }) diff --git a/cli/src/index.tsx b/cli/src/index.tsx index cae4e380eb..9098b75fa7 100644 --- a/cli/src/index.tsx +++ b/cli/src/index.tsx @@ -28,17 +28,19 @@ import { loadPackageVersion, parseArgs } from './cli-args' import { handlePublish } from './commands/publish' import { runPlainLogin } from './login/plain-login' import { initializeApp } from './init/init-app' -import { getProjectRoot, setProjectRoot } from './project-files' +import { getProjectRoot } from './project-files' import { trackEvent } from './utils/analytics' import { getAuthToken, getAuthTokenDetails } from './utils/auth' -import { resetCodebuffClient } from './utils/codebuff-client' import { setApiClientAuthToken } from './utils/codebuff-api' import { IS_FREEBUFF } from './utils/constants' import { initializeAgentRegistry } from './utils/local-agent-registry' import { trimOversizedChatLogs } from './utils/chat-history' import { clearLogFile, logger } from './utils/logger' import { drainClientLogs } from './utils/log-shipper' -import { shouldShowProjectPicker } from './utils/project-picker' +import { + activateProject, + shouldShowProjectPicker, +} from './utils/project-picker' import { saveRecentProject } from './utils/recent-projects' import { startEngagementTracking } from './utils/engagement' import { @@ -343,8 +345,9 @@ async function main(): Promise { // Callback for when user selects a new project from the picker const handleProjectChange = React.useCallback( async (newProjectPath: string) => { - // Change process working directory - process.chdir(newProjectPath) + await activateProject(newProjectPath, { + reloadAgentRegistry: !hasAgentOverride, + }) // Track directory change (avoid logging full paths for privacy) const isGitRepo = fs.existsSync(path.join(newProjectPath, '.git')) @@ -354,10 +357,6 @@ async function main(): Promise { pathDepth, isHomeDir: newProjectPath === os.homedir(), }) - // Update the project root in the module state - setProjectRoot(newProjectPath) - // Reset client to ensure tools use the updated project root - resetCodebuffClient() // Save to recent projects list saveRecentProject(newProjectPath) // Update local state @@ -367,7 +366,7 @@ async function main(): Promise { // Hide the picker and show the chat setShowProjectPickerScreen(false) }, - [], + [hasAgentOverride], ) return ( diff --git a/cli/src/utils/project-picker.ts b/cli/src/utils/project-picker.ts index 0fa732a6c4..eacb08c4e8 100644 --- a/cli/src/utils/project-picker.ts +++ b/cli/src/utils/project-picker.ts @@ -1,5 +1,27 @@ import path from 'path' +import { setProjectRoot } from '../project-files' +import { resetCodebuffClient } from './codebuff-client' +import { initializeAgentRegistry } from './local-agent-registry' + +interface ActivateProjectOptions { + reloadAgentRegistry?: boolean +} + +export async function activateProject( + projectPath: string, + { reloadAgentRegistry = true }: ActivateProjectOptions = {}, +): Promise { + process.chdir(projectPath) + setProjectRoot(projectPath) + + if (reloadAgentRegistry) { + await initializeAgentRegistry() + } + + resetCodebuffClient() +} + export function shouldShowProjectPicker( startCwd: string, homeDir: string, From 2f246ab886b6451b4c26beff80fb8f253c2e0603 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:01:26 -0300 Subject: [PATCH 2/2] fix(cli): clear local agent caches on project change --- .../__tests__/utils/project-picker.test.ts | 37 ++++++++++++++++++- cli/src/utils/local-agent-registry.ts | 13 +++++++ cli/src/utils/project-picker.ts | 4 +- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/cli/src/__tests__/utils/project-picker.test.ts b/cli/src/__tests__/utils/project-picker.test.ts index 201a68a08f..079fb5d8db 100644 --- a/cli/src/__tests__/utils/project-picker.test.ts +++ b/cli/src/__tests__/utils/project-picker.test.ts @@ -11,9 +11,11 @@ import { } from '../../project-files' import { __resetLocalAgentRegistryForTests, + findAgentsDirectory, getLoadedMCPServers, initializeAgentRegistry, loadAgentDefinitions, + loadLocalAgents, } from '../../utils/local-agent-registry' import { activateProject, @@ -53,16 +55,35 @@ describe('cli/utils/project-picker', () => { expect(shouldShowProjectPicker(siblingDir, homeDir)).toBe(false) }) - test('reloads MCP servers after selecting a project', async () => { + test('reloads local agents and MCP servers after selecting a project', async () => { const originalCwd = process.cwd() const originalProjectRoot = tryGetProjectRoot() const tempDir = mkdtempSync(path.join(os.tmpdir(), 'freebuff-project-')) const launchDir = path.join(tempDir, 'launch') + const launchAgentsDir = path.join(launchDir, '.agents') const projectDir = path.join(tempDir, 'project') const agentsDir = path.join(projectDir, '.agents') - mkdirSync(launchDir) + mkdirSync(launchAgentsDir, { recursive: true }) mkdirSync(agentsDir, { recursive: true }) + writeFileSync( + path.join(launchAgentsDir, 'launch-agent.ts'), + `export default { + id: 'launch-project-agent', + displayName: 'Launch Project Agent', + model: 'anthropic/claude-sonnet-4', + instructions: 'Loaded from the launch project' + }`, + ) + writeFileSync( + path.join(agentsDir, 'selected-agent.ts'), + `export default { + id: 'selected-project-agent', + displayName: 'Selected Project Agent', + model: 'anthropic/claude-sonnet-4', + instructions: 'Loaded from the selected project' + }`, + ) writeFileSync( path.join(agentsDir, 'mcp.json'), JSON.stringify({ @@ -81,12 +102,24 @@ describe('cli/utils/project-picker', () => { __resetLocalAgentRegistryForTests() await initializeAgentRegistry() + expect(findAgentsDirectory()).toBe(launchAgentsDir) + expect( + loadLocalAgents().find((agent) => agent.id === 'launch-project-agent'), + ).toBeDefined() expect(getLoadedMCPServers().projectPickerServer).toBeUndefined() await activateProject(projectDir) expect(process.cwd()).toBe(projectDir) expect(getProjectRoot()).toBe(projectDir) + expect(findAgentsDirectory()).toBe(agentsDir) + const localAgents = loadLocalAgents() + expect( + localAgents.find((agent) => agent.id === 'launch-project-agent'), + ).toBeUndefined() + expect( + localAgents.find((agent) => agent.id === 'selected-project-agent'), + ).toBeDefined() expect(getLoadedMCPServers().projectPickerServer).toMatchObject({ command: 'node', args: ['server.js'], diff --git a/cli/src/utils/local-agent-registry.ts b/cli/src/utils/local-agent-registry.ts index 1781e50db3..fec30fce46 100644 --- a/cli/src/utils/local-agent-registry.ts +++ b/cli/src/utils/local-agent-registry.ts @@ -89,6 +89,19 @@ export async function initializeAgentRegistry(): Promise { } } +/** + * Reload the local agent registry after the active project changes. + * + * The derived agent-list and directory caches depend on the current working + * directory, so they must be cleared before the registry is initialized for + * the new project. + */ +export async function reloadLocalAgentRegistry(): Promise { + cachedAgentsByMode.clear() + cachedAgentsDir = null + await initializeAgentRegistry() +} + /** * Get default agent directories to scan. * Matches the SDK's getDefaultAgentDirs() to ensure consistency. diff --git a/cli/src/utils/project-picker.ts b/cli/src/utils/project-picker.ts index eacb08c4e8..7b9921c2d6 100644 --- a/cli/src/utils/project-picker.ts +++ b/cli/src/utils/project-picker.ts @@ -2,7 +2,7 @@ import path from 'path' import { setProjectRoot } from '../project-files' import { resetCodebuffClient } from './codebuff-client' -import { initializeAgentRegistry } from './local-agent-registry' +import { reloadLocalAgentRegistry } from './local-agent-registry' interface ActivateProjectOptions { reloadAgentRegistry?: boolean @@ -16,7 +16,7 @@ export async function activateProject( setProjectRoot(projectPath) if (reloadAgentRegistry) { - await initializeAgentRegistry() + await reloadLocalAgentRegistry() } resetCodebuffClient()