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": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ClawPal",
|
||||
"title": "ClawPal by zhixian",
|
||||
"width": 1200,
|
||||
"height": 820,
|
||||
"minWidth": 1024,
|
||||
|
||||
51
src/App.tsx
51
src/App.tsx
@@ -1,6 +1,4 @@
|
||||
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 { Recipes } from "./pages/Recipes";
|
||||
import { Cook } from "./pages/Cook";
|
||||
@@ -82,29 +80,6 @@ export function App() {
|
||||
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) => {
|
||||
setActiveInstance(id);
|
||||
@@ -311,16 +286,26 @@ export function App() {
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start hover:bg-accent text-muted-foreground"
|
||||
onClick={handleCheckForUpdates}
|
||||
disabled={checkingAppUpdate}
|
||||
>
|
||||
{checkingAppUpdate ? "Checking..." : "Check for Updates"}
|
||||
</Button>
|
||||
</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 && (
|
||||
<div className="px-2 pb-2 space-y-1.5">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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 { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -74,11 +72,6 @@ export function Home({
|
||||
const [backingUp, setBackingUp] = useState(false);
|
||||
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
|
||||
const [showCreateAgent, setShowCreateAgent] = useState(false);
|
||||
@@ -201,46 +194,6 @@ export function Home({
|
||||
return () => clearTimeout(timer);
|
||||
}, [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) => {
|
||||
if (isRemote && !isConnected) return;
|
||||
@@ -301,46 +254,6 @@ export function Home({
|
||||
)}
|
||||
</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>
|
||||
<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 { 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 { useInstance } from "@/lib/instance-context";
|
||||
import type { ModelCatalogProvider, ModelProfile, ProviderAuthSuggestion, ResolvedApiKey } from "@/lib/types";
|
||||
@@ -125,6 +128,61 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
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="space-y-3">
|
||||
{/* Create / Edit form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -413,6 +472,56 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -494,6 +603,7 @@ export function Settings({ onDataChange }: { onDataChange?: () => void }) {
|
||||
{message && (
|
||||
<p className="text-sm text-muted-foreground mt-3">{message}</p>
|
||||
)}
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user