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:
1 parent
e4bbe8578a
commit
74fb784f6d
10 files changed
+485
-39
No files matched your search
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.6.4",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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} → 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user