From 74fb784f6d9d979493f4cc144fd6b94256e6f029 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Mon, 9 Feb 2026 23:14:42 +0800 Subject: [PATCH] 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 --- package-lock.json | 4 +- package.json | 2 +- src/app/api/app/updates/route.ts | 53 ++++++++ src/app/api/settings/app/route.ts | 1 + src/app/settings/page.tsx | 170 ++++++++++++++++++++++++- src/components/layout/AppShell.tsx | 119 ++++++++++++++--- src/components/layout/NavRail.tsx | 50 +++++--- src/components/layout/UpdateDialog.tsx | 82 ++++++++++++ src/hooks/useUpdate.ts | 32 +++++ src/lib/claude-client.ts | 11 +- 10 files changed, 485 insertions(+), 39 deletions(-) create mode 100644 src/app/api/app/updates/route.ts create mode 100644 src/components/layout/UpdateDialog.tsx create mode 100644 src/hooks/useUpdate.ts 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.href === "/settings" && hasUpdate && ( + )} - > - - - {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 ( + { + if (!open) dismissUpdate(); + }}> + + + New Version Available + + {updateInfo.releaseName} + {updateInfo.publishedAt && ( + + {new Date(updateInfo.publishedAt).toLocaleDateString()} + + )} + + + + {updateInfo.releaseNotes && ( +
+

{children}

, + h2: ({ children }) =>

{children}

, + h3: ({ children }) =>

{children}

, + p: ({ children }) =>

{children}

, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + a: ({ href, children }) => ( + + {children} + + ), + code: ({ children }) => ( + {children} + ), + }} + > + {updateInfo.releaseNotes} +
    +
    + )} + +

    + Current: v{updateInfo.currentVersion} → Latest: v{updateInfo.latestVersion} +

    + + + + + +
    +
    + ); +} 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