mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 18:38:35 +02:00
feat: add dedicated project settings panel accessible from sidebar
Move project-specific settings (discovered MCP servers) out of the global Settings panel and into a dedicated ProjectSettingsPanel overlay. Users can now access any project's settings via a gear icon on the sidebar project header, or by clicking the pending-discovery badge. - Add ProjectSettingsPanel component with project info, discovered MCP server management (accept/dismiss/rescan), and project removal - Add gear icon and clickable discovery badge to Sidebar ProjectGroup - Remove project-specific discovery props from SettingsPanel (now shows only user-level discovered servers) - Wire ProjectSettingsPanel overlay in App.tsx with auto-close on project removal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+30
-6
@@ -5,6 +5,7 @@ import { ActivityPanel } from '@renderer/components/ActivityPanel';
|
||||
import { ChatPane } from '@renderer/components/ChatPane';
|
||||
import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal';
|
||||
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
||||
import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel';
|
||||
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
||||
import { Sidebar } from '@renderer/components/Sidebar';
|
||||
import { resolveChatToolingSettings } from '@renderer/lib/chatTooling';
|
||||
@@ -101,6 +102,7 @@ export default function App() {
|
||||
const [turnEventLogs, setTurnEventLogs] = useState<TurnEventLogMap>({});
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [projectSettingsId, setProjectSettingsId] = useState<string>();
|
||||
const [newSessionProjectId, setNewSessionProjectId] = useState<string>();
|
||||
const [showDiscoveryModal, setShowDiscoveryModal] = useState(false);
|
||||
|
||||
@@ -251,6 +253,16 @@ export default function App() {
|
||||
}
|
||||
}, [api, workspace]);
|
||||
|
||||
const projectForSettings = useMemo(
|
||||
() => workspace?.projects.find((p) => p.id === projectSettingsId),
|
||||
[workspace?.projects, projectSettingsId],
|
||||
);
|
||||
|
||||
// Close project settings if the project was removed
|
||||
useEffect(() => {
|
||||
if (projectSettingsId && !projectForSettings) setProjectSettingsId(undefined);
|
||||
}, [projectSettingsId, projectForSettings]);
|
||||
|
||||
// Loading state
|
||||
if (!workspace) {
|
||||
return (
|
||||
@@ -397,15 +409,9 @@ export default function App() {
|
||||
theme={workspace.settings.theme}
|
||||
toolingSettings={workspace.settings.tooling}
|
||||
discoveredUserTooling={workspace.settings.discoveredUserTooling}
|
||||
discoveredProjectTooling={selectedProject?.discoveredTooling}
|
||||
selectedProjectName={selectedProject?.name}
|
||||
onRescanProjectConfigs={selectedProject ? () => void api.rescanProjectConfigs({ projectId: selectedProject.id }) : undefined}
|
||||
onResolveUserDiscoveredTooling={(serverIds, resolution) => {
|
||||
void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution });
|
||||
}}
|
||||
onResolveProjectDiscoveredTooling={selectedProject ? (serverIds, resolution) => {
|
||||
void api.resolveProjectDiscoveredTooling({ projectId: selectedProject.id, serverIds, resolution });
|
||||
} : undefined}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
@@ -423,6 +429,7 @@ export default function App() {
|
||||
setNewSessionProjectId(projectId);
|
||||
}}
|
||||
onOpenSettings={() => setShowSettings(true)}
|
||||
onOpenProjectSettings={(projectId) => setProjectSettingsId(projectId)}
|
||||
onProjectSelect={(projectId) => {
|
||||
void api.selectProject(projectId);
|
||||
}}
|
||||
@@ -484,6 +491,23 @@ export default function App() {
|
||||
userDiscoveredTooling={workspace.settings.discoveredUserTooling}
|
||||
/>
|
||||
)}
|
||||
|
||||
{projectForSettings && (
|
||||
<ProjectSettingsPanel
|
||||
project={projectForSettings}
|
||||
onClose={() => setProjectSettingsId(undefined)}
|
||||
onRescanConfigs={() => {
|
||||
void api.rescanProjectConfigs({ projectId: projectForSettings.id });
|
||||
}}
|
||||
onResolveDiscoveredTooling={(serverIds, resolution) => {
|
||||
void api.resolveProjectDiscoveredTooling({ projectId: projectForSettings.id, serverIds, resolution });
|
||||
}}
|
||||
onRemoveProject={() => {
|
||||
void api.removeProject(projectForSettings.id);
|
||||
setProjectSettingsId(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ChevronLeft, FolderOpen, GitBranch, RefreshCw, Server, Trash2, AlertTriangle, Circle } from 'lucide-react';
|
||||
|
||||
import type { ProjectRecord, ProjectGitContext } from '@shared/domain/project';
|
||||
import type { DiscoveredMcpServer } from '@shared/domain/discoveredTooling';
|
||||
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
|
||||
interface ProjectSettingsPanelProps {
|
||||
project: ProjectRecord;
|
||||
onClose: () => void;
|
||||
onRescanConfigs: () => void;
|
||||
onResolveDiscoveredTooling: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onRemoveProject: () => void;
|
||||
}
|
||||
|
||||
export function ProjectSettingsPanel({
|
||||
project,
|
||||
onClose,
|
||||
onRescanConfigs,
|
||||
onResolveDiscoveredTooling,
|
||||
onRemoveProject,
|
||||
}: ProjectSettingsPanelProps) {
|
||||
const [confirmingRemove, setConfirmingRemove] = useState(false);
|
||||
|
||||
const acceptedServers = listAcceptedDiscoveredMcpServers(project.discoveredTooling);
|
||||
const pendingServers = listPendingDiscoveredMcpServers(project.discoveredTooling);
|
||||
const hasDiscoveredServers = acceptedServers.length + pendingServers.length > 0;
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
if (!confirmingRemove) {
|
||||
setConfirmingRemove(true);
|
||||
return;
|
||||
}
|
||||
onRemoveProject();
|
||||
}, [confirmingRemove, onRemoveProject]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col bg-[var(--color-surface-0)]">
|
||||
{/* Header */}
|
||||
<div className="drag-region flex items-center gap-3 border-b border-[var(--color-border)] px-5 pb-3 pt-3">
|
||||
<button
|
||||
className="no-drag flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<h2 className="text-[13px] font-semibold text-zinc-100">Project Settings</h2>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-2xl px-8 py-6 space-y-8">
|
||||
{/* Project info */}
|
||||
<ProjectInfoSection project={project} />
|
||||
|
||||
{/* Discovered MCP Servers */}
|
||||
{hasDiscoveredServers ? (
|
||||
<DiscoveredServersSection
|
||||
accepted={acceptedServers}
|
||||
onRescan={onRescanConfigs}
|
||||
onResolve={onResolveDiscoveredTooling}
|
||||
pending={pendingServers}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="MCP servers discovered from project config files (.vscode/mcp.json, .mcp.json, .copilot/mcp.json)."
|
||||
title="Discovered MCP Servers"
|
||||
>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-lg bg-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-200 transition hover:bg-zinc-700"
|
||||
onClick={onRescanConfigs}
|
||||
title="Scan project config files for MCP servers"
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Scan
|
||||
</button>
|
||||
</SectionHeader>
|
||||
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/20 px-5 py-8 text-center text-[12px] leading-relaxed text-zinc-500">
|
||||
No MCP servers discovered in this project yet. Click Scan to check project config files.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remove project */}
|
||||
<div className="border-t border-zinc-800 pt-6">
|
||||
<h3 className="text-[13px] font-semibold text-zinc-200">Danger zone</h3>
|
||||
<p className="mt-1 text-[12px] text-zinc-500">
|
||||
Removing a project deletes all its sessions and discovered tooling from Aryx.
|
||||
Your project files on disk are not affected.
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<button
|
||||
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] font-medium transition ${
|
||||
confirmingRemove
|
||||
? 'bg-red-600 text-white hover:bg-red-500'
|
||||
: 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
|
||||
}`}
|
||||
onClick={handleRemove}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
{confirmingRemove ? 'Confirm removal' : 'Remove project'}
|
||||
</button>
|
||||
{confirmingRemove && (
|
||||
<button
|
||||
className="rounded-lg px-3 py-1.5 text-[13px] font-medium text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
onClick={() => setConfirmingRemove(false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Project info ─────────────────────────────────────────── */
|
||||
|
||||
function ProjectInfoSection({ project }: { project: ProjectRecord }) {
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="Project details and git status."
|
||||
title="Project Info"
|
||||
/>
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/40 px-5 py-4 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderOpen className="size-5 shrink-0 text-indigo-400" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-[13px] font-medium text-zinc-200">{project.name}</div>
|
||||
<div className="mt-0.5 truncate text-[12px] text-zinc-500">{project.path}</div>
|
||||
</div>
|
||||
</div>
|
||||
{project.git && <ProjectGitInfo git={project.git} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectGitInfo({ git }: { git: ProjectGitContext }) {
|
||||
if (git.status === 'not-repository') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[12px] text-zinc-600">
|
||||
<GitBranch className="size-3.5" />
|
||||
Not a git repository
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (git.status === 'git-missing') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[12px] text-amber-500/70">
|
||||
<AlertTriangle className="size-3.5" />
|
||||
Git is not installed
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (git.status === 'error') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[12px] text-red-400/70">
|
||||
<AlertTriangle className="size-3.5" />
|
||||
{git.errorMessage ?? 'Git error'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const branchLabel = git.branch ?? git.head?.shortHash ?? 'HEAD';
|
||||
const parts: string[] = [];
|
||||
if (git.isDirty && git.changedFileCount) parts.push(`${git.changedFileCount} changed`);
|
||||
if (git.ahead) parts.push(`${git.ahead} ahead`);
|
||||
if (git.behind) parts.push(`${git.behind} behind`);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[12px] text-zinc-400">
|
||||
<GitBranch className="size-3.5 shrink-0" />
|
||||
<span>{branchLabel}</span>
|
||||
{git.isDirty && <Circle className="size-1.5 shrink-0 fill-amber-500 text-amber-500" />}
|
||||
{parts.length > 0 && (
|
||||
<span className="text-zinc-600">· {parts.join(' · ')}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Discovered MCP servers ──────────────────────────────── */
|
||||
|
||||
function DiscoveredServersSection({
|
||||
accepted,
|
||||
pending,
|
||||
onRescan,
|
||||
onResolve,
|
||||
}: {
|
||||
accepted: DiscoveredMcpServer[];
|
||||
pending: DiscoveredMcpServer[];
|
||||
onRescan: () => void;
|
||||
onResolve: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="MCP servers discovered from project config files (.vscode/mcp.json, .mcp.json, .copilot/mcp.json)."
|
||||
title="Discovered MCP Servers"
|
||||
>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-lg bg-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-200 transition hover:bg-zinc-700"
|
||||
onClick={onRescan}
|
||||
title="Re-scan project config files"
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Re-scan
|
||||
</button>
|
||||
</SectionHeader>
|
||||
|
||||
<div className="space-y-1">
|
||||
{accepted.map((server) => (
|
||||
<DiscoveredServerRow
|
||||
key={server.id}
|
||||
onDismiss={() => onResolve([server.id], 'dismiss')}
|
||||
server={server}
|
||||
status="accepted"
|
||||
/>
|
||||
))}
|
||||
{pending.map((server) => (
|
||||
<DiscoveredServerRow
|
||||
key={server.id}
|
||||
onAccept={() => onResolve([server.id], 'accept')}
|
||||
onDismiss={() => onResolve([server.id], 'dismiss')}
|
||||
server={server}
|
||||
status="pending"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pending.length > 1 && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-lg bg-emerald-500/10 px-3 py-1.5 text-[12px] font-medium text-emerald-400 transition hover:bg-emerald-500/20"
|
||||
onClick={() => onResolve(pending.map((s) => s.id), 'accept')}
|
||||
type="button"
|
||||
>
|
||||
Accept all ({pending.length})
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg px-3 py-1.5 text-[12px] font-medium text-zinc-500 transition hover:bg-zinc-800 hover:text-zinc-300"
|
||||
onClick={() => onResolve(pending.map((s) => s.id), 'dismiss')}
|
||||
type="button"
|
||||
>
|
||||
Dismiss all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscoveredServerRow({
|
||||
server,
|
||||
status,
|
||||
onAccept,
|
||||
onDismiss,
|
||||
}: {
|
||||
server: DiscoveredMcpServer;
|
||||
status: 'accepted' | 'pending';
|
||||
onAccept?: () => void;
|
||||
onDismiss?: () => void;
|
||||
}) {
|
||||
const detail =
|
||||
server.transport === 'local'
|
||||
? server.command || 'No command'
|
||||
: server.url || 'No URL';
|
||||
|
||||
const statusBadge = status === 'accepted'
|
||||
? 'bg-emerald-500/10 text-emerald-400'
|
||||
: 'bg-amber-500/10 text-amber-400';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-transparent px-4 py-3 hover:border-zinc-800 hover:bg-zinc-900">
|
||||
<Server className="size-4 shrink-0 text-zinc-600" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-[13px] font-medium text-zinc-200">{server.name}</span>
|
||||
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-zinc-400">
|
||||
{server.transport}
|
||||
</span>
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${statusBadge}`}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-[12px] text-zinc-500">
|
||||
{detail}
|
||||
<span className="ml-2 text-zinc-700">· {server.sourceLabel}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{onAccept && (
|
||||
<button
|
||||
className="rounded-lg px-2.5 py-1 text-[12px] font-medium text-emerald-400 transition hover:bg-emerald-500/10"
|
||||
onClick={onAccept}
|
||||
type="button"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
)}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="rounded-lg px-2.5 py-1 text-[12px] font-medium text-zinc-500 transition hover:bg-zinc-800 hover:text-zinc-300"
|
||||
onClick={onDismiss}
|
||||
type="button"
|
||||
>
|
||||
{status === 'accepted' ? 'Remove' : 'Dismiss'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Shared helpers ──────────────────────────────────────── */
|
||||
|
||||
function SectionHeader({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-[13px] font-semibold text-zinc-200">{title}</h3>
|
||||
<p className="mt-0.5 text-[12px] text-zinc-500">{description}</p>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Code, Cpu, FolderOpen, Palette, Plus, RefreshCw, Server, TriangleAlert, Workflow, Wrench } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Code, Cpu, FolderOpen, Palette, Plus, Server, TriangleAlert, Workflow, Wrench } from 'lucide-react';
|
||||
|
||||
import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
|
||||
import { PatternEditor } from '@renderer/components/PatternEditor';
|
||||
import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
|
||||
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
|
||||
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
|
||||
import type { DiscoveredMcpServer, DiscoveredToolingState, ProjectDiscoveredTooling } from '@shared/domain/discoveredTooling';
|
||||
import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling';
|
||||
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
import type { ModelDefinition } from '@shared/domain/models';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
@@ -26,8 +26,6 @@ interface SettingsPanelProps {
|
||||
theme: AppearanceTheme;
|
||||
toolingSettings: WorkspaceToolingSettings;
|
||||
discoveredUserTooling: DiscoveredToolingState;
|
||||
discoveredProjectTooling?: ProjectDiscoveredTooling;
|
||||
selectedProjectName?: string;
|
||||
isRefreshingCapabilities: boolean;
|
||||
onRefreshCapabilities: () => void;
|
||||
onClose: () => void;
|
||||
@@ -43,9 +41,7 @@ interface SettingsPanelProps {
|
||||
onSetTheme: (theme: AppearanceTheme) => void;
|
||||
onOpenAppDataFolder: () => void;
|
||||
onResetLocalWorkspace: () => Promise<void>;
|
||||
onRescanProjectConfigs?: () => void;
|
||||
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onResolveProjectDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
}
|
||||
|
||||
type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting';
|
||||
@@ -107,8 +103,6 @@ export function SettingsPanel({
|
||||
theme,
|
||||
toolingSettings,
|
||||
discoveredUserTooling,
|
||||
discoveredProjectTooling,
|
||||
selectedProjectName,
|
||||
isRefreshingCapabilities,
|
||||
onRefreshCapabilities,
|
||||
onClose,
|
||||
@@ -124,9 +118,7 @@ export function SettingsPanel({
|
||||
onSetTheme,
|
||||
onOpenAppDataFolder,
|
||||
onResetLocalWorkspace,
|
||||
onRescanProjectConfigs,
|
||||
onResolveUserDiscoveredTooling,
|
||||
onResolveProjectDiscoveredTooling,
|
||||
}: SettingsPanelProps) {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
||||
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
||||
@@ -287,12 +279,8 @@ export function SettingsPanel({
|
||||
)}
|
||||
{activeSection === 'mcp-servers' && (
|
||||
<DiscoveredMcpSection
|
||||
discoveredProjectTooling={discoveredProjectTooling}
|
||||
discoveredUserTooling={discoveredUserTooling}
|
||||
onRescanProjectConfigs={onRescanProjectConfigs}
|
||||
onResolveProjectDiscoveredTooling={onResolveProjectDiscoveredTooling}
|
||||
onResolveUserDiscoveredTooling={onResolveUserDiscoveredTooling}
|
||||
selectedProjectName={selectedProjectName}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'lsp-profiles' && (
|
||||
@@ -607,68 +595,32 @@ function EmptyState({ children }: { children: ReactNode }) {
|
||||
|
||||
function DiscoveredMcpSection({
|
||||
discoveredUserTooling,
|
||||
discoveredProjectTooling,
|
||||
selectedProjectName,
|
||||
onRescanProjectConfigs,
|
||||
onResolveUserDiscoveredTooling,
|
||||
onResolveProjectDiscoveredTooling,
|
||||
}: {
|
||||
discoveredUserTooling: DiscoveredToolingState;
|
||||
discoveredProjectTooling?: ProjectDiscoveredTooling;
|
||||
selectedProjectName?: string;
|
||||
onRescanProjectConfigs?: () => void;
|
||||
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onResolveProjectDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
}) {
|
||||
const acceptedUser = listAcceptedDiscoveredMcpServers(discoveredUserTooling);
|
||||
const pendingUser = listPendingDiscoveredMcpServers(discoveredUserTooling);
|
||||
const acceptedProject = listAcceptedDiscoveredMcpServers(discoveredProjectTooling);
|
||||
const pendingProject = listPendingDiscoveredMcpServers(discoveredProjectTooling);
|
||||
|
||||
const hasAny = acceptedUser.length + pendingUser.length + acceptedProject.length + pendingProject.length > 0;
|
||||
const hasAny = acceptedUser.length + pendingUser.length > 0;
|
||||
|
||||
if (!hasAny) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<SectionHeader
|
||||
description="MCP servers discovered from project and user config files. Accepted servers are available for session tooling."
|
||||
description="MCP servers discovered from user config files. Accepted servers are available for session tooling."
|
||||
title="Discovered MCP Servers"
|
||||
>
|
||||
{onRescanProjectConfigs && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-lg bg-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-200 transition hover:bg-zinc-700"
|
||||
onClick={onRescanProjectConfigs}
|
||||
title="Re-scan project config files"
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Re-scan
|
||||
</button>
|
||||
)}
|
||||
</SectionHeader>
|
||||
/>
|
||||
|
||||
{/* User-level discovered */}
|
||||
{(acceptedUser.length > 0 || pendingUser.length > 0) && (
|
||||
<DiscoveredSubSection
|
||||
label="User-level"
|
||||
description="From ~/.copilot/mcp.json"
|
||||
accepted={acceptedUser}
|
||||
pending={pendingUser}
|
||||
onResolve={onResolveUserDiscoveredTooling}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Project-level discovered */}
|
||||
{(acceptedProject.length > 0 || pendingProject.length > 0) && (
|
||||
<DiscoveredSubSection
|
||||
label={selectedProjectName ? `Project: ${selectedProjectName}` : 'Project-level'}
|
||||
description="From .vscode/mcp.json, .mcp.json, or .copilot/mcp.json"
|
||||
accepted={acceptedProject}
|
||||
pending={pendingProject}
|
||||
onResolve={onResolveProjectDiscoveredTooling}
|
||||
/>
|
||||
)}
|
||||
<DiscoveredSubSection
|
||||
label="User-level"
|
||||
description="From ~/.copilot/mcp.json"
|
||||
accepted={acceptedUser}
|
||||
pending={pendingUser}
|
||||
onResolve={onResolveUserDiscoveredTooling}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ interface SidebarProps {
|
||||
onProjectSelect: (projectId?: string) => void;
|
||||
onSessionSelect: (sessionId: string) => void;
|
||||
onOpenSettings: () => void;
|
||||
onOpenProjectSettings: (projectId: string) => void;
|
||||
onRenameSession: (sessionId: string, title: string) => void;
|
||||
onDuplicateSession: (sessionId: string) => void;
|
||||
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
|
||||
@@ -328,6 +329,7 @@ function ProjectGroup({
|
||||
onRenameSubmit,
|
||||
onRenameCancel,
|
||||
onRefreshGitContext,
|
||||
onOpenProjectSettings,
|
||||
onNewSession,
|
||||
newSessionLabel,
|
||||
}: {
|
||||
@@ -341,6 +343,7 @@ function ProjectGroup({
|
||||
onRenameSubmit: (sessionId: string, title: string) => void;
|
||||
onRenameCancel: () => void;
|
||||
onRefreshGitContext?: (projectId: string) => void;
|
||||
onOpenProjectSettings?: (projectId: string) => void;
|
||||
onNewSession?: () => void;
|
||||
newSessionLabel?: string;
|
||||
}){
|
||||
@@ -394,6 +397,19 @@ function ProjectGroup({
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{!isScratchpad && onOpenProjectSettings && (
|
||||
<span
|
||||
className="flex size-5 items-center justify-center rounded text-zinc-600 opacity-0 transition hover:bg-zinc-700 hover:text-zinc-300 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenProjectSettings(project.id);
|
||||
}}
|
||||
role="button"
|
||||
title="Project settings"
|
||||
>
|
||||
<Settings className="size-3" />
|
||||
</span>
|
||||
)}
|
||||
{!isScratchpad && onRefreshGitContext && (
|
||||
<span
|
||||
className="flex size-5 items-center justify-center rounded text-zinc-600 opacity-0 transition hover:bg-zinc-700 hover:text-zinc-300 group-hover:opacity-100"
|
||||
@@ -415,8 +431,13 @@ function ProjectGroup({
|
||||
)}
|
||||
{pendingDiscoveryCount > 0 && (
|
||||
<span
|
||||
className="flex items-center gap-1 rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-400"
|
||||
title={`${pendingDiscoveryCount} MCP server${pendingDiscoveryCount === 1 ? '' : 's'} discovered`}
|
||||
className="flex cursor-pointer items-center gap-1 rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-400 transition hover:bg-amber-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenProjectSettings?.(project.id);
|
||||
}}
|
||||
role="button"
|
||||
title={`${pendingDiscoveryCount} MCP server${pendingDiscoveryCount === 1 ? '' : 's'} discovered — click to review`}
|
||||
>
|
||||
{pendingDiscoveryCount} new
|
||||
</span>
|
||||
@@ -475,6 +496,7 @@ export function Sidebar({
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onOpenSettings,
|
||||
onOpenProjectSettings,
|
||||
onRenameSession,
|
||||
onDuplicateSession,
|
||||
onSetSessionPinned,
|
||||
@@ -678,6 +700,7 @@ export function Sidebar({
|
||||
onRenameSubmit={handleRenameSubmit}
|
||||
onRenameCancel={() => setRenamingSessionId(undefined)}
|
||||
onRefreshGitContext={onRefreshGitContext}
|
||||
onOpenProjectSettings={onOpenProjectSettings}
|
||||
renamingSessionId={renamingSessionId}
|
||||
patterns={workspace.patterns}
|
||||
project={project}
|
||||
|
||||
Reference in New Issue
Block a user