Skip to content

Core: Show "new" status on newly added individual stories#34504

Open
ghengeveld wants to merge 5 commits intonextfrom
story-new-status
Open

Core: Show "new" status on newly added individual stories#34504
ghengeveld wants to merge 5 commits intonextfrom
story-new-status

Conversation

@ghengeveld
Copy link
Copy Markdown
Member

@ghengeveld ghengeveld commented Apr 8, 2026

What I did

Enhanced the change detection mechanism with story index change detection, to mark newly added stories with status-value:new. To determine whether new stories were added, an initial snapshot of the story index is taken at startup, and used as a baseline for future comparisons. If the Git repository is clean at that time, the snapshot is persisted on disk and reused across runs, otherwise the baseline is reset whenever the Storybook server restarts.

Screenshot 2026-04-08 at 23 14 14

Note: this PR also incorporates #34429 to hide the "modified" icons on sibling stories.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced baseline entry tracking for the story index to improve change detection accuracy
  • Improvements

    • Enhanced module graph change detection to work reliably when listeners register after builder starts
    • Improved sidebar status display with clearer visual mapping and human-readable labels
    • Added git state detection capabilities (working tree status, HEAD commit information)
    • Optimized change detection startup timing to begin after initial story rendering or documentation preparation

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a baseline-index tracking service for change detection, refactors module-graph subscription startup in builder-vite to support post-start listener registration, extends git command capabilities, enhances status merging logic in change detection, gates change-detection startup to event-driven initialization, and updates sidebar status display mapping.

Changes

Cohort / File(s) Summary
Builder-Vite Module-Graph Subscription
code/builders/builder-vite/src/index.ts, code/builders/builder-vite/src/index.test.ts
Replaced moduleGraphRegistrationClosed flag with moduleGraphTrackingStarted. Added startModuleGraphTracking() to lazily initialize module-graph change detection when listeners are present post-start. Removed ViteModuleGraphSubscriptionError throw behavior and moved watcher setup into the new tracking method. Updated test to verify listener registration after builder startup succeeds with proper callback invocation.
Change Detection Service Integration
code/core/src/core-server/change-detection/ChangeDetectionService.ts, code/core/src/core-server/change-detection/ChangeDetectionService.test.ts
Added IndexBaselineService integration with baseline entry ID fetching on git state changes. Introduced exported helpers mergeChangeDetectionStatuses(), buildIndexBaselineStatuses(), and updated mergeStatusValues() to use StatusValue type. Enhanced status merging to accumulate changedFiles arrays and seed/merge baseline statuses. Added onStoryIndexInvalidated() callback for debounced baseline scans. Extended test coverage with typed mock IndexBaselineService and tests for status-merging utilities.
Git Command Execution
code/core/src/core-server/change-detection/GitDiffProvider.ts, code/core/src/core-server/change-detection/GitDiffProvider.test.ts
Refactored git command execution by centralizing it in private runGitCommand() method. Added public methods getHeadCommit() (returns HEAD commit SHA) and isWorkingTreeClean() (checks git status). Updated tests to cover new git command methods with mocked execa results.
Index Baseline Service
code/core/src/core-server/change-detection/IndexBaselineService.ts, code/core/src/core-server/change-detection/IndexBaselineService.test.ts
New service managing baseline story index entry IDs with git state synchronization and filesystem caching. Exports extractBaselineEntryIds() helper. Implements lazy start(), getBaselineEntryIds(), and handleGitStateChange() to regenerate and cache baseline snapshots keyed by HEAD commit. Comprehensive test suite validates cache hit/miss behavior, git state transitions, and entry ID extraction for story/docs entries.
Dev-Server Integration
code/core/src/core-server/dev-server.ts
Introduced event-driven change-detection startup: defers changeDetectionService.start(..., true) until first STORY_RENDERED or DOCS_PREPARED event when features?.changeDetection !== false. Wired onStoryIndexInvalidated callback from change-detection service into registerIndexJsonRoute. Added error handling with setChangeDetectionReadiness and guard against repeated starts.
Index JSON Route Callback
code/core/src/core-server/utils/index-json.ts, code/core/src/core-server/utils/index-json.test.ts
Added optional onStoryIndexInvalidated?: () => void callback parameter to registerIndexJsonRoute. Debounced invalidation now invokes this callback after emitting STORY_INDEX_INVALIDATED event. Updated test to verify callback is invoked on invalidation.
Sidebar Status Display
code/core/src/manager/components/sidebar/Tree.tsx
Added getDisplayStatus() function to remap story/docs statuses (modified, affected) to unknown and derive human-readable labels. Refactored status rendering to use mapped display status for StatusButton and ariaLabel. Reordered context menu computation before early-return check. Updated branch status visibility and icon selection to use displayed status.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch story-new-status

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

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (4)
code/core/src/manager/components/sidebar/Tree.tsx (1)

216-222: Consider exporting getDisplayStatus for direct testability.

This utility function contains conditional logic that could benefit from unit testing. As per coding guidelines, functions that need direct tests should be exported.

♻️ Suggested change
-const getDisplayStatus = (
+export const getDisplayStatus = (
   itemType: Item['type'],
   status: StatusValue
 ): { status: StatusValue; label: string } =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager/components/sidebar/Tree.tsx` around lines 216 - 222,
Export the getDisplayStatus utility so it can be imported in unit tests: change
its declaration to a named export (e.g., export const getDisplayStatus = ... or
export function getDisplayStatus(...)) and update any internal references or
imports that rely on the previous non-exported symbol (ensure consuming
components still import it by name). Keep the implementation unchanged so tests
can directly import getDisplayStatus to assert the conditional mapping for
story/docs hidden statuses and the default label formatting.
code/core/src/core-server/change-detection/GitDiffProvider.test.ts (1)

148-168: Consider simplifying the nested ternary dispatch.

The mock dispatch table is becoming deeply nested (7 levels). While functional, a Map or object lookup would improve readability.

♻️ Optional refactor using a lookup object
-    vi.mocked(execa).mockImplementation(((_command: string | URL, ...rest: unknown[]) => {
-      const args = Array.isArray(rest[0]) ? rest[0] : [];
-      const gitArgs = args.join(' ');
-      const result =
-        gitArgs === 'rev-parse --show-toplevel'
-          ? repoRootResult
-          : gitArgs === 'diff --name-only --diff-filter=ad --cached'
-            ? stagedResult
-            : gitArgs === 'diff --name-only --diff-filter=ad'
-              ? unstagedResult
-              : gitArgs === 'ls-files --others --exclude-standard'
-                ? untrackedResult
-                : gitArgs === 'diff --name-only --diff-filter=A --cached'
-                  ? stagedAddedResult
-                  : gitArgs === 'diff --name-only --diff-filter=A'
-                    ? intentToAddResult
-                    : gitArgs === 'rev-parse HEAD'
-                      ? headCommitResult
-                      : gitArgs === 'status --porcelain'
-                        ? statusPorcelainResult
-                        : undefined;
+    vi.mocked(execa).mockImplementation(((_command: string | URL, ...rest: unknown[]) => {
+      const args = Array.isArray(rest[0]) ? rest[0] : [];
+      const gitArgs = args.join(' ');
+      const resultMap: Record<string, ExecaMockResult | undefined> = {
+        'rev-parse --show-toplevel': repoRootResult,
+        'diff --name-only --diff-filter=ad --cached': stagedResult,
+        'diff --name-only --diff-filter=ad': unstagedResult,
+        'ls-files --others --exclude-standard': untrackedResult,
+        'diff --name-only --diff-filter=A --cached': stagedAddedResult,
+        'diff --name-only --diff-filter=A': intentToAddResult,
+        'rev-parse HEAD': headCommitResult,
+        'status --porcelain': statusPorcelainResult,
+      };
+      const result = resultMap[gitArgs];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/change-detection/GitDiffProvider.test.ts` around
lines 148 - 168, The nested ternary inside the
vi.mocked(execa).mockImplementation block (which inspects gitArgs and returns
repoRootResult, stagedResult, unstagedResult, untrackedResult,
stagedAddedResult, intentToAddResult, headCommitResult, statusPorcelainResult)
should be replaced with a clear lookup table: build a Map or plain object keyed
by the gitArgs strings and return map.get(gitArgs) (or map[gitArgs]) instead of
the long nested ternary; ensure you keep the existing handling for
Array.isArray(rest[0]) to derive args and preserve returning undefined for
unmatched keys.
code/core/src/core-server/change-detection/ChangeDetectionService.ts (2)

391-403: Minor redundancy in status value merging.

The value is merged twice: first explicitly on line 394 via mergeStatusValues(existingStatus?.value, value), then again inside mergeChangeDetectionStatuses on line 403. Since mergeStatusValues is idempotent for its priority logic, this works correctly but is slightly redundant.

Consider removing the explicit merge on line 394 and letting mergeChangeDetectionStatuses handle it:

Proposed simplification
          const nextStatus: Status = {
            storyId,
            typeId: CHANGE_DETECTION_STATUS_TYPE_ID,
-           value: mergeStatusValues(existingStatus?.value, value),
+           value,
            title: '',
            description: '',
            data: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts` around
lines 391 - 403, The code builds nextStatus with value already merged via
mergeStatusValues(existingStatus?.value, value) and then calls
statuses.set(storyId, mergeChangeDetectionStatuses(existingStatus, nextStatus))
which re-merges the values; remove the redundant merge by assigning
nextStatus.value = value (the incoming value) and let
mergeChangeDetectionStatuses perform the merge using existingStatus, ensuring
you update the construction of nextStatus (the const nextStatus object) and
leave the call to mergeChangeDetectionStatuses(existingStatus, nextStatus)
unchanged.

236-238: Consider logging suppressed errors for debugging.

The .catch(() => undefined) silently discards any errors from handleGitStateChange(). While this prevents unhandled rejections, it may hide issues during debugging. Consider logging at debug level.

Proposed enhancement
       this.scheduleScan(this.debounceMs);
       void this.getIndexBaselineService()
         .handleGitStateChange()
-        .catch(() => undefined);
+        .catch((error) => {
+          logger.debug('IndexBaselineService.handleGitStateChange failed: %s', error);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts` around
lines 236 - 238, The current call to
this.getIndexBaselineService().handleGitStateChange().catch(() => undefined)
swallows errors; update the catch to log the error at debug level instead of
discarding it so issues are visible during debugging. Locate the call in
ChangeDetectionService (the getIndexBaselineService().handleGitStateChange
invocation) and replace the empty catch with one that logs the caught error via
the class logger (e.g., this.logger.debug or the existing logger instance)
including context like "handleGitStateChange error" and the error object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/core-server/change-detection/ChangeDetectionService.test.ts`:
- Around line 137-139: The test mock incorrectly overrides getHeadSha() while
the GitDiffProvider interface defines getHeadCommit(); update the mock to
implement async getHeadCommit(): Promise<string> { return this.getHeadShaMock();
} (or rename getHeadShaMock to getHeadCommitMock and wire it into getHeadCommit)
so the mock class implements GitDiffProvider correctly and the override and
helper names (getHeadShaMock / getHeadSha) are aligned with getHeadCommit.

In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts`:
- Around line 180-186: The constructor parameter is named with PascalCase
("IndexBaselineService") instead of camelCase; rename the parameter to
indexBaselineService in the ChangeDetectionService constructor signature and
update the assignment inside the constructor (currently
this.indexBaselineService = options.IndexBaselineService) to use the new
camelCase name (this.indexBaselineService = options.indexBaselineService) so
parameter naming follows JS/TS conventions and matches the existing property.

---

Nitpick comments:
In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts`:
- Around line 391-403: The code builds nextStatus with value already merged via
mergeStatusValues(existingStatus?.value, value) and then calls
statuses.set(storyId, mergeChangeDetectionStatuses(existingStatus, nextStatus))
which re-merges the values; remove the redundant merge by assigning
nextStatus.value = value (the incoming value) and let
mergeChangeDetectionStatuses perform the merge using existingStatus, ensuring
you update the construction of nextStatus (the const nextStatus object) and
leave the call to mergeChangeDetectionStatuses(existingStatus, nextStatus)
unchanged.
- Around line 236-238: The current call to
this.getIndexBaselineService().handleGitStateChange().catch(() => undefined)
swallows errors; update the catch to log the error at debug level instead of
discarding it so issues are visible during debugging. Locate the call in
ChangeDetectionService (the getIndexBaselineService().handleGitStateChange
invocation) and replace the empty catch with one that logs the caught error via
the class logger (e.g., this.logger.debug or the existing logger instance)
including context like "handleGitStateChange error" and the error object.

In `@code/core/src/core-server/change-detection/GitDiffProvider.test.ts`:
- Around line 148-168: The nested ternary inside the
vi.mocked(execa).mockImplementation block (which inspects gitArgs and returns
repoRootResult, stagedResult, unstagedResult, untrackedResult,
stagedAddedResult, intentToAddResult, headCommitResult, statusPorcelainResult)
should be replaced with a clear lookup table: build a Map or plain object keyed
by the gitArgs strings and return map.get(gitArgs) (or map[gitArgs]) instead of
the long nested ternary; ensure you keep the existing handling for
Array.isArray(rest[0]) to derive args and preserve returning undefined for
unmatched keys.

In `@code/core/src/manager/components/sidebar/Tree.tsx`:
- Around line 216-222: Export the getDisplayStatus utility so it can be imported
in unit tests: change its declaration to a named export (e.g., export const
getDisplayStatus = ... or export function getDisplayStatus(...)) and update any
internal references or imports that rely on the previous non-exported symbol
(ensure consuming components still import it by name). Keep the implementation
unchanged so tests can directly import getDisplayStatus to assert the
conditional mapping for story/docs hidden statuses and the default label
formatting.
🪄 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: 84a7a20e-82af-49b7-8c20-da8bbd1c7b89

📥 Commits

Reviewing files that changed from the base of the PR and between 7022915 and 2295b1f.

📒 Files selected for processing (12)
  • code/builders/builder-vite/src/index.test.ts
  • code/builders/builder-vite/src/index.ts
  • code/core/src/core-server/change-detection/ChangeDetectionService.test.ts
  • code/core/src/core-server/change-detection/ChangeDetectionService.ts
  • code/core/src/core-server/change-detection/GitDiffProvider.test.ts
  • code/core/src/core-server/change-detection/GitDiffProvider.ts
  • code/core/src/core-server/change-detection/IndexBaselineService.test.ts
  • code/core/src/core-server/change-detection/IndexBaselineService.ts
  • code/core/src/core-server/dev-server.ts
  • code/core/src/core-server/utils/index-json.test.ts
  • code/core/src/core-server/utils/index-json.ts
  • code/core/src/manager/components/sidebar/Tree.tsx

Comment on lines +137 to +139
override getHeadSha(): Promise<string> {
return this.getHeadShaMock();
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the method name in GitDiffProvider.ts
rg -n 'getHead(Commit|Sha)' --type=ts code/core/src/core-server/change-detection/

Repository: storybookjs/storybook

Length of output: 1358


🏁 Script executed:

grep -n "getHeadCommit\|gitDiffProvider" code/core/src/core-server/change-detection/ChangeDetectionService.ts | head -20

Repository: storybookjs/storybook

Length of output: 532


🏁 Script executed:

grep -n "getHeadCommit" code/core/src/core-server/change-detection/ChangeDetectionService.ts

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

grep -n "getChangedFiles\|getRepoRoot\|getHeadCommit\|getHeadSha" code/core/src/core-server/change-detection/ChangeDetectionService.test.ts | head -30

Repository: storybookjs/storybook

Length of output: 1473


🏁 Script executed:

grep -n "getHeadShaMock\|getHeadSha()" code/core/src/core-server/change-detection/ChangeDetectionService.test.ts

Repository: storybookjs/storybook

Length of output: 229


🏁 Script executed:

grep -n "\.getHeadSha\|\.getHeadCommit" code/core/src/core-server/change-detection/ChangeDetectionService.test.ts

Repository: storybookjs/storybook

Length of output: 102


🏁 Script executed:

grep -n "getHeadSha\|getHeadCommit" code/core/src/core-server/change-detection/GitDiffProvider.ts

Repository: storybookjs/storybook

Length of output: 110


Method name mismatch: getHeadSha vs getHeadCommit.

The mock method is named getHeadShaMock and override getHeadSha(), but GitDiffProvider.ts defines async getHeadCommit(): Promise<string>. This causes the mock to not properly implement the GitDiffProvider interface. While the method is not currently called in this test suite, the incorrect override violates the interface contract and should be corrected for consistency.

🐛 Proposed fix to align method names
-  readonly getHeadShaMock = vi.fn(async (): Promise<string> => 'mock-sha');
+  readonly getHeadCommitMock = vi.fn(async (): Promise<string> => 'mock-sha');

-  override getHeadSha(): Promise<string> {
-    return this.getHeadShaMock();
+  override getHeadCommit(): Promise<string> {
+    return this.getHeadCommitMock();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/change-detection/ChangeDetectionService.test.ts`
around lines 137 - 139, The test mock incorrectly overrides getHeadSha() while
the GitDiffProvider interface defines getHeadCommit(); update the mock to
implement async getHeadCommit(): Promise<string> { return this.getHeadShaMock();
} (or rename getHeadShaMock to getHeadCommitMock and wire it into getHeadCommit)
so the mock class implements GitDiffProvider correctly and the override and
helper names (getHeadShaMock / getHeadSha) are aligned with getHeadCommit.

Comment on lines +180 to +186
IndexBaselineService?: IndexBaselineService;
workingDir?: string;
debounceMs?: number;
}
) {
this.gitDiffProvider = options.gitDiffProvider;
this.indexBaselineService = options.IndexBaselineService;
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.

⚠️ Potential issue | 🟡 Minor

Use camelCase for parameter names.

The parameter IndexBaselineService uses PascalCase, which is typically reserved for types and classes. Parameter names should use camelCase for consistency with JavaScript/TypeScript conventions and the adjacent gitDiffProvider parameter.

Proposed fix
  constructor(
    private readonly options: {
      storyIndexGeneratorPromise: Promise<StoryIndexGenerator>;
      statusStore: StatusStoreByTypeId;
      gitDiffProvider?: GitDiffProvider;
-     IndexBaselineService?: IndexBaselineService;
+     indexBaselineService?: IndexBaselineService;
      workingDir?: string;
      debounceMs?: number;
    }
  ) {
    this.gitDiffProvider = options.gitDiffProvider;
-   this.indexBaselineService = options.IndexBaselineService;
+   this.indexBaselineService = options.indexBaselineService;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
IndexBaselineService?: IndexBaselineService;
workingDir?: string;
debounceMs?: number;
}
) {
this.gitDiffProvider = options.gitDiffProvider;
this.indexBaselineService = options.IndexBaselineService;
indexBaselineService?: IndexBaselineService;
workingDir?: string;
debounceMs?: number;
}
) {
this.gitDiffProvider = options.gitDiffProvider;
this.indexBaselineService = options.indexBaselineService;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts` around
lines 180 - 186, The constructor parameter is named with PascalCase
("IndexBaselineService") instead of camelCase; rename the parameter to
indexBaselineService in the ChangeDetectionService constructor signature and
update the assignment inside the constructor (currently
this.indexBaselineService = options.IndexBaselineService) to use the new
camelCase name (this.indexBaselineService = options.indexBaselineService) so
parameter naming follows JS/TS conventions and matches the existing property.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant