From 8b1af9708c103c857b6f86ab31764b23da53113f Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 22 Apr 2026 17:02:48 -0700 Subject: [PATCH 1/3] feat(canvas): default tier T3 and hide T1/T2 on SaaS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On SaaS every workspace gets its own EC2 VM — the Docker-sandbox distinction between T1 (sandboxed), T2 (standard Docker), and T3 (full host access) doesn't apply. A SaaS workspace is always a dedicated VM, which is "full access" by construction. Showing T1/T2 in that UI is a category error: users pick a sandbox level that has no effect on the actual EC2 machine they get. Changes: - tenant.ts: export isSaaSTenant() — returns true when canvas is served at .moleculesai.app (SSR-safe: false on server) - CreateWorkspaceDialog: when isSaaSTenant(), render only the T3 option, default tier=3, grid collapses to a single column. Label gets a " — dedicated VM" hint so the user knows what they're getting. On self-hosted the full T1/T2/T3 picker is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/CreateWorkspaceDialog.tsx | 41 ++++++++++++++----- canvas/src/lib/tenant.ts | 15 +++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/canvas/src/components/CreateWorkspaceDialog.tsx b/canvas/src/components/CreateWorkspaceDialog.tsx index 37e1231d..bcd8d040 100644 --- a/canvas/src/components/CreateWorkspaceDialog.tsx +++ b/canvas/src/components/CreateWorkspaceDialog.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState, useEffect, useRef, useCallback, useId } from "react"; +import { useState, useEffect, useRef, useCallback, useId, useMemo } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { api } from "@/lib/api"; +import { isSaaSTenant } from "@/lib/tenant"; interface WorkspaceOption { id: string; @@ -39,7 +40,6 @@ export function CreateWorkspaceButton() { const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [role, setRole] = useState(""); - const [tier, setTier] = useState(1); const [template, setTemplate] = useState(""); const [parentId, setParentId] = useState(""); const [budgetLimit, setBudgetLimit] = useState(""); @@ -51,13 +51,29 @@ export function CreateWorkspaceButton() { const [hermesProvider, setHermesProvider] = useState("anthropic"); const [hermesApiKey, setHermesApiKey] = useState(""); + // Tier picker: on SaaS every workspace gets its own EC2 VM (Full Access + // by construction), so we hide the T1/T2 sandbox options and lock to T3. + // On self-hosted we still offer all three because Docker-sandbox tiers + // are a real choice there. useMemo is SSR-safe via the isSaaSTenant() + // contract (returns false on server); first client render may flip the + // picker — acceptable one-frame reflow, no data loss. + const isSaaS = useMemo(() => isSaaSTenant(), []); + const TIERS = useMemo( + () => + isSaaS + ? [{ value: 3, label: "T3", desc: "Full Access" }] + : [ + { value: 1, label: "T1", desc: "Sandboxed" }, + { value: 2, label: "T2", desc: "Standard" }, + { value: 3, label: "T3", desc: "Full Access" }, + ], + [isSaaS], + ); + const defaultTier = isSaaS ? 3 : 1; + const [tier, setTier] = useState(defaultTier); + // Refs for roving tabIndex on the tier radio group (WCAG 2.1 arrow-key nav) const radioRefs = useRef>([]); - const TIERS = [ - { value: 1, label: "T1", desc: "Sandboxed" }, - { value: 2, label: "T2", desc: "Standard" }, - { value: 3, label: "T3", desc: "Full Access" }, - ]; const handleRadioKeyDown = useCallback( (e: React.KeyboardEvent, currentIndex: number) => { @@ -85,7 +101,7 @@ export function CreateWorkspaceButton() { if (!open) return; setName(""); setRole(""); - setTier(1); + setTier(defaultTier); setTemplate(""); setParentId(""); setBudgetLimit(""); @@ -96,6 +112,9 @@ export function CreateWorkspaceButton() { .get("/workspaces") .then((ws) => setWorkspaces(ws)) .catch(() => {}); + // defaultTier is stable for the session (derived from window.location), + // safe to omit from deps. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const handleCreate = async () => { @@ -209,10 +228,10 @@ export function CreateWorkspaceButton() {
-
- Tier +
+ Tier{isSaaS ? " — dedicated VM" : ""}
{TIERS.map((t, idx) => (