Add version control command center#3177
Conversation
- Query open change requests across supported remotes - Surface actionable branch rows only when local branch is behind the MR/PR base - Pass remote repository context to GitHub, GitLab, and Azure DevOps CLIs
…ol-panel-work # Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/environmentApi.ts # apps/web/src/environments/runtime/service.threadSubscriptions.test.ts # packages/client-runtime/src/wsRpcClient.ts
- Extract shared source control panel state and API wiring - Move VS Code project scope and subagent parent resolution into runtime helpers - Add tests for presentation state and subagent control resolution
|
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)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review 11 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
- Treat same-name remotes as publish/pull upstreams - Split sync-state logic into shared web module - Cover publish target behavior with tests and docs
- Add `forceRefresh` support to query runners and runtime execution - Refresh cached SWR atoms before resolving so panel data stays current - Cover first-run and cached-refresh behavior with runtime tests
- Move sync refresh feedback into a compact spinner icon - Keep the banner for non-refresh sync messages
- Sum merged working-tree row stats across both sides - Keep default compare ref on the repository default branch - Restrict diverged merge sync to the checked-out branch - Surface tracked discard failures and rename sources - Move shared panel logic into the logic module
- Infer rename status for numstat fallback entries - Align diverged merge sync guard and disabled state - Clarify aggregate working-tree stats and date buckets
…ol-panel-work # Conflicts: # apps/desktop/src/electron/ElectronMenu.ts # apps/server/src/sourceControl/AzureDevOpsCli.ts # apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts # apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts # apps/server/src/sourceControl/GitHubCli.ts # apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts # apps/server/src/sourceControl/GitHubSourceControlProvider.ts # apps/server/src/sourceControl/GitLabCli.ts # apps/server/src/sourceControl/SourceControlProvider.ts # apps/server/src/vcs/VcsStatusBroadcaster.ts # apps/server/src/ws.ts # apps/web/src/components/ChatView.tsx # apps/web/src/components/Sidebar.logic.ts # apps/web/src/hooks/useHandleNewThread.ts
There was a problem hiding this comment.
Effect Service Conventions: one finding in DesktopBackendManager.ts. See inline comment.
Posted via Macroscope — Effect Service Conventions
| }, [key, query, target.fallbackSkills]); | ||
|
|
||
| const previousWorkspaceSkillsRef = useRef<ProviderWorkspaceSkillsSnapshot | null>(null); | ||
| const querySkills = query.data?.skills ?? null; |
There was a problem hiding this comment.
🟡 Medium lib/providerWorkspaceSkillsState.ts:128
When a workspace skills request fails after a prior success, query.data still holds the previous success value (due to AsyncResult.value preserving it on failure), so querySkills at line 128 returns stale skills instead of null. The resolveProviderWorkspaceSkills logic at lines 145-153 then returns those stale workspace-specific skills rather than falling back to target.fallbackSkills, causing the composer to offer commands for the wrong repository after a failed refresh. Consider checking query.error before using query.data.skills, or ensure querySkills is null when query.error is set.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/lib/providerWorkspaceSkillsState.ts around line 128:
When a workspace skills request fails after a prior success, `query.data` still holds the previous success value (due to `AsyncResult.value` preserving it on failure), so `querySkills` at line 128 returns stale skills instead of `null`. The `resolveProviderWorkspaceSkills` logic at lines 145-153 then returns those stale workspace-specific skills rather than falling back to `target.fallbackSkills`, causing the composer to offer commands for the wrong repository after a failed refresh. Consider checking `query.error` before using `query.data.skills`, or ensure `querySkills` is `null` when `query.error` is set.
Partial cherry-pick of 1c6f66b for packages/client-runtime/src/state/vcs.ts only. Unrelated auth, config, settings, VS Code docs, and CUSTOMIZED.md changes were dropped.
Partial cherry-pick of 4b17fa4 for source-control metadata handling and SOURCE_CONTROL.md. Service changes were already applied from local main; terminal UI and CUSTOMIZED.md changes were dropped.
Partial cherry-pick of 4dc799a for source-control metadata stale-result handling, error pruning, and dismiss behavior. Unrelated terminal helper changes were left out.
Partial cherry-pick of a611f8e for SOURCE_CONTROL.md and SourceControlPanel.tsx only. Terminal, chat timeline, session-logic, and CUSTOMIZED.md changes were dropped.
Cherry-picked source-control portion of 373349a. CUSTOMIZED.md was left absent for this split worktree.
Cherry-picked 19c0d1a for SourceControlPanel.tsx.
Ported apps/server/src/diagnostics/ErrorCause.ts because the selected SourceControlPanelService changes depend on sanitizeErrorCause for bounded panel diagnostics.
- Drop host display preferences and workspace scope helpers - Always expose the source control panel when thread and repo are available - Simplify thread subscription stream handling
- Drop the unused `t3HostBridge` contract and window type - Remove sidebar thread row helpers no longer used by the web app
7421816 to
ea9bf9b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ea9bf9b. Configure here.
| if (template.length > 0 && !lastWasSeparator) { | ||
| template.push({ type: "separator" }); | ||
| lastWasSeparator = true; | ||
| } |
There was a problem hiding this comment.
Destructive flag blocks later separator
Low Severity
In buildTemplate, the first destructive item sets hasInsertedDestructiveSeparator even when no separator is inserted because the menu starts with that item. A later destructive entry then skips the auto-separator path, so a destructive action after normal items can appear without the intended separator.
Reviewed by Cursor Bugbot for commit ea9bf9b. Configure here.
| return { project: sshProject, repository: sshRepository }; | ||
| } | ||
| const repository = parts.at(-1); | ||
| return repository ? { repository } : undefined; |
There was a problem hiding this comment.
Azure repo parse drops project
Medium Severity
When azureRepositoryFromContext cannot parse _git or SSH v3 segments from a remote URL, it returns only the last path segment as repository and omits project. Azure DevOps PR listing for that remote can target the wrong project or miss open change requests in the Version Control panel.
Reviewed by Cursor Bugbot for commit ea9bf9b. Configure here.
| Effect.ignoreCause({ log: true }), | ||
| ); | ||
|
|
||
| const retainLocalWatcher = Effect.fn("VcsStatusBroadcaster.retainLocalWatcher")(function* ( |
There was a problem hiding this comment.
🟡 Medium vcs/VcsStatusBroadcaster.ts:555
retainLocalWatcher reuses an existing entry from watchersRef by incrementing subscriberCount without checking whether the entry's fiber is still running. Because makeLocalWatchLoop wraps both the stream consumption and each refresh in Effect.ignoreCause, a failed or ended fs.watch stream causes the fiber to terminate normally while the map entry persists. Subsequent subscribers then attach to the dead fiber — subscriberCount is incremented but no new filesystem watcher is created — so local status stops auto-refreshing for that repository until the process restarts. Consider checking fiber liveness (e.g. via Fiber.poll) before reusing an entry, and replacing stale entries with a fresh watcher if the fiber has terminated.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 555:
`retainLocalWatcher` reuses an existing entry from `watchersRef` by incrementing `subscriberCount` without checking whether the entry's `fiber` is still running. Because `makeLocalWatchLoop` wraps both the stream consumption and each refresh in `Effect.ignoreCause`, a failed or ended `fs.watch` stream causes the fiber to terminate normally while the map entry persists. Subsequent subscribers then attach to the dead fiber — `subscriberCount` is incremented but no new filesystem watcher is created — so local status stops auto-refreshing for that repository until the process restarts. Consider checking fiber liveness (e.g. via `Fiber.poll`) before reusing an entry, and replacing stale entries with a fresh watcher if the fiber has terminated.
| }); | ||
| export type VcsPanelBranchActionInput = typeof VcsPanelBranchActionInput.Type; | ||
|
|
||
| export const VcsPanelDeleteBranchInput = Schema.Struct({ |
There was a problem hiding this comment.
🟠 High src/git.ts:551
VcsPanelDeleteBranchInput accepts a full VcsRef object from the client, including isRemote and remoteName. Since the service uses those fields to decide whether to run a destructive git push <remote> --delete versus a local git branch -d, a caller can forge { name: "main", isRemote: true, remoteName: "origin" } to delete a remote branch that was never marked remote in the server's branch snapshot. The destructive remote-deletion decision should not depend on untrusted client-supplied metadata. Consider accepting only branchName (as VcsPanelBranchActionInput does) and resolving the ref server-side.
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/git.ts around line 551:
`VcsPanelDeleteBranchInput` accepts a full `VcsRef` object from the client, including `isRemote` and `remoteName`. Since the service uses those fields to decide whether to run a destructive `git push <remote> --delete` versus a local `git branch -d`, a caller can forge `{ name: "main", isRemote: true, remoteName: "origin" }` to delete a remote branch that was never marked remote in the server's branch snapshot. The destructive remote-deletion decision should not depend on untrusted client-supplied metadata. Consider accepting only `branchName` (as `VcsPanelBranchActionInput` does) and resolving the ref server-side.
| const unstaged: VcsPanelFileChange[] = []; | ||
| const conflicts: VcsPanelFileChange[] = []; | ||
|
|
||
| for (const line of input.status.split(/\r?\n/u)) { |
There was a problem hiding this comment.
🟡 Medium sourceControl/SourceControlPanelService.ts:290
parsePorcelainStatus stores paths exactly as they appear in the git status --porcelain=2 output, but when the command is run without -z, Git C-quotes any path containing special characters (tabs, newlines, quotes, backslashes, non-ASCII bytes). The parser never unquotes these, so such files are stored under the quoted form (e.g. "\303\242\303\244.txt") instead of the real path. Downstream lookups against parseNumstat() results and requestedPathSet use the real pathname, so stats and enrichment for those files silently fail and the UI displays the quoted string. Consider adding a C-unquote step for every path extracted from the status output, or running the command with -z to disable quoting.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/SourceControlPanelService.ts around line 290:
`parsePorcelainStatus` stores paths exactly as they appear in the `git status --porcelain=2` output, but when the command is run without `-z`, Git C-quotes any path containing special characters (tabs, newlines, quotes, backslashes, non-ASCII bytes). The parser never unquotes these, so such files are stored under the quoted form (e.g. `"\303\242\303\244.txt"`) instead of the real path. Downstream lookups against `parseNumstat()` results and `requestedPathSet` use the real pathname, so stats and enrichment for those files silently fail and the UI displays the quoted string. Consider adding a C-unquote step for every path extracted from the status output, or running the command with `-z` to disable quoting.
| status: input.status, | ||
| stagedFiles: [], | ||
| stagedStats: new Map(), |
There was a problem hiding this comment.
🟡 Medium sourceControl/SourceControlPanelService.ts:382
unstagedFilesFromPorcelainStatus passes stagedFiles: [] to parsePorcelainStatus. When a file has both staged and unstaged changes (e.g. MM, AM), the staged block hits if (input.stagedFiles !== undefined) continue; and skips the rest of the loop iteration before the unstaged block runs. This drops the file from the returned unstaged list even though it has unstaged changes. Removing stagedFiles: [] (so the property is undefined) lets the loop fall through to the unstaged block for mixed-status files.
status: input.status,
- stagedFiles: [],
stagedStats: new Map(),🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/SourceControlPanelService.ts around lines 382-384:
`unstagedFilesFromPorcelainStatus` passes `stagedFiles: []` to `parsePorcelainStatus`. When a file has both staged and unstaged changes (e.g. `MM`, `AM`), the staged block hits `if (input.stagedFiles !== undefined) continue;` and skips the rest of the loop iteration before the unstaged block runs. This drops the file from the returned unstaged list even though it has unstaged changes. Removing `stagedFiles: []` (so the property is `undefined`) lets the loop fall through to the unstaged block for mixed-status files.
| const discardKey = `file-discard:${file.path}`; | ||
| const diffSource = { | ||
| kind: "working-tree", | ||
| staged: file.hasStagedChanges, |
There was a problem hiding this comment.
🟡 Medium source-control/SourceControlPanel.tsx:2222
In renderWorkingFile, the diff source uses staged: file.hasStagedChanges. For a file that has both staged and unstaged edits, hasStagedChanges is true, so expanding the row always requests the staged diff (git diff --cached) and never shows the unstaged portion — even though the row displays aggregate staged + unstaged churn. The inline diff is therefore incomplete and misleading for mixed-state files. Consider using staged: !file.hasUnstagedChanges && file.hasStagedChanges so the unstaged diff is shown when it exists.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/source-control/SourceControlPanel.tsx around line 2222:
In `renderWorkingFile`, the diff source uses `staged: file.hasStagedChanges`. For a file that has both staged and unstaged edits, `hasStagedChanges` is `true`, so expanding the row always requests the staged diff (`git diff --cached`) and never shows the unstaged portion — even though the row displays aggregate staged + unstaged churn. The inline diff is therefore incomplete and misleading for mixed-state files. Consider using `staged: !file.hasUnstagedChanges && file.hasStagedChanges` so the unstaged diff is shown when it exists.
| | { readonly kind: "remote"; readonly name: string }; | ||
|
|
||
| function displayHeadRefs(headRefs: readonly string[]): DisplayHeadRef[] { | ||
| const localRefs = new Set(headRefs.filter((ref) => !ref.includes("/"))); |
There was a problem hiding this comment.
🟡 Medium source-control/SourceControlPanel.tsx:466
displayHeadRefs uses ref.includes("/") to decide whether a ref is local or remote, but local branch names in Git commonly contain slashes (e.g. feature/login). A local branch named feature/login is excluded from localRefs at line 466, then re-parsed as a remote ref at line 469, which truncates the displayed name to login and drops the local/synced state. The slash heuristic cannot reliably distinguish remote_name/branch from a local namespace/branch, so consider having the data source indicate whether each ref is local or remote rather than inferring it from the presence of a slash.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/source-control/SourceControlPanel.tsx around line 466:
`displayHeadRefs` uses `ref.includes("/")` to decide whether a ref is local or remote, but local branch names in Git commonly contain slashes (e.g. `feature/login`). A local branch named `feature/login` is excluded from `localRefs` at line 466, then re-parsed as a remote ref at line 469, which truncates the displayed name to `login` and drops the local/synced state. The slash heuristic cannot reliably distinguish `remote_name/branch` from a local `namespace/branch`, so consider having the data source indicate whether each ref is local or remote rather than inferring it from the presence of a slash.
| return { hasUpstream, aheadCount, behindCount }; | ||
| } | ||
|
|
||
| function panelStatusFromLocal( |
There was a problem hiding this comment.
🟡 Medium sourceControl/SourceControlPanelService.ts:418
panelStatusFromLocal hard-codes aheadOfDefaultCount: 0, so the returned status always reports the branch as 0 commits ahead of the default branch regardless of the actual divergence. This causes the panel to always display 0 ahead of default, hiding the real sync signal. Consider deriving aheadOfDefaultCount from the remote/default-branch status exposed by the workflow layer instead of using a constant.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/SourceControlPanelService.ts around line 418:
`panelStatusFromLocal` hard-codes `aheadOfDefaultCount: 0`, so the returned status always reports the branch as 0 commits ahead of the default branch regardless of the actual divergence. This causes the panel to always display `0 ahead of default`, hiding the real sync signal. Consider deriving `aheadOfDefaultCount` from the remote/default-branch status exposed by the workflow layer instead of using a constant.
| onKeyDown={ | ||
| onFileToggle | ||
| ? (event) => { | ||
| if (event.key !== "Enter" && event.key !== " ") return; | ||
| event.preventDefault(); | ||
| onFileToggle(file); | ||
| } | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
🟡 Medium source-control/SourceControlPanel.tsx:808
The row's onKeyDown handler fires for key events that bubble up from the nested action buttons in RowActions. When a user focuses the Open file or Open in VS Code button and presses Enter/Space, the button action runs and onFileToggle(file) runs, so the row unexpectedly expands/collapses. RowActions already calls event.stopPropagation() for click events but not for keydown. Consider guarding the row handler with event.target === event.currentTarget so it only fires when the row itself is focused.
onKeyDown={
onFileToggle
? (event) => {
+ if (event.target !== event.currentTarget) return;
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
onFileToggle(file);
}
: undefined
}🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/source-control/SourceControlPanel.tsx around lines 808-816:
The row's `onKeyDown` handler fires for key events that bubble up from the nested action buttons in `RowActions`. When a user focuses the `Open file` or `Open in VS Code` button and presses `Enter`/`Space`, the button action runs *and* `onFileToggle(file)` runs, so the row unexpectedly expands/collapses. `RowActions` already calls `event.stopPropagation()` for click events but not for keydown. Consider guarding the row handler with `event.target === event.currentTarget` so it only fires when the row itself is focused.
| return activity !== 0 ? activity : left.name.localeCompare(right.name); | ||
| } | ||
|
|
||
| function avatarUrlForEmail(email: string | null | undefined): string | null { |
There was a problem hiding this comment.
🟡 Medium sourceControl/SourceControlPanelService.ts:625
avatarUrlForEmail constructs a gravatar.com URL containing an MD5 hash of the commit author's email. When the client renders this URL in an <img>, the browser contacts gravatar.com for every commit author, exposing a reversible hash of private contributor emails to a third party with no user opt-in. Consider removing the third-party avatar lookup (returning null) or making it opt-in via configuration so contributor identity isn't leaked outside the app by default.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/SourceControlPanelService.ts around line 625:
`avatarUrlForEmail` constructs a `gravatar.com` URL containing an MD5 hash of the commit author's email. When the client renders this URL in an `<img>`, the browser contacts gravatar.com for every commit author, exposing a reversible hash of private contributor emails to a third party with no user opt-in. Consider removing the third-party avatar lookup (returning `null`) or making it opt-in via configuration so contributor identity isn't leaked outside the app by default.
| await gitAction.run({ | ||
| actionId: newCommandId(), | ||
| action: "commit", | ||
| ...(commitMessage ? { commitMessage } : {}), | ||
| filePaths: [...selectedChangePathList], | ||
| }); |
There was a problem hiding this comment.
🟠 High source-control/SourceControlPanel.tsx:1796
runPanelCommit awaits gitAction.run(...) but never inspects the returned AtomCommandResult. Because useGitStackedAction is configured with reportFailure: false, gitAction.run resolves failures as a value instead of throwing, so a failed commit never enters the catch block in runAction. The dialog closes, the panel refreshes, and no error is surfaced to the user. Consider checking result._tag and throwing on failure so runAction can call setError.
- await gitAction.run({
- actionId: newCommandId(),
- action: "commit",
- ...(commitMessage ? { commitMessage } : {}),
- filePaths: [...selectedChangePathList],
- });
+ const result = await gitAction.run({
+ actionId: newCommandId(),
+ action: "commit",
+ ...(commitMessage ? { commitMessage } : {}),
+ filePaths: [...selectedChangePathList],
+ });
+ if (result._tag !== "Success" && !isAtomCommandInterrupted(result)) {
+ throw squashAtomCommandFailure(result);
+ }Also found in 1 other location(s)
apps/web/src/state/sourceControlPanel.ts:28
unwrapPanelCommandthrows for every non-success result, including interrupt-only failures.@t3tools/client-runtimeexposesisAtomCommandInterrupted()because queries/commands can legitimately end with an interrupt-onlyFailure(for example when the panel unmounts or the environment changes mid-request). In this panel those thrown interrupts are caught byrefresh()/runAction()and surfaced viasetError(...), so cancelling an in-flight refresh/action shows a false source-control error even though nothing actually failed.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/source-control/SourceControlPanel.tsx around lines 1796-1801:
`runPanelCommit` awaits `gitAction.run(...)` but never inspects the returned `AtomCommandResult`. Because `useGitStackedAction` is configured with `reportFailure: false`, `gitAction.run` resolves failures as a value instead of throwing, so a failed commit never enters the `catch` block in `runAction`. The dialog closes, the panel refreshes, and no error is surfaced to the user. Consider checking `result._tag` and throwing on failure so `runAction` can call `setError`.
Also found in 1 other location(s):
- apps/web/src/state/sourceControlPanel.ts:28 -- `unwrapPanelCommand` throws for every non-success result, including interrupt-only failures. `@t3tools/client-runtime` exposes `isAtomCommandInterrupted()` because queries/commands can legitimately end with an interrupt-only `Failure` (for example when the panel unmounts or the environment changes mid-request). In this panel those thrown interrupts are caught by `refresh()`/`runAction()` and surfaced via `setError(...)`, so cancelling an in-flight refresh/action shows a false source-control error even though nothing actually failed.


Summary
Adds a Git-backed Version Control surface to the existing right panel. The surface is scoped to the active project/repository cwd, stays server-authoritative for Git operations, and can be opened from draft or existing conversations once repository context is available.
The feature is intentionally a command center rather than a full Git dashboard: it highlights currently actionable work from the working tree, local branches, stashes, remotes, same-name fork branches, and open change-request bases.
What Changed
SOURCE_CONTROL.mdto document the shipped Version Control panel behavior, entry points, layout, operations, error handling, and validation expectations.SourceControlPanelServiceon the server for panel snapshots, branch/stash/commit/compare data, selected-file commit and stash flows, discard operations, branch operations, remote operations, and provider-derived PR/MR base checks.VcsStatusBroadcasterwith ref-counted filesystem watching, ignored.git/events, debounce/fingerprint suppression, gitignore-aware filtering, and a factoredVcsLocalWatchhelper.ActionableandRemotessections, working-tree file selection, inline diffs, branch/stash/commit rows, context menus, guarded destructive actions, manual fetch controls, and a conservative five-minuteActionableremote fetch.Actionablesection flow while keeping lazy row enrichment viaIntersectionObserver, avoiding a nested virtualized scroll area inside the panel.git worktree list --porcelainfirst, falling back togit branch --formatworktree placeholders when needed, and falling back safely on older Git versions that do not support the placeholder.Why
This is an attempt at a version control center for T3 Code. Rather than building a full Git-based dashboard, it acts more as a command center that gives the user information on what is currently actionable based on their working tree and local branches versus existing remotes.
That keeps the surface focused on decisions users can take now: committing or stashing selected work, seeing branches that need sync or publication, inspecting stashes, and noticing when an open PR/MR base has moved ahead. The mounted periodic fetch keeps those remote-derived branch states fresh without requiring users to manually refresh or blur/refocus the window, while avoiding a high-frequency network/Git loop.
Validation
pnpm exec vp test run apps/server/src/sourceControl/SourceControlPanelService.test.ts apps/server/src/vcs/VcsStatusBroadcaster.test.ts apps/server/src/vcs/GitVcsDriverCore.test.tspassed on 2026-06-26: 3 files, 69 tests.pnpm exec vp test run apps/web/src/components/source-control/SourceControlPanel.logic.test.ts apps/web/src/state/sourceControlPanel.test.tspassed on 2026-06-26: 2 files, 10 tests.pnpm exec vp test run packages/contracts/src/git.test.ts packages/shared/src/git.test.ts packages/client-runtime/src/state/vcsAction.test.tspassed on 2026-06-26: 3 files, 28 tests.pnpm exec vp checkpassed on 2026-06-26 with 0 errors and 20 existing warnings in unrelated web/mobile files.pnpm exec vp run typecheckpassed on 2026-06-26.Proof
Note
High Risk
Introduces a large server-owned Git mutation and provider-query layer; incorrect upstream/publish or discard logic could affect user repositories.
Overview
This diff adds
SOURCE_CONTROL.mdas the canonical spec for the Git-backed Version Control right-panel surface (layout, actionable rows, server RPC model, validation commands).Server: Introduces
SourceControlPanelService(~2.2k lines) as the authoritative backend for panel snapshots and mutations—working-tree commit/stash/discard (including rename paths and partitioned discard), branch fetch/pull/push/publish with upstream-vs-compare-base rules, lazy untracked enrichment, actionable same-name fork and open PR/MR base detection, worktree path fallbacks for older Git, andsanitizeErrorCauseon wrappedGitCommandErrorcauses.Providers & desktop: GitHub, GitLab, and Azure DevOps adapters now pass remote-derived
repository(and Azureproject) into CLI list calls so change requests resolve per remote; errors route throughsourceControlProviderErrorwith bounded causes (tests updated). Electron context menus gain explicitseparatoritem handling with deduplication.Tests: Adds
SourceControlPanelService.test.tscovering history ranges, discard edge cases, publish targets, renames, fork/PR actionable rows, and invalid branch names.Reviewed by Cursor Bugbot for commit ea9bf9b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Version Control panel to the right panel with full VCS operations
gitCwd); the panel lazy-loadsSourceControlPanelvia Suspense.SourceControlPanelServiceon the server with a comprehensive set of VCS operations (snapshot, branch details, staging/unstaging/discarding, commit, pull/push, stash, merge/rebase, compare, remote management) exposed as typed WebSocket RPCs.vcs.tswith 5-second caching for queries and auseSourceControlPanelApihook that force-refreshes cached queries and throws failures to callers.listChangeRequestscalls to the repository/project derived from the remote URL context; normalizes provider errors to a sanitized, bounded shape viasourceControlProviderError.VcsStatusBroadcaster: debounces fs events, filters.gitand git-ignored paths, and triggers a git status refresh on relevant changes.Macroscope summarized ea9bf9b.