Stream git hook progress events for stacked git actions#1214
Stream git hook progress events for stacked git actions#1214juliusmarminge merged 14 commits intomainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
- Add hook/output progress callbacks to git execution and commit flow - Emit ordered git action progress events (start, phases, hooks, finish/fail) - Publish progress over WebSocket only to the initiating client - Update shared contracts and tests for the new progress event channel
- Add `actionId` to `GitRunStackedActionInput` and related API calls - Pass client-generated `actionId` from web UI through WS server to progress events - Update server/web/contracts tests to assert deterministic progress correlation
- Switch Git trace2 monitoring from polling to filesystem watch - Parse trace chunks with schema decoding and keep hook progress flowing - Update test expectations for hook output ordering
- Emit the commit phase_started event only when the hook needs to generate a message - Keep pre-resolved suggestions from triggering an unnecessary progress update
- Replace the ref indirection with `useEffectEvent` - Keep push and PR actions wired to the latest toast state
- Remove the extra checkout helper in GitActionsControl - Keep feature-branch flags with the action calls
a3df5e0 to
004247f
Compare
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Trace2 watcher path comparison may silently skip events
- The watcher now compares event.path against the basename of the trace file path (via path.basename()) in addition to the full path, so basename-only paths from fs.watch correctly trigger readTraceDelta.
- ✅ Fixed: Concurrent flush and watcher may race on shared state
- Wrapped readTraceDelta with a Semaphore(1) mutex so the watcher fiber and flush call cannot concurrently access the shared processedChars and lineBuffer state.
Or push these changes by commenting:
@cursor push e1599eebd9
Preview (e1599eebd9)
diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts
--- a/apps/server/src/git/Layers/GitCore.ts
+++ b/apps/server/src/git/Layers/GitCore.ts
@@ -12,6 +12,7 @@
Result,
Schema,
Scope,
+ Semaphore,
Stream,
} from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
@@ -361,30 +362,36 @@
}
});
- const readTraceDelta = fs.readFileString(traceFilePath).pipe(
- Effect.catch(() => Effect.succeed("")),
- Effect.flatMap((delta) =>
- Effect.gen(function* () {
- if (delta.length <= processedChars) {
- return;
- }
- const appended = delta.slice(processedChars);
- processedChars = delta.length;
- lineBuffer += appended;
- let newlineIndex = lineBuffer.indexOf("\n");
- while (newlineIndex >= 0) {
- const line = lineBuffer.slice(0, newlineIndex);
- lineBuffer = lineBuffer.slice(newlineIndex + 1);
- yield* handleTraceLine(line);
- newlineIndex = lineBuffer.indexOf("\n");
- }
- }),
+ const deltaMutex = Semaphore.makeUnsafe(1);
+ const readTraceDelta = deltaMutex.withPermit(
+ fs.readFileString(traceFilePath).pipe(
+ Effect.catch(() => Effect.succeed("")),
+ Effect.flatMap((delta) =>
+ Effect.gen(function* () {
+ if (delta.length <= processedChars) {
+ return;
+ }
+ const appended = delta.slice(processedChars);
+ processedChars = delta.length;
+ lineBuffer += appended;
+ let newlineIndex = lineBuffer.indexOf("\n");
+ while (newlineIndex >= 0) {
+ const line = lineBuffer.slice(0, newlineIndex);
+ lineBuffer = lineBuffer.slice(newlineIndex + 1);
+ yield* handleTraceLine(line);
+ newlineIndex = lineBuffer.indexOf("\n");
+ }
+ }),
+ ),
),
);
+ const traceFileName = path.basename(traceFilePath);
const watchTraceFile = Stream.runForEach(fs.watch(traceFilePath), (event) => {
const eventPath = event.path;
const isTargetTraceEvent =
- eventPath === traceFilePath || path.resolve(eventPath) === traceFilePath;
+ eventPath === traceFilePath ||
+ eventPath === traceFileName ||
+ path.basename(eventPath) === traceFileName;
if (!isTargetTraceEvent) return Effect.void;
return readTraceDelta;
}).pipe(Effect.ignoreCause({ log: true }));Bug 1: fs.watch provides basename-only paths on most platforms, so the comparison against the full absolute traceFilePath never matched. Now compares using path.basename() so the watcher correctly triggers readTraceDelta for real-time hook progress streaming. Bug 2: readTraceDelta is shared between the forked watcher fiber and the flush call. Both could enter concurrently at the async readFileString boundary, causing duplicate line processing. Wraps readTraceDelta with a Semaphore(1) mutex to serialize access to processedChars and lineBuffer. Applied via @cursor push command
- Reset trace2 line buffer after flushing final lines - Clear queued websocket work only for the active connection
- Wrap trace tail updates in an uninterruptible section - Preserve line processing while avoiding partial state updates
| ), | ||
| ), | ||
| Effect.ignore({ log: true }), | ||
| ), |
There was a problem hiding this comment.
Trace file fully re-read on every watch event
Low Severity
readTraceDelta calls fs.readFileString(traceFilePath) on every file-system watch event, re-reading the entire file from the start, then slicing from processedChars. As the trace file grows during a long-running commit with multiple hooks, the cumulative bytes read grows quadratically in the file size. Using a byte-offset–based read (or keeping a file handle open for incremental reads) would avoid re-reading already-processed content.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>



Closes #1194
Summary
runStackedAction, includingaction_started, phase transitions, hook lifecycle, hook output, success, and failure eventsactionIdand publishgitActionProgressevents only to the initiating clientTesting
apps/server/src/git/Layers/GitManager.test.ts: added tests for ordered commit-hook progress events andaction_failedon hook rejectionapps/server/src/wsServer.test.ts: added test to verify git action progress is delivered only to the initiating websocketapps/web/src/wsNativeApi.test.tsandapps/web/src/wsTransport.test.ts: added progress-channel handling coveragepackages/contracts/src/ws.test.ts: added contract-level ws progress event checksbun fmtbun lintbun typecheckbun run testNote
Stream real-time git hook progress events to the initiating WebSocket client during stacked git actions
GitActionProgressEventschema coveringaction_started,phase_started,hook_started,hook_output,hook_finished,action_finished, andaction_failedevents, broadcast over a newgit.actionProgressWebSocket push channel.GIT_TRACE2_EVENTto a temp file, parsing JSON hook lifecycle events, and forwarding them via progress callbacks.runStackedActionin GitManager.ts now accepts aprogressReporterandactionId, emitting phase and hook events throughout the action and reporting structured failure on error.git.onActionProgressfor UI subscriptions.actionIdper action, subscribes to progress events, and updates a long-running toast with phase labels, elapsed time, and live hook output.git.runStackedActionrequests now settimeoutMs: nullto disable client-side timeouts;WsTransportis updated to handle nullable timeouts and reject pending requests on WebSocket close.Macroscope summarized 035f61d.
Note
Medium Risk
Adds a new end-to-end progress event pipeline (Git TRACE2 parsing, websocket push channel, UI toast updates) and changes the
git.runStackedActioncontract to requireactionId, which can break existing clients and is moderately complex to get correct under concurrency/timeouts.Overview
Adds end-to-end progress reporting for
git.runStackedAction, emitting structured lifecycle events (action_started, phase transitions, hook start/finish, hook output, success/failure) correlated via a client-providedactionId.On the server,
GitCorenow supports per-line stdout/stderr callbacks and uses GitTRACE2event logs to detect commit hook execution;GitManagerwires these into aprogressReporter, applies a longer commit timeout, and publishesgit.actionProgresspushes only to the initiating websocket connection.On the client, the contracts/websocket protocol gain the
git.actionProgresschannel andactionIdrequirement; the web transport supports disabling request timeouts (used forrunStackedAction) and rejects in-flight requests on socket close, whileGitActionsControlsubscribes to progress pushes to drive a live “running…” toast with hook/output updates. Tests were added/updated across server, web, and contracts to cover event ordering, hook failures, correlation, and delivery scoping.Written by Cursor Bugbot for commit 035f61d. This will update automatically on new commits. Configure here.