feat(web): add Brave Search API key to tools config UI (#627)

* feat(web): add Brave Search API key to tools config UI

Expose `web.brave.api_key` in the Web Search settings with password input,
strip masked values on save, and show the field only when Brave Search is
enabled. Add en/vi/zh strings for label, tooltip, placeholder, and managed key.

* refactor(web): extract isSecret to shared utility and fix mobile input font-size

DRY: move duplicated isSecret() from providers-section and tools-web-section
into lib/secret.ts. Add text-base md:text-sm to number inputs for iOS
Safari auto-zoom prevention.

---------

Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
Huy Doanandviettranx authored and GitHub committed 2026-04-02 12:07:21 +07:00
1 parent 6baef0fb9c
commit 040ac31156
6 files changed
+53 -5

No files matched your search

+4
View File
@@ -211,6 +211,10 @@
"tools.webSearch": "Web Search",
"tools.webFetch": "Web fetch and browser automation",
"tools.maxResults": "Max Results",
"tools.braveApiKey": "API key",
"tools.braveApiKeyTip": "Required to use Brave Search. Create a key at https://brave.com/search/api/",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "A key is already stored on the server and cannot be shown here.",
"tools.webFetchPolicy": "Web Fetch Policy",
"tools.webFetchPolicyTip": "Controls which URLs the agent can fetch. 'Allowlist' restricts to approved domains only.",
"tools.allowedDomains": "Allowed Domains",
+4
View File
@@ -211,6 +211,10 @@
"tools.webSearch": "Tìm kiếm web",
"tools.webFetch": "Tải web và tự động hóa trình duyệt",
"tools.maxResults": "Kết quả tối đa",
"tools.braveApiKey": "API Key",
"tools.braveApiKeyTip": "Cần thiết để dùng Brave Search. Tạo khóa tại https://brave.com/search/api/",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "Khóa đã được lưu trên máy chủ và không thể hiển thị tại đây.",
"tools.webFetchPolicy": "Chính sách tải web",
"tools.webFetchPolicyTip": "Kiểm soát URL nào agent có thể tải. 'Danh sách cho phép' giới hạn chỉ các domain được duyệt.",
"tools.allowedDomains": "Domain được phép",
+4
View File
@@ -211,6 +211,10 @@
"tools.webSearch": "网页搜索",
"tools.webFetch": "网页抓取和浏览器自动化",
"tools.maxResults": "最大结果数",
"tools.braveApiKey": "API 密钥",
"tools.braveApiKeyTip": "使用 Brave 搜索时需要。可在 https://brave.com/search/api/ 创建密钥",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "密钥已保存在服务器上,无法在此显示。",
"tools.webFetchPolicy": "网页抓取策略",
"tools.webFetchPolicyTip": "控制 Agent 可以抓取哪些 URL。\"允许列表\"仅限已批准的域名。",
"tools.allowedDomains": "允许的域名",
+4
View File
@@ -0,0 +1,4 @@
/** Returns true if the value is a masked secret (contains "***"). */
export function isSecret(val: unknown): boolean {
return typeof val === "string" && val.includes("***");
}
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { isSecret } from "@/lib/secret";
type ProviderEntry = {
api_key?: string;
@@ -28,10 +29,6 @@ const KNOWN_PROVIDERS = [
{ key: "ollama_cloud", label: "Ollama Cloud", envKey: "GOCLAW_OLLAMA_CLOUD_API_KEY" },
];
function isSecret(val: unknown): boolean {
return typeof val === "string" && val.includes("***");
}
interface Props {
data: ProvidersData | undefined;
onSave: (value: ProvidersData) => Promise<void>;
@@ -22,6 +22,7 @@ import {
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { InfoLabel } from "@/components/shared/info-label";
import { isSecret } from "@/lib/secret";
/* eslint-disable @typescript-eslint/no-explicit-any */
type ToolsData = Record<string, any>;
@@ -50,6 +51,18 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
setDirty(true);
};
const handleSave = () => {
const toSave: ToolsData = { ...draft };
const web = { ...(toSave.web ?? {}) };
const brave = { ...(web.brave ?? {}) };
if (isSecret(brave.api_key)) {
delete brave.api_key;
}
web.brave = brave;
toSave.web = web;
onSave(toSave);
};
if (!data) return null;
const web = draft.web ?? {};
@@ -79,6 +92,7 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
<Input
type="number"
className="text-base md:text-sm"
value={ddg.max_results ?? ""}
onChange={(e) => updateNested("web", { duckduckgo: { ...ddg, max_results: Number(e.target.value) } })}
placeholder="5"
@@ -98,12 +112,33 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
<Input
type="number"
className="text-base md:text-sm"
value={brave.max_results ?? ""}
onChange={(e) => updateNested("web", { brave: { ...brave, max_results: Number(e.target.value) } })}
placeholder="5"
min={1}
/>
</div>
{brave.enabled && (
<div className="grid gap-1.5">
<InfoLabel tip={t("tools.braveApiKeyTip")}>{t("tools.braveApiKey")}</InfoLabel>
<Input
type="password"
className="text-base md:text-sm"
value={brave.api_key ?? ""}
disabled={isSecret(brave.api_key)}
readOnly={isSecret(brave.api_key)}
onChange={(e) =>
updateNested("web", { brave: { ...brave, api_key: e.target.value } })
}
placeholder={t("tools.braveApiKeyPlaceholder")}
autoComplete="off"
/>
{isSecret(brave.api_key) && (
<p className="text-xs text-muted-foreground">{t("tools.braveApiKeyManaged")}</p>
)}
</div>
)}
</div>
</div>
@@ -181,7 +216,7 @@ export function ToolsWebSection({ data, onSave, saving }: Props) {
{dirty && (
<div className="flex justify-end pt-2">
<Button size="sm" onClick={() => onSave(draft)} disabled={saving} className="gap-1.5">
<Button size="sm" onClick={handleSave} disabled={saving} className="gap-1.5">
<Save className="h-3.5 w-3.5" /> {saving ? t("saving") : t("save")}
</Button>
</div>