diff --git a/ui/src/components/NexusOnboardingWizard.tsx b/ui/src/components/NexusOnboardingWizard.tsx index 509ac164..2c157cd8 100644 --- a/ui/src/components/NexusOnboardingWizard.tsx +++ b/ui/src/components/NexusOnboardingWizard.tsx @@ -1,4 +1,4 @@ -// [nexus] Replacement onboarding wizard — 4-step flow: hardware detection, mode selection, provider selection, root directory +// [nexus] Replacement onboarding wizard — 5-step flow: hardware detection, mode selection, provider selection, root directory, summary // Exports `OnboardingWizard` to match the named import in App.tsx. // Wired via Vite alias: all imports of ./components/OnboardingWizard are // redirected here at build time; the original file is preserved for upstream rebase. @@ -21,10 +21,23 @@ import { cn } from "../lib/utils"; import { ModeSelector } from "./onboarding/ModeSelector"; import { HardwareSummaryStep } from "./onboarding/HardwareSummaryStep"; import { ProviderSelectionStep } from "./onboarding/ProviderSelectionStep"; +import { OnboardingSummaryStep } from "./onboarding/OnboardingSummaryStep"; import { useHardwareInfo } from "../hooks/useHardwareInfo"; import { updateNexusSettings, type NexusMode } from "../api/hardware"; +import { useChatPanel } from "../context/ChatPanelContext"; -// [nexus] 4-step onboarding wizard: hardware detection → mode selection → provider selection → root directory +function deriveProviderLabel( + puterToken: string | null, + googleOAuthStateId: string | null, + apiKeyData: { provider: string; apiKey: string } | null, +): string { + if (puterToken) return "Puter (free, zero-config)"; + if (googleOAuthStateId) return "Google Gemini (free tier)"; + if (apiKeyData) return `API key — ${apiKeyData.provider}`; + return "None selected"; +} + +// [nexus] 5-step onboarding wizard: hardware detection → mode selection → provider selection → root directory → summary export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany(); @@ -33,6 +46,7 @@ export function OnboardingWizard() { const location = useLocation(); const { companyPrefix } = useParams<{ companyPrefix?: string }>(); const [routeDismissed, setRouteDismissed] = useState(false); + const { setChatOpen } = useChatPanel(); // Preserve wizard-show detection logic from the original OnboardingWizard const routeOnboardingOptions = @@ -51,7 +65,7 @@ export function OnboardingWizard() { setRouteDismissed(false); }, [location.pathname]); - // Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = root directory + // Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = root directory, 5 = summary const [step, setStep] = useState(1); // Mode state: "both" pre-selected per UI-SPEC @@ -116,6 +130,101 @@ export function OnboardingWizard() { closeOnboarding(); } + // [nexus] Shared workspace creation logic used by both handleSubmit (step 4 direct) and handleStartChat (step 5) + async function createWorkspace() { + // Step 1: Create workspace (company) named after VOCAB.appName + const company = await companiesApi.create({ name: VOCAB.appName }); + setSelectedCompanyId(company.id); + queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); + + // [nexus] hermes_local doesn't require a cwd; directory is optional + const baseAdapterConfig = defaultAdapter === "hermes_local" + ? (rootDir.trim() ? { cwd: rootDir.trim() } : {}) + : { cwd: rootDir.trim() }; + + // [nexus] Hermes agents need a promptTemplate so they follow the Nexus heartbeat + // workflow. The server's ensureDefaultInstructionsBundle will materialize this as + // AGENTS.md alongside the full role bundle (HEARTBEAT.md, SOUL.md, TOOLS.md). + const hermesPromptTemplate = [ + `You are "{{agentName}}", an AI agent managed by ${VOCAB.appName}.`, + "", + "Your identity:", + " Agent ID: {{agentId}}", + " Company ID: {{companyId}}", + " API Base: {{paperclipApiUrl}}", + " Run ID: {{runId}}", + "", + "IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.", + 'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.', + "", + "Before starting any task:", + "1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions", + "2. Follow the HEARTBEAT.md workflow from your instructions", + "3. Use TOOLS.md for available API endpoints", + "", + "{{#taskId}}", + "Assigned task: {{taskId}} - {{taskTitle}}", + "{{/taskId}}", + ].join("\n"); + + const adapterConfig = defaultAdapter === "hermes_local" + ? { ...baseAdapterConfig, promptTemplate: hermesPromptTemplate, persistSession: true } + : baseAdapterConfig; + + const runtimeConfig = { + heartbeat: { + enabled: true, + intervalSec: 3600, + wakeOnDemand: true, + cooldownSec: 10, + maxConcurrentRuns: 1, + }, + }; + + // Step 2: Create PM agent with role "ceo" for elevated permissions + await agentsApi.create(company.id, { + name: "Project Manager", + role: "ceo", + adapterType: defaultAdapter, + adapterConfig, + runtimeConfig, + }); + + // Step 3: Create Engineer agent + await agentsApi.create(company.id, { + name: "Engineer", + role: "engineer", + adapterType: defaultAdapter, + adapterConfig, + runtimeConfig, + }); + + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(company.id), + }); + + // Persist selected mode — non-blocking + try { + await updateNexusSettings({ mode: selectedMode }); + } catch { + // Non-blocking — mode defaults to "both" if save fails + } + + // [nexus] Store collected provider credentials after company creation — all non-blocking + 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(() => {}); + } + + return company; + } + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (defaultAdapter === "claude_local" && !rootDir.trim()) return; @@ -124,95 +233,7 @@ export function OnboardingWizard() { setError(null); try { - // Step 1: Create workspace (company) named after VOCAB.appName - const company = await companiesApi.create({ name: VOCAB.appName }); - setSelectedCompanyId(company.id); - queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); - - // [nexus] hermes_local doesn't require a cwd; directory is optional - const baseAdapterConfig = defaultAdapter === "hermes_local" - ? (rootDir.trim() ? { cwd: rootDir.trim() } : {}) - : { cwd: rootDir.trim() }; - - // [nexus] Hermes agents need a promptTemplate so they follow the Nexus heartbeat - // workflow. The server's ensureDefaultInstructionsBundle will materialize this as - // AGENTS.md alongside the full role bundle (HEARTBEAT.md, SOUL.md, TOOLS.md). - const hermesPromptTemplate = [ - `You are "{{agentName}}", an AI agent managed by ${VOCAB.appName}.`, - "", - "Your identity:", - " Agent ID: {{agentId}}", - " Company ID: {{companyId}}", - " API Base: {{paperclipApiUrl}}", - " Run ID: {{runId}}", - "", - "IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.", - 'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.', - "", - "Before starting any task:", - "1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions", - "2. Follow the HEARTBEAT.md workflow from your instructions", - "3. Use TOOLS.md for available API endpoints", - "", - "{{#taskId}}", - "Assigned task: {{taskId}} - {{taskTitle}}", - "{{/taskId}}", - ].join("\n"); - - const adapterConfig = defaultAdapter === "hermes_local" - ? { ...baseAdapterConfig, promptTemplate: hermesPromptTemplate, persistSession: true } - : baseAdapterConfig; - - const runtimeConfig = { - heartbeat: { - enabled: true, - intervalSec: 3600, - wakeOnDemand: true, - cooldownSec: 10, - maxConcurrentRuns: 1, - }, - }; - - // Step 2: Create PM agent with role "ceo" for elevated permissions - await agentsApi.create(company.id, { - name: "Project Manager", - role: "ceo", - adapterType: defaultAdapter, - adapterConfig, - runtimeConfig, - }); - - // Step 3: Create Engineer agent - await agentsApi.create(company.id, { - name: "Engineer", - role: "engineer", - adapterType: defaultAdapter, - adapterConfig, - runtimeConfig, - }); - - queryClient.invalidateQueries({ - queryKey: queryKeys.agents.list(company.id), - }); - - // Persist selected mode — non-blocking - try { - await updateNexusSettings({ mode: selectedMode }); - } catch { - // Non-blocking — mode defaults to "both" if save fails - } - - // [nexus] Store collected provider credentials after company creation — all non-blocking - 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(() => {}); - } + const company = await createWorkspace(); // Navigate to dashboard — not an issue detail page closeOnboarding(); @@ -223,6 +244,30 @@ export function OnboardingWizard() { } } + // [nexus] ONBD-06: Creates workspace then opens chat panel after navigation + async function handleStartChat() { + // Guard: claude_local requires rootDir + if (defaultAdapter === "claude_local" && !rootDir.trim()) { + setError("Root directory is required for Claude Code. Go back to step 4 to set it."); + return; + } + + setLoading(true); + setError(null); + + try { + const company = await createWorkspace(); + + // Navigate to dashboard then open chat panel + closeOnboarding(); + navigate(`/${company.issuePrefix}/dashboard`); + setChatOpen(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Setup failed. Please try again."); + setLoading(false); + } + } + if (!effectiveOnboardingOpen) return null; return ( @@ -242,7 +287,9 @@ export function OnboardingWizard() { )} > {/* Step indicator */} -

Step {step} of 4

+

+ {step === 5 ? "Summary" : `Step ${step} of 4`} +

{/* Step 1 — Hardware Detection */} {step === 1 && ( @@ -259,13 +306,23 @@ export function OnboardingWizard() { isError={hwError} /> - +
+ + +
)} @@ -296,6 +353,14 @@ export function OnboardingWizard() { > Back + )} @@ -348,7 +413,7 @@ export function OnboardingWizard() { {/* Form */} -
+
)} + + {/* Step 5 — Summary */} + {step === 5 && ( + setStep(4)} + /> + )}