mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-07 12:18:44 +02:00
feat: add MCP and LSP tooling support
Add global MCP/LSP settings, per-session Activity toggles, sidecar runtime integration, tests, and documentation updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -10,7 +10,15 @@ import {
|
||||
} from '@renderer/lib/sessionActivity';
|
||||
import { inferProvider } from '@shared/domain/models';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import {
|
||||
resolveSessionToolingSelection,
|
||||
type SessionRecord,
|
||||
} from '@shared/domain/session';
|
||||
import type {
|
||||
LspProfileDefinition,
|
||||
McpServerDefinition,
|
||||
SessionToolingSelection,
|
||||
} from '@shared/domain/tooling';
|
||||
import { ProviderIcon } from './ProviderIcons';
|
||||
|
||||
function formatModel(model: string): string {
|
||||
@@ -30,17 +38,31 @@ function formatEffort(effort: string | undefined): string | undefined {
|
||||
|
||||
interface ActivityPanelProps {
|
||||
activity?: SessionActivityState;
|
||||
lspProfiles: LspProfileDefinition[];
|
||||
mcpServers: McpServerDefinition[];
|
||||
onUpdateSessionTooling: (selection: SessionToolingSelection) => void;
|
||||
pattern: PatternDefinition;
|
||||
projectIsScratchpad: boolean;
|
||||
session: SessionRecord;
|
||||
}
|
||||
|
||||
export function ActivityPanel({ activity, pattern, session }: ActivityPanelProps) {
|
||||
export function ActivityPanel({
|
||||
activity,
|
||||
lspProfiles,
|
||||
mcpServers,
|
||||
onUpdateSessionTooling,
|
||||
pattern,
|
||||
projectIsScratchpad,
|
||||
session,
|
||||
}: ActivityPanelProps) {
|
||||
const activityRows = useMemo(
|
||||
() => buildAgentActivityRows(activity, pattern.agents),
|
||||
[activity, pattern.agents],
|
||||
);
|
||||
const selection = useMemo(() => resolveSessionToolingSelection(session), [session]);
|
||||
|
||||
const isBusy = session.status === 'running';
|
||||
const toolsDisabled = isBusy || projectIsScratchpad;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -57,7 +79,71 @@ export function ActivityPanel({ activity, pattern, session }: ActivityPanelProps
|
||||
|
||||
{/* Agent cards */}
|
||||
<div className="flex-1 overflow-y-auto px-3 py-3">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-[12px] font-semibold text-zinc-200">Session tools</h3>
|
||||
<p className="mt-0.5 text-[11px] text-zinc-500">
|
||||
Enable globally configured MCPs and LSPs for this session.
|
||||
</p>
|
||||
</div>
|
||||
{toolsDisabled && (
|
||||
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium text-zinc-400">
|
||||
{projectIsScratchpad ? 'Scratchpad disabled' : 'Locked while running'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{projectIsScratchpad ? (
|
||||
<p className="mt-3 text-[11px] leading-relaxed text-zinc-500">
|
||||
Scratchpad stays tool-free. Start a project-backed session to use MCPs or LSPs.
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-3 space-y-3">
|
||||
<ToolToggleGroup
|
||||
description="Globally configured MCP servers"
|
||||
emptyMessage="No MCP servers configured in Settings."
|
||||
enabledIds={selection.enabledMcpServerIds}
|
||||
items={mcpServers.map((server) => ({
|
||||
id: server.id,
|
||||
label: server.name,
|
||||
detail:
|
||||
server.transport === 'local'
|
||||
? server.command
|
||||
: server.url,
|
||||
}))}
|
||||
onToggle={(id) =>
|
||||
onUpdateSessionTooling({
|
||||
...selection,
|
||||
enabledMcpServerIds: toggleId(selection.enabledMcpServerIds, id),
|
||||
})
|
||||
}
|
||||
title="MCP servers"
|
||||
disabled={toolsDisabled}
|
||||
/>
|
||||
<ToolToggleGroup
|
||||
description="Globally configured LSP profiles"
|
||||
emptyMessage="No LSP profiles configured in Settings."
|
||||
enabledIds={selection.enabledLspProfileIds}
|
||||
items={lspProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
label: profile.name,
|
||||
detail: `${profile.languageId} · ${profile.command}`,
|
||||
}))}
|
||||
onToggle={(id) =>
|
||||
onUpdateSessionTooling({
|
||||
...selection,
|
||||
enabledLspProfileIds: toggleId(selection.enabledLspProfileIds, id),
|
||||
})
|
||||
}
|
||||
title="LSP profiles"
|
||||
disabled={toolsDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activityRows.map((row, index) => {
|
||||
const agent = pattern.agents[index];
|
||||
const isActive = isAgentActivityActive(row.activity);
|
||||
@@ -149,3 +235,76 @@ export function ActivityPanel({ activity, pattern, session }: ActivityPanelProps
|
||||
);
|
||||
}
|
||||
|
||||
function ToolToggleGroup({
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
enabledIds,
|
||||
onToggle,
|
||||
emptyMessage,
|
||||
disabled,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
items: Array<{ id: string; label: string; detail?: string }>;
|
||||
enabledIds: string[];
|
||||
onToggle: (id: string) => void;
|
||||
emptyMessage: string;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<h4 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-zinc-500">
|
||||
{title}
|
||||
</h4>
|
||||
<p className="mt-0.5 text-[11px] text-zinc-600">{description}</p>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<p className="text-[11px] text-zinc-600">{emptyMessage}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{items.map((item) => {
|
||||
const enabled = enabledIds.includes(item.id);
|
||||
return (
|
||||
<button
|
||||
className={`flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left transition ${
|
||||
enabled
|
||||
? 'border-blue-500/30 bg-blue-500/5'
|
||||
: 'border-zinc-800 bg-zinc-900/30'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-60' : 'hover:border-zinc-700 hover:bg-zinc-900/60'}`}
|
||||
disabled={disabled}
|
||||
key={item.id}
|
||||
onClick={() => onToggle(item.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[12px] font-medium text-zinc-200">{item.label}</div>
|
||||
{item.detail && (
|
||||
<div className="truncate text-[11px] text-zinc-500">{item.detail}</div>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
||||
enabled
|
||||
? 'bg-blue-500/10 text-blue-300'
|
||||
: 'bg-zinc-800 text-zinc-500'
|
||||
}`}
|
||||
>
|
||||
{enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function toggleId(current: string[], id: string): string[] {
|
||||
return current.includes(id)
|
||||
? current.filter((currentId) => currentId !== id)
|
||||
: [...current, id];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user