Skip to content

feat(frontend): create an agent from a website template deep link#5423

Draft
mmabrouk wants to merge 1 commit into
mainfrom
feat/website-template-deeplink
Draft

feat(frontend): create an agent from a website template deep link#5423
mmabrouk wants to merge 1 commit into
mainfrom
feat/website-template-deeplink

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

What a user sees

A visitor on the marketing website clicks "Use this template" on a template card. They land on the app, sign up if they need an account, and arrive on onboarding with that template's prompt already staged: the empty-chat screen shows the prompt in a "We'll start with" card above a Start button. Nothing runs until they press Start. If they are already signed in, the same thing happens right away without a signup step.

This pull request is the app side of that flow. The website-side change, which adds ?template=<key> to each card link, ships separately.

How it works

The design is documented in docs/design/website-templates-deeplink/ (plan PR #5422). The app reuses the exact technique the workspace-invite flow already uses to survive a full signup.

  • Capture. A new module, web/oss/src/state/url/template.ts, reads the template query parameter and saves it to local storage with a capture-time timestamp. It is called from syncAuthStateFromUrl, right next to the invite capture, so both are read from the same URL on every navigation. The capture module is kept separate from auth.ts so that file does not become a registry of unrelated features.
  • Runtime validation. The URL parameter is user-controlled, so the key is validated by exact lookup against the app template registry (templates.ts) at consume time. An unknown or stale key is ignored and cleared from storage, never creates an agent, and never falls back to another template.
  • Consume. A new hook, useConsumePendingTemplate, is mounted in /apps above both first-run surfaces (the native onboarding playground for new users, the agent home page for returning users), so the create happens wherever the user lands. It waits for a confirmed workspace and project, claims the key, then calls useCreateAgent({name, seedMessage, autoSendSeed: false}) so the seed renders behind a Start button rather than auto-sending.
  • At most once. Local storage has no atomic compare-and-set, so the claim runs inside the Web Locks API (navigator.locks.request), which serializes the read-claim-write across same-origin tabs. Where the API is unavailable the claim degrades to a documented best-effort local-storage compare-and-set.
  • Region redirect. The cloud can redirect a visitor from cloud.agenta.ai to eu.cloud.agenta.ai or us.cloud.agenta.ai, which drops local storage but preserves the query string. Because capture runs on every navigation, it re-runs on the regional host and re-saves the key there, so the flow works on both the US and EU clouds.
  • Analytics. The consume path fires first_agent_intent events (source website_template) for the claim, a successful create, a failed create, and an invalid key, reusing the existing PostHog helper.

Files

  • web/oss/src/state/url/template.ts (new) — capture, storage, TTL, registry validation, clear, Web Locks claim.
  • web/oss/src/state/url/template.test.ts (new) — unit tests.
  • web/oss/src/state/url/auth.ts — one call to captureTemplateFromUrl next to the invite capture.
  • web/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.ts (new) — the consume hook.
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/index.tsx — mounts the consume hook.
  • web/oss/src/components/pages/agent-home/assets/onboardingAnalytics.ts — adds the website_template analytics source.

Tests

Unit tests (template.test.ts, 14 cases, all green) cover: parsing the parameter; storage round-trip; TTL expiry measured from capture time; capture stamping the time once and not resetting it; region recapture falling back to stored; clear removing storage, the atom, and the URL parameter so it cannot resurrect; registry validation rejecting an unknown key without substituting another; and the at-most-once claim under both the Web Locks path and the best-effort fallback.

These co-located oss/src/**/*.test.ts files follow the existing convention (templates.test.ts, lastAuthMethod.test.ts) and run under vitest. Note that web/oss has no vitest suite wired into CI today, so like the existing co-located tests they run via a direct vitest invocation, not the CI unit layer.

QA

Live browser QA of the full signup flow was not performed (a headless signup E2E is not feasible in this environment). Wire-level walkthrough to verify manually on the local or cloud stack:

  1. Open https://<host>/?template=pr-reviewer while signed out. In dev tools, confirm localStorage.pendingTemplate holds {"key":"pr-reviewer","capturedAt":<ts>} and the template param stays in the URL through the auth redirect.
  2. Complete signup. On landing at /w/<ws>/p/<proj>/apps, confirm you are taken into a new agent's playground with the PR-reviewer prompt shown in the "We'll start with" card above a Start button, with no assistant response yet.
  3. Confirm localStorage.pendingTemplate is cleared and the template param is gone from the URL.
  4. Open /?template=not-a-real-template: confirm no agent is created, normal onboarding proceeds, and the stored key is cleared.
  5. Region switch: on cloud.agenta.ai/?template=pr-reviewer, trigger a region switch and confirm the parameter survives to the regional host and is recaptured there.

An automated end-to-end Playwright test for the native-onboarding happy path is specified in the design plan and belongs with the website-side change that completes the flow.

https://claude.ai/code/session_013Qe1Vf2yvj33BVd5W1q7HB

Capture a ?template=<key> deep-link into localStorage on arrival (mirroring the
invite flow), validate the key at runtime against the app template registry, and
consume it at whichever first-run surface the new or returning user reaches. The
agent is created with the template prompt held behind a Start button (autoSendSeed
false). At-most-once creation is enforced with the Web Locks API and a documented
best-effort fallback. Capture re-runs after a region redirect so it works on both
the US and EU clouds. The website-side link change ships separately.

Claude-Session: https://claude.ai/code/session_013Qe1Vf2yvj33BVd5W1q7HB
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 20, 2026 7:19pm

Request Review

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for opening website templates through deep links.
    • Website templates are captured, validated, and preserved across redirects or page reloads.
    • Templates are claimed only once, preventing duplicate agent creation across tabs.
    • Added onboarding progress feedback while a template is being applied.
    • Added analytics tracking for successful, invalid, claimed, and failed template launches.
  • Bug Fixes

    • Expired or invalid template links are automatically discarded.

Walkthrough

Website template deep-links are captured from URLs, persisted with expiration, claimed at most once, and consumed by creating an agent from the template seed. The Apps page shows an onboarding loader while consumption runs, and analytics supports the new source.

Changes

Website template onboarding flow

Layer / File(s) Summary
Template capture, persistence, and claiming
web/oss/src/state/url/template.ts, web/oss/src/state/url/template.test.ts
Adds URL parsing, template validation, TTL-backed storage, atom synchronization, clearing, and Web Locks/localStorage claim coordination with coverage for each behavior.
Authentication URL integration
web/oss/src/state/url/auth.ts
Captures website template parameters during authentication URL synchronization, including regional redirect handling.
Agent creation and page gating
web/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.ts, web/oss/src/pages/w/.../apps/index.tsx, web/oss/src/components/pages/agent-home/assets/onboardingAnalytics.ts
Validates and claims pending templates, creates agents with template seeds, records outcomes, clears state, holds the loader during processing, and adds "website_template" as an intent source.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AuthURLSync
  participant TemplateState
  participant Apps
  participant AgentCreation
  User->>AuthURLSync: open website template deep-link
  AuthURLSync->>TemplateState: captureTemplateFromUrl(url)
  TemplateState-->>Apps: expose pending template
  Apps->>TemplateState: claimTemplate(key)
  TemplateState-->>Apps: claim result
  Apps->>AgentCreation: create agent from template seed
  AgentCreation-->>Apps: creation result
  Apps->>TemplateState: clearTemplate()
Loading

Possibly related PRs

  • Agenta-AI/agenta#5086: Defines related onboarding intent analytics helpers and types used by the updated source union.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: creating an agent from a website template deep link.
Description check ✅ Passed The description is detailed and directly matches the changeset, covering capture, validation, consumption, analytics, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/website-template-deeplink

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.ts (1)

60-62: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout if workspaceId/projectId never resolve.

The comment asserts both are always present on this route, but if that invariant ever breaks (stalled identifiers, invalid workspace segment), holding is already true from line 58 and the effect just returns with no fallback — the Apps page would be stuck behind OnboardingLoader indefinitely. Worth verifying whether appIdentifiersAtom can ever remain empty on this project-scoped route, and if so, adding a bounded wait before giving up and releasing the loader.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e3e13c3-a9f8-43ad-8927-1c13cfbe4cc1

📥 Commits

Reviewing files that changed from the base of the PR and between 3588f13 and 31aacdc.

📒 Files selected for processing (6)
  • web/oss/src/components/pages/agent-home/assets/onboardingAnalytics.ts
  • web/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.ts
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/index.tsx
  • web/oss/src/state/url/auth.ts
  • web/oss/src/state/url/template.test.ts
  • web/oss/src/state/url/template.ts

Comment on lines +37 to +38
const startedRef = useRef(false)
const [holding, setHolding] = useState<boolean>(() => Boolean(pendingKey))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

startedRef is a boolean, not keyed to pendingKey — a second template key gets silently stuck.

Once startedRef.current is set true for the first key, any later transition of activeTemplateAtom to a different non-null key (e.g., a second ?template= deep link captured while this page instance stays mounted) sets holding back to true at line 58 but then bails at line 63 before ever calling claimTemplate/createAgent/clearTemplate. Nothing ever flips holding back to false for that key, so apps/index.tsx renders OnboardingLoader forever until a full reload.

🐛 Proposed fix: key the guard to the specific pending key
-    const startedRef = useRef(false)
+    const startedForKeyRef = useRef<string | null>(null)
@@
-        if (!workspaceId || !projectId) return
-        if (startedRef.current) return
-        startedRef.current = true
+        if (!workspaceId || !projectId) return
+        if (startedForKeyRef.current === pendingKey) return
+        startedForKeyRef.current = pendingKey

Also applies to: 58-64, 105-105

Comment on lines +145 to +177
/**
* At-most-once claim. localStorage has no atomic compare-and-set, so two tabs could both read a
* key as unclaimed and both create. The Web Locks API serialises the read-claim-write across
* same-origin tabs, so only one caller can move the key from unclaimed to claimed. Where the API
* is unavailable the claim degrades to a best-effort local-storage compare-and-set, and in that
* narrow case the guarantee softens from at-most-once to best-effort. Returns true only for the
* caller that wins the claim.
*/
export const claimTemplate = async (key: string): Promise<boolean> => {
if (!hasWindow()) return false

const compareAndSet = (): boolean => {
try {
if (window.localStorage.getItem(TEMPLATE_CLAIM_KEY) === key) return false
window.localStorage.setItem(TEMPLATE_CLAIM_KEY, key)
return true
} catch (error) {
console.error("Failed to claim pending template:", error)
return false
}
}

const locks = (navigator as Navigator & {locks?: LockManager})?.locks
if (locks?.request) {
try {
return await locks.request(`pendingTemplate:${key}`, compareAndSet)
} catch (error) {
console.error("Web Locks claim failed, falling back to best-effort:", error)
return compareAndSet()
}
}
return compareAndSet()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Claim key has no expiry — an interrupted session permanently blocks re-consumption of the same key.

TEMPLATE_CLAIM_KEY is only cleared by clearTemplate(), which runs in the consuming hook's finally after createAgent settles. If the tab is closed/crashed before that (network hang, force-close), the claim value lingers forever with no TTL, unlike PendingTemplate which expires via TEMPLATE_TTL_MS. A later legitimate attempt with the same key will then always see compareAndSet return false, indistinguishable from "another tab won," permanently disabling that template deep link on that browser until localStorage is manually cleared.

🔒 Proposed fix: stamp claims with a timestamp and treat stale claims as expired
-    const compareAndSet = (): boolean => {
-        try {
-            if (window.localStorage.getItem(TEMPLATE_CLAIM_KEY) === key) return false
-            window.localStorage.setItem(TEMPLATE_CLAIM_KEY, key)
-            return true
-        } catch (error) {
-            console.error("Failed to claim pending template:", error)
-            return false
-        }
-    }
+    const compareAndSet = (): boolean => {
+        try {
+            const raw = window.localStorage.getItem(TEMPLATE_CLAIM_KEY)
+            if (raw) {
+                try {
+                    const claim = JSON.parse(raw) as {key: string; claimedAt: number}
+                    if (claim.key === key && Date.now() - claim.claimedAt < TEMPLATE_CLAIM_TTL_MS) {
+                        return false
+                    }
+                } catch {
+                    // corrupt claim record: treat as unclaimed
+                }
+            }
+            window.localStorage.setItem(
+                TEMPLATE_CLAIM_KEY,
+                JSON.stringify({key, claimedAt: Date.now()}),
+            )
+            return true
+        } catch (error) {
+            console.error("Failed to claim pending template:", error)
+            return false
+        }
+    }

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant