diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27fb0da..da7f446 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.0.23] - 2026-07-28
+
+### Added
+
+- Admins now receive a one-time **Later** / **Restart Now** prompt when a
+ desktop update has finished downloading and verification, without
+ needing to open Profile or manually check first.
+- Fresh macOS and Windows installations can connect to an existing 1Helm
+ workspace with the clean `[workspace].1helm.com` gateway or its alternate
+ HTTPS URL flow. **New User?** reveals the explicit option to set the current
+ PC up as a new 1Helm server, while configured desktop servers continue to
+ open their existing local workspace normally and Linux stays headless. A
+ client-only desktop does not create a second local server or server login
+ item behind the connection screen.
+- The live Routes graphic includes one collapsed **Custom** provider node and
+ illuminates its line when any custom OpenAI-compatible endpoint handles a
+ request, while the request details retain the actual endpoint name.
+
+### Fixed
+
+- Presentations fit the entire dotted printable boundary into view whenever a
+ deck opens or a slide is selected, created, or duplicated, without changing
+ the slide's printable dimensions, content, persistence, or PDF export.
+- The Cowork agent is available from a section or nested folder before a file
+ is opened, and a new chat receives that exact `/workspace` folder path just
+ as file-scoped chats receive their exact file path.
+- Long Cowork Code files scroll inside their finite editor viewport instead of
+ extending below the visible canvas.
+- The Android and iOS connection shell releases its native splash after its
+ first paint, uses the real 1Helm artwork, and defaults to the same clean
+ workspace-name connection flow.
+
## [0.0.22] - 2026-07-28
### Added
@@ -700,7 +732,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
notarization, stapled tickets, Gatekeeper verification, persistent
Application Support, and isolated Apple container machines.
-[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.22...HEAD
+[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.23...HEAD
+[0.0.23]: https://github.com/gitcommit90/1Helm/compare/v0.0.22...v0.0.23
[0.0.22]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.22
[0.0.21]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.21
[0.0.20]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.20
diff --git a/README.md b/README.md
index 53f1ee7..4ae9369 100644
--- a/README.md
+++ b/README.md
@@ -307,7 +307,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. |
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
-| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.22` | Versioned channel-machine image contract. |
+| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.23` | Versioned channel-machine image contract. |
### Agent-first JSON CLI
diff --git a/desktop/gateway.html b/desktop/gateway.html
new file mode 100644
index 0000000..e7d04f8
--- /dev/null
+++ b/desktop/gateway.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+ Connect to 1Helm
+
+
+
+
+
+
Connect to 1Helm
+
Enter your workspace name.
+
+
+
+
+
+ Your saved workspace opens here next time. Other links stay outside the app.
+
+
+
+
diff --git a/desktop/main.cjs b/desktop/main.cjs
index 073c615..5b38adf 100644
--- a/desktop/main.cjs
+++ b/desktop/main.cjs
@@ -8,6 +8,7 @@ const crypto = require("node:crypto");
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const { createNativeUpdateService } = require("./updater.cjs");
+const { allowedRemoteUrl, desktopGatewayAction, isHostedWorkspaceOrigin, normalizeRemoteOrigin } = require("./workspace-target.cjs");
const LOOPBACK = "127.0.0.1";
let mainWindow = null;
@@ -16,6 +17,23 @@ let localOrigin = "";
let quitting = false;
let hostUpdateService = null;
const remoteWorkspacePath = () => path.join(app.getPath("userData"), "remote-workspace");
+const desktopModePath = () => path.join(app.getPath("userData"), "desktop-mode");
+const localDatabasePath = () => path.join(app.getPath("userData"), "ctrl-pane.db");
+
+function desktopMode() {
+ try {
+ const mode = fs.readFileSync(desktopModePath(), "utf8").trim();
+ if (["client", "server"].includes(mode)) return mode;
+ } catch { /* older installations have no explicit mode */ }
+ if (fs.existsSync(remoteWorkspacePath())) return "client";
+ if (fs.existsSync(localDatabasePath())) return "server";
+ return "choose";
+}
+
+function rememberDesktopMode(mode) {
+ if (!["client", "server"].includes(mode)) return;
+ fs.writeFileSync(desktopModePath(), `${mode}\n`, { mode: 0o600 });
+}
function handleSquirrelEvent() {
if (process.platform !== "win32") return false;
@@ -38,17 +56,20 @@ function handleSquirrelEvent() {
}
function preferredWorkspaceOrigin() {
+ if (desktopMode() !== "client") return localOrigin;
try {
const value = fs.readFileSync(remoteWorkspacePath(), "utf8").trim();
- return allowedTeamUrl(value) ? new URL(value).origin : localOrigin;
+ return normalizeRemoteOrigin(value);
} catch {
- return localOrigin;
+ return "";
}
}
function rememberTeamUrl(raw) {
- if (!allowedTeamUrl(raw)) return;
- fs.writeFileSync(remoteWorkspacePath(), new URL(raw).origin + "\n", { mode: 0o600 });
+ const origin = normalizeRemoteOrigin(raw);
+ if (!origin) return;
+ fs.writeFileSync(remoteWorkspacePath(), origin + "\n", { mode: 0o600 });
+ rememberDesktopMode("client");
}
process.on("1helm-removal-prepared", () => {
@@ -117,6 +138,10 @@ function keepSkipperAvailable() {
app.setLoginItemSettings({ openAtLogin: true, type: "mainAppService" });
}
+function stopAutomaticServerStartup() {
+ app.setLoginItemSettings({ openAtLogin: false, type: "mainAppService" });
+}
+
function prepareWindowsWslDataRoot() {
if (process.platform !== "win32") return;
// Per-channel virtual disks stay outside the replaceable application
@@ -134,15 +159,60 @@ function allowedLocalUrl(raw) {
}
function allowedTeamUrl(raw) {
+ return allowedRemoteUrl(raw, preferredWorkspaceOrigin() === localOrigin ? "" : preferredWorkspaceOrigin());
+}
+
+const allowedAppUrl = (raw) => allowedLocalUrl(raw) || allowedTeamUrl(raw);
+
+async function connectRemoteWorkspace(window, origin) {
try {
- const url = new URL(raw);
- return url.protocol === "https:" && /^[a-z0-9](?:[a-z0-9-]{1,46}[a-z0-9])?\.1helm\.com$/i.test(url.hostname) && !["demo.1helm.com", "provision.1helm.com"].includes(url.hostname.toLowerCase());
- } catch {
- return false;
+ const response = await fetch(`${origin}/api/mobile/compatibility`, { signal: AbortSignal.timeout(15_000) });
+ const result = await response.json().catch(() => ({}));
+ if (!response.ok || result.product !== "1Helm" || result.mobile_api !== 1) throw new Error("That address is not a compatible 1Helm instance.");
+ if (!result.has_users || !result.setup_complete) throw new Error("Finish setting up this 1Helm instance before connecting the app.");
+ rememberTeamUrl(origin);
+ await window.loadURL(origin);
+ } catch (error) {
+ await loadDesktopGateway(window, { origin, error: error instanceof Error ? error.message : "Could not connect to this instance." });
}
}
-const allowedAppUrl = (raw) => allowedLocalUrl(raw) || allowedTeamUrl(raw);
+function loadDesktopGateway(window, state = {}) {
+ return window.loadFile(path.join(__dirname, "gateway.html"), { query: {
+ ...(state.origin ? { origin: state.origin, custom: isHostedWorkspaceOrigin(state.origin) ? "0" : "1" } : {}),
+ ...(state.error ? { error: state.error } : {}),
+ } });
+}
+
+async function loadInitialWorkspace(window) {
+ const mode = desktopMode();
+ if (mode === "server" || process.platform === "linux") { await window.loadURL(localOrigin); return; }
+ if (mode === "client") {
+ const preferred = preferredWorkspaceOrigin();
+ if (preferred) { await window.loadURL(preferred); return; }
+ }
+ await loadDesktopGateway(window);
+}
+
+async function startServerMode(window) {
+ try {
+ keepSkipperAvailable();
+ prepareWindowsWslDataRoot();
+ if (!localOrigin) await startLocalRuntime();
+ rememberDesktopMode("server");
+ hostUpdateService?.schedule();
+ await window.loadURL(localOrigin);
+ } catch (error) {
+ stopAutomaticServerStartup();
+ await dialog.showMessageBox({
+ type: "error",
+ title: "1Helm could not start",
+ message: `The local 1Helm runtime could not start on this ${process.platform === "win32" ? "Windows PC" : "Mac"}.`,
+ detail: error instanceof Error ? error.stack || error.message : String(error),
+ });
+ await loadDesktopGateway(window, { error: "This PC could not start its local 1Helm server." });
+ }
+}
function microphonePermissionAllowed(webContents, permission, details = {}) {
const pageUrl = webContents?.getURL?.() || "";
@@ -202,6 +272,13 @@ function createWindow(showWhenReady = true) {
return { action: "deny" };
});
window.webContents.on("will-navigate", (event, url) => {
+ const gatewayAction = desktopGatewayAction(url);
+ if (gatewayAction) {
+ event.preventDefault();
+ if (gatewayAction.type === "setup") void startServerMode(window);
+ else void connectRemoteWorkspace(window, gatewayAction.origin);
+ return;
+ }
if (allowedTeamUrl(url)) { rememberTeamUrl(url); return; }
if (allowedLocalUrl(url)) return;
event.preventDefault();
@@ -211,7 +288,7 @@ function createWindow(showWhenReady = true) {
});
if (showWhenReady) window.once("ready-to-show", () => window.show());
window.on("closed", () => { if (mainWindow === window) mainWindow = null; });
- void window.loadURL(preferredWorkspaceOrigin());
+ void loadInitialWorkspace(window);
mainWindow = window;
}
@@ -254,8 +331,6 @@ if (handleSquirrelEvent()) {
});
try {
removeLegacyWakeLaunchAgent();
- keepSkipperAvailable();
- prepareWindowsWslDataRoot();
hostUpdateService = createNativeUpdateService({ app, autoUpdater });
hostUpdateService.initialize();
globalThis[Symbol.for("1helm.nativeUpdater")] = {
@@ -264,8 +339,15 @@ if (handleSquirrelEvent()) {
install: hostUpdateService.install,
};
process.on("1helm-native-update-ready", () => { hostUpdateService?.commitInstall(); });
- await startLocalRuntime();
- hostUpdateService.schedule();
+ const mode = desktopMode();
+ if (mode === "server" || process.platform === "linux") {
+ keepSkipperAvailable();
+ prepareWindowsWslDataRoot();
+ await startLocalRuntime();
+ hostUpdateService.schedule();
+ } else {
+ stopAutomaticServerStartup();
+ }
const login = app.getLoginItemSettings({ type: "mainAppService" });
createWindow(!login.wasOpenedAtLogin && !process.argv.includes("--1helm-background"));
} catch (error) {
@@ -279,7 +361,7 @@ if (handleSquirrelEvent()) {
}
});
- app.on("activate", () => { if (!mainWindow && localOrigin) createWindow(); });
+ app.on("activate", () => { if (!mainWindow) createWindow(); });
app.on("window-all-closed", () => {
// On macOS 1Helm remains the native scheduler/fleet manager until Cmd-Q.
if (process.platform !== "darwin") app.quit();
@@ -290,6 +372,6 @@ if (handleSquirrelEvent()) {
quitting = true;
// Explicit Quit is respected; the signed main-app login service starts the
// local control plane hidden again at the next user login.
- process.emit("SIGTERM", "SIGTERM");
+ if (localOrigin) process.emit("SIGTERM", "SIGTERM");
});
}
diff --git a/desktop/workspace-target.cjs b/desktop/workspace-target.cjs
new file mode 100644
index 0000000..e71a842
--- /dev/null
+++ b/desktop/workspace-target.cjs
@@ -0,0 +1,58 @@
+"use strict";
+
+const WORKSPACE_HOST = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.1helm\.com$/i;
+const RESERVED_WORKSPACES = new Set(["demo.1helm.com", "provision.1helm.com"]);
+const DESKTOP_ACTION_ORIGIN = "https://desktop-action.1helm.invalid";
+const CONNECT_PATH = "/connect";
+const LOCAL_SETUP_PATH = "/setup";
+
+function normalizeRemoteOrigin(raw) {
+ try {
+ const url = new URL(String(raw || "").trim());
+ if (url.protocol !== "https:" || url.username || url.password) return "";
+ return url.origin;
+ } catch {
+ return "";
+ }
+}
+
+function isHostedWorkspaceOrigin(raw) {
+ const origin = normalizeRemoteOrigin(raw);
+ if (!origin) return false;
+ const url = new URL(origin);
+ return !url.port && WORKSPACE_HOST.test(url.hostname) && !RESERVED_WORKSPACES.has(url.hostname.toLowerCase());
+}
+
+function allowedRemoteUrl(raw, selectedOrigin = "") {
+ try {
+ const url = new URL(raw);
+ if (url.protocol !== "https:") return false;
+ if (isHostedWorkspaceOrigin(url.origin)) return true;
+ return Boolean(selectedOrigin) && url.origin === normalizeRemoteOrigin(selectedOrigin);
+ } catch {
+ return false;
+ }
+}
+
+function desktopGatewayAction(raw) {
+ try {
+ const url = new URL(raw);
+ if (url.origin !== DESKTOP_ACTION_ORIGIN || url.hash || url.username || url.password) return null;
+ if (url.pathname === LOCAL_SETUP_PATH && !url.search) return { type: "setup" };
+ if (url.pathname !== CONNECT_PATH) return null;
+ const origin = normalizeRemoteOrigin(url.searchParams.get("origin"));
+ return origin ? { type: "connect", origin } : null;
+ } catch {
+ return null;
+ }
+}
+
+module.exports = {
+ CONNECT_PATH,
+ DESKTOP_ACTION_ORIGIN,
+ LOCAL_SETUP_PATH,
+ allowedRemoteUrl,
+ desktopGatewayAction,
+ isHostedWorkspaceOrigin,
+ normalizeRemoteOrigin,
+};
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index a0f3f69..7799aa0 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -357,7 +357,9 @@ Signed Mac releases are unique patch versions. Profile → Check for updates
always operates on the machine hosting the active 1Helm instance. In the native
Mac app, Electron downloads and verifies a notarized update ZIP on that Mac and
offers **Restart & install** only when it is ready. It does not navigate the
-browsing device to a DMG.
+browsing device to a DMG. An admin who is signed in when that verified download
+finishes receives a one-time **Later** / **Restart Now** prompt; choosing
+**Later** leaves the same update ready in Profile.
The standard Linux systemd install uses a root-owned updater. 1Helm can request
that one fixed operation, but cannot choose an arbitrary URL, command, or target
diff --git a/mobile-gateway/1helm-logo.png b/mobile-gateway/1helm-logo.png
new file mode 100644
index 0000000..8323e6e
Binary files /dev/null and b/mobile-gateway/1helm-logo.png differ
diff --git a/mobile-gateway/error.html b/mobile-gateway/error.html
index c49b130..9063c66 100644
--- a/mobile-gateway/error.html
+++ b/mobile-gateway/error.html
@@ -4,5 +4,5 @@
1Helm unavailable
-
Instance unavailable
1Helm could not reach the selected instance. Check its connection, retry, or choose a different instance.
+
Instance unavailable
1Helm could not reach the selected instance. Check its connection, retry, or choose a different instance.