Skip to content

Handle missing workspaces across server and UI#1523

Draft
juliusmarminge wants to merge 1 commit intomainfrom
t3code/missing-cwd-handling
Draft

Handle missing workspaces across server and UI#1523
juliusmarminge wants to merge 1 commit intomainfrom
t3code/missing-cwd-handling

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 29, 2026

Summary

  • Added shared workspace path validation and availability state tracking on the server, with clearer errors for missing, deleted, non-directory, and inaccessible workspaces.
  • Guarded git, provider, terminal, and websocket operations so they fail early when the target workspace no longer exists.
  • Extended orchestration snapshots and contracts with workspaceState and effectiveCwd metadata so the UI can distinguish project vs. worktree availability.
  • Updated the web app to hide or disable workspace-dependent actions when the underlying project or thread workspace is unavailable.
  • Added and adjusted tests for missing-workspace scenarios across git, provider recovery, checkpointing, and orchestration projection logic.

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test
  • Added targeted tests for missing workspace handling in server layers and projection/query code.

Note

Medium Risk
Moderate risk because it adds new workspace validation gates across WebSocket routes, git/provider/session flows, and UI queries; incorrect state detection or caching could block previously-working operations.

Overview
Adds a shared server utility (workspacePaths.ts) to classify and cache workspace path availability (missing/not-a-directory/inaccessible) and to fail fast via assertWorkspaceDirectory / WorkspacePathError.

Propagates workspace availability into orchestration snapshots and contracts by adding workspaceState on projects and effectiveCwd/effectiveCwdSource/effectiveCwdState on threads, and updates projection snapshot hydration to compute these values from filesystem inspection.

Guards workspace-dependent operations to return clearer errors when folders disappear: wraps git execution and statusDetails, validates provider session start/recovery and provider-command session creation, blocks checkpoint revert/diff when workspace is unavailable, and validates wsServer routes (git/terminal/file search+write/open-in-editor) while formatting operational errors more cleanly.

Updates the web app to disable/hide git, terminal, diff, scripts, and path search when the relevant project/worktree is unavailable, shows “Workspace Missing” messaging with actions (relink project, fall back to project root, delete thread), and adjusts React Query options/invalidation to avoid refetch-on-focus and refresh snapshot on focus/visibility changes. Tests are updated/added for the new snapshot fields and missing-workspace failure paths.

Written by Cursor Bugbot for commit ae625d2. This will update automatically on new commits. Configure here.

Note

Handle missing workspaces across server and UI by adding availability checks and user-facing warnings

  • Adds WorkspaceAvailabilityState and ThreadEffectiveCwdSource to the contracts schema, and propagates workspaceState, effectiveCwd, effectiveCwdSource, and effectiveCwdState through the server read model, client store, and web types.
  • Introduces inspectWorkspacePathState and assertWorkspaceDirectory in workspacePaths.ts; git, terminal, provider, and WebSocket route handlers now fail early with structured WorkspacePathError or GitCommandError messages when a workspace path is missing or inaccessible.
  • The chat view, sidebar, and branch toolbar conditionally disable git queries, terminal access, and project scripts when the effective workspace is unavailable, and display warning banners with actions to relink, switch to project root, or delete the thread.
  • Git status and branch queries in gitReactQuery.ts gain an explicit enabled flag and no longer auto-refetch on window focus or reconnect (snapshot sync now handles invalidation on focus instead).
  • Risk: any code path that previously silently proceeded with a missing cwd will now surface an error to the user or fail a turn early.
📊 Macroscope summarized ae625d2. 32 files reviewed, 4 issues evaluated, 1 issue filtered, 1 comment posted

🗂️ Filtered Issues

apps/web/src/components/BranchToolbar.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 50: When there is no serverThread (draft thread) but draftThread?.worktreePath is set (e.g., when the user selected a branch that already lives in an existing worktree), the new branchCwd computation ignores the worktree path entirely and falls back to activeProject.cwd. The old code used activeWorktreePath ?? activeProject?.cwd ?? null, which correctly prioritized the worktree path. This causes git operations (branch listing, checkout) to run against the wrong directory. [ Cross-file consolidated ]

- validate workspace paths before git, provider, terminal, and WS ops
- surface workspace availability in projections and client state
- add tests for missing cwd recovery and status handling
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 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: 506923a1-edd8-4d5f-ba6e-b6cb0133e048

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/missing-cwd-handling

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

@juliusmarminge juliusmarminge marked this pull request as draft March 29, 2026 07:46
@github-actions github-actions bot added size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 29, 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 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Draft thread effectiveCwd is null, breaking git/terminal features
    • Added projectCwd parameter to buildLocalDraftThread and passed fallbackDraftProject.cwd from the call site, so effectiveCwd now falls back to the project's cwd when no worktreePath is set, matching the server-side projection logic.
  • ✅ Fixed: Redundant workspace assertion in statusDetails before execute
    • Removed the redundant ensureGitWorkspace call in statusDetails and the single-use ensureGitWorkspace helper, since the execute wrapper already calls assertWorkspaceDirectory for every git command.

Create PR

Or push these changes by commenting:

@cursor push b9268a3495
Preview (b9268a3495)
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
@@ -673,20 +673,6 @@
       }),
     );
 
-  const ensureGitWorkspace = (operation: string, cwd: string, args: readonly string[] = []) =>
-    assertWorkspaceDirectory(cwd, operation).pipe(
-      Effect.mapError(
-        (error) =>
-          new GitCommandError({
-            operation,
-            command: quoteGitCommand(args),
-            cwd,
-            detail: error.message,
-            cause: error,
-          }),
-      ),
-    );
-
   const runGit = (
     operation: string,
     cwd: string,
@@ -1073,11 +1059,6 @@
   });
 
   const statusDetails: GitCoreShape["statusDetails"] = Effect.fn("statusDetails")(function* (cwd) {
-    yield* ensureGitWorkspace("GitCore.statusDetails", cwd, [
-      "status",
-      "--porcelain=2",
-      "--branch",
-    ]);
     yield* refreshStatusUpstreamIfStale(cwd).pipe(Effect.ignoreCause({ log: true }));
 
     const [statusStdout, unstagedNumstatStdout, stagedNumstatStdout] = yield* Effect.all(

diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts
--- a/apps/web/src/components/ChatView.logic.ts
+++ b/apps/web/src/components/ChatView.logic.ts
@@ -19,6 +19,7 @@
   draftThread: DraftThreadState,
   fallbackModelSelection: ModelSelection,
   error: string | null,
+  projectCwd: string | null,
 ): Thread {
   return {
     id: threadId,
@@ -37,8 +38,8 @@
     lastVisitedAt: draftThread.createdAt,
     branch: draftThread.branch,
     worktreePath: draftThread.worktreePath,
-    effectiveCwd: draftThread.worktreePath ?? null,
-    effectiveCwdSource: draftThread.worktreePath ? "worktree" : null,
+    effectiveCwd: draftThread.worktreePath ?? projectCwd,
+    effectiveCwdSource: draftThread.worktreePath ? "worktree" : projectCwd ? "project" : null,
     effectiveCwdState: "available",
     turnDiffSummaries: [],
     activities: [],

diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -486,9 +486,16 @@
               model: DEFAULT_MODEL_BY_PROVIDER.codex,
             },
             localDraftError,
+            fallbackDraftProject?.cwd ?? null,
           )
         : undefined,
-    [draftThread, fallbackDraftProject?.defaultModelSelection, localDraftError, threadId],
+    [
+      draftThread,
+      fallbackDraftProject?.defaultModelSelection,
+      fallbackDraftProject?.cwd,
+      localDraftError,
+      threadId,
+    ],
   );
   const activeThread = serverThread ?? localDraftThread;
   const runtimeMode =

branch: draftThread.branch,
worktreePath: draftThread.worktreePath,
effectiveCwd: draftThread.worktreePath ?? null,
effectiveCwdSource: draftThread.worktreePath ? "worktree" : 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.

Draft thread effectiveCwd is null, breaking git/terminal features

High Severity

buildLocalDraftThread sets effectiveCwd to draftThread.worktreePath ?? null, which is null for the common case of a draft thread without a worktree. The old code used projectScriptCwd, which fell back to activeProject.cwd. Now in ChatView.tsx, gitCwd is derived as threadWorkspaceAvailable ? (activeThread?.effectiveCwd ?? null) : null, so for draft threads it resolves to null. This disables git branches, diff panel, file search, and the terminal drawer for all new draft threads.

Additional Locations (1)
Fix in Cursor Fix in Web

"status",
"--porcelain=2",
"--branch",
]);
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.

Redundant workspace assertion in statusDetails before execute

Low Severity

ensureGitWorkspace in statusDetails is redundant because the execute wrapper (line 618–632) already calls assertWorkspaceDirectory for every git command. The three subsequent runGitStdout calls each go through execute, so the workspace is validated four times total. The ensureGitWorkspace helper itself is only used in this one location.

Fix in Cursor Fix in Web

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.

🟠 High

const cwd = input.preferSessionRuntime
? (Option.match(fromSession, {
onNone: () => undefined,
onSome: (runtime) => runtime.cwd,
}) ?? fromThread)
: (fromThread ??
Option.match(fromSession, {
onNone: () => undefined,
onSome: (runtime) => runtime.cwd,
}));
if (input.thread.effectiveCwdState !== "available" || !cwd) {
return undefined;
}
if (!isGitWorkspace(cwd)) {
return undefined;
}
return cwd;

When preferSessionRuntime is true and the session provides a valid cwd, the function still checks input.thread.effectiveCwdState !== "available" and returns undefined if the thread's cached state is stale. This incorrectly blocks checkpoint operations even though a usable workspace exists via the session. Consider validating the state of the actually-resolved cwd (as done in handleRevertRequested) rather than the thread's cached property.

    const cwd = input.preferSessionRuntime
      ? (Option.match(fromSession, {
          onNone: () => undefined,
          onSome: (runtime) => runtime.cwd,
        }) ?? fromThread)
      : (fromThread ??
        Option.match(fromSession, {
          onNone: () => undefined,
          onSome: (runtime) => runtime.cwd,
        }));

-    if (input.thread.effectiveCwdState !== "available" || !cwd) {
+    const resolvedState = yield* Effect.promise(() =>
+      inspectWorkspacePathState(cwd ?? input.thread.effectiveCwd ?? ""),
+    );
+    if (resolvedState !== "available" || !cwd) {
       return undefined;
     }
     if (!isGitWorkspace(cwd)) {
Also found in 1 other location(s)

apps/web/src/components/BranchToolbar.tsx:50

When there is no serverThread (draft thread) but draftThread?.worktreePath is set (e.g., when the user selected a branch that already lives in an existing worktree), the new branchCwd computation ignores the worktree path entirely and falls back to activeProject.cwd. The old code used activeWorktreePath ?? activeProject?.cwd ?? null, which correctly prioritized the worktree path. This causes git operations (branch listing, checkout) to run against the wrong directory.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/CheckpointReactor.ts around lines 176-193:

When `preferSessionRuntime` is true and the session provides a valid `cwd`, the function still checks `input.thread.effectiveCwdState !== "available"` and returns `undefined` if the thread's cached state is stale. This incorrectly blocks checkpoint operations even though a usable workspace exists via the session. Consider validating the state of the actually-resolved `cwd` (as done in `handleRevertRequested`) rather than the thread's cached property.

Evidence trail:
apps/server/src/orchestration/Layers/CheckpointReactor.ts lines 159-193 (REVIEWED_COMMIT): `resolveCheckpointCwd` function resolves cwd preferring session runtime (lines 176-185) but checks `input.thread.effectiveCwdState !== "available"` at line 186 regardless of cwd source.

apps/server/src/orchestration/Layers/CheckpointReactor.ts lines 568-607 (REVIEWED_COMMIT): `handleRevertRequested` validates session runtime's cwd state using `inspectWorkspacePathState(sessionRuntime.value.cwd)` at line 596-598, checking the actual resolved cwd's state.

Also found in 1 other location(s):
- apps/web/src/components/BranchToolbar.tsx:50 -- When there is no `serverThread` (draft thread) but `draftThread?.worktreePath` is set (e.g., when the user selected a branch that already lives in an existing worktree), the new `branchCwd` computation ignores the worktree path entirely and falls back to `activeProject.cwd`. The old code used `activeWorktreePath ?? activeProject?.cwd ?? null`, which correctly prioritized the worktree path. This causes git operations (branch listing, checkout) to run against the wrong directory.

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