Skip to content

feat(vcs): JJ#2461

Open
juliusmarminge wants to merge 9 commits intomainfrom
t3code/jj-driver-vsc
Open

feat(vcs): JJ#2461
juliusmarminge wants to merge 9 commits intomainfrom
t3code/jj-driver-vsc

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented May 2, 2026

Summary

  • Introduce a provider-neutral VCS layer with a new VcsDriver, VcsProcess, registry, and Git/JJ driver split.
  • Add a pluggable source-control provider layer and refactor GitHub PR handling to fit the new provider abstraction.
  • Update server and web consumers to use the new VCS/source-control contracts, including branch/status, checkout, and PR-related flows.
  • Refresh contracts, shared helpers, tests, and release workflow wiring to match the new architecture.

Testing

  • Not run locally in this turn.
  • Expected validation: bun fmt
  • Expected validation: bun lint
  • Expected validation: bun typecheck

Note

Medium Risk
Adds a new VCS driver and changes the status contract to require a kind field, which can affect validation/serialization and behavior across server + web. Also touches workflow routing and status caching/normalization, so regressions could surface in repo detection and Git-only action flows.

Overview
Adds first-class Jujutsu (jj) support by introducing a new JjVcsDriver (repo detection, workspace file listing, remotes, ignore filtering via git fallback, bookmarks/workspaces/current change) and registering it in VcsDriverRegistry, with auto-detection preferring JJ over Git for colocated repos.

Extends VCS status contracts/results to include a required kind (git/jj/unknown) and refactors status composition (GitWorkflowService now merges local+remote parts and returns explicit non-Git local status). Updates VcsStatusBroadcaster to normalize CWDs via FileSystem.realPath and propagates the new status shape through tests.

Updates the web UI to be VCS-aware: introduces vcsPresentation terminology (branches vs bookmarks/workspaces), adjusts labels/errors, and disables Git-only workflow actions when the active repo kind isn’t Git. CI/release workflows now install jj via a new composite setup-jj action.

Reviewed by Cursor Bugbot for commit a251619. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Jujutsu (jj) VCS driver with auto-detection and VCS-aware UI terminology

  • Introduces a full JjVcsDriver in JjVcsDriver.ts supporting repository detection, file listing, remote/bookmark/workspace listing, command execution, and ignore filtering via a git check-ignore fallback.
  • Updates VcsDriverRegistry to attempt JJ detection before Git when auto-detecting, and registers JJ as a known driver.
  • Adds a kind field ('git', 'jj', or 'unknown') to VcsStatusLocalShape and all status result objects, propagated through GitWorkflowService, GitManager, shared contracts, and stream event handlers.
  • Replaces hardcoded Git terminology in BranchToolbarBranchSelector, ChatHeader, DiffPanel, and GitActionsControl with VCS-aware labels via a new vcsPresentation.ts module; Git-specific workflow actions are disabled with a tooltip for non-Git repos.
  • Installs the jj CLI in CI via a new reusable setup-jj composite action used in both quality and release workflows.
  • Risk: VcsStatusLocalShape now requires a kind field; any producer or consumer of this contract that does not include kind will fail schema validation.

Macroscope summarized a251619.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 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: 52cb29e7-cd51-4ba1-9007-bf91a124f5be

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/jj-driver-vsc

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 2, 2026
@juliusmarminge juliusmarminge changed the base branch from main to t3code/pluggable-git-integration May 2, 2026 08:39
@juliusmarminge juliusmarminge changed the title Add pluggable VCS and source-control provider foundations feat(vcs): JJ May 2, 2026
Comment thread apps/server/src/vcs/GitVcsDriverCore.ts
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 2, 2026

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR adds a new VCS driver for Jujutsu (jj), introducing significant new capability including 500+ lines of driver code, VCS-specific UI terminology, and workflow behavior changes. Open review comments identify potential runtime issues that should be addressed.

You can customize Macroscope's approvability policy. Learn more.

Copy link
Copy Markdown
Member Author

@juliusmarminge juliusmarminge left a comment

Choose a reason for hiding this comment

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

Reviewed this against the pluggable VCS/source-control plan. This is a useful direction, especially the driver registration, shared contract tests, and making the web UI aware that not every repository is Git. I do not think we should treat this as final JJ support yet.

What I would change before this becomes the long-term shape:

  • Prefer JJ detection over Git for colocated JJ repositories. VcsDriverRegistry currently tries Git first, so jj --git repos will resolve as Git unless project config pins JJ. That makes auto-detect wrong for one of the most likely JJ setups.
  • Do not represent JJ power only through the generic VcsDriver plus disabled Git actions. The generic core is fine for status/diff/process basics, but JJ needs a typed extension surface for changes, bookmarks, workspaces, and evolution operations. Otherwise the app will only ever support the lowest common denominator.
  • Revisit the capability names. supportsWorktrees: true is misleading unless the generic API actually exposes worktree operations. If this means JJ workspaces, that should be a JJ capability/workflow, not a Git-term capability flag.
  • Make JjVcsDriver fully Effect-native. It currently uses raw node:fs/node:path/node:os temp directory handling. New driver code should follow the service style from this phase: detailed errors, FileSystem/Path, scoped acquire/release cleanup, and the shared process/scoping discipline.
  • The git check-ignore --no-index approach may be acceptable as a Git-compatible ignore fallback, but it should be explicitly modeled as that. It should not become the core JJ ignore semantics by accident.

The big architectural point: this PR should introduce jj as a first-class VCS with generic core operations plus JJ-specific extension points, not as a driver that mostly proves it can fit into a Git-shaped surface.

@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 May 2, 2026
Copy link
Copy Markdown
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.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicated utility functions across VCS drivers
    • Extracted splitNullSeparatedPaths and chunkPathsForCheckIgnore into a shared VcsPathUtils.ts module and updated both GitVcsDriver.ts and JjVcsDriver.ts to import from it, eliminating the duplication.

Create PR

Or push these changes by commenting:

@cursor push 31735f3ec7
Preview (31735f3ec7)
diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts
--- a/apps/server/src/vcs/GitVcsDriver.ts
+++ b/apps/server/src/vcs/GitVcsDriver.ts
@@ -21,6 +21,7 @@
 import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
 import { VcsDriver, type VcsDriverShape } from "./VcsDriver.ts";
 import { nowFreshness } from "./VcsFreshness.ts";
+import { chunkPathsForCheckIgnore, splitNullSeparatedPaths } from "./VcsPathUtils.ts";
 import { VcsProcess, type VcsProcessShape } from "./VcsProcess.ts";
 
 export interface ExecuteGitInput {
@@ -202,7 +203,6 @@
 ) {}
 
 const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
-const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;
 const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [
   "-c",
   "core.fsmonitor=false",
@@ -210,47 +210,6 @@
   "core.untrackedCache=false",
 ] as const;
 
-function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
-  const parts = input.split("\0");
-  if (parts.length === 0) return [];
-
-  if (truncated && parts[parts.length - 1]?.length) {
-    parts.pop();
-  }
-
-  return parts.filter((value) => value.length > 0);
-}
-
-function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {
-  const chunks: string[][] = [];
-  let chunk: string[] = [];
-  let chunkBytes = 0;
-
-  for (const relativePath of relativePaths) {
-    const relativePathBytes = Buffer.byteLength(relativePath) + 1;
-    if (chunk.length > 0 && chunkBytes + relativePathBytes > GIT_CHECK_IGNORE_MAX_STDIN_BYTES) {
-      chunks.push(chunk);
-      chunk = [];
-      chunkBytes = 0;
-    }
-
-    chunk.push(relativePath);
-    chunkBytes += relativePathBytes;
-
-    if (chunkBytes >= GIT_CHECK_IGNORE_MAX_STDIN_BYTES) {
-      chunks.push(chunk);
-      chunk = [];
-      chunkBytes = 0;
-    }
-  }
-
-  if (chunk.length > 0) {
-    chunks.push(chunk);
-  }
-
-  return chunks;
-}
-
 const gitCommand = (
   process: VcsProcessShape,
   operation: string,
@@ -392,7 +351,7 @@
       }
 
       const ignoredPaths = new Set<string>();
-      const chunks = chunkPathsForGitCheckIgnore(relativePaths);
+      const chunks = chunkPathsForCheckIgnore(relativePaths);
 
       for (const chunk of chunks) {
         const result = yield* gitCommand(

diff --git a/apps/server/src/vcs/JjVcsDriver.ts b/apps/server/src/vcs/JjVcsDriver.ts
--- a/apps/server/src/vcs/JjVcsDriver.ts
+++ b/apps/server/src/vcs/JjVcsDriver.ts
@@ -3,22 +3,11 @@
 import { VcsOutputDecodeError, VcsProcessExitError } from "@t3tools/contracts";
 import { VcsDriver, type VcsDriverShape } from "./VcsDriver.ts";
 import { nowFreshness } from "./VcsFreshness.ts";
+import { chunkPathsForCheckIgnore, splitNullSeparatedPaths } from "./VcsPathUtils.ts";
 import { VcsProcess, type VcsProcessShape } from "./VcsProcess.ts";
 
 const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
-const CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;
 
-function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
-  const parts = input.split("\0");
-  if (parts.length === 0) return [];
-
-  if (truncated && parts[parts.length - 1]?.length) {
-    parts.pop();
-  }
-
-  return parts.filter((value) => value.length > 0);
-}
-
 function splitLineSeparatedPaths(input: string, truncated: boolean): string[] {
   const lines = input.split(/\r?\n/g);
   if (truncated && lines[lines.length - 1]?.length) {
@@ -28,36 +17,6 @@
   return lines.map((line) => line.trim()).filter((line) => line.length > 0);
 }
 
-function chunkPathsForCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {
-  const chunks: string[][] = [];
-  let chunk: string[] = [];
-  let chunkBytes = 0;
-
-  for (const relativePath of relativePaths) {
-    const relativePathBytes = Buffer.byteLength(relativePath) + 1;
-    if (chunk.length > 0 && chunkBytes + relativePathBytes > CHECK_IGNORE_MAX_STDIN_BYTES) {
-      chunks.push(chunk);
-      chunk = [];
-      chunkBytes = 0;
-    }
-
-    chunk.push(relativePath);
-    chunkBytes += relativePathBytes;
-
-    if (chunkBytes >= CHECK_IGNORE_MAX_STDIN_BYTES) {
-      chunks.push(chunk);
-      chunk = [];
-      chunkBytes = 0;
-    }
-  }
-
-  if (chunk.length > 0) {
-    chunks.push(chunk);
-  }
-
-  return chunks;
-}
-
 const processCommand = (
   process: VcsProcessShape,
   command: string,

diff --git a/apps/server/src/vcs/VcsPathUtils.ts b/apps/server/src/vcs/VcsPathUtils.ts
new file mode 100644
--- /dev/null
+++ b/apps/server/src/vcs/VcsPathUtils.ts
@@ -1,0 +1,42 @@
+const CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;
+
+export function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
+  const parts = input.split("\0");
+  if (parts.length === 0) return [];
+
+  if (truncated && parts[parts.length - 1]?.length) {
+    parts.pop();
+  }
+
+  return parts.filter((value) => value.length > 0);
+}
+
+export function chunkPathsForCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {
+  const chunks: string[][] = [];
+  let chunk: string[] = [];
+  let chunkBytes = 0;
+
+  for (const relativePath of relativePaths) {
+    const relativePathBytes = Buffer.byteLength(relativePath) + 1;
+    if (chunk.length > 0 && chunkBytes + relativePathBytes > CHECK_IGNORE_MAX_STDIN_BYTES) {
+      chunks.push(chunk);
+      chunk = [];
+      chunkBytes = 0;
+    }
+
+    chunk.push(relativePath);
+    chunkBytes += relativePathBytes;
+
+    if (chunkBytes >= CHECK_IGNORE_MAX_STDIN_BYTES) {
+      chunks.push(chunk);
+      chunk = [];
+      chunkBytes = 0;
+    }
+  }
+
+  if (chunk.length > 0) {
+    chunks.push(chunk);
+  }
+
+  return chunks;
+}

You can send follow-ups to the cloud agent here.

Comment thread apps/server/src/vcs/JjVcsDriver.ts
Copy link
Copy Markdown
Member Author

@juliusmarminge juliusmarminge left a comment

Choose a reason for hiding this comment

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

Second pass after the follow-up commits. This fixed several concrete issues from the first review: JJ is now detected before Git, temp directory handling moved to Effect FileSystem, and the capability flags are less misleading. That is progress.

I still do not think this should merge as the long-term JJ shape yet.

Required changes / architectural blockers:

  • JjVcsDriver still only implements the generic VcsDriver surface (execute, detectRepository, isInsideWorkTree, listWorkspaceFiles, filterIgnoredPaths). That proves JJ can be detected, but it does not establish the extension-method model we need. A JJ PR should introduce a typed JJ extension service or extension registry shape now, even if only a small set is implemented. Examples: current change, bookmarks, workspace operations, operation log/evolution primitives. Without that, JJ support remains lowest-common-denominator VCS support with Git UI disabled.
  • packages/contracts/src/vcs.ts now has generic capabilities like supportsBookmarks and supportsAtomicSnapshot, but no typed API behind them. These flags are not enough for extensibility. Capabilities should either describe operations exposed on the generic core, or point to typed extension services. Otherwise callers can see that a capability exists but have no safe way to use it.
  • ignoreClassifier: "git-compatible-fallback" is better than silently pretending this is native, but JjVcsDriver.filterIgnoredPaths still shells out to Git and creates a synthetic bare Git dir. If this stays, it needs to be very clearly treated as a compatibility fallback and isolated from the eventual JJ-native extension path. We should not build future JJ behavior on top of Git semantics by default.
  • The UI path still mostly treats non-Git VCS as “disable Git actions and swap some labels”. That is acceptable for a detection-only slice, but not for claiming JJ as the first extension-capable VCS driver. The PR should either add the extension point now or explicitly narrow the scope/title to detection/status-only JJ support.

The core requirement from the plan is: generic VCS core plus VCS-specific extension methods. This PR improves the generic core, but it still does not add the extension-method half. I would hold the line here so we do not merge a shape that makes future JJ features feel bolted on.

@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from 1a1976b to e2e40c2 Compare May 2, 2026 16:37
Comment on lines +219 to +227
function fileSystemError(operation: string, cwd: string, detail: string, cause: unknown) {
return new VcsOutputDecodeError({
operation,
command: "git check-ignore",
cwd,
detail,
cause,
});
}
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.

🟢 Low vcs/JjVcsDriver.ts:219

fileSystemError hardcodes command: "git check-ignore" but is called from makeScopedTempGitDir when temp directory creation fails. This causes filesystem errors to report the wrong command, misleading anyone debugging the actual failure. Consider passing the correct command name as a parameter, or using a more generic error type for filesystem operations.

-function fileSystemError(operation: string, cwd: string, detail: string, cause: unknown) {
-  return new VcsOutputDecodeError({
-    operation,
-    command: "git check-ignore",
-    cwd,
-    detail,
-    cause,
-  });
-}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/vcs/JjVcsDriver.ts around lines 219-227:

`fileSystemError` hardcodes `command: "git check-ignore"` but is called from `makeScopedTempGitDir` when temp directory creation fails. This causes filesystem errors to report the wrong command, misleading anyone debugging the actual failure. Consider passing the correct command name as a parameter, or using a more generic error type for filesystem operations.

Evidence trail:
apps/server/src/vcs/JjVcsDriver.ts lines 219-226 (fileSystemError hardcodes command: "git check-ignore"), lines 229-236 (makeScopedTempGitDir calls fileSystemError on temp dir creation failure), lines 406-416 (makeScopedTempGitDir called from filterIgnoredPaths). packages/contracts/src/vcs.ts lines 104-118 (VcsOutputDecodeError class with message template "VCS output decode failed in ${this.operation}: ${this.command}").

Comment on lines +427 to +433
return yield* new VcsProcessExitError({
operation,
command: "git init --bare",
cwd,
exitCode: initResult.exitCode,
detail: initResult.stderr.trim() || "git init --bare failed",
});
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.

vcs/JjVcsDriver.ts:427

yield* new VcsProcessExitError({...}) at line 427 yields a plain error class instead of an Effect, causing a runtime type error when the generator tries to interpret it as an Effect. The correct pattern is yield* Effect.fail(new VcsProcessExitError({...})) as used at lines 313-320.

Suggested change
return yield* new VcsProcessExitError({
operation,
command: "git init --bare",
cwd,
exitCode: initResult.exitCode,
detail: initResult.stderr.trim() || "git init --bare failed",
});
return yield* Effect.fail(new VcsProcessExitError({
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/vcs/JjVcsDriver.ts around lines 427-433:

`yield* new VcsProcessExitError({...})` at line 427 yields a plain error class instead of an Effect, causing a runtime type error when the generator tries to interpret it as an Effect. The correct pattern is `yield* Effect.fail(new VcsProcessExitError({...}))` as used at lines 313-320.

Evidence trail:
packages/contracts/src/vcs.ts:75 - VcsProcessExitError extends Schema.TaggedErrorClass which makes instances yieldable Effects. apps/server/src/vcs/JjVcsDriver.ts:427 - the yield* new VcsProcessExitError pattern. apps/server/src/vcs/JjVcsDriver.ts:313-320 - Effect.fail() used in non-generator context (ternary). git_grep for 'yield\* new Vcs' shows 8+ uses of this pattern across the codebase (VcsProcess.ts:218, GitVcsDriver.ts:429,484, CheckpointStore.ts:147,165,253).

Copy link
Copy Markdown
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.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Thrown error becomes defect instead of typed error
    • Changed decodeJjCurrentChange to return Effect.Effect<JjCurrentChange | null, VcsOutputDecodeError> using Effect.fail/Effect.succeed, and updated the call site from Effect.map to Effect.flatMap so errors route to the typed error channel.

Create PR

Or push these changes by commenting:

@cursor push 02cac6dd61
Preview (02cac6dd61)
diff --git a/apps/server/src/vcs/JjVcsDriver.ts b/apps/server/src/vcs/JjVcsDriver.ts
--- a/apps/server/src/vcs/JjVcsDriver.ts
+++ b/apps/server/src/vcs/JjVcsDriver.ts
@@ -80,27 +80,32 @@
   return record.split("\0").map((value) => value.trim());
 }
 
-function decodeJjCurrentChange(raw: string, cwd: string): JjCurrentChange | null {
+function decodeJjCurrentChange(
+  raw: string,
+  cwd: string,
+): Effect.Effect<JjCurrentChange | null, VcsOutputDecodeError> {
   const trimmed = raw.trim();
   if (trimmed.length === 0) {
-    return null;
+    return Effect.succeed(null);
   }
 
   const [changeId, commitId, description] = parseNullRecord(trimmed);
   if (!changeId) {
-    throw new VcsOutputDecodeError({
-      operation: "JjVcsDriver.currentChange",
-      command: "jj log",
-      cwd,
-      detail: "jj current change output did not include a change id",
-    });
+    return Effect.fail(
+      new VcsOutputDecodeError({
+        operation: "JjVcsDriver.currentChange",
+        command: "jj log",
+        cwd,
+        detail: "jj current change output did not include a change id",
+      }),
+    );
   }
 
-  return {
+  return Effect.succeed({
     changeId,
     commitId: commitId || null,
     description: description || null,
-  };
+  });
 }
 
 function decodeJjBookmarkList(raw: string): ReadonlyArray<JjBookmark> {
@@ -370,7 +375,7 @@
         timeoutMs: 5_000,
         maxOutputBytes: 64 * 1024,
       },
-    ).pipe(Effect.map((result) => decodeJjCurrentChange(result.stdout, cwd)));
+    ).pipe(Effect.flatMap((result) => decodeJjCurrentChange(result.stdout, cwd)));
 
   const listBookmarks: JjVcsDriverShape["listBookmarks"] = (cwd) =>
     jjCommand(

You can send follow-ups to the cloud agent here.

Comment thread apps/server/src/vcs/JjVcsDriver.ts
@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch 3 times, most recently from 1b93238 to 2df8ce2 Compare May 2, 2026 20:03
Copy link
Copy Markdown
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.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Remote-only status fallback creates misleading unknown VCS kind
    • Added 'unknown' to the supported kinds in resolveVcsActionPresentation so the transient state from remoteUpdated fallback does not incorrectly disable git workflow controls.

Create PR

Or push these changes by commenting:

@cursor push 408123a544
Preview (408123a544)
diff --git a/apps/web/src/vcsPresentation.ts b/apps/web/src/vcsPresentation.ts
--- a/apps/web/src/vcsPresentation.ts
+++ b/apps/web/src/vcsPresentation.ts
@@ -58,7 +58,8 @@
   kind: VcsDriverKind | null | undefined,
 ): VcsActionPresentation {
   const terms = resolveVcsTerms(kind);
-  const supportsGitWorkflowActions = kind === undefined || kind === null || kind === "git";
+  const supportsGitWorkflowActions =
+    kind === undefined || kind === null || kind === "git" || kind === "unknown";
 
   return {
     supportsGitWorkflowActions,

You can send follow-ups to the cloud agent here.

Comment thread packages/shared/src/git.ts
@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from 2df8ce2 to e544202 Compare May 2, 2026 21:10
@juliusmarminge juliusmarminge force-pushed the t3code/pluggable-git-integration branch from 7aa00ff to f4180a4 Compare May 2, 2026 21:27
@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from e544202 to 31fb100 Compare May 2, 2026 21:27
Copy link
Copy Markdown
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.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Dead "sapling" branch in resolveVcsTerms
    • Removed the unreachable kind === "sapling" branch since VcsDriverKind only includes "git" | "jj" | "unknown".

Create PR

Or push these changes by commenting:

@cursor push 00f33428b1
Preview (00f33428b1)
diff --git a/apps/web/src/vcsPresentation.ts b/apps/web/src/vcsPresentation.ts
--- a/apps/web/src/vcsPresentation.ts
+++ b/apps/web/src/vcsPresentation.ts
@@ -40,13 +40,6 @@
     };
   }
 
-  if (kind === "sapling") {
-    return {
-      ...gitLikeTerms,
-      systemName: "Sapling",
-    };
-  }
-
   if (kind === "git") {
     return gitLikeTerms;
   }

You can send follow-ups to the cloud agent here.

Comment thread apps/web/src/vcsPresentation.ts Outdated
@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from 31fb100 to 156065c Compare May 2, 2026 21:50
};
});

const currentChange: JjVcsDriverShape["currentChange"] = (cwd) =>
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.

🟡 Medium vcs/JjVcsDriver.ts:359

decodeJjCurrentChange throws VcsOutputDecodeError synchronously, but it's called inside Effect.map() at line 376. In Effect, synchronous exceptions in Effect.map become defects (untyped failures) rather than typed errors in the error channel. The return type Effect.Effect<JjCurrentChange | null, VcsError> promises a typed VcsError, but the thrown exception bypasses this channel. Consider wrapping the decoding in Effect.try or Effect.catchAll to convert the throw into a typed error, or use Effect.gen with yield* like the pattern at line 430.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/vcs/JjVcsDriver.ts around line 359:

`decodeJjCurrentChange` throws `VcsOutputDecodeError` synchronously, but it's called inside `Effect.map()` at line 376. In Effect, synchronous exceptions in `Effect.map` become defects (untyped failures) rather than typed errors in the error channel. The return type `Effect.Effect<JjCurrentChange | null, VcsError>` promises a typed `VcsError`, but the thrown exception bypasses this channel. Consider wrapping the decoding in `Effect.try` or `Effect.catchAll` to convert the throw into a typed error, or use `Effect.gen` with `yield*` like the pattern at line 430.

Evidence trail:
apps/server/src/vcs/JjVcsDriver.ts lines 83-103 (decodeJjCurrentChange throws VcsOutputDecodeError at line 91); apps/server/src/vcs/JjVcsDriver.ts line 376 (called inside Effect.map); packages/contracts/src/vcs.ts lines 104-118 (VcsOutputDecodeError definition) and lines 146-154 (VcsError union includes VcsOutputDecodeError); Effect-TS documentation at https://github.com/Effect-TS/effect/blob/main/packages/effect/src/Effect.ts confirms Effect.sync is for non-throwing code, Effect.try is for code that throws; Effect-TS language-service at https://github.com/Effect-TS/language-service lists 'tryCatchInEffectGen' diagnostic that discourages try/catch in Effect generators in favor of Effect error handling, confirming throws bypass typed error channels.

Base automatically changed from t3code/pluggable-git-integration to main May 2, 2026 23:08
Comment on lines +36 to +49
const knownSourceControlDiscoveryKeys = new Set<string>();

export const sourceControlDiscoveryStateAtom = Atom.family((key: string) => {
knownSourceControlDiscoveryKeys.add(key);
return Atom.make(INITIAL_SOURCE_CONTROL_DISCOVERY_STATE).pipe(
Atom.keepAlive,
Atom.withLabel(`source-control-discovery:${key}`),
);
});

export const EMPTY_SOURCE_CONTROL_DISCOVERY_ATOM = Atom.make(
EMPTY_SOURCE_CONTROL_DISCOVERY_STATE,
).pipe(Atom.keepAlive, Atom.withLabel("source-control-discovery:null"));

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.

🟢 Low src/sourceControlDiscoveryState.ts:36

knownSourceControlDiscoveryKeys is module-scoped but mutated by each manager instance's reset() method. If multiple SourceControlDiscoveryManager instances are created (e.g., in parallel tests), calling reset() on one manager clears the shared Set, so subsequent reset() calls on other managers skip resetting atoms they should have tracked.

-const knownSourceControlDiscoveryKeys = new Set<string>();
+
 export const sourceControlDiscoveryStateAtom = Atom.family((key: string) => {
-  knownSourceControlDiscoveryKeys.add(key);
   return Atom.make(INITIAL_SOURCE_CONTROL_DISCOVERY_STATE).pipe(
     Atom.keepAlive,
     Atom.withLabel(`source-control-discovery:${key}`),
   );
 });

 export const EMPTY_SOURCE_CONTROL_DISCOVERY_ATOM = Atom.make(
   EMPTY_SOURCE_CONTROL_DISCOVERY_STATE,
 ).pipe(Atom.keepAlive, Atom.withLabel("source-control-discovery:null"));
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/client-runtime/src/sourceControlDiscoveryState.ts around lines 36-49:

`knownSourceControlDiscoveryKeys` is module-scoped but mutated by each manager instance's `reset()` method. If multiple `SourceControlDiscoveryManager` instances are created (e.g., in parallel tests), calling `reset()` on one manager clears the shared `Set`, so subsequent `reset()` calls on other managers skip resetting atoms they should have tracked.

Evidence trail:
packages/client-runtime/src/sourceControlDiscoveryState.ts line 36: `const knownSourceControlDiscoveryKeys = new Set<string>();` — module-scoped mutable set.
Line 38-39: `sourceControlDiscoveryStateAtom` family adds keys to this shared set.
Lines 177-182: `reset()` iterates the shared set, resets atoms, then calls `.clear()` on it.
Line 86: `createSourceControlDiscoveryManager` is a factory function creating per-instance `refreshInFlight` but sharing the module-level `knownSourceControlDiscoveryKeys`.

@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from 156065c to d75f5df Compare May 2, 2026 23:20
Copy link
Copy Markdown
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 prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Double async realPath in refreshStatus path
    • Extracted refreshLocalStatusCore (no normalization) so refreshStatus calls it directly after its own normalization, eliminating the redundant second realPath call.

Create PR

Or push these changes by commenting:

@cursor push 87975b1c96
Preview (87975b1c96)
diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts
--- a/apps/server/src/vcs/VcsStatusBroadcaster.ts
+++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts
@@ -201,13 +201,19 @@
       return mergeGitStatusParts(local, remote);
     });
 
+    const refreshLocalStatusCore = Effect.fn("VcsStatusBroadcaster.refreshLocalStatusCore")(
+      function* (cwd: string) {
+        yield* workflow.invalidateLocalStatus(cwd);
+        const local = yield* workflow.localStatus({ cwd });
+        return yield* updateCachedLocalStatus(cwd, local, { publish: true });
+      },
+    );
+
     const refreshLocalStatus: VcsStatusBroadcasterShape["refreshLocalStatus"] = Effect.fn(
       "VcsStatusBroadcaster.refreshLocalStatus",
     )(function* (rawCwd) {
       const cwd = yield* normalizeCwd(rawCwd);
-      yield* workflow.invalidateLocalStatus(cwd);
-      const local = yield* workflow.localStatus({ cwd });
-      return yield* updateCachedLocalStatus(cwd, local, { publish: true });
+      return yield* refreshLocalStatusCore(cwd);
     });
 
     const refreshRemoteStatus = Effect.fn("VcsStatusBroadcaster.refreshRemoteStatus")(function* (
@@ -223,7 +229,7 @@
     )(function* (rawCwd) {
       const cwd = yield* normalizeCwd(rawCwd);
       const [local, remote] = yield* Effect.all([
-        refreshLocalStatus(cwd),
+        refreshLocalStatusCore(cwd),
         refreshRemoteStatus(cwd),
       ]);
       return mergeGitStatusParts(local, remote);

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit d75f5df. Configure here.

Comment thread apps/server/src/vcs/VcsStatusBroadcaster.ts
- Detect and route jj repositories alongside git
- Thread VCS kind through status and UI labels
- Add jj driver contract coverage and shared freshness helper
Julius Marminge added 8 commits May 2, 2026 19:03
- Replace ad hoc temp-dir handling with scoped FileSystem APIs
- Provide NodeServices in VCS driver and status broadcaster tests
- Normalize CWDs through FileSystem realpath
- Detect jj before git for colocated repositories
- Add ignoreClassifier to VCS capabilities
- Fix trace2 hook exit code reporting
@juliusmarminge juliusmarminge force-pushed the t3code/jj-driver-vsc branch from d75f5df to a251619 Compare May 3, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 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.

1 participant