feat: redesign auto-approval pill with server-level grouping and batch toggle

Group MCP tools by server and LSP tools by profile in the approval popover
instead of showing a flat list. Each server/profile group gets a collapsible
header with a batch toggle to approve/unapprove all tools at once. Add a
search filter (visible when >10 tools) and partial-state indicators for
groups with mixed approval. Add groupApprovalToolsByProvider shared helper
with tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-28 18:18:17 +01:00
co-authored by Copilot
parent 5ad85db0f5
commit 53a08e0ed4
4 changed files with 352 additions and 53 deletions
+63
View File
@@ -265,6 +265,69 @@ export function listApprovalToolNames(
return listApprovalToolDefinitions(tooling, runtimeTools).map((tool) => tool.id);
}
export interface ApprovalToolGroup {
id: string;
label: string;
kind: ApprovalToolKind;
tools: ApprovalToolDefinition[];
}
const approvalToolGroupKindOrder: ApprovalToolKind[] = ['builtin', 'mcp', 'lsp', 'mixed'];
export function groupApprovalToolsByProvider(
tools: ReadonlyArray<ApprovalToolDefinition>,
tooling: WorkspaceToolingSettings,
): ApprovalToolGroup[] {
const serverNames = new Map(tooling.mcpServers.map((s) => [s.id, s.name]));
const profileNames = new Map(tooling.lspProfiles.map((p) => [p.id, p.name]));
const groups = new Map<string, ApprovalToolGroup>();
for (const tool of tools) {
const groupKey = resolveApprovalToolGroupKey(tool, serverNames, profileNames);
let group = groups.get(groupKey.id);
if (!group) {
group = { id: groupKey.id, label: groupKey.label, kind: groupKey.kind, tools: [] };
groups.set(groupKey.id, group);
}
group.tools.push(tool);
}
return [...groups.values()].sort((a, b) => {
const kindDiff = approvalToolGroupKindOrder.indexOf(a.kind) - approvalToolGroupKindOrder.indexOf(b.kind);
if (kindDiff !== 0) return kindDiff;
return a.label.localeCompare(b.label);
});
}
function resolveApprovalToolGroupKey(
tool: ApprovalToolDefinition,
serverNames: ReadonlyMap<string, string>,
profileNames: ReadonlyMap<string, string>,
): { id: string; label: string; kind: ApprovalToolKind } {
if (tool.kind === 'builtin') {
return { id: 'builtin', label: 'Built-in', kind: 'builtin' };
}
const primaryProviderId = tool.providerIds[0];
if (tool.kind === 'mcp' && primaryProviderId) {
return {
id: `mcp:${primaryProviderId}`,
label: serverNames.get(primaryProviderId) ?? tool.providerNames[0] ?? primaryProviderId,
kind: 'mcp',
};
}
if (tool.kind === 'lsp' && primaryProviderId) {
return {
id: `lsp:${primaryProviderId}`,
label: profileNames.get(primaryProviderId) ?? tool.providerNames[0] ?? primaryProviderId,
kind: 'lsp',
};
}
return { id: 'other', label: 'Other', kind: 'mixed' };
}
export function validateMcpServerDefinition(server: McpServerDefinition): string | undefined {
if (!server.name.trim()) {
return 'MCP server name is required.';