diff --git a/package-lock.json b/package-lock.json
index affdace..d19c6bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "codepilot",
- "version": "0.6.4",
+ "version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codepilot",
- "version": "0.6.4",
+ "version": "0.7.0",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.33",
"@hugeicons/core-free-icons": "^3.1.1",
diff --git a/package.json b/package.json
index a1b56cd..0d853ea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "codepilot",
- "version": "0.6.4",
+ "version": "0.7.0",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
diff --git a/src/app/api/app/updates/route.ts b/src/app/api/app/updates/route.ts
new file mode 100644
index 0000000..9294f49
--- /dev/null
+++ b/src/app/api/app/updates/route.ts
@@ -0,0 +1,53 @@
+import { NextResponse } from "next/server";
+
+const GITHUB_REPO = "op7418/CodePilot";
+
+function compareSemver(a: string, b: string): number {
+ const pa = a.replace(/^v/, "").split(".").map(Number);
+ const pb = b.replace(/^v/, "").split(".").map(Number);
+ for (let i = 0; i < 3; i++) {
+ const diff = (pa[i] || 0) - (pb[i] || 0);
+ if (diff !== 0) return diff;
+ }
+ return 0;
+}
+
+export async function GET() {
+ try {
+ const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
+
+ const res = await fetch(
+ `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
+ {
+ headers: { Accept: "application/vnd.github.v3+json" },
+ next: { revalidate: 300 },
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: "Failed to fetch release info" },
+ { status: 502 }
+ );
+ }
+
+ const release = await res.json();
+ const latestVersion = (release.tag_name || "").replace(/^v/, "");
+ const updateAvailable = compareSemver(latestVersion, currentVersion) > 0;
+
+ return NextResponse.json({
+ latestVersion,
+ currentVersion,
+ updateAvailable,
+ releaseName: release.name || `v${latestVersion}`,
+ releaseNotes: release.body || "",
+ publishedAt: release.published_at || "",
+ releaseUrl: release.html_url || "",
+ });
+ } catch {
+ return NextResponse.json(
+ { error: "Failed to check for updates" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/settings/app/route.ts b/src/app/api/settings/app/route.ts
index 47ccfdd..5d89ac0 100644
--- a/src/app/api/settings/app/route.ts
+++ b/src/app/api/settings/app/route.ts
@@ -9,6 +9,7 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
+ 'dangerously_skip_permissions',
];
export async function GET() {
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index bb60cac..e9b22f3 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -26,6 +26,7 @@ import {
Loading02Icon,
} from "@hugeicons/core-free-icons";
import { ProviderManager } from "@/components/settings/ProviderManager";
+import { useUpdate } from "@/hooks/useUpdate";
interface SettingsData {
[key: string]: unknown;
@@ -61,7 +62,59 @@ export default function SettingsPage() {
);
}
-// ApiConfigSection removed — replaced by ProviderManager component
+// --- 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}
+
+
+
+
+ {updateInfo && !checking && (
+
+ {updateInfo.updateAvailable ? (
+
+
+
+ Update available: v{updateInfo.latestVersion}
+
+
+
+ ) : (
+
You're on the latest version.
+ )}
+
+ )}
+
+ );
+}
// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
@@ -77,6 +130,11 @@ function SettingsPageInner() {
"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");
@@ -96,9 +154,24 @@ function SettingsPageInner() {
}
}, []);
+ // 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();
- }, [fetchSettings]);
+ fetchAppSettings();
+ }, [fetchSettings, fetchAppSettings]);
const hasChanges =
JSON.stringify(settings) !== JSON.stringify(originalSettings);
@@ -167,6 +240,38 @@ function SettingsPageInner() {
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 (
@@ -178,8 +283,33 @@ function SettingsPageInner() {
+
+ {/* 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 ? (
@@ -388,6 +518,42 @@ function SettingsPageInner() {
+
+ {/* 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/AppShell.tsx b/src/components/layout/AppShell.tsx
index 2571f98..fb0aae6 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -6,9 +6,13 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { NavRail } from "./NavRail";
import { ChatListPanel } from "./ChatListPanel";
import { RightPanel } from "./RightPanel";
+import { UpdateDialog } from "./UpdateDialog";
import { PanelContext, type PanelContent } from "@/hooks/usePanel";
+import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate";
const LG_BREAKPOINT = 1024;
+const CHECK_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours
+const DISMISSED_VERSION_KEY = "codepilot_dismissed_update_version";
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
@@ -56,6 +60,80 @@ export function AppShell({ children }: { children: React.ReactNode }) {
return () => mql.removeEventListener("change", handler);
}, [isChatRoute]);
+ // --- Skip-permissions indicator ---
+ const [skipPermissionsActive, setSkipPermissionsActive] = useState(false);
+
+ const fetchSkipPermissions = useCallback(async () => {
+ try {
+ const res = await fetch("/api/settings/app");
+ if (res.ok) {
+ const data = await res.json();
+ setSkipPermissionsActive(data.settings?.dangerously_skip_permissions === "true");
+ }
+ } catch {
+ // ignore
+ }
+ }, []);
+
+ // Poll periodically so the indicator stays in sync when settings change
+ useEffect(() => {
+ fetchSkipPermissions();
+ const id = setInterval(fetchSkipPermissions, 5000);
+ return () => clearInterval(id);
+ }, [fetchSkipPermissions]);
+
+ // --- Update check state ---
+ const [updateInfo, setUpdateInfo] = useState
(null);
+ const [checking, setChecking] = useState(false);
+ const [showDialog, setShowDialog] = useState(false);
+
+ const checkForUpdates = useCallback(async () => {
+ setChecking(true);
+ try {
+ const res = await fetch("/api/app/updates");
+ if (!res.ok) return;
+ const data: UpdateInfo = await res.json();
+ setUpdateInfo(data);
+
+ if (data.updateAvailable) {
+ const dismissed = localStorage.getItem(DISMISSED_VERSION_KEY);
+ if (dismissed !== data.latestVersion) {
+ setShowDialog(true);
+ }
+ }
+ } catch {
+ // silently ignore network errors
+ } finally {
+ setChecking(false);
+ }
+ }, []);
+
+ const dismissUpdate = useCallback(() => {
+ setShowDialog(false);
+ if (updateInfo?.latestVersion) {
+ localStorage.setItem(DISMISSED_VERSION_KEY, updateInfo.latestVersion);
+ }
+ }, [updateInfo]);
+
+ // Check on mount + every 8 hours
+ useEffect(() => {
+ checkForUpdates();
+ const id = setInterval(checkForUpdates, CHECK_INTERVAL);
+ return () => clearInterval(id);
+ }, [checkForUpdates]);
+
+ const updateContextValue = useMemo(
+ () => ({
+ updateInfo,
+ checking,
+ checkForUpdates,
+ dismissUpdate,
+ showDialog,
+ setShowDialog,
+ }),
+ [updateInfo, checking, checkForUpdates, dismissUpdate, showDialog]
+ );
+
const panelContextValue = useMemo(
() => ({
panelOpen,
@@ -77,25 +155,30 @@ export function AppShell({ children }: { children: React.ReactNode }) {
);
return (
-
-
-
-
setChatListOpen(!chatListOpen)}
- />
-
-
- {/* Electron draggable title bar region */}
-
+
+
+
+
setChatListOpen(!chatListOpen)}
+ hasUpdate={updateInfo?.updateAvailable ?? false}
+ skipPermissionsActive={skipPermissionsActive}
/>
- {children}
+
+
+ {/* Electron draggable title bar region */}
+
+
{children}
+
+ {isChatDetailRoute && }
- {isChatDetailRoute && }
-
-
-
+
+
+
+
);
}
diff --git a/src/components/layout/NavRail.tsx b/src/components/layout/NavRail.tsx
index b622ffe..4cf258c 100644
--- a/src/components/layout/NavRail.tsx
+++ b/src/components/layout/NavRail.tsx
@@ -25,6 +25,8 @@ import { cn } from "@/lib/utils";
interface NavRailProps {
chatListOpen: boolean;
onToggleChatList: () => void;
+ hasUpdate?: boolean;
+ skipPermissionsActive?: boolean;
}
const navItems = [
@@ -33,7 +35,7 @@ const navItems = [
{ href: "/settings", label: "Settings", icon: Settings02Icon },
] as const;
-export function NavRail({ chatListOpen, onToggleChatList }: NavRailProps) {
+export function NavRail({ chatListOpen, onToggleChatList, hasUpdate, skipPermissionsActive }: NavRailProps) {
const pathname = usePathname();
const router = useRouter();
const { theme, setTheme } = useTheme();
@@ -102,20 +104,25 @@ export function NavRail({ chatListOpen, onToggleChatList }: NavRailProps) {
{item.label}
) : (
-
+
)}
{item.label}
@@ -124,8 +131,21 @@ export function NavRail({ chatListOpen, onToggleChatList }: NavRailProps) {
})}
- {/* Bottom: theme toggle */}
+ {/* Bottom: skip-permissions indicator + theme toggle */}
+ {skipPermissionsActive && (
+
+
+
+
+
+
+
+
+
+ Auto-approve is ON
+
+ )}
{mounted && (
diff --git a/src/components/layout/UpdateDialog.tsx b/src/components/layout/UpdateDialog.tsx
new file mode 100644
index 0000000..c867209
--- /dev/null
+++ b/src/components/layout/UpdateDialog.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import ReactMarkdown from "react-markdown";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { useUpdate } from "@/hooks/useUpdate";
+
+export function UpdateDialog() {
+ const { updateInfo, showDialog, setShowDialog, dismissUpdate } = useUpdate();
+
+ if (!updateInfo?.updateAvailable) return null;
+
+ return (
+
+ );
+}
diff --git a/src/hooks/useUpdate.ts b/src/hooks/useUpdate.ts
new file mode 100644
index 0000000..65f4039
--- /dev/null
+++ b/src/hooks/useUpdate.ts
@@ -0,0 +1,32 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+export interface UpdateInfo {
+ updateAvailable: boolean;
+ latestVersion: string;
+ currentVersion: string;
+ releaseName: string;
+ releaseNotes: string;
+ releaseUrl: string;
+ publishedAt: string;
+}
+
+export interface UpdateContextValue {
+ updateInfo: UpdateInfo | null;
+ checking: boolean;
+ checkForUpdates: () => Promise;
+ dismissUpdate: () => void;
+ showDialog: boolean;
+ setShowDialog: (v: boolean) => void;
+}
+
+export const UpdateContext = createContext(null);
+
+export function useUpdate(): UpdateContextValue {
+ const ctx = useContext(UpdateContext);
+ if (!ctx) {
+ throw new Error("useUpdate must be used within an UpdateProvider");
+ }
+ return ctx;
+}
diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts
index 3cbd06a..ca8dcdb 100644
--- a/src/lib/claude-client.ts
+++ b/src/lib/claude-client.ts
@@ -245,14 +245,23 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream