Skip to content

Stream git hook progress events for stacked git actions#1214

Merged
juliusmarminge merged 14 commits intomainfrom
t3code/show-git-hook-progress
Mar 24, 2026
Merged

Stream git hook progress events for stacked git actions#1214
juliusmarminge merged 14 commits intomainfrom
t3code/show-git-hook-progress

Conversation

@juliusmarminge
Copy link
Member

@juliusmarminge juliusmarminge commented Mar 19, 2026

Closes #1194

Summary

  • add end-to-end git action progress reporting for runStackedAction, including action_started, phase transitions, hook lifecycle, hook output, success, and failure events
  • extend git execution layers to support progress callbacks and surface hook start/finish via Git Trace2 during commit execution
  • wire WebSocket handling to generate an actionId and publish gitActionProgress events only to the initiating client
  • update shared contracts/protocol types for git progress events and plumb support through web transport/native API/UI
  • add server, web, and contracts tests covering hook progress ordering, hook failure behavior, and websocket event scoping

Testing

  • apps/server/src/git/Layers/GitManager.test.ts: added tests for ordered commit-hook progress events and action_failed on hook rejection
  • apps/server/src/wsServer.test.ts: added test to verify git action progress is delivered only to the initiating websocket
  • apps/web/src/wsNativeApi.test.ts and apps/web/src/wsTransport.test.ts: added progress-channel handling coverage
  • packages/contracts/src/ws.test.ts: added contract-level ws progress event checks
  • Not run: bun fmt
  • Not run: bun lint
  • Not run: bun typecheck
  • Not run: bun run test

Note

Stream real-time git hook progress events to the initiating WebSocket client during stacked git actions

  • Adds a discriminated union GitActionProgressEvent schema covering action_started, phase_started, hook_started, hook_output, hook_finished, action_finished, and action_failed events, broadcast over a new git.actionProgress WebSocket push channel.
  • Sets up git trace2 monitoring (GitCore.ts) by pointing GIT_TRACE2_EVENT to a temp file, parsing JSON hook lifecycle events, and forwarding them via progress callbacks.
  • runStackedAction in GitManager.ts now accepts a progressReporter and actionId, emitting phase and hook events throughout the action and reporting structured failure on error.
  • Progress events are scoped to the requesting client: wsServer.ts injects a per-socket reporter and wsNativeApi.ts exposes git.onActionProgress for UI subscriptions.
  • The UI (GitActionsControl.tsx) generates a UUID actionId per action, subscribes to progress events, and updates a long-running toast with phase labels, elapsed time, and live hook output.
  • git.runStackedAction requests now set timeoutMs: null to disable client-side timeouts; WsTransport is 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.runStackedAction contract to require actionId, 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-provided actionId.

On the server, GitCore now supports per-line stdout/stderr callbacks and uses Git TRACE2 event logs to detect commit hook execution; GitManager wires these into a progressReporter, applies a longer commit timeout, and publishes git.actionProgress pushes only to the initiating websocket connection.

On the client, the contracts/websocket protocol gain the git.actionProgress channel and actionId requirement; the web transport supports disabling request timeouts (used for runStackedAction) and rejects in-flight requests on socket close, while GitActionsControl subscribes 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.

@juliusmarminge juliusmarminge marked this pull request as draft March 19, 2026 18:16
@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 19, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e3c94be5-e7f5-4161-b06e-6a2798074a25

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/show-git-hook-progress

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

- 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
@juliusmarminge juliusmarminge force-pushed the t3code/show-git-hook-progress branch from a3df5e0 to 004247f Compare March 24, 2026 07:50
@juliusmarminge juliusmarminge marked this pull request as ready for review March 24, 2026 07:54
@juliusmarminge
Copy link
Member Author

bugbot run

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions github-actions bot added size:XL 500-999 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Mar 24, 2026
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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.

Create PR

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 }));

@juliusmarminge
Copy link
Member Author

@cursor push e1599ee

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
@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Mar 24, 2026
- 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
@juliusmarminge juliusmarminge merged commit 384f350 into main Mar 24, 2026
11 checks passed
@juliusmarminge juliusmarminge deleted the t3code/show-git-hook-progress branch March 24, 2026 09:25
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

),
),
Effect.ignore({ log: true }),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

kkorenn pushed a commit to kkorenn/k1code that referenced this pull request Mar 24, 2026
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Git commit time-out

2 participants