diff --git a/CHANGELOG.md b/CHANGELOG.md index 0342451..099abe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ Human-curated release notes. Earlier 0.9.x notes also live on the [GitHub releases](https://github.com/benjsmith/switchbay/releases) page. +## 2026-10-08 — v0.13.1 + +**Migration:** none. **Breaking:** none. + +### Fixed + +- **Restart / Update / Quit with skill embeds** — when proxied Graph or Agents + embeds were open, Settings → Restart, Update, and Quit were rewritten onto + `/embed/{ce|okstratr}/api/*` and came back as a red "not found" toast. Those + control-plane paths (and the other Switchbay-owned `/api/*` prefixes the shell + and Settings call) now stay on the Switchbay daemon. +- **CE sidebar remount** — skip `mountSidebar` when the sidebar slot is + disconnected or missing `#sidebar`, avoiding a console appendChild error + during sync. + ## 2026-09-29 — v0.13.0 **Migration:** none. **Breaking:** none for wiki/vault format. After pull, diff --git a/frontend/package.json b/frontend/package.json index 03578dc..2cc184a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "switchbay-frontend", "private": true, - "version": "0.13.0", + "version": "0.13.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/widgets/embed/ceEmbedSession.ts b/frontend/src/widgets/embed/ceEmbedSession.ts index aa31a6d..3247b96 100644 --- a/frontend/src/widgets/embed/ceEmbedSession.ts +++ b/frontend/src/widgets/embed/ceEmbedSession.ts @@ -326,7 +326,11 @@ class CeEmbedSession { if (nameEl) nameEl.textContent = workspaceLabel(data); } try { - handle.mountSidebar(this.sidebarEl); + // Guard: CE's mountSidebar appendChilds into #sidebar. Skip when + // the slot was unmounted mid-sync (disconnected) or inject failed. + if (this.sidebarEl.isConnected && this.sidebarEl.querySelector("#sidebar")) { + handle.mountSidebar(this.sidebarEl); + } } catch (e) { console.warn("[CeEmbedSession] mountSidebar failed", e); } diff --git a/frontend/src/widgets/embed/embedMount.test.ts b/frontend/src/widgets/embed/embedMount.test.ts index 4210409..a09cf6a 100644 --- a/frontend/src/widgets/embed/embedMount.test.ts +++ b/frontend/src/widgets/embed/embedMount.test.ts @@ -154,6 +154,32 @@ test("isSwitchbayReservedApi protects control-plane paths", () => { assert.equal(isSwitchbayReservedApi("/api/workspaces/switch"), true); assert.equal(isSwitchbayReservedApi("/api/tree"), true); assert.equal(isSwitchbayReservedApi("/api/llm/providers"), true); + // Settings power controls must stay on the Switchbay daemon while an + // embed fetch shim is installed (otherwise Restart/Update toast "not found" + // from okstratr/CE via /embed/*/api/restart). + assert.equal(isSwitchbayReservedApi("/api/restart"), true); + assert.equal(isSwitchbayReservedApi("/api/update"), true); + assert.equal(isSwitchbayReservedApi("/api/update/check"), true); + assert.equal(isSwitchbayReservedApi("/api/quit"), true); + assert.equal(isSwitchbayReservedApi("/api/versions"), true); + assert.equal(isSwitchbayReservedApi("/api/admin-policy"), true); + // Same class: other Switchbay-owned chrome/settings paths that Settings or + // the shell call while a Graph/Agents embed fetch shim is installed. + assert.equal(isSwitchbayReservedApi("/api/fs/reveal"), true); + assert.equal(isSwitchbayReservedApi("/api/file"), true); + assert.equal(isSwitchbayReservedApi("/api/file-routes"), true); + assert.equal(isSwitchbayReservedApi("/api/tabs/terminal"), true); + assert.equal(isSwitchbayReservedApi("/api/runs/active"), true); + assert.equal(isSwitchbayReservedApi("/api/permission/pending"), true); + assert.equal(isSwitchbayReservedApi("/api/okstratr/harness"), true); + assert.equal(isSwitchbayReservedApi("/api/localllm/status"), true); + assert.equal(isSwitchbayReservedApi("/api/easter/thrusters"), true); + assert.equal(isSwitchbayReservedApi("/api/report-packages/open"), true); + assert.equal(isSwitchbayReservedApi("/api/micro-edits/model"), true); + assert.equal(isSwitchbayReservedApi("/api/walkthrough/status"), true); + assert.equal(isSwitchbayReservedApi("/api/share/status"), true); + assert.equal(isSwitchbayReservedApi("/api/web-policy"), true); + // Skill-owned paths stay remappable onto /embed/* assert.equal(isSwitchbayReservedApi("/api/page?path=x"), false); assert.equal(isSwitchbayReservedApi("/api/vault/foo"), false); assert.equal(isSwitchbayReservedApi("/api/desk/status"), false); @@ -187,6 +213,13 @@ test("embedFetchShimScript rewrites data.json onto public base", () => { void g.fetch("/api/core-skills/status"); void g.fetch("/api/tree"); void g.fetch("/api/llm/providers"); + void g.fetch("/api/restart", { method: "POST" }); + void g.fetch("/api/update", { method: "POST" }); + void g.fetch("/api/quit", { method: "POST" }); + void g.fetch("/api/fs/reveal", { method: "POST" }); + void g.fetch("/api/runs/active"); + void g.fetch("/api/easter/thrusters", { method: "POST" }); + void g.fetch("/api/okstratr/harness"); assert.equal(calls[0], "/embed/ce/data.json"); assert.equal(calls[1], "/embed/ce/data.json?t=1"); assert.equal(calls[2], "/embed/ce/api/page?path=notes/x.md"); @@ -195,6 +228,13 @@ test("embedFetchShimScript rewrites data.json onto public base", () => { assert.equal(calls[5], "/api/core-skills/status"); assert.equal(calls[6], "/api/tree"); assert.equal(calls[7], "/api/llm/providers"); + assert.equal(calls[8], "/api/restart"); + assert.equal(calls[9], "/api/update"); + assert.equal(calls[10], "/api/quit"); + assert.equal(calls[11], "/api/fs/reveal"); + assert.equal(calls[12], "/api/runs/active"); + assert.equal(calls[13], "/api/easter/thrusters"); + assert.equal(calls[14], "/api/okstratr/harness"); // Release restores fetch assert.equal(typeof g.window.__syEmbedReleaseFetch, "function"); g.window.__syEmbedReleaseFetch(); diff --git a/frontend/src/widgets/embed/embedMount.ts b/frontend/src/widgets/embed/embedMount.ts index f65d21b..14ccf15 100644 --- a/frontend/src/widgets/embed/embedMount.ts +++ b/frontend/src/widgets/embed/embedMount.ts @@ -260,47 +260,109 @@ export function prepareEmbedHtml( * shell unable to talk to the daemon while Graph is open. */ export const SWITCHBAY_API_RESERVED_PREFIXES = [ + // Shell chrome / settings / lifecycle (must never hit /embed/*) "/api/settings", + "/api/admin-policy", + "/api/restart", + "/api/update", + "/api/quit", + "/api/versions", + "/api/version", + "/api/health", + "/api/auth", + "/api/config", + "/api/walkthrough", + "/api/share", + "/api/web-policy", + "/api/history", + "/api/user-tabs", + "/api/tabs", + "/api/ui", + // Workspace / graph SSOT / core skills + "/api/workspaces", "/api/core-skills", "/api/graph", - "/api/workspaces", - "/api/desks", "/api/mode", - "/api/files", + "/api/desks", "/api/tree", + "/api/files", + "/api/file", + "/api/file-routes", + "/api/fs", + "/api/sources", + "/api/ingest", + "/api/watch-folders", + "/api/curation", + "/api/curator-profile", + // Chat / rail / agents "/api/llm", - "/api/rail", - "/api/threads", - "/api/packs", - "/api/pack", - "/api/auth", - "/api/health", - "/api/version", - "/api/config", "/api/models", "/api/providers", + "/api/rail", + "/api/threads", + "/api/runs", + "/api/digest", + "/api/decisions", + "/api/proposals", + "/api/permission", + "/api/provider-retry", + "/api/chat", + "/api/verbs", + "/api/shell", + "/api/action-buttons", + "/api/ce-action", + "/api/tools", + "/api/agent_rules", + "/api/command_palettes", + "/api/skills", + "/api/skill", "/api/orchestration", - "/api/micro", "/api/schedules", "/api/projects", + // Packs / MCP / local models / okstratr harness (Switchbay host routes) + "/api/packs", + "/api/pack", + "/api/mcp-servers", + "/api/localllm", + "/api/local-models", + "/api/okstratr", + "/api/copilot", + "/api/micro-edits", + "/api/micro", + // Surfaces Switchbay owns (not CE /api/page) "/api/library", + "/api/report-packages", "/api/report", "/api/reports", + "/api/reviews", "/api/sketch", "/api/sketches", "/api/sheet", + "/api/table", "/api/duckdb", + "/api/db", "/api/vega", + "/api/plot", + "/api/plots", "/api/terminal", "/api/pty", - "/api/ws", - "/api/events", - "/api/notify", + "/api/slideshows", + "/api/worksheets", + "/api/decks", + "/api/analyses", + "/api/analysis", + "/api/comms", + "/api/streams", "/api/pasteboard", "/api/clipboard", "/api/owid", + "/api/easter", "/api/thrusters", "/api/intro", + "/api/split", + "/api/ws", + "/api/events", + "/api/notify", ] as const; /** True when a path is a Switchbay daemon route (do not remap onto /embed/*). */ diff --git a/pyproject.toml b/pyproject.toml index 64efa8d..e8580fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "switchbay" -version = "0.13.0" +version = "0.13.1" description = "Local single-user workbench over knowledge bases — a driven second brain." requires-python = ">=3.11" authors = [{ name = "Benjamin Smith" }] diff --git a/src/switchbay/__init__.py b/src/switchbay/__init__.py index 70a9c1b..810dca7 100644 --- a/src/switchbay/__init__.py +++ b/src/switchbay/__init__.py @@ -4,7 +4,7 @@ # checker both read this, so drift here makes a current install look # stale and offers an "update" to a release it is already past. # tests/unit/test_version_sync.py fails the build if the two diverge. -__version__ = "0.13.0" +__version__ = "0.13.1" # Use the OS certificate store for HTTPS (corporate TLS proxies, custom # CAs). Must run before any aiohttp ClientSession creates an SSL context diff --git a/uv.lock b/uv.lock index bfbf92d..8c063c4 100644 --- a/uv.lock +++ b/uv.lock @@ -2515,7 +2515,7 @@ wheels = [ [[package]] name = "switchbay" -version = "0.13.0" +version = "0.13.1" source = { virtual = "." } dependencies = [ { name = "aiohttp" },