Skip to content

Add version control command center#3177

Open
Quicksaver wants to merge 32 commits into
pingdotgg:mainfrom
Quicksaver:split/version-control-panel-work
Open

Add version control command center#3177
Quicksaver wants to merge 32 commits into
pingdotgg:mainfrom
Quicksaver:split/version-control-panel-work

Conversation

@Quicksaver

@Quicksaver Quicksaver commented Jun 19, 2026

Copy link
Copy Markdown

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

  • Added SOURCE_CONTROL.md to document the shipped Version Control panel behavior, entry points, layout, operations, error handling, and validation expectations.
  • Added SourceControlPanelService on 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.
  • Extended GitHub, GitLab, Azure DevOps, and Bitbucket provider plumbing so the panel can detect actionable branches relative to open PR/MR base branches while preserving sanitized provider error context.
  • Updated VcsStatusBroadcaster with ref-counted filesystem watching, ignored .git/ events, debounce/fingerprint suppression, gitignore-aware filtering, and a factored VcsLocalWatch helper.
  • Added the React Version Control panel UI in the right panel with Actionable and Remotes sections, working-tree file selection, inline diffs, branch/stash/commit rows, context menus, guarded destructive actions, manual fetch controls, and a conservative five-minute Actionable remote fetch.
  • Reworked working-tree file rows to render in the existing Actionable section flow while keeping lazy row enrichment via IntersectionObserver, avoiding a nested virtualized scroll area inside the panel.
  • Hardened local branch worktree detection by reading git worktree list --porcelain first, falling back to git branch --format worktree placeholders when needed, and falling back safely on older Git versions that do not support the placeholder.
  • Added environment-scoped client presentation state and source-control panel state so selection, expansion, and working-tree enrichment follow the active project/repository context.
  • Extended shared RPC, IPC, Git, settings, client-runtime, and VCS contracts for the new source-control snapshot and operation protocol.
  • Added native context-menu separator support for desktop menus and the web fallback so source-control destructive actions can be grouped consistently.
  • Added focused tests for provider change-request base detection, source-control service behavior, VCS broadcaster behavior, source-control panel state, shared Git contracts, and client-runtime VCS action helpers.
  • Hardened review-discovered edge cases around default compare refs, non-current diverged branch merge sync, selected-file discard failures, fallback rename parsing, aggregate working-tree stats, relative date formatting, source-control metadata update sequencing, and source-control error cause sanitization.

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.ts passed 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.ts passed 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.ts passed on 2026-06-26: 3 files, 28 tests.
  • pnpm exec vp check passed on 2026-06-26 with 0 errors and 20 existing warnings in unrelated web/mobile files.
  • pnpm exec vp run typecheck passed on 2026-06-26.

Proof

Screenshot 2026-06-16 at 19 26 39

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.md as 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, and sanitizeErrorCause on wrapped GitCommandError causes.

Providers & desktop: GitHub, GitLab, and Azure DevOps adapters now pass remote-derived repository (and Azure project) into CLI list calls so change requests resolve per remote; errors route through sourceControlProviderError with bounded causes (tests updated). Electron context menus gain explicit separator item handling with deduplication.

Tests: Adds SourceControlPanelService.test.ts covering 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

  • Adds a new 'source-control' right panel surface that is only shown when the active thread has a Git repository (gitCwd); the panel lazy-loads SourceControlPanel via Suspense.
  • Introduces SourceControlPanelService on 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.
  • Adds VCS panel atoms in vcs.ts with 5-second caching for queries and a useSourceControlPanelApi hook that force-refreshes cached queries and throws failures to callers.
  • Scopes GitHub, GitLab, and Azure DevOps listChangeRequests calls to the repository/project derived from the remote URL context; normalizes provider errors to a sanitized, bounded shape via sourceControlProviderError.
  • Adds local filesystem watching in VcsStatusBroadcaster: debounces fs events, filters .git and git-ignored paths, and triggers a git status refresh on relevant changes.
  • Changes the default automatic git fetch interval from 30 seconds to 5 minutes.
  • Risk: the fetch interval change is a behavioral default change that affects all projects using automatic git fetch.

Macroscope summarized ea9bf9b.

- 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
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

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: da452307-c8a3-423b-949b-bc49bcd7b011

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

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.

❤️ Share

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 19, 2026
Comment thread apps/server/src/sourceControl/SourceControlPanelService.ts
Comment thread apps/web/src/components/source-control/SourceControlPanel.tsx
Comment thread apps/server/src/sourceControl/SourceControlPanelService.ts
Comment thread apps/web/src/components/source-control/SourceControlPanel.tsx Outdated
Comment thread apps/web/src/components/source-control/SourceControlPanel.tsx Outdated
Comment thread apps/server/src/sourceControl/SourceControlPanelService.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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
Comment thread apps/vscode-extension/scripts/package-dependencies.mjs Outdated
Comment thread apps/vscode-extension/src/extension.ts Outdated
Comment thread apps/vscode-extension/scripts/package.mjs Outdated
Comment thread apps/web/src/session-logic.ts Outdated
Comment thread apps/server/src/vscodeWorkspaceBootstrap/http.ts Outdated
Comment thread apps/web/src/session-logic.ts Outdated
Comment thread apps/vscode-extension/src/backendManager.ts Outdated
Comment thread apps/vscode-extension/src/webview.ts Outdated
Comment thread apps/server/src/sourceControl/SourceControlPanelService.ts
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

Effect Service Conventions: one finding in DesktopBackendManager.ts. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/lib/providerWorkspaceSkillsState.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
Comment thread apps/web/src/lib/providerWorkspaceSkillsState.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
}, [key, query, target.fallbackSkills]);

const previousWorkspaceSkillsRef = useRef<ProviderWorkspaceSkillsSnapshot | null>(null);
const querySkills = query.data?.skills ?? 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.

🟡 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.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Partial cherry-pick of fa101ad, 1c6f66b, and 4b17fa4 source-control service changes using the local main resolution. Unrelated chat, provider-skill, terminal, and CUSTOMIZED.md changes were dropped.
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
@Quicksaver Quicksaver force-pushed the split/version-control-panel-work branch from 7421816 to ea9bf9b Compare June 26, 2026 06:34

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

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

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ea9bf9b. Configure here.

return { project: sshProject, repository: sshRepository };
}
const repository = parts.at(-1);
return repository ? { repository } : undefined;

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ea9bf9b. Configure here.

Effect.ignoreCause({ log: true }),
);

const retainLocalWatcher = Effect.fn("VcsStatusBroadcaster.retainLocalWatcher")(function* (

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/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({

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 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)) {

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 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.

Comment on lines +382 to +384
status: input.status,
stagedFiles: [],
stagedStats: new Map(),

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 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,

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 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("/")));

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 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(

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 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.

Comment on lines +808 to +816
onKeyDown={
onFileToggle
? (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
onFileToggle(file);
}
: undefined
}

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 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 {

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 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.

Comment on lines +1796 to +1801
await gitAction.run({
actionId: newCommandId(),
action: "commit",
...(commitMessage ? { commitMessage } : {}),
filePaths: [...selectedChangePathList],
});

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 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

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.

🤖 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.

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:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant