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:
zhixian
2026-02-19 22:34:52 +09:00
parent 7138e97e87
commit ffcdcd22b2
4 changed files with 130 additions and 122 deletions

View File

@@ -11,7 +11,7 @@
"app": {
"windows": [
{
"title": "ClawPal",
"title": "ClawPal by zhixian",
"width": 1200,
"height": 820,
"minWidth": 1024,

View File

@@ -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">

View File

@@ -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 &amp; 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">

View File

@@ -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 &amp; 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>
);
}