feat: redesign settings page with sidebar navigation

Extract settings into General / Providers / Claude CLI sections with
a left sidebar layout. Keep Skills and MCP on the Extensions page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-12 17:28:29 +08:00
1 parent a5cff792e3
commit d5b1d6eb9d
5 files changed
+613 -544

No files matched your search

+4 -543
View File
@@ -1,52 +1,9 @@
"use client";
import { Suspense, useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Suspense } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
FloppyDiskIcon,
ReloadIcon,
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
} from "@hugeicons/core-free-icons";
import { ProviderManager } from "@/components/settings/ProviderManager";
import { useUpdate } from "@/hooks/useUpdate";
interface SettingsData {
[key: string]: unknown;
}
// Structured known fields from ~/.claude/settings.json
const KNOWN_FIELDS = [
{
key: "permissions",
label: "Permissions",
description: "Configure permission settings for Claude CLI",
type: "object" as const,
},
{
key: "env",
label: "Environment Variables",
description: "Environment variables passed to Claude",
type: "object" as const,
},
] as const;
import { Loading02Icon } from "@hugeicons/core-free-icons";
import { SettingsLayout } from "@/components/settings/SettingsLayout";
export default function SettingsPage() {
return (
@@ -57,503 +14,7 @@ export default function SettingsPage() {
</div>
}
>
<SettingsPageInner />
<SettingsLayout />
</Suspense>
);
}
// --- About & Updates card ---
function UpdateCard() {
const { updateInfo, checking, checkForUpdates } = useUpdate();
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
return (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">CodePilot</h2>
<p className="text-xs text-muted-foreground">Version {currentVersion}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={checking}
className="gap-2"
>
{checking ? (
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
) : (
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
)}
{checking ? "Checking..." : "Check for Updates"}
</Button>
</div>
{updateInfo && !checking && (
<div className="mt-3">
{updateInfo.updateAvailable ? (
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-blue-500" />
<span className="text-sm">
Update available: <span className="font-medium">v{updateInfo.latestVersion}</span>
</span>
<Button
variant="link"
size="sm"
className="h-auto p-0 text-sm"
onClick={() => window.open(updateInfo.releaseUrl, "_blank")}
>
View Release
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">You&apos;re on the latest version.</p>
)}
</div>
)}
</div>
);
}
// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
const [originalSettings, setOriginalSettings] = useState<SettingsData>({});
const [jsonText, setJsonText] = useState("");
const [jsonError, setJsonError] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingSaveAction, setPendingSaveAction] = useState<
"form" | "json" | null
>(null);
// Skip-permissions toggle state
const [skipPermissions, setSkipPermissions] = useState(false);
const [showSkipPermWarning, setShowSkipPermWarning] = useState(false);
const [skipPermSaving, setSkipPermSaving] = useState(false);
const fetchSettings = useCallback(async () => {
try {
const res = await fetch("/api/settings");
if (res.ok) {
const data = await res.json();
const s = data.settings || {};
setSettings(s);
setOriginalSettings(s);
setJsonText(JSON.stringify(s, null, 2));
}
} catch {
setSettings({});
setOriginalSettings({});
setJsonText("{}");
} finally {
setLoading(false);
}
}, []);
// Fetch app-level settings (dangerously_skip_permissions)
const fetchAppSettings = useCallback(async () => {
try {
const res = await fetch("/api/settings/app");
if (res.ok) {
const data = await res.json();
const appSettings = data.settings || {};
setSkipPermissions(appSettings.dangerously_skip_permissions === "true");
}
} catch {
// ignore
}
}, []);
useEffect(() => {
fetchSettings();
fetchAppSettings();
}, [fetchSettings, fetchAppSettings]);
const hasChanges =
JSON.stringify(settings) !== JSON.stringify(originalSettings);
const handleSave = async (source: "form" | "json") => {
let dataToSave: SettingsData;
if (source === "json") {
try {
dataToSave = JSON.parse(jsonText);
setJsonError("");
} catch {
setJsonError("Invalid JSON format");
return;
}
} else {
dataToSave = settings;
}
setSaving(true);
try {
const res = await fetch("/api/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: dataToSave }),
});
if (res.ok) {
setSettings(dataToSave);
setOriginalSettings(dataToSave);
setJsonText(JSON.stringify(dataToSave, null, 2));
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
}
} catch {
// Handle error silently
} finally {
setSaving(false);
setShowConfirmDialog(false);
setPendingSaveAction(null);
}
};
const handleReset = () => {
setSettings(originalSettings);
setJsonText(JSON.stringify(originalSettings, null, 2));
setJsonError("");
};
const handleFormatJson = () => {
try {
const parsed = JSON.parse(jsonText);
setJsonText(JSON.stringify(parsed, null, 2));
setJsonError("");
} catch {
setJsonError("Cannot format: invalid JSON");
}
};
const confirmSave = (source: "form" | "json") => {
setPendingSaveAction(source);
setShowConfirmDialog(true);
};
const updateField = (key: string, value: unknown) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
// Handle skip-permissions toggle
const handleSkipPermToggle = (checked: boolean) => {
if (checked) {
// Show warning dialog before enabling
setShowSkipPermWarning(true);
} else {
// Disable immediately without warning
saveSkipPermissions(false);
}
};
const saveSkipPermissions = async (enabled: boolean) => {
setSkipPermSaving(true);
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: { dangerously_skip_permissions: enabled ? "true" : "" },
}),
});
if (res.ok) {
setSkipPermissions(enabled);
}
} catch {
// ignore
} finally {
setSkipPermSaving(false);
setShowSkipPermWarning(false);
}
};
return (
<div className="flex h-full flex-col">
<div className="border-b border-border/50 px-6 pt-4 pb-4">
<h1 className="text-xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage CodePilot and Claude CLI settings
</p>
</div>
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<UpdateCard />
<ProviderManager />
{/* Dangerous Settings */}
<div className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Auto-approve All Actions</h2>
<p className="text-xs text-muted-foreground">
Skip all permission checks and auto-approve every tool action.
This is dangerous and should only be used for trusted tasks.
</p>
</div>
<Switch
checked={skipPermissions}
onCheckedChange={handleSkipPermToggle}
disabled={skipPermSaving}
/>
</div>
{skipPermissions && (
<div className="mt-3 flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
All tool actions will be auto-approved without confirmation. Use with caution.
</div>
)}
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">
Loading settings...
</span>
</div>
) : (
<Tabs defaultValue="form">
<TabsList className="mb-4">
<TabsTrigger value="form" className="gap-2">
<HugeiconsIcon icon={SlidersHorizontalIcon} className="h-4 w-4" />
Visual Editor
</TabsTrigger>
<TabsTrigger value="json" className="gap-2">
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
JSON Editor
</TabsTrigger>
</TabsList>
<TabsContent value="form">
<div className="space-y-6">
{KNOWN_FIELDS.map((field) => (
<div
key={field.key}
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
>
<Label className="text-sm font-medium">
{field.label}
</Label>
<p className="mb-2 text-xs text-muted-foreground">
{field.description}
</p>
<Textarea
value={
typeof settings[field.key] === "object"
? JSON.stringify(settings[field.key], null, 2)
: String(settings[field.key] ?? "")
}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
updateField(field.key, parsed);
} catch {
updateField(field.key, e.target.value);
}
}}
className="font-mono text-sm"
rows={4}
/>
</div>
))}
{Object.entries(settings)
.filter(
([key]) => !KNOWN_FIELDS.some((f) => f.key === key)
)
.map(([key, value]) => (
<div
key={key}
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
>
<Label className="text-sm font-medium">{key}</Label>
{typeof value === "boolean" ? (
<div className="mt-2 flex items-center gap-2">
<Switch
checked={value}
onCheckedChange={(checked) =>
updateField(key, checked)
}
/>
<span className="text-sm text-muted-foreground">
{value ? "Enabled" : "Disabled"}
</span>
</div>
) : typeof value === "string" ? (
<Input
value={value}
onChange={(e) =>
updateField(key, e.target.value)
}
className="mt-2"
/>
) : (
<Textarea
value={JSON.stringify(value, null, 2)}
onChange={(e) => {
try {
updateField(key, JSON.parse(e.target.value));
} catch {
updateField(key, e.target.value);
}
}}
className="mt-2 font-mono text-sm"
rows={4}
/>
)}
</div>
))}
<div className="flex items-center gap-3">
<Button
onClick={() => confirmSave("form")}
disabled={!hasChanges || saving}
className="gap-2"
>
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Changes"}
</Button>
<Button
variant="outline"
onClick={handleReset}
disabled={!hasChanges}
className="gap-2"
>
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
Reset
</Button>
{saveSuccess && (
<span className="text-sm text-green-600 dark:text-green-400">
Settings saved successfully
</span>
)}
</div>
</div>
</TabsContent>
<TabsContent value="json">
<div className="space-y-4">
<Textarea
value={jsonText}
onChange={(e) => {
setJsonText(e.target.value);
setJsonError("");
}}
className="min-h-[400px] font-mono text-sm"
placeholder='{"key": "value"}'
/>
{jsonError && (
<p className="text-sm text-destructive">{jsonError}</p>
)}
<div className="flex items-center gap-3">
<Button
onClick={() => confirmSave("json")}
disabled={saving}
className="gap-2"
>
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save JSON"}
</Button>
<Button
variant="outline"
onClick={handleFormatJson}
className="gap-2"
>
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
Format
</Button>
<Button
variant="outline"
onClick={handleReset}
className="gap-2"
>
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
Reset
</Button>
{saveSuccess && (
<span className="text-sm text-green-600 dark:text-green-400">
Settings saved successfully
</span>
)}
</div>
</div>
</TabsContent>
</Tabs>
)}
</div>
</div>
{/* Confirmation dialog */}
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Save</AlertDialogTitle>
<AlertDialogDescription>
This will overwrite your current ~/.claude/settings.json file. Are
you sure you want to continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => pendingSaveAction && handleSave(pendingSaveAction)}
>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Skip-permissions warning dialog */}
<AlertDialog open={showSkipPermWarning} onOpenChange={setShowSkipPermWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Enable Auto-approve All Actions?</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>
This will bypass all permission checks. Claude will be able to
execute any tool action without asking for your confirmation,
including:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>Running arbitrary shell commands</li>
<li>Reading, writing, and deleting files</li>
<li>Making network requests</li>
</ul>
<p className="font-medium text-orange-600 dark:text-orange-400">
Only enable this if you fully trust the task at hand. This
setting applies to all new chat sessions.
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => saveSkipPermissions(true)}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
Enable Auto-approve
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
-1
View File
@@ -5,7 +5,6 @@ import { usePathname, useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import type { IconSvgElement } from "@hugeicons/react";
import {
Message02Icon,
PlusSignIcon,
@@ -0,0 +1,322 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { HugeiconsIcon } from "@hugeicons/react";
import {
FloppyDiskIcon,
ReloadIcon,
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
} from "@hugeicons/core-free-icons";
interface SettingsData {
[key: string]: unknown;
}
const KNOWN_FIELDS = [
{
key: "permissions",
label: "Permissions",
description: "Configure permission settings for Claude CLI",
type: "object" as const,
},
{
key: "env",
label: "Environment Variables",
description: "Environment variables passed to Claude",
type: "object" as const,
},
] as const;
export function CliSettingsSection() {
const [settings, setSettings] = useState<SettingsData>({});
const [originalSettings, setOriginalSettings] = useState<SettingsData>({});
const [jsonText, setJsonText] = useState("");
const [jsonError, setJsonError] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingSaveAction, setPendingSaveAction] = useState<"form" | "json" | null>(null);
const fetchSettings = useCallback(async () => {
try {
const res = await fetch("/api/settings");
if (res.ok) {
const data = await res.json();
const s = data.settings || {};
setSettings(s);
setOriginalSettings(s);
setJsonText(JSON.stringify(s, null, 2));
}
} catch {
setSettings({});
setOriginalSettings({});
setJsonText("{}");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
const hasChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings);
const handleSave = async (source: "form" | "json") => {
let dataToSave: SettingsData;
if (source === "json") {
try {
dataToSave = JSON.parse(jsonText);
setJsonError("");
} catch {
setJsonError("Invalid JSON format");
return;
}
} else {
dataToSave = settings;
}
setSaving(true);
try {
const res = await fetch("/api/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: dataToSave }),
});
if (res.ok) {
setSettings(dataToSave);
setOriginalSettings(dataToSave);
setJsonText(JSON.stringify(dataToSave, null, 2));
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
}
} catch {
// Handle error silently
} finally {
setSaving(false);
setShowConfirmDialog(false);
setPendingSaveAction(null);
}
};
const handleReset = () => {
setSettings(originalSettings);
setJsonText(JSON.stringify(originalSettings, null, 2));
setJsonError("");
};
const handleFormatJson = () => {
try {
const parsed = JSON.parse(jsonText);
setJsonText(JSON.stringify(parsed, null, 2));
setJsonError("");
} catch {
setJsonError("Cannot format: invalid JSON");
}
};
const confirmSave = (source: "form" | "json") => {
setPendingSaveAction(source);
setShowConfirmDialog(true);
};
const updateField = (key: string, value: unknown) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Loading settings...</span>
</div>
);
}
return (
<div className="max-w-3xl">
<Tabs defaultValue="form">
<TabsList className="mb-4">
<TabsTrigger value="form" className="gap-2">
<HugeiconsIcon icon={SlidersHorizontalIcon} className="h-4 w-4" />
Visual Editor
</TabsTrigger>
<TabsTrigger value="json" className="gap-2">
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
JSON Editor
</TabsTrigger>
</TabsList>
<TabsContent value="form">
<div className="space-y-6">
{KNOWN_FIELDS.map((field) => (
<div
key={field.key}
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
>
<Label className="text-sm font-medium">{field.label}</Label>
<p className="mb-2 text-xs text-muted-foreground">{field.description}</p>
<Textarea
value={
typeof settings[field.key] === "object"
? JSON.stringify(settings[field.key], null, 2)
: String(settings[field.key] ?? "")
}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
updateField(field.key, parsed);
} catch {
updateField(field.key, e.target.value);
}
}}
className="font-mono text-sm"
rows={4}
/>
</div>
))}
{Object.entries(settings)
.filter(([key]) => !KNOWN_FIELDS.some((f) => f.key === key))
.map(([key, value]) => (
<div
key={key}
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
>
<Label className="text-sm font-medium">{key}</Label>
{typeof value === "boolean" ? (
<div className="mt-2 flex items-center gap-2">
<Switch
checked={value}
onCheckedChange={(checked) => updateField(key, checked)}
/>
<span className="text-sm text-muted-foreground">
{value ? "Enabled" : "Disabled"}
</span>
</div>
) : typeof value === "string" ? (
<Input
value={value}
onChange={(e) => updateField(key, e.target.value)}
className="mt-2"
/>
) : (
<Textarea
value={JSON.stringify(value, null, 2)}
onChange={(e) => {
try {
updateField(key, JSON.parse(e.target.value));
} catch {
updateField(key, e.target.value);
}
}}
className="mt-2 font-mono text-sm"
rows={4}
/>
)}
</div>
))}
<div className="flex items-center gap-3">
<Button onClick={() => confirmSave("form")} disabled={!hasChanges || saving} className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Changes"}
</Button>
<Button variant="outline" onClick={handleReset} disabled={!hasChanges} className="gap-2">
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
Reset
</Button>
{saveSuccess && (
<span className="text-sm text-green-600 dark:text-green-400">
Settings saved successfully
</span>
)}
</div>
</div>
</TabsContent>
<TabsContent value="json">
<div className="space-y-4">
<Textarea
value={jsonText}
onChange={(e) => {
setJsonText(e.target.value);
setJsonError("");
}}
className="min-h-[400px] font-mono text-sm"
placeholder='{"key": "value"}'
/>
{jsonError && <p className="text-sm text-destructive">{jsonError}</p>}
<div className="flex items-center gap-3">
<Button onClick={() => confirmSave("json")} disabled={saving} className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save JSON"}
</Button>
<Button variant="outline" onClick={handleFormatJson} className="gap-2">
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
Format
</Button>
<Button variant="outline" onClick={handleReset} className="gap-2">
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
Reset
</Button>
{saveSuccess && (
<span className="text-sm text-green-600 dark:text-green-400">
Settings saved successfully
</span>
)}
</div>
</div>
</TabsContent>
</Tabs>
{/* Confirmation dialog */}
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Save</AlertDialogTitle>
<AlertDialogDescription>
This will overwrite your current ~/.claude/settings.json file. Are you sure you want to continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => pendingSaveAction && handleSave(pendingSaveAction)}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
+189
View File
@@ -0,0 +1,189 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { HugeiconsIcon } from "@hugeicons/react";
import { ReloadIcon, Loading02Icon } from "@hugeicons/core-free-icons";
import { useUpdate } from "@/hooks/useUpdate";
function UpdateCard() {
const { updateInfo, checking, checkForUpdates } = useUpdate();
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
return (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">CodePilot</h2>
<p className="text-xs text-muted-foreground">Version {currentVersion}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={checking}
className="gap-2"
>
{checking ? (
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
) : (
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
)}
{checking ? "Checking..." : "Check for Updates"}
</Button>
</div>
{updateInfo && !checking && (
<div className="mt-3">
{updateInfo.updateAvailable ? (
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-blue-500" />
<span className="text-sm">
Update available: <span className="font-medium">v{updateInfo.latestVersion}</span>
</span>
<Button
variant="link"
size="sm"
className="h-auto p-0 text-sm"
onClick={() => window.open(updateInfo.releaseUrl, "_blank")}
>
View Release
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">You&apos;re on the latest version.</p>
)}
</div>
)}
</div>
);
}
export function GeneralSection() {
const [skipPermissions, setSkipPermissions] = useState(false);
const [showSkipPermWarning, setShowSkipPermWarning] = useState(false);
const [skipPermSaving, setSkipPermSaving] = useState(false);
const fetchAppSettings = useCallback(async () => {
try {
const res = await fetch("/api/settings/app");
if (res.ok) {
const data = await res.json();
const appSettings = data.settings || {};
setSkipPermissions(appSettings.dangerously_skip_permissions === "true");
}
} catch {
// ignore
}
}, []);
useEffect(() => {
fetchAppSettings();
}, [fetchAppSettings]);
const handleSkipPermToggle = (checked: boolean) => {
if (checked) {
setShowSkipPermWarning(true);
} else {
saveSkipPermissions(false);
}
};
const saveSkipPermissions = async (enabled: boolean) => {
setSkipPermSaving(true);
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: { dangerously_skip_permissions: enabled ? "true" : "" },
}),
});
if (res.ok) {
setSkipPermissions(enabled);
}
} catch {
// ignore
} finally {
setSkipPermSaving(false);
setShowSkipPermWarning(false);
}
};
return (
<div className="max-w-3xl space-y-6">
<UpdateCard />
{/* Auto-approve toggle */}
<div className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Auto-approve All Actions</h2>
<p className="text-xs text-muted-foreground">
Skip all permission checks and auto-approve every tool action.
This is dangerous and should only be used for trusted tasks.
</p>
</div>
<Switch
checked={skipPermissions}
onCheckedChange={handleSkipPermToggle}
disabled={skipPermSaving}
/>
</div>
{skipPermissions && (
<div className="mt-3 flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
All tool actions will be auto-approved without confirmation. Use with caution.
</div>
)}
</div>
{/* Skip-permissions warning dialog */}
<AlertDialog open={showSkipPermWarning} onOpenChange={setShowSkipPermWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Enable Auto-approve All Actions?</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>
This will bypass all permission checks. Claude will be able to
execute any tool action without asking for your confirmation,
including:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>Running arbitrary shell commands</li>
<li>Reading, writing, and deleting files</li>
<li>Making network requests</li>
</ul>
<p className="font-medium text-orange-600 dark:text-orange-400">
Only enable this if you fully trust the task at hand. This
setting applies to all new chat sessions.
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => saveSkipPermissions(true)}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
Enable Auto-approve
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -0,0 +1,98 @@
"use client";
import { useState, useEffect } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import type { IconSvgElement } from "@hugeicons/react";
import {
Settings02Icon,
CodeIcon,
} from "@hugeicons/core-free-icons";
import { Plug01Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/lib/utils";
import { GeneralSection } from "./GeneralSection";
import { ProviderManager } from "./ProviderManager";
import { CliSettingsSection } from "./CliSettingsSection";
type Section = "general" | "providers" | "cli";
interface SidebarItem {
id: Section;
label: string;
icon: IconSvgElement;
}
const sidebarItems: SidebarItem[] = [
{ id: "general", label: "General", icon: Settings02Icon },
{ id: "providers", label: "Providers", icon: Plug01Icon },
{ id: "cli", label: "Claude CLI", icon: CodeIcon },
];
function getInitialSection(): Section {
if (typeof window !== "undefined") {
const hash = window.location.hash.replace("#", "");
if (sidebarItems.some((item) => item.id === hash)) {
return hash as Section;
}
}
return "general";
}
export function SettingsLayout() {
const [activeSection, setActiveSection] = useState<Section>(getInitialSection);
// Sync hash on mount and on popstate
useEffect(() => {
const onHashChange = () => {
const hash = window.location.hash.replace("#", "");
if (sidebarItems.some((item) => item.id === hash)) {
setActiveSection(hash as Section);
}
};
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
const handleSectionChange = (section: Section) => {
setActiveSection(section);
window.history.replaceState(null, "", `/settings#${section}`);
};
return (
<div className="flex h-full flex-col">
<div className="border-b border-border/50 px-6 pt-4 pb-4">
<h1 className="text-xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage CodePilot and Claude CLI settings
</p>
</div>
<div className="flex min-h-0 flex-1">
{/* Sidebar */}
<nav className="flex w-52 shrink-0 flex-col gap-1 border-r border-border/50 p-3">
{sidebarItems.map((item) => (
<button
key={item.id}
onClick={() => handleSectionChange(item.id)}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors text-left",
activeSection === item.id
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)}
>
<HugeiconsIcon icon={item.icon} className="h-4 w-4 shrink-0" />
{item.label}
</button>
))}
</nav>
{/* Content */}
<div className="flex-1 overflow-auto p-6">
{activeSection === "general" && <GeneralSection />}
{activeSection === "providers" && <ProviderManager />}
{activeSection === "cli" && <CliSettingsSection />}
</div>
</div>
</div>
);
}