// *****************************************************************************
// Copyright (C) 2026 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AISettingsService,
ParsedCapability,
GenericCapabilitySelections,
CAPABILITY_TYPE_PROMPT_MAP,
ServerToolDescriptor,
} from '@theia/ai-core/lib/common';
import { nls } from '@theia/core';
import { codicon } from '@theia/core/lib/browser';
import * as React from '@theia/core/shared/react';
import { AiConfigurationItemRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-item-row';
import { AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row-service';
import { AiToggleSwitch } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
/**
* An agent-detail subsection header with an optional trailing action, so a section-wide action (e.g. its
* reset) rides on the title line instead of taking a prominent row of its own above the list.
*/
const AgentSectionHeader: React.FC<{ title: string; action?: React.ReactNode }> = ({ title, action }) => (
{title}
{action}
);
/**
* The section-wide reset: the same discard icon button the prompt variant rows use, rather than a text
* button, so it reads as a Theia reset affordance. Its tooltip says what the section-wide reset does.
*/
const ResetAllButton: React.FC<{ title: string; disabled?: boolean; onReset: () => void }> = ({ title, disabled, onReset }) => (
);
export interface AgentCapabilitiesSettingsProps {
capabilities: ParsedCapability[];
agentId: string;
savedOverrides: Record | undefined;
aiSettingsService: AISettingsService;
settingsRowService: AiSettingsRowService;
/**
* Reveals the capability's prompt fragment on the Prompt Snippets page. The text is global, shared by
* every agent that references it, which is why the row links there instead of editing it in place.
*/
onOpenPromptSnippet: (fragmentId: string) => void;
}
/**
* The "Available Capabilities" section of the agent detail page: one toggle per capability the prompt
* declares. Each toggle writes an override only when it differs from the agent's default; a per-row
* reset (gear menu) and a "Reset All to Defaults" button clear overrides back to the declared defaults.
*/
export const AgentCapabilitiesSettings = ({
capabilities, agentId, savedOverrides, aiSettingsService, settingsRowService, onOpenPromptSnippet
}: AgentCapabilitiesSettingsProps) => {
const [loading, setLoading] = React.useState(false);
const handleToggle = async (fragmentId: string, currentValue: boolean): Promise => {
if (loading) {
return;
}
setLoading(true);
try {
const capability = capabilities.find(c => c.fragmentId === fragmentId);
if (!capability) {
return;
}
const newValue = !currentValue;
const newOverrides = { ...savedOverrides };
if (newValue === capability.defaultEnabled) {
delete newOverrides[fragmentId];
} else {
newOverrides[fragmentId] = newValue;
}
await aiSettingsService.updateAgentSettings(agentId, { capabilityOverrides: newOverrides });
} finally {
setLoading(false);
}
};
const handleResetAll = async (): Promise => {
if (loading) {
return;
}
setLoading(true);
try {
await aiSettingsService.updateAgentSettings(agentId, { capabilityOverrides: undefined });
} finally {
setLoading(false);
}
};
// Drop a single capability's override so it falls back to the agent's default.
const resetCapability = async (fragmentId: string): Promise => {
if (loading) {
return;
}
setLoading(true);
try {
const newOverrides = { ...savedOverrides };
delete newOverrides[fragmentId];
await aiSettingsService.updateAgentSettings(agentId, { capabilityOverrides: newOverrides });
} finally {
setLoading(false);
}
};
const getCurrentValue = (capability: ParsedCapability): boolean =>
savedOverrides && capability.fragmentId in savedOverrides ? savedOverrides[capability.fragmentId] : capability.defaultEnabled;
const hasOverride = (capability: ParsedCapability): boolean => savedOverrides !== undefined && capability.fragmentId in savedOverrides;
const hasAnyOverrides = savedOverrides && Object.keys(savedOverrides).length > 0;
// Show "Name (id)" when the fragment declares a name; otherwise just the id.
const labelFor = (capability: ParsedCapability): string =>
capability.name ? `${capability.name} (${capability.fragmentId})` : capability.fragmentId;
const snippetLinkTooltip = nls.localize('theia/ai/ide/agentConfiguration/showCapabilitySnippet', 'Show snippet');
return <>
}
/>
>;
};
export interface AgentServerToolsSettingsProps {
agentId: string;
/** Server tools the agent's model offers. */
tools: ServerToolDescriptor[];
/** Vendor the selections are keyed by: the vendor of the model offering the tools. */
vendor: string;
savedSelections: Record | undefined;
aiSettingsService: AISettingsService;
/** Backs each enabled row's gear menu, whose "Reset Setting" switches that tool back off. */
settingsRowService: AiSettingsRowService;
}
/**
* The "Server Tools" section of the agent detail page: one toggle per tool the agent's model offers, executed
* by the provider rather than by Theia. The same per-agent setting the chat input's capabilities popup writes,
* keyed by model vendor because the tools are vendor-specific.
*
* Nothing is enabled unless it is listed, so an enabled tool is the deviation from the default and carries the
* modified indicator; "Reset All to Defaults" drops this vendor's selections, leaving other vendors' alone.
*/
export const AgentServerToolsSettings = ({
agentId, tools, vendor, savedSelections, aiSettingsService, settingsRowService
}: AgentServerToolsSettingsProps) => {
const [loading, setLoading] = React.useState(false);
const enabledIds = savedSelections?.[vendor] ?? [];
const update = async (ids: string[]): Promise => {
if (loading) {
return;
}
setLoading(true);
try {
// Drop the vendor's entry entirely once nothing is enabled, rather than leaving an empty array behind.
const next = { ...savedSelections };
if (ids.length > 0) {
next[vendor] = ids;
} else {
delete next[vendor];
}
await aiSettingsService.updateAgentSettings(agentId, {
serverToolSelections: Object.keys(next).length > 0 ? next : undefined
});
} finally {
setLoading(false);
}
};
const handleToggle = (toolId: string, enabled: boolean): Promise =>
update(enabled ? [...enabledIds, toolId] : enabledIds.filter(id => id !== toolId));
return <>
0 && update([])}
disabled={loading}
title={nls.localize('theia/ai/ide/agentConfiguration/resetAllServerTools', 'Turn off every server tool of this provider')}
/>}
/>
{/* Same warning the capabilities popup shows on its Server Tools group, since enabling one here has
exactly that consequence. Reuses that key so both read identically in every language. */}
{nls.localize('theia/ai/chat-ui/serverToolsWarning',
'These tools are auto-approved when selected. They may be executed by the model at any time.')}