- {PRODUCT_MODE === "cloud" && !EMBEDDED_CLOUD && hostSwitcher}
+ {PRODUCT_MODE === "cloud" && hostSwitcher}
- )
+
}
mobileNavigation={
- EMBEDDED_CLOUD ? undefined : (
-
- )
+
}
sessionList={destination === "sessions" ? list : undefined}
showSessionRail={destination === "sessions"}
- showMobileNavigation={!EMBEDDED_CLOUD && (destination !== "sessions" || activeSessionId === undefined)}
+ showMobileNavigation={destination !== "sessions" || activeSessionId === undefined}
sessionsOpen={destination === "sessions" && sessionsOpen}
conversationActive={destination === "sessions" && activeSessionId !== undefined}
onHideSessions={() => setSessionsOpen(false)}
diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts
index 6223d2b..daac769 100644
--- a/packages/web/src/config.ts
+++ b/packages/web/src/config.ts
@@ -6,7 +6,3 @@ export type ProductMode = "standalone" | "cloud";
/** Product behavior is explicit. A URL prefix is routing, not a reliable statement about the product mode. */
export const PRODUCT_MODE: ProductMode =
(import.meta.env.VITE_PRODUCT_MODE as string | undefined) === "cloud" ? "cloud" : "standalone";
-
-/** Same-origin Cloud workbench embeds the encrypted terminal without duplicating global navigation. */
-export const EMBEDDED_CLOUD =
- PRODUCT_MODE === "cloud" && new URLSearchParams(window.location.search).get("embed") === "1";
diff --git a/site/scripts/hosted-product-audit.mjs b/site/scripts/hosted-product-audit.mjs
index fc200b1..5320847 100644
--- a/site/scripts/hosted-product-audit.mjs
+++ b/site/scripts/hosted-product-audit.mjs
@@ -90,21 +90,21 @@ const findings = [];
const screenshots = [];
const stepContracts = new Map(
[
- ["01-sessions-desktop", "/app/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
- ["02-automations-desktop", "/app/automations", "Automations — RoamCode", "Automations", "automations"],
+ ["01-sessions-desktop", "/terminal/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
+ ["02-automations-desktop", "/terminal/automations", "Automations — RoamCode", "Automations", "automations"],
["03-agents-desktop", "/app/agents", "Agents — RoamCode", "Agents", "agents"],
["04-people-desktop", "/app/people", "People & Access — RoamCode", "People & Access", "people"],
["05-account-desktop", "/app/account", "Account — RoamCode", "Account", "account"],
- ["06-sessions-mobile", "/app/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
+ ["06-sessions-mobile", "/terminal/sessions", "Sessions — RoamCode", "Sessions", null],
["07-agents-mobile", "/app/agents", "Agents — RoamCode", "Agents", "agents"],
["08-sign-in-desktop", "/app", "Sign in — RoamCode", "Welcome back", null],
["09-sign-up-desktop", "/app", "Create account — RoamCode", "Create your RoamCode account", null],
- ["10-sessions-desktop", "/app/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
- ["11-automations-desktop", "/app/automations", "Automations — RoamCode", "Automations", "automations"],
+ ["10-sessions-desktop", "/terminal/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
+ ["11-automations-desktop", "/terminal/automations", "Automations — RoamCode", "Automations", "automations"],
["12-agents-desktop", "/app/agents", "Agents — RoamCode", "Agents", "agents"],
["13-people-desktop", "/app/people", "People & Access — RoamCode", "People & Access", "people"],
["14-account-desktop", "/app/account", "Account — RoamCode", "Account", "account"],
- ["15-sessions-narrow", "/app/sessions", "Sessions — RoamCode", "Sessions", "sessions"],
+ ["15-sessions-narrow", "/terminal/sessions", "Sessions — RoamCode", "Sessions", null],
[
"16-organization-desktop",
"/app/organization",
@@ -423,9 +423,9 @@ function terminalWorkbenchFixture(destination) {
const detail = automation
? `
Release readiness Next run · Mon 09:00
TRIGGER Weekdays at 09:00 · Europe/Istanbul RUNS ON Studio Mac · /remote-coder ACTION Open an inspectable Codex session
`
: `
Release candidate Codex · connected
~/remote-coder main \n\n› Review the release candidate and run the complete verification suite.\n\n✓ Typecheck, unit tests, and production build passed.\n█ `;
- return Buffer.from(`
${title} `);
+ return Buffer.from(`
${title} — RoamCode `);
}
function staticFixture(url) {
@@ -523,7 +523,7 @@ function auditExpression() {
minimum: minimumFor(element),
};
};
- const heading = document.querySelector("h1");
+ const heading = Array.from(document.querySelectorAll("h1")).find(visible);
const headingRect = heading?.getBoundingClientRect();
return {
title: document.title,
@@ -540,6 +540,7 @@ function auditExpression() {
mainHeading: heading?.textContent?.trim(),
mainHeadingTop: headingRect ? Math.round(headingRect.top) : null,
mainHeadingBottom: headingRect ? Math.round(headingRect.bottom) : null,
+ iframeCount: document.querySelectorAll("iframe").length,
bodyText: document.body.innerText.slice(0, 500),
};
})()`;
@@ -561,6 +562,7 @@ function recordEvidence(browser, step, evidence) {
if (JSON.stringify(evidence.activeRoutes) !== JSON.stringify(expectedRoutes)) {
failures.push(`active routes ${JSON.stringify(evidence.activeRoutes)} != ${JSON.stringify(expectedRoutes)}`);
}
+ if (evidence.iframeCount !== 0) failures.push(`page contains ${evidence.iframeCount} iframe(s)`);
if (Math.abs(evidence.scrollY) > 1) failures.push(`page retained scroll position ${evidence.scrollY}`);
if (
typeof evidence.mainHeadingTop !== "number" ||
@@ -573,7 +575,7 @@ function recordEvidence(browser, step, evidence) {
}
async function waitForChromePage(page, heading) {
- await page.locator("h1", { hasText: heading }).waitFor({ state: "visible", timeout: 15_000 });
+ await page.locator("h1:visible", { hasText: heading }).waitFor({ state: "visible", timeout: 15_000 });
await page.waitForFunction(
() =>
Array.from(document.querySelectorAll("iframe")).every(
@@ -649,7 +651,7 @@ async function runChrome() {
recordRuntimeSignals(mobile, errors, "mobile ");
await mobile.goto(`${origin}/app/sessions`, { waitUntil: "domcontentloaded" });
await captureChrome(mobile, "06-sessions-mobile", "Sessions");
- await mobile.getByRole("link", { name: "Agents", exact: true }).last().click();
+ await mobile.goto(`${origin}/app/agents`, { waitUntil: "domcontentloaded" });
await captureChrome(mobile, "07-agents-mobile", "Agents");
await mobile.locator("a.rc-cloud-avatar:visible").click();
await mobile.locator("a.rc-cloud-context-manage:visible").click();
diff --git a/site/src/app-shell.css b/site/src/app-shell.css
index 7827b31..58ff3b1 100644
--- a/site/src/app-shell.css
+++ b/site/src/app-shell.css
@@ -1542,106 +1542,121 @@
align-self: center;
}
-.rc-cloud-workbench {
- width: 100%;
+.rc-cloud-product-handoff {
min-height: calc(100dvh - 48px);
display: grid;
- grid-template-rows: auto auto minmax(560px, 1fr);
- gap: 12px;
+ align-content: center;
+ justify-items: center;
+ gap: 10px;
+ padding: 32px;
+ text-align: center;
}
-.rc-cloud-workbench-head {
- display: flex;
- align-items: end;
- justify-content: space-between;
- gap: 20px;
+.rc-cloud-product-handoff .rc-cloud-loader {
+ margin-bottom: 10px;
}
-.rc-cloud-workbench-head > div {
+.rc-cloud-product-handoff h1 {
+ margin: 0;
+ color: var(--rc-text);
+ font-family: var(--display);
+ font-size: clamp(28px, 4vw, 44px);
+ letter-spacing: -0.045em;
+}
+
+.rc-cloud-product-handoff p {
+ max-width: 460px;
+ margin: 0 0 10px;
+ color: var(--rc-muted);
+}
+
+.rc-cloud-node-launcher {
display: grid;
- gap: 5px;
+ grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.3fr);
+ gap: 28px;
+ padding: 28px;
+ border: 1px solid var(--rc-line);
+ border-radius: 18px;
+ background: var(--rc-panel);
+}
+
+.rc-cloud-node-launcher > div:first-child {
+ align-content: start;
+ display: grid;
+ gap: 8px;
}
-.rc-cloud-workbench-head h1 {
+.rc-cloud-node-launcher h2,
+.rc-cloud-node-launcher p {
margin: 0;
+}
+
+.rc-cloud-node-launcher h2 {
color: var(--rc-text);
font-family: var(--display);
- font-size: clamp(24px, 3vw, 38px);
+ font-size: clamp(22px, 3vw, 34px);
letter-spacing: -0.04em;
}
-.rc-cloud-workbench-head p {
- margin: 0;
+.rc-cloud-node-launcher p {
color: var(--rc-muted);
- font-size: 12px;
}
-.rc-cloud-workbench-nodes {
- display: flex;
- gap: 7px;
- overflow-x: auto;
- padding-bottom: 2px;
+.rc-cloud-node-launcher-grid {
+ display: grid;
+ gap: 8px;
}
-.rc-cloud-workbench-node {
- min-width: 150px;
+.rc-cloud-node-launcher-grid a {
+ min-width: 0;
display: grid;
- grid-template-columns: auto 1fr;
+ grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
- gap: 2px 8px;
- padding: 9px 11px;
+ gap: 3px 10px;
+ padding: 14px;
border: 1px solid var(--rc-line);
- border-radius: 11px;
+ border-radius: 12px;
background: rgba(255, 255, 255, 0.025);
- color: var(--rc-muted);
- text-align: left;
- cursor: pointer;
+ color: var(--rc-text);
+ text-decoration: none;
+ transition:
+ border-color 140ms ease,
+ background 140ms ease,
+ transform 140ms ease;
}
-.rc-cloud-workbench-node .rc-cloud-dot {
+.rc-cloud-node-launcher-grid a:hover {
+ border-color: var(--rc-line-strong);
+ background: rgba(255, 255, 255, 0.045);
+ transform: translateY(-1px);
+}
+
+.rc-cloud-node-launcher-grid a:focus-visible {
+ outline: 2px solid var(--rc-cyan);
+ outline-offset: 2px;
+}
+
+.rc-cloud-node-launcher-grid .rc-cloud-dot {
grid-row: 1 / 3;
}
-.rc-cloud-workbench-node b {
+.rc-cloud-node-launcher-grid strong,
+.rc-cloud-node-launcher-grid small {
overflow: hidden;
- color: var(--rc-text);
- font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
-.rc-cloud-workbench-node small {
+.rc-cloud-node-launcher-grid small {
color: var(--rc-faint);
- font-size: 9px;
-}
-
-.rc-cloud-workbench-node.is-active {
- border-color: rgba(92, 225, 230, 0.45);
- background: rgba(92, 225, 230, 0.08);
- box-shadow: inset 0 0 0 1px rgba(92, 225, 230, 0.06);
-}
-
-.rc-cloud-workbench-node:disabled {
- cursor: not-allowed;
- opacity: 0.48;
-}
-
-.rc-cloud-workbench-frame {
- min-height: 0;
- overflow: hidden;
- border: 1px solid var(--rc-line-strong);
- border-radius: 17px;
- background: #08090b;
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
+ font-size: 10px;
}
-.rc-cloud-workbench-frame iframe {
- width: 100%;
- height: 100%;
- min-height: 560px;
- display: block;
- border: 0;
- background: #08090b;
+.rc-cloud-node-launcher-grid b {
+ grid-column: 3;
+ grid-row: 1 / 3;
+ color: var(--rc-muted);
+ font-size: 11px;
}
.rc-cloud-profile,
@@ -2335,19 +2350,24 @@
width: 100%;
}
- .rc-cloud-workbench {
- min-height: calc(100dvh - 124px);
- grid-template-rows: auto auto minmax(520px, 1fr);
+ .rc-cloud-product-handoff {
+ min-height: calc(100dvh - 136px);
+ padding: 28px 18px;
}
- .rc-cloud-workbench-head p {
- display: none;
+ .rc-cloud-node-launcher {
+ grid-template-columns: 1fr;
+ padding: 22px 18px;
+ }
+
+ .rc-cloud-node-launcher-grid a {
+ grid-template-columns: auto minmax(0, 1fr);
}
- .rc-cloud-workbench-frame,
- .rc-cloud-workbench-frame iframe {
- min-height: 520px;
- border-radius: 12px;
+ .rc-cloud-node-launcher-grid b {
+ grid-column: 2;
+ grid-row: 3;
+ margin-top: 4px;
}
.rc-cloud-consent-actions {
diff --git a/site/src/app-shell.test.ts b/site/src/app-shell.test.ts
index 51a1fcf..33eb5b8 100644
--- a/site/src/app-shell.test.ts
+++ b/site/src/app-shell.test.ts
@@ -324,6 +324,7 @@ describe("hosted account shell", () => {
let organizationCreated = false;
let organizationName = "Mühendislik Lab";
let nodeName = "Studio Mac";
+ const productHandoffs: string[] = [];
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(new URL(String(input), location.origin), init);
const url = new URL(request.url);
@@ -536,7 +537,7 @@ describe("hosted account shell", () => {
});
vi.stubGlobal("fetch", fetchMock);
- mountAccountShell();
+ mountAccountShell((href) => productHandoffs.push(href));
await vi.waitFor(() => {
expect(document.body.textContent).toContain("Connect the computer that will run your agents");
@@ -609,22 +610,21 @@ describe("hosted account shell", () => {
expect(document.activeElement).toBe(openNode);
document.querySelector
('.rc-cloud-primary--bottom [data-route="sessions"]')?.click();
- await vi.waitFor(() => expect(document.body.textContent).toContain("Choose the Node that owns the repository"));
- const sessionsWorkbench = document.querySelector('iframe[title="Sessions on Studio Mac"]');
- expect(sessionsWorkbench?.getAttribute("src")).toBe(
- `/terminal/sessions?enroll=${HOST_ID}&embed=1&context=${ORGANIZATION_ID}`,
+ await vi.waitFor(() =>
+ expect(productHandoffs.at(-1)).toBe(`/terminal/sessions?enroll=${HOST_ID}&context=${ORGANIZATION_ID}`),
+ );
+ expect(document.body.textContent).toContain("Opening Sessions");
+ expect(document.querySelector("iframe")).toBeNull();
+ expect(document.querySelector("a[data-product-handoff]")?.getAttribute("href")).toBe(
+ `/terminal/sessions?enroll=${HOST_ID}&context=${ORGANIZATION_ID}`,
);
- expect(document.querySelector('[aria-label="Execution Node"]')).not.toBeNull();
- expect(document.querySelector('a[aria-label^="Open Sessions on"]')).toBeNull();
document.querySelector('.rc-cloud-primary--bottom [data-route="automations"]')?.click();
- await vi.waitFor(() => expect(document.body.textContent).toContain("Choose the Node that will execute each Run"));
- const automationsWorkbench = document.querySelector('iframe[title="Automations on Studio Mac"]');
- expect(automationsWorkbench?.getAttribute("src")).toBe(
- `/terminal/automations?enroll=${HOST_ID}&embed=1&context=${ORGANIZATION_ID}`,
+ await vi.waitFor(() =>
+ expect(productHandoffs.at(-1)).toBe(`/terminal/automations?enroll=${HOST_ID}&context=${ORGANIZATION_ID}`),
);
- expect(document.querySelector('[aria-label="Execution Node"]')).not.toBeNull();
- expect(document.querySelector('a[aria-label^="Open Automations on"]')).toBeNull();
+ expect(document.body.textContent).toContain("Opening Automations");
+ expect(document.querySelector("iframe")).toBeNull();
expect(document.querySelector("#organization-dialog")).toBeNull();
document.querySelector('[data-route="organization"]')?.click();
@@ -958,6 +958,88 @@ describe("hosted account shell", () => {
expect(inventoryAttempts).toBe(2);
});
+ test("keeps multi-Node routing as a single explicit choice before opening the full product surface", async () => {
+ history.replaceState(null, "", "/app/sessions");
+ const secondHostId = "00000000-0000-4000-8000-000000000004";
+ const productHandoffs: string[] = [];
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
+ const request = input instanceof Request ? input : new Request(new URL(String(input), location.origin), init);
+ const url = new URL(request.url);
+ if (url.pathname === "/api/v1/meta/providers") {
+ return json({ email_password: true, passkey: false, github: false, google: false, mode: "local_dev" });
+ }
+ if (url.pathname === "/api/auth/get-session") {
+ return json({
+ session: { id: "session-1" },
+ user: { id: "user-1", name: "Ada", email: "ada@example.test", emailVerified: true },
+ });
+ }
+ if (url.pathname === "/api/v1/account/bootstrap") {
+ return json({
+ user: { id: "user-1", name: "Ada", email: "ada@example.test" },
+ contexts: [
+ {
+ id: PERSONAL_ID,
+ kind: "personal",
+ slug: "personal-user-1",
+ name: "Personal",
+ plan: "free",
+ role: "owner",
+ },
+ ],
+ });
+ }
+ if (url.pathname === `/api/v1/orgs/${PERSONAL_ID}/hosts`) {
+ return json({
+ hosts: [
+ { id: HOST_ID, name: "Studio Mac", slug: "studio-mac" },
+ { id: secondHostId, name: "Build Mac", slug: "build-mac" },
+ ].map((host) => ({
+ ...host,
+ organizationId: PERSONAL_ID,
+ status: "online",
+ tokenVersion: 1,
+ provisioningSagaId: `saga-${host.id}`,
+ agentVersion: "1.4.2",
+ lastSeenAt: "2026-07-19T00:00:00.000Z",
+ createdAt: "2026-07-18T00:00:00.000Z",
+ heartbeatState: "ready",
+ capabilities: ["terminal.v1", "relay.v1", "managed-device-enrollment.v1"],
+ revision: 1,
+ })),
+ });
+ }
+ if (
+ url.pathname === `/api/v1/hosts/${HOST_ID}/status` ||
+ url.pathname === `/api/v1/hosts/${secondHostId}/status`
+ ) {
+ return json({
+ host: {},
+ relay: {
+ status: { hostOnline: true, activeDevices: 1 },
+ route: { id: `route-${url.pathname}`, label: "Node", deviceCount: 1 },
+ connection: { path: "/v1/connect", protocolVersion: 1 },
+ },
+ });
+ }
+ throw new Error(`Unexpected request: ${request.method} ${url.pathname}`);
+ }),
+ );
+
+ mountAccountShell((href) => productHandoffs.push(href));
+
+ await vi.waitFor(() => expect(document.body.textContent).toContain("Where should Sessions open?"));
+ expect(productHandoffs).toEqual([]);
+ expect(document.querySelector("iframe")).toBeNull();
+ const choices = Array.from(document.querySelectorAll(".rc-cloud-node-launcher-grid a"));
+ expect(choices.map((choice) => choice.getAttribute("href"))).toEqual([
+ `/terminal/sessions?enroll=${HOST_ID}&context=${PERSONAL_ID}`,
+ `/terminal/sessions?enroll=${secondHostId}&context=${PERSONAL_ID}`,
+ ]);
+ });
+
test("does not report a Node offline when only its status request failed", async () => {
history.replaceState(null, "", "/app/sessions");
vi.stubGlobal(
diff --git a/site/src/app-shell.ts b/site/src/app-shell.ts
index fb7273a..8245897 100644
--- a/site/src/app-shell.ts
+++ b/site/src/app-shell.ts
@@ -19,6 +19,7 @@ import {
type ProductRoute =
"sessions" | "automations" | "agents" | "organization" | "account" | "people" | "activate" | "invite" | "reset";
+type ProductSurfaceNavigator = (href: string) => void;
type AuthMode = "sign-in" | "sign-up" | "reset-request";
type CloudIconName =
| "chevron-right"
@@ -578,6 +579,7 @@ function safeDocumentUrl(value: string): string | undefined {
class AccountShell {
private readonly root: HTMLElement;
+ private readonly navigateToProductSurface: ProductSurfaceNavigator;
private route = routeFromPath(location.pathname);
private authMode: AuthMode;
private providers: AuthProviders = {
@@ -616,7 +618,7 @@ class AccountShell {
private hostAccessError?: string;
private hostInventoryState: "idle" | "loading" | "ready" | "error" = "idle";
private hostInventoryError?: string;
- private productHostId?: string;
+ private pendingProductHandoff?: string;
private cloudDevices: CloudDevice[] = [];
private cloudDevicesState: "idle" | "loading" | "ready" | "error" = "idle";
private cloudDevicesError?: string;
@@ -651,7 +653,8 @@ class AccountShell {
private organizationDraft = { name: "", slug: "" };
private organizationSlugEdited = false;
- constructor() {
+ constructor(navigateToProductSurface: ProductSurfaceNavigator) {
+ this.navigateToProductSurface = navigateToProductSurface;
this.authMode = consumeRequestedAuthMode();
document.documentElement.classList.add("rc-account-mode");
document.body.replaceChildren();
@@ -851,9 +854,6 @@ class AccountShell {
});
this.hosts = hosts;
this.hostStatuses = hostStatuses;
- if (!this.productHostId || !hosts.some((host) => host.id === this.productHostId)) {
- this.productHostId = hosts.find((host) => this.hostIsOnline(host))?.id ?? hosts[0]?.id;
- }
this.members = members;
this.membersState = !shouldLoadMembers ? "idle" : membersResult.status === "fulfilled" ? "ready" : "error";
this.membersError =
@@ -1101,6 +1101,25 @@ class AccountShell {
queueMicrotask(() => dialog.querySelector('input[name="name"]')?.focus());
}
}
+ this.scheduleProductHandoff();
+ }
+
+ private scheduleProductHandoff(): void {
+ const link = this.root.querySelector("a[data-product-handoff]");
+ const href = link?.getAttribute("href");
+ if (!link || !href) {
+ this.pendingProductHandoff = undefined;
+ return;
+ }
+ if (this.pendingProductHandoff === href) return;
+ this.pendingProductHandoff = href;
+ queueMicrotask(() => {
+ if (!link.isConnected || this.root.querySelector("a[data-product-handoff]") !== link) {
+ if (this.pendingProductHandoff === href) this.pendingProductHandoff = undefined;
+ return;
+ }
+ this.navigateToProductSurface(href);
+ });
}
private renderContent(): string {
@@ -1477,9 +1496,8 @@ class AccountShell {
return `${requestLabel} `;
}
- private terminalDestination(destination: "sessions" | "automations", hostId: string, embedded = false): string {
+ private terminalDestination(destination: "sessions" | "automations", hostId: string): string {
const query = new URLSearchParams({ enroll: hostId });
- if (embedded) query.set("embed", "1");
if (this.context) query.set("context", this.context.id);
return `/terminal/${destination}?${query.toString()}`;
}
@@ -1506,22 +1524,25 @@ class AccountShell {
this.hostSupportsManagedEnrollment(host) &&
(this.context?.kind !== "organization" || this.hostGrantIsCurrent(this.hostAccess.get(host.id))),
);
- const selected = usableHosts.find((host) => host.id === this.productHostId) ?? usableHosts[0];
- const tabs = this.hosts
- .map((host) => {
- const usable = usableHosts.some((candidate) => candidate.id === host.id);
- const active = selected?.id === host.id;
- return `${escapeHtml(host.name)} ${usable ? "Ready" : this.hostAccessCopy(host)} `;
- })
- .join("");
+ const selected = usableHosts[0];
if (!selected) {
return `${this.pageHeader(eyebrow, title, copy, `Refresh `)}Access required No Node can open this workspace yet Online Nodes need a current browser grant and managed terminal capability.
Review Nodes `;
}
- const src = this.terminalDestination(destination, selected.id, true);
- return `
- ${escapeHtml(eyebrow)} ${escapeHtml(title)} ${escapeHtml(copy)}
Refresh
- ${tabs}
-
+ if (usableHosts.length > 1) {
+ return `${this.pageHeader(eyebrow, title, copy)}Execution Node Where should ${escapeHtml(title)} open? Choose the computer that owns this work. The selected Node opens as the full RoamCode workspace, never inside a second app frame.
`;
+ }
+ const href = this.terminalDestination(destination, selected.id);
+ return `
+
+ ${escapeHtml(selected.name)}
+ Opening ${escapeHtml(title)}
+ Loading the encrypted Node workspace as one RoamCode surface.
+ Open ${escapeHtml(title)}
`;
}
const statusUnknown = !noHosts && this.hosts.some((host) => !this.hostStatuses.has(host.id));
@@ -1918,19 +1939,6 @@ class AccountShell {
if (action === "retry-managed-host-devices") return this.reloadManagedHostDevices();
if (action === "retry-people") return this.refreshPeopleData();
if (action === "retry-organization-settings") return this.loadOrganizationSettings();
- if (action === "select-product-node") {
- const hostId = target.dataset.hostId;
- const host = this.hosts.find((candidate) => candidate.id === hostId);
- if (
- !host ||
- !this.hostIsOnline(host) ||
- !this.hostSupportsManagedEnrollment(host) ||
- (this.context?.kind === "organization" && !this.hostGrantIsCurrent(this.hostAccess.get(host.id)))
- )
- return;
- this.productHostId = host.id;
- return this.render();
- }
if (action === "prepare-member-remove") {
if (this.busy?.startsWith("member:")) return;
const userId = target.dataset.userId;
@@ -2789,6 +2797,8 @@ class AccountShell {
}
}
-export function mountAccountShell(): void {
- void new AccountShell().start();
+export function mountAccountShell(
+ navigateToProductSurface: ProductSurfaceNavigator = (href) => window.location.replace(href),
+): void {
+ void new AccountShell(navigateToProductSurface).start();
}