Skip to content

feat(workspace): browser-based workspace creation handoff - #1100

Draft
sahrizvi wants to merge 6 commits into
mainfrom
feat/workspace-browser-handoff
Draft

feat(workspace): browser-based workspace creation handoff#1100
sahrizvi wants to merge 6 commits into
mainfrom
feat/workspace-browser-handoff

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a browser-based workspace creation handoff to the CLI: post-scan / altimate-code link opens the Altimate SaaS on <tenant>.ws.myaltimate.com/create-and-link with the current project's context, the user approves in a single modal, and the SaaS delivers the newly-created workspace's id back to a CLI-local loopback listener (same pattern as gateway sign-in). CLI then binds the current project via the existing POST /datamate-project-bindings/bind.

Stacked on the Workspaces draft PR (#1099 / feat/agent-workspaces).

What's added

  • packages/opencode/src/altimate/workspace/browser-handoff.ts (new) — per-flow loopback listener (own instance, walks 7317..7325 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 once both flows have prod experience.
  • OfferDialog (post-scan) — adds "Set up in browser (recommended)" as the default option when the deployment supports it (freemium only for pilot; resolveWorkspaceWebUrl returns null otherwise and the option auto-hides).
  • altimate-code link picker — adds "+ Set up in browser" as the first row under the same condition.
  • Dev escape hatchALTIMATE_WORKSPACE_WEB_URL env var overrides the deployment map lookup (used for local integration testing; never set in production).

What's unchanged

Every pre-existing option in the post-scan dialog and altimate-code link picker (Create quick workspace here, Link to an existing workspace, Skip for now, existing workspace-picker rows) continues to work exactly as it does today. The browser-handoff option is strictly additive. Rolling back is a single-commit revert with no schema, no cache format, and no backend-contract implications.

A user whose deployment doesn't support the browser flow (localhost, enterprise) sees zero behavior change — the new option auto-hides.

Tests

  • 14 new unit tests for browser-handoff.ts covering URL resolution edge cases (freemium / localhost / enterprise / malformed), pre-flight failures (unavailable / not-configured), end-to-end via dependency-injected browser opener (happy path, tenant mismatch, cancel via ?error=cancelled, missing workspace_id, invalid workspace_id, browser-open failure with authorizeUrl copyable), and port walk past a squatting listener on 7317.
  • 32/32 workspace + plugin tests pass — no regressions.
  • Typecheck clean.

E2E verified against the live backend

Ran the CLI-side round-trip against a live altimate-backend on localhost:

  • ✅ CLI URL construction with all expected params (client, redirect, state, project_path, project_name, #cli_context)
  • ✅ Loopback listener binds + receives callback
  • ✅ State + tenant validation
  • ✅ Real backend POST /datamates/ creates workspace
  • ✅ Real backend POST /bind links with project_path-based binding (no git remote)
  • ✅ Real backend GET /by-path returns the binding after
  • ✅ Local cache persists to XDG state dir with correct schema
  • ✅ 409-conflict handling on repeated runs is the intended behavior

Full SaaS-side E2E (real browser clicking Approve) is a manual smoke once the paired SaaS PR is up.

Paired SaaS PR

AltimateAI/altimate-frontend PR (feat/AI-8510-workspace-browser-handoff) — stacked on Ralph's feature/AI-8496-ws-list-create branch.

Follow-ups (deliberately out of scope)

  • Loopback-listener extraction into a shared helper (currently duplicated from altimate.ts with a TODO comment).
  • Google-first-time-on-ws. UX (session gate returns user to default post-auth destination instead of /create-and-link; password login works correctly).
  • Enterprise workspace-web-host mapping.
  • Removing the CLI-direct create path (a discussion for after this ships).

🤖 Generated with Claude Code

https://claude.ai/code/session_016H42Vt4pt5dcD7opRqckeM


Summary by cubic

Adds a browser-based workspace creation handoff and completes the Workspaces pilot in the CLI/TUI. Previously create/link ran only in-CLI; now post-scan and altimate-code link default to a SaaS approval flow that creates the workspace, returns its id to the CLI via loopback, then binds and shows a confirmation. The prompt now waits for session idle, and projects without a git remote fall back to path identity.

  • Handoff: independent loopback listener on 127.0.0.1 (ports 7317–7325), top‑level navigation callback, 15‑min timeout with abort, cancel handling, and copyable‑URL fallback; validates state/tenant, re‑reads credentials before bind, guards tenant/origin and integer workspace_id.
  • UI/TUI: post‑scan OfferDialog adds “Set up in browser” when resolveWorkspaceWebUrl is available; WorkspaceLinkedDialog after any bind; right‑pane sidebar tile shows the current binding and polls every 30s. Existing in‑CLI flows remain unchanged.
  • CLI: new altimate link subcommand uses the shared API and detect/state modules; orphan‑safe create (rebind if already linked) and matched‑identifier rebind; shows the manage URL on success; command and plugin registration are flag‑gated.
  • Core: workspace/api-client.ts (typed errors, empty‑body guard, 15s timeout), workspace/detect.ts (git or path identity with credential scrub), workspace/state.ts (per‑tenant cache with canonical‑key migration). Post‑scan trigger publishes only after session idle to avoid onboarding race.
  • Scope: behind Flag.ALTIMATE_WORKSPACE; aligns with AI‑8398 (Workspaces) and AI‑8510 (browser handoff). Paired SaaS PR: feat/AI-8510-workspace-browser-handoff.

Rollout

  • Enable with the experimental flag that drives Flag.ALTIMATE_WORKSPACE.
  • Freemium deployments only; enterprise/local/custom domains hide the browser option.
  • For local testing, set ALTIMATE_WORKSPACE_WEB_URL to a ws. host under your environment.

Written for commit cd33f3c. Summary will update on new commits.

Review in cubic

Haider and others added 2 commits August 12, 2026 23:06
Adds the CLI half of the Workspaces pilot: after the first-run scan
completes and the CLI is authenticated with Altimate, prompt the user
once to create a new workspace or attach the project to an existing one.
The link is a direct authenticated call — no device flow — and the
browser opens after create so the user can configure integrations /
knowledge in the SaaS.

Fork-owned TuiPlugin per docs/internal/2026-06-23-tui-fork-features-
as-plugins-adr.md: single file at
`packages/opencode/src/plugin/tui/altimate/workspace.tsx`, added to
the existing `altimateTuiPlugins()` aggregator. Upstream
`packages/tui/**` stays byte-for-byte upstream. Uses the real
`api.ui.*` / `api.keymap.registerLayer` / `api.state.path.directory`
/ `api.kv` (persistent) surface.

Shared modules under `packages/opencode/src/altimate/workspace/` so
the plugin and the `altimate link` subcommand can't drift on request
shape or error handling:

- `api-client.ts` — typed errors (Conflict/Precondition/NotFound/
  Forbidden/NotConfigured/Api), FastAPI `{"detail": {...}}` parsing,
  15s abort timeout, credentials re-read on every call so an account
  switch is picked up without restart.
- `detect.ts` — `detectProjectRemote` + `projectNameFromRemote`;
  reuses `stripGitRemoteCredentials` (now exported from
  `project-scan.ts` so the two callers can't drift).
- `state.ts` — local binding cache scoped to (tenant, apiUrl) with
  atomic write + post-write `chmod 0o600` + corruption recovery.

Trigger: `onboarding-telemetry.ts` `tool.execute.after` hook publishes
`TuiEvent.CommandExecute` with `"altimate.workspace.postScan"` when
`project_scan` completes, gated on the new `Flag.ALTIMATE_WORKSPACE`
and `AltimateApi.isConfigured()` (BYOK users are silently skipped —
no place to send them). Never blocks onboarding on a publish failure.

Server-authoritative pre-check via `GET /datamate-project-bindings/
by-remote`; local cache used only as an offline fallback, and the
fallback path renders a mandatory "unverified" banner rather than
silently trusting stale data. Browser-open failure surfaces a
copyable-URL toast rather than swallowing silently.

7-day Skip latch lives in `api.kv` keyed by SHA-1(remote) — UTC
rolling window; `altimate link` (user-initiated) deliberately
bypasses the latch.

New `altimate-code link` subcommand runs the same three-way flow
outside a TUI session via `@clack/prompts` for scripting / catch-up
after a Skip. Bails early with helpful messages when credentials
are missing or no git remote is set.

Tests: 17 unit tests covering project-name parsing, git detection
graceful failure, cache read/write + chmod + tenant-scoping (account-
switch invalidation), and Skip latch TTL semantics with UTC boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8FGy89Qpr39k8nCSpCcK2
…prompt

Two user-flagged issues on the Workspaces post-scan prompt landed in
7c7e17f:

1. Post-scan dialog raced the LLM's onboarding-menu streaming — the
   dialog painted while text was still generating, and Enter didn't
   register until streaming finished. Fix: arm a one-shot `session.idle`
   listener via `EventV2Bridge` from `onboarding-telemetry.ts` and
   publish `TuiEvent.CommandExecute` only after the session settles.
   Costs a few seconds of latency; kills the race.

2. `resolveProjectRemote` returned undefined for projects without a git
   remote (materialized sample dbt scaffolds, fresh scratch dirs), so
   the post-scan prompt and `altimate-code link` both bailed silently.
   Fix: new `resolveProjectIdentifier` in `workspace/detect.ts` always
   returns a `{repoRemote?, projectPath}` pair (path is symlink-resolved
   `realpath`). `ProjectIdentifier` type threads through `WorkspaceApi`,
   the TuiPlugin dialogs, and the `link` subcommand — remote is
   preferred when available (stronger identity, survives directory
   moves); path is the fallback the backend indexes symmetrically.

Also: `projectNameFromPath` fallback for auto-naming (derives from
directory basename when no remote); Skip-latch key hashes remote-or-path
so path-only projects also get the 7-day suppression; `runFlow` and
`runOnDemandPicker` reworked to use `WorkspaceApi.getBindingForProject`
(tries remote first, then path); `CachedBinding` in state.ts extended
with `projectPath: string | null`.

Tests updated + one new latch test covers the path-only case. `bun test
test/altimate/plugin/workspace.test.ts` → 18/18.
@gitguardian

gitguardian Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
33124345 Triggered Basic Auth String 7c7e17f packages/opencode/src/altimate/workspace/detect.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e26de9b-209b-43f9-9b19-48e7c686cdf7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

2 similar comments
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Haider and others added 4 commits August 17, 2026 03:53
…atched-identifier rebind, req() hardening

Addresses the review findings that belong to this PR's commits (7c7e17f
+ 76de5a9). The three remaining findings introduced by the stacked
browser-handoff PR are fixed on that branch.

- Gate the LinkCommand registration in src/index.ts AND the Workspace TUI
  plugin registration behind Flag.ALTIMATE_WORKSPACE. Previously the flag
  gated only the post-scan trigger publish, so the palette command,
  altimate-code link subcommand, and post-scan handler shipped to 100% of
  users regardless of the flag setting. (M1)
- createAndBindInline / createAndBind now accept an "already linked"
  outcome and rebind after create. Before this, "+ Create a new workspace"
  on an already-linked project silently orphaned the freshly-created
  workspace in the SaaS — a real (billable) resource the CLI knew nothing
  about. On rebind failure the error message tells the user the workspace
  exists and how to recover. (M2)
- getBindingForProject now returns which identifier arm matched (remote or
  path) via a new ``matchedBy`` field. AlreadyLinkedDialog, PickerDialog,
  bindOrRebindInline, and cli/cmd/link.ts all use matched-identifier for
  the rebind endpoint — not the CURRENT identifier — so a repo whose
  remote was renamed still repairs via its path binding instead of 404'ing
  on rebindByRemote. hasDrift is now computed from matched-vs-current
  identifier instead of hardcoded false. (M3)
- listDatamates now routes through req() (via a new ``base`` option) so it
  inherits the 15s abort, typed error mapping, empty-body guard, and
  detail parsing every other endpoint gets. Non-integer / non-positive ids
  are filtered out at the boundary. (M5)
- req() throws WorkspaceApiError on an empty 2xx body (previously returned
  undefined as T, producing a downstream TypeError the typed switches
  couldn't classify). ``allowEmptyBody`` opt-in for 204 endpoints. (m7)
- AbortError is now distinguished from a network failure — the 15s abort
  produces "Request timed out after 15s" instead of the generic "Cannot
  reach" message. (m8)
- Session-idle listener now captures the unsubscribe from events.listen()
  and tears itself down when the pending-sessions Set drains. Previously
  the listener was permanently installed for the process lifetime, and a
  failed install could leave a duplicate handler behind that fired
  workspace prompts twice. (m4)
- Failed pre-check in cli/cmd/link.ts now retries a bindExisting → 409 as
  an unconditional rebind, so a user whose pre-check network-flaked isn't
  stuck at "Already linked to X" with no next step. (m10)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016H42Vt4pt5dcD7opRqckeM
Adds a browser handoff for creating and linking a Workspace: CLI opens the
SaaS approval modal on `<tenant>.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.
…r tile

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016H42Vt4pt5dcD7opRqckeM
… re-verify, sidebar polish, cache canonicalization

Addresses the review findings introduced by this PR's commits (browser
handoff + top-level nav / sidebar tile). PR #1099 fixes landed
separately.

- `runHandoffWithOpener` now wraps preflight (`getCredentials`) AND the
  post-listener async IIFE in one try/catch that converts every error to
  a `HandoffResult`. Previously a malformed credentials file rejected the
  returned Promise with no toast, and a throw inside the lazy
  `import("../plugin/altimate")` left the caller waiting the full 15
  minutes with no reason surfaced. The port is captured into a local
  immediately after `startListener` resolves so a timeout-cleared handle
  can't be dereferenced later. (M4)
- `HandoffSuccess` now carries a `credentials` fingerprint (apiUrl +
  tenant) that the handoff was validated against. `runBrowserHandoff` in
  both entry points re-reads `AltimateApi.getCredentials()` immediately
  before `bindExisting` and refuses if either field drifted — workspace
  ids are tenant-schema-local so a mid-flow account switch would
  otherwise bind under the wrong tenant. (M6)
- `resolveWorkspaceWebUrl` guards the tenant with a DNS-label regex and
  reconstructs the origin from the parsed URL, so a credential row
  carrying `evil.example/path?x=` cannot open the handoff at
  `https://evil.example`. Override still available for local dev; both
  paths reject non-http(s) protocols. (m3)
- Optional `AbortSignal` on `OpenBrowserHandoffInput` — a caller-fired
  abort tears down the listener immediately with `reason: "aborted"`
  instead of holding the port for 15 minutes; timeout is `.unref()`'d so
  it doesn't keep the CLI process alive on its own. (m2)
- `port_exhausted` is now only returned when the errno is `EADDRINUSE`
  — other codes (EACCES, EBADF) map to `reason: "error"` so the user
  isn't told "ports all in use" for a permissions problem. (m5)
- `project_path` + `project_remote` moved to the URL fragment, matching
  the `cli_context` rationale — those two values carry usernames /
  customer names / internal paths that shouldn't land in SaaS access
  logs, WAF logs, or browser history. `project_name` stays in the query
  because the SaaS approval modal renders it. Test updated. (m6)
- `workspace_id` uses `Number.isInteger` instead of `Number.isFinite`,
  so `42.5` no longer reaches a backend expecting an integer. (m9)
- Inline `<script>` blocks now escape `</script` in JSON.stringify'd
  values via a `<\/script` replacement, closing the theoretical inline-
  script-break vector. (N5.b)
- Local binding cache: one-shot migration to canonical keys on the
  first `readLocalBinding` that finds a non-canonical key, followed by
  a plain property lookup for every subsequent read. Deletes the O(n)
  `realpathSync` rescan that ran on every cache miss under the 3s
  sidebar poll. (N1)
- Sidebar tile polls at 30s instead of 3s, memoizes the manage-URL
  base per (apiUrl, tenant), and guards against overlapping refreshes.
  Copy updated from "run /link" (the slash command doesn't exist —
  N2) to "run altimate-code link" (the actual CLI subcommand).
  Interval timer `.unref()`'d.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016H42Vt4pt5dcD7opRqckeM
@sahrizvi
sahrizvi force-pushed the feat/workspace-browser-handoff branch from 5340671 to cd33f3c Compare August 16, 2026 22:32
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant