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 (
{label}
{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}
)}
Save
{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}
)}
{running ? "Updating…" : "Update now"}
{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 }) => (
))}
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.
);
}