Skip to content

fix(init): skip the welcome animation for reduced-motion users - #1462

Merged
clay-good merged 3 commits into
mainfrom
fix/init-no-animation
Jul 27, 2026
Merged

fix(init): skip the welcome animation for reduced-motion users#1462
clay-good merged 3 commits into
mainfrom
fix/init-no-animation

Conversation

@clay-good

@clay-good clay-good commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #722

Status: LGTM — full suite green (112 files, 2,287 tests), verified end-to-end against a real PTY, three independent adversarial review passes applied.

What was wrong: the openspec init welcome screen animates eight ASCII frames on a 120ms loop with ANSI cursor-clearing, and there was no way to turn it off. As #722 explains, flashing and animation are seizure and nausea triggers; the only escape hatches (NO_COLOR, a narrow terminal) are unrelated workarounds.

How it's fixed: canAnimate() now also routes to the pre-existing static welcome screen when any of these say "no motion":

Signal Behavior
OS reduced-motion preference Detected best-effort: macOS Reduce Motion (defaults read com.apple.universalaccess reduceMotion), GNOME animations disabled (gsettings get org.gnome.desktop.interface enable-animations). 500ms timeout, SIGKILL on expiry; any lookup failure keeps animation on.
OPENSPEC_NO_ANIMATION env var Presence-based: any value, including empty, disables the animation
openspec init --no-animation Per-run flag

Defaults are untouched: users with no preference set see the exact same animation, non-TTY output is byte-identical, and the OS lookup only runs on interactive TTYs (last check, so CI/tests never spawn a process). The static screen was already the fallback for non-TTY/NO_COLOR, so no new rendering path is introduced.

Proof: driving the built CLI through a PTY (script) and counting welcome-frame paints in the transcript:

default:                  9 paints, 10 cursor-up repaint sequences (animates, unchanged)
--no-animation:           1 paint, no repaints
OPENSPEC_NO_ANIMATION=1:  1 paint, no repaints

New unit tests cover both env/flag static paths (including the empty-value env case), the macOS/GNOME/other-platform detection branches (child_process mocked, so tests are deterministic on machines that really have Reduce Motion on), and the completion-registry parity test pins the new flag against Commander.

Notes:

  • Detection is deliberately fail-open (any error = animate) so a missing defaults/gsettings, sandboxed exec, or slow MDM machine can never break or stall init — worst case is one ≤500ms lookup before an animation the user sits at anyway.
  • Windows has no reliable console reduced-motion signal, so it relies on the flag/env var (no process is spawned there).
  • Scope limit: the ora progress spinners later in init ("Setting up <tool>...") still animate; they're single-line, brief, and orthogonal to the welcome-screen loop the issue describes. Extending the no-motion signal to spinners (init + update) is a natural follow-up.
  • Pre-existing nit, unchanged: the static welcome text ends with "Press Enter to select tools..." even though the static path doesn't wait for Enter (the selector appears immediately). Same behavior NO_COLOR users already get; happy to fix in a follow-up.
  • Docs: docs/cli.md init options + environment-variable table updated; changeset included (patch).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a --no-animation option to openspec init to show a static welcome screen instead of animations.
    • The welcome animation now automatically falls back to the static screen when reduced-motion preferences are enabled, when animations are disabled via OPENSPEC_NO_ANIMATION, or when NO_COLOR is set.
  • Documentation
    • Updated CLI docs to document --no-animation, OPENSPEC_NO_ANIMATION, and reduced-motion behavior.
  • Tests
    • Expanded welcome-screen tests to cover reduced-motion and animation-disabling scenarios.

The openspec init welcome animation had no off switch: it repainted eight
frames on a 120ms loop with ANSI cursor-clearing, which is a seizure and
nausea trigger for motion-sensitive users (#722).

canAnimate() now also yields the existing static welcome screen when:
- the OS reduced-motion preference is on (macOS Reduce Motion, GNOME
  animations disabled), detected best-effort with a 500ms timeout and
  animation kept on any lookup failure
- OPENSPEC_NO_ANIMATION is set
- the new init --no-animation flag is passed

Closes #722

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good
clay-good requested a review from a team as a code owner July 27, 2026 21:54
@clay-good
clay-good requested review from alfred-openspec and removed request for a team July 27, 2026 21:54
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

openspec init now supports --no-animation, OPENSPEC_NO_ANIMATION, and operating-system reduced-motion preferences. When animation is disabled, the command renders a static welcome screen. Tests, documentation, and Changesets metadata cover the new behavior.

Changes

Init welcome accessibility

Layer / File(s) Summary
CLI animation control
src/cli/index.ts, src/core/completions/command-registry.ts, src/core/init.ts
The init command defines --no-animation, stores the animation setting, and passes it to the welcome screen.
Welcome motion detection and static rendering
src/ui/welcome-screen.ts
The welcome screen checks explicit options, OPENSPEC_NO_ANIMATION, and macOS/Linux reduced-motion preferences before choosing animated or static output.
Validation and user-facing documentation
test/ui/welcome-screen.test.ts, test/core/init.test.ts, docs/cli.md, .changeset/init-no-animation.md
Tests cover animation suppression and platform detection; documentation and release metadata describe the new controls.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant InitCommand
  participant WelcomeScreen
  participant OSSettings
  CLI->>InitCommand: pass animation option
  InitCommand->>WelcomeScreen: showWelcomeScreen(workflows, animate)
  WelcomeScreen->>OSSettings: query reduced-motion preferences
  OSSettings-->>WelcomeScreen: return motion preference
  WelcomeScreen-->>InitCommand: render animated or static screen
Loading

Possibly related PRs

Suggested reviewers: alfred-openspec

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies #722 by adding a --no-animation option and related accessibility controls for the init welcome screen.
Out of Scope Changes check ✅ Passed The added env var support, reduced-motion detection, docs, tests, and changeset all support the stated init accessibility objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: skipping the init welcome animation for reduced-motion users.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/init-no-animation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying openspec-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5cdb0a3
Status: ✅  Deploy successful!
Preview URL: https://757b261a.openspec-docs.pages.dev
Branch Preview URL: https://fix-init-no-animation.openspec-docs.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/core/init.test.ts (1)

902-902: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the disabled value through InitCommand.

This only locks in the default path. Add a companion test constructing InitCommand with { animation: false } and asserting showWelcomeScreenMock receives { animate: false }, so a core-layer wiring regression cannot pass the UI-only static-render tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/init.test.ts` at line 902, Add a companion test around the existing
InitCommand welcome-screen test that constructs InitCommand with animation
disabled, then invokes it and asserts showWelcomeScreenMock is called with
['explore', 'new'] and { animate: false }.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ui/welcome-screen.ts`:
- Around line 124-125: Update the OPENSPEC_NO_ANIMATION check in the animation
decision logic to disable animation whenever the environment variable is
explicitly set, including an empty string, by checking specifically for
undefined. Add a regression test covering OPENSPEC_NO_ANIMATION= and preserve
the existing behavior when the variable is unset.

---

Nitpick comments:
In `@test/core/init.test.ts`:
- Line 902: Add a companion test around the existing InitCommand welcome-screen
test that constructs InitCommand with animation disabled, then invokes it and
asserts showWelcomeScreenMock is called with ['explore', 'new'] and { animate:
false }.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bd23da5-b35e-4752-9dcc-1287a003ce3b

📥 Commits

Reviewing files that changed from the base of the PR and between 05c7019 and 5fc4421.

📒 Files selected for processing (8)
  • .changeset/init-no-animation.md
  • docs/cli.md
  • src/cli/index.ts
  • src/core/completions/command-registry.ts
  • src/core/init.ts
  • src/ui/welcome-screen.ts
  • test/core/init.test.ts
  • test/ui/welcome-screen.test.ts

Comment thread src/ui/welcome-screen.ts Outdated
clay-good and others added 2 commits July 27, 2026 17:01
Presence is what counts, like NO_COLOR: OPENSPEC_NO_ANIMATION= (set but
empty) now also disables the welcome animation, matching the documented
'when set' behavior. CodeRabbit review follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 5cdb0a3. Reduced-motion detection, --no-animation, and present-even-empty OPENSPEC_NO_ANIMATION all use the existing static path; 84 focused tests, build, lint, the 2,286-test cross-platform suite, and all CI/security checks pass.

@clay-good
clay-good added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit ebf66c7 Jul 27, 2026
17 checks passed
@clay-good
clay-good deleted the fix/init-no-animation branch July 27, 2026 22:19
clay-good added a commit that referenced this pull request Jul 28, 2026
The static branch printed 'Press Enter to select tools...' and returned
immediately, so the Enter landed in the tool picker and submitted the
pre-selected set sight-unseen. #1462 routed reduced-motion,
OPENSPEC_NO_ANIMATION, --no-animation, NO_COLOR, and narrow-terminal
users onto this path. Wait in a TTY; drop the prompt line when there is
no TTY to wait on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
birhantprkc pushed a commit to birhantprkc/OpenSpec that referenced this pull request Jul 29, 2026
…-audit follow-ups (Fission-AI#1475)

* fix(archive): make the scenario-drift check fence-aware

parseScenarioBlocks matched #### Scenario: headers on raw lines while the
validator's countScenarios masks fenced code blocks (Fission-AI#1151). The drift
check (Fission-AI#1391) inherited the raw scan, so a fenced scenario example in the
current spec aborted an archive that validate had passed, and a fenced
name in the MODIFIED block counted as keeping a scenario the block had
actually dropped. Build the shared code-fence mask and skip masked lines
in both the header scan and the block-end scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): tear down the redirected request when the budget expires

The overall request budget was armed inside the first send() and its
callback closed over that hop's request. After a redirect the timer
destroyed the already-dead first request, so a redirect target that
trickled bytes kept resetting its idle timeout and held the socket open
until the body-size cap. Track the in-flight request and have the budget
timer destroy whichever one is open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(release): add changesets for user-facing changes missing from the 1.7.0 notes

18 feat/fix commits merged since v1.6.0 without a changeset, so the
pending Version Packages PR would have released them silently: five tool
integrations (ZCode, Hermes, CodeArts, Kimi Code rename, Codex
skills-only), skills.sh distribution, symlinked schema dirs, nested spec
discovery, drift multiplicity, checkbox markers, Windows welcome input,
npx avoidance, doctor store drift, local dates, missing-core-workflows
warning, store-aware main specs, open-questions guidance, and spec
content guidance. Plus changesets for this branch's two fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): escape TOML-active characters in Gemini command files

The gemini adapter interpolated the description into a TOML basic string
and the body into a multiline basic string with no escaping. Every
current template value happens to be safe; the first description with a
double quote or backslash would silently produce invalid TOML for all
Gemini command files. Escape both contexts (Fission-AI#1447 fixed the same class
for the YAML adapters but scoped itself to YAML).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): harden install detection and redirect handling

Three follow-ups from the release audit:
- A path segment literally named volta (a user or project directory)
  classified the install as volta-managed and swallowed the upgrade
  offer. The undotted spelling now requires volta's own tools/image
  layout, matching how pnpm and yarn already demand corroboration.
- The Windows npm-ownership fallback checked that the npm prefix exists,
  which is true of any X\node_modules\pkg tree, hand-copied ones
  included. Corroborate with the openspec.cmd shim npm actually writes.
- A https registry redirecting to plain http was followed; a MITM on
  that reply controls the newer-version answer. Refuse the downgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(cli): export zcodeAdapter from the barrel and sync a completion description

zcode was registered but missing from the adapters barrel (its test
imported the module directly), and the completion registry still carried
the pre-Fission-AI#1062 description for the instructions command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parser): strip a UTF-8 BOM before parsing specs and deltas

A BOM-prefixed delta spec (Windows editors, PowerShell Out-File) failed
validate and archive with 'No delta sections found' because the first
line never matched '## ADDED Requirements'. Strip the BOM in both
normalizers, the same way tool detection already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): reject over-long change names with a validation message

A 300-character change name surfaced two raw ENAMETOOLONG errno dumps
from stat and mkdir. Bound the name at 200 characters in
validateChangeName so the failure is a normal validation error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): finish the early-sync no-op rules for MODIFIED and RENAMED

Two asymmetries left over from the Fission-AI#1376/Fission-AI#1386/Fission-AI#1437 no-op work:

- MODIFIED counted every delta as applied even when the block was
  byte-equal to the main spec, so a fully early-synced change rewrote
  the file (normalization churn), printed '~ N modified', and reported
  specsUpdated: true where its ADDED/REMOVED/RENAMED twins print 'Specs
  already in sync; no files changed.' Count only real replacements.
- RENAMED's already-synced skip (source gone, target present) had no
  near-miss guard: a case/whitespace variant of the source still in the
  spec means a typo'd header, and REMOVED already hard-aborts on that
  signal. Apply the same guard, excluding the target itself so a
  case-only rename still no-ops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(validate): stop reporting an unreadable specs dir as 'no deltas'

The delta-validation loop swallowed every error as 'if no specs dir,
treat as no deltas', so an EACCES capability folder produced the
misleading 'Change must have at least one delta' while archive let the
same error propagate. Tolerate only ENOENT and ENOTDIR (a stray specs
file); anything else stays loud, matching discoverSpecFiles' documented
fail-loud contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): say when commands-only delivery leaves a tool with nothing

Under delivery: commands, update removed the skills of adapterless
skills-only tools (Hermes, Kimi Code, Vibe, CodeArts, ForgeCode) without
a word — leaving zero OpenSpec artifacts while the tool's detection dir
kept re-suggesting an init that would also generate nothing. Print the
same per-tool configuration correction init already prints, pointing at
'openspec config set delivery both'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): honor $ZSH and $ZSH_CUSTOM for Oh My Zsh installs

The installer used a set $ZSH only as an is-installed signal and then
wrote to ~/.oh-my-zsh regardless, so a custom OMZ location got a
freshly created ~/.oh-my-zsh tree that no shell ever loads — and
isInstalled/uninstall looked in the same wrong place. Route every path
through the $ZSH/$ZSH_CUSTOM-aware helpers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(init): make the static welcome screen wait for the Enter it asks for

The static branch printed 'Press Enter to select tools...' and returned
immediately, so the Enter landed in the tool picker and submitted the
pre-selected set sight-unseen. Fission-AI#1462 routed reduced-motion,
OPENSPEC_NO_ANIMATION, --no-animation, NO_COLOR, and narrow-terminal
users onto this path. Wait in a TTY; drop the prompt line when there is
no TTY to wait on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(feedback): keep the manual fallback on every gh failure

Only missing-gh and unauthenticated flows showed the formatted feedback
and pre-filled submission URL; issues-disabled, network, or rate-limit
failures printed gh's stderr and discarded the path to submit what the
user had already typed. Route those through the same manual fallback,
preserving gh's exit code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(update): model the npm shim in the Windows prefix fixture

The ownership corroboration now checks for the openspec.cmd shim npm
writes beside node_modules; the Homebrew-prefix fixture built the layout
without it, so the test failed on windows-pwsh. Write the shim in the
fixture and pin the inverse: the same shape with nothing npm wrote (a
hand-copied portable tree) is not an npm install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): require volta's full tools/image layout for the undotted spelling

The corroboration used has('tools', 'image'), which is some() — volta
AND (tools OR image) — so /srv/volta/tools/apps/... still classified as
a Volta install and swallowed the upgrade offer. Require both segments,
matching the real %LOCALAPPDATA%\Volta\tools\image layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): escape control characters in Gemini multiline prompts

escapeTomlMultilineBasicString handled backslashes and quote-triples but
not the C0 controls that are as invalid in a multiline basic string as
in a single-line one. Reuse TOML_CONTROL_CHARS, applied last so the
escapes it introduces are not re-doubled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): finish the $ZSH_CUSTOM support and isolate it in tests

The fpath verification advice still grepped the literal
custom/completions, which a relocated $ZSH_CUSTOM need never contain —
grep the actual directory instead. The installer tests cleared only
$ZSH, so on a machine exporting $ZSH_CUSTOM they would have written
into (and deleted from) the developer's real OMZ custom dir — the same
leakage class Fission-AI#1400 fixed for $ZSH. Clear/restore both, and pin the
custom-location paths with two new tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(release): correct the hermes and zcode changeset wording

Hermes is skills-only (no command adapter), and zcode's namespaced
commands register /opsx:<id>, not /opsx-* — the release notes must not
reintroduce the invocation-spelling confusion Fission-AI#1471 removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(feedback): pin the manual fallback on a non-label gh failure

The new reportGhFailure output (formatted feedback + pre-filled URL) had
no coverage; the network-failure test now asserts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): match fpath entries as literal strings in the OMZ guidance

The verification advice interpolated the completions dir into
grep "<dir>" where regex metacharacters make the check unreliable and
quotes could break the displayed command. Print one fpath entry per
line and match with grep -F on a shell-quoted literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): never emit a bare carriage return in Gemini TOML prompts

A lone CR is illegal in a multiline basic string — Python 3.13 tomllib
rejects the file — and the control-char pass deliberately skipped it on
the assumption it only appears as CRLF. Normalize CRLF to LF and escape
any remaining CR as \r. The escaping guarantee is now parser-backed:
smol-toml (new devDependency) round-trips every hostile body in the
regression matrix (lone CR, CRLF, CR before a quote run, NUL/VT/FF,
trailing backslash, four- and five-quote runs), and the same outputs
were verified against Python tomllib.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build(nix): update the pnpm deps hash for the smol-toml devDependency

The lockfile changed, so the fixed-output derivation hash moved; value
taken from the CI mismatch report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

prefers-reduced-motion

2 participants