Skip to content

perf(watch): stop watch-mode signature checks from stalling the review UI - #677

Open
benvinegar wants to merge 2 commits into
mainfrom
claude/watch-signature-non-blocking
Open

perf(watch): stop watch-mode signature checks from stalling the review UI#677
benvinegar wants to merge 2 commits into
mainfrom
claude/watch-signature-non-blocking

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Why

Watch mode re-runs watchSignature on every debounced file event and every safety poll. The Git backend's implementation ran three Bun.spawnSync calls — a full git diff, a rev-parse, and ls-files --others — plus a statSync per untracked file. So during active editing, which is exactly when file events fire most, the TUI froze roughly once a second for as long as Git took on the repo.

Measured with a 10 ms interval standing in for the render loop, across five signature checks on this repo:

render ticks worst stall
before (sync) 5 17 ms
after (async) 12 2 ms

Total wall time is unchanged (~120 ms either way). This does not make the check faster — it stops it freezing the terminal. End-to-end against a live repo (tracked edit, then a new untracked file) both refreshes fire and the loop ticks 203 times over 2.1 s.

What changed

Async Git runners, sharing all logic with the sync ones. Only the spawn differs; argument building, exit-code policy, and stderr translation moved into shared helpers (interpretGitResult, planWorkingTreeGitDiffCheck, revsIncludeWorkingTree) so the two paths cannot drift on what counts as a failure. The one-shot load path stays synchronous, where blocking is free.

Cancellation funnelled into one place. beginCheck had four isClosed() guards and two identical catch blocks, so safety depended on remembering a guard at every new await site. One runCheckStep helper now answers both questions — did we close, did the work fail — in a single place. Closing also aborts the signal handed to getSignature/refresh, so an in-flight git diff is killed rather than running to completion for a result nobody reads.

Deadline scheduler extracted. The four named deadlines collapsed into one chained timer move to src/core/watchDeadlines.ts with their own tests, leaving the controller to talk about phases instead of timer handles.

Extension API

watchSignature widens to string | Promise<string> and ExtensionVcsLoadContext gains an optional signal. Both are additive: an existing synchronous watchSignature still satisfies the contract, so third-party adapters keep working unchanged. check:pack verifies the published .d.ts still typechecks for consumers under both nodenext and bundler resolution across all 21 docs/extensions.md examples. Docs updated in docs/extensions.md and the website VCS-adapter page.

Sized as minor on the grounds that the extension contract gained a capability. If you'd rather treat it purely as the UI-responsiveness fix it is for end users, patch is defensible.

Testing

  • typecheck, lint, format:check, check:pack, check:docs — clean
  • bun test — 1774 pass, 0 fail
  • test:tty-smoke — 9 pass
  • test:integration — 84 pass, 2 fail; the same 2 fail on unmodified main (PTY file views > retains three preview types… and PTY chrome > filter focus narrows…), and both pass in isolation. Pre-existing flakes under parallel load.
  • New coverage: 12 tests for the deadline scheduler, 4 for the controller's abort/funnel behavior (including that the controller's own abort is silent while an upstream one is still reported).

Not done

The signature is still the entire patch text, so a large changeset allocates and compares megabytes of string on every poll. Hashing would fix that but trades away being able to eyeball a signature when debugging — left as a separate call.


Generated by Claude Code

…w UI

Watch mode re-runs `watchSignature` on every debounced file event and every
safety poll. The Git backend's implementation ran three `Bun.spawnSync`
calls — a full `git diff`, a `rev-parse`, and `ls-files --others` — so during
active editing, which is exactly when events fire most, the TUI froze once a
second for as long as Git took on the repo.

A render-loop proxy ticking at 10ms across five signature checks saw 5 ticks
before and 12 after over the same wall time. Total time is unchanged: this
does not make the check faster, it stops it freezing the terminal.

Three changes, all Effect-independent findings from the migration spike:

- Add async Git runners alongside the sync ones. Only the spawn differs;
  argument building, exit-code policy, and stderr translation stay in shared
  helpers so the two paths cannot drift. `watchSignature` widens to
  `string | Promise<string>` — backward compatible, an existing synchronous
  implementation still satisfies it — and `ExtensionVcsLoadContext` gains an
  optional `signal`.

- Funnel the controller's cancellation checks. `beginCheck` had four
  `isClosed()` guards and two identical catch blocks, so safety depended on
  remembering a guard at every new await site. One `runCheckStep` helper now
  answers both questions in one place, and closing aborts the signal handed
  to `getSignature`/`refresh` so in-flight work stops rather than running to
  completion for a result nobody reads.

- Extract the named-deadline scheduler. Four deadlines collapsed into one
  chained timer moves to `watchDeadlines.ts` with its own tests, leaving the
  controller to talk about phases instead of timer handles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YLF3qLZdxVvT87YXBESEib
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hunk-web Ready Ready Preview Aug 5, 2026 4:41pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves Git-backed watch signatures off the UI event loop, propagates cancellation through watch operations, and extracts watch timing into a named-deadline scheduler.

  • Adds shared synchronous/asynchronous Git command execution and asynchronous Git watch signatures.
  • Extends the VCS extension contract with promised signatures and optional cancellation signals.
  • Centralizes watch-controller cancellation and asynchronous error handling.
  • Adds a tested deadline scheduler for debounce, startup, and safety polling.
  • Updates extension documentation and package release metadata.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure identified.

The asynchronous Git paths preserve the existing command arguments and error interpretation, controller closure prevents stale results from being applied, and deadline consumption retains the prior debounce and polling behavior.

Important Files Changed

Filename Overview
src/core/vcs/git.ts Introduces shared Git result handling and cancellable asynchronous command, repository-root, and untracked-file helpers while retaining synchronous load behavior.
src/extensions/default/vcs/git/index.ts Converts Git watch signatures to asynchronous subprocess execution and threads the watcher cancellation signal through each operation.
src/core/watchController.ts Centralizes awaited-step error and closure handling, aborts in-flight work on close, and delegates timing to the deadline scheduler.
src/core/watchDeadlines.ts Adds a passive named-deadline scheduler that maintains one timer targeting the earliest pending deadline.
src/ui/hooks/useWatchedInput.ts Supports asynchronous baseline initialization and wires controller cancellation into subsequent signature checks.
src/extension-api/types.ts Additively permits promised watch signatures and exposes an optional cancellation signal to VCS extensions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Event[Filesystem event or safety deadline] --> Controller[Watch controller]
  Controller -->|AbortSignal| Signature[Compute watch signature]
  Signature --> Adapter[VCS adapter]
  Adapter --> AsyncGit[Async Git subprocess]
  AsyncGit --> Compare{Signature changed?}
  Compare -->|No| Schedule[Schedule next deadline]
  Compare -->|Yes| Refresh[Refresh review]
  Refresh --> Schedule
  Close[Watcher closes] -->|Abort| Controller
  Scheduler[Named deadline scheduler] --> Controller
Loading

Reviews (1): Last reviewed commit: "perf(watch): stop watch-mode signature c..." | Re-trigger Greptile

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.

2 participants