mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 13:23:57 +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 { ChatPane } from '@renderer/components/ChatPane';
|
||||||
import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal';
|
import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal';
|
||||||
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
||||||
|
import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel';
|
||||||
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
||||||
import { Sidebar } from '@renderer/components/Sidebar';
|
import { Sidebar } from '@renderer/components/Sidebar';
|
||||||
import { resolveChatToolingSettings } from '@renderer/lib/chatTooling';
|
import { resolveChatToolingSettings } from '@renderer/lib/chatTooling';
|
||||||
@@ -101,6 +102,7 @@ export default function App() {
|
|||||||
const [turnEventLogs, setTurnEventLogs] = useState<TurnEventLogMap>({});
|
const [turnEventLogs, setTurnEventLogs] = useState<TurnEventLogMap>({});
|
||||||
|
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const [projectSettingsId, setProjectSettingsId] = useState<string>();
|
||||||
const [newSessionProjectId, setNewSessionProjectId] = useState<string>();
|
const [newSessionProjectId, setNewSessionProjectId] = useState<string>();
|
||||||
const [showDiscoveryModal, setShowDiscoveryModal] = useState(false);
|
const [showDiscoveryModal, setShowDiscoveryModal] = useState(false);
|
||||||
|
|
||||||
@@ -251,6 +253,16 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [api, workspace]);
|
}, [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
|
// Loading state
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return (
|
return (
|
||||||
@@ -397,15 +409,9 @@ export default function App() {
|
|||||||
theme={workspace.settings.theme}
|
theme={workspace.settings.theme}
|
||||||
toolingSettings={workspace.settings.tooling}
|
toolingSettings={workspace.settings.tooling}
|
||||||
discoveredUserTooling={workspace.settings.discoveredUserTooling}
|
discoveredUserTooling={workspace.settings.discoveredUserTooling}
|
||||||
discoveredProjectTooling={selectedProject?.discoveredTooling}
|
|
||||||
selectedProjectName={selectedProject?.name}
|
|
||||||
onRescanProjectConfigs={selectedProject ? () => void api.rescanProjectConfigs({ projectId: selectedProject.id }) : undefined}
|
|
||||||
onResolveUserDiscoveredTooling={(serverIds, resolution) => {
|
onResolveUserDiscoveredTooling={(serverIds, resolution) => {
|
||||||
void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution });
|
void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution });
|
||||||
}}
|
}}
|
||||||
onResolveProjectDiscoveredTooling={selectedProject ? (serverIds, resolution) => {
|
|
||||||
void api.resolveProjectDiscoveredTooling({ projectId: selectedProject.id, serverIds, resolution });
|
|
||||||
} : undefined}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -423,6 +429,7 @@ export default function App() {
|
|||||||
setNewSessionProjectId(projectId);
|
setNewSessionProjectId(projectId);
|
||||||
}}
|
}}
|
||||||
onOpenSettings={() => setShowSettings(true)}
|
onOpenSettings={() => setShowSettings(true)}
|
||||||
|
onOpenProjectSettings={(projectId) => setProjectSettingsId(projectId)}
|
||||||
onProjectSelect={(projectId) => {
|
onProjectSelect={(projectId) => {
|
||||||
void api.selectProject(projectId);
|
void api.selectProject(projectId);
|
||||||
}}
|
}}
|
||||||
@@ -484,6 +491,23 @@ export default function App() {
|
|||||||
userDiscoveredTooling={workspace.settings.discoveredUserTooling}
|
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 { 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 { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
|
||||||
import { PatternEditor } from '@renderer/components/PatternEditor';
|
import { PatternEditor } from '@renderer/components/PatternEditor';
|
||||||
import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
|
import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
|
||||||
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
|
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
|
||||||
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
|
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 { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||||
import type { ModelDefinition } from '@shared/domain/models';
|
import type { ModelDefinition } from '@shared/domain/models';
|
||||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||||
@@ -26,8 +26,6 @@ interface SettingsPanelProps {
|
|||||||
theme: AppearanceTheme;
|
theme: AppearanceTheme;
|
||||||
toolingSettings: WorkspaceToolingSettings;
|
toolingSettings: WorkspaceToolingSettings;
|
||||||
discoveredUserTooling: DiscoveredToolingState;
|
discoveredUserTooling: DiscoveredToolingState;
|
||||||
discoveredProjectTooling?: ProjectDiscoveredTooling;
|
|
||||||
selectedProjectName?: string;
|
|
||||||
isRefreshingCapabilities: boolean;
|
isRefreshingCapabilities: boolean;
|
||||||
onRefreshCapabilities: () => void;
|
onRefreshCapabilities: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -43,9 +41,7 @@ interface SettingsPanelProps {
|
|||||||
onSetTheme: (theme: AppearanceTheme) => void;
|
onSetTheme: (theme: AppearanceTheme) => void;
|
||||||
onOpenAppDataFolder: () => void;
|
onOpenAppDataFolder: () => void;
|
||||||
onResetLocalWorkspace: () => Promise<void>;
|
onResetLocalWorkspace: () => Promise<void>;
|
||||||
onRescanProjectConfigs?: () => void;
|
|
||||||
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => 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';
|
type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting';
|
||||||
@@ -107,8 +103,6 @@ export function SettingsPanel({
|
|||||||
theme,
|
theme,
|
||||||
toolingSettings,
|
toolingSettings,
|
||||||
discoveredUserTooling,
|
discoveredUserTooling,
|
||||||
discoveredProjectTooling,
|
|
||||||
selectedProjectName,
|
|
||||||
isRefreshingCapabilities,
|
isRefreshingCapabilities,
|
||||||
onRefreshCapabilities,
|
onRefreshCapabilities,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -124,9 +118,7 @@ export function SettingsPanel({
|
|||||||
onSetTheme,
|
onSetTheme,
|
||||||
onOpenAppDataFolder,
|
onOpenAppDataFolder,
|
||||||
onResetLocalWorkspace,
|
onResetLocalWorkspace,
|
||||||
onRescanProjectConfigs,
|
|
||||||
onResolveUserDiscoveredTooling,
|
onResolveUserDiscoveredTooling,
|
||||||
onResolveProjectDiscoveredTooling,
|
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
||||||
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
||||||
@@ -287,12 +279,8 @@ export function SettingsPanel({
|
|||||||
)}
|
)}
|
||||||
{activeSection === 'mcp-servers' && (
|
{activeSection === 'mcp-servers' && (
|
||||||
<DiscoveredMcpSection
|
<DiscoveredMcpSection
|
||||||
discoveredProjectTooling={discoveredProjectTooling}
|
|
||||||
discoveredUserTooling={discoveredUserTooling}
|
discoveredUserTooling={discoveredUserTooling}
|
||||||
onRescanProjectConfigs={onRescanProjectConfigs}
|
|
||||||
onResolveProjectDiscoveredTooling={onResolveProjectDiscoveredTooling}
|
|
||||||
onResolveUserDiscoveredTooling={onResolveUserDiscoveredTooling}
|
onResolveUserDiscoveredTooling={onResolveUserDiscoveredTooling}
|
||||||
selectedProjectName={selectedProjectName}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeSection === 'lsp-profiles' && (
|
{activeSection === 'lsp-profiles' && (
|
||||||
@@ -607,68 +595,32 @@ function EmptyState({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
function DiscoveredMcpSection({
|
function DiscoveredMcpSection({
|
||||||
discoveredUserTooling,
|
discoveredUserTooling,
|
||||||
discoveredProjectTooling,
|
|
||||||
selectedProjectName,
|
|
||||||
onRescanProjectConfigs,
|
|
||||||
onResolveUserDiscoveredTooling,
|
onResolveUserDiscoveredTooling,
|
||||||
onResolveProjectDiscoveredTooling,
|
|
||||||
}: {
|
}: {
|
||||||
discoveredUserTooling: DiscoveredToolingState;
|
discoveredUserTooling: DiscoveredToolingState;
|
||||||
discoveredProjectTooling?: ProjectDiscoveredTooling;
|
|
||||||
selectedProjectName?: string;
|
|
||||||
onRescanProjectConfigs?: () => void;
|
|
||||||
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||||
onResolveProjectDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
|
||||||
}) {
|
}) {
|
||||||
const acceptedUser = listAcceptedDiscoveredMcpServers(discoveredUserTooling);
|
const acceptedUser = listAcceptedDiscoveredMcpServers(discoveredUserTooling);
|
||||||
const pendingUser = listPendingDiscoveredMcpServers(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;
|
if (!hasAny) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<SectionHeader
|
<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"
|
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 */}
|
<DiscoveredSubSection
|
||||||
{(acceptedUser.length > 0 || pendingUser.length > 0) && (
|
label="User-level"
|
||||||
<DiscoveredSubSection
|
description="From ~/.copilot/mcp.json"
|
||||||
label="User-level"
|
accepted={acceptedUser}
|
||||||
description="From ~/.copilot/mcp.json"
|
pending={pendingUser}
|
||||||
accepted={acceptedUser}
|
onResolve={onResolveUserDiscoveredTooling}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface SidebarProps {
|
|||||||
onProjectSelect: (projectId?: string) => void;
|
onProjectSelect: (projectId?: string) => void;
|
||||||
onSessionSelect: (sessionId: string) => void;
|
onSessionSelect: (sessionId: string) => void;
|
||||||
onOpenSettings: () => void;
|
onOpenSettings: () => void;
|
||||||
|
onOpenProjectSettings: (projectId: string) => void;
|
||||||
onRenameSession: (sessionId: string, title: string) => void;
|
onRenameSession: (sessionId: string, title: string) => void;
|
||||||
onDuplicateSession: (sessionId: string) => void;
|
onDuplicateSession: (sessionId: string) => void;
|
||||||
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
|
onSetSessionPinned: (sessionId: string, isPinned: boolean) => void;
|
||||||
@@ -328,6 +329,7 @@ function ProjectGroup({
|
|||||||
onRenameSubmit,
|
onRenameSubmit,
|
||||||
onRenameCancel,
|
onRenameCancel,
|
||||||
onRefreshGitContext,
|
onRefreshGitContext,
|
||||||
|
onOpenProjectSettings,
|
||||||
onNewSession,
|
onNewSession,
|
||||||
newSessionLabel,
|
newSessionLabel,
|
||||||
}: {
|
}: {
|
||||||
@@ -341,6 +343,7 @@ function ProjectGroup({
|
|||||||
onRenameSubmit: (sessionId: string, title: string) => void;
|
onRenameSubmit: (sessionId: string, title: string) => void;
|
||||||
onRenameCancel: () => void;
|
onRenameCancel: () => void;
|
||||||
onRefreshGitContext?: (projectId: string) => void;
|
onRefreshGitContext?: (projectId: string) => void;
|
||||||
|
onOpenProjectSettings?: (projectId: string) => void;
|
||||||
onNewSession?: () => void;
|
onNewSession?: () => void;
|
||||||
newSessionLabel?: string;
|
newSessionLabel?: string;
|
||||||
}){
|
}){
|
||||||
@@ -394,6 +397,19 @@ function ProjectGroup({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1.5">
|
<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 && (
|
{!isScratchpad && onRefreshGitContext && (
|
||||||
<span
|
<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"
|
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 && (
|
{pendingDiscoveryCount > 0 && (
|
||||||
<span
|
<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"
|
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"
|
||||||
title={`${pendingDiscoveryCount} MCP server${pendingDiscoveryCount === 1 ? '' : 's'} discovered`}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenProjectSettings?.(project.id);
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
title={`${pendingDiscoveryCount} MCP server${pendingDiscoveryCount === 1 ? '' : 's'} discovered — click to review`}
|
||||||
>
|
>
|
||||||
{pendingDiscoveryCount} new
|
{pendingDiscoveryCount} new
|
||||||
</span>
|
</span>
|
||||||
@@ -475,6 +496,7 @@ export function Sidebar({
|
|||||||
onProjectSelect,
|
onProjectSelect,
|
||||||
onSessionSelect,
|
onSessionSelect,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
|
onOpenProjectSettings,
|
||||||
onRenameSession,
|
onRenameSession,
|
||||||
onDuplicateSession,
|
onDuplicateSession,
|
||||||
onSetSessionPinned,
|
onSetSessionPinned,
|
||||||
@@ -678,6 +700,7 @@ export function Sidebar({
|
|||||||
onRenameSubmit={handleRenameSubmit}
|
onRenameSubmit={handleRenameSubmit}
|
||||||
onRenameCancel={() => setRenamingSessionId(undefined)}
|
onRenameCancel={() => setRenamingSessionId(undefined)}
|
||||||
onRefreshGitContext={onRefreshGitContext}
|
onRefreshGitContext={onRefreshGitContext}
|
||||||
|
onOpenProjectSettings={onOpenProjectSettings}
|
||||||
renamingSessionId={renamingSessionId}
|
renamingSessionId={renamingSessionId}
|
||||||
patterns={workspace.patterns}
|
patterns={workspace.patterns}
|
||||||
project={project}
|
project={project}
|
||||||
|
|||||||
Reference in New Issue
Block a user