Skip to content

perf(packages/core,packages/cli): incremental sync engine#78

Merged
zrosenbauer merged 29 commits into
mainfrom
perf/incremental-sync-engine
Apr 8, 2026
Merged

perf(packages/core,packages/cli): incremental sync engine#78
zrosenbauer merged 29 commits into
mainfrom
perf/incremental-sync-engine

Conversation

@zrosenbauer

Copy link
Copy Markdown
Member

Summary

  • Incremental sync engine: mtime-based page skip, parallel page/asset copy, OpenAPI spec caching, structural change detection — only processes what changed instead of full sync each pass
  • CLI build system migration: switched from rslib to kidd (tsdown-based) with React/Ink TUI dev screen
  • Dev TUI fixes: replace broken ASCII banner with ink-big-text + ink-gradient, fix "q" hotkey not exiting
  • Sidebar build fix: pre-create directories referenced in root _meta.json to prevent Rspress ENOENT crash on empty sections

Changes

  • @zpress/core: incremental sync engine (minor), sidebar dir creation fix (patch)
  • @zpress/cli: kidd build migration (minor), dev TUI banner + quit fix (patch)

Test plan

  • Run zpress dev and verify styled banner renders correctly
  • Press q to confirm the dev server exits cleanly
  • Run zpress build on a project with empty section directories
  • Verify incremental sync skips unchanged files on subsequent passes
  • Confirm pnpm check passes

zrosenbauer and others added 25 commits March 31, 2026 13:08
Skip unchanged pages via mtime + frontmatter hash, parallelize page copy
and public asset copy, cache OpenAPI spec parses across dev-mode syncs,
skip asset generation when config hash is unchanged, and skip image
re-copy when destination is current.

Co-Authored-By: Claude <noreply@anthropic.com>
Document the sync pipeline, page transformation, build/dev flows,
watcher triggers, and incremental sync optimizations.

Co-Authored-By: Claude <noreply@anthropic.com>
Break `architecture.md` into three files:
- `architecture.md` — package ecosystem, layers, design decisions
- `sync-engine.md` — pipeline, page transformation, entry resolution,
  incremental sync, OpenAPI sync
- `config.md` — config system, output structure, Rspress integration

Update `cli.md` with corrected watcher details (fs.watch, not chokidar)
and dev lifecycle reflecting OpenAPI cache and Rspress restart behavior.

Revert CONTRIBUTING.md to its original state (architecture docs belong
in contributing/, not the root file).

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the imperative dev command handler with an interactive
React/Ink screen component that renders live watcher status,
sync results, and keyboard shortcuts (r/c/o/q).

- Add DevScreen component with phase-based rendering
- Extract WatcherStatus, WatcherCallbacks, WatcherHandle types
- Return resolved port from startDevServer for TUI display
- Decouple watcher from Log interface via callbacks
- Add resync() to WatcherHandle for hotkey-triggered re-sync
- Cancel pending debounces on watcher close
- Guard all async state updates against unmount
- Wrap sync/server init in try/catch for proper error display

Co-Authored-By: Claude <noreply@anthropic.com>
- Replace Object.assign with reduce for type-safe specMtimes merge
- Add language specifiers to bare code fences in docs

Co-Authored-By: Claude <noreply@anthropic.com>
- Use function declaration instead of arrow for guard helper (func-style)
- Use Object.assign instead of spread in reduce accumulator (perf)

Co-Authored-By: Claude <noreply@anthropic.com>
The contributing docs had broken links to engine/*.md and
references/*.md because those directories were not included
in the zpress config glob patterns. This caused the Vercel
preview build to fail on link validation.

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix config.md: defineConfig() is pass-through, validation in loadConfig()
- Fix dev.md: add `text` language identifier to lifecycle code fence
- Fix openapi.md: tighten cache/config wording in steps 1-2
- Fix pipeline.md: correct .generated/ paths to .zpress/content/.generated/
- Fix pipeline.md: correct public/ copy description
- Fix pipeline.md: use `title` instead of `text` in explicit items example

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix overview.md: tighten "get their own namespace" wording
- Fix dev-screen.tsx: replace `let cancelled` with useRef-backed flag
- Fix dev-screen.tsx: honor `quiet` prop instead of hard-coding true
- Fix openapi.ts: use immutable spread instead of Object.assign mutation
- Fix openapi.ts: evict stale cache entry on dereference failure

Co-Authored-By: Claude <noreply@anthropic.com>
…isable

The oxlint `no-accumulating-spread` rule flags spreading in reduce
for O(n^2) performance. Object.assign mutating the accumulator is the
correct pattern here — restored with a targeted lint disable comment.

Co-Authored-By: Claude <noreply@anthropic.com>
The dev-screen.tsx component uses JSX but the Rslib bundler was
defaulting to the classic React.createElement transform (ignoring
tsconfig's jsx: react-jsx). Configure SWC's react transform to use
the automatic runtime so jsx/jsxs are imported from react/jsx-runtime.

Co-Authored-By: Claude <noreply@anthropic.com>
Rspack's file-based storage layer holds a transaction lock on .temp/
inside the cache directory. Rsbuild's close() resolves before that
lock is fully released, causing the new dev() call to panic with
"Transaction already in progress". Add a 500ms settle window after
close to let the lock release before starting the new instance.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace themeConfig.sidebar/nav with per-directory _meta.json and
root _nav.json files, enabling Rspress's built-in HMR for sidebar
and navigation changes without dev server restarts.

- Add filesystem-first _meta.json generation (meta.ts) that derives
  placement from actual file paths rather than config tree position
- Add write-meta.ts to write _meta.json per directory and _nav.json
- Remove old multi.ts sidebar builder and its tests
- Strip sidebar/nav from themeConfig in UI config
- Gate dev server restarts to config changes that actually require
  a rebuild (title, theme, colors, etc.) via restartRelevantHash
- Suppress Rspress dev output (logLevel: silent, progressBar: false)
- Keep sidebar.json/nav.json as .generated/ snapshots for debugging

Co-Authored-By: Claude <noreply@anthropic.com>
Rspress creates per-directory sidebar scopes by default when only
_nav.json is present. Adding a root _meta.json that lists all
top-level sections as dir items creates a single unified sidebar
keyed by "/" — matching the expected behavior.

Also simplifies the sidebar.json snapshot to a single "/" key
since standalone sections now share the unified sidebar.

Co-Authored-By: Claude <noreply@anthropic.com>
… sections

Rspress's _meta.json system only supports unified or per-directory sidebars,
not both. This adds a custom Sidebar component that filters the unified "/"
sidebar at runtime to isolate standalone sections (Packages, Contributing)
into their own scope while keeping the main 5 sections as a shared sidebar.

- Sync engine writes .generated/scopes.json with standalone section paths
- Config loads scopes and passes via themeConfig.standaloneScopePaths
- Custom Sidebar component filters unified sidebar data by current route
- Reimplements Rspress's createInitialSidebar collapsed state logic
- HMR preserved: _meta.json auto-discovery still runs with addDependency()

Co-Authored-By: Claude <noreply@anthropic.com>
Uses the kidd-cli `fullscreen: true` screen option to render the dev
TUI in the terminal's alternate screen buffer, preserving scrollback.

Co-Authored-By: Claude <noreply@anthropic.com>
- Upgrade @kidd-cli/core from 0.13.0 to 0.20.0
- Replace @inkjs/ui Spinner with kidd's Spinner component
- Use Alert, StatusMessage, useHotkey, useFullScreen from new API
- Migrate cli() commands config to v0.20.0 shape (order → help.order)
- Add dev-screen.stories.tsx with 7 story variants for visual testing

Co-Authored-By: Claude <noreply@anthropic.com>
- Register `zpress stories` command using kidd StoriesScreen
- Rename dev-screen.stories.tsx → dev.stories.tsx
- Add `pnpm stories` script for convenience

Co-Authored-By: Claude <noreply@anthropic.com>
Prevents kidd autoload from picking up story files as commands.

Co-Authored-By: Claude <noreply@anthropic.com>
…dd stories

- Switch all command files to `export default command()`/`screen()`
- Use default imports in index.ts for cleaner command map
- Remove `zpress stories` CLI command (stories are a dev tool, not user-facing)
- Add `pnpm stories` script via `kidd stories` from @kidd-cli/cli
- Add @kidd-cli/cli as devDependency for story viewer
- Remove unused @inkjs/ui dependency

Co-Authored-By: Claude <noreply@anthropic.com>
kidd stories requires a kidd.config.ts to recognize the project.
Move dev.stories.tsx back to src/commands/ where the default
src/**/*.stories.tsx glob can find it.

Co-Authored-By: Claude <noreply@anthropic.com>
Switch CLI bundling from rslib to kidd's native tsdown-based build
system. Move non-command files out of the commands directory to avoid
kidd's autoload scanner picking them up.

- Replace rslib.config.ts with kidd.config.ts entry/commands config
- Use static command imports in index.ts (bypass autoloading)
- Move dev-screen.tsx to src/screens/ (not a command)
- Move dev.stories.tsx to src/stories/ (not a command)
- Update bin/exports to index.mjs output format
- Split dependencies for proper tsdown externalization
- Upgrade @kidd-cli/core to ^0.22.1, @kidd-cli/cli to ^0.11.2

Co-Authored-By: Claude <noreply@anthropic.com>
- Replace ternary expressions with ts-pattern match in LogLine components
- Destructure props.width in DevScreenPreview story
- Rename clean default import to cleanCmd to avoid named export shadow
- Add root `pnpm stories` script proxying to @zpress/cli

Co-Authored-By: Claude <noreply@anthropic.com>
… sidebar build crash

- Replace broken ASCII banner with ink-big-text + ink-gradient
- Fix "q" not exiting by calling process.exit(0) after Ink exit()
- Pre-create directories referenced in root _meta.json to prevent
  Rspress ENOENT crash on empty sections

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

vercel Bot commented Apr 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
oss-zpress Ready Ready Preview, Comment Apr 8, 2026 3:52pm

Request Review

@changeset-bot

changeset-bot Bot commented Apr 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 47e5c2a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@zpress/cli Minor
@zpress/core Minor
@zpress/kit Patch
@zpress/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb38bb5a-7cd4-41f5-89fe-ab9cc9afc3e7

📥 Commits

Reviewing files that changed from the base of the PR and between fa04311 and 47e5c2a.

📒 Files selected for processing (1)
  • packages/cli/src/screens/dev-screen.tsx

📝 Walkthrough

Walkthrough

This PR updates the CLI dev UI and watcher, and enhances the core sync engine and metadata handling. It replaces the ASCII banner with a styled Banner component, ensures quit hotkey fully exits the dev process, converts the watcher to a callback-driven API with conditional restart logic, implements incremental sync (mtime/frontmatter/hash checks, concurrent copy, OpenAPI mtime caching), adds writeMetaFiles to pre-create directories and emit _meta.json/_nav.json, and includes small documentation and tooling adjustments.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: an incremental sync engine optimization for core and CLI packages, which is the main focus of the pull request.
Description check ✅ Passed The description clearly relates to the changeset, covering the major updates (incremental sync engine, CLI build migration, dev TUI fixes, and sidebar directory creation) and providing a concrete test plan.
Docstring Coverage ✅ Passed Docstring coverage is 96.67% which is sufficient. The required threshold is 80.00%.

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


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

@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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/lib/watcher.ts (1)

139-143: 🧹 Nitpick | 🔵 Trivial

Awkward reduce pattern used to avoid expression statements.

The reduce returning null is a workaround to satisfy the "no expression statements" rule. While technically compliant, this pattern is harder to read than a simple loop or forEach. Consider using a more idiomatic approach if the linting rules allow.

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

In `@packages/cli/src/lib/watcher.ts` around lines 139 - 143, The callback passed
to watch (used to create the watcher constant) uses an awkward
Array.prototype.reduce returning null to avoid expression statements; replace
that reduce with a clearer imperative loop (e.g., forEach or for...of) inside
the (_event, filename) => { ... } handler so you can perform side-effecting
operations directly and remove the null return workaround, leaving the watcher,
watch, and repoRoot usages intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contributing/concepts/engine/incremental.md`:
- Line 81: Replace the British English "afterwards" with American English
"afterward" in the sentence "Empty parent directories are pruned afterwards."
within the incremental.md content so the line reads "Empty parent directories
are pruned afterward." to maintain consistent American spelling.

In `@contributing/concepts/engine/openapi.md`:
- Line 62: Replace the double hyphen in the link text "Dev Mode -- OpenAPI
Cache" with an em dash for typographic consistency; update the markdown so the
link reads "Dev Mode — OpenAPI Cache" (keep the URL ./dev.md#openapi-cache and
surrounding sentence unchanged) to improve readability.

In `@contributing/concepts/engine/overview.md`:
- Around line 27-30: Update the OpenAPI input node label to reflect that specs
are sourced from configuration rather than a fixed filename: change the SPECS
node's label (currently "openapi.yaml") to something like "config.openapi.spec"
or "OpenAPI spec files (from zpress.config.ts)" and mention/respect the single
source of truth symbol CONFIG (zpress.config.ts) and the runtime boundary
loadConfig(); ensure any text or diagram note references that specs come from
the zpress.config.ts/workspace openapi config and that outputs assume the
.zpress/content/ root and .zpress/content/.generated/ for generated metadata.

In `@packages/cli/kidd.config.ts`:
- Around line 3-6: Summary: enable TypeScript declaration emission by adding
dts: true to the Kidd config. Fix: update the exported config object passed to
defineConfig in kidd.config.ts to include dts: true alongside the existing
commands and entry keys (e.g., export default defineConfig({ commands:
'./src/commands', entry: './src/index.ts', dts: true })), so tsdown will emit
./dist/index.d.ts that matches the package's exports.types entry.

In `@packages/cli/src/lib/watcher.ts`:
- Around line 230-243: The current restartRelevantHash function uses
JSON.stringify on the relevant object which can produce nondeterministic key
ordering and cause spurious hash changes; update restartRelevantHash to produce
a deterministic serialization (e.g., implement a stable stringify that sorts
object keys recursively or use a canonicalization helper) before passing to
createHash('sha256').update(...).digest('hex'), and apply it to the constructed
relevant object (fields: title, description, tagline, icon, theme, sidebar,
socialLinks, footer, home, openapi) so identical content with different
insertion orders yields the same hash.

In `@packages/cli/src/stories/dev.stories.tsx`:
- Around line 43-45: DevScreenPreview accepts an unbounded width so code that
computes width - 2 can produce negative values; guard and normalize the prop at
the top of DevScreenPreview by deriving a safeWidth (e.g., clamp to a minimum of
2 or coerce to a number with a sensible default) and then use safeWidth
everywhere instead of width (this will prevent negative widths passed to Alert
and separators referenced in the function and other places that compute width -
2).

In `@packages/config/schemas/schema.json`:
- Line 3: Regenerate the published schema.json (update the $id) after modifying
the schema so that title under ZpressConfig →
properties.apps.items.properties.title and the analogous locations for packages
and workspaces[].items accept only a plain string (remove or stop referencing
the TitleConfig object-form there); ensure TitleConfig remains reserved for
Section node shapes only and update any $ref entries so those three paths point
to a simple string schema (not the object TitleConfig), then rebuild/export the
schema.json so the new $id matches the regenerated file.

In `@packages/core/src/banner/index.ts`:
- Around line 164-173: Read the file contents into existing as you already do,
but normalize CRLF to LF before doing the marker and equality checks: convert
existing (and newContent for the equality check) to a normalized form (e.g.,
replace "\r\n" with "\n") and then derive firstLine from the normalized string;
then compare firstLine to GENERATED_MARKER and compare the normalized existing
to the normalized newContent. Update the logic around existing, firstLine,
GENERATED_MARKER and newContent to use the normalized strings so Windows CRLF
doesn't break the checks.

In `@packages/core/src/sync/copy.ts`:
- Around line 29-32: The fast-path after calling tryMtimeSkip(page, ctx) should
not return the cached result unless the materialized file actually exists on
disk; update the logic so that after obtaining a non-null cached value you
verify the corresponding materialized file (e.g., the .zpress/content entry or
the page's output path resolved from ctx/page) exists and is readable, and only
then return cached—otherwise proceed with rewrite. Apply the same existence
check to the other fast-path in the same module (the block referenced at lines
~131-163) so both shortcuts validate on-disk presence before skipping
regeneration.

In `@packages/core/src/sync/openapi.ts`:
- Around line 60-64: The reducer for specMtimes mutates the accumulator via
Object.assign(acc, result.specMtimes); change reduce on configResults to collect
entries immutably (e.g., accumulate arrays of [key,value] or use a new object
returned each iteration) and only materialize the final Record<string, number>
once so specMtimes is created without mutating prior state; update the reduce
usage around specMtimes/configResults to return a new object or gather entries
and then build the final specMtimes map after reduction.

---

Outside diff comments:
In `@packages/cli/src/lib/watcher.ts`:
- Around line 139-143: The callback passed to watch (used to create the watcher
constant) uses an awkward Array.prototype.reduce returning null to avoid
expression statements; replace that reduce with a clearer imperative loop (e.g.,
forEach or for...of) inside the (_event, filename) => { ... } handler so you can
perform side-effecting operations directly and remove the null return
workaround, leaving the watcher, watch, and repoRoot usages intact.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: a98b7d92-0963-4ae8-aedb-4bbaac88a941

📥 Commits

Reviewing files that changed from the base of the PR and between 61b7b7d and a7a8647.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (52)
  • .changeset/fix-dev-banner-and-exit.md
  • .changeset/fix-meta-missing-dirs.md
  • .changeset/incremental-sync-engine.md
  • CONTRIBUTING.md
  • contributing/README.md
  • contributing/concepts/architecture.md
  • contributing/concepts/cli.md
  • contributing/concepts/config.md
  • contributing/concepts/engine/dev.md
  • contributing/concepts/engine/incremental.md
  • contributing/concepts/engine/openapi.md
  • contributing/concepts/engine/overview.md
  • contributing/concepts/engine/pipeline.md
  • contributing/guides/getting-started.md
  • contributing/references/cli.md
  • package.json
  • packages/cli/kidd.config.ts
  • packages/cli/package.json
  • packages/cli/rslib.config.ts
  • packages/cli/src/commands/build.ts
  • packages/cli/src/commands/check.ts
  • packages/cli/src/commands/clean.ts
  • packages/cli/src/commands/dev.ts
  • packages/cli/src/commands/diff.ts
  • packages/cli/src/commands/draft.ts
  • packages/cli/src/commands/dump.ts
  • packages/cli/src/commands/serve.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/index.ts
  • packages/cli/src/lib/dev-types.ts
  • packages/cli/src/lib/rspress.ts
  • packages/cli/src/lib/watcher.ts
  • packages/cli/src/screens/dev-screen.tsx
  • packages/cli/src/stories/dev.stories.tsx
  • packages/cli/tsconfig.json
  • packages/config/schemas/schema.json
  • packages/core/src/banner/index.ts
  • packages/core/src/sync/copy.ts
  • packages/core/src/sync/images.ts
  • packages/core/src/sync/index.ts
  • packages/core/src/sync/openapi.ts
  • packages/core/src/sync/sidebar/index.ts
  • packages/core/src/sync/sidebar/meta.ts
  • packages/core/src/sync/sidebar/multi.test.ts
  • packages/core/src/sync/sidebar/multi.ts
  • packages/core/src/sync/sidebar/write-meta.ts
  • packages/core/src/sync/types.ts
  • packages/ui/src/config.ts
  • packages/ui/src/theme/components/sidebar/sidebar-scope.tsx
  • packages/ui/src/theme/hooks/use-zpress.ts
  • packages/ui/src/theme/index.tsx
  • zpress.config.ts
💤 Files with no reviewable changes (4)
  • packages/cli/rslib.config.ts
  • contributing/concepts/cli.md
  • packages/core/src/sync/sidebar/multi.test.ts
  • packages/core/src/sync/sidebar/multi.ts

Comment thread contributing/concepts/engine/incremental.md Outdated
Comment thread contributing/concepts/engine/openapi.md Outdated
Comment thread contributing/concepts/engine/overview.md Outdated
Comment thread packages/cli/kidd.config.ts
Comment thread packages/cli/src/lib/watcher.ts
Comment thread packages/cli/src/stories/dev.stories.tsx
Comment thread packages/config/schemas/schema.json
Comment thread packages/core/src/banner/index.ts
Comment thread packages/core/src/sync/copy.ts
Comment thread packages/core/src/sync/openapi.ts
…home page HMR

- Extract Banner into reusable component at components/banner.tsx
- Switch font from chrome to block with zpress purple gradient
- Add standalone Banner story
- Include actions, features, apps, packages, and workspaces in
  restart-relevance hash so home page updates on config changes

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

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cli/src/lib/watcher.ts (2)

48-58: 🛠️ Refactor suggestion | 🟠 Major

Avoid adding more let-backed watcher state.

The new sync coordinator suppresses the repo’s functional TS rule in five places. Please collapse this into an immutable state transition/helper instead of growing a mutable closure; the current reentrancy path is already hard to audit.

As per coding guidelines "No let — only const. No reassignment, no mutation."

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

In `@packages/cli/src/lib/watcher.ts` around lines 48 - 58, Replace the multiple
let-backed pieces of watcher state (config, syncing, pendingReloadConfig,
consecutiveFailures) with a single immutable watcher state and a pure
reducer/updater function (e.g., transitionWatcherState or watchStateReducer)
that returns new state objects instead of mutating variables; keep
callbacks.onStatusChange usage but call it with the new state's status. Locate
uses of config, syncing, pendingReloadConfig, and consecutiveFailures and adapt
them to read from the single state object and dispatch actions (e.g., { type:
'startSync'|'finishSync'|'fail'|'reloadConfig' }) to the reducer which returns
the updated state, ensuring no let or in-place mutation remains in the closure.

125-133: ⚠️ Potential issue | 🟠 Major

Don't drop non-markdown sync inputs.

After the root zpress.config.* special case, every other change is ignored unless the path ends in .md/.mdx. That skips imported config helpers, OpenAPI specs, public assets, and other non-markdown sync inputs, so dev can keep serving stale output until a manual resync or an unrelated markdown save.

Also applies to: 152-163

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/screens/dev-screen.tsx`:
- Around line 242-243: Clamp the computed terminal width to a safe minimum
before using it: replace the direct const width = Math.min(columns, 80) with a
clamped value (e.g., const width = Math.max(Math.min(columns, 80), 2)) so width
- 2 cannot go negative; then use this clamped width everywhere you derive layout
sizes (the separator render and the error alert code paths that currently rely
on width/columns) to avoid unstable rendering. Make sure any other calculations
in the same file that read columns or recompute sizes (the blocks around the
current width usage) use this clamped variable rather than raw columns.
- Around line 225-239: The input handler in useInput (dev-screen.tsx) returns
early when phase !== 'ready', which prevents the quit keys from working during
loading/error; change the handler so it first checks for quit keys (input ===
'q' or key.ctrl && input === 'c') and performs the watcherRef.current.close(),
exit(), and process.exit(0) path regardless of phase, and only after that apply
the phase !== 'ready' early-return for other keys/behavior; keep references to
watcherRef.current, exit(), and process.exit as used now and do not alter
isActive logic.
- Around line 143-145: The TUI currently only stores and renders the watcher
status enum (collapsing errors to "● Error") instead of the full WatcherStatus
with its message; update the watcher state to store the entire WatcherStatus
object (including message) and change the callbacks to pass the full status
object—specifically modify the WatcherCallbacks usage in the callbacks object
(onStatusChange: guard(setWatcherStatus) and onSyncComplete) so they call
setWatcherStatus with the whole WatcherStatus, and update the render/path that
shows the tag (the component that reads watcher status) to display
status.message when status.type === 'error' (or otherwise show the message)
rather than only the status label. Ensure types (WatcherStatus/WatcherCallbacks)
align with this change.

---

Outside diff comments:
In `@packages/cli/src/lib/watcher.ts`:
- Around line 48-58: Replace the multiple let-backed pieces of watcher state
(config, syncing, pendingReloadConfig, consecutiveFailures) with a single
immutable watcher state and a pure reducer/updater function (e.g.,
transitionWatcherState or watchStateReducer) that returns new state objects
instead of mutating variables; keep callbacks.onStatusChange usage but call it
with the new state's status. Locate uses of config, syncing,
pendingReloadConfig, and consecutiveFailures and adapt them to read from the
single state object and dispatch actions (e.g., { type:
'startSync'|'finishSync'|'fail'|'reloadConfig' }) to the reducer which returns
the updated state, ensuring no let or in-place mutation remains in the closure.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: aaefb94b-f73e-4a65-8456-953f33d0f156

📥 Commits

Reviewing files that changed from the base of the PR and between a7a8647 and 5435d8c.

📒 Files selected for processing (6)
  • .changeset/fix-home-page-hmr.md
  • packages/cli/src/components/banner.tsx
  • packages/cli/src/lib/watcher.ts
  • packages/cli/src/screens/dev-screen.tsx
  • packages/cli/src/stories/banner.stories.tsx
  • packages/cli/src/stories/dev.stories.tsx

Comment thread packages/cli/src/screens/dev-screen.tsx
Comment thread packages/cli/src/screens/dev-screen.tsx
Comment thread packages/cli/src/screens/dev-screen.tsx Outdated
zrosenbauer and others added 2 commits April 7, 2026 15:27
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix "afterwards" → "afterward" (American English consistency)
- Replace double hyphens with em dash in openapi docs
- Rename "openapi.yaml" → "OpenAPI specs" in overview flowchart
- Normalize CRLF line endings in banner shouldGenerate check
- Add output file existence check in mtime skip optimization
- Surface watcher error messages in dev TUI status bar
- Keep quit hotkey active during loading/error phases
- Clamp terminal width to prevent negative separator lengths
- Guard story preview against narrow widths

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

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/screens/dev-screen.tsx`:
- Around line 280-283: The inline ternary in the watcher error label should be
removed; compute the label before JSX or use a match expression instead of `?:`.
For example, derive a string like `const watcherErrorLabel = watcherStatus._tag
=== 'error' ? \`: ${watcherStatus.message}\` : ''` using an if/else or
ts-pattern `match` (reference symbols: watcherStatus and the Text rendering
block that uses `.with('error', () => (`)) and then render `<Text color="red">●
Error{watcherErrorLabel}</Text>` so no ternary remains inside the JSX
expression.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 364b3c32-cef0-4eb5-83c8-ee83cc26e1e9

📥 Commits

Reviewing files that changed from the base of the PR and between 5435d8c and fa04311.

📒 Files selected for processing (8)
  • contributing/concepts/engine/incremental.md
  • contributing/concepts/engine/openapi.md
  • contributing/concepts/engine/overview.md
  • packages/cli/package.json
  • packages/cli/src/screens/dev-screen.tsx
  • packages/cli/src/stories/dev.stories.tsx
  • packages/core/src/banner/index.ts
  • packages/core/src/sync/copy.ts

Comment thread packages/cli/src/screens/dev-screen.tsx Outdated
@zrosenbauer zrosenbauer merged commit c169109 into main Apr 8, 2026
5 checks passed
@zrosenbauer zrosenbauer deleted the perf/incremental-sync-engine branch April 8, 2026 16:05
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