feat: move Check for Updates to Settings, add sidebar links, customize title
- Move ClawPal self-update from Home page and sidebar to Settings page as a "Current Version" card showing version + check/update button - Set window title to "ClawPal by zhixian" - Add Website · @zhixian links at bottom of sidebar Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "ClawPal",
|
"title": "ClawPal by zhixian",
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 820,
|
"height": 820,
|
||||||
"minWidth": 1024,
|
"minWidth": 1024,
|
||||||
|
|||||||
51
src/App.tsx
51
src/App.tsx
@@ -1,6 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { check } from "@tauri-apps/plugin-updater";
|
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
|
||||||
import { Home } from "./pages/Home";
|
import { Home } from "./pages/Home";
|
||||||
import { Recipes } from "./pages/Recipes";
|
import { Recipes } from "./pages/Recipes";
|
||||||
import { Cook } from "./pages/Cook";
|
import { Cook } from "./pages/Cook";
|
||||||
@@ -82,29 +80,6 @@ export function App() {
|
|||||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// App self-update (manual check from sidebar)
|
|
||||||
const [checkingAppUpdate, setCheckingAppUpdate] = useState(false);
|
|
||||||
|
|
||||||
const handleCheckForUpdates = useCallback(async () => {
|
|
||||||
setCheckingAppUpdate(true);
|
|
||||||
try {
|
|
||||||
const update = await check();
|
|
||||||
if (update) {
|
|
||||||
const confirmed = window.confirm(`ClawPal v${update.version} is available. Update and restart now?`);
|
|
||||||
if (confirmed) {
|
|
||||||
await update.downloadAndInstall();
|
|
||||||
await relaunch();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast("You're on the latest version");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Update check failed:", e);
|
|
||||||
showToast(`Update check failed: ${e}`, "error");
|
|
||||||
} finally {
|
|
||||||
setCheckingAppUpdate(false);
|
|
||||||
}
|
|
||||||
}, [showToast]);
|
|
||||||
|
|
||||||
const handleInstanceSelect = useCallback((id: string) => {
|
const handleInstanceSelect = useCallback((id: string) => {
|
||||||
setActiveInstance(id);
|
setActiveInstance(id);
|
||||||
@@ -311,16 +286,26 @@ export function App() {
|
|||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="justify-start hover:bg-accent text-muted-foreground"
|
|
||||||
onClick={handleCheckForUpdates}
|
|
||||||
disabled={checkingAppUpdate}
|
|
||||||
>
|
|
||||||
{checkingAppUpdate ? "Checking..." : "Check for Updates"}
|
|
||||||
</Button>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div className="px-4 pb-2 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
onClick={(e) => { e.preventDefault(); api.openUrl("https://clawpal.zhixian.io"); }}
|
||||||
|
>
|
||||||
|
Website
|
||||||
|
</a>
|
||||||
|
<span>·</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
onClick={(e) => { e.preventDefault(); api.openUrl("https://x.com/zhixianio"); }}
|
||||||
|
>
|
||||||
|
@zhixian
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dirty config action bar */}
|
{/* Dirty config action bar */}
|
||||||
{dirty && (
|
{dirty && (
|
||||||
<div className="px-2 pb-2 space-y-1.5">
|
<div className="px-2 pb-2 space-y-1.5">
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { check } from "@tauri-apps/plugin-updater";
|
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -74,11 +72,6 @@ export function Home({
|
|||||||
const [backingUp, setBackingUp] = useState(false);
|
const [backingUp, setBackingUp] = useState(false);
|
||||||
const [backupMessage, setBackupMessage] = useState("");
|
const [backupMessage, setBackupMessage] = useState("");
|
||||||
|
|
||||||
// ClawPal app self-update state
|
|
||||||
const [appUpdate, setAppUpdate] = useState<{ version: string; body?: string } | null>(null);
|
|
||||||
const [appUpdateChecking, setAppUpdateChecking] = useState(false);
|
|
||||||
const [appUpdating, setAppUpdating] = useState(false);
|
|
||||||
const [appUpdateProgress, setAppUpdateProgress] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Create agent dialog
|
// Create agent dialog
|
||||||
const [showCreateAgent, setShowCreateAgent] = useState(false);
|
const [showCreateAgent, setShowCreateAgent] = useState(false);
|
||||||
@@ -201,46 +194,6 @@ export function Home({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isRemote, isConnected, instanceId]);
|
}, [isRemote, isConnected, instanceId]);
|
||||||
|
|
||||||
// ClawPal app self-update check
|
|
||||||
useEffect(() => {
|
|
||||||
setAppUpdateChecking(true);
|
|
||||||
check()
|
|
||||||
.then((update) => {
|
|
||||||
if (update) {
|
|
||||||
setAppUpdate({ version: update.version, body: update.body });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => console.error("Failed to check app update:", e))
|
|
||||||
.finally(() => setAppUpdateChecking(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAppUpdate = useCallback(async () => {
|
|
||||||
setAppUpdating(true);
|
|
||||||
setAppUpdateProgress(0);
|
|
||||||
try {
|
|
||||||
const update = await check();
|
|
||||||
if (!update) return;
|
|
||||||
let totalBytes = 0;
|
|
||||||
let downloadedBytes = 0;
|
|
||||||
await update.downloadAndInstall((event) => {
|
|
||||||
if (event.event === "Started" && event.data.contentLength) {
|
|
||||||
totalBytes = event.data.contentLength;
|
|
||||||
} else if (event.event === "Progress") {
|
|
||||||
downloadedBytes += event.data.chunkLength;
|
|
||||||
if (totalBytes > 0) {
|
|
||||||
setAppUpdateProgress(Math.round((downloadedBytes / totalBytes) * 100));
|
|
||||||
}
|
|
||||||
} else if (event.event === "Finished") {
|
|
||||||
setAppUpdateProgress(100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await relaunch();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("App update failed:", e);
|
|
||||||
setAppUpdating(false);
|
|
||||||
setAppUpdateProgress(null);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteAgent = (agentId: string) => {
|
const handleDeleteAgent = (agentId: string) => {
|
||||||
if (isRemote && !isConnected) return;
|
if (isRemote && !isConnected) return;
|
||||||
@@ -301,46 +254,6 @@ export function Home({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ClawPal app self-update */}
|
|
||||||
{(appUpdateChecking || appUpdate) && (
|
|
||||||
<>
|
|
||||||
<span className="text-sm text-muted-foreground">App Update</span>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
{appUpdateChecking && (
|
|
||||||
<Badge variant="outline" className="text-muted-foreground">Checking for app updates...</Badge>
|
|
||||||
)}
|
|
||||||
{!appUpdateChecking && appUpdate && !appUpdating && (
|
|
||||||
<>
|
|
||||||
<Badge variant="outline" className="text-primary border-primary">
|
|
||||||
ClawPal v{appUpdate.version} available
|
|
||||||
</Badge>
|
|
||||||
<Button size="sm" className="text-xs h-6" onClick={handleAppUpdate}>
|
|
||||||
Update & Restart
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{appUpdating && (
|
|
||||||
<>
|
|
||||||
<Badge variant="outline" className="text-muted-foreground">
|
|
||||||
{appUpdateProgress !== null && appUpdateProgress < 100
|
|
||||||
? `Downloading... ${appUpdateProgress}%`
|
|
||||||
: appUpdateProgress === 100
|
|
||||||
? "Installing..."
|
|
||||||
: "Preparing..."}
|
|
||||||
</Badge>
|
|
||||||
{appUpdateProgress !== null && appUpdateProgress < 100 && (
|
|
||||||
<div className="w-32 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-primary rounded-full transition-all"
|
|
||||||
style={{ width: `${appUpdateProgress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground">Default Model</span>
|
<span className="text-sm text-muted-foreground">Default Model</span>
|
||||||
<div className="max-w-xs">
|
<div className="max-w-xs">
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { FormEvent } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { check } from "@tauri-apps/plugin-updater";
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useInstance } from "@/lib/instance-context";
|
import { useInstance } from "@/lib/instance-context";
|
||||||
import type { ModelCatalogProvider, ModelProfile, ProviderAuthSuggestion, ResolvedApiKey } from "@/lib/types";
|
import type { ModelCatalogProvider, ModelProfile, ProviderAuthSuggestion, ResolvedApiKey } from "@/lib/types";
|
||||||
@@ -125,6 +128,61 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
|||||||
|
|
||||||
const [catalogRefreshed, setCatalogRefreshed] = useState(false);
|
const [catalogRefreshed, setCatalogRefreshed] = useState(false);
|
||||||
|
|
||||||
|
// ClawPal app version & self-update
|
||||||
|
const [appVersion, setAppVersion] = useState<string>("");
|
||||||
|
const [appUpdate, setAppUpdate] = useState<{ version: string; body?: string } | null>(null);
|
||||||
|
const [appUpdateChecking, setAppUpdateChecking] = useState(false);
|
||||||
|
const [appUpdating, setAppUpdating] = useState(false);
|
||||||
|
const [appUpdateProgress, setAppUpdateProgress] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getVersion().then(setAppVersion).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCheckForUpdates = useCallback(async () => {
|
||||||
|
setAppUpdateChecking(true);
|
||||||
|
setAppUpdate(null);
|
||||||
|
try {
|
||||||
|
const update = await check();
|
||||||
|
if (update) {
|
||||||
|
setAppUpdate({ version: update.version, body: update.body });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Update check failed:", e);
|
||||||
|
} finally {
|
||||||
|
setAppUpdateChecking(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAppUpdate = useCallback(async () => {
|
||||||
|
setAppUpdating(true);
|
||||||
|
setAppUpdateProgress(0);
|
||||||
|
try {
|
||||||
|
const update = await check();
|
||||||
|
if (!update) return;
|
||||||
|
let totalBytes = 0;
|
||||||
|
let downloadedBytes = 0;
|
||||||
|
await update.downloadAndInstall((event) => {
|
||||||
|
if (event.event === "Started" && event.data.contentLength) {
|
||||||
|
totalBytes = event.data.contentLength;
|
||||||
|
} else if (event.event === "Progress") {
|
||||||
|
downloadedBytes += event.data.chunkLength;
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
setAppUpdateProgress(Math.round((downloadedBytes / totalBytes) * 100));
|
||||||
|
}
|
||||||
|
} else if (event.event === "Finished") {
|
||||||
|
setAppUpdateProgress(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("App update failed:", e);
|
||||||
|
setAppUpdating(false);
|
||||||
|
setAppUpdateProgress(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Extract profiles from remote config on first load
|
// Extract profiles from remote config on first load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRemote || !isConnected) return;
|
if (!isRemote || !isConnected) return;
|
||||||
@@ -291,6 +349,7 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 items-start">
|
<div className="grid grid-cols-2 gap-3 items-start">
|
||||||
|
<div className="space-y-3">
|
||||||
{/* Create / Edit form */}
|
{/* Create / Edit form */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -413,6 +472,56 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Current Version */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Current Version</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<span className="text-sm font-medium">{appVersion ? `v${appVersion}` : "..."}</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCheckForUpdates}
|
||||||
|
disabled={appUpdateChecking || appUpdating}
|
||||||
|
>
|
||||||
|
{appUpdateChecking ? "Checking..." : "Check for Updates"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!appUpdateChecking && appUpdate && !appUpdating && (
|
||||||
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
<Badge variant="outline" className="text-primary border-primary">
|
||||||
|
v{appUpdate.version} available
|
||||||
|
</Badge>
|
||||||
|
<Button size="sm" onClick={handleAppUpdate}>
|
||||||
|
Update & Restart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{appUpdating && (
|
||||||
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
<Badge variant="outline" className="text-muted-foreground">
|
||||||
|
{appUpdateProgress !== null && appUpdateProgress < 100
|
||||||
|
? `Downloading... ${appUpdateProgress}%`
|
||||||
|
: appUpdateProgress === 100
|
||||||
|
? "Installing..."
|
||||||
|
: "Preparing..."}
|
||||||
|
</Badge>
|
||||||
|
{appUpdateProgress !== null && appUpdateProgress < 100 && (
|
||||||
|
<div className="w-32 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all"
|
||||||
|
style={{ width: `${appUpdateProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Profiles list */}
|
{/* Profiles list */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -494,6 +603,7 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
|||||||
{message && (
|
{message && (
|
||||||
<p className="text-sm text-muted-foreground mt-3">{message}</p>
|
<p className="text-sm text-muted-foreground mt-3">{message}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user