Skip to content

Add guided tips feature discovery system#673

Merged
selfcontained merged 28 commits into
mainfrom
agt_39b2a3c92ffc/agent-c92ffc
Jun 13, 2026
Merged

Add guided tips feature discovery system#673
selfcontained merged 28 commits into
mainfrom
agt_39b2a3c92ffc/agent-c92ffc

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Summary

  • Inline tip popovers: TipSpot wrapper component that auto-opens a popover on upgrade-eligible features (version-gated via lastSeenVersion vs tip.since). Only one popover at a time via TipQueueProvider. Includes sparkle icon, description, "Learn more →" docs link, dismiss ✕, and "Don't show tips" affordance.
  • Ambient tip bar: Bottom bar that appears after 2.5min idle with 40% random chance. Auto-hides after 30s with hover-pause. Shows a random undismissed tip with doc link and dismiss controls.
  • Settings integration: "Tips & Guidance" section in Notifications settings with "Show tips" toggle and "Reset dismissed tips" button.
  • Unified tip registry: Centralized tips.ts with 5 starter tips (Quick Phrases, Personas, Brain, Automations, Media Sidebar). Each tip specifies since version and surfaces (inline/ambient).
  • State management: Three Jotai atoms (tipsEnabled, dismissedTips, lastSeenVersion) persisted via atomWithLocalStorage. First-time users (no lastSeenVersion) see only ambient tips, not inline popovers.

Test plan

  • pnpm run check — type check clean
  • pnpm run finalize:web — production build succeeds
  • pnpm --filter @dispatch/web test — 180/180 tests pass (includes 23 new tip tests)
  • pnpm run test:e2e — 168/168 pass, 12 skipped
  • Visual validation: inline popover appears on simulated upgrade, dismiss works, settings toggle and reset work

🤖 Generated with Claude Code

selfcontained and others added 28 commits June 11, 2026 20:28
Implements the central registry for tip definitions with 5 starter tips
covering Quick Phrases, Personas, Brain, Automations, and Media Sidebar.
Each tip includes ID, title, body, optional docs reference, version
introduced (since), and surfaces (inline/ambient). Includes getTipById
helper and comprehensive test suite validating structure and uniqueness.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Create tips-state.ts with three persistent atoms for tips feature:
- tipsEnabledAtom: master toggle (defaults to true)
- dismissedTipsAtom: tracks dismissed tip IDs (defaults to [])
- lastSeenVersionAtom: enables version-gating (defaults to null)

All atoms use atomWithLocalStorage for JSON serialization and cross-tab sync.
Export atomWithLocalStorage from store.ts to support these atoms.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add @testing-library/react and jsdom dev deps, configure vitest with
react plugin and __DISPATCH_VERSION__ define, and implement the
useTip hook that combines tip registry, state atoms, and version
comparison to drive inline and ambient tip visibility.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Two bugs prevented the inline popover from rendering:

1. React StrictMode double-invokes effects (run → cleanup → run).
   The mountedRef guard persisted across the cleanup cycle, so the
   second invocation returned early and never started the open timer.
   Fix: remove the mountedRef and let the effect lifecycle handle it.

2. PopoverTrigger with asChild tried to forward a ref to the child
   component, but function components without forwardRef silently
   fail. Fix: wrap children in a <span> so Radix can attach its ref.

Also latch eligibility via eligibleRef so the popover survives
lastSeenVersion atom updates from TipsVersionInit.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Replace display:contents span with inline-flex so Radix has a
  bounding box to anchor the popover against
- Add PopoverArrow pointing from popover to the trigger element
- Center-align the Quick Phrases popover on the icon instead of
  start-aligning

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Hitting "Reset dismissed tips" in settings now also sets
lastSeenVersion to "0.0.0", so inline tips reappear on the
next page load. Previously it only cleared the dismissed list,
which meant inline tips stayed hidden because they weren't
considered "new" relative to the current version.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Use fill-[hsl(var(--card))] to match the popover background and
stroke-white/20 on the SVG polygon to match the glassOverlay border.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Fill the arrow with white/20 to match the glassOverlay border color,
making it look like the border itself extends outward as a triangle.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Arrow border uses purple-500/20 to match popover's border-purple-500/20,
and fill uses --popover background instead of --card.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace double-triangle technique with a rotated square clipped to
show only the arrow half. The clip edge aligns with the popover border
so the base is invisible — only the two diagonal edges are visible,
making the arrow look like the popover border bulging outward.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace CSS pseudo-element rotated square with an inline SVG element.
The SVG draws a filled polygon (popover bg) with an open path (border
color, no base line) so the base edge is invisible. Removes backdrop-
filter and inset shadow from the tip popover so the SVG fill matches
the popover background exactly.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Learn More button navigates to /settings/help/:section#anchor with
  hash-based deep linking to specific doc headings
- Internalize navigation in TipSpot and AmbientTipBar via useNavigate
  (removed onOpenDocs prop plumbing)
- Arrow SVG offset tracks trigger position so it points at the
  referenced element, not the popover center
- Tip won't open when trigger is occluded by mobile sidebar overlay
  (uses elementFromPoint check)
- Polling retry ensures tip appears after sidebar closes
- Fix docsSection mappings: quick-phrases → agents#quick-phrases,
  brain → tools#brain
- Add id prop to H3 doc primitive for anchor targets

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…lity

Single triggerReachable state driven by MutationObserver + resize
listener. Both "wait to open" and "close when occluded" react to
the same state — no setInterval polling.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…bserver

Replace document-wide MutationObserver with an IntersectionObserver
scoped to the trigger element. The IO tracks viewport visibility;
an elementFromPoint gate at open time prevents showing behind
overlays. No document-level observers — zero overhead when the
tip's target element isn't rendered.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Drop IntersectionObserver entirely. Instead, check elementFromPoint
during render to gate the popover — if the trigger is covered by an
overlay, the popover doesn't show. Any re-render from normal app
activity (agent status, SSE events, navigation) re-evaluates
automatically. No observers, no polling.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The render-time elementFromPoint check read stale DOM from the previous
commit, so opening the sidebar showed the tip incorrectly because the
overlay wasn't in the DOM yet during that render. Moving the check to
useLayoutEffect ensures it runs after the DOM commits but before paint.

Also guards handleOpenChange so reachability-driven closes don't
permanently dismiss the tip.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ew tips

Add on-demand lightbulb button (desktop only) to request tips without waiting
for idle timer. Rework tip bar layout: 3-column flexbox with inline Learn more
link, Disable/Dismiss text actions, fade transitions, and proper vertical
centering in the footer area. Hide ambient tip surface on mobile. Add 4 new
ambient tips (keyboard shortcuts, worktrees, personalities, notifications).
Move job prompt files from .dispatch/job-prompts/ to docs/jobs/.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ersionInit

- Add aria-labels to lightbulb and Learn more buttons (a11y)
- Add pointer-events-none to tip bar wrapper, pointer-events-auto on interactive children
- Throttle mousemove idle timer resets (10s debounce)
- Add Escape key handler to dismiss TipSpot popovers
- Extract TipsVersionInit from App.tsx into tips/tips-version-init.tsx

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@selfcontained
selfcontained merged commit 04c7ce1 into main Jun 13, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_39b2a3c92ffc/agent-c92ffc branch June 13, 2026 03:36
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