From 70eeaeab2f5816b5cec4a596ac6d2d815ee04a5c Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 22 Sep 2026 13:35:21 -0700 Subject: [PATCH 1/8] Collect Flows onboarding choices in the signup page --- web/app/signup/agent/[product]/route.ts | 7 ++- web/components/AgentSignupJourney.tsx | 47 ++++++++++++-- .../agent-signup-journey.module.css | 11 ++++ web/lib/agent-signup-progress.ts | 19 +++++- web/lib/agent-signup.ts | 62 ++++++++++++++++--- web/lib/test/agent-signup-progress.test.ts | 6 ++ web/lib/test/agent-signup.test.ts | 12 ++++ 7 files changed, 149 insertions(+), 15 deletions(-) diff --git a/web/app/signup/agent/[product]/route.ts b/web/app/signup/agent/[product]/route.ts index d662392..e63ff20 100644 --- a/web/app/signup/agent/[product]/route.ts +++ b/web/app/signup/agent/[product]/route.ts @@ -6,9 +6,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ prod const { product } = await params; if (!isAgentSignupProduct(product)) return new Response('Not found', { status: 404 }); const url = new URL(request.url); + const requestHost = request.headers.get('host'); + const localRequestHost = requestHost && /^(localhost|127\.0\.0\.1)(:\d{1,5})?$/.test(requestHost) ? requestHost : null; // The apex router's HTTP fallback rewrites the URL to the marketing origin. // Sign-in and /cloud remain on the public apex, never that upstream host. - const site = url.hostname === 'origin-web.agentrelay.com' ? SITE_URL : url.origin; + // Next dev can normalize request.url to localhost while the browser used + // 127.0.0.1; keep the exact local host so OAuth and the progress page agree. + const site = url.hostname === 'origin-web.agentrelay.com' ? SITE_URL + : ['localhost', '127.0.0.1'].includes(url.hostname) && localRequestHost ? `${url.protocol}//${localRequestHost}` : url.origin; const cloud = new URL(teamsCloudUrl(''), site).href.replace(/\/$/, ''); return new Response(agentSignupInstructions(product, site, cloud), { headers: { diff --git a/web/components/AgentSignupJourney.tsx b/web/components/AgentSignupJourney.tsx index b7361fb..10d848d 100644 --- a/web/components/AgentSignupJourney.tsx +++ b/web/components/AgentSignupJourney.tsx @@ -74,6 +74,9 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) const [expired, setExpired] = useState(false); const [attempt, setAttempt] = useState(0); const [showPrompt, setShowPrompt] = useState(false); + const [answerValue, setAnswerValue] = useState(''); + const [answerError, setAnswerError] = useState(''); + const [answerSubmitting, setAnswerSubmitting] = useState(false); const textarea = useRef(null); const boot = useRef | null>(null); const steps = signupSteps[product]; @@ -83,6 +86,9 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) const failed = progress?.state === 'failed'; const endpoint = origin ? new URL(apiPath, origin).href : ''; const prompt = progress && token ? trackedSignupPrompt(product, origin, endpoint, { id: progress.id, writeToken: token }) : ''; + const inputRequest = product === 'flows' ? progress?.inputRequest : undefined; + + useEffect(() => { setAnswerValue(''); setAnswerError(''); }, [inputRequest?.id]); latest.current = { step: active, owner: Boolean(token) }; useEffect(() => { @@ -159,16 +165,33 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) try { await navigator.clipboard.writeText(prompt); setCopyMessage('Copied. Paste it into your agent.'); analytics.track('prompt_copied'); } catch { analytics.track('manual_copy_shown'); setShowPrompt(true); setCopyMessage('Select and copy the prompt below.'); requestAnimationFrame(() => { textarea.current?.focus(); textarea.current?.select(); }); } } + async function submitAnswer(event: React.FormEvent) { + event.preventDefault(); + if (!progress || !token || !inputRequest || inputRequest.status !== 'pending' || !answerValue.trim() || answerSubmitting) return; + setAnswerSubmitting(true); setAnswerError(''); + try { + const response = await fetch(`${apiPath}/${encodeURIComponent(progress.id)}`, { + method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ requestId: inputRequest.id, answer: answerValue.trim() }), signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) throw new Error(response.status === 409 ? 'This question changed. Wait for the latest request.' : 'Could not send your answer. Please try again.'); + const next: unknown = await response.json(); + if (!isSignupProgress(next)) throw new Error('Could not confirm your answer. Please refresh the page.'); + setProgress(previous => !previous || next.revision >= previous.revision ? next : previous); + setAnswerValue(''); + } catch (cause) { setAnswerError(cause instanceof Error ? cause.message : 'Could not send your answer. Please try again.'); } + finally { setAnswerSubmitting(false); } + } function restart() { analytics.restart(Boolean(token)); try { sessionStorage.removeItem(storageKey(product)); } catch { /* Best effort. */ } window.location.assign(window.location.pathname); } - const title = expired ? 'Session expired' : complete ? 'You’re all set.' : failed ? 'Your agent needs a hand.' : paused ? 'A quick approval from you.' : active ? steps[active - 1].title : 'Waiting for your agent'; - const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : failed ? 'Check your agent’s conversation to resolve the issue. Progress will resume here.' : paused ? 'Follow the approval request in your agent’s conversation. We’ll pick up right here.' : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; + const title = expired ? 'Session expired' : complete ? 'You’re all set.' : failed ? 'Your agent needs a hand.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received.' : 'A quick approval from you.') : active ? steps[active - 1].title : 'Waiting for your agent'; + const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : failed ? (product === 'flows' ? 'Your agent will report what needs attention here.' : 'Check your agent’s conversation to resolve the issue. Progress will resume here.') : inputRequest?.status === 'pending' ? 'Answer on this page and your agent will keep going.' : paused ? (product === 'flows' ? inputRequest?.status === 'answered' ? 'Your answer is in. Your agent is moving to the next step.' : 'Complete the sign-in or connection approval page your agent opened. Setup will resume here.' : 'Follow the approval request in your agent’s conversation. We’ll pick up right here.') : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; const mode = expired || error ? 'offline' : complete ? 'complete' : failed ? 'failed' : paused ? 'paused' : active ? 'working' : 'waiting'; - const heading = complete ? 'All yours.' : active ? title : 'Leave it to your agent.'; + const heading = expired ? 'Session expired' : complete ? 'All yours.' : inputRequest?.status === 'pending' ? 'Your input is needed.' : active ? title : 'Leave it to your agent.'; return (
@@ -188,10 +211,24 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) )} {complete ? { if (token) analytics.track('dashboard_opened', active); }} className={s.primary} href={teamsCloudUrl(product === 'teams' ? '/dashboard/sessions' : '/dashboard')}>Open {product === 'teams' ? 'your workspace' : 'dashboard'} : !active ? : null} -

{complete ? '' : active ? (paused ? 'Your agent will continue after you approve.' : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

+

{complete ? '' : active ? (inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Your agent is processing your answer.' : 'Your agent will continue after you approve.') : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

+ {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || paused) &&
+ {inputRequest.status === 'pending' ? <> +

YOUR AGENT IS ASKING

+

{inputRequest.label}

+ {token ?
void submitAnswer(event)}> + {inputRequest.type === 'select' ? : setAnswerValue(event.target.value)} placeholder={inputRequest.key === 'approver' ? '@username' : inputRequest.key === 'repository' ? 'owner/repository' : 'Type your answer'} required />} + +
:

Open the original signup tab to answer this question.

} + {answerError &&

{answerError}

} + :

Answer sent. Your agent will continue setup here.

} +
}
- {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'} + {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : active ? `${active} of 5 · ${inputRequest?.status === 'pending' ? 'Waiting for your answer' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received' : 'Waiting for your approval') : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'}
{error &&

{error}

{!progress && }
}
{ if (event.currentTarget.open && token) analytics.track('details_opened', active); }}> diff --git a/web/components/agent-signup-journey.module.css b/web/components/agent-signup-journey.module.css index 4cfe698..32f6529 100644 --- a/web/components/agent-signup-journey.module.css +++ b/web/components/agent-signup-journey.module.css @@ -13,6 +13,16 @@ .primary { composes: btn btn-primary from global; } .primary:disabled { opacity: .45; cursor: default; transform: none; } .copyStatus { min-height: 20px; margin: 14px 0 0; color: #7899af; font-size: 11px; line-height: 1.7; } +.inputCard { box-sizing: border-box; margin: 30px auto 0; padding: 24px; max-width: 480px; border: 1px solid #8dc7e94d; border-radius: 14px; background: #0b2636e8; text-align: left; box-shadow: 0 16px 48px #030f1899; } +.inputEyebrow { margin: 0 0 9px; color: #8dc7e9; font-size: 10px; font-weight: 700; letter-spacing: .12em; } +.inputCard h2 { margin: 0 0 18px; font-size: 19px; font-weight: 500; line-height: 1.4; letter-spacing: -.025em; } +.inputCard form { display: flex; gap: 10px; } +.inputCard input, .inputCard select { min-width: 0; flex: 1; box-sizing: border-box; height: 44px; padding: 0 12px; border: 1px solid #45667a; border-radius: 8px; background: #071722; color: #e8f2fa; font: inherit; font-size: 13px; } +.inputCard input:focus-visible, .inputCard select:focus-visible { outline: 2px solid #8dc7e9; outline-offset: 2px; } +.inputCard button { flex: none; border: 0; border-radius: 8px; padding: 0 16px; background: #8dc7e9; color: #09202e; font: inherit; font-size: 12px; font-weight: 700; cursor: pointer; } +.inputCard button:disabled { opacity: .5; cursor: default; } +.inputCard .inputError { margin: 12px 0 0; color: #efb6a8; font-size: 12px; } +.inputSent { margin: 0; color: #a9d6c9; font-size: 13px; } .progress { margin: 32px 0 0; color: #9eb9cb; font-size: 11px; line-height: 1.7; } .progressDots { display: flex; gap: 6px; justify-content: center; margin-bottom: 11px; } .progressDots i { width: 23px; height: 2px; border-radius: 3px; background: #294355; transition: background 1s; } @@ -46,6 +56,7 @@ .main { padding-top: 43svh; padding-bottom: 30px; }.atmosphere { height: 720px; } .content h1 { font-size: 34px; }.subtitle { font-size: 14px; margin-bottom: 24px; } .progress { margin-top: 26px; }.copyStatus { font-size: 10px; } + .inputCard { padding: 20px; }.inputCard form { flex-direction: column; }.inputCard button { min-height: 44px; } } @media (max-height: 700px) and (min-width: 601px) { .main { padding-top: 360px; }.atmosphere { height: 720px; } diff --git a/web/lib/agent-signup-progress.ts b/web/lib/agent-signup-progress.ts index e2ac7c7..5ce78c2 100644 --- a/web/lib/agent-signup-progress.ts +++ b/web/lib/agent-signup-progress.ts @@ -4,6 +4,10 @@ export type SignupProgress = { id: string; product: AgentSignupProduct; step: number; state: 'working' | 'waiting' | 'failed' | 'complete'; revision: number; updatedAt: string; expiresAt: string; + inputRequest?: { + id: string; key: string; label: string; type: 'text' | 'select'; + options?: string[]; status: 'pending' | 'answered'; + }; }; export type SignupSession = SignupProgress & { writeToken: string }; @@ -13,11 +17,22 @@ export function isSignupProgress(value: unknown): value is SignupProgress { return typeof p.id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(p.id) && ['teams', 'flows'].includes(p.product) && Number.isInteger(p.step) && p.step >= 0 && p.step <= 5 && ['working', 'waiting', 'failed', 'complete'].includes(p.state) && (p.state !== 'complete' || p.step === 5) && - Number.isSafeInteger(p.revision) && p.revision >= 0 && typeof p.updatedAt === 'string' && typeof p.expiresAt === 'string' && Number.isFinite(Date.parse(p.updatedAt)) && Number.isFinite(Date.parse(p.expiresAt)); + Number.isSafeInteger(p.revision) && p.revision >= 0 && typeof p.updatedAt === 'string' && typeof p.expiresAt === 'string' && Number.isFinite(Date.parse(p.updatedAt)) && Number.isFinite(Date.parse(p.expiresAt)) && + (p.inputRequest === undefined || isSignupInputRequest(p.inputRequest)); +} + +function isSignupInputRequest(value: unknown): boolean { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const input = value as NonNullable; + return Object.keys(input).every(key => ['id', 'key', 'label', 'type', 'options', 'status'].includes(key)) && + typeof input.id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.id) && + typeof input.key === 'string' && typeof input.label === 'string' && ['text', 'select'].includes(input.type) && + ['pending', 'answered'].includes(input.status) && + (input.type === 'text' ? input.options === undefined : Array.isArray(input.options) && input.options.every(option => typeof option === 'string')); } export function trackedSignupPrompt(product: AgentSignupProduct, origin: string, endpoint: string, session: Pick) { - return `${agentSignupPrompt(product, origin)}\n\nReport real progress using the guide’s protocol so I can watch the signup page.\n\nProgress session: ${session.id}\nProgress API: ${endpoint}/${session.id}\nProgress token: ${session.writeToken}\n\nKeep this token private; it authorizes progress updates only.`; + return `${agentSignupPrompt(product, origin)}\n\nReport real progress using the guide’s protocol so I can watch the signup page.${product === 'flows' ? ' Request any missing non-secret choices through the web-input protocol in the guide; do not ask me to answer in chat.' : ''}\n\nProgress session: ${session.id}\nProgress API: ${endpoint}/${session.id}\nProgress token: ${session.writeToken}\n\nKeep this token private; it authorizes progress and web-input requests only.`; } export const signupSteps = { diff --git a/web/lib/agent-signup.ts b/web/lib/agent-signup.ts index 66a124e..180c24c 100644 --- a/web/lib/agent-signup.ts +++ b/web/lib/agent-signup.ts @@ -19,8 +19,9 @@ You are setting up Agent Relay on behalf of the user who gave you this URL. ${agentSignupInteractionPolicy} Do the API calls, installation, configuration, and verification yourself. Keep working across user approvals; do not hand the user a checklist to execute. -The signup page is a spectator view for the user. Do not read or control it; -report progress only with GET/PATCH on the supplied Progress API URL. +${product === 'flows' ? `The signup page is a live view for the user. Do not read or control it; +report progress and request choices only through the supplied Progress API URL.` : `The signup page is a spectator view for the user. Do not read or control it; +report progress only with GET/PATCH on the supplied Progress API URL.`} For an approval URL, use the OS URL opener (for example macOS open with the URL passed as a separate subprocess argument, never interpolated into shell code). If no opener is available, give the link to the user. Do not launch a @@ -52,6 +53,7 @@ response fields, authentication, polling and error handling. - Refresh: POST ${cloud}/api/v1/auth/token/refresh before token expiry. - Progress: GET/PATCH the exact Progress API URL in the user's prompt; PATCH uses the separate Progress token, not the account access token. +${product === 'flows' ? '- Web input: POST the Progress API URL to request a choice, then GET it with the Progress token to read the answer. The original browser tab submits the choice with PUT.' : ''} ${product === 'teams' ? `- Desktop install/connect/share/status: the bundled agent-relay-probe CLI in sections 2–4. These are local machine operations, not dashboard clicks; there is no public HTTP endpoint that installs an app on the user's Mac.` : `- Flow catalog: GET ${site}/api/v1/flows/catalog and /. @@ -67,7 +69,8 @@ ${product === 'teams' ? `- Desktop install/connect/share/status: the bundled age The user is watching a setup page. Report real milestones using the Progress API and Progress token supplied in their prompt. The token authorizes progress only; it is NOT a Cloud access token. Never send account tokens, OAuth codes, passwords, -logs, approval URLs, or personal information to the progress endpoint. Do not put +logs, approval URLs, or sensitive personal information to the progress endpoint. +${product === 'flows' ? 'The web-input protocol may carry a repository name or public GitHub approver handle when the user chooses it; do not request emails or secrets.' : ''} Do not put the progress token in URLs or output it in your final reply. Use the same site and /cloud origin shown above; never forward it to another environment. @@ -114,6 +117,49 @@ stop reporting (the session expired or the token is invalid) and tell the user; do not recreate or switch their session silently. A progress service outage must not roll back working setup or cause duplicate installation/activation. +${product === 'flows' ? `## Web input — ask on the signup page, never in chat + +When a repository, workflow, trigger, approver, or confirmation is missing, +use the Progress API to show the question in the user's original signup tab. +Do not ask the user to answer in your chat. Do not operate the page yourself. +Only request non-secret choices; never ask for passwords, OAuth codes, API keys, +or sensitive personal data through this endpoint. Sign-in and provider consent stay on +their own approval pages, opened for the user as described below. + +First PATCH the current progress step to state: waiting using its latest +revision. Then POST the exact Progress API URL with Authorization: Bearer + and Content-Type: application/json: + +~~~json +{"key":"repository","label":"Which owner/repository should this flow use?","type":"text"} +~~~ + +For a choice list use type: select and options, for example: + +~~~json +{"key":"workflow","label":"Which workflow should we activate?","type":"select","options":["software-factory","code-review"]} +~~~ + +Keys are stable lowercase identifiers (up to 40 characters); labels are at +most 160 characters. Ask one question at a time. The response has +inputRequest.id and status: pending. Repeating the same key is idempotent; +a different question while one is pending returns 409 input_pending. +The original browser tab can answer; a read-only watcher link cannot. +Do not include answers in progress PATCH bodies or in final chat output. + +Poll GET on the same Progress API URL with Authorization: Bearer +every 3 seconds until inputRequest.id matches and status is answered. The +authenticated GET includes inputRequest.answer. An unauthenticated GET never +includes the answer. Check the answer against the catalog/repository contract, +then PATCH the current step back to working using the latest revision. If the +session expires or the page cannot accept input, report that blocker; do not +silently switch to chat questions or fabricate a choice. + +The bearer token is shared only with the agent and the original browser tab; +keep it out of URLs and logs. Its authorization does not grant Cloud account +access. PUT is for the browser to submit a choice, not an agent shortcut. +` : ''} + ## 1. Sign up and obtain an API session Use the existing OAuth device flow. No API key, invitation, dashboard wizard, @@ -301,10 +347,12 @@ function flowsInstructions(site: string, cloud: string): string { return ` ## 2. Choose the flow and repository -Ask only for missing product choices: repository, desired workflow/trigger, and -approver. For GitHub use the approver's GitHub login as github:@handle (for -example github:@octocat), not their Google email. Human-gate replies are matched -to the provider identity. Infer choices from the user's request and current +Request missing product choices through the web-input protocol above: +repository, desired workflow/trigger, and approver. Ask for the approver's +GitHub username in plain language (for example, "octocat" or "@octocat"), +not an internal provider-address format or their Google email. Normalize the +answer to github:@handle when constructing the deploy API request. Human-gate +replies are matched to that provider identity. Infer choices from the user's request and current repository where clear. Do not invent a repository or enable automation on an unrelated project. diff --git a/web/lib/test/agent-signup-progress.test.ts b/web/lib/test/agent-signup-progress.test.ts index ea318e2..751c742 100644 --- a/web/lib/test/agent-signup-progress.test.ts +++ b/web/lib/test/agent-signup-progress.test.ts @@ -9,6 +9,9 @@ describe('signup progress handoff', () => { expect(isSignupProgress(progress)).toBe(true); for (const invalid of [null, {}, {...progress, step: 6}, {...progress, state: 'complete'}, {...progress, id: '../'.repeat(12)}, {...progress, updatedAt: 0}, {...progress, revision: -1}]) expect(isSignupProgress(invalid)).toBe(false); expect(isSignupProgress({...progress, step: 5, state: 'complete'})).toBe(true); + expect(isSignupProgress({...progress, product: 'flows', inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'text', status: 'pending' }})).toBe(true); + expect(isSignupProgress({...progress, inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'select', status: 'pending' }})).toBe(false); + expect(isSignupProgress({...progress, inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'text', status: 'answered', answer: 'private/repo' }})).toBe(false); }); it.each(['teams', 'flows'] as const)('carries the %s progress capability separately from URLs and preserves the local environment', (product) => { const token = 'a'.repeat(64); @@ -21,6 +24,7 @@ describe('signup progress handoff', () => { expect(prompt).toContain(`Progress API: http://localhost:3100/cloud/api/v1/signup/agent/sessions/${id}`); expect(prompt).not.toContain('/api/v1/auth/device/start'); expect(prompt).toContain(`Progress token: ${token}`); + if (product === 'flows') expect(prompt).toContain('do not ask me to answer in chat'); expect(prompt.match(/https?:\/\/\S+/g)?.every(url => !url.includes(token))).toBe(true); const guide = agentSignupInstructions(product, 'http://localhost:3100', 'http://localhost:3100/cloud'); expect(guide).toContain('Authorization: Bearer '); @@ -40,6 +44,8 @@ describe('signup progress handoff', () => { } else { expect(guide).toContain('POST http://localhost:3100/cloud/api/v1/flows/deploy'); expect(guide).toContain('GET http://localhost:3100/api/v1/flows/catalog'); + expect(guide).toContain('POST the exact Progress API URL'); + expect(guide).toContain('authenticated GET includes inputRequest.answer'); } }); }); diff --git a/web/lib/test/agent-signup.test.ts b/web/lib/test/agent-signup.test.ts index 7ef401f..05bd373 100644 --- a/web/lib/test/agent-signup.test.ts +++ b/web/lib/test/agent-signup.test.ts @@ -46,6 +46,15 @@ describe('agent signup instructions', () => { expect(agentSignupPrompt('teams', 'http://127.0.0.1:3199')).toContain('http://127.0.0.1:3199/signup/agent/teams'); }); + it('uses the browser local hostname when Next normalizes the request URL', async () => { + vi.stubEnv('NEXT_PUBLIC_CLOUD_URL', '/cloud'); + const content = await (await GET(new Request('http://localhost:3100/signup/agent/flows', { headers: { host: '127.0.0.1:3100' } }), { + params: Promise.resolve({ product: 'flows' }), + })).text(); + expect(content).toContain('Cloud API base: http://127.0.0.1:3100/cloud'); + expect(content).toContain('POST the Progress API URL to request a choice'); + }); + it.each(['unknown', 'Teams', 'flows/extra'])('returns 404 for unsupported product %s', async (product) => { const response = await GET(new Request('https://agentrelay.com/signup/agent/unknown'), { params: Promise.resolve({ product }), @@ -80,6 +89,9 @@ describe('agent signup instructions', () => { expect(deploy).not.toHaveProperty('flowId'); expect(deploy).not.toHaveProperty('repositories'); expect(content).toContain('one deployment per'); + expect(content).toContain('Ask for the approver\'s'); + expect(content).toContain('GitHub username in plain language'); + expect(content).toContain('Normalize the'); expect(content).not.toContain(''); }); }); From df42acb24839626e22c30295d9919fb7fa89394d Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 22 Sep 2026 13:58:02 -0700 Subject: [PATCH 2/8] Gate webhook flows on OAuth and repository access, not optional syncs --- web/lib/agent-signup.ts | 10 ++++++++-- web/lib/test/agent-signup.test.ts | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/lib/agent-signup.ts b/web/lib/agent-signup.ts index 180c24c..59fe933 100644 --- a/web/lib/agent-signup.ts +++ b/web/lib/agent-signup.ts @@ -380,8 +380,14 @@ private. Connect only the repository and tools the chosen flow requires. For GitHub the user must grant repository access. Repeat with the chosen trigger provider if different. Reuse existing ready connections rather than relinking. Check GET /api/v1/workspaces//integrations//status -until ready is true (poll with backoff and a bounded timeout); a returned connect -link or a closed popup alone does not prove the integration is ready. +until oauth.connected is true (poll with backoff and a bounded timeout); a +returned connect link or a closed popup alone does not prove the connection. +For the GitHub-triggered Software Garden flow, activation separately checks +that the connected GitHub App covers the selected repository. Its trigger +does not require background data indexing, so do not block solely because +the broader status.ready is false from queued syncs. +If a different flow declares Relayfile data that requires synced records, +verify that readiness separately before activating it. Do not connect a Claude or Codex subscription yet. The first three runs use Cloud's own model key, so no provider login is needed to activate. After those diff --git a/web/lib/test/agent-signup.test.ts b/web/lib/test/agent-signup.test.ts index 05bd373..690c8fe 100644 --- a/web/lib/test/agent-signup.test.ts +++ b/web/lib/test/agent-signup.test.ts @@ -92,6 +92,10 @@ describe('agent signup instructions', () => { expect(content).toContain('Ask for the approver\'s'); expect(content).toContain('GitHub username in plain language'); expect(content).toContain('Normalize the'); + expect(content).toContain('oauth.connected is true'); + expect(content).toContain('does not require background data indexing'); + expect(content).not.toContain('deploy Nango syncs'); + expect(content).not.toContain('until ready is true'); expect(content).not.toContain(''); }); }); From a3296802b22fd1254d99a624dabd86953c390681 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 22 Sep 2026 14:04:17 -0700 Subject: [PATCH 3/8] Keep promotional Flow signup off user model login when house routing is unavailable --- web/lib/agent-signup.ts | 9 +++++++++ web/lib/test/agent-signup.test.ts | 3 +++ 2 files changed, 12 insertions(+) diff --git a/web/lib/agent-signup.ts b/web/lib/agent-signup.ts index 59fe933..46b10dd 100644 --- a/web/lib/agent-signup.ts +++ b/web/lib/agent-signup.ts @@ -395,6 +395,15 @@ runs, activation and launches will ask for your own subscription; only then use the official Relay CLI with the private credential environment from step 1 and a PTY: +This promotion depends on Cloud's internal house-key proxy and account +enrollment. If local activation returns flow_credentials_unavailable or +flow_model_not_connected before the three promotional runs, treat it as an +internal configuration or eligibility issue. Do not ask the user to connect +Claude/Codex, provide an API key, or choose an inactive draft as a workaround. +Report the blocker through the Progress API and have an internal developer +verify the proxy, promotion flag, provider readiness, and enrollment. Never +copy a house key into the agent environment or expose it in this guide. + ~~~sh npx --yes agent-relay@latest cloud connect anthropic --api-url '${cloud}' ~~~ diff --git a/web/lib/test/agent-signup.test.ts b/web/lib/test/agent-signup.test.ts index 690c8fe..feb7f3f 100644 --- a/web/lib/test/agent-signup.test.ts +++ b/web/lib/test/agent-signup.test.ts @@ -94,6 +94,9 @@ describe('agent signup instructions', () => { expect(content).toContain('Normalize the'); expect(content).toContain('oauth.connected is true'); expect(content).toContain('does not require background data indexing'); + expect(content).toContain('flow_credentials_unavailable'); + expect(content).toContain('Do not ask the user to connect'); + expect(content).toContain('internal house-key proxy'); expect(content).not.toContain('deploy Nango syncs'); expect(content).not.toContain('until ready is true'); expect(content).not.toContain(''); From 0c83f55696023e4b493b7dd012beaaa57b3c236b Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 22 Sep 2026 14:36:50 -0700 Subject: [PATCH 4/8] Show saved Flow preview and activation next step on signup page --- web/components/AgentSignupJourney.tsx | 19 ++++++++++++------- .../agent-signup-journey.module.css | 3 +++ web/lib/agent-signup-progress.ts | 12 +++++++----- web/lib/agent-signup.ts | 12 ++++++++++++ web/lib/test/agent-signup-progress.test.ts | 2 ++ web/lib/test/agent-signup.test.ts | 2 ++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/web/components/AgentSignupJourney.tsx b/web/components/AgentSignupJourney.tsx index 10d848d..e5b0048 100644 --- a/web/components/AgentSignupJourney.tsx +++ b/web/components/AgentSignupJourney.tsx @@ -187,11 +187,12 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) try { sessionStorage.removeItem(storageKey(product)); } catch { /* Best effort. */ } window.location.assign(window.location.pathname); } - const title = expired ? 'Session expired' : complete ? 'You’re all set.' : failed ? 'Your agent needs a hand.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received.' : 'A quick approval from you.') : active ? steps[active - 1].title : 'Waiting for your agent'; - const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : failed ? (product === 'flows' ? 'Your agent will report what needs attention here.' : 'Check your agent’s conversation to resolve the issue. Progress will resume here.') : inputRequest?.status === 'pending' ? 'Answer on this page and your agent will keep going.' : paused ? (product === 'flows' ? inputRequest?.status === 'answered' ? 'Your answer is in. Your agent is moving to the next step.' : 'Complete the sign-in or connection approval page your agent opened. Setup will resume here.' : 'Follow the approval request in your agent’s conversation. We’ll pick up right here.') : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; + const notice = product === 'flows' && inputRequest?.type === 'notice' && inputRequest.status === 'pending' ? inputRequest : undefined; + const title = expired ? 'Session expired' : complete ? 'You’re all set.' : notice ? 'Your Flow preview is saved.' : failed ? 'Your agent needs a hand.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received.' : 'A quick approval from you.') : active ? steps[active - 1].title : 'Waiting for your agent'; + const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : notice ? 'Review the inactive draft below. Internal model routing must be configured before live activation.' : failed ? (product === 'flows' ? 'Your agent will report what needs attention here.' : 'Check your agent’s conversation to resolve the issue. Progress will resume here.') : inputRequest?.status === 'pending' ? 'Answer on this page and your agent will keep going.' : paused ? (product === 'flows' ? inputRequest?.status === 'answered' ? 'Your answer is in. Your agent is moving to the next step.' : 'Complete the sign-in or connection approval page your agent opened. Setup will resume here.' : 'Follow the approval request in your agent’s conversation. We’ll pick up right here.') : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; const mode = expired || error ? 'offline' : complete ? 'complete' : failed ? 'failed' : paused ? 'paused' : active ? 'working' : 'waiting'; - const heading = expired ? 'Session expired' : complete ? 'All yours.' : inputRequest?.status === 'pending' ? 'Your input is needed.' : active ? title : 'Leave it to your agent.'; + const heading = expired ? 'Session expired' : complete ? 'All yours.' : notice ? 'Preview saved.' : inputRequest?.status === 'pending' ? 'Your input is needed.' : active ? title : 'Leave it to your agent.'; return (
@@ -211,9 +212,13 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) )} {complete ? { if (token) analytics.track('dashboard_opened', active); }} className={s.primary} href={teamsCloudUrl(product === 'teams' ? '/dashboard/sessions' : '/dashboard')}>Open {product === 'teams' ? 'your workspace' : 'dashboard'} : !active ? : null} -

{complete ? '' : active ? (inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Your agent is processing your answer.' : 'Your agent will continue after you approve.') : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

- {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || paused) &&
- {inputRequest.status === 'pending' ? <> +

{complete ? '' : notice ? 'No action is required to keep this draft saved.' : active ? (inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Your agent is processing your answer.' : 'Your agent will continue after you approve.') : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

+ {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || paused) &&
+ {notice ? <> +

INACTIVE PREVIEW

+

{notice.label}

+ {notice.actionHref && Open saved preview } + : inputRequest.status === 'pending' ? <>

YOUR AGENT IS ASKING

{inputRequest.label}

{token ?
void submitAnswer(event)}> @@ -228,7 +233,7 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct })
}
- {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : active ? `${active} of 5 · ${inputRequest?.status === 'pending' ? 'Waiting for your answer' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received' : 'Waiting for your approval') : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'} + {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : active ? `${active} of 5 · ${inputRequest?.status === 'pending' ? 'Waiting for your answer' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received' : 'Waiting for your approval') : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'}
{error &&

{error}

{!progress && }
}
{ if (event.currentTarget.open && token) analytics.track('details_opened', active); }}> diff --git a/web/components/agent-signup-journey.module.css b/web/components/agent-signup-journey.module.css index 32f6529..30209e1 100644 --- a/web/components/agent-signup-journey.module.css +++ b/web/components/agent-signup-journey.module.css @@ -23,6 +23,9 @@ .inputCard button:disabled { opacity: .5; cursor: default; } .inputCard .inputError { margin: 12px 0 0; color: #efb6a8; font-size: 12px; } .inputSent { margin: 0; color: #a9d6c9; font-size: 13px; } +.inputAction { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; box-sizing: border-box; padding: 0 15px; border-radius: 8px; background: #8dc7e9; color: #09202e; font-size: 13px; font-weight: 700; text-decoration: none; } +.inputAction:hover { background: #b3ddf4; } +.inputAction:focus-visible { outline: 2px solid #d4efff; outline-offset: 3px; } .progress { margin: 32px 0 0; color: #9eb9cb; font-size: 11px; line-height: 1.7; } .progressDots { display: flex; gap: 6px; justify-content: center; margin-bottom: 11px; } .progressDots i { width: 23px; height: 2px; border-radius: 3px; background: #294355; transition: background 1s; } diff --git a/web/lib/agent-signup-progress.ts b/web/lib/agent-signup-progress.ts index 5ce78c2..c2838ef 100644 --- a/web/lib/agent-signup-progress.ts +++ b/web/lib/agent-signup-progress.ts @@ -5,8 +5,8 @@ export type SignupProgress = { state: 'working' | 'waiting' | 'failed' | 'complete'; revision: number; updatedAt: string; expiresAt: string; inputRequest?: { - id: string; key: string; label: string; type: 'text' | 'select'; - options?: string[]; status: 'pending' | 'answered'; + id: string; key: string; label: string; type: 'text' | 'select' | 'notice'; + options?: string[]; actionHref?: string; status: 'pending' | 'answered'; }; }; export type SignupSession = SignupProgress & { writeToken: string }; @@ -24,11 +24,13 @@ export function isSignupProgress(value: unknown): value is SignupProgress { function isSignupInputRequest(value: unknown): boolean { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const input = value as NonNullable; - return Object.keys(input).every(key => ['id', 'key', 'label', 'type', 'options', 'status'].includes(key)) && + return Object.keys(input).every(key => ['id', 'key', 'label', 'type', 'options', 'actionHref', 'status'].includes(key)) && typeof input.id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.id) && - typeof input.key === 'string' && typeof input.label === 'string' && ['text', 'select'].includes(input.type) && + typeof input.key === 'string' && typeof input.label === 'string' && ['text', 'select', 'notice'].includes(input.type) && ['pending', 'answered'].includes(input.status) && - (input.type === 'text' ? input.options === undefined : Array.isArray(input.options) && input.options.every(option => typeof option === 'string')); + (input.type === 'notice' + ? input.status === 'pending' && input.options === undefined && typeof input.actionHref === 'string' && /^\/dashboard\/workflows\/listeners\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.actionHref) + : input.actionHref === undefined && (input.type === 'text' ? input.options === undefined : Array.isArray(input.options) && input.options.every(option => typeof option === 'string'))); } export function trackedSignupPrompt(product: AgentSignupProduct, origin: string, endpoint: string, session: Pick) { diff --git a/web/lib/agent-signup.ts b/web/lib/agent-signup.ts index 46b10dd..4f6d006 100644 --- a/web/lib/agent-signup.ts +++ b/web/lib/agent-signup.ts @@ -147,6 +147,18 @@ a different question while one is pending returns 409 input_pending. The original browser tab can answer; a read-only watcher link cannot. Do not include answers in progress PATCH bodies or in final chat output. +If the user explicitly chooses to save an inactive preview, verify the draft +through GET /api/v1/flows/listeners/, then POST a non-interactive +notice to the same Progress API URL. Use type: notice, a concise label, and +actionHref set only to /dashboard/workflows/listeners/ (a UUID). The +page will show a preview link and say activation is still pending. Notices +cannot collect answers and must not be used to claim an inactive draft is a +completed signup: + +~~~json +{"key":"draft_saved","label":"Your Flow is saved as an inactive preview. It will not run until activated.","type":"notice","actionHref":"/dashboard/workflows/listeners/537e4857-5590-42e8-8731-66441b466542"} +~~~ + Poll GET on the same Progress API URL with Authorization: Bearer every 3 seconds until inputRequest.id matches and status is answered. The authenticated GET includes inputRequest.answer. An unauthenticated GET never diff --git a/web/lib/test/agent-signup-progress.test.ts b/web/lib/test/agent-signup-progress.test.ts index 751c742..5d2d3d9 100644 --- a/web/lib/test/agent-signup-progress.test.ts +++ b/web/lib/test/agent-signup-progress.test.ts @@ -12,6 +12,8 @@ describe('signup progress handoff', () => { expect(isSignupProgress({...progress, product: 'flows', inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'text', status: 'pending' }})).toBe(true); expect(isSignupProgress({...progress, inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'select', status: 'pending' }})).toBe(false); expect(isSignupProgress({...progress, inputRequest: { id, key: 'repository', label: 'Which repository?', type: 'text', status: 'answered', answer: 'private/repo' }})).toBe(false); + expect(isSignupProgress({...progress, product: 'flows', inputRequest: { id, key: 'draft_saved', label: 'Preview saved.', type: 'notice', status: 'pending', actionHref: `/dashboard/workflows/listeners/${id}` }})).toBe(true); + expect(isSignupProgress({...progress, inputRequest: { id, key: 'draft_saved', label: 'Preview saved.', type: 'notice', status: 'pending', actionHref: 'https://evil.test' }})).toBe(false); }); it.each(['teams', 'flows'] as const)('carries the %s progress capability separately from URLs and preserves the local environment', (product) => { const token = 'a'.repeat(64); diff --git a/web/lib/test/agent-signup.test.ts b/web/lib/test/agent-signup.test.ts index feb7f3f..b2ec855 100644 --- a/web/lib/test/agent-signup.test.ts +++ b/web/lib/test/agent-signup.test.ts @@ -97,6 +97,8 @@ describe('agent signup instructions', () => { expect(content).toContain('flow_credentials_unavailable'); expect(content).toContain('Do not ask the user to connect'); expect(content).toContain('internal house-key proxy'); + expect(content).toContain('type: notice'); + expect(content).toContain('completed signup'); expect(content).not.toContain('deploy Nango syncs'); expect(content).not.toContain('until ready is true'); expect(content).not.toContain(''); From 7528c0053bbbcf42c6b066594ce6dce55a41809b Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 23 Sep 2026 17:01:43 +0200 Subject: [PATCH 5/8] fix(signup): preserve answered input state at step zero Session-Id: 01a0cc08-1399-7b02-871e-542dc1e28509 --- node_modules | 1 - web/components/AgentSignupJourney.tsx | 26 +++++++++++++++----------- web/e2e/agent-signup.spec.ts | 16 ++++++++++++++++ web/node_modules | 1 - 4 files changed, 31 insertions(+), 13 deletions(-) delete mode 120000 node_modules delete mode 120000 web/node_modules diff --git a/node_modules b/node_modules deleted file mode 120000 index 0ffbcc1..0000000 --- a/node_modules +++ /dev/null @@ -1 +0,0 @@ -/home/khaliqgant/Projects/AgentWorkforce/agentrelay-112-fix/node_modules \ No newline at end of file diff --git a/web/components/AgentSignupJourney.tsx b/web/components/AgentSignupJourney.tsx index 992c096..8a25a07 100644 --- a/web/components/AgentSignupJourney.tsx +++ b/web/components/AgentSignupJourney.tsx @@ -82,11 +82,15 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) const steps = signupSteps[product]; const complete = progress?.state === 'complete'; const active = progress?.step || 0; + const inputRequest = product === 'flows' ? progress?.inputRequest : undefined; + // An answered web-input request can arrive before the agent advances the + // numbered progress step. Keep the response card visible at step 0 so the + // user sees confirmation instead of the initial copy-prompt state. + const answeredInput = product === 'flows' && inputRequest?.status === 'answered'; const paused = progress?.state === 'waiting' && active > 0; const failed = progress?.state === 'failed'; const endpoint = origin ? new URL(apiPath, origin).href : ''; const prompt = progress && token ? trackedSignupPrompt(product, origin, endpoint, { id: progress.id, writeToken: token }) : ''; - const inputRequest = product === 'flows' ? progress?.inputRequest : undefined; useEffect(() => { setAnswerValue(''); setAnswerError(''); }, [inputRequest?.id]); @@ -194,11 +198,11 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct }) window.location.assign(window.location.pathname); } const notice = product === 'flows' && inputRequest?.type === 'notice' && inputRequest.status === 'pending' ? inputRequest : undefined; - const title = expired ? 'Session expired' : complete ? 'You’re all set.' : notice ? 'Your Flow preview is saved.' : failed ? 'Your agent needs a hand.' : paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received.' : 'A quick approval from you.') : active ? steps[active - 1].title : 'Waiting for your agent'; - const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : notice ? 'Review the inactive draft below. Internal model routing must be configured before live activation.' : failed ? (product === 'flows' ? 'Your agent will report what needs attention here.' : 'Check your agent’s conversation to resolve the issue. Progress will resume here.') : inputRequest?.status === 'pending' ? 'Answer on this page and your agent will keep going.' : paused ? (product === 'flows' ? inputRequest?.status === 'answered' ? 'Your answer is in. Your agent is moving to the next step.' : 'Complete the sign-in or connection approval page your agent opened. Setup will resume here.' : 'Follow the approval request in your agent’s conversation. We’ll pick up right here.') : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; - const mode = expired || error ? 'offline' : complete ? 'complete' : failed ? 'failed' : paused ? 'paused' : active ? 'working' : 'waiting'; + const title = expired ? 'Session expired' : complete ? 'You’re all set.' : notice ? 'Your Flow preview is saved.' : failed ? 'Your agent needs a hand.' : answeredInput ? 'Answer received.' : paused ? 'A quick approval from you.' : active ? steps[active - 1].title : 'Waiting for your agent'; + const detail = expired ? 'Start a new session to keep watching setup.' : complete ? 'Your agent has verified setup. You’re ready to go.' : notice ? 'Review the inactive draft below. Internal model routing must be configured before live activation.' : failed ? (product === 'flows' ? 'Your agent will report what needs attention here.' : 'Check your agent’s conversation to resolve the issue. Progress will resume here.') : inputRequest?.status === 'pending' ? 'Answer on this page and your agent will keep going.' : answeredInput ? 'Your answer is in. Your agent is moving to the next step.' : paused ? (product === 'flows' ? 'Complete the sign-in or connection approval page your agent opened. Setup will resume here.' : 'Follow the approval request in your agent’s conversation. We’ll pick up right here.') : active ? steps[active - 1].detail : 'The show starts when you paste the prompt into your agent.'; + const mode = expired || error ? 'offline' : complete ? 'complete' : failed ? 'failed' : paused || answeredInput ? 'paused' : active ? 'working' : 'waiting'; - const heading = expired ? 'Session expired' : complete ? 'All yours.' : notice ? 'Preview saved.' : inputRequest?.status === 'pending' ? 'Your input is needed.' : failed || active ? title : 'Leave it to your agent.'; + const heading = expired ? 'Session expired' : complete ? 'All yours.' : notice ? 'Preview saved.' : inputRequest?.status === 'pending' ? 'Your input is needed.' : answeredInput || failed || active ? title : 'Leave it to your agent.'; return (
@@ -207,8 +211,8 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct })

{heading}

-

{complete ? 'Your agent has verified setup. You’re ready to go.' : expired || failed || inputRequest?.status === 'pending' || active ? detail : 'Give this prompt to your coding agent and hang out here to watch it sign you up.'}

- {!complete && !active && !expired && !failed && inputRequest?.status !== 'pending' && ( +

{complete ? 'Your agent has verified setup. You’re ready to go.' : expired || failed || inputRequest?.status === 'pending' || answeredInput || active ? detail : 'Give this prompt to your coding agent and hang out here to watch it sign you up.'}

+ {!complete && !active && !expired && !failed && !answeredInput && inputRequest?.status !== 'pending' && (
@@ -217,9 +221,9 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct })
)} {complete ? { if (token) analytics.track('dashboard_opened', active); }} className={s.primary} href={teamsCloudUrl(product === 'teams' ? '/dashboard/sessions' : '/dashboard')}>Open {product === 'teams' ? 'your workspace' : 'dashboard'} - : !active && !expired && inputRequest?.status !== 'pending' ? : null} -

{complete ? '' : expired ? '' : notice ? 'No action is required to keep this draft saved.' : inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : active ? (paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Your agent is processing your answer.' : 'Your agent will continue after you approve.') : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

- {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || paused) &&
+ : !active && !expired && !answeredInput && inputRequest?.status !== 'pending' ? : null} +

{complete ? '' : expired ? '' : notice ? 'No action is required to keep this draft saved.' : inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : answeredInput ? 'Your agent is processing your answer.' : active ? (paused ? 'Your agent will continue after you approve.' : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

+ {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || answeredInput || paused) &&
{notice ? <>

INACTIVE PREVIEW

{notice.label}

@@ -239,7 +243,7 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct })
}
- {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : active ? `${active} of 5 · ${paused ? (product === 'flows' && inputRequest?.status === 'answered' ? 'Answer received' : 'Waiting for your approval') : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'} + {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : answeredInput ? `${active} of 5 · Answer received` : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'}
{error &&

{error}

{!progress &&
}
- {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : answeredInput ? `${active} of 5 · Answer received` : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'} + {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : answeredInput ? 'Answer received' : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'}
{error &&

{error}

{!progress && : null}

{complete ? '' : expired ? '' : notice ? 'No action is required to keep this draft saved.' : inputRequest?.status === 'pending' ? 'Your answer goes straight to your agent.' : answeredInput ? 'Your agent is processing your answer.' : active ? (paused ? 'Your agent will continue after you approve.' : 'You can leave this page open.') : copyMessage || (progress && !token ? 'Watching this session. The prompt is in the original browser tab.' : product === 'teams' ? 'Paste into a coding agent on your Mac.' : 'Paste into a coding agent with terminal access.')}

- {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || answeredInput || paused) &&
+ {inputRequest && !expired && !complete && (inputRequest.status === 'pending' || answeredInput) &&
{notice ? <>

INACTIVE PREVIEW

{notice.label}

@@ -243,7 +248,7 @@ export function AgentSignupJourney({ product }: { product: AgentSignupProduct })
}
- {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : answeredInput ? 'Answer received' : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'} + {expired ? 'Session expired' : error ? 'Waiting for a connection' : complete ? 'Setup complete' : notice ? `${active} of 5 · Preview saved; activation pending` : inputRequest?.status === 'pending' ? 'Waiting for your answer' : answeredInput ? (active ? `${active} of 5 · Answer received` : 'Answer received') : active ? `${active} of 5 · ${paused ? 'Waiting for your approval' : failed ? 'Needs your attention' : steps[active - 1].title}` : progress ? 'Ready when your agent is' : 'Preparing your session…'}
{error &&

{error}

{!progress &&