feat(frontend): create an agent from a website template deep link#5423
feat(frontend): create an agent from a website template deep link#5423mmabrouk wants to merge 1 commit into
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughWebsite 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. ChangesWebsite template onboarding flow
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()
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
web/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.ts (1)
60-62: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo timeout if
workspaceId/projectIdnever resolve.The comment asserts both are always present on this route, but if that invariant ever breaks (stalled identifiers, invalid workspace segment),
holdingis alreadytruefrom line 58 and the effect just returns with no fallback — the Apps page would be stuck behindOnboardingLoaderindefinitely. Worth verifying whetherappIdentifiersAtomcan 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
📒 Files selected for processing (6)
web/oss/src/components/pages/agent-home/assets/onboardingAnalytics.tsweb/oss/src/components/pages/agent-home/hooks/useConsumePendingTemplate.tsweb/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/index.tsxweb/oss/src/state/url/auth.tsweb/oss/src/state/url/template.test.tsweb/oss/src/state/url/template.ts
| const startedRef = useRef(false) | ||
| const [holding, setHolding] = useState<boolean>(() => Boolean(pendingKey)) |
There was a problem hiding this comment.
🩺 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 = pendingKeyAlso applies to: 58-64, 105-105
| /** | ||
| * 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() | ||
| } |
There was a problem hiding this comment.
🩺 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
+ }
+ }
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.web/oss/src/state/url/template.ts, reads thetemplatequery parameter and saves it to local storage with a capture-time timestamp. It is called fromsyncAuthStateFromUrl, right next to the invite capture, so both are read from the same URL on every navigation. The capture module is kept separate fromauth.tsso that file does not become a registry of unrelated features.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.useConsumePendingTemplate, is mounted in/appsabove 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 callsuseCreateAgent({name, seedMessage, autoSendSeed: false})so the seed renders behind a Start button rather than auto-sending.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.cloud.agenta.aitoeu.cloud.agenta.aiorus.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.first_agent_intentevents (sourcewebsite_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 tocaptureTemplateFromUrlnext 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 thewebsite_templateanalytics 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.tsfiles follow the existing convention (templates.test.ts,lastAuthMethod.test.ts) and run under vitest. Note thatweb/osshas 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:
https://<host>/?template=pr-reviewerwhile signed out. In dev tools, confirmlocalStorage.pendingTemplateholds{"key":"pr-reviewer","capturedAt":<ts>}and thetemplateparam stays in the URL through the auth redirect./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.localStorage.pendingTemplateis cleared and thetemplateparam is gone from the URL./?template=not-a-real-template: confirm no agent is created, normal onboarding proceeds, and the stored key is cleared.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