Skip to content

feat(web-ui): onboarding guidance card for new workspaces#489

Merged
frankbria merged 4 commits intomainfrom
feature/issue-467-ux-onboarding-guidance
Mar 24, 2026
Merged

feat(web-ui): onboarding guidance card for new workspaces#489
frankbria merged 4 commits intomainfrom
feature/issue-467-ux-onboarding-guidance

Conversation

@frankbria
Copy link
Owner

@frankbria frankbria commented Mar 23, 2026

Fixes #467

Summary

  • Adds OnboardingCard component that displays when a workspace is initialized but has no tasks yet
  • Shows the Think → Build → Prove → Ship pipeline with one-line descriptions for each step
  • "Get Started →" CTA links to /prd
  • Dismiss button persists state to localStorage per-workspace (never re-appears after dismiss)
  • Card is hidden when workspace already has tasks

Changes

  • web-ui/src/lib/workspace-storage.ts — adds getOnboardingDismissed and setOnboardingDismissed helpers
  • web-ui/src/components/workspace/OnboardingCard.tsx — new component
  • web-ui/src/components/workspace/index.ts — barrel export
  • web-ui/src/app/page.tsx — renders card when tasksData.tasks.length === 0
  • web-ui/src/__tests__/components/workspace/OnboardingCard.test.tsx — 7 unit tests

Test plan

  • npm test — 380/380 unit tests pass, 7 new tests for OnboardingCard
  • Card shows all 4 pipeline steps (Think, Build, Prove, Ship)
  • CTA links to /prd
  • Dismiss hides card and persists to localStorage
  • Card does not render if already dismissed (reads localStorage on mount)
  • Card is hidden when tasks exist (conditional in page.tsx)
  • No new lint errors introduced

Summary by CodeRabbit

  • New Features

    • Onboarding card shown for workspaces with no tasks once task data loads (not shown for uninitialized/not-found workspaces)
    • Displays pipeline steps: Think, Build, Prove, Ship
    • "Get Started" CTA linking to the product page
    • Dismissible per workspace; dismissal is persisted and respected
  • Tests

    • Added tests verifying rendering, steps, CTA, dismissal behavior, and persisted dismissal lookup

Show a welcome card explaining the Think→Build→Prove→Ship pipeline when
a workspace is initialized but has no tasks yet. Card is dismissible and
persists dismiss state per-workspace in localStorage.

- Add getOnboardingDismissed/setOnboardingDismissed to workspace-storage.ts
- New OnboardingCard component with 4-step pipeline and /prd CTA
- Render card in page.tsx when tasksData is loaded and tasks.length === 0
- 7 Jest tests covering all acceptance criteria
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00649800-ef10-4d92-a261-04b65f4a0089

📥 Commits

Reviewing files that changed from the base of the PR and between b81c271 and 1b504e5.

📒 Files selected for processing (1)
  • README.md

Walkthrough

A client OnboardingCard component and storage helpers were added; the workspace page now conditionally renders the card for empty, initialized workspaces. Tests for the card were added, the component was re-exported from the workspace index, and a small SQLite schema migration was introduced to add github_issue_number to tasks when missing.

Changes

Cohort / File(s) Summary
Onboarding component
web-ui/src/components/workspace/OnboardingCard.tsx
New client component that shows a 4-step pipeline (Think, Build, Prove, Ship), a "Get Started →" CTA to /prd, and a dismiss (X) control that persists dismissal per workspacePath.
Page integration
web-ui/src/app/page.tsx
WorkspacePage now conditionally renders OnboardingCard when tasksData is loaded and empty and workspace is not "not found".
Storage helpers
web-ui/src/lib/workspace-storage.ts
Added SSR-safe getOnboardingDismissed(workspacePath) and setOnboardingDismissed(workspacePath) using localStorage keys scoped per workspace.
Tests for onboarding
web-ui/src/__tests__/components/workspace/OnboardingCard.test.tsx
New Jest + React Testing Library tests mocking next/link and @/lib/workspace-storage, asserting render when not dismissed, absence when dismissed, pipeline labels, CTA href, dismiss behavior, and lookup with workspacePath.
Exports
web-ui/src/components/workspace/index.ts
Re-exported the new OnboardingCard.
Schema migration
codeframe/core/workspace.py
Added schema upgrade to add github_issue_number INTEGER column to tasks if missing.
Docs
README.md
Marked onboarding guidance card feature in the "What Works Today" checklist.

Sequence Diagram(s)

sequenceDiagram
    participant Page as WorkspacePage
    participant Card as OnboardingCard (Client)
    participant Storage as localStorage

    Page->>Card: render(workspacePath, tasksData)
    Card->>Storage: getOnboardingDismissed(workspacePath)
    Storage-->>Card: boolean (dismissed?)
    alt not dismissed
        Card-->>Page: render card (4 steps, CTA -> /prd)
        Page->>Card: user clicks Dismiss
        Card->>Storage: setOnboardingDismissed(workspacePath)
        Storage-->>Card: ack
        Card-->>Card: update local state -> hide card
    else dismissed
        Card-->>Page: render nothing
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped in with a tiny card to guide your first café,

Think, Build, Prove, Ship — a path to chase the day away.
Tap the arrow to PRD, or close me when you please,
I’ll tuck that choice in storage, per workspace, safe with ease.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes directly support the onboarding card feature; however, the database schema migration (github_issue_number column) in codeframe/core/workspace.py appears unrelated to the onboarding requirements. Review whether the github_issue_number column addition belongs in this PR or should be moved to a separate database schema migration PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature addition: an onboarding guidance card for new workspaces, which is the core change across all modified files.
Linked Issues check ✅ Passed The PR implementation fulfills all acceptance criteria from issue #467: card appears when no tasks exist, displays the 4-step pipeline (Think→Build→Prove→Ship), includes a CTA to /prd, is dismissible with per-workspace persistence, and hides when tasks are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-467-ux-onboarding-guidance

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
web-ui/src/lib/workspace-storage.ts (1)

85-88: Consider encoding workspacePath in the localStorage key.

Workspace paths may contain special characters (spaces, backslashes on Windows, etc.) that could lead to inconsistent key matching if paths are formatted differently between calls. Using encodeURIComponent would ensure consistent key generation.

♻️ Optional: Encode workspace path for robustness
 export function getOnboardingDismissed(workspacePath: string): boolean {
   if (typeof window === 'undefined') return false;
-  return localStorage.getItem(`codeframe_onboarding_dismissed_${workspacePath}`) === 'true';
+  return localStorage.getItem(`codeframe_onboarding_dismissed_${encodeURIComponent(workspacePath)}`) === 'true';
 }

Apply the same pattern to setOnboardingDismissed:

 export function setOnboardingDismissed(workspacePath: string): void {
   if (typeof window === 'undefined') return;
-  localStorage.setItem(`codeframe_onboarding_dismissed_${workspacePath}`, 'true');
+  localStorage.setItem(`codeframe_onboarding_dismissed_${encodeURIComponent(workspacePath)}`, 'true');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/lib/workspace-storage.ts` around lines 85 - 88, The localStorage
key built from workspacePath in getOnboardingDismissed can be inconsistent for
paths with special characters; update getOnboardingDismissed to use
encodeURIComponent(workspacePath) when constructing the key
(`codeframe_onboarding_dismissed_${...}`) and make the identical change in
setOnboardingDismissed so both reader and writer use the same encoded key
format; keep the existing typeof window === 'undefined' guard and the 'true'
string comparison logic unchanged.
web-ui/src/components/workspace/OnboardingCard.tsx (2)

33-40: Potential flash of content when card was previously dismissed.

Initializing isDismissed to false and then updating it in useEffect means users who previously dismissed the card will briefly see it flash before it disappears on hydration. Consider initializing the state lazily or adding a loading state to prevent this flicker.

♻️ Option 1: Use lazy initial state
 export function OnboardingCard({ workspacePath }: OnboardingCardProps) {
-  const [isDismissed, setIsDismissed] = useState(false);
-
-  useEffect(() => {
-    setIsDismissed(getOnboardingDismissed(workspacePath));
-  }, [workspacePath]);
+  const [isDismissed, setIsDismissed] = useState(() => 
+    getOnboardingDismissed(workspacePath)
+  );
+
+  // Re-check if workspacePath changes
+  useEffect(() => {
+    setIsDismissed(getOnboardingDismissed(workspacePath));
+  }, [workspacePath]);

   if (isDismissed) return null;

This works because getOnboardingDismissed safely returns false during SSR, but will return the actual value during client-side hydration when window is defined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/workspace/OnboardingCard.tsx` around lines 33 - 40, The
flash happens because isDismissed starts false then is set in useEffect; instead
initialize it lazily and keep the effect to handle workspacePath changes: change
useState to useState(() => getOnboardingDismissed(workspacePath)) in
OnboardingCard so the correct value is used on first render (prevents flicker),
and retain the existing useEffect([workspacePath]) with
setIsDismissed(getOnboardingDismissed(workspacePath)) to update when
workspacePath changes.

29-31: Consider moving OnboardingCardProps to the shared types file.

Per coding guidelines, TypeScript types should be defined in web-ui/src/types/index.ts. While this is a simple interface, consistency with the codebase guidelines would suggest moving it there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/workspace/OnboardingCard.tsx` around lines 29 - 31,
Move the OnboardingCardProps interface out of OnboardingCard.tsx into the shared
types module (export it from web-ui/src/types/index.ts as OnboardingCardProps)
and update OnboardingCard.tsx to import { OnboardingCardProps } from that types
file; ensure the exported name matches the original interface and update any
other files referencing OnboardingCardProps to import from the central types
file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/src/__tests__/components/workspace/OnboardingCard.test.tsx`:
- Around line 6-12: The MockLink in the jest.mock('next/link') block uses the
React.ReactNode type but React isn't imported; add an import for React (e.g.,
import * as React from 'react' or import React from 'react') at the top of the
test file so the React.ReactNode annotation on the MockLink props is valid and
TypeScript errors are resolved.

---

Nitpick comments:
In `@web-ui/src/components/workspace/OnboardingCard.tsx`:
- Around line 33-40: The flash happens because isDismissed starts false then is
set in useEffect; instead initialize it lazily and keep the effect to handle
workspacePath changes: change useState to useState(() =>
getOnboardingDismissed(workspacePath)) in OnboardingCard so the correct value is
used on first render (prevents flicker), and retain the existing
useEffect([workspacePath]) with
setIsDismissed(getOnboardingDismissed(workspacePath)) to update when
workspacePath changes.
- Around line 29-31: Move the OnboardingCardProps interface out of
OnboardingCard.tsx into the shared types module (export it from
web-ui/src/types/index.ts as OnboardingCardProps) and update OnboardingCard.tsx
to import { OnboardingCardProps } from that types file; ensure the exported name
matches the original interface and update any other files referencing
OnboardingCardProps to import from the central types file.

In `@web-ui/src/lib/workspace-storage.ts`:
- Around line 85-88: The localStorage key built from workspacePath in
getOnboardingDismissed can be inconsistent for paths with special characters;
update getOnboardingDismissed to use encodeURIComponent(workspacePath) when
constructing the key (`codeframe_onboarding_dismissed_${...}`) and make the
identical change in setOnboardingDismissed so both reader and writer use the
same encoded key format; keep the existing typeof window === 'undefined' guard
and the 'true' string comparison logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03213cec-fd31-4533-b077-37ca1af9650e

📥 Commits

Reviewing files that changed from the base of the PR and between 1a6e499 and 3e9583a.

📒 Files selected for processing (5)
  • web-ui/src/__tests__/components/workspace/OnboardingCard.test.tsx
  • web-ui/src/app/page.tsx
  • web-ui/src/components/workspace/OnboardingCard.tsx
  • web-ui/src/components/workspace/index.ts
  • web-ui/src/lib/workspace-storage.ts

@claude
Copy link

claude bot commented Mar 24, 2026

test edit

@claude
Copy link

claude bot commented Mar 24, 2026

Review: OnboardingCard (#467)

Clean, focused PR. SSR hydration approach is correct, test coverage solid, component scoped appropriately.

Minor Issue 1 - Flash on first render (dismissed users): The useState(false)+useEffect pattern is the right call for SSR safety. But dismissed users briefly see the card before the effect fires. Standard fix: track a mounted boolean alongside isDismissed, set both in the useEffect, gate the null return on both. Not a blocker.

Minor Issue 2 - Dismiss button uses raw element: The className replicates variant=ghost size=icon. Use the Button component for consistency with the design system.

Minor Issue 3 - localStorage key uses raw workspace path: codeframe_onboarding_dismissed_workspacePath embeds the path directly - can produce odd keys on Windows or paths with special chars. Wrap with encodeURIComponent(workspacePath).

Looks Good: SSR guard (typeof window === undefined) in workspace-storage.ts matches existing helpers. Page conditional on tasksData !== undefined is correct. 7 tests with mock-then-render work because RTL render() flushes via act(). WorkspaceStatsCards placement is right. aria-label on icon-only button is correct.

Suggestion: A clearOnboardingDismissed export in workspace-storage.ts would be useful for reset-workspace flows and E2E test setup.

Overall: ready to merge after Button component consistency fix and localStorage key encoding. Flash trade-off is acceptable.

- Add React import to test file (React.ReactNode type annotation)
- Use encodeURIComponent in localStorage key for path safety
- Use lazy useState initializer to prevent flash-of-content
@claude
Copy link

claude bot commented Mar 24, 2026

Review: feat(web-ui): onboarding guidance card for new workspaces

Good addition — the feature is well-scoped, tests are solid, and the encodeURIComponent on the localStorage key (already in the diff) is the right call.

One correctness issue worth addressing before merge:


🔴 SSR/Hydration Mismatch — OnboardingCard.tsx

The useState lazy initializer calls getOnboardingDismissed directly:

const [isDismissed, setIsDismissed] = useState(() =>
  getOnboardingDismissed(workspacePath)
);

In Next.js App Router, 'use client' components are still server-rendered for the initial HTML. On the server, getOnboardingDismissed returns false (the typeof window === 'undefined' guard). On the client, it may return true from localStorage. This mismatch causes a React hydration error or warning for users who have already dismissed the card.

The fix is to initialise with false and read localStorage in a useEffect:

const [isDismissed, setIsDismissed] = useState(false);

useEffect(() => {
  setIsDismissed(getOnboardingDismissed(workspacePath));
}, [workspacePath]);

This also consolidates the two separate useState/useEffect calls into one clean pattern. The brief flash (card visible → hidden) on return visits is acceptable UX for a dismissed state.


🟡 Missing test: workspace switching via useEffect

The useEffect that re-reads dismiss state when workspacePath changes is a meaningful code path but not covered. A test like this would close the gap:

it('updates dismiss state when workspacePath changes', () => {
  mockGetOnboardingDismissed.mockReturnValue(false);
  const { rerender } = render(<OnboardingCard workspacePath="/project-a" />);
  expect(screen.getByRole('link', { name: /get started/i })).toBeInTheDocument();

  mockGetOnboardingDismissed.mockReturnValue(true);
  rerender(<OnboardingCard workspacePath="/project-b" />);
  expect(screen.queryByRole('link', { name: /get started/i })).not.toBeInTheDocument();
});

✅ Everything else looks good

  • page.tsx condition (tasksData !== undefined && tasks.length === 0) is correct — card only appears when data is loaded and empty
  • Barrel export in index.ts is clean
  • ol with custom numbered badges is semantically appropriate for ordered steps
  • 7 tests cover all the primary states and interactions

The hydration fix is the only blocking item; the workspace-switching test is recommended but not blocking.

…sting DBs

Existing workspaces created before github_issue_number was added were
missing the column in _ensure_schema_upgrades, causing API 500 errors
on tasks endpoints. Matches the migration already in _init_database.
@claude
Copy link

claude bot commented Mar 24, 2026

Review

The onboarding card implementation is clean and well-scoped. The CodeRabbit suggestions (lazy useState init, encodeURIComponent key encoding) have already been applied in the current diff — good. A few remaining items worth addressing:


🔴 Unrelated change: workspace.py schema migration

The github_issue_number column addition in codeframe/core/workspace.py has nothing to do with the onboarding card. No model, serializer, or CLI code in this PR uses it, so it appears to be a stray change that belongs in a separate PR. Mixing schema migrations with UI features makes bisecting and rollback harder.


🟡 Dismiss button uses raw <button>, not the Button component

OnboardingCard.tsx uses <Button asChild> for the CTA but a raw <button> for the dismiss control. For consistency with the rest of the UI (and to get focus/hover states from the design system):

<Button
  variant="ghost"
  size="icon"
  onClick={handleDismiss}
  aria-label="Dismiss onboarding"
  className="ml-4 -mr-2 -mt-1 h-7 w-7"
>
  <Cancel01Icon className="h-4 w-4" />
</Button>

🟡 No way to clear dismissed state

setOnboardingDismissed only marks dismissed — there's no clearOnboardingDismissed. If a workspace is re-initialized or the user wants to re-read the guide, there's no escape hatch short of clearing localStorage manually. A companion function (even unexported for now) would be cheap to add and useful for future workspace-reset flows.


🟢 Minor: guard against empty workspacePath

If workspacePath is an empty string, the localStorage key becomes codeframe_onboarding_dismissed_ (shared across all empty-path workspaces). A one-line guard in getOnboardingDismissed / setOnboardingDismissed prevents the edge case:

if (!workspacePath) return false; // or return early in setter

✅ What's good

  • 'use client' directive is correct
  • typeof window === 'undefined' guard prevents SSR crashes
  • Lazy useState init + useEffect on workspacePath change is the right pattern
  • tasksData !== undefined guard avoids flash during loading
  • 7 tests cover all behavioral branches cleanly
  • Barrel export and page integration are minimal and correct

@claude
Copy link

claude bot commented Mar 24, 2026

The component is well-scoped and the implementation is clean. The fixes from CodeRabbit's previous pass (lazy useState, encodeURIComponent in localStorage keys, React import in tests) have all been applied.

One concern: unrelated schema migration in workspace.py

The addition of github_issue_number INTEGER to the tasks table is not mentioned in the PR description and is unrelated to the onboarding card. Schema migrations in _ensure_schema_upgrades should ship with the code that reads/writes the new column — otherwise the column sits unused and reviewers cannot trace the intent. Please either move it to the PR that actually uses github_issue_number, or add a note here explaining why it belongs with this change.

Minor: OnboardingCardProps placement

CodeRabbit flagged moving the interface to web-ui/src/types/index.ts per codebase conventions. Worth a one-line move if that is the established pattern; fine to leave inline otherwise.

Everything else looks good:

  • Lazy initializer correctly handles SSR and prevents the dismissed-card flicker
  • encodeURIComponent key encoding is consistent between get/set
  • 7 tests cover the meaningful behaviors; a test for the useEffect workspace-switch path would be a nice-to-have but is not blocking
  • aria-label on the dismiss button is correct
  • Conditional render in page.tsx correctly avoids showing the card during the loading state

The workspace.py schema change is the only item I would ask to be resolved before merge.

@frankbria frankbria merged commit 39c1f8b into main Mar 24, 2026
10 checks passed
@frankbria frankbria deleted the feature/issue-467-ux-onboarding-guidance branch March 24, 2026 02:48
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.

UX: Add onboarding guidance after workspace initialization

1 participant