---
phase: 31-puter.js-zero-config-cloud
plan: "03"
type: execute
wave: 2
depends_on: ["31-01", "31-02"]
files_modified:
  - ui/src/components/onboarding/ProviderSelectionStep.tsx
  - ui/src/components/onboarding/PuterAuthButton.tsx
  - ui/src/components/onboarding/GoogleOAuthButton.tsx
  - ui/src/components/onboarding/ApiKeyEntryForm.tsx
  - ui/src/components/NexusOnboardingWizard.tsx
  - ui/src/api/puter-proxy.ts
autonomous: true
requirements: [CLOUD-01, CLOUD-03, CLOUD-04, CLOUD-05]

must_haves:
  truths:
    - "User sees a Provider Selection step (Step 3 of 4) in the onboarding wizard with Puter, Google, and API Key options"
    - "Clicking 'Continue with Puter' triggers Puter auth popup, stores token in React state for later server submission"
    - "Google option shows policy-risk warning before enabling the Sign in button (3-second gate)"
    - "Google OAuth opens popup, callback stores tokens by stateId, stateId captured in React state for later claim"
    - "API key option shows inline form with provider dropdown and key input"
    - "Skip for now button is always visible and advances to root directory step"
    - "Pre-installed tools (Hermes, Claude Code, OpenClaw) show detected badges via adapter probe"
    - "Step indicator shows 'Step N of 4' (updated from 3)"
    - "handleSubmit posts collected credentials to server AFTER company creation (Puter token, Google claim, API key)"
  artifacts:
    - path: "ui/src/components/onboarding/ProviderSelectionStep.tsx"
      provides: "Provider selection UI with three option cards and skip button"
      exports: ["ProviderSelectionStep"]
    - path: "ui/src/components/onboarding/PuterAuthButton.tsx"
      provides: "Puter auth button that loads CDN script, calls signIn, captures token in state"
      exports: ["PuterAuthButton"]
    - path: "ui/src/components/onboarding/GoogleOAuthButton.tsx"
      provides: "Google OAuth button with 3-second risk warning gate, opens popup, captures stateId"
      exports: ["GoogleOAuthButton"]
    - path: "ui/src/components/onboarding/ApiKeyEntryForm.tsx"
      provides: "API key entry form with provider dropdown"
      exports: ["ApiKeyEntryForm"]
    - path: "ui/src/api/puter-proxy.ts"
      provides: "API client for puter-proxy, oauth, and api-keys endpoints"
      exports: ["puterProxyApi"]
    - path: "ui/src/components/NexusOnboardingWizard.tsx"
      provides: "4-step wizard with provider selection step inserted"
  key_links:
    - from: "ui/src/components/onboarding/PuterAuthButton.tsx"
      to: "POST /api/puter-proxy/token"
      via: "puterProxyApi.storeToken called in wizard handleSubmit after company creation"
      pattern: "puter-proxy/token"
    - from: "ui/src/components/onboarding/GoogleOAuthButton.tsx"
      to: "POST /api/oauth/google/authorize"
      via: "fetch call to get auth URL + stateId, then window.open"
      pattern: "oauth/google/authorize"
    - from: "ui/src/components/NexusOnboardingWizard.tsx"
      to: "POST /api/oauth/google/claim"
      via: "claim call in handleSubmit with stateId + companyId after company creation"
      pattern: "oauth/google/claim"
    - from: "ui/src/components/onboarding/ApiKeyEntryForm.tsx"
      to: "POST /api/api-keys/store"
      via: "puterProxyApi.storeApiKey called in wizard handleSubmit after company creation"
      pattern: "api-keys/store"
    - from: "ui/src/components/NexusOnboardingWizard.tsx"
      to: "ui/src/components/onboarding/ProviderSelectionStep.tsx"
      via: "import and render in step 3"
      pattern: "ProviderSelectionStep"
    - from: "ui/src/components/NexusOnboardingWizard.tsx"
      to: "GET /api/adapters/:type/probe"
      via: "probeAdapter calls for hermes_local, claude_local, openclaw_gateway on mount"
      pattern: "probeAdapter|adapters.*probe"
---

<objective>
Build the Provider Selection step UI components and wire them into the existing 3-step onboarding wizard (making it 4 steps). This includes the Puter auth button, Google OAuth button with risk warning, API key entry form, and adapter auto-detection badges.

Purpose: CLOUD-01 (Puter zero-config UI), CLOUD-03 (Google OAuth UI), CLOUD-04 (auto-detect tools), CLOUD-05 (API key entry UI). This is the user-facing surface for all cloud provider paths.
Output: ProviderSelectionStep, PuterAuthButton, GoogleOAuthButton, ApiKeyEntryForm, updated wizard
</objective>

<execution_context>
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
@/home/mikkel/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md
@.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
@.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md

<interfaces>
From ui/src/components/NexusOnboardingWizard.tsx (current state):
```typescript
// 3-step wizard: step 1 = hardware, step 2 = mode, step 3 = root directory
// Step indicator: "Step {step} of 3"
// defaultAdapter state: "claude_local" | "hermes_local"
// probing state for adapter detection
// handleSubmit creates company + agents on step 3 form submit
```

From ui/src/components/onboarding/ModeSelector.tsx (selected state pattern):
```typescript
// Cards use: border-primary bg-primary/5 when selected
// Vertical stack with gap-3
// Each card is a <button type="button"> element
```

From ui/src/api/agents.ts:
```typescript
probeAdapter: (type: string) =>
  api.get<{ available: boolean; status: string }>(`/adapters/${encodeURIComponent(type)}/probe`),
```

From 31-UI-SPEC.md (copywriting):
```
Step heading: "Choose a provider"
Step subheading: "No API keys needed for the zero-config path."
Puter card label: "Puter -- free, zero-config"
Puter card description: "Free AI powered by your Puter.com account. No API key needed."
Google card label: "Google -- Gemini free tier"
Google risk warning body: "Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk."
API key card label: "API key -- subscription provider"
```

From Plan 02 routes (pendingTokens pattern):
```typescript
// POST /oauth/google/authorize returns { url, stateId }
// GET /oauth/google/callback stores tokens by stateId, redirects to /?google_oauth=success&state={stateId}
// POST /oauth/google/claim { stateId, companyId } -> moves tokens to secretService
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: API client + PuterAuthButton + GoogleOAuthButton + ApiKeyEntryForm</name>
  <files>
    ui/src/api/puter-proxy.ts,
    ui/src/components/onboarding/PuterAuthButton.tsx,
    ui/src/components/onboarding/GoogleOAuthButton.tsx,
    ui/src/components/onboarding/ApiKeyEntryForm.tsx
  </files>
  <read_first>
    ui/src/api/agents.ts,
    ui/src/api/client.ts,
    ui/src/components/onboarding/ModeSelector.tsx,
    ui/src/components/NexusOnboardingWizard.tsx,
    .planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
  </read_first>
  <action>
    **Create ui/src/api/puter-proxy.ts:**

    Import `api` from `./client`. Export `puterProxyApi` with:
    - `storeToken: (companyId: string, token: string) => api.post("/puter-proxy/token", { companyId, token })`
    - `getAuthUrl: () => api.post<{ url: string; stateId: string }>("/oauth/google/authorize", {})` — no companyId needed
    - `claimGoogleTokens: (stateId: string, companyId: string) => api.post("/oauth/google/claim", { stateId, companyId })`
    - `storeApiKey: (companyId: string, provider: string, apiKey: string) => api.post("/api-keys/store", { companyId, provider, apiKey })`

    **Create ui/src/components/onboarding/PuterAuthButton.tsx:**

    Props: `{ onSuccess: (token: string) => void; onError: (msg: string) => void }`.

    State: `loading` boolean, `connected` boolean.

    `loadScript` helper: check if `window.puter` exists; if not, create a `<script>` tag with `src="https://js.puter.com/v2/"`, append to `<head>`, wait for `load` event. Return a promise.

    `handleClick` (the onClick handler — MUST be synchronous entry point for popup to work):
    1. Set `loading(true)`.
    2. Call `loadScript()` to ensure Puter SDK is loaded.
    3. Call `(window as any).puter.auth.signIn()` — this opens the popup. Must be in the same synchronous-ish call chain from the click event.
    4. After signIn resolves, extract token: try `(window as any).puter.authToken` first, then `(window as any).puter.auth?.token`, then call `(window as any).puter.auth?.getUser?.()` and check for token on the returned object. Log a warning if token is undefined (per Pitfall 1 in RESEARCH.md).
    5. Call `onSuccess(token)` — parent stores the token in React state for later submission to server after company creation.
    6. Set `connected(true)`, `loading(false)`.
    7. On any error: `onError("Puter sign-in failed. Check your Puter.com account and try again.")`.

    Render a `<Button>` (from @/components/ui/button). When `loading`, show spinner (h-4 w-4 animate-spin SVG) and text "Connecting to Puter...". When `connected`, show CheckCircle icon (from lucide-react) and text "Puter connected". Otherwise show LogIn icon and text "Continue with Puter". Use `aria-busy={loading}`.

    **Create ui/src/components/onboarding/GoogleOAuthButton.tsx:**

    Props: `{ onSuccess: (stateId: string) => void; onError: (msg: string) => void }`.

    State: `loading`, `warningTimer` (boolean, starts false — becomes true after 3 seconds), `connected` boolean.

    On mount, start a 3-second timeout that sets `warningTimer = true`. Clean up timeout on unmount.

    Render:
    1. Policy-risk warning (always visible when this component is rendered):
       ```
       <div role="alert" className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
         <p className="font-medium">Policy risk -- read before continuing</p>
         <p className="mt-1">Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk.</p>
       </div>
       ```
    2. Button: "Sign in with Google" (disabled until `warningTimer === true`). On click:
       a. Set `loading(true)`.
       b. Call `puterProxyApi.getAuthUrl()` to get `{ url, stateId }`.
       c. Open popup: `window.open(url, "_blank", "popup,width=600,height=700")`.
       d. Poll for popup close or listen for `window.addEventListener("message", ...)`. Simpler: poll every 500ms checking if the popup is closed (`popup.closed`). When popup closes, check `window.location.search` for `google_oauth=success` query param, OR just trust that the callback happened server-side and the tokens are parked.
       e. Actually, the simplest approach: after opening the popup, the Google OAuth callback redirects the popup to `/?google_oauth=success&state={stateId}`. The popup lands on that URL. Use `window.addEventListener("message")` — but since the popup is our own origin, we can poll it. Simplest: after popup opens, poll `popup.closed` every 500ms. When closed, assume success (the callback stored tokens server-side). Call `onSuccess(stateId)`.
       f. Set `connected(true)`, `loading(false)`.
    3. On error: call `onError(msg)`.

    **Create ui/src/components/onboarding/ApiKeyEntryForm.tsx:**

    Props: `{ onSave: (provider: string, apiKey: string) => void; onError: (msg: string) => void }`.

    State: `provider` (default "openai"), `apiKey` (string), `saved` (boolean).

    Render:
    1. `<Label>` + select dropdown with options: OpenAI, Anthropic, Groq.
    2. `<Label>` + `<Input>` for API key, type="password".
    3. `<Button>` "Save API key". On click: validate apiKey not empty, call `onSave(provider, apiKey)`. Set `saved(true)`.
    4. When `saved`, show CheckCircle icon and "API key saved" text.
    5. Use shadcn `Label`, `Input`, `Button` components. Inline form (no page navigation).
  </action>
  <verify>
    <automated>cd /opt/nexus && ls ui/src/api/puter-proxy.ts ui/src/components/onboarding/PuterAuthButton.tsx ui/src/components/onboarding/GoogleOAuthButton.tsx ui/src/components/onboarding/ApiKeyEntryForm.tsx</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "puterProxyApi" ui/src/api/puter-proxy.ts
    - grep -q "puter-proxy/token" ui/src/api/puter-proxy.ts
    - grep -q "oauth/google/claim" ui/src/api/puter-proxy.ts
    - grep -q "stateId" ui/src/api/puter-proxy.ts
    - grep -q "js.puter.com/v2" ui/src/components/onboarding/PuterAuthButton.tsx
    - grep -q "puter.auth.signIn" ui/src/components/onboarding/PuterAuthButton.tsx
    - grep -q "Continue with Puter" ui/src/components/onboarding/PuterAuthButton.tsx
    - grep -q "Connecting to Puter" ui/src/components/onboarding/PuterAuthButton.tsx
    - grep -q "Policy risk" ui/src/components/onboarding/GoogleOAuthButton.tsx
    - grep -q "suspended accounts" ui/src/components/onboarding/GoogleOAuthButton.tsx
    - grep -q "Sign in with Google" ui/src/components/onboarding/GoogleOAuthButton.tsx
    - grep -q "3000\|3 .* second\|setTimeout" ui/src/components/onboarding/GoogleOAuthButton.tsx
    - grep -q "openai\|anthropic\|groq" ui/src/components/onboarding/ApiKeyEntryForm.tsx
    - grep -q "Save API key" ui/src/components/onboarding/ApiKeyEntryForm.tsx
    - grep -q 'type="password"' ui/src/components/onboarding/ApiKeyEntryForm.tsx
  </acceptance_criteria>
  <done>PuterAuthButton loads CDN and triggers signIn popup (token captured in React state), GoogleOAuthButton shows risk warning with 3s gate and opens OAuth popup (stateId captured), ApiKeyEntryForm captures provider/key pairs — all pass data back to parent via callbacks for post-company-creation submission</done>
</task>

<task type="auto">
  <name>Task 2: ProviderSelectionStep + wire 4-step wizard</name>
  <files>
    ui/src/components/onboarding/ProviderSelectionStep.tsx,
    ui/src/components/NexusOnboardingWizard.tsx
  </files>
  <read_first>
    ui/src/components/NexusOnboardingWizard.tsx,
    ui/src/components/onboarding/ModeSelector.tsx,
    ui/src/components/onboarding/PuterAuthButton.tsx,
    ui/src/components/onboarding/GoogleOAuthButton.tsx,
    ui/src/components/onboarding/ApiKeyEntryForm.tsx,
    ui/src/api/agents.ts,
    .planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
  </read_first>
  <action>
    **Create ui/src/components/onboarding/ProviderSelectionStep.tsx:**

    Props:
    ```typescript
    interface ProviderSelectionStepProps {
      onPuterToken: (token: string) => void;
      onGoogleOAuthState: (stateId: string) => void;
      onApiKey: (provider: string, apiKey: string) => void;
      onSkip: () => void;
      onContinue: () => void;
      detectedAdapters: Record<string, boolean>; // e.g., { hermes_local: true, claude_local: false }
    }
    ```

    State: `selectedProvider` ("puter" | "google" | "apikey" | null), `providerReady` boolean (true when auth is complete for selected provider), `error` string | null.

    Layout (following ModeSelector card pattern from 30-02):
    1. Three vertical cards in a stack (`flex flex-col gap-3`):
       - **Puter card**: `<button type="button">` with `border-primary bg-primary/5` when selected. Label: "Puter -- free, zero-config". Description: "Free AI powered by your Puter.com account. No API key needed." If any adapters detected, show badges inside card: `<span className="text-xs text-primary">{Name} detected</span>`.
       - **Google card**: Same pattern. Label: "Google -- Gemini free tier". Description: "Sign in with Google to access Gemini via your Google account."
       - **API key card**: Same pattern. Label: "API key -- subscription provider". Description: "Use your own OpenAI, Anthropic, or Groq API key."

    2. Below cards, conditionally render the action component for the selected provider:
       - Puter selected: `<PuterAuthButton onSuccess={(token) => { onPuterToken(token); setProviderReady(true); }} onError={setError} />`
       - Google selected: `<GoogleOAuthButton onSuccess={(stateId) => { onGoogleOAuthState(stateId); setProviderReady(true); }} onError={setError} />`
       - API key selected: `<ApiKeyEntryForm onSave={(prov, key) => { onApiKey(prov, key); setProviderReady(true); }} onError={setError} />`

    3. If `providerReady`: show a "Continue" button calling `onContinue()`.

    4. Always show: `<Button variant="ghost" onClick={onSkip}>Skip for now</Button>` with `aria-label="Skip provider setup for now"`.

    5. Show detected adapter badges on each card:
       For each adapter in `detectedAdapters` where value is true, show `<span className="text-xs text-primary">Hermes detected</span>` (or "Claude Code detected", "OpenClaw detected") inside the relevant card area.

    6. Error display: if `error` is set, show `<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">{error}</p>`.

    **Update ui/src/components/NexusOnboardingWizard.tsx:**

    Changes:
    1. Import `ProviderSelectionStep` from `./onboarding/ProviderSelectionStep`.
    2. Import `puterProxyApi` from `../api/puter-proxy`.
    3. Change step count from 3 to 4. Step indicator: `"Step {step} of 4"`.
    4. Add state: `puterToken` (string | null), `googleOAuthStateId` (string | null), `apiKeyData` ({ provider: string; apiKey: string } | null).
    5. Add state: `detectedAdapters` (Record<string, boolean>, default `{}`).
    6. Update the adapter probe useEffect to also probe `claude_local` and `openclaw_gateway` (in parallel with existing `hermes_local` probe). Store results in `detectedAdapters`.
    7. Renumber steps:
       - Step 1: Hardware Detection (unchanged)
       - Step 2: Mode Selection (unchanged)
       - Step 3: Provider Selection (NEW)
       - Step 4: Root Directory (was step 3)
    8. Step 2 "Continue" now goes to step 3 (not step 3/root dir).
    9. Step 3 renders `<ProviderSelectionStep>`. "Skip" and "Continue" both advance to step 4. Back goes to step 2.
    10. Step 4 is the existing root directory form. Back goes to step 3.
    11. In `handleSubmit` (after company creation, before navigate), add credential storage:
       ```typescript
       // Store collected provider credentials after company creation
       if (puterToken) {
         await puterProxyApi.storeToken(company.id, puterToken).catch(() => {});
       }
       if (googleOAuthStateId) {
         // Claim Google OAuth tokens for this company
         await puterProxyApi.claimGoogleTokens(googleOAuthStateId, company.id).catch(() => {});
       }
       if (apiKeyData) {
         await puterProxyApi.storeApiKey(company.id, apiKeyData.provider, apiKeyData.apiKey).catch(() => {});
       }
       ```
       All credential storage is non-blocking (catch(() => {})) — same pattern as mode save.
    12. Reset new state variables in the reset effect (when wizard closes): `setPuterToken(null)`, `setGoogleOAuthStateId(null)`, `setApiKeyData(null)`.
    13. Update heading for step 3: "Choose a provider" with subheading "No API keys needed for the zero-config path."
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -20</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "ProviderSelectionStep" ui/src/components/onboarding/ProviderSelectionStep.tsx
    - grep -q "Choose a provider" ui/src/components/onboarding/ProviderSelectionStep.tsx
    - grep -q "Skip for now" ui/src/components/onboarding/ProviderSelectionStep.tsx
    - grep -q "border-primary bg-primary/5" ui/src/components/onboarding/ProviderSelectionStep.tsx
    - grep -q "detected" ui/src/components/onboarding/ProviderSelectionStep.tsx
    - grep -q "Step 3.*Provider\|ProviderSelectionStep" ui/src/components/NexusOnboardingWizard.tsx
    - grep -q "Step {step} of 4\|of 4" ui/src/components/NexusOnboardingWizard.tsx
    - grep -q "puterToken" ui/src/components/NexusOnboardingWizard.tsx
    - grep -q "googleOAuthStateId" ui/src/components/NexusOnboardingWizard.tsx
    - grep -q "claimGoogleTokens" ui/src/components/NexusOnboardingWizard.tsx
    - grep -q "openclaw_gateway\|claude_local" ui/src/components/NexusOnboardingWizard.tsx
    - TypeScript compilation passes (tsc --noEmit exits 0)
  </acceptance_criteria>
  <done>4-step wizard with ProviderSelectionStep at step 3, adapter auto-detection badges for hermes/claude/openclaw, Puter token captured in state then posted after company creation, Google OAuth stateId captured then claimed after company creation, API key captured then stored after company creation, Skip always available</done>
</task>

</tasks>

<verification>
- `cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit` — no TypeScript errors
- All new components exist with correct exports
- Wizard shows 4 steps with provider selection at step 3
- Provider cards match ModeSelector visual pattern
- All UI copy matches 31-UI-SPEC.md copywriting contract
- Google OAuth flow uses stateId/claim pattern (no companyId before company creation)
</verification>

<success_criteria>
- Provider Selection step renders three provider cards with correct copy
- Puter auth triggers popup and captures token in React state
- Google OAuth shows risk warning with 3-second gate, opens popup, captures stateId
- API key form captures provider/key pairs
- Auto-detected adapters show badges on cards
- Skip for now always works
- Wizard is 4 steps, all credentials stored after company creation via handleSubmit
</success_criteria>

<output>
After completion, create `.planning/phases/31-puter.js-zero-config-cloud/31-03-SUMMARY.md`
</output>
