feat: show MCP probe progress in approval pill

- Thread mcpProbingServerIds from workspace state through App → ChatPane → InlineApprovalPill
- Show animated spinner and 'probing…' label on pill button while any MCP server is being probed
- Show per-server probing indicator in popover: spinning icon, 'probing…' badge, hidden toggle
- Render approval pill during probing even when no MCP tools are known yet

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-28 22:08:25 +01:00
co-authored by Copilot
parent cc13ed29f5
commit 651a7d27fc
3 changed files with 53 additions and 14 deletions
+1
View File
@@ -333,6 +333,7 @@ export default function App() {
});
}}
availableModels={availableModels}
mcpProbingServerIds={workspace.mcpProbingServerIds}
pattern={patternForSession}
project={projectForSession}
runtimeTools={sidecarCapabilities?.runtimeTools}
+8 -2
View File
@@ -41,6 +41,7 @@ interface ChatPaneProps {
session: SessionRecord;
availableModels: ReadonlyArray<ModelDefinition>;
toolingSettings: WorkspaceToolingSettings;
mcpProbingServerIds?: string[];
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
sessionUsage?: SessionUsageState;
onSend: (content: string, attachments?: ChatMessageAttachment[], messageMode?: MessageMode) => Promise<void>;
@@ -65,6 +66,7 @@ export function ChatPane({
session,
availableModels,
toolingSettings,
mcpProbingServerIds,
runtimeTools,
sessionUsage,
onSend,
@@ -143,6 +145,8 @@ export function ChatPane({
}
return counted.size;
}, [approvalTools, effectiveAutoApproved, toolingSettings]);
const isProbingMcp = (mcpProbingServerIds?.length ?? 0) > 0;
const hasApprovalContent = approvalTools.length > 0 || isProbingMcp;
useEffect(() => {
transcriptRef.current?.scrollTo({
@@ -487,13 +491,14 @@ export function ChatPane({
selection={toolSelection}
/>
)}
{hasToolCallApproval && onUpdateSessionApprovalSettings && approvalTools.length > 0 && (
{hasToolCallApproval && onUpdateSessionApprovalSettings && hasApprovalContent && (
<InlineApprovalPill
approvalTools={approvalTools}
disabled={isComposerDisabled}
effectiveAutoApproved={effectiveAutoApproved}
effectiveAutoApprovedCount={effectiveAutoApprovedCount}
isOverridden={isApprovalOverridden}
mcpProbingServerIds={mcpProbingServerIds}
onUpdate={onUpdateSessionApprovalSettings}
toolingSettings={toolingSettings}
/>
@@ -543,13 +548,14 @@ export function ChatPane({
selection={toolSelection}
/>
)}
{hasToolCallApproval && onUpdateSessionApprovalSettings && approvalTools.length > 0 && (
{hasToolCallApproval && onUpdateSessionApprovalSettings && hasApprovalContent && (
<InlineApprovalPill
approvalTools={approvalTools}
disabled={isComposerDisabled}
effectiveAutoApproved={effectiveAutoApproved}
effectiveAutoApprovedCount={effectiveAutoApprovedCount}
isOverridden={isApprovalOverridden}
mcpProbingServerIds={mcpProbingServerIds}
onUpdate={onUpdateSessionApprovalSettings}
toolingSettings={toolingSettings}
/>
+44 -12
View File
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { ChevronDown, ChevronRight, Minus, Search, Sparkles } from 'lucide-react';
import { ChevronDown, ChevronRight, Loader2, Minus, Search, Sparkles } from 'lucide-react';
import { ProviderIcon } from '@renderer/components/ProviderIcons';
import { PopoverToggleRow } from '@renderer/components/ui';
@@ -347,6 +347,7 @@ export function InlineApprovalPill({
effectiveAutoApprovedCount,
isOverridden,
disabled,
mcpProbingServerIds,
onUpdate,
}: {
approvalTools: ApprovalToolDefinition[];
@@ -355,6 +356,7 @@ export function InlineApprovalPill({
effectiveAutoApprovedCount: number;
isOverridden: boolean;
disabled: boolean;
mcpProbingServerIds?: string[];
onUpdate: (settings: { autoApprovedToolNames?: string[] }) => void;
}){
const [open, setOpen] = useState(false);
@@ -362,6 +364,12 @@ export function InlineApprovalPill({
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const ref = useClickOutside<HTMLDivElement>(() => { setOpen(false); setSearch(''); }, open);
const probingSet = useMemo(
() => new Set(mcpProbingServerIds ?? []),
[mcpProbingServerIds],
);
const isProbingAny = probingSet.size > 0;
const groups = useMemo(
() => groupApprovalToolsByProvider(approvalTools, toolingSettings),
[approvalTools, toolingSettings],
@@ -439,6 +447,12 @@ export function InlineApprovalPill({
return 'none';
}
function isGroupProbing(group: ApprovalToolGroup): boolean {
if (group.kind !== 'mcp') return false;
const serverId = group.id.replace(/^mcp:/, '');
return probingSet.has(serverId);
}
function toggleExpanded(groupId: string) {
setExpandedGroups((prev) => {
const next = new Set(prev);
@@ -472,8 +486,15 @@ export function InlineApprovalPill({
onClick={() => setOpen(!open)}
type="button"
>
<ShieldCheck className="size-2.5" />
<span>{effectiveAutoApprovedCount}/{totalItemCount} auto-approved</span>
{isProbingAny ? (
<Loader2 className="size-2.5 animate-spin" aria-label="Probing MCP servers" />
) : (
<ShieldCheck className="size-2.5" />
)}
<span>
{effectiveAutoApprovedCount}/{totalItemCount} auto-approved
{isProbingAny && <span className="text-zinc-500"> · probing</span>}
</span>
<ChevronDown className={`size-2.5 transition ${open ? 'rotate-180' : ''}`} />
</button>
@@ -525,6 +546,7 @@ export function InlineApprovalPill({
const isBuiltin = group.kind === 'builtin';
const isCollapsible = !isBuiltin;
const expanded = isBuiltin || isGroupExpanded(group.id);
const probing = isGroupProbing(group);
const groupState = isGroupApproved(group);
const allApproved = groupState === 'all';
const someApproved = groupState === 'some';
@@ -547,20 +569,30 @@ export function InlineApprovalPill({
role="button"
tabIndex={0}
>
{group.tools.length > 0 ? (
{probing ? (
<Loader2 className="size-3 shrink-0 animate-spin text-indigo-400" aria-label="Probing server" />
) : group.tools.length > 0 ? (
<ChevronRight className={`size-3 shrink-0 text-zinc-600 transition ${expanded ? 'rotate-90' : ''}`} />
) : (
<Server className="size-3 shrink-0 text-zinc-600" />
)}
<span className="min-w-0 flex-1 truncate text-[12px] font-medium text-zinc-300">{group.label}</span>
<span className="shrink-0 rounded-full bg-zinc-800/80 px-1.5 py-px text-[9px] font-medium tabular-nums text-zinc-500">
{approvedLabel}
</span>
<GroupToggle
allApproved={allApproved}
someApproved={someApproved}
onToggle={(e) => { e.stopPropagation(); toggleGroup(group); }}
/>
{probing ? (
<span className="shrink-0 rounded-full bg-indigo-500/10 px-1.5 py-px text-[9px] font-medium text-indigo-400">
probing
</span>
) : (
<span className="shrink-0 rounded-full bg-zinc-800/80 px-1.5 py-px text-[9px] font-medium tabular-nums text-zinc-500">
{approvedLabel}
</span>
)}
{!probing && (
<GroupToggle
allApproved={allApproved}
someApproved={someApproved}
onToggle={(e) => { e.stopPropagation(); toggleGroup(group); }}
/>
)}
</div>
)}