Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions openspec/changes/add-onboarding-tour/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Design — Add Onboarding Tour

## Trigger and gating

The tour is gated by a single persisted field, `tourCompletedAt: number |
null`, stored alongside other UI state in `src/store/persistence.ts`.

`tourCompletedAt` is `null` after `loadState()` for both fresh installs and
existing users who upgrade. To avoid hijacking the UI for users who already
know the app, the activation pass first inspects the loaded state for any
prior project or task. If at least one exists, the tour is treated as
already completed: `tourCompletedAt` is set to the current timestamp and no
overlay is shown. Only installs with zero projects and zero tasks proceed to
actual activation.

When activation does proceed, the tour starts from `App.tsx` `onMount` after
`loadState()` resolves. If a modal dialog is open at that moment (unlikely
on first launch, but possible after `restartTour` from settings), activation
is deferred: a Solid effect watches the modal-flag signals (`showHelpDialog`,
`showSettingsDialog`, `showNewTaskDialog`, `showArena`) and triggers
activation as soon as they are all false, provided `tourCompletedAt` is
still `null`. The keybinding migration banner is suppressed while the tour
is active and re-evaluates after completion or skip.

## Architecture

A new store slice `src/store/tour.ts` exposes:

- `tourActive: boolean`
- `tourStep: number` (0-indexed)
- `startTour()`, `nextStep()`, `prevStep()`, `skipTour()`, `finishTour()`
- `restartTour()` — clears `tourCompletedAt` and calls `startTour()`

A new component `src/components/TourOverlay.tsx` renders when `tourActive` is
true. It is mounted in `App.tsx` near the existing dialogs (`HelpDialog`,
`SettingsDialog`, `ArenaOverlay`). Steps are declared as data, not JSX:

```ts
type TourStep = {
id: string;
anchorId: string | null; // null = centered, no spotlight
title: string;
body: string;
placement: 'top' | 'right' | 'bottom' | 'left' | 'center';
beforeEnter?: () => void; // e.g. open NewTaskDialog so its anchor exists
afterLeave?: () => void; // e.g. close it again
};
```

The overlay locates its anchor via `document.querySelector('[data-tour-id="<
id>"]')`, observes its `getBoundingClientRect`, and renders:

- A full-viewport dimmer with an SVG cutout over the anchor's bounding rect.
- A tooltip panel positioned relative to the anchor (simple heuristic; no
popper dependency).
- Prev / Next / Skip controls; step counter ("3 of 8"); Esc to skip.

Anchor positions are recomputed on `resize` and via a `ResizeObserver` on the
anchor element so the spotlight follows window resizes and layout shifts.

## Steps

| # | `anchorId` | Teaches |
|---|-------------------------|------------------------------------------------------------------------|
| 1 | `null` (centered) | Welcome; one-line model: "every task = its own git worktree." |
| 2 | `tour-project-picker` | "Pick or add a project — your repo lives here." |
| 3 | `tour-new-task` | "Each task creates a branch + worktree automatically." |
| 4 | `tour-agent-selector` | "Choose Claude Code, Codex, Gemini, or a custom agent." |
| 5 | `tour-task-terminal` | "Watch the agent work live; type to interject." |
| 6 | `tour-changed-files` | "Review diffs as files change; click for full Monaco view." |
| 7 | `tour-merge-action` | "Merge back to main from the sidebar when you're happy." |
| 8 | `null` (centered) | "Press `?` anytime to see all shortcuts. You're done." |

Step 4 needs the `NewTaskDialog` open so its anchor exists. The step uses
`beforeEnter: () => toggleNewTaskDialog(true)` and
`afterLeave: () => toggleNewTaskDialog(false)`. The dialog's normal
keybindings are suppressed while the tour is active so the user can't
accidentally submit a task during the tour.

## First-run with no project

If the user has no project at the time the tour starts, steps 5–7 have no
DOM anchor. We resolve this by partitioning the tour into two phases:

- **Phase 1 (steps 1–4)** runs immediately on first launch and ends with a
prompt: "Create your first task to continue the tour, or skip."
- **Phase 2 (steps 5–8)** resumes the first time a task panel mounts after
Phase 1 completed, gated by a `tourStep` resume token persisted alongside
`tourCompletedAt`.

Both phases share the same store and overlay; only the gating logic differs.
Skipping in either phase finalises `tourCompletedAt` so the tour does not
re-trigger.

## Accessibility

- Tooltip is `role="dialog"` with `aria-labelledby` (title) and
`aria-describedby` (body).
- `lib/focus-trap.ts` is reused to trap focus inside the tooltip; on close,
focus is restored to the anchor element.
- An `aria-live="polite"` region inside the overlay announces step
transitions ("Step N of M — <title>") so screen-reader users hear forward
/ back navigation. The live region only updates on actual step changes,
not on anchor reposition.
- `prefers-reduced-motion: reduce` disables the spotlight transition and any
fade-ins.
- The overlay's dimmer has `aria-hidden="true"` so screen readers ignore it.

## Known implementation risks

These are not spec-level requirements but implementation decisions that need
care during the actual build. Calling them out here so they don't surprise
the implementer.

- **Anchor lookup timing.** A flat 300 ms wait for the anchor to appear is
fragile on slow machines; prefer a `MutationObserver` that resolves as
soon as the element appears, with a longer absolute fallback (e.g. 3 s)
before skipping. Skipping a step should be logged.
- **Anchor disappearance mid-step.** If the anchor unmounts while a step is
visible (e.g. user collapses a panel via shortcut), advance the tour
rather than render a spotlight over an empty rectangle.
- **Anchor existence test.** Add a test that walks the step list, mounts
the relevant components, and asserts every non-null `anchorId` resolves
to a DOM node — so a future refactor that drops a `data-tour-id` fails
loudly instead of producing a silently broken tour.
- **`aria-live` region implementation.** The live region must be keyed on
`tourStep` (not re-rendered on every overlay tick) so that anchor
repositions or focus restorations don't re-announce the step. A naive
implementation that sets `textContent` inside an effect with broader
dependencies will violate the spec's de-duplication scenario.
- **Single-window scope.** Activation logic assumes a single renderer; if
the app ever opens a second window, only the first window to read
`tourCompletedAt === null` runs the tour. The implementation may guard
against this with a per-process flag, but the spec does not promise
multi-window behavior.
- **Hook failure logging.** `beforeEnter` and `afterLeave` hooks (e.g.
opening / closing `NewTaskDialog`) can throw; anchor lookups can time
out and skip. These catch sites must route through the structured
logger added by the `add-structured-logging` proposal under category
`tour`, not `console.warn`. If logging lands first, this is a hard
dependency at implementation time; if tour lands first, expect a
follow-up sweep.
- **Cross-proposal: dialog stack.** The spec lists four named modal
signals to defer activation against. The `improve-dialog-accessibility`
proposal introduces a stack-counted store of open dialogs; if it lands
first the tour implementation should consume that stack instead of
enumerating named signals, so a future fifth dialog doesn't silently
let the tour activate over it. The spec deliberately states the
observable behaviour (defer while a modal is open) without committing
to either source of truth.

## Out of scope

- Per-OS or per-agent tour variants.
- Telemetry on tour completion (no infra exists today).
- Animations, video, or interactive demo data.
- Replacing or restructuring `HelpDialog`.
43 changes: 43 additions & 0 deletions openspec/changes/add-onboarding-tour/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Add Onboarding Tour

## Why

The app's mental model — every task is a git worktree on its own branch, and
multiple agents can run in parallel — is unusual enough that first-time users
hit dead ends before they understand the workflow. `HelpDialog` lists the
shortcuts but never explains what the worktree-per-task model is, or how to get
from "fresh install" to "merged change". A short, skippable, first-launch tour
that points at the real UI elements (project picker, new task button, agent
selector, terminal panel, changed-files panel, merge action, help) closes that
gap without forcing users to read documentation.

## What changes

- Add a guided tour overlay that runs once on first launch and walks the user
through the project → spawn → review → merge flow.
- Mount the overlay at the top level of the app shell so it can dim the page
and spotlight real DOM anchors via `data-tour-id` attributes added to
existing components (no DOM restructuring).
- Persist a single `tourCompletedAt` timestamp so the tour does not re-trigger
after dismissal or completion.
- Auto-mark the tour completed for users who upgrade with prior projects or
tasks already in their persisted state, so existing installs do not get
hijacked by a tour they did not ask for.
- Add a "Restart tour" button to `SettingsDialog` so users can replay it on
demand.
- Defer the existing keybinding-migration banner until the tour is dismissed
or completed, so the two onboarding surfaces never overlap.

## Impact

- New capability `onboarding-tour`.
- New persisted store field `tourCompletedAt: number | null` (handled by the
existing autosave persistence path; no migration code needed for additive
optional fields).
- Additions to `App.tsx` (mount the overlay, gate first-launch activation),
`SettingsDialog.tsx` ("Restart tour" button), and a small set of existing
components (`Sidebar`, `NewTaskDialog`, `TaskPanel`) which gain
`data-tour-id` attributes. The final step is centered with no anchor since
the app does not surface a visible help button — help is invoked via the
`?` shortcut, which the step text mentions.
- No new IPC channels, no new backend work.
Loading
Loading