Skip to content

feat(web): first-run onboarding for workspaces#392

Merged
AyushRajSinghParihar merged 2 commits into
mainfrom
worktree-lazy-growing-peach
Apr 24, 2026
Merged

feat(web): first-run onboarding for workspaces#392
AyushRajSinghParihar merged 2 commits into
mainfrom
worktree-lazy-growing-peach

Conversation

@AyushRajSinghParihar
Copy link
Copy Markdown
Collaborator

Summary

  • Dismissible welcome card on the Runs page with a 3-step checklist (deploy two agents → pick a challenge pack → run your first clash), driven by server-fetched counts so each row flips to ✓ as the user makes progress.
  • Action-oriented empty states on Runs, Deployments, Challenge Packs, and Builds — each CTA opens the right Create/Publish dialog, which required threading optional controlled open/onOpenChange props through the four dialogs and splitting each page into a thin *-list-client.tsx that owns the dialog state.
  • Plain-language GlossaryTerm tooltips on jargon labels (Challenge Pack, Input Set, Agent Deployments, Regression Coverage, Official Pack Mode) and in the Runs subtitle.
  • "Restart onboarding" entry in the user menu, and a one-time "First clash complete — View replay" sonner toast the first time a workspace's only run reaches a terminal state.
  • useOnboardingState(workspaceId) hook, SSR-safe via useSyncExternalStore, backed by two per-workspace localStorage keys and synced cross-tab + same-tab via storage / custom events.

Test plan

  • cd web && pnpm lint passes
  • cd web && npx tsc --noEmit passes
  • cd web && pnpm exec vitest run passes (161 passed, 3 pre-existing skipped)
  • Fresh workspace (via /onboard) shows the welcome card with 0/3 checklist
  • After make db-seed, the welcome shows ✓ on Deploy and Pack but not Run
  • Clicking the × dismisses the welcome; reload keeps it dismissed
  • User menu → "Restart onboarding" brings it back immediately (no reload)
  • Dismiss in one tab → welcome disappears in another (storage event)
  • Dismissal is per-workspace (workspace A dismissed ≠ workspace B dismissed)
  • "Run your first clash" opens the existing CreateRunDialog
  • Deployments / Challenge Packs / Builds empty-state CTAs open the matching Create/Publish dialog
  • Glossary (?) icons are keyboard-focusable (Tab → Enter / Space)
  • First-run toast fires once when a workspace's only run completes; doesn't re-fire on reload
  • Mobile (≤640px) — card stacks, CTAs wrap, no horizontal scroll
  • Dark mode — semantic tokens render correctly (default for the app)

Out of scope (follow-ups)

  • Backend starter-content endpoint so "Use sample setup" can one-click provision two deployments + a pack.
  • DB-backed onboarding preference so dismissal survives cleared browser storage.
  • Help (?) menu in the top bar as a durable home for docs + restart.

🤖 Generated with Claude Code

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentclash Ready Ready Preview, Comment Apr 24, 2026 7:34am

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR adds a first-run onboarding flow: a dismissible WorkspaceWelcome checklist card on the Runs page, action-oriented empty states with CTA buttons across Builds/Deployments/Challenge Packs/Runs, GlossaryTerm tooltips for jargon labels, a "Restart onboarding" user-menu item, and a one-time first-clash toast. The localStorage-backed useOnboardingState hook is SSR-safe and cross-tab via useSyncExternalStore.

  • Step 3 ("Run your first clash") won't auto-complete on the same page visit. WorkspaceWelcome receives runsCount={initialTotal} — a frozen server-rendered value. RunList owns a live total state that updates via polling after a run is created, but this is never surfaced back to WorkspaceWelcome, so the checklist item stays unchecked and the card never self-dismisses without a hard refresh.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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
Loading

Fix All in Codex

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

Comment on lines +55 to +61
<WorkspaceWelcome
workspaceId={workspaceId}
deploymentsCount={deploymentsCount}
packsCount={packsCount}
runsCount={initialTotal}
onOpenCreateRun={openCreateRun}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Codex

"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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Codex

…-peach

# Conflicts:
#	web/src/app/(workspace)/workspaces/[workspaceId]/challenge-packs/publish-pack-dialog.tsx
@AyushRajSinghParihar AyushRajSinghParihar merged commit 409d2fe into main Apr 24, 2026
1 of 2 checks passed
Atharva-Kanherkar added a commit that referenced this pull request Apr 24, 2026
Revert "feat(web): first-run onboarding for workspaces" (#392)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant