Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions go/api/config/crd/bases/kagent.dev_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2277,7 +2277,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
status:
Expand Down Expand Up @@ -10158,7 +10157,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
x-kubernetes-validations:
Expand Down
1 change: 0 additions & 1 deletion go/api/config/crd/bases/kagent.dev_sandboxagents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7808,7 +7808,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
x-kubernetes-validations:
Expand Down
1 change: 0 additions & 1 deletion go/api/v1alpha1/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
// Whether to stream the response from the model.
// If not specified, the default value is true.
// +optional
Stream *bool `json:"stream,omitempty"`

Check failure on line 42 in go/api/v1alpha1/agent_types.go

View workflow job for this annotation

GitHub Actions / go-lint

File is not properly formatted (gofmt)
// +kubebuilder:validation:MaxItems=20
Tools []*Tool `json:"tools,omitempty"`
// Can either be a reference to the name of a Memory in the same namespace as the referencing Agent, or a reference to the name of a Memory in a different namespace in the form <namespace>/<name>
// +optional
Expand Down
1 change: 0 additions & 1 deletion go/api/v1alpha2/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@
// Whether to stream the response from the model.
// If not specified, the default value is false.
// +optional
Stream bool `json:"stream,omitempty"`

Check failure on line 182 in go/api/v1alpha2/agent_types.go

View workflow job for this annotation

GitHub Actions / go-lint

File is not properly formatted (gofmt)
// +kubebuilder:validation:MaxItems=20
Tools []*Tool `json:"tools,omitempty"`
// A2AConfig instantiates an A2A server for this agent,
// served on the HTTP port of the kagent kubernetes
Expand Down
2 changes: 0 additions & 2 deletions helm/kagent-crds/templates/kagent.dev_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2277,7 +2277,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
status:
Expand Down Expand Up @@ -10158,7 +10157,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
x-kubernetes-validations:
Expand Down
1 change: 0 additions & 1 deletion helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7808,7 +7808,6 @@ spec:
rule: '!(has(self.agent) && self.type != ''Agent'')'
- message: type.agent must be specified for Agent filter.type
rule: '!(!has(self.agent) && self.type == ''Agent'')'
maxItems: 20
type: array
type: object
x-kubernetes-validations:
Expand Down
25 changes: 9 additions & 16 deletions ui/src/components/create/SelectToolsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { toast } from "sonner";
import KagentLogo from "../kagent-logo";
import { k8sRefUtils } from "@/lib/k8sUtils";

// Maximum number of tools that can be selected
const MAX_TOOLS_LIMIT = 20;
const TOOLS_WARNING_THRESHOLD = 20;

interface SelectToolsDialogProps {
open: boolean;
Expand Down Expand Up @@ -119,7 +118,7 @@ export const SelectToolsDialog: React.FC<SelectToolsDialogProps> = ({ open, onOp
}, 0);
}, [localSelectedTools]);

const isLimitReached = actualSelectedCount >= MAX_TOOLS_LIMIT;
const showToolCountWarning = actualSelectedCount >= TOOLS_WARNING_THRESHOLD;

const filteredAvailableItems = useMemo(() => {
const searchLower = searchTerm.toLowerCase();
Expand Down Expand Up @@ -223,10 +222,6 @@ export const SelectToolsDialog: React.FC<SelectToolsDialogProps> = ({ open, onOp
return;
}

if (actualSelectedCount >= MAX_TOOLS_LIMIT) {
return;
}

let toolToAdd: Tool;

if (isAgentResponse(item)) {
Expand Down Expand Up @@ -427,20 +422,18 @@ export const SelectToolsDialog: React.FC<SelectToolsDialogProps> = ({ open, onOp
{items.map((item) => {
const { displayName, description, identifier, providerText } = getItemDisplayInfo(item);
const isSelected = isItemSelected(item);
const isDisabled = !isSelected && isLimitReached;

return (
<div
key={identifier}
className={`flex items-center justify-between p-3 pr-2 group min-w-0 ${isDisabled ? 'opacity-50 cursor-not-allowed' : isSelected ? 'cursor-default' : 'cursor-pointer hover:bg-muted/50'}`}
onClick={() => !isDisabled && !isSelected && handleAddItem(item)}
className={`flex items-center justify-between p-3 pr-2 group min-w-0 ${isSelected ? 'cursor-default' : 'cursor-pointer hover:bg-muted/50'}`}
onClick={() => !isSelected && handleAddItem(item)}
>
<div className="flex-1 overflow-hidden pr-2">
<p className="font-medium text-sm truncate overflow-hidden">{highlightMatch(displayName, searchTerm)}</p>
{description && <p className="text-xs text-muted-foreground">{highlightMatch(description, searchTerm)}</p>}
{providerText && <p className="text-xs text-muted-foreground/80 font-mono mt-1">{highlightMatch(providerText, searchTerm)}</p>}
</div>
{!isSelected && !isDisabled && (
{!isSelected && (
<Button variant="ghost" size="icon" className="h-7 w-7 opacity-0 group-hover:opacity-100 text-green-600 hover:text-green-700" >
<PlusCircle className="h-4 w-4"/>
</Button>
Expand Down Expand Up @@ -506,17 +499,17 @@ export const SelectToolsDialog: React.FC<SelectToolsDialogProps> = ({ open, onOp
{/* Right Panel: Selected Tools */}
<div className="w-1/2 min-w-0 flex flex-col p-4 space-y-4">
<div className="flex items-center justify-between gap-2">
<h3 className="text-lg font-semibold">Selected ({actualSelectedCount}/{MAX_TOOLS_LIMIT})</h3>
<h3 className="text-lg font-semibold">Selected ({actualSelectedCount})</h3>
<Button variant="ghost" size="sm" onClick={clearAllSelectedTools} disabled={actualSelectedCount === 0}>
Clear All
</Button>
</div>

{isLimitReached && actualSelectedCount >= MAX_TOOLS_LIMIT && (
{showToolCountWarning && (
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 flex items-start gap-2 text-amber-800 text-sm">
<AlertCircle className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div>
Tool limit reached. Deselect a tool to add another.
You have selected {actualSelectedCount} tools. A large number of tools may increase token usage and affect performance.
</div>
</div>
)}
Expand Down Expand Up @@ -679,7 +672,7 @@ export const SelectToolsDialog: React.FC<SelectToolsDialogProps> = ({ open, onOp
<DialogFooter className="p-4 border-t mt-auto">
<div className="flex justify-between w-full items-center">
<div className="text-sm text-muted-foreground">
Select up to {MAX_TOOLS_LIMIT} tools for your agent.
Select tools and agents for your agent.
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>Cancel</Button>
Expand Down
70 changes: 70 additions & 0 deletions ui/src/components/create/__tests__/SelectToolsDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @jest-environment jsdom
*/
import { describe, expect, it, jest } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SelectToolsDialog } from "@/components/create/SelectToolsDialog";
import type { AgentResponse, Tool, ToolsResponse } from "@/types";

const serverRef = "kagent/kagent-tool-server";

const makeTool = (id: string): ToolsResponse => ({
id,
server_name: serverRef,
description: `Description for ${id}`,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
deleted_at: "",
group_kind: "MCPServer.kagent.dev",
});

const makeSelectedTools = (count: number): Tool[] => [
{
type: "McpServer",
mcpServer: {
name: "kagent-tool-server",
namespace: "kagent",
kind: "MCPServer",
apiGroup: "kagent.dev",
toolNames: Array.from({ length: count }, (_, i) => `tool_${i}`),
},
},
];

const renderOpenDialog = (selectedTools: Tool[], availableTools: ToolsResponse[]) => {
const props = {
open: false,
onOpenChange: jest.fn(),
availableTools,
selectedTools,
onToolsSelected: jest.fn(),
availableAgents: [] as AgentResponse[],
loadingAgents: false,
currentAgentNamespace: "kagent",
};

const view = render(<SelectToolsDialog {...props} />);
view.rerender(<SelectToolsDialog {...props} open />);
return props;
};

describe("SelectToolsDialog", () => {
it("warns but does not block selection when many tools are selected", async () => {
const user = userEvent.setup();
const availableTools = Array.from({ length: 21 }, (_, i) => makeTool(`tool_${i}`));

renderOpenDialog(makeSelectedTools(20), availableTools);

expect(screen.getByText("Selected (20)")).toBeInTheDocument();
expect(screen.getByText(/You have selected 20 tools/i)).toBeInTheDocument();
expect(screen.queryByText(/Tool limit reached/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Select up to/i)).not.toBeInTheDocument();

await user.click(screen.getByText("tool_20"));

expect(screen.getByText("Selected (21)")).toBeInTheDocument();
expect(screen.getByText(/You have selected 21 tools/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Save Selection \(21\)/i })).toBeInTheDocument();
});
});
Loading