From a2c3cc22ab19b06fdadfc52cffa2e021f1e57cb7 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 27 Jul 2026 18:51:59 -0700 Subject: [PATCH] [eric] onboarding: sidebar can't cover the flow, managed pro lane isn't a user connection, aux-less prep re-runs on real connect; tools pane header slimmed --- backend/apps/onboarding/models.py | 2 ++ backend/apps/onboarding/prep.py | 1 + frontend/src/app/components/Layout/AppShell.tsx | 8 +++++--- .../components/OnboardingV3/OnboardingV3Root.tsx | 2 +- .../components/OnboardingV3/onboardingV3Api.ts | 1 + .../OnboardingV3/useOnboardingV3Pipeline.ts | 12 ++++++++++-- frontend/src/app/pages/Tools/Tools.tsx | 16 +++++++--------- frontend/src/shared/state/subscriptionsSlice.ts | 6 ++++-- 8 files changed, 31 insertions(+), 17 deletions(-) diff --git a/backend/apps/onboarding/models.py b/backend/apps/onboarding/models.py index 68f1268d..5366ca55 100644 --- a/backend/apps/onboarding/models.py +++ b/backend/apps/onboarding/models.py @@ -75,3 +75,5 @@ class PrepResponse(BaseModel): automations: List[PersonalizedAutomation] = Field(default_factory=list) # The hero's two-level menu (4 categories x 4 tailored starters); None only if prep never ran. menu: Optional[PersonalizedMenu] = None + # False = scan-grounded fallback (no aux lane at call time); lets the pipeline re-run prep once a real connect lands. + used_llm: bool = False diff --git a/backend/apps/onboarding/prep.py b/backend/apps/onboarding/prep.py index 92ad21a9..a3cc32b6 100644 --- a/backend/apps/onboarding/prep.py +++ b/backend/apps/onboarding/prep.py @@ -390,6 +390,7 @@ async def build_prep(settings: AppSettings, request: PrepRequest) -> PrepRespons parsed = parse_prep(safe_resp_text(resp)) if parsed is not None: parsed.menu = await menu_task + parsed.used_llm = True return parsed except Exception: pass diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index adf3ff9f..7e01bb2f 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -146,6 +146,8 @@ const AppShell: React.FC = () => { const modelsLoaded = useAppSelector((s) => s.models.loaded); // "Connected" = the user's OWN model (key/sub/pro/custom), NOT a non-empty /models list: the free-trial Haiku is always in that list now, so a byProvider-length check would falsely read as connected and hide the out-of-runs banner. const hasModelConnected = useAppSelector(selectHasModelConnected); + // While onboarding owns the window, the floating sidebar (and its hover-peek strip) must not exist; both out-z the overlay. + const v3FlowActive = useAppSelector((st) => st.onboardingV3.flowActive); // During an active free trial the user CAN run things, so a red "no model connected" warning is misleading and discouraging (it sits right above the working starter chips). The trial flips connection_mode back to own_key the moment it's spent, so this banner returns then, landing the connect-a-model nudge after the win, not before it. const freeTrialActive = useAppSelector((s) => { const d = s.settings.data as any; @@ -701,7 +703,7 @@ const AppShell: React.FC = () => { return ( - {sidebarAway && !sidePeek && ( + {sidebarAway && !sidePeek && !v3FlowActive && ( { cancelPeekClose(); setSidePeek(true); }} sx={{ position: 'fixed', top: 0, left: 0, bottom: 0, width: 14, zIndex: 2147483000, pointerEvents: 'auto' }} /> )} {/* Top bar dropped (Arc/Zen): a zero-height anchor left only to float the agent-activity island at top-center; the island renders nothing when idle. */} @@ -933,9 +935,9 @@ const AppShell: React.FC = () => { position: 'fixed', top: 0, left: 0, bottom: 0, zIndex: 1000002, borderRadius: '0 14px 14px 0', overflow: 'hidden', boxShadow: '8px 0 32px rgba(0,0,0,0.28)', borderRight: `1px solid ${c.border.medium}`, - transform: sidePeek ? 'translateX(0)' : 'translateX(-118%)', + transform: sidePeek && !v3FlowActive ? 'translateX(0)' : 'translateX(-118%)', transition: 'transform 240ms cubic-bezier(0.22,1,0.36,1)', - pointerEvents: sidePeek ? 'auto' : 'none', + pointerEvents: sidePeek && !v3FlowActive ? 'auto' : 'none', } : fsActive ? { // Fullscreen with the sidebar pinned: the panel sits beside a full-bleed surface, so its // dashboard-facing right corners curve like a pill; normal docked mode stays square. diff --git a/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx b/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx index 5c8df322..ae43cf39 100644 --- a/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx +++ b/frontend/src/app/components/OnboardingV3/OnboardingV3Root.tsx @@ -130,7 +130,7 @@ const OnboardingV3Root: React.FC = () => { kickIdentity(); kickScan(true); if (connectedProvider) kickUsageRead(connectedProvider, true); - kickPrep(picks); + kickPrep(picks, true); }, [kickIdentity, kickScan, kickUsageRead, kickPrep, connectedProvider, picks]); // Backstop: onConnected fires prep for subscription/api-key connects; this covers any path where it diff --git a/frontend/src/app/components/OnboardingV3/onboardingV3Api.ts b/frontend/src/app/components/OnboardingV3/onboardingV3Api.ts index 188444d0..3df7bdcf 100644 --- a/frontend/src/app/components/OnboardingV3/onboardingV3Api.ts +++ b/frontend/src/app/components/OnboardingV3/onboardingV3Api.ts @@ -38,6 +38,7 @@ export interface PrepResponse { browser_reason: string; automations: PersonalizedAutomation[]; menu?: PersonalizedMenu | null; + used_llm?: boolean; } export async function fetchIdentity(): Promise { diff --git a/frontend/src/app/components/OnboardingV3/useOnboardingV3Pipeline.ts b/frontend/src/app/components/OnboardingV3/useOnboardingV3Pipeline.ts index a5319492..c6feec93 100644 --- a/frontend/src/app/components/OnboardingV3/useOnboardingV3Pipeline.ts +++ b/frontend/src/app/components/OnboardingV3/useOnboardingV3Pipeline.ts @@ -61,8 +61,16 @@ export function useOnboardingV3Pipeline() { : Promise.resolve(null); }, []); - const kickPrep = useCallback((pickedApps: string[]) => { - if (prepRef.current) return; + const kickPrep = useCallback((pickedApps: string[], rerunIfGrounded = false) => { + // A real connect landing AFTER an aux-less prep re-runs it once, so a slow OAuth never locks the + // reveal to the scan-grounded template (the prep raced the connect and had no model to call). + if (prepRef.current) { + if (!rerunIfGrounded) return; + const prev = prepReadyRef.current; + if (prev === null || prev.used_llm) return; + prepRef.current = null; + prepReadyRef.current = null; + } const scanPromise = scanRef.current ?? Promise.resolve(null); const usagePromise = usageReadRef.current ?? Promise.resolve(); prepRef.current = Promise.all([scanPromise, usagePromise]) diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx index 26e50281..8ed19c1a 100644 --- a/frontend/src/app/pages/Tools/Tools.tsx +++ b/frontend/src/app/pages/Tools/Tools.tsx @@ -104,19 +104,17 @@ const Tools: React.FC = () => { }, [regServersRaw, a.regSource, CURATED_MCP_NAMES]); return ( - - - - Tool Library - Define and manage custom tools for your Claude Code agents. - + + {/* The pane header already says "Tools"; a slim action row beats a second page title. */} + diff --git a/frontend/src/shared/state/subscriptionsSlice.ts b/frontend/src/shared/state/subscriptionsSlice.ts index 388566ca..25fc5a58 100644 --- a/frontend/src/shared/state/subscriptionsSlice.ts +++ b/frontend/src/shared/state/subscriptionsSlice.ts @@ -117,8 +117,10 @@ export function selectSubscriptionConnections( ): SubscriptionConnection[] { const providers = state.subscriptions.status?.providers; if (!providers) return EMPTY_CONNECTIONS; - if (Array.isArray(providers)) return providers; - return providers.connections ?? EMPTY_CONNECTIONS; + const rows = Array.isArray(providers) ? providers : (providers.connections ?? EMPTY_CONNECTIONS); + // The free-trial/Pro lane is an OpenSwarm-managed router row, not a connection the USER made; counting it made a fresh trial look "connected" (dead Connect-beat rows, premature onConnected, phantom green rings). + const filtered = rows.filter((p) => !/\(OpenSwarm-managed\)/.test(p.name ?? '')); + return filtered.length === rows.length ? rows : filtered; } export function hasAnyActiveSubscription(state: WithSubscriptions): boolean {