diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index e9b22f3..9b942b4 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -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() {
}
>
-
+
);
}
-
-// --- About & Updates card ---
-function UpdateCard() {
- const { updateInfo, checking, checkForUpdates } = useUpdate();
- const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
-
- return (
-
-
-
-
CodePilot
-
Version {currentVersion}
-
-
- {checking ? (
-
- ) : (
-
- )}
- {checking ? "Checking..." : "Check for Updates"}
-
-
-
- {updateInfo && !checking && (
-
- {updateInfo.updateAvailable ? (
-
-
-
- Update available: v{updateInfo.latestVersion}
-
- window.open(updateInfo.releaseUrl, "_blank")}
- >
- View Release
-
-
- ) : (
-
You're on the latest version.
- )}
-
- )}
-
- );
-}
-
-// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
-function SettingsPageInner() {
- const [settings, setSettings] = useState({});
- const [originalSettings, setOriginalSettings] = useState({});
- 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 (
-
-
-
Settings
-
- Manage CodePilot and Claude CLI settings
-
-
-
-
-
-
-
-
- {/* Dangerous Settings */}
-
-
-
-
Auto-approve All Actions
-
- Skip all permission checks and auto-approve every tool action.
- This is dangerous and should only be used for trusted tasks.
-
-
-
-
- {skipPermissions && (
-
-
- All tool actions will be auto-approved without confirmation. Use with caution.
-
- )}
-
-
- {loading ? (
-
-
-
- Loading settings...
-
-
- ) : (
-
-
-
-
- Visual Editor
-
-
-
- JSON Editor
-
-
-
-
-
- {KNOWN_FIELDS.map((field) => (
-
-
- {field.label}
-
-
- {field.description}
-
-
- ))}
-
- {Object.entries(settings)
- .filter(
- ([key]) => !KNOWN_FIELDS.some((f) => f.key === key)
- )
- .map(([key, value]) => (
-
-
{key}
- {typeof value === "boolean" ? (
-
-
- updateField(key, checked)
- }
- />
-
- {value ? "Enabled" : "Disabled"}
-
-
- ) : typeof value === "string" ? (
-
- updateField(key, e.target.value)
- }
- className="mt-2"
- />
- ) : (
-
- ))}
-
-
- confirmSave("form")}
- disabled={!hasChanges || saving}
- className="gap-2"
- >
- {saving ? (
-
- ) : (
-
- )}
- {saving ? "Saving..." : "Save Changes"}
-
-
-
- Reset
-
- {saveSuccess && (
-
- Settings saved successfully
-
- )}
-
-
-
-
-
-
-
-
- )}
-
-
-
- {/* Confirmation dialog */}
-
-
-
- Confirm Save
-
- This will overwrite your current ~/.claude/settings.json file. Are
- you sure you want to continue?
-
-
-
- Cancel
- pendingSaveAction && handleSave(pendingSaveAction)}
- >
- Save
-
-
-
-
-
- {/* Skip-permissions warning dialog */}
-
-
-
- Enable Auto-approve All Actions?
-
-
-
- This will bypass all permission checks. Claude will be able to
- execute any tool action without asking for your confirmation,
- including:
-
-
- Running arbitrary shell commands
- Reading, writing, and deleting files
- Making network requests
-
-
- Only enable this if you fully trust the task at hand. This
- setting applies to all new chat sessions.
-
-
-
-
-
- Cancel
- saveSkipPermissions(true)}
- className="bg-orange-600 hover:bg-orange-700 text-white"
- >
- Enable Auto-approve
-
-
-
-
-
- );
-}
diff --git a/src/components/layout/NavRail.tsx b/src/components/layout/NavRail.tsx
index 4cf258c..aa5cc04 100644
--- a/src/components/layout/NavRail.tsx
+++ b/src/components/layout/NavRail.tsx
@@ -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,
diff --git a/src/components/settings/CliSettingsSection.tsx b/src/components/settings/CliSettingsSection.tsx
new file mode 100644
index 0000000..9e170d3
--- /dev/null
+++ b/src/components/settings/CliSettingsSection.tsx
@@ -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({});
+ const [originalSettings, setOriginalSettings] = useState({});
+ 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 (
+
+
+ Loading settings...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Visual Editor
+
+
+
+ JSON Editor
+
+
+
+
+
+ {KNOWN_FIELDS.map((field) => (
+
+
{field.label}
+
{field.description}
+
+ ))}
+
+ {Object.entries(settings)
+ .filter(([key]) => !KNOWN_FIELDS.some((f) => f.key === key))
+ .map(([key, value]) => (
+
+
{key}
+ {typeof value === "boolean" ? (
+
+ updateField(key, checked)}
+ />
+
+ {value ? "Enabled" : "Disabled"}
+
+
+ ) : typeof value === "string" ? (
+
updateField(key, e.target.value)}
+ className="mt-2"
+ />
+ ) : (
+
+ ))}
+
+
+ confirmSave("form")} disabled={!hasChanges || saving} className="gap-2">
+ {saving ? (
+
+ ) : (
+
+ )}
+ {saving ? "Saving..." : "Save Changes"}
+
+
+
+ Reset
+
+ {saveSuccess && (
+
+ Settings saved successfully
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Confirmation dialog */}
+
+
+
+ Confirm Save
+
+ This will overwrite your current ~/.claude/settings.json file. Are you sure you want to continue?
+
+
+
+ Cancel
+ pendingSaveAction && handleSave(pendingSaveAction)}>
+ Save
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx
new file mode 100644
index 0000000..68c0b0c
--- /dev/null
+++ b/src/components/settings/GeneralSection.tsx
@@ -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 (
+
+
+
+
CodePilot
+
Version {currentVersion}
+
+
+ {checking ? (
+
+ ) : (
+
+ )}
+ {checking ? "Checking..." : "Check for Updates"}
+
+
+
+ {updateInfo && !checking && (
+
+ {updateInfo.updateAvailable ? (
+
+
+
+ Update available: v{updateInfo.latestVersion}
+
+ window.open(updateInfo.releaseUrl, "_blank")}
+ >
+ View Release
+
+
+ ) : (
+
You're on the latest version.
+ )}
+
+ )}
+
+ );
+}
+
+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 (
+
+
+
+ {/* Auto-approve toggle */}
+
+
+
+
Auto-approve All Actions
+
+ Skip all permission checks and auto-approve every tool action.
+ This is dangerous and should only be used for trusted tasks.
+
+
+
+
+ {skipPermissions && (
+
+
+ All tool actions will be auto-approved without confirmation. Use with caution.
+
+ )}
+
+
+ {/* Skip-permissions warning dialog */}
+
+
+
+ Enable Auto-approve All Actions?
+
+
+
+ This will bypass all permission checks. Claude will be able to
+ execute any tool action without asking for your confirmation,
+ including:
+
+
+ Running arbitrary shell commands
+ Reading, writing, and deleting files
+ Making network requests
+
+
+ Only enable this if you fully trust the task at hand. This
+ setting applies to all new chat sessions.
+
+
+
+
+
+ Cancel
+ saveSkipPermissions(true)}
+ className="bg-orange-600 hover:bg-orange-700 text-white"
+ >
+ Enable Auto-approve
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/SettingsLayout.tsx b/src/components/settings/SettingsLayout.tsx
new file mode 100644
index 0000000..21ed38b
--- /dev/null
+++ b/src/components/settings/SettingsLayout.tsx
@@ -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(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 (
+
+
+
Settings
+
+ Manage CodePilot and Claude CLI settings
+
+
+
+
+ {/* Sidebar */}
+
+ {sidebarItems.map((item) => (
+ 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"
+ )}
+ >
+
+ {item.label}
+
+ ))}
+
+
+ {/* Content */}
+
+ {activeSection === "general" &&
}
+ {activeSection === "providers" &&
}
+ {activeSection === "cli" &&
}
+
+
+
+ );
+}