CustomToolsSettings.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { useState, useEffect, useCallback, useMemo } from "react"
  2. import { useEvent } from "react-use"
  3. import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
  4. import { RefreshCw, Loader2, FileCode } from "lucide-react"
  5. import type { SerializedCustomToolDefinition } from "@roo-code/types"
  6. import { useAppTranslation } from "@/i18n/TranslationContext"
  7. import { vscode } from "@/utils/vscode"
  8. import { Button } from "@/components/ui"
  9. interface ToolParameter {
  10. name: string
  11. type: string
  12. description?: string
  13. required: boolean
  14. }
  15. interface ProcessedTool {
  16. name: string
  17. description: string
  18. parameters: ToolParameter[]
  19. source?: string
  20. }
  21. interface CustomToolsSettingsProps {
  22. enabled: boolean
  23. onChange: (enabled: boolean) => void
  24. }
  25. export const CustomToolsSettings = ({ enabled, onChange }: CustomToolsSettingsProps) => {
  26. const { t } = useAppTranslation()
  27. const [tools, setTools] = useState<SerializedCustomToolDefinition[]>([])
  28. const [isRefreshing, setIsRefreshing] = useState(false)
  29. const [refreshError, setRefreshError] = useState<string | null>(null)
  30. useEffect(() => {
  31. if (enabled) {
  32. vscode.postMessage({ type: "refreshCustomTools" })
  33. } else {
  34. setTools([])
  35. }
  36. }, [enabled])
  37. useEvent("message", (event: MessageEvent) => {
  38. const message = event.data
  39. if (message.type === "customToolsResult") {
  40. setTools(message.tools || [])
  41. setIsRefreshing(false)
  42. setRefreshError(message.error ?? null)
  43. }
  44. })
  45. const onRefresh = useCallback(() => {
  46. setIsRefreshing(true)
  47. setRefreshError(null)
  48. vscode.postMessage({ type: "refreshCustomTools" })
  49. }, [])
  50. const processedTools = useMemo<ProcessedTool[]>(
  51. () =>
  52. tools.map((tool) => {
  53. const params = tool.parameters
  54. const properties = (params?.properties ?? {}) as Record<string, { type?: string; description?: string }>
  55. const required = (params?.required as string[] | undefined) ?? []
  56. return {
  57. name: tool.name,
  58. description: tool.description,
  59. source: tool.source,
  60. parameters: Object.entries(properties).map(([name, def]) => ({
  61. name,
  62. type: def.type ?? "any",
  63. description: def.description,
  64. required: required.includes(name),
  65. })),
  66. }
  67. }),
  68. [tools],
  69. )
  70. return (
  71. <div className="space-y-4">
  72. <div>
  73. <div className="flex items-center gap-2">
  74. <VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
  75. <span className="font-medium">{t("settings:experimental.CUSTOM_TOOLS.name")}</span>
  76. </VSCodeCheckbox>
  77. </div>
  78. <p className="text-vscode-descriptionForeground text-sm mt-0">
  79. {t("settings:experimental.CUSTOM_TOOLS.description")}
  80. </p>
  81. </div>
  82. {enabled && (
  83. <div className="ml-2 space-y-3">
  84. <div className="flex items-center justify-between gap-4">
  85. <label className="block font-medium">
  86. {t("settings:experimental.CUSTOM_TOOLS.toolsHeader")}
  87. </label>
  88. <Button variant="outline" onClick={onRefresh} disabled={isRefreshing}>
  89. <div className="flex items-center gap-2">
  90. {isRefreshing ? (
  91. <Loader2 className="w-4 h-4 animate-spin" />
  92. ) : (
  93. <RefreshCw className="w-4 h-4" />
  94. )}
  95. {isRefreshing
  96. ? t("settings:experimental.CUSTOM_TOOLS.refreshing")
  97. : t("settings:experimental.CUSTOM_TOOLS.refreshButton")}
  98. </div>
  99. </Button>
  100. </div>
  101. {refreshError && (
  102. <div className="p-2 bg-vscode-inputValidation-errorBackground text-vscode-errorForeground rounded text-sm border border-vscode-inputValidation-errorBorder">
  103. {t("settings:experimental.CUSTOM_TOOLS.refreshError")}: {refreshError}
  104. </div>
  105. )}
  106. {processedTools.length === 0 ? (
  107. <p className="text-vscode-descriptionForeground text-sm italic">
  108. {t("settings:experimental.CUSTOM_TOOLS.noTools")}
  109. </p>
  110. ) : (
  111. processedTools.map((tool) => (
  112. <div
  113. key={tool.name}
  114. className="bg-vscode-editor-background border border-vscode-panel-border rounded space-y-3 p-3">
  115. <div className="space-y-1">
  116. <div className="font-medium text-vscode-foreground">{tool.name}</div>
  117. {tool.source && (
  118. <div className="flex items-center text-xs text-vscode-descriptionForeground">
  119. <FileCode className="size-3 flex-shrink-0" />
  120. <span className="font-mono truncate" title={tool.source}>
  121. {tool.source}
  122. </span>
  123. </div>
  124. )}
  125. </div>
  126. <div className="text-vscode-descriptionForeground text-sm">{tool.description}</div>
  127. {tool.parameters.length > 0 && (
  128. <div className="space-y-1">
  129. <div className="text-xs font-medium text-vscode-foreground">
  130. {t("settings:experimental.CUSTOM_TOOLS.toolParameters")}:
  131. </div>
  132. <div>
  133. {tool.parameters.map((param) => (
  134. <div
  135. key={param.name}
  136. className="flex items-start gap-2 text-xs pl-2 py-1 border-l-2 border-vscode-panel-border">
  137. <code className="text-vscode-textLink-foreground font-mono">
  138. {param.name}
  139. </code>
  140. <span className="text-vscode-descriptionForeground">
  141. ({param.type})
  142. </span>
  143. {param.required && (
  144. <span className="text-vscode-errorForeground text-[10px] uppercase">
  145. required
  146. </span>
  147. )}
  148. {param.description && (
  149. <span className="text-vscode-descriptionForeground">
  150. — {param.description}
  151. </span>
  152. )}
  153. </div>
  154. ))}
  155. </div>
  156. </div>
  157. )}
  158. </div>
  159. ))
  160. )}
  161. </div>
  162. )}
  163. </div>
  164. )
  165. }