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() {
)}
>
- {/* Top bar: Company name (bold) + Search — aligned with top sections (no visible border) */}
+ {/* Top bar: single workspace name (static) + search. No switcher. */}