feat: overhaul OpenClaw config panels with JSON5 round-trip write engine

- Add json-five crate for JSON5 serialization preserving comments and formatting
- Rewrite openclaw_config.rs with comment-preserving JSON5 read/write engine
- Add Tauri commands: get_openclaw_live_provider, write_openclaw_config_section
- Redesign EnvPanel as full JSON editor with structured error handling
- Add tools.profile selection (minimal/coding/messaging/full) to ToolsPanel
- Add legacy timeout migration support to AgentsDefaultsPanel
- Add OpenClawHealthBanner component for config validation warnings
- Add supporting hooks, mutations, utility functions, and unit tests
This commit is contained in:
Jason
2026-03-06 18:35:23 +08:00
parent b4fdd5fc0d
commit 7e6f803035
18 changed files with 1242 additions and 385 deletions
+90 -24
View File
@@ -1,12 +1,13 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, Save } from "lucide-react";
import { Plus, Trash2, Save, TriangleAlert } from "lucide-react";
import { toast } from "sonner";
import { useOpenClawTools, useSaveOpenClawTools } from "@/hooks/useOpenClaw";
import { extractErrorMessage } from "@/utils/errorUtils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Select,
SelectContent,
@@ -14,15 +15,20 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { OpenClawToolsConfig } from "@/types";
import type { OpenClawToolsConfig, OpenClawToolsProfile } from "@/types";
import {
getOpenClawToolsProfileSelectValue,
getOpenClawUnsupportedProfile,
OPENCLAW_TOOL_PROFILES,
OPENCLAW_UNSET_PROFILE,
OPENCLAW_UNSUPPORTED_PROFILE,
} from "./utils";
interface ListItem {
id: string;
value: string;
}
const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"];
const ToolsPanel: React.FC = () => {
const { t } = useTranslation();
const { data: toolsData, isLoading } = useOpenClawTools();
@@ -35,31 +41,59 @@ const ToolsPanel: React.FC = () => {
if (toolsData) {
setConfig(toolsData);
setAllowList(
(toolsData.allow ?? []).map((v) => ({
(toolsData.allow ?? []).map((value) => ({
id: crypto.randomUUID(),
value: v,
value,
})),
);
setDenyList(
(toolsData.deny ?? []).map((v) => ({
(toolsData.deny ?? []).map((value) => ({
id: crypto.randomUUID(),
value: v,
value,
})),
);
}
}, [toolsData]);
const unsupportedProfile = getOpenClawUnsupportedProfile(config.profile);
const profileLabels = useMemo<Record<OpenClawToolsProfile, string>>(
() => ({
minimal: t("openclaw.tools.profileMinimal", {
defaultValue: "Minimal",
}),
coding: t("openclaw.tools.profileCoding", {
defaultValue: "Coding",
}),
messaging: t("openclaw.tools.profileMessaging", {
defaultValue: "Messaging",
}),
full: t("openclaw.tools.profileFull", {
defaultValue: "Full",
}),
}),
[t],
);
const handleSave = async () => {
try {
const { profile, allow, deny, ...other } = config;
const { allow, deny, ...other } = config;
const newConfig: OpenClawToolsConfig = {
...other,
profile: config.profile,
allow: allowList.map((item) => item.value).filter((s) => s.trim()),
deny: denyList.map((item) => item.value).filter((s) => s.trim()),
};
await saveToolsMutation.mutateAsync(newConfig);
toast.success(t("openclaw.tools.saveSuccess"));
const outcome = await saveToolsMutation.mutateAsync(newConfig);
toast.success(t("openclaw.tools.saveSuccess"), {
description: outcome.backupPath
? t("openclaw.backupCreated", {
path: outcome.backupPath,
defaultValue: "Backup created: {{path}}",
})
: undefined,
});
} catch (error) {
const detail = extractErrorMessage(error);
toast.error(t("openclaw.tools.saveFailed"), {
@@ -101,29 +135,63 @@ const ToolsPanel: React.FC = () => {
{t("openclaw.tools.description")}
</p>
{/* Profile selector */}
{unsupportedProfile && (
<Alert className="mb-6 border-amber-500/30 bg-amber-500/5">
<TriangleAlert className="h-4 w-4" />
<AlertTitle>
{t("openclaw.tools.unsupportedProfileTitle", {
defaultValue: "Unsupported tools profile",
})}
</AlertTitle>
<AlertDescription>
{t("openclaw.tools.unsupportedProfileDescription", {
value: unsupportedProfile,
defaultValue:
"The current tools.profile value '{{value}}' is not in the supported OpenClaw list. It will be preserved until you choose a new value.",
})}
</AlertDescription>
</Alert>
)}
<div className="mb-6">
<Label className="mb-2 block">{t("openclaw.tools.profile")}</Label>
<Select
value={config.profile ?? "default"}
onValueChange={(val) =>
setConfig((prev) => ({ ...prev, profile: val }))
}
value={getOpenClawToolsProfileSelectValue(config.profile)}
onValueChange={(value) => {
if (value === OPENCLAW_UNSUPPORTED_PROFILE) return;
if (value === OPENCLAW_UNSET_PROFILE) {
setConfig((prev) => ({ ...prev, profile: undefined }));
return;
}
setConfig((prev) => ({ ...prev, profile: value }));
}}
>
<SelectTrigger className="w-[200px]">
<SelectTrigger className="w-[220px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROFILE_OPTIONS.map((opt) => (
<SelectItem key={opt} value={opt}>
{t(`openclaw.tools.profiles.${opt}`)}
<SelectItem value={OPENCLAW_UNSET_PROFILE}>
{t("openclaw.tools.profileUnset", {
defaultValue: "Not set",
})}
</SelectItem>
{unsupportedProfile && (
<SelectItem
value={OPENCLAW_UNSUPPORTED_PROFILE}
disabled={true}
>{`${unsupportedProfile} (${t("openclaw.tools.unsupportedProfileLabel", {
defaultValue: "unsupported",
})})`}</SelectItem>
)}
{OPENCLAW_TOOL_PROFILES.map((profile) => (
<SelectItem key={profile} value={profile}>
{profileLabels[profile]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Allow list */}
<div className="mb-6">
<Label className="mb-2 block">{t("openclaw.tools.allowList")}</Label>
<div className="space-y-2">
@@ -163,7 +231,6 @@ const ToolsPanel: React.FC = () => {
</div>
</div>
{/* Deny list */}
<div className="mb-6">
<Label className="mb-2 block">{t("openclaw.tools.denyList")}</Label>
<div className="space-y-2">
@@ -203,7 +270,6 @@ const ToolsPanel: React.FC = () => {
</div>
</div>
{/* Save button */}
<div className="flex justify-end">
<Button
size="sm"