21 KiB
21 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31-puter.js-zero-config-cloud | 03 | execute | 2 |
|
|
true |
|
|
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
<execution_context> @/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md @/home/mikkel/.claude/get-shit-done/templates/summary.md </execution_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 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):
// 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:
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):
// 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
Task 1: API client + PuterAuthButton + GoogleOAuthButton + ApiKeyEntryForm
ui/src/api/puter-proxy.ts,
ui/src/components/onboarding/PuterAuthButton.tsx,
ui/src/components/onboarding/GoogleOAuthButton.tsx,
ui/src/components/onboarding/ApiKeyEntryForm.tsx
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
**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).
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
- 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
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
Task 2: ProviderSelectionStep + wire 4-step wizard
ui/src/components/onboarding/ProviderSelectionStep.tsx,
ui/src/components/NexusOnboardingWizard.tsx
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
**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."
cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -20
- 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)
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
- `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)
<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>