feat(web): first-run onboarding for workspaces#392
Conversation
Dismissible welcome card on the Runs page with a 3-step checklist (deploy two agents → pick a challenge pack → run your first clash), glossary tooltips on jargon-heavy labels, action-oriented empty states on the feeder pages, a "Restart onboarding" entry in the user menu, and a one-time "first clash complete" toast. Dismissal and first-run state persist per-workspace in localStorage via a useSyncExternalStore hook that syncs across tabs and components. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a first-run onboarding flow: a dismissible
Confidence Score: 4/5Safe to merge after addressing the live runsCount wiring; the rest of the feature is well-structured. One P1 logic issue: step 3 of the onboarding checklist never flips to ✓ and the card never auto-hides on the same page visit where the first run is created, directly contradicting the documented intent. Everything else (hook correctness, SSR safety, cross-tab sync, dialog controlled/uncontrolled pattern, toast deduplication) looks solid. web/src/app/(workspace)/workspaces/[workspaceId]/runs/runs-page-client.tsx and web/src/components/onboarding/workspace-welcome.tsx — the live runs total needs to flow back from RunList to WorkspaceWelcome.
|
| Filename | Overview |
|---|---|
| web/src/app/(workspace)/workspaces/[workspaceId]/runs/runs-page-client.tsx | New client wrapper that owns dialog state and passes initialTotal as a frozen runsCount to WorkspaceWelcome — the live total from RunList is never surfaced back up. |
| web/src/components/onboarding/workspace-welcome.tsx | New dismissible onboarding card with 3-step checklist; step 3 uses a static server-rendered runsCount so it never auto-completes on the same page visit, plus a minor duplicate CTA string on step 2. |
| web/src/components/onboarding/use-onboarding-state.ts | New SSR-safe hook using useSyncExternalStore with localStorage-backed flags, cross-tab sync via storage event, and same-tab sync via custom event; implementation is correct. |
| web/src/app/(workspace)/workspaces/[workspaceId]/runs/run-list.tsx | Adds first-run success toast gated on total ≤ 1 and firstRunSeen flag; timer + cleanup pattern is correct and guards against duplicate fires. |
| web/src/app/(workspace)/workspaces/[workspaceId]/runs/page.tsx | Extends parallel data fetching with deployments and challenge-packs counts; delegates all rendering to RunsPageClient. |
| web/src/components/onboarding/glossary-term.tsx | New keyboard-accessible tooltip component wrapping a HelpCircle icon; clean and self-contained. |
| web/src/components/app-shell/user-menu.tsx | Adds optional workspaceId prop and a "Restart onboarding" menu item that calls restartOnboarding and shows a toast. |
Sequence Diagram
sequenceDiagram
participant Server as runs/page.tsx (Server)
participant Client as RunsPageClient
participant Welcome as WorkspaceWelcome
participant List as RunList
participant LS as localStorage
Server->>Server: fetch runs, deployments, packs in parallel
Server->>Client: initialRuns, initialTotal, deploymentsCount, packsCount
Client->>Welcome: runsCount=initialTotal (frozen)
Client->>List: initialTotal, initialRuns
Note over List: Polls /runs every 5 s
List->>List: setTotal(res.total) — live
List->>LS: markFirstRunSeen() → fires toast
Note over Welcome: runsCount never updates ← bug
List--xWelcome: no channel back to update runsCount
Prompt To Fix All With AI
This is a comment left during a code review.
Path: web/src/app/(workspace)/workspaces/[workspaceId]/runs/runs-page-client.tsx
Line: 55-61
Comment:
**Step 3 won't flip live after the first run is created**
`WorkspaceWelcome` receives `runsCount={initialTotal}` — the server-rendered value frozen at mount time. `RunList` maintains its own `total` state that updates via polling, but this is never wired back to `WorkspaceWelcome`. After a user clicks "Run your first clash" and the run is created, `step3Done` (`runsCount >= 1`) stays `false` and the card never auto-hides on that page visit. A hard refresh is required, which contradicts the documented behavior ("each row flips to ✓ as the user makes progress").
The simplest fix is to have `RunsPageClient` maintain a `runsTotal` state that `RunList` can update via an `onTotalChange` callback:
```tsx
// In RunsPageClient:
const [runsTotal, setRunsTotal] = useState(initialTotal);
// Pass to RunList:
<RunList ... onTotalChange={setRunsTotal} />
// Pass live total to WorkspaceWelcome:
<WorkspaceWelcome ... runsCount={runsTotal} />
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: web/src/components/onboarding/workspace-welcome.tsx
Line: 58
Comment:
**Duplicate CTA string for step 2**
Both branches of the ternary produce the same string, so the conditional is dead code. Since `cta` only renders when `emphasized && !step.done`, the `step2Done ? "Browse packs" :` arm can never actually be shown — but it's still confusing and looks like it was meant to say something different (e.g., `"Manage packs"` when done).
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(web): add first-run onboarding for ..." | Re-trigger Greptile
| <WorkspaceWelcome | ||
| workspaceId={workspaceId} | ||
| deploymentsCount={deploymentsCount} | ||
| packsCount={packsCount} | ||
| runsCount={initialTotal} | ||
| onOpenCreateRun={openCreateRun} | ||
| /> |
There was a problem hiding this comment.
Step 3 won't flip live after the first run is created
WorkspaceWelcome receives runsCount={initialTotal} — the server-rendered value frozen at mount time. RunList maintains its own total state that updates via polling, but this is never wired back to WorkspaceWelcome. After a user clicks "Run your first clash" and the run is created, step3Done (runsCount >= 1) stays false and the card never auto-hides on that page visit. A hard refresh is required, which contradicts the documented behavior ("each row flips to ✓ as the user makes progress").
The simplest fix is to have RunsPageClient maintain a runsTotal state that RunList can update via an onTotalChange callback:
// In RunsPageClient:
const [runsTotal, setRunsTotal] = useState(initialTotal);
// Pass to RunList:
<RunList ... onTotalChange={setRunsTotal} />
// Pass live total to WorkspaceWelcome:
<WorkspaceWelcome ... runsCount={runsTotal} />Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/app/(workspace)/workspaces/[workspaceId]/runs/runs-page-client.tsx
Line: 55-61
Comment:
**Step 3 won't flip live after the first run is created**
`WorkspaceWelcome` receives `runsCount={initialTotal}` — the server-rendered value frozen at mount time. `RunList` maintains its own `total` state that updates via polling, but this is never wired back to `WorkspaceWelcome`. After a user clicks "Run your first clash" and the run is created, `step3Done` (`runsCount >= 1`) stays `false` and the card never auto-hides on that page visit. A hard refresh is required, which contradicts the documented behavior ("each row flips to ✓ as the user makes progress").
The simplest fix is to have `RunsPageClient` maintain a `runsTotal` state that `RunList` can update via an `onTotalChange` callback:
```tsx
// In RunsPageClient:
const [runsTotal, setRunsTotal] = useState(initialTotal);
// Pass to RunList:
<RunList ... onTotalChange={setRunsTotal} />
// Pass live total to WorkspaceWelcome:
<WorkspaceWelcome ... runsCount={runsTotal} />
```
How can I resolve this? If you propose a fix, please make it concise.| "The task each agent will attempt. Publish your own or browse the catalog.", | ||
| done: step2Done, | ||
| href: `/workspaces/${workspaceId}/challenge-packs`, | ||
| cta: step2Done ? "Browse packs" : "Browse packs", |
There was a problem hiding this comment.
Duplicate CTA string for step 2
Both branches of the ternary produce the same string, so the conditional is dead code. Since cta only renders when emphasized && !step.done, the step2Done ? "Browse packs" : arm can never actually be shown — but it's still confusing and looks like it was meant to say something different (e.g., "Manage packs" when done).
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/onboarding/workspace-welcome.tsx
Line: 58
Comment:
**Duplicate CTA string for step 2**
Both branches of the ternary produce the same string, so the conditional is dead code. Since `cta` only renders when `emphasized && !step.done`, the `step2Done ? "Browse packs" :` arm can never actually be shown — but it's still confusing and looks like it was meant to say something different (e.g., `"Manage packs"` when done).
How can I resolve this? If you propose a fix, please make it concise.…-peach # Conflicts: # web/src/app/(workspace)/workspaces/[workspaceId]/challenge-packs/publish-pack-dialog.tsx
Revert "feat(web): first-run onboarding for workspaces" (#392)
Summary
open/onOpenChangeprops through the four dialogs and splitting each page into a thin*-list-client.tsxthat owns the dialog state.GlossaryTermtooltips on jargon labels (Challenge Pack, Input Set, Agent Deployments, Regression Coverage, Official Pack Mode) and in the Runs subtitle.useOnboardingState(workspaceId)hook, SSR-safe viauseSyncExternalStore, backed by two per-workspace localStorage keys and synced cross-tab + same-tab viastorage/ custom events.Test plan
cd web && pnpm lintpassescd web && npx tsc --noEmitpassescd web && pnpm exec vitest runpasses (161 passed, 3 pre-existing skipped)/onboard) shows the welcome card with 0/3 checklistmake db-seed, the welcome shows ✓ on Deploy and Pack but not RunCreateRunDialog(?)icons are keyboard-focusable (Tab → Enter / Space)Out of scope (follow-ups)
starter-contentendpoint so "Use sample setup" can one-click provision two deployments + a pack.?) menu in the top bar as a durable home for docs + restart.🤖 Generated with Claude Code