Changes view fails with "Failed to load changes" on Windows

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Note: I was able to fix this bug in cursor locally with cursor and can verify it works.

On Windows, the Changes view (the “Glass” diff panel) fails with “Failed to load changes”.
Root cause is in GlassDiffService (out/vs/workbench/workbench.glass.main.js): it matches the SCM git repository to the workspace
folder using a case-sensitive comparison of URI.path. The repository’s rootUri arrives with a lowercase drive letter (/c:/…) while
the workspace folder URI has an uppercase drive letter (/C:/…). The comparison therefore never matches, no repository is bound to the folder,
and the service is left without a working directory — so all subsequent diff calls run with an empty cwd and fail.

Observed in logs

renderer.log on failure:

[GlassDiffService] Refresh failed (reason=immediate, cwd=<empty>, gitRepoRoot=C:/<path>/<repo>): Failed to execute git
[GlassDiffService] Failed to refresh scope (scope=staged, cwd=<empty>): Failed to execute git
[GlassDiffService] Failed to refresh scope (scope=unstaged, cwd=<empty>): Failed to execute git

Note cwd=<empty> — the diff has no repository working directory.

Direct evidence (captured by temporarily instrumenting _resolveWorkspaceRootRepositories):
a failing repo vs a working repo on the same machine, same session —

# FAILING (folder URI drive is uppercase)
folder=/C:/<path>/<repoA>  repo=/c:/<path>/<repoA>  isGit=true  caseSensitiveMatch=false  caseInsensitiveMatch=true

# WORKING (folder URI drive is lowercase)
folder=/c:/<path>/<repoB>  repo=/c:/<path>/<repoB>  isGit=true  caseSensitiveMatch=true   caseInsensitiveMatch=true

caseSensitiveMatch=false is exactly the current (buggy) behavior; caseInsensitiveMatch=true
is what the fix produces.

Root cause (Cursor internals)

1. Case-sensitive repo↔folder matching

GlassDiffService._isUriAtOrInside compares URI.path literally, with no case normalization:

_isUriAtOrInside(n, e) {
  if (n.scheme !== e.scheme || n.authority !== e.authority) return false;
  const t = s => s.replace(/\/+$/, "") || "/",
        i = t(n.path),   // "/c:/<path>/<repo>"  — repo rootUri, LOWERCASE drive
        r = t(e.path);   // "/C:/<path>/<repo>"  — workspace folder uri, UPPERCASE drive
  return r === i || (i === "/" ? r.startsWith("/") : r.startsWith(`${i}/`));
}

_resolveWorkspaceRootRepositories() uses it to associate each SCM repo with a workspace folder; the mismatch makes it return an empty list:

_resolveWorkspaceRootRepositories() {
  const n = this._workspaceContextService.getWorkspace().folders, e = [];
  for (const i of this._scmService.repositories) {
    if (!this._isGitScmRepository(i)) continue;
    const r = i.provider.rootUri;                 // "/c:/..."  (lowercase)
    const s = n.find(o => this._isUriAtOrInside(o.uri, r) || this._isUriAtOrInside(r, o.uri));  // "/C:/..." never matches "/c:/..."
    s && (... push ...);                          // never pushed
  }
  return e;                                        // -> []  (empty)
}

2. Why the two casings differ

  • The repo rootUri comes from the built-in git extension, which lowercases the drive letter when resolving the repository root (extensions/git/dist/main.js, getRepositoryRoot).
  • The workspace folder URI (folders[0].uri, from URI.file()) keeps whatever drive-letter
    casing the open path had, and is not normalized to lowercase the way the git root is.

How the bug is triggered (origin of the uppercase drive)

The workspace folder URI’s drive casing depends on how the folder is opened, not on the repo:

  • cursor CLI from a terminal (cursor . / cursor C:\path\to\repo): on Windows the shell’s
    working directory is reported with an uppercase drive (C:\...), so the resulting folder URI
    is /C:/... → mismatch → fails.
  • Open via the Cursor UI / restored window list: uses the persisted workspace URI, which is
    stored lowercase (file:///c%3A/... in globalStorage/storage.json) → folder URI is /c:/...
    → matches → works.

This is why two repos on the same machine behave differently: the failing one had first been
opened via the CLI (uppercase drive), the working one via the UI (lowercase drive). Note that the
uppercase form, once introduced, is then persisted (see “Persistence” below), so it sticks across
later reopens. Confirmed with the instrumented caseSensitiveMatch evidence above.

3. Empty list → empty cwd → diff fails

Because the repo list is empty, _rootUri is never set and _cwd stays at its initializer (""). The diff entry point then runs git with that empty cwd and no --git-dir/--work-tree:

async getDiffFileEntries(e) {
  const t = ["--no-optional-locks", "diff", ...this._getDiffRangeArgs(e)];
  ...
  this.executeGitCommandStable(e.cwd /* "" */, [...t, "--name-status", "-z", ...], { caller: "GitProvider.getDiffFileEntries.nameStatus" });
}

With an empty cwd, the diff cannot resolve the repository and fails; GlassDiffService classifies it as the generic "unknown" snapshot error → “Failed to load changes.”

(_getGitRootStatus() does separately recover a root via a fallback and stores it in _gitRepoRootFsPath — that’s the uppercase gitRepoRoot=C:/… seen in the log — but _cwd is never populated from it, so the diff path stays broken.)

Persistence (why the failure is sticky)

The uppercase-drive form is persisted in %APPDATA%\Cursor\User\globalStorage\state.vscdb, inside
serialized URI objects under several keys, e.g.:

  • composer.composerHeaders
  • glass.localAgentProjects.v1
  • workspaceMetadata.entries
  • cursor/glass.additionalProjects
  • cursor/glass.removedProjects

Each serialized URI looks like:

{ "external": "file:///c%3A/<path>/<repo>", "path": "/C:/<path>/<repo>", "scheme": "file" }

Note the split: external (canonical URI.toString()) is lowercase, but the .path field
retains the uppercase drive from when the URI was constructed via URI.file() with an uppercase
input path. _isUriAtOrInside compares .path, so it uses the uppercase value.

Consequences:

  • Once these uppercase-.path URIs are persisted (originally seeded by a CLI open from an uppercase
    C:\ cwd), reopening the folder — even from the UI — rehydrates them with the uppercase .path,
    so the folder URI stays /C:/... and the match keeps failing. The failure therefore persists
    across restarts and is not cleared by reopening with a lowercase path or a .code-workspace.
  • The plain workspace.json files and globalStorage/storage.json store only the lowercase
    external form and are unaffected; the problematic data is specifically the serialized-URI
    blobs (with uppercase .path) in globalStorage/state.vscdb.

Verified fix

Making _isUriAtOrInside compare paths case-insensitively fixes it. We patched out/vs/workbench/workbench.glass.main.js and confirmed the Changes view loads correctly afterward (renderer.log then shows a non-empty cwd=c:/… and no “Refresh failed”):

// before
const t = s => s.replace(/\/+$/, "") || "/",
      i = t(n.path), r = t(e.path);

// after — lowercase both before comparing
const t = s => (s.replace(/\/+$/, "") || "/").toLowerCase(),
      i = t(n.path), r = t(e.path);

Steps to Reproduce

The trigger is the drive-letter casing of the workspace folder URI. The failure occurs when
that URI’s .path has an uppercase drive (/C:/...), because the git repo rootUri.path
always has a lowercase drive (/c:/...); see Root cause. Important: once a folder is opened
with an uppercase drive, Cursor persists the uppercase form (see “Persistence” below), so the
failure then survives reopening the folder even via the UI — clearing the persisted state is
required to get back to a clean baseline.

Reliable repro from a clean state:

  1. Ensure no persisted uppercase URI exists for the repo yet (fresh repo never opened in Cursor,
    or clear its entries — see Persistence). Confirm git rev-parse --show-toplevel reports a
    lowercase drive (c:\...), which is the default on Windows.
  2. From a terminal whose working directory is an uppercase path (PowerShell reports the cwd as
    C:\..., the Windows default), open the repo via the CLI: cursor . or cursor C:\path\to\repo.
    This constructs the folder URI with an uppercase drive (.path = /C:/...).
  3. Open the Changes view → “Failed to load changes”.

Contrast: opening the same repo so the folder URI is lowercase (/c:/...) — e.g. an entry whose
persisted path is lowercase — works. This is why two repos on the same machine differ purely by
how/when they were first opened. The Source Control view always works (it uses vscode.git
with an explicit --git-dir); only the Changes/Glass view fails.

Expected Behavior

Changes view shows the changes and no error message

Screenshots / Screen Recordings

Operating System

Windows 10/11

Version Information

Version: 3.8.24 (user setup)
VS Code Extension API: 1.105.1
Commit: cf80f4b937f3b9c48070d7085129a838ce7876a0
Date: 2026-06-24T06:55:08.142Z
Layout: editor
Build Type: Stable
Release Track: Default
Electron: 40.10.3
Chromium: 144.0.7559.236
Node.js: 24.15.0
V8: 14.4.258.32-electron.0
xterm.js: 6.1.0-beta.256
OS: Windows_NT x64 10.0.26100

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor

3 posts were merged into an existing topic: Agents Window cannot open Git repository