Roo-Code/webview-ui/src/components/settings/CustomToolsSettings.tsx
2025-12-21 12:43:06 -08:00

183 lines
5.6 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from "react"
import { useEvent } from "react-use"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { RefreshCw, Loader2, FileCode } from "lucide-react"
import type { SerializedCustomToolDefinition } from "@roo-code/types"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@/utils/vscode"
import { Button } from "@/components/ui"
interface ToolParameter {
name: string
type: string
description?: string
required: boolean
}
interface ProcessedTool {
name: string
description: string
parameters: ToolParameter[]
source?: string
}
interface CustomToolsSettingsProps {
enabled: boolean
onChange: (enabled: boolean) => void
}
export const CustomToolsSettings = ({ enabled, onChange }: CustomToolsSettingsProps) => {
const { t } = useAppTranslation()
const [tools, setTools] = useState<SerializedCustomToolDefinition[]>([])
const [isRefreshing, setIsRefreshing] = useState(false)
const [refreshError, setRefreshError] = useState<string | null>(null)
useEffect(() => {
if (enabled) {
vscode.postMessage({ type: "refreshCustomTools" })
} else {
setTools([])
}
}, [enabled])
useEvent("message", (event: MessageEvent) => {
const message = event.data
if (message.type === "customToolsResult") {
setTools(message.tools || [])
setIsRefreshing(false)
setRefreshError(message.error ?? null)
}
})
const onRefresh = useCallback(() => {
setIsRefreshing(true)
setRefreshError(null)
vscode.postMessage({ type: "refreshCustomTools" })
}, [])
const processedTools = useMemo<ProcessedTool[]>(
() =>
tools.map((tool) => {
const params = tool.parameters
const properties = (params?.properties ?? {}) as Record<string, { type?: string; description?: string }>
const required = (params?.required as string[] | undefined) ?? []
return {
name: tool.name,
description: tool.description,
source: tool.source,
parameters: Object.entries(properties).map(([name, def]) => ({
name,
type: def.type ?? "any",
description: def.description,
required: required.includes(name),
})),
}
}),
[tools],
)
return (
<div className="space-y-4">
<div>
<div className="flex items-center gap-2">
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
<span className="font-medium">{t("settings:experimental.CUSTOM_TOOLS.name")}</span>
</VSCodeCheckbox>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:experimental.CUSTOM_TOOLS.description")}
</p>
</div>
{enabled && (
<div className="ml-2 space-y-3">
<div className="flex items-center justify-between gap-4">
<label className="block font-medium">
{t("settings:experimental.CUSTOM_TOOLS.toolsHeader")}
</label>
<Button variant="outline" onClick={onRefresh} disabled={isRefreshing}>
<div className="flex items-center gap-2">
{isRefreshing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4" />
)}
{isRefreshing
? t("settings:experimental.CUSTOM_TOOLS.refreshing")
: t("settings:experimental.CUSTOM_TOOLS.refreshButton")}
</div>
</Button>
</div>
{refreshError && (
<div className="p-2 bg-vscode-inputValidation-errorBackground text-vscode-errorForeground rounded text-sm border border-vscode-inputValidation-errorBorder">
{t("settings:experimental.CUSTOM_TOOLS.refreshError")}: {refreshError}
</div>
)}
{processedTools.length === 0 ? (
<p className="text-vscode-descriptionForeground text-sm italic">
{t("settings:experimental.CUSTOM_TOOLS.noTools")}
</p>
) : (
processedTools.map((tool) => (
<div
key={tool.name}
className="bg-vscode-editor-background border border-vscode-panel-border rounded space-y-3 p-3">
<div className="space-y-1">
<div className="font-medium text-vscode-foreground">{tool.name}</div>
{tool.source && (
<div className="flex items-center text-xs text-vscode-descriptionForeground">
<FileCode className="size-3 flex-shrink-0" />
<span className="font-mono truncate" title={tool.source}>
{tool.source}
</span>
</div>
)}
</div>
<div className="text-vscode-descriptionForeground text-sm">{tool.description}</div>
{tool.parameters.length > 0 && (
<div className="space-y-1">
<div className="text-xs font-medium text-vscode-foreground">
{t("settings:experimental.CUSTOM_TOOLS.toolParameters")}:
</div>
<div>
{tool.parameters.map((param) => (
<div
key={param.name}
className="flex items-start gap-2 text-xs pl-2 py-1 border-l-2 border-vscode-panel-border">
<code className="text-vscode-textLink-foreground font-mono">
{param.name}
</code>
<span className="text-vscode-descriptionForeground">
({param.type})
</span>
{param.required && (
<span className="text-vscode-errorForeground text-[10px] uppercase">
required
</span>
)}
{param.description && (
<span className="text-vscode-descriptionForeground">
{param.description}
</span>
)}
</div>
))}
</div>
</div>
)}
</div>
))
)}
</div>
)}
</div>
)
}