diff --git a/src/browser/components/Settings/sections/ProjectSettingsSection.tsx b/src/browser/components/Settings/sections/ProjectSettingsSection.tsx index 76ceb2aa5..df9387004 100644 --- a/src/browser/components/Settings/sections/ProjectSettingsSection.tsx +++ b/src/browser/components/Settings/sections/ProjectSettingsSection.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useAPI } from "@/browser/contexts/API"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; +import { useSettings } from "@/browser/contexts/SettingsContext"; import { Trash2, Play, @@ -174,10 +175,11 @@ const ToolAllowlistSection: React.FC<{ export const ProjectSettingsSection: React.FC = () => { const { api } = useAPI(); const { projects } = useProjectContext(); + const { initialProjectPath } = useSettings(); const projectList = Array.from(projects.keys()); // Core state - const [selectedProject, setSelectedProject] = useState(""); + const [selectedProject, setSelectedProject] = useState(initialProjectPath ?? ""); const [servers, setServers] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -225,12 +227,14 @@ export const ProjectSettingsSection: React.FC = () => { setIdleHoursInput(idleHours?.toString() ?? "24"); }, [idleHours]); - // Set default project when projects load + // Set project when initialProjectPath changes or default to first project useEffect(() => { - if (projectList.length > 0 && !selectedProject) { + if (initialProjectPath && projectList.includes(initialProjectPath)) { + setSelectedProject(initialProjectPath); + } else if (projectList.length > 0 && !selectedProject) { setSelectedProject(projectList[0]); } - }, [projectList, selectedProject]); + }, [projectList, selectedProject, initialProjectPath]); const refresh = useCallback(async () => { if (!api || !selectedProject) return; diff --git a/src/browser/components/WorkspaceHeader.tsx b/src/browser/components/WorkspaceHeader.tsx index f902da58b..cea9e96d1 100644 --- a/src/browser/components/WorkspaceHeader.tsx +++ b/src/browser/components/WorkspaceHeader.tsx @@ -3,7 +3,7 @@ import { Pencil, Server } from "lucide-react"; import { GitStatusIndicator } from "./GitStatusIndicator"; import { RuntimeBadge } from "./RuntimeBadge"; import { BranchSelector } from "./BranchSelector"; -import { WorkspaceMCPModal } from "./WorkspaceMCPModal"; + import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { useGitStatus } from "@/browser/stores/GitStatusStore"; @@ -13,6 +13,7 @@ import type { RuntimeConfig } from "@/common/types/runtime"; import { useTutorial } from "@/browser/contexts/TutorialContext"; import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal"; import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor"; +import { useSettings } from "@/browser/contexts/SettingsContext"; interface WorkspaceHeaderProps { workspaceId: string; @@ -36,8 +37,8 @@ export const WorkspaceHeader: React.FC = ({ const gitStatus = useGitStatus(workspaceId); const { canInterrupt } = useWorkspaceSidebarState(workspaceId); const { startSequence: startTutorial, isSequenceCompleted } = useTutorial(); + const { open: openSettings } = useSettings(); const [editorError, setEditorError] = useState(null); - const [mcpModalOpen, setMcpModalOpen] = useState(false); const handleOpenTerminal = useCallback(() => { openTerminal(workspaceId, runtimeConfig); @@ -96,7 +97,7 @@ export const WorkspaceHeader: React.FC = ({ - Configure MCP servers for this workspace + Configure MCP servers for this project @@ -141,12 +142,6 @@ export const WorkspaceHeader: React.FC = ({ - ); }; diff --git a/src/browser/contexts/SettingsContext.tsx b/src/browser/contexts/SettingsContext.tsx index 4041520c9..311dc8375 100644 --- a/src/browser/contexts/SettingsContext.tsx +++ b/src/browser/contexts/SettingsContext.tsx @@ -10,7 +10,9 @@ import React, { interface SettingsContextValue { isOpen: boolean; activeSection: string; - open: (section?: string) => void; + /** Pre-selected project path when opening the projects section */ + initialProjectPath: string | null; + open: (section?: string, projectPath?: string) => void; close: () => void; setActiveSection: (section: string) => void; } @@ -28,9 +30,11 @@ const DEFAULT_SECTION = "general"; export function SettingsProvider(props: { children: ReactNode }) { const [isOpen, setIsOpen] = useState(false); const [activeSection, setActiveSection] = useState(DEFAULT_SECTION); + const [initialProjectPath, setInitialProjectPath] = useState(null); - const open = useCallback((section?: string) => { + const open = useCallback((section?: string, projectPath?: string) => { if (section) setActiveSection(section); + setInitialProjectPath(projectPath ?? null); setIsOpen(true); }, []); @@ -42,11 +46,12 @@ export function SettingsProvider(props: { children: ReactNode }) { () => ({ isOpen, activeSection, + initialProjectPath, open, close, setActiveSection, }), - [isOpen, activeSection, open, close] + [isOpen, activeSection, initialProjectPath, open, close] ); return {props.children}; diff --git a/src/browser/stories/App.mcp.stories.tsx b/src/browser/stories/App.mcp.stories.tsx index f00c06d6c..88a30a943 100644 --- a/src/browser/stories/App.mcp.stories.tsx +++ b/src/browser/stories/App.mcp.stories.tsx @@ -158,20 +158,24 @@ async function openProjectSettings(canvasElement: HTMLElement): Promise { mcpHeading.scrollIntoView({ block: "start" }); } -/** Open the workspace MCP modal */ -async function openWorkspaceMCPModal(canvasElement: HTMLElement): Promise { +/** Open project settings by clicking MCP button in workspace header */ +async function openProjectSettingsViaMCPButton(canvasElement: HTMLElement): Promise { const canvas = within(canvasElement); const body = within(canvasElement.ownerDocument.body); // Wait for workspace header to load await canvas.findByTestId("workspace-header", {}, { timeout: 10000 }); - // Click the MCP server button in the header + // Click the MCP server button in the header - now opens project settings const mcpButton = await canvas.findByTestId("workspace-mcp-button"); await userEvent.click(mcpButton); - // Wait for dialog + // Wait for settings dialog await body.findByRole("dialog"); + + // Scroll to MCP Servers section + const mcpHeading = await body.findByText("MCP Servers"); + mcpHeading.scrollIntoView({ behavior: "instant", block: "start" }); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -283,11 +287,11 @@ export const ProjectSettingsWithToolAllowlist: AppStory = { }; // ═══════════════════════════════════════════════════════════════════════════════ -// WORKSPACE MCP MODAL STORIES +// MCP BUTTON NAVIGATION STORIES // ═══════════════════════════════════════════════════════════════════════════════ -/** Workspace MCP modal with servers from project (no overrides) */ -export const WorkspaceMCPNoOverrides: AppStory = { +/** MCP button in header opens project settings with MCP servers visible */ +export const MCPButtonOpensProjectSettings: AppStory = { render: () => ( @@ -306,48 +310,18 @@ export const WorkspaceMCPNoOverrides: AppStory = { /> ), play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); + await openProjectSettingsViaMCPButton(canvasElement); const body = within(canvasElement.ownerDocument.body); - // Both servers should be shown and enabled + // Both servers should be shown in project settings await body.findByText("mux"); await body.findByText("posthog"); }, }; -/** Workspace MCP modal - server disabled at project level, can be enabled */ -export const WorkspaceMCPProjectDisabledServer: AppStory = { - render: () => ( - - setupMCPStory({ - servers: { - mux: { command: "npx -y @anthropics/mux-server", disabled: false }, - posthog: { command: "npx -y posthog-mcp-server", disabled: true }, - }, - testResults: { - mux: MOCK_TOOLS, - posthog: POSTHOG_TOOLS, - }, - preCacheTools: true, - }) - } - /> - ), - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); - - const body = within(canvasElement.ownerDocument.body); - - // posthog should show "(disabled at project level)" but switch should still be toggleable - await body.findByText("posthog"); - await body.findByText(/disabled at project level/i); - }, -}; - -/** Workspace MCP modal - server disabled at project level, enabled at workspace level */ -export const WorkspaceMCPEnabledOverride: AppStory = { +/** MCP button opens project settings with disabled server */ +export const MCPButtonWithDisabledServer: AppStory = { render: () => ( @@ -356,44 +330,6 @@ export const WorkspaceMCPEnabledOverride: AppStory = { mux: { command: "npx -y @anthropics/mux-server", disabled: false }, posthog: { command: "npx -y posthog-mcp-server", disabled: true }, }, - workspaceOverrides: { - enabledServers: ["posthog"], - }, - testResults: { - mux: MOCK_TOOLS, - posthog: POSTHOG_TOOLS, - }, - preCacheTools: true, - }) - } - /> - ), - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); - - const body = within(canvasElement.ownerDocument.body); - - // posthog should be enabled despite project-level disable - await body.findByText("posthog"); - await body.findByText(/disabled at project level/i); - - // The switch should be ON (enabled at workspace level) - }, -}; - -/** Workspace MCP modal - server enabled at project level, disabled at workspace level */ -export const WorkspaceMCPDisabledOverride: AppStory = { - render: () => ( - - setupMCPStory({ - servers: { - mux: { command: "npx -y @anthropics/mux-server", disabled: false }, - posthog: { command: "npx -y posthog-mcp-server", disabled: false }, - }, - workspaceOverrides: { - disabledServers: ["posthog"], - }, testResults: { mux: MOCK_TOOLS, posthog: POSTHOG_TOOLS, @@ -404,54 +340,21 @@ export const WorkspaceMCPDisabledOverride: AppStory = { /> ), play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); + await openProjectSettingsViaMCPButton(canvasElement); const body = within(canvasElement.ownerDocument.body); - // mux should be enabled, posthog should be disabled + // Both servers should be visible await body.findByText("mux"); await body.findByText("posthog"); }, }; -/** Workspace MCP modal with tool allowlist filtering */ -export const WorkspaceMCPWithToolAllowlist: AppStory = { - render: () => ( - - setupMCPStory({ - servers: { - posthog: { command: "npx -y posthog-mcp-server", disabled: false }, - }, - workspaceOverrides: { - toolAllowlist: { - posthog: ["docs-search", "error-details", "list-errors"], - }, - }, - testResults: { - posthog: POSTHOG_TOOLS, - }, - preCacheTools: true, - }) - } - /> - ), - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); - - const body = within(canvasElement.ownerDocument.body); - await body.findByText("posthog"); - - // Should show filtered tool count - await body.findByText(/3 of 14 tools enabled/i); - }, -}; - // ═══════════════════════════════════════════════════════════════════════════════ // INTERACTION STORIES // ═══════════════════════════════════════════════════════════════════════════════ -/** Interact with tool selector - click All/None buttons */ +/** Interact with tool selector in project settings - click All/None buttons */ export const ToolSelectorInteraction: AppStory = { render: () => ( ), play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); + await openProjectSettingsViaMCPButton(canvasElement); const body = within(canvasElement.ownerDocument.body); @@ -489,7 +392,7 @@ export const ToolSelectorInteraction: AppStory = { }, }; -/** Toggle server enabled state in workspace modal */ +/** Toggle server enabled state in project settings */ export const ToggleServerEnabled: AppStory = { render: () => ( ), play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { - await openWorkspaceMCPModal(canvasElement); + await openProjectSettingsViaMCPButton(canvasElement); const body = within(canvasElement.ownerDocument.body);