import { useEffect, useState } from "react"; import type { AutoUpdateLedger, AutoUpdateStatus, } from "../../../../hooks/lib/auto-update"; import type { ControlRoomPrefs } from "../../prefs"; import type { ServerStatus } from "../../server"; import { Button } from "../components/button"; import { Input } from "../components/input"; import { Label } from "../components/label"; import { Switch } from "../components/switch"; import { clock } from "../format"; import { Empty, Panel, Pending } from "../frame"; import { useLoaded } from "../lib/api"; import { runUpdateNow, setAutoUpdate, setInstallSettings, setPrefs as writePrefs, } from "../lib/write"; interface InstallSettings { actor: string; timezone: string; } function Field({ label, hint, value, onChange, }: { label: string; hint: string; value: string; onChange: (next: string) => void; }) { return (
{hint} onChange(e.target.value)} />
); } function ThisInstall({ initial }: { initial: InstallSettings }) { const [form, setForm] = useState(initial); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); const dirty = form.actor !== initial.actor || form.timezone !== initial.timezone; useEffect(() => { if (dirty) setSaved(false); }, [dirty]); const save = () => { void setInstallSettings({ ...form }).then((failure) => { setError(failure); setSaved(failure === null); }); }; return (
setForm({ ...form, actor })} /> setForm({ ...form, timezone })} /> {error && (

{error}

)}
{saved && saved}
); } function latest(last: AutoUpdateLedger): string { return last.finishedAt ?? last.attemptedAt ?? last.skippedAt ?? ""; } function lastRunLine(last: AutoUpdateLedger | null): string { if (!last) return "never run"; const at = latest(last); const when = at ? ` · ${clock(at)}` : ""; if (at && at === last.skippedAt) return `waited — ${last.skipped}${when}`; if (last.finishedAt) { return last.ok ? `updated ${last.from} → ${last.to}${when}` : `failed — ${last.error ?? "unknown error"}${when}`; } return `running since ${clock(last.attemptedAt ?? "")}`; } function versionLine(status: AutoUpdateStatus): string { if (status.available && status.latest) return `${status.current} → ${status.latest}`; return `${status.current} · up to date`; } function Updates({ initial }: { initial: AutoUpdateStatus }) { const [status, setStatus] = useState(initial); const [error, setError] = useState(null); const [running, setRunning] = useState(false); const refresh = () => { void fetch("/api/update") .then((res) => res.json() as Promise) .then(setStatus) .catch(() => setError("could not read update status")); }; const toggle = (enabled: boolean) => { setStatus({ ...status, enabled }); void setAutoUpdate(enabled).then((failure) => { setError(failure); if (failure) setStatus({ ...status, enabled: !enabled }); }); }; const now = () => { setRunning(true); void runUpdateNow().then((failure) => { setError(failure); setTimeout(() => { setRunning(false); refresh(); }, 4000); }); }; return (
update once a day, when a session closes
version
{versionLine(status)}
last run
{lastRunLine(status.last)}
{error && (

{error}

)}
{status.mode === "repo" && ( waits if this clone has uncommitted changes )}
); } const ATTENTION_LABELS: Record = { refusals: "A hook blocked a call, or you denied one", waiting: "A handoff left a question for you", unranked: "A project has no purpose on record", }; const THRESHOLDS = [ { key: "quietAfterDays", label: 'a project is "gone quiet" after' }, { key: "urgentWithinDays", label: "a dated step is urgent within" }, ] as const; function Ranking({ initial }: { initial: ControlRoomPrefs }) { const [prefs, setPrefs] = useState(initial); const [error, setError] = useState(null); const save = (update: Record) => { void writePrefs(update).then((failure) => { setError(failure); if (!failure) setPrefs({ ...prefs, ...update } as ControlRoomPrefs); }); }; return ( <>
{THRESHOLDS.map(({ key, label }) => (
{ const days = Number(e.target.value); setPrefs({ ...prefs, [key]: days }); if (Number.isInteger(days) && days > 0) save({ [key]: days }); }} /> days
))}
rank stated goals in the grid, not just projects save({ rankGoals })} />
{error && (

{error}

)}

What lights the bell. Each source is a fact PAL already holds — nothing here asks a model.

{Object.entries(prefs.attention).map(([source, on]) => (
{ATTENTION_LABELS[source] ?? source} save({ attention: { [source]: next } })} />
))}
); } export function Settings() { const settings = useLoaded("/api/settings"); const prefs = useLoaded("/api/prefs"); const status = useLoaded("/api/status"); const updates = useLoaded("/api/update"); if (settings.state !== "ready") return ; return (
{prefs.state === "ready" && } {updates.state === "ready" && } {status.state === "ready" ? (
machine
{status.data.machine}
port
{status.data.port}
pid
{status.data.pid}
ledger
{status.data.ledgerFiles} file(s)
up since
{status.data.startedAt}
) : ( not answering )}

Loopback only. The machine label is set with pal cli machine, and stays on this machine.

); }