portable-agent-layer 0.76.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  rel="stylesheet"
11
11
  href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@400;500;600&display=swap"
12
12
  />
13
- <script type="module" crossorigin src="/assets/index-BoQjFpzV.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-ZQVmA4BJ.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-Dncp2bYg.css">
15
15
  </head>
16
16
  <body>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The page's four write calls. Each POSTs one named field and hands back the
2
+ * The page's write calls. Each POSTs one named field and hands back the
3
3
  * server's own error text, so a refusal reads the same wherever it surfaces.
4
4
  */
5
5
 
@@ -41,3 +41,11 @@ export function setSnooze(project: string, id: number, days: number) {
41
41
  export function setPrefs(update: Record<string, unknown>) {
42
42
  return post("/api/prefs", update);
43
43
  }
44
+
45
+ export function setAutoUpdate(enabled: boolean) {
46
+ return post("/api/update", { enabled });
47
+ }
48
+
49
+ export function runUpdateNow() {
50
+ return post("/api/update/run", {});
51
+ }
@@ -1,13 +1,23 @@
1
1
  import { useEffect, useState } from "react";
2
+ import type {
3
+ AutoUpdateLedger,
4
+ AutoUpdateStatus,
5
+ } from "../../../../hooks/lib/auto-update";
2
6
  import type { ControlRoomPrefs } from "../../prefs";
3
7
  import type { ServerStatus } from "../../server";
4
8
  import { Button } from "../components/button";
5
9
  import { Input } from "../components/input";
6
10
  import { Label } from "../components/label";
7
11
  import { Switch } from "../components/switch";
12
+ import { clock } from "../format";
8
13
  import { Empty, Panel, Pending } from "../frame";
9
14
  import { useLoaded } from "../lib/api";
10
- import { setInstallSettings, setPrefs as writePrefs } from "../lib/write";
15
+ import {
16
+ runUpdateNow,
17
+ setAutoUpdate,
18
+ setInstallSettings,
19
+ setPrefs as writePrefs,
20
+ } from "../lib/write";
11
21
 
12
22
  interface InstallSettings {
13
23
  actor: string;
@@ -82,6 +92,96 @@ function ThisInstall({ initial }: { initial: InstallSettings }) {
82
92
  );
83
93
  }
84
94
 
95
+ function latest(last: AutoUpdateLedger): string {
96
+ return last.finishedAt ?? last.attemptedAt ?? last.skippedAt ?? "";
97
+ }
98
+
99
+ function lastRunLine(last: AutoUpdateLedger | null): string {
100
+ if (!last) return "never run";
101
+ const at = latest(last);
102
+ const when = at ? ` · ${clock(at)}` : "";
103
+ if (at && at === last.skippedAt) return `waited — ${last.skipped}${when}`;
104
+ if (last.finishedAt) {
105
+ return last.ok
106
+ ? `updated ${last.from} → ${last.to}${when}`
107
+ : `failed — ${last.error ?? "unknown error"}${when}`;
108
+ }
109
+ return `running since ${clock(last.attemptedAt ?? "")}`;
110
+ }
111
+
112
+ function versionLine(status: AutoUpdateStatus): string {
113
+ if (status.available && status.latest) return `${status.current} → ${status.latest}`;
114
+ return `${status.current} · up to date`;
115
+ }
116
+
117
+ function Updates({ initial }: { initial: AutoUpdateStatus }) {
118
+ const [status, setStatus] = useState(initial);
119
+ const [error, setError] = useState<string | null>(null);
120
+ const [running, setRunning] = useState(false);
121
+
122
+ const refresh = () => {
123
+ void fetch("/api/update")
124
+ .then((res) => res.json() as Promise<AutoUpdateStatus>)
125
+ .then(setStatus)
126
+ .catch(() => setError("could not read update status"));
127
+ };
128
+
129
+ const toggle = (enabled: boolean) => {
130
+ setStatus({ ...status, enabled });
131
+ void setAutoUpdate(enabled).then((failure) => {
132
+ setError(failure);
133
+ if (failure) setStatus({ ...status, enabled: !enabled });
134
+ });
135
+ };
136
+
137
+ const now = () => {
138
+ setRunning(true);
139
+ void runUpdateNow().then((failure) => {
140
+ setError(failure);
141
+ setTimeout(() => {
142
+ setRunning(false);
143
+ refresh();
144
+ }, 4000);
145
+ });
146
+ };
147
+
148
+ return (
149
+ <Panel title="Updates">
150
+ <div className="flex flex-col gap-4">
151
+ <div className="flex items-center justify-between gap-3 text-[12.5px]">
152
+ <span>update once a day, at session start</span>
153
+ <Switch
154
+ aria-label="update once a day"
155
+ checked={status.enabled}
156
+ onCheckedChange={toggle}
157
+ />
158
+ </div>
159
+ <dl className="m-0 grid grid-cols-[110px_1fr] gap-y-1.5 text-[12.5px]">
160
+ <dt className="text-neutral-600">version</dt>
161
+ <dd className="m-0 tabular-nums">{versionLine(status)}</dd>
162
+ <dt className="text-neutral-600">last run</dt>
163
+ <dd className="m-0">{lastRunLine(status.last)}</dd>
164
+ </dl>
165
+ {error && (
166
+ <p className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
167
+ {error}
168
+ </p>
169
+ )}
170
+ <div className="flex items-center gap-3">
171
+ <Button variant="primary" disabled={running} onClick={now}>
172
+ {running ? "Updating…" : "Update now"}
173
+ </Button>
174
+ {status.mode === "repo" && (
175
+ <span className="text-[11px] text-neutral-600">
176
+ waits if this clone has uncommitted changes
177
+ </span>
178
+ )}
179
+ </div>
180
+ </div>
181
+ </Panel>
182
+ );
183
+ }
184
+
85
185
  const ATTENTION_LABELS: Record<string, string> = {
86
186
  refusals: "A hook blocked a call, or you denied one",
87
187
  waiting: "A handoff left a question for you",
@@ -172,12 +272,14 @@ export function Settings() {
172
272
  const settings = useLoaded<InstallSettings>("/api/settings");
173
273
  const prefs = useLoaded<ControlRoomPrefs>("/api/prefs");
174
274
  const status = useLoaded<ServerStatus>("/api/status");
275
+ const updates = useLoaded<AutoUpdateStatus>("/api/update");
175
276
 
176
277
  if (settings.state !== "ready") return <Pending value={settings} />;
177
278
  return (
178
279
  <div className="grid items-start gap-6 md:grid-cols-2">
179
280
  <ThisInstall initial={settings.data} />
180
281
  {prefs.state === "ready" && <Ranking initial={prefs.data} />}
282
+ {updates.state === "ready" && <Updates initial={updates.data} />}
181
283
  <Panel title="Where it runs">
182
284
  {status.state === "ready" ? (
183
285
  <dl className="m-0 grid grid-cols-[110px_1fr] gap-y-1.5 text-[12.5px]">
@@ -96,6 +96,17 @@ export function writeInstallSettings(update: Partial<InstallSettings>): WriteOut
96
96
  return { ok: true, changed: true };
97
97
  }
98
98
 
99
+ /**
100
+ * Flipping the switch here is itself a decision, so `decided` closes the
101
+ * one-time install question too — nobody is asked about a setting they just set.
102
+ */
103
+ export function setAutoUpdate(enabled: boolean): WriteOutcome {
104
+ const data = rawSettings();
105
+ writeSettings({ ...data, autoUpdate: { ...data.autoUpdate, enabled, decided: true } });
106
+ reload();
107
+ return { ok: true, changed: true };
108
+ }
109
+
99
110
  function isTimezone(value: string): boolean {
100
111
  try {
101
112
  new Intl.DateTimeFormat("en-US", { timeZone: value });