[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

This commit is contained in:
ciregenz
2026-07-27 18:51:59 -07:00
parent dd8ca444f2
commit a2c3cc22ab
8 changed files with 31 additions and 17 deletions
+2
View File
@@ -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
+1
View File
@@ -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
@@ -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 (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: c.bg.secondary }}>
{sidebarAway && !sidePeek && (
{sidebarAway && !sidePeek && !v3FlowActive && (
<Box onMouseEnter={() => { 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.
@@ -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
@@ -38,6 +38,7 @@ export interface PrepResponse {
browser_reason: string;
automations: PersonalizedAutomation[];
menu?: PersonalizedMenu | null;
used_llm?: boolean;
}
export async function fetchIdentity(): Promise<ProviderIdentity[]> {
@@ -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])
+7 -9
View File
@@ -104,19 +104,17 @@ const Tools: React.FC = () => {
}, [regServersRaw, a.regSource, CURATED_MCP_NAMES]);
return (
<Box sx={{ p: 3, height: '100%', overflow: 'auto' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h5" sx={{ color: c.text.primary, fontWeight: 700, mb: 0.5 }}>Tool Library</Typography>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.875rem' }}>Define and manage custom tools for your Claude Code agents.</Typography>
</Box>
<Box sx={{ px: 3, pt: 1, pb: 3, height: '100%', overflow: 'auto' }}>
{/* The pane header already says "Tools"; a slim action row beats a second page title. */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', mb: 1.5 }}>
<Box>
<Button
size="small"
variant="contained"
startIcon={<AddIcon />}
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 18 }} />}
startIcon={<AddIcon sx={{ fontSize: 16 }} />}
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 16 }} />}
onClick={handleMenuOpen}
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.pressed }, textTransform: 'none', borderRadius: 2 }}
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.pressed }, textTransform: 'none', borderRadius: 2, fontSize: '0.8125rem' }}
>
New Tool
</Button>
@@ -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 {