diff --git a/frontend/src/app/components/nudges/RevealHero.tsx b/frontend/src/app/components/nudges/RevealHero.tsx
new file mode 100644
index 00000000..47085f09
--- /dev/null
+++ b/frontend/src/app/components/nudges/RevealHero.tsx
@@ -0,0 +1,151 @@
+import React, { useMemo, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Sparkles, FolderCheck, LayoutDashboard, Search, CalendarClock, Check, X } from 'lucide-react';
+import { useClaudeTokens } from '@/shared/styles/ThemeContext';
+import { useAppSelector } from '@/shared/hooks';
+import type { PreppedJob } from '@/shared/state/onboardingV3Slice';
+
+// The unmissable anchor of the reveal: a top-center panel that says, in plain language, exactly what
+// OpenSwarm did while the user set up, with LIVE status (working -> done) on each item. The scattered
+// cards are the real clickable work; this is the legend that makes the whole thing instantly readable.
+
+type JobStatus = 'working' | 'done' | 'snag';
+
+function iconFor(kind: PreppedJob['kind']): React.ReactNode {
+ const size = 17;
+ if (kind === 'app') return ;
+ if (kind === 'research') return ;
+ if (kind === 'schedule') return ;
+ return ;
+}
+
+// Plain-language line, present-continuous while working, past once done. Super easy to grok at a glance.
+function lineFor(job: PreppedJob, status: JobStatus): string {
+ const t = job.title;
+ if (job.kind === 'app') return status === 'done' ? `Built you a dashboard of your world` : `Building you a dashboard of your world`;
+ if (job.kind === 'research') return status === 'done' ? `Looked into ${t} for you` : `Looking into ${t} for you`;
+ if (job.kind === 'schedule') return `Set up ${t} to run on its own`;
+ return status === 'done' ? `Tidied up your files (nothing moved or deleted)` : `Tidying up your files (nothing moved or deleted)`;
+}
+
+const RevealHero: React.FC = () => {
+ const c = useClaudeTokens();
+ const [dismissed, setDismissed] = useState(false);
+ const prepped = useAppSelector((s) => s.onboardingV3.prepped);
+ const flowActive = useAppSelector((s) => s.onboardingV3.flowActive);
+ const revealPending = useAppSelector((s) => s.onboardingV3.revealPending);
+ const sessions = useAppSelector((s) => s.agents.sessions);
+ const userName = useAppSelector((s) => s.settings.data.user_name);
+
+ // Dashboard-first order (the star), then research, cleanup, and the recurring task.
+ const order: PreppedJob['kind'][] = ['app', 'research', 'audit', 'schedule'];
+ const jobs = useMemo(
+ () => [...prepped].sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind)),
+ [prepped],
+ );
+
+ const statusOf = (job: PreppedJob): JobStatus => {
+ if (job.kind === 'schedule') return 'done'; // a workflow: created instantly
+ const s = job.sessionId ? sessions[job.sessionId] : undefined;
+ if (!s) return 'working';
+ if (s.status === 'completed' || s.status === 'stopped') return 'done';
+ if (s.status === 'error') return 'snag';
+ return 'working';
+ };
+
+ const open = !dismissed && !flowActive && !revealPending && jobs.length > 0;
+ const doneCount = jobs.filter((j) => statusOf(j) === 'done').length;
+
+ // Portal to body: the dashboard canvas is a transformed ancestor, so a position:fixed child would
+ // anchor to IT (drifting off-center), not the window. The OUTER div owns the fixed top-center
+ // placement (translateX(-50%)); the inner motion.div owns the entrance (framer drives its transform,
+ // which would clobber the centering translate if they shared one element).
+ return createPortal(
+
+
+ {open && (
+
+
+
+
+
+
+
+ {userName ? `${userName}, while you set up I got to work` : `While you set up, I got to work`}
+
+
+ {doneCount === jobs.length ? `All done, here on your canvas` : `${doneCount} of ${jobs.length} done, the rest are running`}
+
+ It's all on your canvas below. Nothing's saved or deleted without you, keep it or clear it anytime.
+
+
+
+ )}
+
+
,
+ document.body,
+ );
+};
+
+export default RevealHero;
diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx
index 12a4a32c..2611e144 100644
--- a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx
+++ b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx
@@ -9,6 +9,7 @@ import MissedRunsToast from '@/app/pages/Workflows/MissedRunsToast';
import ProviderHealthToast from '@/app/components/overlays/ProviderHealthToast';
import ScheduleOfferToast from '@/app/components/nudges/ScheduleOfferToast';
import PrepKeepToast from '@/app/components/nudges/PrepKeepToast';
+import RevealHero from '@/app/components/nudges/RevealHero';
import type { AgentSession } from '@/shared/state/agentsSlice';
import type {
CardPosition,
@@ -162,6 +163,9 @@ const DashboardOverlays: React.FC = ({
{/* One-shot dependency beat: first completed personalized starter offers to become a weekly job */}
+ {/* Unmissable top-center legend of the work the reveal did, with live working/done status */}
+
+
{/* Accept-or-deny for the audit + app the flow started on the user's behalf */}
>
diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useOnboardingRevealSeed.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useOnboardingRevealSeed.ts
index 90c56c8c..c1e4c0a1 100644
--- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useOnboardingRevealSeed.ts
+++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useOnboardingRevealSeed.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, type RefObject } from 'react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { store } from '@/shared/state/store';
import {
- addNote, setNoteSize, placeCard, addWorkflowCard, setWorkflowCardPosition, setViewCardPosition,
+ placeCard, addWorkflowCard, setWorkflowCardPosition, setViewCardPosition,
DEFAULT_CARD_W, DEFAULT_CARD_H, EXPANDED_CARD_MIN_H,
} from '@/shared/state/dashboardLayoutSlice';
import { clearReveal, setRevealAnchor } from '@/shared/state/onboardingV3Slice';
@@ -18,20 +18,16 @@ interface Args {
}
const GAP = 48;
-const NOTE_W = 340;
-const NOTE_H = 440;
-/** Where the reveal's app view card is born: the end of the reading arc, jobs -> chat -> note -> APP, top-aligned. */
+/** Where the reveal's app view card is born: right of the welcome chat, top-aligned. The "here's what I did" legend is the fixed RevealHero panel, not a canvas note, so the app sits right next to the chat. */
export function revealAppSpot(anchor: { cx: number; cy: number }): { x: number; y: number } {
- return { x: anchor.cx + DEFAULT_CARD_W / 2 + GAP + NOTE_W + GAP, y: anchor.cy - EXPANDED_CARD_MIN_H / 2 };
+ return { x: anchor.cx + DEFAULT_CARD_W / 2 + GAP, y: anchor.cy - EXPANDED_CARD_MIN_H / 2 };
}
// The reveal: onboarding v3 finished behind the curtain and the prepped work (a personal dashboard app, a live web-research dig, a read-only file tidy-up, and one recurring task) has been running since mid-flow. Compose one tight readable cluster: welcome chat center, jobs stacked left, the plain-English note right; the app view card is born at the arc end (via revealAnchor + the lifecycle auto-add) and the camera glides to it when it arrives. The keep/discard toast owns the jobs' fate afterward.
export function useOnboardingRevealSeed({ isActive, dashboardId, expandedSessionIds, viewportRef, canvasStateRef, createWelcomeDraft, fitToCards }: Args): void {
const dispatch = useAppDispatch();
const revealPending = useAppSelector((s) => s.onboardingV3.revealPending);
- const starters = useAppSelector((s) => s.onboardingV3.starters);
- const scanSummary = useAppSelector((s) => s.onboardingV3.scanSummary);
const prepped = useAppSelector((s) => s.onboardingV3.prepped);
const settingsLoaded = useAppSelector((s) => s.settings.loaded);
const seededRef = useRef(false);
@@ -75,30 +71,9 @@ export function useOnboardingRevealSeed({ isActive, dashboardId, expandedSession
const cy = (vr.height / 2 - cs.panY) / cs.zoom;
anchorRef.current = { cx, cy };
dispatch(setRevealAnchor({ cx, cy }));
- const audit = prepped.find((j) => j.kind === 'audit');
const app = prepped.find((j) => j.kind === 'app');
- const research = prepped.find((j) => j.kind === 'research');
- const schedule = prepped.find((j) => j.kind === 'schedule');
- // Lead every card with a plain everyday label so a first-time non-dev instantly gets WHAT each
- // one is. Phrased as work in motion: at curtain-lift these jobs are usually still running, and
- // "already done" would be a lie for a few minutes. The cards themselves show live status.
- const jobSentence = (j: typeof prepped[number]): string => {
- if (j.kind === 'app') return `App: I'm building you "${j.title}", a dashboard of everything you're working on, in one place. It opens right here when it's ready.`;
- if (j.kind === 'research') return `Research: I'm looking into "${j.title}" for you on the live web and writing up what's worth knowing.`;
- if (j.kind === 'schedule') return `Auto-task: "${j.title}" now runs by itself on a schedule, so you don't have to.`;
- return `Cleanup: I'm going through your files and writing you a plan for what to keep or toss. Nothing gets moved or deleted.`;
- };
- const lines: string[] = [`While you were setting up, I got a head start.`];
- if (scanSummary) lines.push(`I looked around your Mac and saw ${scanSummary}.`);
- // Dashboard first (the star), then the web research, the tidy-up, and the recurring task.
- const jobs = [app, research, audit, schedule].filter((j): j is typeof prepped[number] => Boolean(j));
- if (jobs.length > 0) lines.push(`Here's what I already have going for you, on the cards to your left:\n${jobs.map((j) => `- ${jobSentence(j)}`).join('\n')}`);
- if (starters.length > 0) lines.push(`A few more things I can do whenever you want:\n${starters.slice(audit ? 1 : 0).map((s) => `- ${s.title}`).join('\n')}`);
- lines.push(`Nothing's ever saved or deleted without you. Keep any of it going, or clear it anytime.`);
- dispatch(addNote({ x: cx + DEFAULT_CARD_W / 2 + GAP, y: cy - EXPANDED_CARD_MIN_H / 2, color: 'yellow', content: lines.join('\n\n') }));
- // addNote mints its own id; it lands in pendingFocusNoteId, which nothing else consumes.
- const noteId = store.getState().dashboardLayout.pendingFocusNoteId;
- if (noteId) dispatch(setNoteSize({ noteId, width: NOTE_W, height: NOTE_H }));
+ // The "here's what I did" legend is the fixed RevealHero panel (top-center, unmissable, live
+ // status), not a canvas note, so there is no wall-of-text sticky to miss here anymore.
placeJobs();
// The app agent often creates its output BEFORE the curtain lifts (it gets a head start at
// connect), so its view card was auto-added with no anchor to stage against. Move it to the
@@ -116,7 +91,7 @@ export function useOnboardingRevealSeed({ isActive, dashboardId, expandedSession
const left = cx - DEFAULT_CARD_W / 2 - GAP - DEFAULT_CARD_W;
const top = cy - EXPANDED_CARD_MIN_H / 2;
fitToCards(
- [{ x: left, y: top, width: (DEFAULT_CARD_W * 2) + NOTE_W + (GAP * 2), height: Math.max(EXPANDED_CARD_MIN_H, DEFAULT_CARD_H * 3 + 48) }],
+ [{ x: left, y: top, width: (DEFAULT_CARD_W * 2) + (GAP * 2), height: Math.max(EXPANDED_CARD_MIN_H, DEFAULT_CARD_H * 3 + 48) }],
0.9,
true,
);
@@ -126,7 +101,7 @@ export function useOnboardingRevealSeed({ isActive, dashboardId, expandedSession
} finally {
dispatch(clearReveal());
}
- }, [revealPending, isActive, settingsLoaded, starters, scanSummary, prepped, dashboardId, expandedSessionIds, viewportRef, canvasStateRef, createWelcomeDraft, fitToCards, dispatch, placeJobs]);
+ }, [revealPending, isActive, settingsLoaded, prepped, dashboardId, expandedSessionIds, viewportRef, canvasStateRef, createWelcomeDraft, fitToCards, dispatch, placeJobs]);
// Jobs that launched after the curtain lifted: drop their cards in as they arrive.
useEffect(() => {