From d478cc3daf659cab90c73bde27fc008db62f5af9 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Fri, 10 Apr 2026 18:19:14 +0000 Subject: [PATCH] feat(nexus): nexus-first navigation and first-run onboarding trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop showing Paperclip's board UI by default. First-time users now land on Personal Assistant (v1.5), see a Nexus-first sidebar, and the NexusOnboardingWizard (built in v1.5) actually fires on first run instead of sitting behind a dead "Start Onboarding" button click. App.tsx - CompanyRootRedirect now reads useNexusMode() and lands the user at /${prefix}/assistant by default. Only project_builder mode lands at /${prefix}/dashboard. "personal_ai" and "both" (the default) both go to the Assistant. - NoCompaniesStartPage gutted: the old "Create your first company" button is gone. Single-workspace mode doesn't ask users to name workspaces; the onboarding wizard handles it. Replaced with a minimal "Setting up your workspace..." loading shim. - OnboardingRoutePage now auto-opens the wizard on mount when no companies exist. Closes the dead-button gap: previously the user had to click "Start Onboarding" to actually get the wizard; now the wizard opens itself as soon as they land. Sidebar.tsx - Restructured around two mode-gated sections: * Always visible (Nexus essentials): Assistant, Content Studio, Convert, Inbox, Skills, Settings. Plus the New Issue button and plugin sidebar items. * project_builder-only: Work (Issues, Routines, Goals), Projects, Agents, and the remaining Workspace items (Org, Costs, Activity). - Top bar no longer renders a company switcher dropdown — single- workspace mode shows the workspace name as a static label with the search button beside it. - Dashboard link removed from the always-visible section. The default landing is /assistant; users who explicitly want the Paperclip dashboard can type the URL or switch to project_builder mode. Layout.tsx - Removed both renderings (mobile and desktop branches). Single-workspace mode doesn't need a multi-company icon rail. Import preserved with a [nexus] comment for upstream rebase compat. - Onboarding useEffect's authenticated-mode gate removed (root cause of the v1.5 wizard-not-firing bug on fresh DB). This effect is now a belt-and-suspenders fallback; the real auto- trigger lives in OnboardingRoutePage because Layout isn't actually mounted during the zero-company first-run state (CompanyRootRedirect navigates to /onboarding before Layout ever renders). NexusOnboardingWizard.tsx - handleSubmit and handleStartChat both used to hardcode the post- creation navigation to /${prefix}/dashboard. Now mode-aware: project_builder lands at /dashboard, everything else lands at /assistant. Matches the Sidebar and CompanyRootRedirect logic — a fresh user never touches the Paperclip dashboard unless they explicitly chose project_builder during the wizard. Not changed: - The Paperclip pages themselves (Dashboard, Issues, Projects, Agents, Org, etc.) — still present, still accessible by URL, still upstream-mergeable. Just hidden from the default nav. - CompanyRail.tsx, CompanySwitcher.tsx, NewCompanyDialog — files preserved for upstream rebase diff minimization. No call sites remain. - /NEX/companies route still registered in boardRoutes(), just unlinked from the default UI. TypeScript: zero new errors (pre-existing errors in AgentConfigForm, command.tsx, useKeyboardShortcuts, usePiperTts, useVadRecorder, OnboardingSummaryStep.test, PersonalAssistant unchanged). Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/App.tsx | 48 ++++++++++++------ ui/src/components/Layout.tsx | 18 +++++-- ui/src/components/NexusOnboardingWizard.tsx | 15 ++++-- ui/src/components/Sidebar.tsx | 54 +++++++++++++-------- 4 files changed, 89 insertions(+), 46 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d21eec44..c6a1d4be 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -11,6 +11,7 @@ import { healthApi } from "./api/health"; import { queryKeys } from "./lib/queryKeys"; import { useCompany } from "./context/CompanyContext"; import { useDialog } from "./context/DialogContext"; +import { useNexusMode } from "./hooks/useNexusMode"; import { loadLastInboxTab } from "./lib/inbox"; import { shouldRedirectCompanylessRouteToOnboarding } from "./lib/onboarding-route"; @@ -233,13 +234,29 @@ function LegacySettingsRedirect() { } function OnboardingRoutePage() { - const { companies } = useCompany(); - const { openOnboarding } = useDialog(); + const { companies, loading: companiesLoading } = useCompany(); + const { openOnboarding, onboardingOpen } = useDialog(); const { companyPrefix } = useParams<{ companyPrefix?: string }>(); const matchedCompany = companyPrefix ? companies.find((company) => company.issuePrefix.toUpperCase() === companyPrefix.toUpperCase()) ?? null : null; + // [nexus] Auto-open the first-run wizard when a user lands on /onboarding + // with zero companies. CompanyRootRedirect navigates here on fresh DB, and + // without this effect the user would see the fallback "Start Onboarding" + // button and have to click it. The wizard itself already renders via its + // URL-based trigger (routeOnboardingOptions), but we also flip the dialog + // state so that post-wizard-creation navigation (which closes via + // closeOnboarding) works consistently. The button below is retained as a + // harmless fallback for the "add another" flows. + useEffect(() => { + if (companiesLoading) return; + if (onboardingOpen) return; + if (companies.length === 0 && !companyPrefix) { + openOnboarding(); + } + }, [companies.length, companiesLoading, companyPrefix, onboardingOpen, openOnboarding]); + const title = matchedCompany ? `Add another agent to ${matchedCompany.name}` : companies.length > 0 @@ -274,9 +291,10 @@ function OnboardingRoutePage() { function CompanyRootRedirect() { const { companies, selectedCompany, loading } = useCompany(); + const { mode, isLoading: nexusModeLoading } = useNexusMode(); const location = useLocation(); - if (loading) { + if (loading || nexusModeLoading) { return
Loading...
; } @@ -293,7 +311,11 @@ function CompanyRootRedirect() { return ; } - return ; + // [nexus] Nexus-first landing: in personal_ai / both (default) modes, land + // on the Personal Assistant. Only project_builder mode lands on the board + // dashboard. URL overrides (typing /PREFIX/dashboard) are still honored. + const landingPath = mode === "project_builder" ? "dashboard" : "assistant"; + return ; } function UnprefixedBoardRedirect() { @@ -326,19 +348,13 @@ function UnprefixedBoardRedirect() { } function NoCompaniesStartPage() { - const { openOnboarding } = useDialog(); - + // [nexus] Single-workspace mode: there is no user-facing "create company" + // action. The onboarding wizard is the only path to initial workspace + // creation and is triggered separately. Show a minimal loading shim while + // that flow runs. return ( -
-
-

Create your first company

-

- Get started by creating a company. -

-
- -
-
+
+ Setting up your workspace…
); } diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 658a6689..eac9e792 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { BookOpen, MessageSquare, Moon, Settings, Sun } from "lucide-react"; import { Link, Navigate, Outlet, useLocation, useNavigate, useParams } from "@/lib/router"; -import { CompanyRail } from "./CompanyRail"; +// [nexus] CompanyRail intentionally not rendered — single-workspace mode. +// The file is preserved for upstream rebase compatibility. import { Sidebar } from "./Sidebar"; import { InstanceSidebar } from "./InstanceSidebar"; import { BreadcrumbBar } from "./BreadcrumbBar"; @@ -96,14 +97,23 @@ export function Layout() { queryFn: () => instanceSettingsApi.getGeneral(), }).data?.keyboardShortcuts === true; + // [nexus] Removed the `health?.deploymentMode === "authenticated"` gate. + // Paperclip's upstream assumed authenticated mode = hosted multi-tenant where + // users self-onboard via invites, so the first-run wizard was suppressed. + // Nexus's authenticated mode is single-user LAN (BETTER_AUTH), which still + // needs the first-run wizard. Note: this effect is mostly dead code for the + // zero-company case because CompanyRootRedirect navigates to /onboarding + // before Layout ever mounts — the real auto-open trigger lives in + // OnboardingRoutePage (App.tsx). This effect is retained as a belt-and- + // suspenders fallback for edge cases where a user reaches a Layout-mounted + // route with zero companies (e.g. /instance/settings/*). useEffect(() => { if (companiesLoading || onboardingTriggered.current) return; - if (health?.deploymentMode === "authenticated") return; if (companies.length === 0) { onboardingTriggered.current = true; openOnboarding(); } - }, [companies, companiesLoading, openOnboarding, health?.deploymentMode]); + }, [companies, companiesLoading, openOnboarding]); useEffect(() => { if (!companyPrefix || companiesLoading || companies.length === 0) return; @@ -314,7 +324,6 @@ export function Layout() { )} >
- {isInstanceSettingsRoute ? : }
@@ -365,7 +374,6 @@ export function Layout() { ) : (
-
- {/* Top bar: Company name (bold) + Search — aligned with top sections (no visible border) */} + {/* Top bar: single workspace name (static) + search. No switcher. */}
{selectedCompany?.brandColor && (
)} - {selectedCompany?.name ?? `Select ${VOCAB.company.toLowerCase()}`} + {selectedCompany?.name ?? "Nexus"} - {isAssistantEnabled && ( )} + + 0 ? "danger" : "default"} alert={inboxBadge.failedRuns > 0} /> + +
- - - - - + {/* Board-mode sections: only visible when the user has explicitly + chosen project_builder mode. Hidden in personal_ai and both + (default) so first-time users never see the board chrome. */} + {showBoard && ( + <> + + + + + - + - + - - - - - - - + + + + + + + )}