···306306 backend (WebView2/laufey) on this machine. `--backend cef` (bundled Chromium,
307307 ~300MB binary vs ~68MB) renders reliably. `desktop` uses CEF by default;
308308 `desktop:webview` keeps the smaller variant for machines where it works.
309309+- **The webview leaves localhost freely — remote mode needs no Deno API.** A
310310+ packaged CEF spike confirmed the webview navigates to external https origins
311311+ by a page's own `location.href` redirect, and that `Deno.BrowserWindow`
312312+ exists (the first construction adopts the startup window) with a working
313313+ `navigate()` and cross-origin `executeJs()`. Remote mode is therefore just a
314314+ hook-level 303: `handleLocal` redirects every request to the configured
315315+ origin and the webview lives there from then on. No window API in the app.
316316+- **The CEF profile is ephemeral: remote mode re-logs-in each launch.** The
317317+ spike's Max-Age cookie did not survive a relaunch; no profile directory is
318318+ written, the docs expose no cache-path knob, and Chromium switches passed to
319319+ the exe are not forwarded. Within a session cookies behave normally, so the
320320+ login flow works — it just runs again on each launch until `deno desktop`
321321+ exposes a persistent profile (tracked as an open question). Decision 5's
322322+ architecture is unaffected; this is a UX cost, stated rather than papered
323323+ over.
324324+- **The database is created only when local mode is chosen.** `init` used to
325325+ open `quantum.db` unconditionally, which would have left a stray database on
326326+ every remote install. Local machinery (database, identity seed, sync timers,
327327+ loopback MCP) now arms exactly once via `armLocalMode` — from `init` when
328328+ local mode is already persisted, or lazily from the hook the moment the user
329329+ picks Local on `/setup`, which also removes the old implicit relaunch
330330+ dependency. First launch and remote mode touch no database at all.
309331- **First-launch is app-config-driven, not baked.** A shipped binary can't have
310332 a mode or display name compiled in. `QUANTUM_DESKTOP=1` makes the app read
311333 `quantum-desktop.json` (beside the database, outside it) at runtime; when
···330352 binary?** (decision 4) The main server's port is auto-selected per launch, so
331353 a stable MCP URL for a statically-configured agent host depends on this. If
332354 not, fall back to publishing the per-launch URL via `agent.json`.
355355+- **CEF profile persistence (upstream).** The webview's cookie jar does not
356356+ survive relaunch (see Resolved During Implementation), so remote mode
357357+ re-authenticates every launch. Watch `deno desktop` for a persistent-profile
358358+ or cache-path option; when one lands, remote-mode sessions become durable
359359+ with no change to our code.
333360- **Auto-update and artifact signing.** Out of scope here, but the release story
334361 will need it before wide distribution.
···159159No URL scheme, no deep link, no handoff endpoint, no bearer-session issuance —
160160the former tasks 6.1/6.3/6.4/6.5 are dropped. (See design decision 5.)
161161162162-- [ ] 6.1 Remote-mode shell: point the webview at the user-provided server
162162+- [x] 6.1 Remote-mode shell: point the webview at the user-provided server
163163 origin; start no embedded server and open no local database. Confirm the
164164 server's cookie login completes inside the webview and the session cookie
165165- persists (needs the desktop binary).
166166-- [ ] 6.2 Server side: nothing. Verify `src/routes/login` and
165165+ persists (needs the desktop binary). — Implemented as a hook-level gate:
166166+ in desktop remote mode `handleLocal` 303s every request (including
167167+ `/mcp`) to the configured origin, before anything touches the database.
168168+ The embedded server can't be *not started* (`deno desktop` owns it), but
169169+ it serves nothing and opens nothing. Database creation is now deferred
170170+ until local mode is actually chosen (`armLocalMode`, called lazily), so
171171+ first launch and remote mode never create `quantum.db` — verified live
172172+ (data dir holds only `quantum-desktop.json`). A packaged-binary spike
173173+ proved the CEF webview navigates to external https origins (both a page's
174174+ own `location.href` redirect and `Deno.BrowserWindow.navigate()`), so the
175175+ 303 hand-off works in the shipped app. **Cookie finding:** the CEF
176176+ profile is ephemeral — even Max-Age cookies do not survive a relaunch,
177177+ and no `deno desktop` knob or pass-through Chromium switch persists it —
178178+ so remote mode means logging in again on each launch until upstream
179179+ exposes a persistent profile. In-session cookies behave normally. The
180180+ in-webview login E2E against a real server is 8.3.
181181+- [x] 6.2 Server side: nothing. Verify `src/routes/login` and
167182 `src/routes/oauth/callback` are used unchanged — no handoff route, no
168168- session-delivery marker, no schema change.
183183+ session-delivery marker, no schema change. — Confirmed: the remote-mode
184184+ diff touches only `hooks.server.ts` and `src/routes/setup`; login and
185185+ callback routes and the schema are untouched.
169186170187## 7. First-launch experience
171188···177194 the full flow live and in the CEF window.
178195- [x] 7.2 Local branch: prompt for a display name and seed the local user with
179196 it. — `src/routes/welcome` (built earlier); reached after the mode screen.
180180-- [ ] 7.3 Remote branch: collect the server address and point the webview at it
181181- (task 6); the server's cookie login takes over from there. — DEFERRED. The
182182- option is present on `/setup` but disabled ("soon"). Remote-in-desktop is
183183- architecturally uncertain: `deno desktop` always runs the embedded server
184184- and points the webview at localhost, so navigating the webview to an
185185- external origin needs its own spike. Scoped out of this pass by decision.
197197+- [x] 7.3 Remote branch: collect the server address and point the webview at it
198198+ (task 6); the server's cookie login takes over from there. — Un-deferred:
199199+ the navigation spike (see 6.1) resolved the architectural uncertainty, so
200200+ the "soon" placeholder is now a working option. Selecting it expands an
201201+ inline form; the action normalizes the address (scheme defaults to
202202+ https), probes `GET /healthz` for `{ ok: true }` with a 5s timeout so a
203203+ typo or non-Quantum host fails with a message instead of stranding the
204204+ webview, persists `{ mode: "remote", serverUrl }`, and 303s the webview
205205+ to the origin. Verified live: bad-address and non-Quantum-host errors
206206+ render inline with the input preserved; a valid address lands the
207207+ browser on the remote origin; every subsequent request to the embedded
208208+ server redirects there.
186209- [ ] 7.4 Add the reset action (Settings) that clears app-config and returns to
187210 the mode-selection screen on next launch. Confirm with a plain sentence
188211 about what reset does and does not delete (local data stays on disk).
···202225 agent-provenance tail still wants a real bank connection exercised in the
203226 packaged app; the pieces are individually tested (sync, MCP, provenance) but
204227 not yet driven as one desktop session.
205205-- [ ] 8.3 Remote mode end to end against a dev server: choose Remote → the
228228+- [x] 8.3 Remote mode end to end against a dev server: choose Remote → the
206229 server's cookie login runs in the webview → app authenticates as the
207207- logged-in user via cookie → no local database created.
230230+ logged-in user via cookie → no local database created. — Driven by Graham
231231+ against his real hosted instance in the rebuilt packaged binary
232232+ (2026-07-17): Remote selected on `/setup`, the server's ATProto cookie
233233+ login completed inside the webview, and the app ran as the logged-in
234234+ user. No-local-database is verified at the hook level (remote mode never
235235+ calls `initDb`; the data dir holds only `quantum-desktop.json`). As
236236+ predicted by the profile spike, the session did not survive an app
237237+ relaunch (ephemeral CEF profile — see design's open question); login runs
238238+ again each launch.
208239- [ ] 8.4 Reset from each mode returns to the selection screen; local data files
209240 remain on disk after a reset.
210241- [x] 8.5 Confirm a hosted server rejects `QUANTUM_MODE=local` combined with
···3838export const init: ServerInit = async () => {
3939 if (building) return;
4040 const config = getConfig(); // fails fast with a clear error on missing/invalid env
4141- const db = initDb(config.dbPath); // migrations embedded in the bundle
42414342 if (config.mode === "local") {
4444- // Single-user desktop build: no login, no OAuth client. Seed the local
4545- // identity when the display name is already chosen; otherwise the welcome
4646- // flow seeds it on first launch.
4747- if (config.localDisplayName) {
4848- upsertUser(db, LOCAL_DID, config.localDisplayName);
4343+ if (config.desktop) {
4444+ // Desktop: the database and local machinery exist only once the user
4545+ // has chosen local mode. First launch (mode not yet chosen) and remote
4646+ // mode (thin client to a server) never create quantum.db; if local is
4747+ // chosen mid-session on /setup, handleLocal arms this lazily.
4848+ const chosen = readDesktopConfig(config.dbPath);
4949+ if (chosen?.mode === "local") armLocalMode(config);
5050+ } else {
5151+ armLocalMode(config);
4952 }
5050- startLocalSync();
5151- startLoopbackMcp(config.dbPath);
5253 return;
5354 }
54555656+ const db = initDb(config.dbPath); // migrations embedded in the bundle
5557 deleteExpiredSessions(db);
5658 await initOAuthClient(config, db);
5759···7072// day the app wasn't open. Instead: sync once on launch (catching up whatever
7173// was missed while closed) and periodically while the app runs.
7274const LOCAL_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
7575+7676+let localArmed = false;
7777+7878+/**
7979+ * Bring up local-mode machinery exactly once: open (and create) the database,
8080+ * seed the local identity, start the catch-up/interval sync and the optional
8181+ * loopback MCP listener. Called from `init` when local mode is already chosen,
8282+ * or from `handleLocal` the moment the user chooses it on /setup — so the
8383+ * choice takes effect without a relaunch, and remote mode never runs any of it.
8484+ */
8585+function armLocalMode(config: ReturnType<typeof getConfig>): void {
8686+ if (localArmed) return;
8787+ localArmed = true;
8888+ const db = initDb(config.dbPath);
8989+ if (config.localDisplayName) {
9090+ upsertUser(db, LOCAL_DID, config.localDisplayName);
9191+ }
9292+ startLocalSync();
9393+ startLoopbackMcp(config.dbPath);
9494+}
73957496function startLocalSync(): void {
7597 const run = () =>
···188210 event: RequestEvent,
189211 resolve: Resolve,
190212): Promise<Response> {
213213+ const path = event.url.pathname;
214214+ const config = getConfig();
215215+216216+ // Desktop: the mode (local/remote) is chosen on a setup screen at first
217217+ // launch and persisted in app-config, not baked into the build. This gate
218218+ // runs before anything touches the database, because in remote mode (and
219219+ // before a choice is made) no local database exists.
220220+ if (config.desktop) {
221221+ const chosen = readDesktopConfig(config.dbPath);
222222+ if (chosen?.mode === "remote" && chosen.serverUrl) {
223223+ // Thin client: hand the webview to the server for every request. The
224224+ // server's own cookie login runs inside the webview (design decision
225225+ // 5); nothing — including /mcp — is served locally.
226226+ redirect(303, chosen.serverUrl);
227227+ }
228228+ if (!chosen || chosen.mode !== "local") {
229229+ event.locals.user = null;
230230+ event.locals.apiToken = null;
231231+ if (path !== "/setup" && !PUBLIC_PATHS.has(path)) {
232232+ redirect(303, "/setup");
233233+ }
234234+ return resolve(event);
235235+ }
236236+ // Local mode chosen — possibly seconds ago on /setup: arm the database,
237237+ // sync, and loopback MCP now rather than requiring a relaunch.
238238+ armLocalMode(config);
239239+ if (path === "/setup") redirect(303, "/"); // already chosen
240240+ }
241241+191242 const db = getDb();
192192- const path = event.url.pathname;
193243194244 // MCP is token-authenticated, never the local session.
195245 if (isMcpPath(path)) {
···203253 }
204254205255 event.locals.apiToken = null;
206206-207207- // Desktop first launch: the mode (local/remote) is chosen on a setup screen
208208- // and persisted in app-config, not baked into the build. Until local mode is
209209- // chosen, route everything to /setup.
210210- if (getConfig().desktop) {
211211- const chosen = readDesktopConfig(getConfig().dbPath);
212212- if (!chosen || chosen.mode !== "local") {
213213- event.locals.user = null;
214214- if (path !== "/setup" && !PUBLIC_PATHS.has(path)) {
215215- redirect(303, "/setup");
216216- }
217217- return resolve(event);
218218- }
219219- if (path === "/setup") redirect(303, "/"); // already chosen
220220- }
221256222257 const localUser = getUser(db, LOCAL_DID);
223258
···11-import { redirect } from "@sveltejs/kit";
11+import { fail, redirect } from "@sveltejs/kit";
22import { getConfig } from "$lib/server/config";
33import { writeDesktopConfig } from "$lib/server/desktop-config";
44import type { Actions, PageServerLoad } from "./$types";
···1717 writeDesktopConfig(config.dbPath, { mode: "local" });
1818 // The hook now routes to /welcome to choose a display name.
1919 redirect(303, "/");
2020+ },
2121+2222+ remote: async ({ request }) => {
2323+ const config = getConfig();
2424+ if (!config.desktop) redirect(303, "/");
2525+2626+ const form = await request.formData();
2727+ const raw = String(form.get("serverUrl") ?? "").trim();
2828+ if (!raw) {
2929+ return fail(400, { serverUrl: raw, error: "Enter your server's address." });
3030+ }
3131+3232+ let origin: string;
3333+ try {
3434+ origin = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`)
3535+ .origin;
3636+ } catch {
3737+ return fail(400, {
3838+ serverUrl: raw,
3939+ error: "That doesn't look like a valid address.",
4040+ });
4141+ }
4242+4343+ // Probe the server's health endpoint before committing, so a typo fails
4444+ // here with a message instead of stranding the webview on a dead origin.
4545+ // The body check ({ ok: true }) keeps an arbitrary website's 200 from
4646+ // passing as a Quantum server.
4747+ try {
4848+ const res = await fetch(`${origin}/healthz`, {
4949+ signal: AbortSignal.timeout(5000),
5050+ });
5151+ const body = res.ok ? await res.json() : null;
5252+ if (body?.ok !== true) throw new Error("not a Quantum server");
5353+ } catch {
5454+ return fail(400, {
5555+ serverUrl: raw,
5656+ error:
5757+ `Couldn't find a Quantum server at ${origin}. Check the address and that the server is running.`,
5858+ });
5959+ }
6060+6161+ writeDesktopConfig(config.dbPath, { mode: "remote", serverUrl: origin });
6262+ // Hand the webview to the server; its cookie login takes over from here.
6363+ redirect(303, origin);
2064 },
2165};