From a926d19779a67aa8c098dda2ae6d75a062d306d3 Mon Sep 17 00:00:00 2001
From: Haider
Date: Fri, 14 Aug 2026 17:36:57 +0530
Subject: [PATCH 1/7] feat(workspace): browser-based workspace creation handoff
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a browser handoff for creating and linking a Workspace: CLI opens the
SaaS approval modal on `.ws.myaltimate.com/create-and-link` with the
current project's context (git remote or path + auto-derived name), user
approves, the SaaS creates a workspace and delivers its ID back to the CLI
via a loopback callback (same pattern as gateway sign-in). CLI then binds
the current project to that workspace via the existing `POST /bind`.
Additive to `feat/agent-workspaces` — every pre-existing option in the
post-scan dialog and `altimate-code link` picker (Create quick workspace,
Link to existing, Skip, workspace-picker rows) continues to work unchanged.
The new "Set up in browser" option auto-hides when the deployment isn't
supported (localhost, enterprise, custom domain) — freemium only for pilot.
- New `packages/opencode/src/altimate/workspace/browser-handoff.ts`:
loopback listener (own instance per flow, port walk 7317..7325 with
natural fallback past a live OAuth listener), tenant-mismatch guard,
typed failure reasons. Duplicates the loopback pattern from
`altimate.ts` deliberately — shared-helper refactor is a follow-up
ticket once both flows have prod experience.
- Post-scan `OfferDialog`: adds "Set up in browser (recommended)" as the
default when available, sitting alongside the existing options.
- `altimate-code link` picker: adds "+ Set up in browser" as the first
row when available.
- Handles browser-open failures with a copy-URL fallback; 15-min timeout;
explicit cancel via SaaS-delivered `?error=cancelled`.
Tests: 14 new unit tests for browser-handoff (URL resolution, pre-flight
failures, end-to-end via dependency-injected browser opener, port walk
past a squatting listener). 32/32 workspace + plugin tests pass.
---
.../src/altimate/workspace/browser-handoff.ts | 341 ++++++++++++++++++
packages/opencode/src/cli/cmd/link.ts | 135 ++++++-
.../src/plugin/tui/altimate/workspace.tsx | 199 +++++++++-
.../workspace/browser-handoff.test.ts | 261 ++++++++++++++
4 files changed, 907 insertions(+), 29 deletions(-)
create mode 100644 packages/opencode/src/altimate/workspace/browser-handoff.ts
create mode 100644 packages/opencode/test/altimate/workspace/browser-handoff.test.ts
diff --git a/packages/opencode/src/altimate/workspace/browser-handoff.ts b/packages/opencode/src/altimate/workspace/browser-handoff.ts
new file mode 100644
index 000000000..78a0bc7ab
--- /dev/null
+++ b/packages/opencode/src/altimate/workspace/browser-handoff.ts
@@ -0,0 +1,341 @@
+// altimate_change - new file
+//
+// Browser-based workspace creation handoff. CLI opens Ralph's SaaS approval
+// modal at ``.ws.myaltimate.com/create-and-link`` with the current
+// project's context, user approves, the SaaS creates a workspace and delivers
+// its ID back to the CLI via a loopback callback. The CLI then binds the
+// current project to that workspace via the existing
+// ``POST /datamate-project-bindings/bind`` endpoint.
+//
+// This module deliberately DUPLICATES the loopback listener pattern from
+// ``altimate/plugin/altimate.ts`` rather than sharing a helper — the two flows
+// are similar enough that a naive extraction would trade duplication for
+// coupling on state/global lifecycle. Refactor to a shared helper is a
+// follow-up ticket once both flows have prod experience; the port range
+// (7317..7325) is walked independently by each listener instance so a live
+// OAuth server on 7317 forces workspace-handoff to bind 7318 without either
+// close operation affecting the other.
+//
+// See docs `workspace-browser-handoff-plan-v3.md` for the design context.
+import { createServer, type Server } from "http"
+import { randomBytes } from "crypto"
+import open from "open"
+
+import { AltimateApi } from "@/altimate/api/client"
+import { Log } from "@/altimate/util/log"
+
+import type { ProjectIdentifier } from "./api-client"
+
+// Freemium is the only deployment served by the workspace stack today. When
+// altimate-backend goes multi-deployment (enterprise), extend this to a small
+// mapping. Returning null means "not supported here" — the CLI hides the
+// browser-handoff option entirely rather than open a broken URL.
+const FREEMIUM_API_HOST = "api.myaltimate.com"
+const FREEMIUM_WORKSPACE_HOST = "ws.myaltimate.com"
+
+// Loopback port range for the workspace-bound callback. Shared with the OAuth
+// sign-in listener in altimate.ts — each listener walks independently, so a
+// live OAuth server on 7317 forces us to 7318 (or later) transparently.
+const CALLBACK_PORT_MIN = 7317
+const CALLBACK_PORT_MAX = 7325
+
+const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000
+const DELIVERY_HTML_SUCCESS = `Altimate Code
+
+Workspace ready
Return to your terminal to finish linking.
+`
+
+const log = Log.create({ service: "altimate-workspace-handoff" })
+
+function escapeHtml(s: string): string {
+ return s.replace(
+ /[&<>"']/g,
+ (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string,
+ )
+}
+
+function htmlError(msg: string): string {
+ return `Altimate Code
+
+Workspace handoff failed
${escapeHtml(msg)}
+Please return to your terminal and try again.
`
+}
+
+export type HandoffFailureReason =
+ | "unavailable" // resolveWorkspaceWebUrl returned null (not freemium)
+ | "not_configured" // CLI credentials not present
+ | "timeout" // 15-min window expired
+ | "cancelled" // user hit Cancel in the browser
+ | "tenant_mismatch" // callback tenant != credentials tenant
+ | "port_exhausted" // 7317..7325 all in use
+ | "browser_open_failed"
+ | "error"
+
+export interface HandoffSuccess {
+ ok: true
+ workspaceId: number
+ tenant: string
+}
+export interface HandoffFailure {
+ ok: false
+ reason: HandoffFailureReason
+ message?: string
+ authorizeUrl?: string // set for browser_open_failed so caller can copy-paste
+}
+export type HandoffResult = HandoffSuccess | HandoffFailure
+
+/** Compute the workspace-stack URL for a given API host + tenant, or null if
+ * this deployment isn't supported (localhost, enterprise, custom domain).
+ *
+ * Dev escape hatch: ``ALTIMATE_WORKSPACE_WEB_URL`` overrides the map lookup
+ * when set (must be a well-formed URL). Used for local integration testing
+ * against a non-freemium SaaS instance. Not something production users touch. */
+export function resolveWorkspaceWebUrl(altimateUrl: string, tenant: string): URL | null {
+ const override = process.env["ALTIMATE_WORKSPACE_WEB_URL"]
+ if (override) {
+ try {
+ return new URL(override)
+ } catch {
+ return null
+ }
+ }
+ try {
+ const apiHost = new URL(altimateUrl).host
+ if (apiHost !== FREEMIUM_API_HOST) return null
+ return new URL(`https://${tenant}.${FREEMIUM_WORKSPACE_HOST}`)
+ } catch {
+ return null
+ }
+}
+
+interface HandoffPending {
+ state: string
+ expectedTenant: string
+ resolve: (v: HandoffSuccess) => void
+ reject: (err: Error & { handoffReason?: HandoffFailureReason }) => void
+}
+
+function markReason(err: E, reason: HandoffFailureReason): E & { handoffReason: HandoffFailureReason } {
+ return Object.assign(err, { handoffReason: reason })
+}
+
+/** Start a per-flow loopback listener on the first available port in the
+ * shared 7317..7325 range. Own server, own pending map — no coupling to the
+ * OAuth listener in altimate.ts. */
+async function startListener(pending: HandoffPending): Promise<{ server: Server; port: number }> {
+ const server = createServer((req, res) => {
+ const port = (server.address() as { port?: number } | null)?.port ?? CALLBACK_PORT_MIN
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`)
+ if (url.pathname !== "/workspace-bound") {
+ res.writeHead(404)
+ res.end("Not found")
+ return
+ }
+
+ const respond = (status: number, body: string) => {
+ res.writeHead(status, { "Content-Type": "text/html" })
+ res.end(body)
+ }
+
+ // Validate state FIRST — a request without the right state can neither
+ // cancel nor deliver anything.
+ const state = url.searchParams.get("state")
+ if (!state || state !== pending.state) {
+ respond(400, htmlError("Invalid or unknown workspace-handoff state"))
+ return
+ }
+
+ // Respond BEFORE resolving/rejecting the pending flow — the reject path
+ // closes the listener via closeListener(), which can race with the
+ // response flush and leave the client fetch hanging. Order matters.
+ const error = url.searchParams.get("error")
+ if (error) {
+ const reason: HandoffFailureReason = error === "cancelled" ? "cancelled" : "error"
+ respond(200, htmlError(error === "cancelled" ? "Cancelled by user" : error))
+ pending.reject(markReason(new Error(error), reason))
+ return
+ }
+
+ const workspaceIdRaw = url.searchParams.get("workspace_id")
+ const tenant = url.searchParams.get("tenant")
+ if (!workspaceIdRaw || !tenant) {
+ const msg = "Missing workspace_id or tenant in callback"
+ respond(400, htmlError(msg))
+ pending.reject(markReason(new Error(msg), "error"))
+ return
+ }
+
+ if (tenant !== pending.expectedTenant) {
+ // Cross-tenant defence: user created the workspace in a tenant that
+ // doesn't match the CLI's credentials. Refuse the bind — the workspace
+ // ID is tenant-schema-local so binding here would 404 or, worse, hit an
+ // unrelated workspace in the CLI's tenant.
+ const msg = `Workspace was created in tenant "${tenant}" but the CLI is signed into "${pending.expectedTenant}"`
+ respond(400, htmlError(msg))
+ pending.reject(markReason(new Error(msg), "tenant_mismatch"))
+ return
+ }
+
+ const workspaceId = Number(workspaceIdRaw)
+ if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
+ const msg = `Invalid workspace_id: ${workspaceIdRaw}`
+ respond(400, htmlError(msg))
+ pending.reject(markReason(new Error(msg), "error"))
+ return
+ }
+
+ respond(200, DELIVERY_HTML_SUCCESS)
+ pending.resolve({ ok: true, workspaceId, tenant })
+ })
+
+ // Walk 7317..7325 — each server instance is independent, so a squatting
+ // OAuth listener on 7317 just makes us bind 7318.
+ const tried: number[] = []
+ let lastErr: NodeJS.ErrnoException | undefined
+ for (let port = CALLBACK_PORT_MIN; port <= CALLBACK_PORT_MAX; port++) {
+ tried.push(port)
+ try {
+ await new Promise((resolve, reject) => {
+ const onErr = (err: NodeJS.ErrnoException) => reject(err)
+ server.once("error", onErr)
+ server.listen(port, "127.0.0.1", () => {
+ server.removeListener("error", onErr)
+ resolve()
+ })
+ })
+ return { server, port }
+ } catch (err) {
+ lastErr = err as NodeJS.ErrnoException
+ // Defensive cleanup in case any listeners linger after a rejected bind.
+ server.removeAllListeners("error")
+ if (lastErr.code !== "EADDRINUSE") break
+ }
+ }
+
+ server.close()
+ const code = lastErr?.code
+ throw markReason(
+ new Error(
+ code === "EADDRINUSE"
+ ? `Every port in ${CALLBACK_PORT_MIN}-${CALLBACK_PORT_MAX} is in use (tried ${tried.join(", ")}). Close what's using them (e.g. \`lsof -i :${CALLBACK_PORT_MIN}\`) and try again.`
+ : `Could not start the workspace-handoff server: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
+ ),
+ "port_exhausted",
+ )
+}
+
+export interface OpenBrowserHandoffInput {
+ identifier: ProjectIdentifier
+ projectName: string
+}
+
+/** Full browser-handoff flow. Returns the created/picked workspace ID on
+ * success, or a typed failure reason on any error path. Never throws — every
+ * error is expressed as ``{ok: false, reason}`` so the caller can toast the
+ * appropriate message. */
+export async function openWorkspaceBrowserHandoff(input: OpenBrowserHandoffInput): Promise {
+ return runHandoffWithOpener(input, (url) => open(url).then(() => undefined))
+}
+
+/** Same as ``openWorkspaceBrowserHandoff`` but takes the browser-open callback
+ * as a dependency so tests can inject a fake that fires the loopback callback
+ * synchronously instead of launching a real browser. Not exported from the
+ * package barrel — only tests import this directly. */
+export async function runHandoffWithOpener(
+ input: OpenBrowserHandoffInput,
+ openBrowser: (url: string) => Promise,
+): Promise {
+ if (!(await AltimateApi.isConfigured().catch(() => false))) {
+ return { ok: false, reason: "not_configured" }
+ }
+ const creds = await AltimateApi.getCredentials()
+ const webUrl = resolveWorkspaceWebUrl(creds.altimateUrl, creds.altimateInstanceName)
+ if (!webUrl) return { ok: false, reason: "unavailable" }
+
+ const state = randomBytes(16).toString("hex")
+
+ // Register pending, then bind the listener. Timeout owns rejection with
+ // reason "timeout"; the listener's own reject paths mark their own reasons.
+ let listenerHandle: { server: Server; port: number } | undefined
+ const closeListener = () => {
+ if (listenerHandle) {
+ try {
+ listenerHandle.server.close()
+ } catch {
+ /* best effort */
+ }
+ listenerHandle = undefined
+ }
+ }
+
+ const settled = new Promise((resolve) => {
+ const pending: HandoffPending = {
+ state,
+ expectedTenant: creds.altimateInstanceName,
+ resolve: (v) => {
+ closeListener()
+ clearTimeout(timeoutHandle)
+ resolve(v)
+ },
+ reject: (err) => {
+ closeListener()
+ clearTimeout(timeoutHandle)
+ const reason = (err as { handoffReason?: HandoffFailureReason }).handoffReason ?? "error"
+ const authorizeUrl = (err as { authorizeUrl?: string }).authorizeUrl
+ resolve({
+ ok: false,
+ reason,
+ message: err.message,
+ ...(authorizeUrl ? { authorizeUrl } : {}),
+ })
+ },
+ }
+ const timeoutHandle = setTimeout(() => {
+ pending.reject(markReason(new Error("Timed out waiting for browser workspace handoff"), "timeout"))
+ }, DEFAULT_TIMEOUT_MS)
+
+ ;(async () => {
+ try {
+ listenerHandle = await startListener(pending)
+ } catch (err) {
+ const reason = (err as { handoffReason?: HandoffFailureReason }).handoffReason ?? "error"
+ pending.reject(markReason(err as Error, reason))
+ return
+ }
+
+ // Import buildCliContext lazily so this module doesn't pull altimate.ts
+ // into every consumer's import graph at load time.
+ const { buildCliContext } = await import("../plugin/altimate")
+ const cliContext = await buildCliContext().catch((err) => {
+ log.warn("buildCliContext failed; proceeding without", { err: String(err) })
+ return ""
+ })
+
+ const redirect = `http://127.0.0.1:${listenerHandle.port}/workspace-bound`
+ const target = new URL("/create-and-link", webUrl)
+ target.searchParams.set("client", "altimate-code")
+ target.searchParams.set("redirect", redirect)
+ target.searchParams.set("state", state)
+ if (input.identifier.repoRemote) target.searchParams.set("project_remote", input.identifier.repoRemote)
+ if (input.identifier.projectPath) target.searchParams.set("project_path", input.identifier.projectPath)
+ target.searchParams.set("project_name", input.projectName)
+ const authorizeUrl = cliContext
+ ? `${target.toString()}#cli_context=${encodeURIComponent(cliContext)}`
+ : target.toString()
+
+ try {
+ await openBrowser(authorizeUrl)
+ } catch (err) {
+ // Browser open failed. Preserve the URL so the caller can copy-paste.
+ pending.reject(
+ Object.assign(
+ markReason(new Error(`Could not open browser: ${err instanceof Error ? err.message : String(err)}`), "browser_open_failed"),
+ { authorizeUrl },
+ ),
+ )
+ }
+ })()
+ })
+
+ return settled
+}
diff --git a/packages/opencode/src/cli/cmd/link.ts b/packages/opencode/src/cli/cmd/link.ts
index 3c9371d73..b251ec0c1 100644
--- a/packages/opencode/src/cli/cmd/link.ts
+++ b/packages/opencode/src/cli/cmd/link.ts
@@ -32,9 +32,15 @@ import {
projectNameFromRemote,
resolveProjectIdentifier,
} from "@/altimate/workspace/detect"
+import {
+ openWorkspaceBrowserHandoff,
+ resolveWorkspaceWebUrl,
+ type HandoffResult,
+} from "@/altimate/workspace/browser-handoff"
import { recordApprovedBinding } from "@/altimate/workspace/state"
const CREATE_NEW_SENTINEL = "__create_new__"
+const SET_UP_IN_BROWSER_SENTINEL = "__browser_handoff__"
export const LinkCommand = cmd({
command: "link",
@@ -118,13 +124,29 @@ export const LinkCommand = cmd({
const currentId = existing?.datamate.id
const currentName = existing?.datamate.name
+ // Only offer the browser-based handoff when the deployment supports it
+ // (freemium only today). Enterprise / localhost / custom-domain callers
+ // silently fall back to the CLI-side quick create.
+ const creds = await AltimateApi.getCredentials()
+ const browserAvailable =
+ resolveWorkspaceWebUrl(creds.altimateUrl, creds.altimateInstanceName) !== null
+
const options: Array<{ value: string; label: string; hint?: string }> = [
+ ...(browserAvailable
+ ? [
+ {
+ value: SET_UP_IN_BROWSER_SENTINEL,
+ label: `+ Set up in browser "${autoName}"`,
+ hint: "Approve in the Altimate SaaS; CLI links your project automatically.",
+ },
+ ]
+ : []),
{
value: CREATE_NEW_SENTINEL,
- label: `+ Create a new workspace "${autoName}"`,
+ label: `+ Create a quick workspace "${autoName}" here`,
hint: existing
- ? "Creates a new workspace and repoints this project to it."
- : "Named from this project; rename in the SaaS after.",
+ ? "Creates a new workspace and repoints this project to it (no browser step)."
+ : "No browser step; configure integrations later in the SaaS.",
},
...list.map((dm) => ({
value: String(dm.id),
@@ -146,6 +168,11 @@ export const LinkCommand = cmd({
return
}
+ if (pick === SET_UP_IN_BROWSER_SENTINEL) {
+ await runBrowserHandoff(identifier, autoName, args.directory)
+ return
+ }
+
if (pick === CREATE_NEW_SENTINEL) {
await createThenBindOrRebind(identifier, autoName, args.directory, existing)
return
@@ -161,12 +188,102 @@ export const LinkCommand = cmd({
},
})
-/** "+ Create a new workspace" flow. When the project is already linked, this
- * MUST rebind after create — otherwise the new workspace is a real (billable)
- * SaaS resource the CLI knows nothing about and the project is still bound to
- * the old workspace (M2 in the consensus review). When rebind fails, the
- * error message tells the user the workspace was created and how to recover;
- * we do NOT silently swallow the orphan. */
+/** Browser-based create-and-bind flow. Same handoff module the TUI post-scan
+ * dialog uses; on success, the CLI calls the existing bind endpoint to link
+ * the current project to the newly-created workspace. When the project is
+ * already linked, bindExisting will 409; the caller re-runs and picks
+ * "+ Create a quick workspace here" instead to trigger the create-and-rebind
+ * path. (Full create-then-rebind via the browser flow is deferred — the
+ * SaaS approval screen doesn't yet know how to receive a "rebind after
+ * create" instruction from the CLI.) */
+async function runBrowserHandoff(
+ identifier: ProjectIdentifier,
+ projectName: string,
+ directory: string,
+): Promise {
+ const spin = prompts.spinner()
+ spin.start("Waiting for browser approval...")
+ const result: HandoffResult = await openWorkspaceBrowserHandoff({ identifier, projectName })
+ if (!result.ok) {
+ spin.stop(handoffFailureMessage(result), 1)
+ process.exitCode = 1
+ return
+ }
+ spin.stop(`Workspace approved. Binding to project...`)
+ const bindSpin = prompts.spinner()
+ bindSpin.start("Linking workspace...")
+ try {
+ const res = await WorkspaceApi.bindExisting(result.workspaceId, identifier)
+ await recordApprovedBinding(directory, {
+ datamateId: res.binding.datamate_id,
+ datamateName: res.binding.datamate_name,
+ repoRemote: res.binding.repo_remote,
+ projectPath: res.binding.project_path,
+ linkedAt: Date.now(),
+ })
+ bindSpin.stop(`Linked to "${res.binding.datamate_name}".`)
+ const manageUrl = await manageUrlFor(res.binding.datamate_id)
+ if (manageUrl) prompts.log.info(`Manage it at: ${manageUrl}`)
+ prompts.outro("Done.")
+ } catch (err) {
+ bindSpin.stop("Link failed.", 1)
+ if (err instanceof ConflictError) {
+ prompts.log.error(
+ `This project is already linked to "${err.detail.existing_datamate_name ?? "another workspace"}". Workspace "${projectName}" was created but is not linked — re-run \`altimate-code link\` and pick a different action to switch, or delete the new workspace in the SaaS.`,
+ )
+ } else if (err instanceof NotFoundError) {
+ prompts.log.error("Workspace not found — the tenant or workspace may have changed.")
+ } else if (err instanceof ForbiddenError) {
+ prompts.log.error("Only the workspace owner can bind projects to it.")
+ } else {
+ prompts.log.error(err instanceof Error ? err.message : String(err))
+ }
+ process.exitCode = 1
+ }
+}
+
+/** Best-effort manage-workspace URL for the current credentials. Returns null
+ * on BYOK / unresolvable deployments — callers omit the "Manage it at" line. */
+async function manageUrlFor(workspaceId: number): Promise {
+ try {
+ const creds = await AltimateApi.getCredentials()
+ const base = resolveWorkspaceWebUrl(creds.altimateUrl, creds.altimateInstanceName)
+ if (!base) return null
+ return `${base.toString().replace(/\/$/, "")}/w/${workspaceId}`
+ } catch {
+ return null
+ }
+}
+
+function handoffFailureMessage(result: Extract): string {
+ switch (result.reason) {
+ case "unavailable":
+ return "Browser handoff isn't available for this deployment."
+ case "not_configured":
+ return "Altimate credentials not configured — sign in first."
+ case "timeout":
+ return "Timed out waiting for browser approval (15 min)."
+ case "cancelled":
+ return "Cancelled by user."
+ case "tenant_mismatch":
+ return result.message ?? "Workspace was set up in a different tenant than the CLI's credentials."
+ case "port_exhausted":
+ return result.message ?? "Loopback ports 7317-7325 all in use."
+ case "browser_open_failed":
+ return `Could not open browser${result.authorizeUrl ? `. Open manually: ${result.authorizeUrl}` : "."}`
+ case "aborted":
+ return result.message ?? "Browser handoff was cancelled."
+ default:
+ return result.message ?? "Browser handoff failed."
+ }
+}
+
+/** "+ Create a quick workspace here" flow. When the project is already
+ * linked, this MUST rebind after create — otherwise the new workspace is a
+ * real (billable) SaaS resource the CLI knows nothing about and the project
+ * is still bound to the old workspace (M2 in the consensus review). When
+ * rebind fails, the error message tells the user the workspace was created
+ * and how to recover; we do NOT silently swallow the orphan. */
async function createThenBindOrRebind(
identifier: ProjectIdentifier,
name: string,
diff --git a/packages/opencode/src/plugin/tui/altimate/workspace.tsx b/packages/opencode/src/plugin/tui/altimate/workspace.tsx
index 440fa8412..0d039465a 100644
--- a/packages/opencode/src/plugin/tui/altimate/workspace.tsx
+++ b/packages/opencode/src/plugin/tui/altimate/workspace.tsx
@@ -37,6 +37,11 @@ import {
type ProjectBindingLookup,
type ProjectIdentifier,
} from "@/altimate/workspace/api-client"
+import {
+ openWorkspaceBrowserHandoff,
+ resolveWorkspaceWebUrl,
+ type HandoffResult,
+} from "@/altimate/workspace/browser-handoff"
import {
projectNameFromPath,
projectNameFromRemote,
@@ -50,6 +55,16 @@ const PLUGIN_ID = "altimate:workspace"
const log = Log.create({ service: "altimate-workspace" })
+/** True when the browser-based workspace-creation handoff is available for
+ * the current credentials (freemium only today). Wrapped so both the post-scan
+ * flow and the on-demand `altimate-code link` picker can hide the option
+ * consistently when the deployment isn't supported. */
+async function isBrowserHandoffAvailable(): Promise {
+ if (!(await AltimateApi.isConfigured().catch(() => false))) return false
+ const creds = await AltimateApi.getCredentials()
+ return resolveWorkspaceWebUrl(creds.altimateUrl, creds.altimateInstanceName) !== null
+}
+
// ─────────────────────────────────────────────────────────────────────────────
// Skip latch (TUI-only). Uses TuiPluginApi.kv — persistent across sessions
// via packages/tui/src/context/kv.tsx (state/kv.json). The `altimate link`
@@ -132,6 +147,12 @@ interface OfferProps {
api: TuiPluginApi
identifier: ProjectIdentifier
defaultName: string
+ /** True when this deployment supports the browser-based workspace-creation
+ * handoff (i.e. ``resolveWorkspaceWebUrl`` returned non-null for the current
+ * credentials). Resolved by the caller so the dialog doesn't need to await
+ * on mount. When false, the "Set up in browser" option is hidden and the
+ * dialog falls back to the pre-browser-handoff behavior. */
+ browserAvailable: boolean
/** (tenant, apiUrl) scope for the Skip latch. Resolved once by the caller
* so the sync ``onSelect`` handler can call ``recordSkip`` without a
* mid-render await. Null when creds are unavailable — latch falls back
@@ -141,36 +162,52 @@ interface OfferProps {
function OfferDialog(props: OfferProps) {
const identLabel = () => props.identifier.repoRemote ?? props.identifier.projectPath ?? "this project"
+ const options = [
+ ...(props.browserAvailable
+ ? [
+ {
+ title: "Set up in browser (recommended)",
+ value: "browser",
+ description: `Approve and name "${props.defaultName}" in the Altimate SaaS; the CLI links your project automatically.`,
+ },
+ ]
+ : []),
+ {
+ title: "Create quick workspace here",
+ value: "create",
+ description: `Auto-named "${props.defaultName}" from this repo — no browser step. Configure integrations later in the SaaS.`,
+ },
+ {
+ title: "Link to an existing workspace",
+ value: "link",
+ description: "Attach this project to a workspace you already own.",
+ },
+ {
+ title: "Skip for now",
+ value: "skip",
+ description: "Won't ask again for 7 days.",
+ },
+ ]
+ const defaultValue = props.browserAvailable ? "browser" : "create"
return (
{
if (option.value === "skip") {
recordSkip(props.api, props.identifier, props.latchScope, Date.now())
props.api.ui.dialog.clear()
return
}
+ if (option.value === "browser") {
+ void runBrowserHandoff(props.api, props.identifier, props.defaultName)
+ return
+ }
if (option.value === "create") {
- // Auto-name from git repo — no name prompt. The SaaS UI is the place to
- // rename / configure; the CLI's job is just to establish the binding.
+ // Local direct-create — the CLI-only fallback. The SaaS UI is the
+ // place to rename / configure; this branch establishes the binding
+ // without a browser round-trip.
void createAndBindInline(props.api, props.identifier, props.defaultName)
return
}
@@ -183,6 +220,120 @@ function OfferDialog(props: OfferProps) {
)
}
+/** Post-scan / on-demand browser-handoff runner. Opens the SaaS approval
+ * modal, waits for the callback, and binds the current project to the
+ * returned workspace via the existing ``POST /bind`` endpoint. Every failure
+ * mode surfaces as a toast; the user can always fall back to another option
+ * by re-invoking the dialog. */
+async function runBrowserHandoff(
+ api: TuiPluginApi,
+ identifier: ProjectIdentifier,
+ projectName: string,
+): Promise {
+ api.ui.dialog.clear()
+ api.ui.toast({
+ variant: "info",
+ message: "Opening browser to set up your workspace — return here once you approve.",
+ })
+ const result: HandoffResult = await openWorkspaceBrowserHandoff({ identifier, projectName })
+ if (!result.ok) {
+ toastHandoffFailure(api, result)
+ return
+ }
+ // Handoff succeeded — bind the project to the returned workspace via the
+ // existing bind endpoint. Same code path as PickerDialog's attach mode.
+ try {
+ const res = await WorkspaceApi.bindExisting(result.workspaceId, identifier)
+ await recordApprovedBinding(api.state.path.directory, {
+ datamateId: res.binding.datamate_id,
+ datamateName: res.binding.datamate_name,
+ repoRemote: res.binding.repo_remote,
+ projectPath: res.binding.project_path,
+ linkedAt: Date.now(),
+ })
+ api.ui.toast({
+ variant: "success",
+ message: `Linked to workspace "${res.binding.datamate_name}".`,
+ })
+ } catch (err) {
+ if (err instanceof ConflictError) {
+ api.ui.toast({
+ variant: "warning",
+ message: `This project is already linked to "${err.detail.existing_datamate_name ?? "another workspace"}". Run \`altimate-code link\` to change.`,
+ })
+ } else if (err instanceof NotFoundError) {
+ api.ui.toast({
+ variant: "error",
+ message: "Workspace not found — the tenant or workspace may have changed. Try again.",
+ })
+ } else if (err instanceof ForbiddenError) {
+ api.ui.toast({
+ variant: "error",
+ message: "Only the workspace owner can bind projects to it.",
+ })
+ } else {
+ api.ui.toast({
+ variant: "error",
+ message: err instanceof Error ? err.message : "Failed to bind workspace",
+ })
+ }
+ }
+}
+
+function toastHandoffFailure(api: TuiPluginApi, result: Extract): void {
+ switch (result.reason) {
+ case "unavailable":
+ // Should not happen if browserAvailable was checked, but guard anyway.
+ api.ui.toast({
+ variant: "warning",
+ message: "Browser-based workspace setup isn't available for this deployment. Use \"Create quick workspace here\" instead.",
+ })
+ break
+ case "not_configured":
+ api.ui.toast({
+ variant: "error",
+ message: "Altimate credentials not configured — sign in first, then re-run.",
+ })
+ break
+ case "timeout":
+ api.ui.toast({
+ variant: "warning",
+ message: "Workspace setup timed out (15 min). Re-run when you're ready.",
+ })
+ break
+ case "cancelled":
+ api.ui.toast({
+ variant: "info",
+ message: "Workspace setup cancelled.",
+ })
+ break
+ case "tenant_mismatch":
+ api.ui.toast({
+ variant: "error",
+ message: result.message ?? "Workspace was set up under a different account than the CLI is signed into.",
+ })
+ break
+ case "port_exhausted":
+ api.ui.toast({
+ variant: "error",
+ message: result.message ?? "Local ports 7317-7325 all in use — free one and try again.",
+ })
+ break
+ case "browser_open_failed":
+ api.ui.toast({
+ variant: "error",
+ message: `Could not open browser. ${result.authorizeUrl ? `Open this URL manually: ${result.authorizeUrl}` : ""}`,
+ duration: 15_000,
+ })
+ break
+ default:
+ api.ui.toast({
+ variant: "error",
+ message: result.message ?? "Workspace setup failed.",
+ })
+ }
+}
+
async function createAndBindInline(
api: TuiPluginApi,
identifier: ProjectIdentifier,
@@ -747,6 +898,12 @@ async function runFlow(api: TuiPluginApi, directory: string): Promise {
? projectNameFromRemote(identifier.repoRemote)
: projectNameFromPath(identifier.projectPath)
+ // Whether the browser-based handoff is available for this deployment. The
+ // OfferDialog hides the "Set up in browser" option when false, silently
+ // falling back to the pre-browser-handoff behavior. Compute here (once,
+ // async) so the dialog itself stays sync.
+ const browserAvailable = await isBrowserHandoffAvailable()
+
let serverBinding: ProjectBindingLookup | null | undefined
try {
serverBinding = await WorkspaceApi.getBindingForProject(identifier)
@@ -799,6 +956,7 @@ async function runFlow(api: TuiPluginApi, directory: string): Promise {
api={api}
identifier={identifier}
defaultName={defaultName}
+ browserAvailable={browserAvailable}
latchScope={latchScope}
/>
))
@@ -840,6 +998,7 @@ async function runFlow(api: TuiPluginApi, directory: string): Promise {
api={api}
identifier={identifier}
defaultName={defaultName}
+ browserAvailable={browserAvailable}
latchScope={latchScope}
/>
))
diff --git a/packages/opencode/test/altimate/workspace/browser-handoff.test.ts b/packages/opencode/test/altimate/workspace/browser-handoff.test.ts
new file mode 100644
index 000000000..386e89b58
--- /dev/null
+++ b/packages/opencode/test/altimate/workspace/browser-handoff.test.ts
@@ -0,0 +1,261 @@
+// altimate_change - new file
+// Unit coverage for the browser-based workspace-creation handoff.
+// (packages/opencode/src/altimate/workspace/browser-handoff.ts.)
+//
+// Uses ``runHandoffWithOpener`` (dependency-injected browser-open callback)
+// so tests fire a synthetic callback at the live loopback listener instead of
+// launching a real browser. The listener itself binds to 127.0.0.1, walks
+// 7317..7325, and processes real HTTP requests — this is genuine end-to-end
+// coverage for the callback validation path.
+import { afterEach, beforeEach, describe, expect, test } from "bun:test"
+import { createServer } from "node:net"
+
+import { AltimateApi } from "../../../src/altimate/api/client"
+import {
+ openWorkspaceBrowserHandoff,
+ resolveWorkspaceWebUrl,
+ runHandoffWithOpener,
+} from "../../../src/altimate/workspace/browser-handoff"
+
+// ── credential stubbing ─────────────────────────────────────────────────────
+const originalIsConfigured = AltimateApi.isConfigured
+const originalGetCreds = AltimateApi.getCredentials
+type Creds = Awaited>
+function stubCreds(tenant: string, apiUrl: string) {
+ ;(AltimateApi as unknown as { isConfigured: () => Promise }).isConfigured =
+ async () => true
+ ;(AltimateApi as unknown as { getCredentials: () => Promise }).getCredentials =
+ async () =>
+ ({
+ altimateInstanceName: tenant,
+ altimateUrl: apiUrl,
+ altimateApiKey: "dummy",
+ }) as Creds
+}
+function unstubCreds() {
+ ;(AltimateApi as unknown as { isConfigured: typeof originalIsConfigured }).isConfigured =
+ originalIsConfigured
+ ;(AltimateApi as unknown as { getCredentials: typeof originalGetCreds }).getCredentials =
+ originalGetCreds
+}
+
+// ── helpers ─────────────────────────────────────────────────────────────────
+
+/** Parse the authorize URL the CLI wants to open; extract the loopback port
+ * and CSRF state so tests can fire the crafted callback at the right address. */
+function parseHandoffUrl(url: string): { port: number; state: string; redirect: string } {
+ const u = new URL(url)
+ const redirect = u.searchParams.get("redirect")!
+ const state = u.searchParams.get("state")!
+ const port = Number(new URL(redirect).port)
+ return { port, state, redirect }
+}
+
+async function fireCallback(redirect: string, params: Record): Promise {
+ const target = new URL(redirect)
+ for (const [k, v] of Object.entries(params)) target.searchParams.set(k, v)
+ const res = await fetch(target.toString(), { method: "GET" })
+ // Drain body so the connection can close and let the CLI's `close()`
+ // proceed without hanging on lingering sockets.
+ await res.text().catch(() => "")
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// resolveWorkspaceWebUrl — the deployment-support gate
+// ─────────────────────────────────────────────────────────────────────────────
+
+describe("resolveWorkspaceWebUrl", () => {
+ test("freemium API host resolves to .ws.myaltimate.com", () => {
+ const url = resolveWorkspaceWebUrl("https://api.myaltimate.com", "acme")
+ expect(url).not.toBeNull()
+ expect(url!.toString()).toBe("https://acme.ws.myaltimate.com/")
+ })
+
+ test("localhost API returns null (browser flow not supported in dev)", () => {
+ expect(resolveWorkspaceWebUrl("http://localhost:5001", "acme")).toBeNull()
+ })
+
+ test("enterprise API host returns null", () => {
+ expect(resolveWorkspaceWebUrl("https://acme.getaltimate.com", "acme")).toBeNull()
+ })
+
+ test("malformed URL returns null instead of throwing", () => {
+ expect(resolveWorkspaceWebUrl("not-a-url", "acme")).toBeNull()
+ expect(resolveWorkspaceWebUrl("", "acme")).toBeNull()
+ })
+})
+
+// ─────────────────────────────────────────────────────────────────────────────
+// openWorkspaceBrowserHandoff — pre-flight failures (do not open a browser)
+// ─────────────────────────────────────────────────────────────────────────────
+
+describe("openWorkspaceBrowserHandoff pre-flight", () => {
+ afterEach(() => unstubCreds())
+
+ test("returns {unavailable} for localhost credentials", async () => {
+ stubCreds("acme", "http://localhost:5001")
+ const result = await openWorkspaceBrowserHandoff({
+ identifier: { repoRemote: "git@github.com:acme/x.git", projectPath: "/x" },
+ projectName: "x",
+ })
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("unavailable")
+ })
+
+ test("returns {not_configured} when credentials are missing", async () => {
+ ;(AltimateApi as unknown as { isConfigured: () => Promise }).isConfigured =
+ async () => false
+ const result = await openWorkspaceBrowserHandoff({
+ identifier: { projectPath: "/x" },
+ projectName: "x",
+ })
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("not_configured")
+ })
+})
+
+// ─────────────────────────────────────────────────────────────────────────────
+// End-to-end via runHandoffWithOpener — real loopback, injected browser-open
+// ─────────────────────────────────────────────────────────────────────────────
+
+describe("runHandoffWithOpener end-to-end", () => {
+ beforeEach(() => stubCreds("acme", "https://api.myaltimate.com"))
+ afterEach(() => unstubCreds())
+
+ test("happy path: valid callback resolves with workspaceId + tenant", async () => {
+ const result = await runHandoffWithOpener(
+ {
+ identifier: { repoRemote: "git@github.com:acme/x.git", projectPath: "/x" },
+ projectName: "x",
+ },
+ async (url) => {
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { workspace_id: "42", state, tenant: "acme" })
+ },
+ )
+ expect(result.ok).toBe(true)
+ if (result.ok) {
+ expect(result.workspaceId).toBe(42)
+ expect(result.tenant).toBe("acme")
+ }
+ })
+
+ test("tenant mismatch is refused", async () => {
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async (url) => {
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { workspace_id: "42", state, tenant: "not-acme" })
+ },
+ )
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("tenant_mismatch")
+ })
+
+ test("?error=cancelled callback resolves as {cancelled}", async () => {
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async (url) => {
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { state, error: "cancelled", tenant: "acme" })
+ },
+ )
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("cancelled")
+ })
+
+ test("missing workspace_id in callback resolves as {error}", async () => {
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async (url) => {
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { state, tenant: "acme" })
+ },
+ )
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("error")
+ })
+
+ test("invalid workspace_id (non-numeric) resolves as {error}", async () => {
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async (url) => {
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { workspace_id: "not-a-number", state, tenant: "acme" })
+ },
+ )
+ expect(result.ok).toBe(false)
+ if (!result.ok) expect(result.reason).toBe("error")
+ })
+
+ test("browser open failure resolves as {browser_open_failed} with authorizeUrl", async () => {
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async () => {
+ throw new Error("mock: no browser available")
+ },
+ )
+ expect(result.ok).toBe(false)
+ if (!result.ok) {
+ expect(result.reason).toBe("browser_open_failed")
+ expect(result.authorizeUrl).toContain("/create-and-link")
+ expect(result.authorizeUrl).toContain("client=altimate-code")
+ expect(result.authorizeUrl).toContain("project_name=x")
+ }
+ })
+
+ test("URL includes project_remote + project_path + project_name from input", async () => {
+ let observed = ""
+ await runHandoffWithOpener(
+ {
+ identifier: { repoRemote: "git@github.com:acme/foo.git", projectPath: "/w/foo" },
+ projectName: "foo",
+ },
+ async (url) => {
+ observed = url
+ // fire callback so the flow doesn't hang for 15 min
+ const { state, redirect } = parseHandoffUrl(url)
+ await fireCallback(redirect, { workspace_id: "1", state, tenant: "acme" })
+ },
+ )
+ const u = new URL(observed)
+ expect(u.searchParams.get("project_remote")).toBe("git@github.com:acme/foo.git")
+ expect(u.searchParams.get("project_path")).toBe("/w/foo")
+ expect(u.searchParams.get("project_name")).toBe("foo")
+ expect(u.pathname).toBe("/create-and-link")
+ })
+})
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Port walk: a squatting listener on 7317 forces handoff to 7318+
+// ─────────────────────────────────────────────────────────────────────────────
+
+describe("port walk", () => {
+ beforeEach(() => stubCreds("acme", "https://api.myaltimate.com"))
+ afterEach(() => unstubCreds())
+
+ test("stale listener on 7317 forces handoff to 7318", async () => {
+ const squatter = createServer()
+ await new Promise((resolve, reject) => {
+ squatter.once("error", reject)
+ squatter.listen(7317, "127.0.0.1", () => resolve())
+ })
+
+ try {
+ let observedPort = -1
+ const result = await runHandoffWithOpener(
+ { identifier: { projectPath: "/x" }, projectName: "x" },
+ async (url) => {
+ const { port, state, redirect } = parseHandoffUrl(url)
+ observedPort = port
+ await fireCallback(redirect, { workspace_id: "1", state, tenant: "acme" })
+ },
+ )
+ expect(result.ok).toBe(true)
+ expect(observedPort).toBeGreaterThan(7317)
+ expect(observedPort).toBeLessThanOrEqual(7325)
+ } finally {
+ await new Promise((r) => squatter.close(() => r()))
+ }
+ })
+})
From ffc2b0f303b215b790891ff65619823dabb1c3a6 Mon Sep 17 00:00:00 2001
From: Haider
Date: Mon, 17 Aug 2026 00:16:47 +0530
Subject: [PATCH 2/7] feat(workspace): top-level nav handoff + confirmation
dialog + sidebar tile
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Deliver workspace handoff to CLI loopback via top-level navigation (matches
OAuth sign-in pattern), bypassing HTTPS→loopback Private Network Access
restrictions that would gate a subresource fetch in prod. Cancel uses the
same mechanism; loopback bounces the browser back to the SaaS workspace
page on success and workspace home on cancel.
- Replace transient success toasts with a persistent post-bind
`WorkspaceLinkedDialog` (workspace name + manage URL + "Continue editing
in browser" / "Done"). Wired into all five bind success paths (browser
handoff, inline create, picker attach, picker rebind, on-demand palette).
- New right-pane sidebar tile showing the currently-linked workspace + manage
URL, polling the local cache every 3s so a fresh bind surfaces without a
TUI reload. Falls back to "Not linked — run /link" for unbound projects.
- Canonicalize local binding cache keys via `realpathSync` on both write and
read paths, with a scan fallback for pre-existing entries. Fixes the macOS
`/tmp` → `/private/tmp` symlink mismatch that caused the sidebar and
by-path lookups to miss bindings the CLI itself had written.
- `altimate-code link` subcommand: show manage URL on success, cancel via
top-level nav for reliability.
Co-Authored-By: Claude Opus 4.7
Claude-Session: https://claude.ai/code/session_016H42Vt4pt5dcD7opRqckeM
---
.../src/altimate/workspace/browser-handoff.ts | 49 ++++-
.../opencode/src/altimate/workspace/state.ts | 47 ++---
packages/opencode/src/cli/cmd/link.ts | 2 +
.../opencode/src/plugin/tui/altimate/index.ts | 12 +-
.../plugin/tui/altimate/workspace-sidebar.tsx | 93 ++++++++++
.../src/plugin/tui/altimate/workspace.tsx | 171 +++++++++++++-----
6 files changed, 297 insertions(+), 77 deletions(-)
create mode 100644 packages/opencode/src/plugin/tui/altimate/workspace-sidebar.tsx
diff --git a/packages/opencode/src/altimate/workspace/browser-handoff.ts b/packages/opencode/src/altimate/workspace/browser-handoff.ts
index 78a0bc7ab..7f04505d8 100644
--- a/packages/opencode/src/altimate/workspace/browser-handoff.ts
+++ b/packages/opencode/src/altimate/workspace/browser-handoff.ts
@@ -40,10 +40,23 @@ const CALLBACK_PORT_MIN = 7317
const CALLBACK_PORT_MAX = 7325
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000
-const DELIVERY_HTML_SUCCESS = `Altimate Code
+
+/** Loopback success page. We land the user back on the SaaS workspace page via
+ * top-level navigation — matches the OAuth sign-in pattern in altimate.ts,
+ * which is proven in prod. The rationale over a subresource fetch: HTTPS→HTTP
+ * loopback subresource fetches trigger Chrome/Safari Private Network Access
+ * checks (preflight OPTIONS with Access-Control-Request-Private-Network); a
+ * top-level navigation from an HTTP 302 or ``window.location.href`` bypasses
+ * PNA entirely. Meta refresh + JS assign for belt-and-suspenders. */
+function deliverySuccessHtml(manageUrl: string): string {
+ const safe = escapeHtml(manageUrl)
+ return `Altimate Code
+
-Workspace ready
Return to your terminal to finish linking.
-`
+Workspace ready
Returning you to the workspace page…
+Continue if you're not redirected automatically.
+
+Cancelled
Returning you to the workspace home…
+Continue if you're not redirected automatically.
+