From f02b0d0bfe46f374ca47073d5f25ddf56a639d11 Mon Sep 17 00:00:00 2001 From: Sean Florez Date: Sat, 28 Feb 2026 01:03:07 +0000 Subject: [PATCH 1/5] feat: replace hard tool limit with configurable warning (#694) Remove the hard 20-tool limit from both the CRD kubebuilder validation and the UI. Instead, show a warning in the UI when 20+ tools are selected, informing users about potential increased token usage without blocking them from proceeding. Changes: - Remove +kubebuilder:validation:MaxItems=20 from v1alpha1 and v1alpha2 agent_types.go Tools field - Replace MAX_TOOLS_LIMIT constant with TOOLS_WARNING_THRESHOLD in SelectToolsDialog.tsx - Change blocking behavior to soft warning when threshold is exceeded - Remove skill count limit of 20 in agent creation page - Add 7 tests verifying the new warning behavior Fixes #694 Signed-off-by: Sean Florez --- go/api/v1alpha1/agent_types.go | 1 - go/api/v1alpha2/agent_types.go | 1 - ui/src/app/agents/new/page.tsx | 4 +- .../components/create/SelectToolsDialog.tsx | 25 ++-- .../__tests__/SelectToolsDialog.test.tsx | 137 ++++++++++++++++++ 5 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 ui/src/components/create/__tests__/SelectToolsDialog.test.tsx diff --git a/go/api/v1alpha1/agent_types.go b/go/api/v1alpha1/agent_types.go index ffd98b31c..fe6e4b74f 100644 --- a/go/api/v1alpha1/agent_types.go +++ b/go/api/v1alpha1/agent_types.go @@ -40,7 +40,6 @@ type AgentSpec struct { // If not specified, the default value is true. // +optional Stream *bool `json:"stream,omitempty"` - // +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 / // +optional diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 7f08aa1d5..0480ab006 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -133,7 +133,6 @@ type DeclarativeAgentSpec struct { // If not specified, the default value is false. // +optional Stream bool `json:"stream,omitempty"` - // +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 diff --git a/ui/src/app/agents/new/page.tsx b/ui/src/app/agents/new/page.tsx index 3c4a51502..f8018623e 100644 --- a/ui/src/app/agents/new/page.tsx +++ b/ui/src/app/agents/new/page.tsx @@ -774,9 +774,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo variant="outline" size="icon" onClick={() => { - if ((state.skillRefs || []).length < 20) { - setState(prev => ({ ...prev, skillRefs: [...prev.skillRefs, ""] })); - } + setState(prev => ({ ...prev, skillRefs: [...prev.skillRefs, ""] })); }} title="Add skill" > diff --git a/ui/src/components/create/SelectToolsDialog.tsx b/ui/src/components/create/SelectToolsDialog.tsx index f03b930a4..57176453d 100644 --- a/ui/src/components/create/SelectToolsDialog.tsx +++ b/ui/src/components/create/SelectToolsDialog.tsx @@ -13,8 +13,8 @@ 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; +// Threshold at which a warning is shown about potential increased token usage +const TOOLS_WARNING_THRESHOLD = 20; interface SelectToolsDialogProps { open: boolean; @@ -117,7 +117,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp }, 0); }, [localSelectedTools]); - const isLimitReached = actualSelectedCount >= MAX_TOOLS_LIMIT; + const isWarningThresholdReached = actualSelectedCount >= TOOLS_WARNING_THRESHOLD; const filteredAvailableItems = useMemo(() => { const searchLower = searchTerm.toLowerCase(); @@ -221,10 +221,6 @@ export const SelectToolsDialog: React.FC = ({ open, onOp return; } - if (actualSelectedCount >= MAX_TOOLS_LIMIT) { - return; - } - let toolToAdd: Tool; if (isAgentResponse(item)) { @@ -399,20 +395,19 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {items.map((item) => { const { displayName, description, identifier, providerText } = getItemDisplayInfo(item); const isSelected = isItemSelected(item); - const isDisabled = !isSelected && isLimitReached; return (
!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)} >

{highlightMatch(displayName, searchTerm)}

{description &&

{highlightMatch(description, searchTerm)}

} {providerText &&

{highlightMatch(providerText, searchTerm)}

}
- {!isSelected && !isDisabled && ( + {!isSelected && ( @@ -478,17 +473,17 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {/* Right Panel: Selected Tools */}
-

Selected ({actualSelectedCount}/{MAX_TOOLS_LIMIT})

+

Selected ({actualSelectedCount})

- {isLimitReached && actualSelectedCount >= MAX_TOOLS_LIMIT && ( + {isWarningThresholdReached && (
- 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.
)} @@ -607,7 +602,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp
- Select up to {MAX_TOOLS_LIMIT} tools for your agent. + Select tools for your agent.
diff --git a/ui/src/components/create/__tests__/SelectToolsDialog.test.tsx b/ui/src/components/create/__tests__/SelectToolsDialog.test.tsx new file mode 100644 index 000000000..25b26c565 --- /dev/null +++ b/ui/src/components/create/__tests__/SelectToolsDialog.test.tsx @@ -0,0 +1,137 @@ +/** + * Test: Tool Limit Warning (Issue #694) + * + * Verifies that the hard 20-tool limit has been replaced with a soft warning. + * - Tools can be added beyond 20 (no hard block) + * - A warning is shown when 20+ tools are selected + * - The warning informs about potential token usage impact + */ + +import { describe, it, expect, jest, beforeEach } from "@jest/globals"; +import { render, screen, act } from "@testing-library/react"; +import { SelectToolsDialog } from "../SelectToolsDialog"; +import type { Tool, ToolsResponse, AgentResponse } from "@/types"; + +// Mock next/link +jest.mock("next/link", () => { + return { + __esModule: true, + default: ({ children, ...props }: { children: React.ReactNode; href: string }) => {children}, + }; +}); + +// Mock sonner toast +jest.mock("sonner", () => ({ + toast: { + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, +})); + +// Mock ScrollArea to render children directly +jest.mock("@/components/ui/scroll-area", () => ({ + ScrollArea: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +// Helper to create mock tools +function createMockTools(count: number): ToolsResponse[] { + return Array.from({ length: count }, (_, i) => ({ + id: `tool-${i}`, + name: `Tool ${i}`, + description: `Description for tool ${i}`, + server_name: `server-${i}`, + server_description: `Server ${i}`, + })) as unknown as ToolsResponse[]; +} + +// Helper to create selected tools +function createSelectedTools(count: number): Tool[] { + return Array.from({ length: count }, (_, i) => ({ + type: "McpServer" as const, + mcpServer: { + name: `server-${i}`, + kind: "ToolServer", + apiGroup: "kagent.dev", + toolNames: [`tool-${i}`], + }, + })); +} + +const baseProps = { + onOpenChange: jest.fn(), + availableTools: createMockTools(30), + onToolsSelected: jest.fn(), + availableAgents: [] as AgentResponse[], + loadingAgents: false, + currentAgentNamespace: "default", +}; + +/** + * Helper: renders the dialog first closed, then opens it to trigger + * the useLayoutEffect that copies selectedTools → localSelectedTools. + */ +function renderDialogWithTools(selectedTools: Tool[]) { + const props = { ...baseProps, selectedTools, open: false }; + const { rerender } = render(); + // Transition from closed to open to trigger useLayoutEffect + act(() => { + rerender(); + }); + return { rerender }; +} + +describe("SelectToolsDialog - Tool Limit Warning (Issue #694)", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should NOT show warning when fewer than 20 tools are selected", () => { + renderDialogWithTools(createSelectedTools(5)); + + expect(screen.queryByText(/may increase token usage/i)).not.toBeInTheDocument(); + }); + + it("should show warning when 20 or more tools are selected", () => { + renderDialogWithTools(createSelectedTools(20)); + + expect(screen.getByText(/may increase token usage/i)).toBeInTheDocument(); + }); + + it("should show warning with correct count when more than 20 tools are selected", () => { + renderDialogWithTools(createSelectedTools(25)); + + expect(screen.getByText(/may increase token usage/i)).toBeInTheDocument(); + expect(screen.getByText(/You have selected 25 tools/)).toBeInTheDocument(); + }); + + it("should display the selected count without a maximum limit", () => { + renderDialogWithTools(createSelectedTools(25)); + + // Should show "Selected (25)" not "Selected (25/20)" + expect(screen.getByText("Selected (25)")).toBeInTheDocument(); + expect(screen.queryByText(/\/20/)).not.toBeInTheDocument(); + }); + + it("should not display 'Tool limit reached' blocking message", () => { + renderDialogWithTools(createSelectedTools(20)); + + expect(screen.queryByText(/Tool limit reached/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Deselect a tool to add another/i)).not.toBeInTheDocument(); + }); + + it("should show footer text without mentioning a maximum", () => { + renderDialogWithTools([]); + + expect(screen.getByText(/Select tools for your agent/i)).toBeInTheDocument(); + expect(screen.queryByText(/Select up to/i)).not.toBeInTheDocument(); + }); + + it("should not disable tool items when at or above the threshold", () => { + renderDialogWithTools(createSelectedTools(20)); + + // No elements should have the opacity-50 cursor-not-allowed class + const disabledElements = document.querySelectorAll(".opacity-50.cursor-not-allowed"); + expect(disabledElements.length).toBe(0); + }); +}); From 4215c9e5532070793b222b2bfdd4043cd4e8fb9d Mon Sep 17 00:00:00 2001 From: fl-sean03 Date: Sat, 28 Feb 2026 01:33:06 +0000 Subject: [PATCH 2/5] fix: regenerate CRDs and remove remaining MaxItems annotations Remove MaxItems=20 from v1alpha2 SkillForAgent.Refs and GitRefs annotations, regenerate CRD manifests, and sync Helm CRD templates to ensure the configurable warning change takes full effect. Signed-off-by: fl-sean03 Signed-off-by: Sean Florez --- go/api/v1alpha2/agent_types.go | 2 -- go/config/crd/bases/kagent.dev_agents.yaml | 4 ---- helm/kagent-crds/templates/kagent.dev_agents.yaml | 4 ---- 3 files changed, 10 deletions(-) diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 0480ab006..985e9d68c 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -77,7 +77,6 @@ type SkillForAgent struct { InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // The list of skill images to fetch. - // +kubebuilder:validation:MaxItems=20 // +kubebuilder:validation:MinItems=1 // +optional Refs []string `json:"refs,omitempty"` @@ -90,7 +89,6 @@ type SkillForAgent struct { GitAuthSecretRef *corev1.LocalObjectReference `json:"gitAuthSecretRef,omitempty"` // Git repositories to fetch skills from. - // +kubebuilder:validation:MaxItems=20 // +kubebuilder:validation:MinItems=1 // +optional GitRefs []GitRepo `json:"gitRefs,omitempty"` diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index 3c6dcbbb2..45e5690c3 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -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: @@ -10086,7 +10085,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: @@ -10142,7 +10140,6 @@ spec: required: - url type: object - maxItems: 20 minItems: 1 type: array insecureSkipVerify: @@ -10154,7 +10151,6 @@ spec: description: The list of skill images to fetch. items: type: string - maxItems: 20 minItems: 1 type: array type: object diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 3c6dcbbb2..45e5690c3 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -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: @@ -10086,7 +10085,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: @@ -10142,7 +10140,6 @@ spec: required: - url type: object - maxItems: 20 minItems: 1 type: array insecureSkipVerify: @@ -10154,7 +10151,6 @@ spec: description: The list of skill images to fetch. items: type: string - maxItems: 20 minItems: 1 type: array type: object From 7d35a43d05ab312bdc31e3a55211fe10f280c747 Mon Sep 17 00:00:00 2001 From: fl-sean03 Date: Sat, 28 Feb 2026 01:42:26 +0000 Subject: [PATCH 3/5] fix: gofmt formatting for struct field alignment Signed-off-by: Sean F Signed-off-by: Sean Florez --- go/api/v1alpha1/agent_types.go | 4 ++-- go/api/v1alpha2/agent_types.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go/api/v1alpha1/agent_types.go b/go/api/v1alpha1/agent_types.go index fe6e4b74f..1780a290e 100644 --- a/go/api/v1alpha1/agent_types.go +++ b/go/api/v1alpha1/agent_types.go @@ -39,8 +39,8 @@ type AgentSpec struct { // Whether to stream the response from the model. // If not specified, the default value is true. // +optional - Stream *bool `json:"stream,omitempty"` - Tools []*Tool `json:"tools,omitempty"` + Stream *bool `json:"stream,omitempty"` + 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 / // +optional Memory []string `json:"memory,omitempty"` diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 985e9d68c..5f1bca432 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -130,8 +130,8 @@ type DeclarativeAgentSpec struct { // Whether to stream the response from the model. // If not specified, the default value is false. // +optional - Stream bool `json:"stream,omitempty"` - Tools []*Tool `json:"tools,omitempty"` + Stream bool `json:"stream,omitempty"` + Tools []*Tool `json:"tools,omitempty"` // A2AConfig instantiates an A2A server for this agent, // served on the HTTP port of the kagent kubernetes // controller (default 8083). From 7dd8cfabd57548af5b32b8089fc68be5ee707982 Mon Sep 17 00:00:00 2001 From: Sean Florez Date: Sat, 28 Feb 2026 02:28:42 +0000 Subject: [PATCH 4/5] fix: exclude test files from Docker build Test files using @testing-library/jest-dom (devDependency) cause build failures in the Docker production build where NODE_ENV=production skips devDependencies installation. Signed-off-by: opspawn Signed-off-by: Sean Florez --- ui/.dockerignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/.dockerignore b/ui/.dockerignore index fcb3ee2f5..880f7e06c 100644 --- a/ui/.dockerignore +++ b/ui/.dockerignore @@ -6,4 +6,9 @@ env/ htmlcov/ workspace/ node_modules/ -.next/ \ No newline at end of file +.next/ +**/__tests__/ +**/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx \ No newline at end of file From eadec69952be0a9c810ec9c9ea14ccee547cb71b Mon Sep 17 00:00:00 2001 From: fl-sean03 Date: Sat, 28 Feb 2026 04:00:44 +0000 Subject: [PATCH 5/5] ci: retrigger flaky e2e checks The test-e2e (postgres) check failed due to an infrastructure issue in the "Install Kagent" step, not related to our code changes. Pushing empty commit to re-trigger CI. Signed-off-by: fl-sean03