From c8bb9d6f59828a952ddd8daf88b5290f3369267f Mon Sep 17 00:00:00 2001 From: David Kaya Date: Wed, 25 Mar 2026 21:57:30 +0100 Subject: [PATCH] fix: align chat MCP tooling with session project Resolve chat tooling from the selected session's project instead of the separately tracked selectedProjectId so project-scoped discovered MCPs do not appear for the wrong session. Also synchronize selectedProjectId when a session is selected and add regression tests for both behaviors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/EryxAppService.ts | 12 +++++ src/renderer/App.tsx | 10 ++-- src/renderer/lib/chatTooling.ts | 14 ++++++ .../main/appServiceDiscoveredTooling.test.ts | 33 +++++++++++++ tests/renderer/discoveredToolingUi.test.ts | 48 +++++++++++++++++++ 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/renderer/lib/chatTooling.ts diff --git a/src/main/EryxAppService.ts b/src/main/EryxAppService.ts index e6c300c..fe4d7c0 100644 --- a/src/main/EryxAppService.ts +++ b/src/main/EryxAppService.ts @@ -852,6 +852,18 @@ export class EryxAppService extends EventEmitter { async selectSession(sessionId?: string): Promise { const workspace = await this.loadWorkspace(); + if (sessionId) { + const session = this.requireSession(workspace, sessionId); + const project = this.requireProject(workspace, session.projectId); + const didSyncProjectTooling = await this.syncProjectDiscoveredTooling(workspace, project); + if (didSyncProjectTooling) { + this.pruneUnavailableSessionToolingSelections(workspace); + await this.pruneUnavailableApprovalTools(workspace); + } + + workspace.selectedProjectId = session.projectId; + } + workspace.selectedSessionId = sessionId; return this.persistAndBroadcast(workspace); } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9c0ef4a..c47daae 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -7,6 +7,7 @@ import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingMo import { NewSessionModal } from '@renderer/components/NewSessionModal'; import { SettingsPanel } from '@renderer/components/SettingsPanel'; import { Sidebar } from '@renderer/components/Sidebar'; +import { resolveChatToolingSettings } from '@renderer/lib/chatTooling'; import { applySessionEventActivity, pruneSessionActivities, @@ -28,7 +29,6 @@ import { syncPatternGraph, type PatternDefinition } from '@shared/domain/pattern import { isScratchpadProject, SCRATCHPAD_PROJECT_ID } from '@shared/domain/project'; import { applyScratchpadSessionConfig } from '@shared/domain/session'; import type { AppearanceTheme, LspProfileDefinition, McpServerDefinition } from '@shared/domain/tooling'; -import { resolveProjectToolingSettings } from '@shared/domain/tooling'; import type { WorkspaceState } from '@shared/domain/workspace'; import { createId, nowIso } from '@shared/utils/ids'; @@ -180,9 +180,9 @@ export default function App() { [workspace?.projects, workspace?.selectedProjectId], ); - const effectiveTooling = useMemo( - () => workspace ? resolveProjectToolingSettings(workspace.settings, selectedProject?.discoveredTooling) : undefined, - [workspace?.settings, selectedProject?.discoveredTooling], + const chatToolingSettings = useMemo( + () => resolveChatToolingSettings(workspace, projectForSession), + [projectForSession, workspace], ); const hasPendingDiscoveries = useMemo(() => { @@ -274,7 +274,7 @@ export default function App() { project={projectForSession} runtimeTools={sidecarCapabilities?.runtimeTools} session={selectedSession} - toolingSettings={effectiveTooling ?? workspace.settings.tooling} + toolingSettings={chatToolingSettings ?? workspace.settings.tooling} /> ); detailPanel = ( diff --git a/src/renderer/lib/chatTooling.ts b/src/renderer/lib/chatTooling.ts new file mode 100644 index 0000000..0e4db00 --- /dev/null +++ b/src/renderer/lib/chatTooling.ts @@ -0,0 +1,14 @@ +import type { ProjectRecord } from '@shared/domain/project'; +import { resolveProjectToolingSettings, type WorkspaceToolingSettings } from '@shared/domain/tooling'; +import type { WorkspaceState } from '@shared/domain/workspace'; + +export function resolveChatToolingSettings( + workspace: WorkspaceState | undefined, + projectForSession: ProjectRecord | undefined, +): WorkspaceToolingSettings | undefined { + if (!workspace) { + return undefined; + } + + return resolveProjectToolingSettings(workspace.settings, projectForSession?.discoveredTooling); +} diff --git a/tests/main/appServiceDiscoveredTooling.test.ts b/tests/main/appServiceDiscoveredTooling.test.ts index 73c968e..7432501 100644 --- a/tests/main/appServiceDiscoveredTooling.test.ts +++ b/tests/main/appServiceDiscoveredTooling.test.ts @@ -224,4 +224,37 @@ describe('EryxAppService discovered tooling', () => { }, ]); }); + + test('selecting a session also selects that session project', async () => { + const workspace = createWorkspaceSeed(); + const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single'); + if (!pattern) { + throw new Error('Expected a single-agent pattern in the workspace seed.'); + } + + const projectAlpha = createProject({ + id: 'project-alpha', + name: 'alpha', + path: 'C:\\workspace\\alpha', + }); + const projectBeta = createProject({ + id: 'project-beta', + name: 'beta', + path: 'C:\\workspace\\beta', + }); + const session = createSession(projectBeta.id, pattern.id, { + id: 'session-beta', + }); + + workspace.projects = [projectAlpha, projectBeta]; + workspace.sessions = [session]; + workspace.selectedProjectId = projectAlpha.id; + + const service = createService(workspace, pattern); + + const updatedWorkspace = await service.selectSession(session.id); + + expect(updatedWorkspace.selectedSessionId).toBe(session.id); + expect(updatedWorkspace.selectedProjectId).toBe(projectBeta.id); + }); }); diff --git a/tests/renderer/discoveredToolingUi.test.ts b/tests/renderer/discoveredToolingUi.test.ts index c61427f..3b2f90f 100644 --- a/tests/renderer/discoveredToolingUi.test.ts +++ b/tests/renderer/discoveredToolingUi.test.ts @@ -1,8 +1,11 @@ import { describe, expect, test } from 'bun:test'; +import { resolveChatToolingSettings } from '@renderer/lib/chatTooling'; import type { DiscoveredLocalMcpServer } from '@shared/domain/discoveredTooling'; import { listPendingDiscoveredMcpServers, normalizeDiscoveredToolingState, type DiscoveredToolingState } from '@shared/domain/discoveredTooling'; +import type { ProjectRecord } from '@shared/domain/project'; import { resolveProjectToolingSettings, createWorkspaceSettings, type WorkspaceSettings } from '@shared/domain/tooling'; +import { createWorkspaceSeed } from '@shared/domain/workspace'; function makeDiscoveredServer( overrides: Partial & { id: string; name: string }, @@ -117,4 +120,49 @@ describe('frontend discovered tooling integration', () => { expect(projectDiscovered).toHaveLength(1); expect(projectDiscovered[0].name).toBe('proj-srv'); }); + + test('chat tooling follows the session project instead of the separately selected project', () => { + const workspace = createWorkspaceSeed(); + const sessionProject: ProjectRecord = { + id: 'project-session', + name: 'session-project', + path: 'C:\\workspace\\session-project', + addedAt: '2026-03-25T00:00:00.000Z', + discoveredTooling: normalizeDiscoveredToolingState({ + mcpServers: [ + makeDiscoveredServer({ + id: 'discovered_project_project_session_vscode_mcp_session_srv', + name: 'session-srv', + status: 'accepted', + scope: 'project', + }), + ], + }), + }; + const separatelySelectedProject: ProjectRecord = { + id: 'project-selected', + name: 'selected-project', + path: 'C:\\workspace\\selected-project', + addedAt: '2026-03-25T00:00:00.000Z', + discoveredTooling: normalizeDiscoveredToolingState({ + mcpServers: [ + makeDiscoveredServer({ + id: 'discovered_project_project_selected_vscode_mcp_selected_srv', + name: 'selected-srv', + status: 'accepted', + scope: 'project', + }), + ], + }), + }; + + workspace.projects = [sessionProject, separatelySelectedProject]; + workspace.selectedProjectId = separatelySelectedProject.id; + + const tooling = resolveChatToolingSettings(workspace, sessionProject); + const serverIds = tooling?.mcpServers.map((server) => server.id) ?? []; + + expect(serverIds).toContain('discovered_project_project_session_vscode_mcp_session_srv'); + expect(serverIds).not.toContain('discovered_project_project_selected_vscode_mcp_selected_srv'); + }); });