feat: Add VS Code-style Source Control panel with AI commit messages#44
feat: Add VS Code-style Source Control panel with AI commit messages#44enesteve0 wants to merge 7 commits into
Conversation
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read and agree to the CLA, then let us know here. |
|
I have read and agree to the CLA. |
There was a problem hiding this comment.
Pull request overview
Adds a VS Code-style Source Control (SCM) view to the app’s navigator, backed by new main-process git IPC handlers and optional AI-powered commit message generation (CLI-first with API-key fallback). Also introduces an AI Settings pane for configuring the fallback API key.
Changes:
- Add new
scmnavigator mode that renders a full Source Control sidebar (status, staging, commit, sync, branch picker, inline diff). - Implement git IPC surface area (status/stage/unstage/discard/commit/diff, sync, branches, stash, show-file) in the main process and expose it via preload + shared window API typings.
- Add AI commit message generation via installed CLI agents, with an Anthropic API fallback + Settings UI for validating/saving an API key.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| collab-electron/src/windows/settings/src/App.tsx | Adds an “AI” settings pane for API key validation/storage. |
| collab-electron/src/windows/nav/src/App.tsx | Adds scm as a third navigator tab and renders the SCM view. |
| collab-electron/src/preload/universal.ts | Exposes new git:* and ai:* IPC APIs to renderer. |
| collab-electron/src/main/ipc.ts | Registers IPC handlers for git operations + AI commit generation. |
| collab-electron/src/main/integrations.ts | Exports agentDetected for reuse by AI commit generation. |
| collab-electron/src/main/index.ts | Prevents renderer pref:get reads of ai.apiKey. |
| collab-electron/src/main/git-source-control.ts | Implements git operations via execFile("git", ...). |
| collab-electron/src/main/ai-commit.ts | Implements commit-message generation via CLI agents or Anthropic API. |
| collab-electron/packages/shared/src/window-api.d.ts | Extends window.api typing for git/AI features. |
| collab-electron/packages/shared/src/git-types.ts | Defines shared git status/branch/remote/stash types. |
| collab-electron/packages/components/src/SourceControl/SourceControlView.tsx | Main SCM UI view (status, staging, commit, sync, branch picker, diff). |
| collab-electron/packages/components/src/SourceControl/CommitBox.tsx | Commit message input + commit + AI-generate controls. |
| collab-electron/packages/components/src/SourceControl/ChangeSectionHeader.tsx | Collapsible section header for staged/unstaged lists. |
| collab-electron/packages/components/src/SourceControl/FileChangeRow.tsx | File row UI with stage/unstage/discard actions. |
| collab-electron/packages/components/src/SourceControl/SyncBar.tsx | Push/pull/sync/publish controls with ahead/behind badges. |
| collab-electron/packages/components/src/SourceControl/BranchPicker.tsx | Branch switch/create/delete dropdown UI. |
| collab-electron/packages/components/src/SourceControl/DiffView.tsx | Inline diff viewer for a selected file. |
| collab-electron/packages/components/src/SourceControl/StashSection.tsx | Stash UI section component (added, not wired into main view here). |
| collab-electron/packages/components/src/SourceControl/SourceControl.css | Styling for the SCM panel components. |
| collab-electron/packages/components/src/SourceControl/index.ts | Re-exports SourceControlView. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| api.getPref("ai.hasKey").then(() => { | ||
| // We can't read the key (guarded), so check via aiHasKey | ||
| (window as unknown as { api: { aiHasKey: () => Promise<boolean> } }).api | ||
| .aiHasKey() | ||
| .then((has: boolean) => { | ||
| setHasExistingKey(has); | ||
| }) | ||
| .catch(() => {}); | ||
| }).catch(() => {}); |
There was a problem hiding this comment.
AiPane calls window.api.aiHasKey() but no aiHasKey API is exposed in the preload typings/implementation, and api.getPref("ai.hasKey") appears to reference a pref key that doesn’t exist. As written this will throw at runtime and hasExistingKey will never be set. Add an explicit IPC/preload API (e.g., ai:has-key) that returns a boolean based on the stored pref, and use that directly here (or remove the unused ai.hasKey pref lookup).
| api.getPref("ai.hasKey").then(() => { | |
| // We can't read the key (guarded), so check via aiHasKey | |
| (window as unknown as { api: { aiHasKey: () => Promise<boolean> } }).api | |
| .aiHasKey() | |
| .then((has: boolean) => { | |
| setHasExistingKey(has); | |
| }) | |
| .catch(() => {}); | |
| }).catch(() => {}); | |
| api | |
| .getPref("ai.apiKey") | |
| .then((storedKey: unknown) => { | |
| if (typeof storedKey === "string" && storedKey.trim() !== "") { | |
| setHasExistingKey(true); | |
| } else { | |
| setHasExistingKey(false); | |
| } | |
| }) | |
| .catch(() => { | |
| setHasExistingKey(false); | |
| }); |
| const handleCommit = useCallback(async () => { | ||
| if (!commitMessage.trim()) return; | ||
| setCommitting(true); | ||
| setError(null); | ||
| setSuccess(null); | ||
| try { | ||
| // Auto-stage all if nothing is staged (VS Code behavior) | ||
| if (!status || status.staged.length === 0) { | ||
| await window.api.gitStageAll(); | ||
| } |
There was a problem hiding this comment.
handleCommit closes over status but its useCallback dependency list doesn’t include status. This can lead to stale state (e.g., auto-staging all even when there are staged changes, or using an old branch state) depending on when the callback was created. Add status (and any other referenced state) to the dependency array or refactor to read the latest status inside the callback.
| const parts = line.split("|"); | ||
| const name = parts[0]!.trim(); | ||
| const isCurrent = parts[1]?.trim() === "*"; | ||
| const upstream = parts[2]?.trim() || undefined; | ||
| const isRemote = name.startsWith("origin/") || name.includes("/"); | ||
|
|
||
| // Skip HEAD pointers like "origin/HEAD" | ||
| if (name.endsWith("/HEAD")) continue; | ||
|
|
||
| branches.push({ name, current: isCurrent, upstream, isRemote }); |
There was a problem hiding this comment.
gitBranches determines isRemote with name.startsWith("origin/") || name.includes("/"), which misclassifies local branches that contain slashes (e.g. feature/foo) as remote. This breaks grouping in the branch picker and prevents local-only actions like delete. Derive isRemote from the actual ref namespace returned by git branch -a (e.g., refs under remotes/) or use a more precise prefix check than includes("/").
| for (const line of statusRaw.split("\n")) { | ||
| if (!line) continue; | ||
| const status = line.slice(0, 2); | ||
| const filePath = line.slice(3); | ||
| if (status === "??") { | ||
| untrackedPaths.push(filePath); | ||
| } else { | ||
| trackedPaths.push(filePath); | ||
| } |
There was a problem hiding this comment.
gitDiscard parses git status --porcelain output with const filePath = line.slice(3). For renames, porcelain uses the old -> new format, so filePath becomes old -> new and later git checkout -- ... will fail. Consider using git status -z for unambiguous parsing, or explicitly handle the rename arrow format (extract the destination path) before building trackedPaths/untrackedPaths.
| const raw = await git( | ||
| ["status", "--porcelain=v2", "--branch", "-uall"], | ||
| cwd, | ||
| ); |
There was a problem hiding this comment.
gitStatus parses git status --porcelain=v2 --branch by splitting on spaces / tabs, but without -z the porcelain format can C-quote/escape paths (depending on core.quotePath) and paths with spaces/special chars won’t round-trip correctly into later git commands. Using --porcelain=v2 -z (and parsing NUL-delimited records) avoids quoting/escaping edge cases and makes rename parsing more reliable.
| result = await runCliAgent("claude", ["-p", fullPrompt], ""); | ||
| break; | ||
| case "codex": | ||
| result = await runCliAgent( | ||
| "codex", | ||
| ["--quiet", "--prompt", fullPrompt], | ||
| "", | ||
| ); | ||
| break; | ||
| case "gemini": | ||
| result = await runCliAgent( | ||
| "gemini", | ||
| ["--prompt", fullPrompt], | ||
| "", | ||
| ); |
There was a problem hiding this comment.
generateCommitMessageViaCli passes the entire fullPrompt (which includes up to 100k chars of diff) as a command-line argument (claude -p <prompt>, codex --prompt <prompt>, gemini --prompt <prompt>). This is very likely to exceed OS argv limits (notably Windows’ ~32k limit) and will fail for moderately sized diffs. Prefer sending the prompt via stdin (or a temp file) if the CLI supports it, and keep argv args small.
| result = await runCliAgent("claude", ["-p", fullPrompt], ""); | |
| break; | |
| case "codex": | |
| result = await runCliAgent( | |
| "codex", | |
| ["--quiet", "--prompt", fullPrompt], | |
| "", | |
| ); | |
| break; | |
| case "gemini": | |
| result = await runCliAgent( | |
| "gemini", | |
| ["--prompt", fullPrompt], | |
| "", | |
| ); | |
| // Send the full prompt via stdin instead of as a command-line argument | |
| result = await runCliAgent("claude", [], fullPrompt); | |
| break; | |
| case "codex": | |
| // Keep non-prompt flags small and pass the prompt through stdin | |
| result = await runCliAgent("codex", ["--quiet"], fullPrompt); | |
| break; | |
| case "gemini": | |
| // Pass the prompt through stdin to avoid oversized argv entries | |
| result = await runCliAgent("gemini", [], fullPrompt); |
Code reviewFound 7 issues:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
|
Hey, human here. This is a great PR; thanks so much for contributing. I'll work to fix up the issues that the agent identified, as well and syncing the styling to our internal system. Sorry about the CLA confusion, we're still figuring this part of the process out. The bot might need you to comment again with a specific message. @cla-bot check |
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read the CLA, then sign by posting the following comment on this PR:
After signing, comment |
|
The cla-bot has been summoned, and re-checked this pull request! |
Thank you so much, it really means a lot. I was already enjoying the project, and the main thing I personally felt was missing was being able to commit, push, and handle git workflow directly in the app, so I decided to take a shot at adding it. I’m really glad it seems helpful. And no worries about the CLA mix-up — happy to resend the exact comment if needed. |
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read the CLA, then sign by posting the following comment on this PR:
After signing, comment |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
I have read the Contributor License Agreement (CLA) and hereby sign the CLA. |
|
@cla-bot check |
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read the CLA, then sign by posting the following comment on this PR:
After signing, comment |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
Don't worry about 'signing' again—I gotta debug this and will manually add you to the .clabot record |
|
@cla-bot check |
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read the CLA, then sign by posting the following comment on this PR:
After signing, comment |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
test comment |
|
@cla-bot check |
|
Thanks for the pull request! It looks like @enesteve0 haven't signed our Contributor License Agreement yet. Please read the CLA, then sign by posting the following comment on this PR:
After signing, comment |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
@cla-bot check |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
Would be nice to have photos in the description of what the pane looks like :)) excited for this PR |
- Added ChangeSectionHeader component for displaying section headers in the source control panel. - Introduced CommitBox component for handling commit messages and actions. - Created FileChangeRow component to represent individual file changes with actions for staging, unstaging, and discarding. - Developed SourceControlView component to manage the overall source control interface, including file status and commit actions. - Implemented git source control logic with functions for staging, unstaging, committing, and discarding changes. - Integrated AI commit message generation functionality with API interaction for generating commit messages based on diffs. - Added CSS styles for the new components and overall layout of the source control panel. - Defined GitChangeStatus and related types for managing file change states.
…allback API key support
- Introduced GitBranch, GitRemote, and GitStash interfaces in git-types.ts. - Expanded CollabApi interface in window-api.d.ts to include methods for Git operations: push, pull, fetch, branch management, and stash operations. - Implemented gitPush, gitPull, gitFetch, gitBranches, gitCheckout, gitCreateBranch, gitDeleteBranch, gitStashSave, gitStashList, gitStashPop, gitStashApply, gitStashDrop, and gitShowFile functions in git-source-control.ts. - Registered IPC handlers for new Git operations in ipc.ts. - Exposed Git API methods in preload script for renderer access. - Created BranchPicker component for branch selection and management. - Developed DiffView component for displaying file diffs. - Added StashSection component for managing stashes. - Implemented SyncBar component for synchronizing branches with remote.
…oving diff retrieval
2eb884f to
69ddf3e
Compare
Capture VS Code-style Source Control UI via Playwright harness and real git fixtures; include regen scripts and dirty-worktree fixture for overview states. Co-authored-by: Cursor <cursoragent@cursor.com>
…ag management - Updated BranchPicker to support Git tags alongside branches. - Modified CommitBox to include options for amending and signing commits. - Enhanced SyncBar to manage multiple remotes and streamline push/pull operations. - Refactored various components to utilize workspacePath for Git operations. - Improved styling and layout for better user experience in the source control panel.
|
@theblondealex — thanks for the suggestion!
Here are dark-mode screenshots of the Source Control panel (also in the PR description). Captured from production SCM components with real SCM overview & commit
Branches, remotes & conflicts
Rebase, stash, diff & settings
|
|
Hey @yiliush I’d love to keep contributing more consistently to the SCM/Git area in this repo. I’ll first finish addressing the feedback on this PR and make sure it’s aligned with the project’s direction. After that, I’d be happy to help with follow-up fixes, testing/fixtures, issue triage, and reviewing SCM-related PRs where useful. If you’re open to it, what would be the most helpful next area for me to take on after this PR? |










Summary
VS Code–style Source Control in the navigator SCM tab: workspace-scoped git IPC, stage/commit/sync, branches/tags/stashes, merge conflicts, interactive rebase, submodules, LFS badges, and AI commit messages.
Phase 0 — Foundation
git:*IPC/preload/API calls takeworkspacePathfor multi-workspace reposrepoState(clean, merging, rebasing, interactive-rebase, cherry-picking, reverting), ahead/behind, selected remotePhase 2 — Advanced SCM
.gitignore.gitmodules; Updatecommit.gpgsignset; Settings → Git read-onlyuser.name,email,credential.helper.gitattributesfilterArchitecture
git-source-control.ts— porcelain v2 (-z), merge/rebase/submodule/LFS helpersipc-git.ts— workspace path resolution for all handlerspackages/components/src/SourceControl/QA fixtures
Tracked scripts under
fixtures/git/; run./fixtures/git/setup-all.shthen open generated workspaces:fixtures/git/dirty-worktreefixtures/git/merge-conflictfixtures/git/submodulefixtures/git/rebase-todobreakManual checklist:
collab-electron/docs/SCM_TEST_MATRIX.md(Phases A–I). Automated fixture results:collab-electron/docs/screenshots/scm/QA_RESULTS.md.Screenshots
Dark mode captures from production SCM components via a Vite + Playwright harness, with a local git bridge calling
git-source-control.tsagainst realfixtures/git/repos (not mocked status). Regenerate:collab-electron/docs/screenshots/scm/README.md.Test plan
fixtures/git/merge-conflict: Merge Changes; commit blocked until resolvedfixtures/git/rebase-todo: interactive rebase panel; Continue / Abortfixtures/git/submodule: Submodules section; Updatenpm run buildincollab-electronpassesbun test src/main/git-source-control.fixture.test.ts(see QA_RESULTS.md)