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
75 changes: 54 additions & 21 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,40 @@ import {
hasValidMetaName,
hasValidMetaPrefix,
isReservedMetaKey,
} from "@/utils/metaUtils";
} from "../utils/metaUtils";

/**
* Extended Tool type that includes optional fields used by the inspector.
*/
interface ExtendedTool extends Tool, WithIcons {
_meta?: Record<string, unknown>;
execution?: {
taskSupport?: "forbidden" | "required" | "optional";
};
}

// Type guard to safely detect the optional _meta field without using `any`
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
typeof (tool as { _meta?: unknown })._meta !== "undefined";
const hasMeta = (
tool: Tool,
): tool is ExtendedTool & { _meta: Record<string, unknown> } =>
typeof (tool as ExtendedTool)._meta !== "undefined";

// Type guard to detect execution.taskSupport
const getTaskSupport = (
tool: Tool | null,
): "forbidden" | "required" | "optional" => {
if (!tool) return "forbidden";
const extendedTool = tool as ExtendedTool;
const taskSupport = extendedTool.execution?.taskSupport;
if (
taskSupport === "forbidden" ||
taskSupport === "required" ||
taskSupport === "optional"
) {
return taskSupport;
}
return "optional";
};

const ToolsTab = ({
tools,
Expand Down Expand Up @@ -131,7 +160,8 @@ const ToolsTab = ({
];
});
setParams(Object.fromEntries(params));
setRunAsTask(false);
const taskSupport = getTaskSupport(selectedTool);
setRunAsTask(taskSupport === "required");

// Reset validation errors when switching tools
setHasValidationErrors(false);
Expand Down Expand Up @@ -170,7 +200,7 @@ const ToolsTab = ({
renderItem={(tool) => (
<div className="flex items-start w-full gap-2">
<div className="flex-shrink-0 mt-1">
<IconDisplay icons={(tool as WithIcons).icons} size="sm" />
<IconDisplay icons={(tool as ExtendedTool).icons} size="sm" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="truncate">{tool.name}</span>
Expand All @@ -191,7 +221,7 @@ const ToolsTab = ({
<div className="flex items-center gap-2">
{selectedTool && (
<IconDisplay
icons={(selectedTool as WithIcons).icons}
icons={(selectedTool as ExtendedTool).icons}
size="md"
/>
)}
Expand Down Expand Up @@ -659,21 +689,24 @@ const ToolsTab = ({
</div>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="run-as-task"
checked={runAsTask}
onCheckedChange={(checked: boolean) =>
setRunAsTask(checked)
}
/>
<Label
htmlFor="run-as-task"
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
Run as task
</Label>
</div>
{getTaskSupport(selectedTool) !== "forbidden" && (
<div className="flex items-center space-x-2">
<Checkbox
id="run-as-task"
checked={runAsTask}
onCheckedChange={(checked: boolean) =>
setRunAsTask(checked)
}
disabled={getTaskSupport(selectedTool) === "required"}
/>
<Label
htmlFor="run-as-task"
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
Run as task
</Label>
</div>
)}
<Button
onClick={async () => {
// Validate JSON inputs before calling tool
Expand Down
55 changes: 55 additions & 0 deletions client/src/components/__tests__/ToolsTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import {
RESERVED_NAMESPACE_MESSAGE,
} from "../../utils/metaUtils";

interface ExtendedTool extends Tool {
_meta?: Record<string, unknown>;
execution?: {
taskSupport?: "forbidden" | "required" | "optional";
};
}

describe("ToolsTab", () => {
beforeEach(() => {
// Clear the output schema cache before each test
Expand Down Expand Up @@ -107,6 +114,54 @@ describe("ToolsTab", () => {
expect(newInput.value).toBe("");
});

it("should show/hide/disable run-as-task checkbox based on taskSupport", async () => {
const forbiddenTool: ExtendedTool = {
...mockTools[0],
name: "forbiddenTool",
execution: { taskSupport: "forbidden" },
};
const requiredTool: ExtendedTool = {
...mockTools[0],
name: "requiredTool",
execution: { taskSupport: "required" },
};
const optionalTool: ExtendedTool = {
...mockTools[0],
name: "optionalTool",
execution: { taskSupport: "optional" },
};

const { rerender } = renderToolsTab({
selectedTool: forbiddenTool,
});

expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();

rerender(
<Tabs defaultValue="tools">
<ToolsTab {...defaultProps} selectedTool={optionalTool} />
</Tabs>,
);
const optionalCheckbox = screen.getByLabelText(
/run as task/i,
) as HTMLInputElement;
expect(optionalCheckbox).toBeInTheDocument();
expect(optionalCheckbox.getAttribute("aria-checked")).toBe("false");
expect(optionalCheckbox).not.toBeDisabled();

rerender(
<Tabs defaultValue="tools">
<ToolsTab {...defaultProps} selectedTool={requiredTool} />
</Tabs>,
);
const requiredCheckbox = screen.getByLabelText(
/run as task/i,
) as HTMLInputElement;
expect(requiredCheckbox).toBeInTheDocument();
expect(requiredCheckbox.getAttribute("aria-checked")).toBe("true");
expect(requiredCheckbox).toBeDisabled();
});

it("should handle integer type inputs", async () => {
renderToolsTab({
selectedTool: mockTools[1], // Use the tool with integer type
Expand Down