mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 00:18:40 +02:00
feat: auto-discover MCP servers from project and user config files
Scan .vscode/mcp.json, .mcp.json, .copilot/mcp.json (project-level) and ~/.copilot/mcp.json (user-level) for MCP server definitions. Discovered servers require explicit user acceptance before activation. Backend: - Add ConfigScannerRegistry with per-format scanners and substitution - Add discovered tooling domain model with fingerprint-based change detection - Integrate scanning into workspace load, project add, and project selection - Merge accepted discovered MCPs into effective runtime tooling - Extend sidecar contracts with env/headers for real-world MCP configs - Add IPC channels for accept/dismiss/rescan operations Frontend: - Add DiscoveredToolingModal for reviewing pending MCP servers - Auto-show modal when pending discoveries exist on load or project switch - Add amber badge on sidebar project headers for pending discoveries - Add discovered MCP section in Settings panel with accept/dismiss/rescan - Group InlinePills tool dropdown by Workspace MCP / User MCP / Project MCP - Pass effective project tooling (including accepted discoveries) to ChatPane Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Check, FileSearch, Server, X, XCircle } from 'lucide-react';
|
||||
|
||||
import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling';
|
||||
import { listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
import type { ProjectDiscoveredTooling } from '@shared/domain/discoveredTooling';
|
||||
|
||||
/* ── Props ─────────────────────────────────────────────────── */
|
||||
|
||||
interface DiscoveredToolingModalProps {
|
||||
userDiscoveredTooling: DiscoveredToolingState;
|
||||
projectDiscoveredTooling?: ProjectDiscoveredTooling;
|
||||
projectName?: string;
|
||||
onResolveUserServers: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onResolveProjectServers: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/* ── Modal ─────────────────────────────────────────────────── */
|
||||
|
||||
export function DiscoveredToolingModal({
|
||||
userDiscoveredTooling,
|
||||
projectDiscoveredTooling,
|
||||
projectName,
|
||||
onResolveUserServers,
|
||||
onResolveProjectServers,
|
||||
onClose,
|
||||
}: DiscoveredToolingModalProps) {
|
||||
const pendingUserServers = useMemo(
|
||||
() => listPendingDiscoveredMcpServers(userDiscoveredTooling),
|
||||
[userDiscoveredTooling],
|
||||
);
|
||||
const pendingProjectServers = useMemo(
|
||||
() => listPendingDiscoveredMcpServers(projectDiscoveredTooling),
|
||||
[projectDiscoveredTooling],
|
||||
);
|
||||
|
||||
const totalPending = pendingUserServers.length + pendingProjectServers.length;
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
// Auto-close when nothing left to review
|
||||
useEffect(() => {
|
||||
if (totalPending === 0) onClose();
|
||||
}, [totalPending, onClose]);
|
||||
|
||||
if (totalPending === 0) return null;
|
||||
|
||||
const userGroups = groupBySource(pendingUserServers);
|
||||
const projectGroups = groupBySource(pendingProjectServers);
|
||||
|
||||
function handleAcceptAll() {
|
||||
if (pendingUserServers.length > 0) {
|
||||
onResolveUserServers(pendingUserServers.map((s) => s.id), 'accept');
|
||||
}
|
||||
if (pendingProjectServers.length > 0) {
|
||||
onResolveProjectServers(pendingProjectServers.map((s) => s.id), 'accept');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDismissAll() {
|
||||
if (pendingUserServers.length > 0) {
|
||||
onResolveUserServers(pendingUserServers.map((s) => s.id), 'dismiss');
|
||||
}
|
||||
if (pendingProjectServers.length > 0) {
|
||||
onResolveProjectServers(pendingProjectServers.map((s) => s.id), 'dismiss');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-labelledby="discovered-tooling-title"
|
||||
aria-modal="true"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
role="dialog"
|
||||
>
|
||||
<div className="flex max-h-[80vh] w-full max-w-lg flex-col rounded-xl border border-zinc-800 bg-zinc-900 shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<FileSearch className="size-4 text-indigo-400" />
|
||||
<h2 id="discovered-tooling-title" className="text-[13px] font-semibold text-zinc-100">
|
||||
MCP servers found in config files
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
className="flex size-7 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-800 hover:text-zinc-300"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
<p className="mb-4 text-[12px] leading-relaxed text-zinc-500">
|
||||
The following MCP servers were found in your config files. Accept the ones you want to
|
||||
use, or dismiss those you don't need. Accepted servers become available for session tooling.
|
||||
</p>
|
||||
|
||||
{pendingUserServers.length > 0 && (
|
||||
<DiscoveredGroup
|
||||
groups={userGroups}
|
||||
onResolve={onResolveUserServers}
|
||||
scopeLabel="User-level"
|
||||
/>
|
||||
)}
|
||||
|
||||
{pendingProjectServers.length > 0 && (
|
||||
<DiscoveredGroup
|
||||
groups={projectGroups}
|
||||
onResolve={onResolveProjectServers}
|
||||
scopeLabel={projectName ? `Project: ${projectName}` : 'Project-level'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between border-t border-zinc-800 px-5 py-3">
|
||||
<span className="text-[12px] text-zinc-600">
|
||||
{totalPending} server{totalPending === 1 ? '' : 's'} pending review
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-lg px-3 py-1.5 text-[13px] text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
onClick={handleDismissAll}
|
||||
type="button"
|
||||
>
|
||||
Dismiss All
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg bg-indigo-600 px-3 py-1.5 text-[13px] font-medium text-white transition hover:bg-indigo-500"
|
||||
onClick={handleAcceptAll}
|
||||
type="button"
|
||||
>
|
||||
Accept All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Discovered group (by scope) ──────────────────────────── */
|
||||
|
||||
function DiscoveredGroup({
|
||||
scopeLabel,
|
||||
groups,
|
||||
onResolve,
|
||||
}: {
|
||||
scopeLabel: string;
|
||||
groups: SourceGroup[];
|
||||
onResolve: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-600">
|
||||
{scopeLabel}
|
||||
</div>
|
||||
{groups.map((group) => (
|
||||
<div className="mb-3" key={group.sourceLabel}>
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-[11px] text-zinc-500">
|
||||
<span className="truncate font-medium">{group.sourceLabel}</span>
|
||||
<span className="text-zinc-700">·</span>
|
||||
<span className="text-zinc-600">
|
||||
{group.servers.length} server{group.servers.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{group.servers.map((server) => (
|
||||
<ServerRow
|
||||
key={server.id}
|
||||
onAccept={() => onResolve([server.id], 'accept')}
|
||||
onDismiss={() => onResolve([server.id], 'dismiss')}
|
||||
server={server}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Server row ────────────────────────────────────────────── */
|
||||
|
||||
function ServerRow({
|
||||
server,
|
||||
onAccept,
|
||||
onDismiss,
|
||||
}: {
|
||||
server: DiscoveredMcpServer;
|
||||
onAccept: () => void;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
const detail =
|
||||
server.transport === 'local'
|
||||
? server.command || 'No command'
|
||||
: server.url || 'No URL';
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 rounded-lg border border-zinc-800/60 bg-zinc-800/20 px-3 py-2.5">
|
||||
<Server className="size-3.5 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-500">
|
||||
{server.transport}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-[12px] text-zinc-500">{detail}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="flex size-7 items-center justify-center rounded-md text-zinc-600 transition hover:bg-red-500/10 hover:text-red-400"
|
||||
onClick={onDismiss}
|
||||
title="Dismiss"
|
||||
type="button"
|
||||
>
|
||||
<XCircle className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="flex size-7 items-center justify-center rounded-md text-zinc-600 transition hover:bg-emerald-500/10 hover:text-emerald-400"
|
||||
onClick={onAccept}
|
||||
title="Accept"
|
||||
type="button"
|
||||
>
|
||||
<Check className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────── */
|
||||
|
||||
interface SourceGroup {
|
||||
sourceLabel: string;
|
||||
servers: DiscoveredMcpServer[];
|
||||
}
|
||||
|
||||
function groupBySource(servers: DiscoveredMcpServer[]): SourceGroup[] {
|
||||
const map = new Map<string, DiscoveredMcpServer[]>();
|
||||
for (const server of servers) {
|
||||
const group = map.get(server.sourceLabel) ?? [];
|
||||
group.push(server);
|
||||
map.set(server.sourceLabel, group);
|
||||
}
|
||||
|
||||
return [...map.entries()]
|
||||
.map(([sourceLabel, groupServers]) => ({ sourceLabel, servers: groupServers }))
|
||||
.sort((a, b) => a.sourceLabel.localeCompare(b.sourceLabel));
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Code, Cpu, FolderOpen, Palette, Plus, Server, TriangleAlert, Workflow, Wrench } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Code, Cpu, FolderOpen, Palette, Plus, RefreshCw, 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 { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
import type { ModelDefinition } from '@shared/domain/models';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import {
|
||||
@@ -23,6 +25,9 @@ interface SettingsPanelProps {
|
||||
sidecarCapabilities?: SidecarCapabilities;
|
||||
theme: AppearanceTheme;
|
||||
toolingSettings: WorkspaceToolingSettings;
|
||||
discoveredUserTooling: DiscoveredToolingState;
|
||||
discoveredProjectTooling?: ProjectDiscoveredTooling;
|
||||
selectedProjectName?: string;
|
||||
isRefreshingCapabilities: boolean;
|
||||
onRefreshCapabilities: () => void;
|
||||
onClose: () => void;
|
||||
@@ -38,6 +43,9 @@ 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';
|
||||
@@ -98,6 +106,9 @@ export function SettingsPanel({
|
||||
sidecarCapabilities,
|
||||
theme,
|
||||
toolingSettings,
|
||||
discoveredUserTooling,
|
||||
discoveredProjectTooling,
|
||||
selectedProjectName,
|
||||
isRefreshingCapabilities,
|
||||
onRefreshCapabilities,
|
||||
onClose,
|
||||
@@ -113,6 +124,9 @@ export function SettingsPanel({
|
||||
onSetTheme,
|
||||
onOpenAppDataFolder,
|
||||
onResetLocalWorkspace,
|
||||
onRescanProjectConfigs,
|
||||
onResolveUserDiscoveredTooling,
|
||||
onResolveProjectDiscoveredTooling,
|
||||
}: SettingsPanelProps) {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
||||
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
||||
@@ -271,6 +285,16 @@ export function SettingsPanel({
|
||||
servers={toolingSettings.mcpServers}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'mcp-servers' && (
|
||||
<DiscoveredMcpSection
|
||||
discoveredProjectTooling={discoveredProjectTooling}
|
||||
discoveredUserTooling={discoveredUserTooling}
|
||||
onRescanProjectConfigs={onRescanProjectConfigs}
|
||||
onResolveProjectDiscoveredTooling={onResolveProjectDiscoveredTooling}
|
||||
onResolveUserDiscoveredTooling={onResolveUserDiscoveredTooling}
|
||||
selectedProjectName={selectedProjectName}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'lsp-profiles' && (
|
||||
<LspProfilesSection
|
||||
onEditProfile={(profile) => setEditingLspProfile(structuredClone(profile))}
|
||||
@@ -579,6 +603,181 @@ function EmptyState({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Discovered MCP section ────────────────────────────────── */
|
||||
|
||||
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;
|
||||
|
||||
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."
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscoveredSubSection({
|
||||
label,
|
||||
description,
|
||||
accepted,
|
||||
pending,
|
||||
onResolve,
|
||||
}: {
|
||||
label: string;
|
||||
description: string;
|
||||
accepted: DiscoveredMcpServer[];
|
||||
pending: DiscoveredMcpServer[];
|
||||
onResolve?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[12px] font-medium text-zinc-300">{label}</span>
|
||||
<p className="text-[11px] text-zinc-600">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{accepted.map((server) => (
|
||||
<DiscoveredServerRow
|
||||
key={server.id}
|
||||
onDismiss={onResolve ? () => onResolve([server.id], 'dismiss') : undefined}
|
||||
server={server}
|
||||
status="accepted"
|
||||
/>
|
||||
))}
|
||||
{pending.map((server) => (
|
||||
<DiscoveredServerRow
|
||||
key={server.id}
|
||||
onAccept={onResolve ? () => onResolve([server.id], 'accept') : undefined}
|
||||
onDismiss={onResolve ? () => onResolve([server.id], 'dismiss') : undefined}
|
||||
server={server}
|
||||
status="pending"
|
||||
/>
|
||||
))}
|
||||
</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">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function TroubleshootingSection({
|
||||
onOpenAppDataFolder,
|
||||
onResetLocalWorkspace,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
import type { OrchestrationMode, PatternDefinition } from '@shared/domain/pattern';
|
||||
import { isScratchpadProject, type ProjectRecord, type ProjectGitContext } from '@shared/domain/project';
|
||||
import { listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { querySessions } from '@shared/domain/sessionLibrary';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
@@ -360,6 +361,10 @@ function ProjectGroup({
|
||||
);
|
||||
|
||||
const runningCount = visibleSessions.filter((s) => s.status === 'running').length;
|
||||
const pendingDiscoveryCount = useMemo(
|
||||
() => isScratchpad ? 0 : listPendingDiscoveredMcpServers(project.discoveredTooling).length,
|
||||
[isScratchpad, project.discoveredTooling],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -404,6 +409,14 @@ function ProjectGroup({
|
||||
{runningCount}
|
||||
</span>
|
||||
)}
|
||||
{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`}
|
||||
>
|
||||
{pendingDiscoveryCount} new
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded-full bg-zinc-800 px-1.5 py-0.5 text-[10px] font-medium text-zinc-500">
|
||||
{visibleSessions.length}
|
||||
</span>
|
||||
|
||||
@@ -225,6 +225,10 @@ export function InlineToolsPill({
|
||||
const enabledCount = selection.enabledMcpServerIds.length + selection.enabledLspProfileIds.length;
|
||||
const totalCount = mcpServers.length + lspProfiles.length;
|
||||
|
||||
const workspaceMcpServers = mcpServers.filter((s) => !s.id.startsWith('discovered_'));
|
||||
const discoveredUserMcpServers = mcpServers.filter((s) => s.id.startsWith('discovered_user_'));
|
||||
const discoveredProjectMcpServers = mcpServers.filter((s) => s.id.startsWith('discovered_project_'));
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
@@ -246,26 +250,29 @@ export function InlineToolsPill({
|
||||
|
||||
{open && !disabled && (
|
||||
<div className="absolute bottom-full left-0 z-40 mb-1.5 w-64 overflow-y-auto rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-2xl">
|
||||
{mcpServers.length > 0 && (
|
||||
<div>
|
||||
<div className="px-3 pb-1 pt-2 text-[9px] font-semibold uppercase tracking-wider text-zinc-600">
|
||||
MCP Servers
|
||||
</div>
|
||||
{mcpServers.map((server) => (
|
||||
<PopoverToggleRow
|
||||
detail={server.transport === 'local' ? server.command : server.url}
|
||||
enabled={selection.enabledMcpServerIds.includes(server.id)}
|
||||
key={server.id}
|
||||
label={server.name}
|
||||
onToggle={() =>
|
||||
onToggle({
|
||||
...selection,
|
||||
enabledMcpServerIds: toggleInArray(selection.enabledMcpServerIds, server.id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{workspaceMcpServers.length > 0 && (
|
||||
<McpServerGroup
|
||||
label="Workspace MCP"
|
||||
onToggle={onToggle}
|
||||
selection={selection}
|
||||
servers={workspaceMcpServers}
|
||||
/>
|
||||
)}
|
||||
{discoveredUserMcpServers.length > 0 && (
|
||||
<McpServerGroup
|
||||
label="User MCP"
|
||||
onToggle={onToggle}
|
||||
selection={selection}
|
||||
servers={discoveredUserMcpServers}
|
||||
/>
|
||||
)}
|
||||
{discoveredProjectMcpServers.length > 0 && (
|
||||
<McpServerGroup
|
||||
label="Project MCP"
|
||||
onToggle={onToggle}
|
||||
selection={selection}
|
||||
servers={discoveredProjectMcpServers}
|
||||
/>
|
||||
)}
|
||||
{lspProfiles.length > 0 && (
|
||||
<div>
|
||||
@@ -294,6 +301,40 @@ export function InlineToolsPill({
|
||||
);
|
||||
}
|
||||
|
||||
function McpServerGroup({
|
||||
label,
|
||||
servers,
|
||||
selection,
|
||||
onToggle,
|
||||
}: {
|
||||
label: string;
|
||||
servers: ReadonlyArray<McpServerDefinition>;
|
||||
selection: SessionToolingSelection;
|
||||
onToggle: (selection: SessionToolingSelection) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="px-3 pb-1 pt-2 text-[9px] font-semibold uppercase tracking-wider text-zinc-600">
|
||||
{label}
|
||||
</div>
|
||||
{servers.map((server) => (
|
||||
<PopoverToggleRow
|
||||
detail={server.transport === 'local' ? server.command : server.url}
|
||||
enabled={selection.enabledMcpServerIds.includes(server.id)}
|
||||
key={server.id}
|
||||
label={server.name}
|
||||
onToggle={() =>
|
||||
onToggle({
|
||||
...selection,
|
||||
enabledMcpServerIds: toggleInArray(selection.enabledMcpServerIds, server.id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── InlineApprovalPill ────────────────────────────────────── */
|
||||
|
||||
const approvalKindOrder: ApprovalToolKind[] = ['builtin', 'mcp', 'lsp', 'mixed'];
|
||||
|
||||
Reference in New Issue
Block a user