agent-orchestrator/frontend/src/renderer/components/UpdatesSection.tsx

125 lines
4.5 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { aoBridge } from "../lib/bridge";
import type { UpdateChannel, UpdateSettings } from "../../main/update-settings";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Label } from "./ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
export const updateSettingsQueryKey = ["update-settings"] as const;
const CHANNEL_OPTIONS: { value: UpdateChannel; label: string }[] = [
{ value: "latest", label: "Stable (latest release)" },
{ value: "nightly", label: "Nightly (pre-release)" },
];
// UpdatesSection is the Global Settings card for the desktop auto-update channel
// (issue #2207). It reads/writes ~/.ao/update-settings.json via the main process
// (the same file auto-updater.ts consumes), letting a user pick Stable vs Nightly.
// Changes apply on the next launch / update check.
export function UpdatesSection() {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: updateSettingsQueryKey,
queryFn: () => aoBridge.updateSettings.get(),
});
const [form, setForm] = useState<UpdateSettings>({ enabled: false, channel: "latest", nightlyAck: false });
const [savedAt, setSavedAt] = useState<number | null>(null);
// Seed the form once settings load (and on refetch). Keying off the loaded
// value keeps local edits responsive without a controlled-from-query loop.
useEffect(() => {
if (query.data) setForm(query.data);
}, [query.data]);
const save = useMutation({
mutationFn: async (next: UpdateSettings) => {
await aoBridge.updateSettings.set(next);
},
onSuccess: () => {
setSavedAt(Date.now());
void queryClient.invalidateQueries({ queryKey: updateSettingsQueryKey });
},
});
const setEnabled = (enabled: boolean) => {
setSavedAt(null);
setForm((f) => ({ ...f, enabled }));
};
const setChannel = (channel: UpdateChannel) => {
setSavedAt(null);
// Selecting Nightly in Settings is itself the acknowledgement of the
// instability warning shown below; Stable clears it.
setForm((f) => ({ ...f, channel, nightlyAck: channel === "nightly" }));
};
return (
<Card>
<CardHeader>
<CardTitle className="text-[13px]">Updates</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label htmlFor="updatesEnabled" className="text-[12px] text-muted-foreground">
Automatic updates
</Label>
<EnabledSelect id="updatesEnabled" value={form.enabled} onChange={setEnabled} />
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="updateChannel" className="text-[12px] text-muted-foreground">
Update channel
</Label>
<Select value={form.channel} onValueChange={(v) => setChannel(v as UpdateChannel)} disabled={!form.enabled}>
<SelectTrigger id="updateChannel" className="h-8 w-full text-[13px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CHANNEL_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{form.channel === "nightly" && form.enabled && (
<p className="text-[12px] leading-5 text-warning">
Nightly builds are cut every day and can be unstable or lose data. Only use Nightly if you are comfortable
with that.
</p>
)}
<div className="flex items-center gap-3">
<Button type="button" variant="primary" onClick={() => save.mutate(form)} disabled={save.isPending}>
{save.isPending ? "Saving…" : "Save changes"}
</Button>
{save.isError && (
<span className="text-[12px] text-error">
{save.error instanceof Error ? save.error.message : "Save failed"}
</span>
)}
{savedAt && !save.isPending && !save.isError && <span className="text-[12px] text-success">Saved.</span>}
</div>
</CardContent>
</Card>
);
}
function EnabledSelect({ id, value, onChange }: { id: string; value: boolean; onChange: (value: boolean) => void }) {
return (
<Select value={value ? "on" : "off"} onValueChange={(v) => onChange(v === "on")}>
<SelectTrigger id={id} className="h-8 w-full text-[13px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="on">Enabled</SelectItem>
<SelectItem value="off">Disabled</SelectItem>
</SelectContent>
</Select>
);
}