void stop(job.id)}
xstyle={styles.stop}
>
diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx
index bc56c4e..9b802a4 100644
--- a/src/routes/settings.tsx
+++ b/src/routes/settings.tsx
@@ -12,6 +12,7 @@ import { PasskeySetting } from "../components/passkey-setting";
import { PushNotifications } from "../components/push-notifications";
import { ThemeSetting } from "../components/theme-setting";
import { Button } from "../components/ui/button";
+import { UpdateSetting } from "../components/update-setting";
import { getCodexAccount, getCodexLogin } from "../features/auth/functions";
import { getPushSettings } from "../features/notifications/functions";
import {
@@ -164,6 +165,9 @@ function SettingsPage() {
+
Looking for an agent’s model, instructions, or automations? Open
that agent to manage its settings.
diff --git a/src/server/auth/client.js b/src/server/auth/client.js
index 8cc48e5..b77ab0f 100644
--- a/src/server/auth/client.js
+++ b/src/server/auth/client.js
@@ -1,10 +1,13 @@
const content = document.querySelector("#content");
const status = document.querySelector("#status");
let setup = null;
+// A fixed destination for updater reauthentication, never a supplied URL/path.
+const returnToUpdates =
+ new URLSearchParams(location.search).get("updates") === "1";
function captureSetup() {
const value = new URLSearchParams(location.hash.slice(1)).get("setup");
if (value) setup = value;
- history.replaceState(null, "", "/auth");
+ history.replaceState(null, "", returnToUpdates ? "/auth?updates=1" : "/auth");
}
captureSetup();
window.addEventListener("hashchange", () => {
@@ -111,7 +114,15 @@ async function passkey(register, name, returnToSettings = false) {
name,
});
setup = null;
- location.assign((register && name) || returnToSettings ? "/auth" : "/");
+ location.assign(
+ register && name
+ ? "/auth"
+ : returnToUpdates
+ ? "/settings?group=updates"
+ : returnToSettings
+ ? "/auth"
+ : "/",
+ );
}
async function render() {
const response = await fetch("/auth/api/state", { cache: "no-store" });
@@ -152,7 +163,7 @@ async function render() {
return;
}
const back = element("a", "Back to Roost");
- back.href = "/settings";
+ back.href = returnToUpdates ? "/settings?group=updates" : "/settings";
content.append(
back,
element("h1", "Passkeys and sessions"),
diff --git a/tests/settings-navigation.test.ts b/tests/settings-navigation.test.ts
index 02d74a4..de40e17 100644
--- a/tests/settings-navigation.test.ts
+++ b/tests/settings-navigation.test.ts
@@ -15,6 +15,7 @@ test("unknown settings groups fall back to appearance", () => {
test("search finds existing controls, conditional options, and vocabulary across groups", () => {
const cases = [
+ ["software update", "updates"],
[" THINKING summaries ", "conversation"],
["tool inputs", "conversation"],
["messages", "conversation"],
@@ -51,7 +52,7 @@ test("search finds existing controls, conditional options, and vocabulary across
findSettings("codex").map((entry) => entry.id),
["conversation", "account"],
);
- assert.equal(findSettings(" \n ").length, 5);
+ assert.equal(findSettings(" \n ").length, 6);
assert.equal(findSettings("nothing-matches-this").length, 0);
assert.equal(findSettings("passkeys trackers").length, 0);
});
diff --git a/tests/update-drafts.test.ts b/tests/update-drafts.test.ts
new file mode 100644
index 0000000..34e5829
--- /dev/null
+++ b/tests/update-drafts.test.ts
@@ -0,0 +1,93 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { readDraft, saveDraft } from "../src/features/updates/drafts";
+import { rememberResult } from "../src/features/updates/state";
+
+function storage() {
+ const values: Record = {};
+ return new Proxy(values, {
+ get: (target, key) =>
+ key === "getItem"
+ ? (k: string) => target[k] ?? null
+ : key === "setItem"
+ ? (k: string, v: string) => {
+ target[k] = v;
+ }
+ : target[String(key)],
+ });
+}
+test("drafts survive reload, preserve independent tab text, and do not revive sent prompts", () => {
+ const oldLocal = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
+ const oldSession = Object.getOwnPropertyDescriptor(
+ globalThis,
+ "sessionStorage",
+ );
+ try {
+ Object.defineProperty(globalThis, "localStorage", {
+ configurable: true,
+ value: storage(),
+ });
+ const first = storage();
+ Object.defineProperty(globalThis, "sessionStorage", {
+ configurable: true,
+ value: first,
+ });
+ saveDraft("agent", "unsent text", []);
+ assert.equal(readDraft("agent")?.text, "unsent text");
+ assert.equal(readDraft("different"), null);
+ Object.defineProperty(globalThis, "sessionStorage", {
+ configurable: true,
+ value: storage(),
+ });
+ assert.equal(readDraft("agent")?.text, "unsent text");
+ saveDraft("agent", "other tab", []);
+ Object.defineProperty(globalThis, "sessionStorage", {
+ configurable: true,
+ value: first,
+ });
+ assert.equal(readDraft("agent")?.text, "unsent text");
+ saveDraft("agent", "", []);
+ assert.equal(readDraft("agent")?.text, "");
+ assert.throws(() => saveDraft("agent", "x".repeat(2 * 1024 * 1024), []));
+ } finally {
+ if (oldLocal) Object.defineProperty(globalThis, "localStorage", oldLocal);
+ else Reflect.deleteProperty(globalThis, "localStorage");
+ if (oldSession)
+ Object.defineProperty(globalThis, "sessionStorage", oldSession);
+ else Reflect.deleteProperty(globalThis, "sessionStorage");
+ }
+});
+
+test("unavailable browser storage is reported separately from connectivity", () => {
+ const old = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
+ try {
+ Object.defineProperty(globalThis, "localStorage", {
+ configurable: true,
+ value: {
+ setItem() {
+ throw new Error("disabled");
+ },
+ },
+ });
+ assert.equal(rememberResult(null), true);
+ assert.equal(
+ rememberResult({
+ id: "operation",
+ requestKey: "key",
+ phase: "accepted",
+ previous: "0.1.40",
+ version: "0.1.41",
+ updatedAt: Date.now(),
+ cancellable: true,
+ committed: false,
+ error: undefined,
+ blockers: undefined,
+ bytes: undefined,
+ }),
+ false,
+ );
+ } finally {
+ if (old) Object.defineProperty(globalThis, "localStorage", old);
+ else Reflect.deleteProperty(globalThis, "localStorage");
+ }
+});