feat: auto update check and dangerously-skip-permissions mode

Add automatic update detection (checks GitHub Releases every 8h with
dialog prompt and blue dot on Settings icon) and a skip-permissions
toggle in Settings that uses the SDK's native bypassPermissions mode
to auto-approve all tool actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-09 23:14:42 +08:00
1 parent e4bbe8578a
commit 74fb784f6d
10 files changed
+485 -39

No files matched your search

+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codepilot",
"version": "0.6.4",
"version": "0.7.0",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
+53
View File
@@ -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 }
);
}
}
+1
View File
@@ -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() {
+168 -2
View File
@@ -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 (
<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() {
@@ -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 (
<div className="flex h-full flex-col">
<div className="border-b border-border/50 px-6 pt-4 pb-4">
@@ -178,8 +283,33 @@ function SettingsPageInner() {
<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" />
@@ -388,6 +518,42 @@ function SettingsPageInner() {
</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>
);
}
+101 -18
View File
@@ -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<UpdateInfo | null>(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 (
<PanelContext.Provider value={panelContextValue}>
<TooltipProvider delayDuration={300}>
<div className="flex h-screen overflow-hidden">
<NavRail
chatListOpen={chatListOpen}
onToggleChatList={() => setChatListOpen(!chatListOpen)}
/>
<ChatListPanel open={chatListOpen} />
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Electron draggable title bar region */}
<div
className="h-11 w-full shrink-0"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
<UpdateContext.Provider value={updateContextValue}>
<PanelContext.Provider value={panelContextValue}>
<TooltipProvider delayDuration={300}>
<div className="flex h-screen overflow-hidden">
<NavRail
chatListOpen={chatListOpen}
onToggleChatList={() => setChatListOpen(!chatListOpen)}
hasUpdate={updateInfo?.updateAvailable ?? false}
skipPermissionsActive={skipPermissionsActive}
/>
<main className="relative flex-1 overflow-hidden">{children}</main>
<ChatListPanel open={chatListOpen} />
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Electron draggable title bar region */}
<div
className="h-11 w-full shrink-0"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
/>
<main className="relative flex-1 overflow-hidden">{children}</main>
</div>
{isChatDetailRoute && <RightPanel />}
</div>
{isChatDetailRoute && <RightPanel />}
</div>
</TooltipProvider>
</PanelContext.Provider>
<UpdateDialog />
</TooltipProvider>
</PanelContext.Provider>
</UpdateContext.Provider>
);
}
+35 -15
View File
@@ -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) {
<span className="sr-only">{item.label}</span>
</Button>
) : (
<Button
asChild
variant="ghost"
size="icon"
className={cn(
"h-9 w-9",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
<div className="relative">
<Button
asChild
variant="ghost"
size="icon"
className={cn(
"h-9 w-9",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
>
<Link href={item.href}>
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
<span className="sr-only">{item.label}</span>
</Link>
</Button>
{item.href === "/settings" && hasUpdate && (
<span className="absolute top-0.5 right-0.5 h-2 w-2 rounded-full bg-blue-500" />
)}
>
<Link href={item.href}>
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
<span className="sr-only">{item.label}</span>
</Link>
</Button>
</div>
)}
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
@@ -124,8 +131,21 @@ export function NavRail({ chatListOpen, onToggleChatList }: NavRailProps) {
})}
</nav>
{/* Bottom: theme toggle */}
{/* Bottom: skip-permissions indicator + theme toggle */}
<div className="mt-auto flex flex-col items-center gap-2">
{skipPermissionsActive && (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex h-8 w-8 items-center justify-center">
<span className="relative flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-orange-400 opacity-75" />
<span className="relative inline-flex h-3 w-3 rounded-full bg-orange-500" />
</span>
</div>
</TooltipTrigger>
<TooltipContent side="right">Auto-approve is ON</TooltipContent>
</Tooltip>
)}
{mounted && (
<Tooltip>
<TooltipTrigger asChild>
+82
View File
@@ -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 (
<Dialog open={showDialog} onOpenChange={(open) => {
if (!open) dismissUpdate();
}}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>New Version Available</DialogTitle>
<DialogDescription>
{updateInfo.releaseName}
{updateInfo.publishedAt && (
<span className="ml-2 text-xs text-muted-foreground">
{new Date(updateInfo.publishedAt).toLocaleDateString()}
</span>
)}
</DialogDescription>
</DialogHeader>
{updateInfo.releaseNotes && (
<div className="max-h-60 overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-sm">
<ReactMarkdown
components={{
h1: ({ children }) => <h3 className="mb-1 text-sm font-semibold">{children}</h3>,
h2: ({ children }) => <h3 className="mb-1 text-sm font-semibold">{children}</h3>,
h3: ({ children }) => <h4 className="mb-1 text-sm font-medium">{children}</h4>,
p: ({ children }) => <p className="mb-2 text-sm leading-relaxed">{children}</p>,
ul: ({ children }) => <ul className="mb-2 list-disc pl-4 text-sm">{children}</ul>,
ol: ({ children }) => <ol className="mb-2 list-decimal pl-4 text-sm">{children}</ol>,
li: ({ children }) => <li className="mb-0.5">{children}</li>,
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">
{children}
</a>
),
code: ({ children }) => (
<code className="rounded bg-muted px-1 py-0.5 text-xs">{children}</code>
),
}}
>
{updateInfo.releaseNotes}
</ReactMarkdown>
</div>
)}
<p className="text-xs text-muted-foreground">
Current: v{updateInfo.currentVersion} &rarr; Latest: v{updateInfo.latestVersion}
</p>
<DialogFooter>
<Button variant="outline" onClick={dismissUpdate}>
Later
</Button>
<Button
onClick={() => {
window.open(updateInfo.releaseUrl, "_blank");
}}
>
View Release
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+32
View File
@@ -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<void>;
dismissUpdate: () => void;
showDialog: boolean;
setShowDialog: (v: boolean) => void;
}
export const UpdateContext = createContext<UpdateContextValue | null>(null);
export function useUpdate(): UpdateContextValue {
const ctx = useContext(UpdateContext);
if (!ctx) {
throw new Error("useUpdate must be used within an UpdateProvider");
}
return ctx;
}
+10 -1
View File
@@ -245,14 +245,23 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
}
}
// Check if dangerously_skip_permissions is enabled in app settings
const skipPermissions = getSetting('dangerously_skip_permissions') === 'true';
const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
includePartialMessages: true,
permissionMode: (permissionMode as Options['permissionMode']) || 'acceptEdits',
permissionMode: skipPermissions
? 'bypassPermissions'
: ((permissionMode as Options['permissionMode']) || 'acceptEdits'),
env: sdkEnv,
};
if (skipPermissions) {
queryOptions.allowDangerouslySkipPermissions = true;
}
// Find claude binary for packaged app where PATH is limited
const claudePath = findClaudePath();
if (claudePath) {