Skip to content

Latest commit

 

History

History
297 lines (166 loc) · 80.9 KB

SOURCE_CONTROL.md

File metadata and controls

297 lines (166 loc) · 80.9 KB

Version Control Panel

This main integration combines source c6661f493a1998db1d72f316d5f71a3e4eec9dcf with Magi, archive/storage and preview customizations above pinned base 348f217a853dd4a23d7fa4834dedb05f545c2075. The source-branch evidence below is historical; final-main checks and runtime ownership are recorded in the rebuild receipt outside the repository. Main retains separate inferred core, Magi and panel handler layers, both settings patches, tuple cwd/file identities, session-only Magi run reveals and persisted panel maximization.

This branch adds Git-backed Version Control surfaces in the web right panel and the native mobile app. Each per-environment panel instance is scoped to one environment and repository cwd, uses server-owned Git operations, and reuses the existing VCS status, source-control provider, and WebSocket RPC infrastructure rather than shelling out from client UI code. On web, the right-panel surface federates those instances for every connected environment containing a project in the active logical repository group, so the current checkout, sibling worktrees, actionable branches, remotes, stashes, and their operations remain visible together without moving Git execution into the browser.

The panel does not require an existing provider session or started server thread. Draft/new conversations can open Version Control as soon as they have project context and a repository cwd. The panel and composer share live Git status, falling back to the checkout status remembered for that environment during the current session and assuming Git only for an unseen checkout. A known non-Git checkout disables the action and hides any retained Source Control surface, including while its status reloads. Thread metadata updates caused by branch switching or detached checkout are routed by ChatView through ChatView.sourceControl: server threads persist through the V2 thread.metadata.update command, while draft conversations update local draft thread context. That command has no expected-branch compare-and-swap, so the client keeps no branch-observation guard: server-thread writes are serialized per thread and only the newest request enqueued for a thread applies. A superseded request skips its write, and a request that finishes after a newer one was enqueued resolves as stale, so neither its success nor its failure changes the thread's metadata error. Server-thread metadata failures surface in the chat error banner through source-control-specific per-thread state. Local server-thread errors take priority over source-control metadata errors, which take priority over persisted provider-session errors; draft errors remain separate. Dismissal clears only the visible local or source-control error, while persisted provider-session errors are masked for the current UI session. This source-aware handling preserves any lower-priority error for subsequent presentation. Queue sequencing remains monotonic for the hook lifetime, so overlapping checkouts reach the latest branch without allowing a stale in-flight failure to overwrite newer state even if a thread is closed and reopened with the same key.

Performance and freshness

Peer synchronization after a push uses independent snapshot reads for its eligibility checks. A shared query with refresh: true may join a snapshot already in flight before the peer fetch and return its old ahead/behind counts. Do not use that shared query for the post-fetch decision to pull; readPanelSnapshot starts a separate RPC without cancelling overview reads.

Web and desktop file and commit lists share their section's existing scroll container through SourceControlVirtualList.tsx. Keep each list at its full content height; a bounded list viewport creates a second scrollbar inside Actionable or Remotes. TanStack Virtual measures expanded rows and tracks each list's offset within the section, including nested commit file lists. Keep the same virtualizer mounted as history grows: switching between plain and virtual lists at a row-count threshold can reset the outer scroll position when an expanded commit contains thousands of files. A virtualizer also scrolls its element to its initial offset when it attaches, so each list attaches after mount and starts from the section's current scroll position; otherwise expanding a branch's history, a commit, or a change list jumps the shared section back to the top. The component opts out of React Compiler memoization because the virtualizer exposes a mutable instance.

The initial web panel load, window focus/visibility refresh, explicit Git action completion, pull-to-refresh, and manual refresh use an authoritative full snapshot. Mobile focus starts with a working-tree refresh so a process-cached structural snapshot can render without rereading branches, remotes, stashes, and provider data; a missing cache or changed repository identity still falls back to full. VCS watcher updates also use a working-tree refresh: the server reloads status plus current and sibling worktree change groups, then merges those slices into the last full snapshot for that cwd. The server runtime owns one SourceControlPanelService shared by every WebSocket session, so snapshot reuse and in-flight fetch de-duplication apply across connected clients instead of being reset for each connection. If branch identity, repository/remote availability, upstream state, ahead/behind counts, provider metadata, or change-request status differs from the cached snapshot, the server automatically performs a full snapshot instead. A working-tree refresh that starts while a full snapshot is in flight is promoted to a full snapshot, so a newer watcher request cannot reuse pre-mutation branch, remote, fork, or stash data and supersede the authoritative refresh. Full-snapshot completion advances the per-cwd barrier on success, failure, or interruption, so a failed newer request does not leave later working-tree refreshes permanently promoted. Snapshot requests use one globally monotonic generation counter while their lookup maps remain bounded per cwd, so evicting and later revisiting a cwd cannot reuse the generation of an older request that is still completing.

Full snapshots reuse the sidebar's automatic per-branch PR discovery with at most four concurrent lookups. The shared GitManager cache reuses successful results, coalesces in-flight requests, and backs off failures. Older PRs remain discoverable regardless of their position in the repository's open PR list. Automatic GitHub discovery uses the shared API repository locator, including a configured GitHub default repository and fork-parent selection, rather than forcing the origin push repository. Repository-wide panel history uses the explicitly selected remote through @t3tools/source-control-github/server/GitHubSourceControlProvider and its shared GraphQL client; commit avatars use the same provider's authenticated REST client. All source-control providers live in upstream's provider packages and receive server settings and Git/process operations through SourceControlHost. The panel imports the provider port and shared cooldown from @t3tools/source-control-core; it does not restore the removed server modules. These reads retain the provider context's API host, including explicit non-default ports, and inherit upstream token selection and rate budgets. The legacy GitHubCli modules are removed. Other providers retain their existing repository selection. GitCafe repository-wide listing accepts an omitted head selector and preserves pagination and limits; supplied head or source identity still narrows results. Forgejo repository-wide listing omits the head query when no selector is supplied; a selected branch still uses its escaped head query and source owner/repository filtering across pages. Forgejo and GitCafe keep commit-avatar fallbacks and do not add opt-in avatar settings.

The provider port in packages/source-control-core/src/server/SourceControlProvider.ts uses upstream's open branded SourceControlProviderKind; typed contexts, provider registry fixture keys and typed matcher values use SourceControlProviderKind.make while ordinary value-only expectations remain literals. Panel avatar settings narrow that identity only to the four configured avatar providers. listChangeRequests.headSelector is optional for repository-wide panel reads; required selectors on helper/create flows do not change that listing contract.

Forgejo listing coverage checks omitted head, escaped owner-qualified heads, explicit source owner/repository filtering, pagination and post-filter limits. Each case states its expected outgoing head independently of the returned count. The GitCafe provider directly requests credentials as well as its API service, so its listing fixture exposes both with Layer.provideMerge(GitCafeCredentials.layer); hiding credentials with provide would prevent those behavioral assertions from executing.

Cancelling a shared Git lookup must notify every waiting request and allow subsequent lookups to succeed. The cancellation notification guard is supplied by the incoming upstream Effect dependency setup; it is not a separate panel runtime shim or branch-specific dependency pin. apps/server/src/vcs/VcsCacheInterruption.test.ts retains the panel regression coverage for interrupted consumers and subsequent successful cache reuse.

Automatic all-remotes fetches preserve the panel's complete remote intent, including secondary remotes. Regular VCS status fetches refresh only the current branch's tracked remote, so a branch tracking origin/main still needs the panel fetch to discover new commits on upstream/main. Panel fetches are keyed by the repository's Git common directory, share in-flight work across worktrees/panel instances, and reuse a successful fetch for the configured interval, five minutes under the balanced preset. Mount, focus, and periodic refresh paths pass through this freshness check, so returning to a window obtains current remote refs when the coordinated fetch is stale without producing focus storms. A failed fetch is retained only for a short cooldown. Explicit Fetch buttons set force, invalidate the freshness entry, and execute a new all-remotes fetch; this keeps manual freshness guarantees while preventing automatic callers from starting duplicate git fetch --all processes.

The panel is intentionally an overview and high-level workflow surface. It focuses on current work, branch sync state, remotes, stashes, selected-file commit/stash flows, and compact branch/commit inspection. It is not a full VS Code SCM replacement and does not implement hunk-level staging.

Source control settings

Host credential forms and icons come from the upstream host definitions in SourceControlSettings.tsx; credentials use sourceControlHosts. GitHub retains its saved-token form even without gh and its authenticated account controls with the discovered auth state. Commit-avatar options remain separate in sourceControl.providers, are disabled by default, and are available only for GitHub, GitLab, Azure DevOps and Bitbucket. A discovered host with neither a credential form nor an avatar option has no empty expandable details row. The retired branch-local host icon table, Bitbucket credential component and credential search target are redundant; do not reintroduce them.

Panel commit and stash generation inherits the registered sibling project settings, or the main project settings for an unregistered sibling. The main-project lookup uses NUL-delimited worktree output where supported and retries the line form on Git before 2.36. Project-action shortcut saves use the destination environment's saved binding and settings grant in both web and Electron, and recheck the grant after saving the project.

Settings search catalogs the individual Source Control controls in settingsSearch.ts. The writing-style, change-request-template, and writer-model controls use searchableSetting for both their visible title and row anchor, so search results navigate directly to the rendered setting without duplicating labels. Current-upstream and panel all-remotes Git intervals plus provider commit-avatar controls are discovery-dependent and live inside collapsible VCS/provider rows; their search results target the stable source-control section instead, so navigation remains valid when a provider is absent or its details are collapsed.

Entry points and host behavior

Version Control is a singleton right-panel surface with kind source-control. Users open it from the existing right-panel surface picker or from the thread details panel, whose Changes row is a split control: the row keeps opening the Diff surface, and its dropdown offers Changes or Version Control. It is not duplicated into the main chat header, project sidebar, or conversation timeline. Availability is project/repository based: the surface is enabled when a thread or draft-thread ref exists for right-panel state and the active project resolves to a repository cwd.

On mobile, Version Control is a native full-screen route over the active thread. It is available directly in the Git header menu beside the quick commit/push action and from the existing Git overview menu that contains commit and branch/worktree actions. Returning from the route closes it without changing the active workspace/thread selection. Both the Version Control route and its dedicated diff route are workspace overlays, so opening a file diff does not replace the Home/Thread path used by the adaptive workspace layout. Publishing an unpublished branch prompts for its destination when the repository has multiple remotes instead of silently preferring origin. Remote choices require source-control write access and an idle mutation; Cancel requires only an idle mutation, so revoking write access does not trap the open dialog.

Version Control uses the shared right-panel tab shell, including the standard context menu and middle-click tab close behavior. The maximized layout is persisted per thread by rightPanelStore, so Source Control remains maximized after switching threads or reloading. ChatView reads that state after normalizing Source Control availability for live and retained presence. apps/web/src/components/rightPanelSurfaceActions.ts is the canonical add-surface policy for availability, activation callbacks, shortcuts, empty-state and compact-menu order, and disabled presentation (short hints in the empty-state launcher, full reasons in the compact menu); RightPanelTabs.tsx derives both launcher action lists together from one memoized snapshot of those inputs and uses the same actions for keyboard activation. Version Control coexists with the shell's multi-tab pull-request surfaces: normalizeSourceControlRightPanelPresence in ChatView.sourceControl.ts applies repository availability to live and retained presence through the same path, removes only an unavailable source-control surface, retains pull-request and File tabs, and selects the first remaining visible tab when Source Control had been active. Version Control uses shortcut V and sits directly after Diff in both the empty-panel launcher and the compact add-surface menu; pull requests retain shortcut P and their existing placement.

Background preview mini-player lifecycle changes and right-panel browser-surface reconciliation remain orthogonal to an active singleton Source Control surface. Browser session/runtime-tab resolution, desktop overlay/audio state, and mute-menu behavior live in apps/web/src/components/rightPanelBrowserTabState.ts rather than the generic surface policy. Adding or removing browser surfaces and opening or closing a mini-player must leave the Source Control surface open, visible, active, and present exactly once. Background-work stop requests are sequenced across thread switches, so a late failure from one thread cannot clear the pending Stop state of a newer thread request.

Provider adapters keep their provider-specific normalization of raw pull-request and merge-request states. Once normalized, resolved Git pull requests and panel VcsStatusChangeRequest schemas in packages/contracts/src/git.ts use the shared ChangeRequestState from packages/contracts/src/sourceControl.ts.

ChatView keys the mounted SourceControlPanel by active environment, thread, and effective Git cwd. Switching between conversations, projects, or worktrees therefore creates a fresh panel instance for that context instead of letting repository state from the previous thread bleed into the next one. Grouped-project draft navigation resolves the panel target from the active draft/thread ref and effective Git cwd on every render, so the singleton source-control surface retargets without reusing another grouped project's repository context. Source-control metadata errors remain keyed to the originating scoped environment/thread key, do not appear on another grouped-project draft, and are pruned when that context is no longer retained. The panel pairs that remount boundary with a bounded in-memory state cache keyed by environment, thread, cwd, and worktree path, so returning to a previously opened Version Control panel can render its last snapshot and UI state immediately while the normal refresh/fetch path updates it in the background.

When the active logical project spans multiple environments, ChatView supplies every physical project environment together with its live connection state and saved project actions. buildSourceControlEnvironmentOption in ChatView.sourceControl.ts constructs the shared option shape used by toolbar routing and the federated panel, including its project payload and preferred action. The Version Control surface requires the environment's sourceControlPanel capability, so the launcher is unavailable on older servers and peers without it are left out of the federated list. It omits disconnected environments entirely and mounts one complete panel instance for each connected environment, with the active environment first. Environment headers show the environment label and cwd alongside the shared environment machine icon. The icon follows the user's selection, falling back to the detected machine kind and then a generic server when neither is available; changes in environment settings update the displayed icon. A failed request from a still-connected environment remains isolated inside that environment's normal panel error and retry surface instead of hiding successful sibling environments.

All Git queries, status streams, automatic and manual fetches, inline diffs, editor launches, mutations, and project actions remain bound to the environment and cwd of their owning panel instance. Only the active environment instance can update the active thread's branch/worktree metadata or open the thread-bound standalone File surface. SourceControlEnvironmentPanel.tsx receives that capability as activeThreadRef; it also passes the ref to PublishRepositoryDialog so repository links can open beside the active thread. Non-active environments receive null, retaining inline file diffs and environment-scoped preferred-editor actions without attaching a foreign workspace to the active conversation. When more than one environment is listed, each expanded repository summary shows the same project-action picker used in the conversation header. The picker reads, adds, edits, deletes, and remembers actions for that physical project. Running an action opens a terminal tab beside the current conversation, but the terminal surface records the selected environment, project, cwd, and worktree. Terminal open, write, split, and close requests then use that recorded environment while the conversation stays on its original environment.

ChatView.sourceControl.ts owns the source-control-specific right-panel availability and presence normalization, logical-project environment-option construction, and open-surface callback so chat timeline and minimap changes in ChatView.tsx stay separate from Version Control panel glue.

Native mobile screen

Both native routes use the shared ScreenHeader, with no sidebar action. The Version Control route provides a close action back to the active thread. Its file-diff route uses an explicit back action in split layouts and the shared in-content back header on Android. Compact iOS uses the native stack's visible Back control to return from the diff to the full-screen Version Control route. Returning from the diff keeps the expanded branch and commit file list intact.

The mobile screen preserves the web panel's compact repository summary plus Actionable and Remotes information architecture, adapted to native touch interaction. The summary is deliberately unboxed so it cannot be mistaken for an interactive row: it shows the current branch, compact ↑x/↓y sync counts, No upstream only when applicable, and either Clean or the current x file(s) +x -x working-tree totals. Resizable desktop sections become collapsible native sections, row actions are touch-sized pills shown only while their owning row is expanded, remote branch rows are hidden together with their collapsed remote, and pull-to-refresh complements live VCS status updates and the same best-effort fetch-on-open behavior. The native route prevents overlapping automatic fetches per cwd, and the server reports whether it performed network work so a fresh shared fetch no longer triggers another full snapshot.

Native snapshot refreshes are sequenced and coalesced per cwd so overlapping focus, fetch, status, and mutation requests produce at most one queued rerun, with full refreshes taking priority. Every emitted VCS status value still requests a refresh even when its serialized summary is unchanged. Mutation completion accepts an authoritative full snapshot before requesting a VCS status refresh, so the later working-tree refresh merges against the post-mutation server cache instead of superseding the full refresh with stale branch, remote, stash, or fork data. Loaded native snapshots retain their owning cwd and are not exposed after the selected thread switches to another checkout, so the new cwd cannot be paired with the previous repository's files while its refresh is in flight. A cwd without a snapshot remains in its initial loading state only until that cwd's current request settles; failed initial loads expose the error and pull-to-refresh controls instead of remaining on the loading screen. When an accepted snapshot changes, cached branch and stash details are invalidated together with their expanded/loading rows, and late detail responses from the previous snapshot are ignored. Branch and stash detail requests are also sequenced per expanded-row key, so collapsing and re-expanding a row cannot let the earlier request overwrite or reintroduce an error after the newer request succeeds. The raw snapshot renders as soon as it arrives; supplemental untracked-file statistics and rename detection continue in the background and apply only if the request is still current. Failed mutations still refresh repository state before preserving their action error in mutation-owned state, so a racing status refresh cannot erase the failure while conflict-producing operations remain visible. Failed remote creation keeps the add-remote form open with its entered values intact.

The native Actionable section includes the current and dirty sibling working trees, local branches requiring attention, same-name fork branch notices, and stashes. Working-tree cards preserve newly-added-file default selection, remove selections for checkouts that become clean, use tri-state-style select-all behavior, show aggregate staged-plus-unstaged stats and rename source paths, and expose selected-file commit/stash/discard operations. The mounted route records a working-tree cwd as expansion-initialized only when that change set is current, so a sibling that later becomes the selected cwd still gets its one-time default Changes expansion. Pressing a working-tree, commit, or stash file opens a dedicated diff screen backed by the same native Review renderer, parsed diff model, syntax highlighting, theme, display metrics, and horizontal scrolling as the rest of the mobile app; unsupported patches retain a raw-text fallback. Staged-only files request the cached diff side, while mixed, unstaged, and conflict-only rows request the working-tree side so unmerged paths do not open an empty staged diff. The cards render the raw snapshot first, then update when cwd-scoped enrichment supplies untracked-file totals and rename detection; selected-file discard separately clears unstaged and staged sides for mixed rows, and conflict-only selections that have neither side stay excluded from the discard count and action availability. Branch cards expose thread-aware checkout, publish/push/pull/fetch/diverged sync choices, merge, rebase, delete confirmation, and comparison details. Expanding an actionable branch renders its ahead and behind commits rather than the aggregate comparison-file list; every commit carries a right-aligned green ahead or yellow behind arrow, and only expanding that commit reveals its changed files. Returning from a file diff preserves the expanded branch and commit state. Failed branch, fork-comparison, and stash detail requests replace the loading placeholder with the request error until the row is retried, instead of leaving an indefinite loading state; a successful retry clears that same shared detail-error banner without clearing an unrelated mutation error. Branches checked out in sibling worktrees keep deletion disabled, while merge sync is offered only for the branch currently checked out in the panel's cwd. Stash cards expose apply, pop, drop, and expandable changed-file details, including rename source paths; expansion and detail caches use the stash commit hash when available so positional ref renumbering cannot bind old details to another stash.

The native Remotes section supports fetch, add, remove, and a deliberately shallower branch inspection model than desktop. Remote branches do not load commit trees on mobile; their rows show local/remote identity icons, current/default/worktree tags, and available ahead/behind counts, then reveal applicable checkout, fetch/sync, merge, rebase, and delete actions when pressed. A remote branch binds to a local counterpart only when that local branch tracks the row's exact remote-qualified ref, so duplicate branch names across remotes cannot borrow another upstream's counts or actions. All mutations use the existing panel RPC atom families; branch switching deliberately reuses the mobile thread Git action path so server-thread metadata and active checkout context continue to update together.

Cross-platform panel presentation rules that affect both web and mobile live in @t3tools/shared/sourceControl, including staged/unstaged file merging and sync-vs-compare branch state. Mobile-only list composition and touch presentation helpers remain in versionControlModel.ts.

Layout

The Version Control panel has a compact repository summary at the top and two resizable, collapsible sections:

  • Actionable
  • Remotes

Remotes is collapsed by default. The sections share the available panel height, each section owns its own overflow area, and section edges can be dragged to resize them relative to each other.

The repository summary shows the current ref, upstream status, changed-file count and line stats, ahead-of-default context, and current error state. Git operation progress is shown in the action button that started the operation. There are no separate Repository, Commit, Sync, or Diagnostics sections in the current layout.

apps/server/src/sourceControl/SourceControlPanelService.ts reads repository summary status through GitWorkflowService.status with includePullRequest: false. GitManager.ts retains local and remote synchronization fields while skipping the provider-backed PR lookup, because panelStatusFromLocal always projects snapshot PR state to null. The shared VCS status stream keeps its normal PR lookup behavior for consumers that render that state.

Live updates

The panel refreshes from the VCS status stream, explicit panel operations, window focus, and document visibility changes. VcsStatusBroadcaster also maintains ref-counted filesystem watchers per cwd while a repository is subscribed, with local path filtering and debounced refresh signal handling factored into VcsLocalWatch. Internal .git/ events are ignored before any refresh decision, file events are debounced, remaining paths are checked against Git ignore rules when possible, and explicit local refreshes publish a local status update even when the summary fingerprint is unchanged so same-path/same-stat file edits can refresh live working-tree diffs. If a local watch fiber has already terminated while an entry remains subscribed, the next subscriber replaces the stale watcher instead of attaching to a dead fiber.

This keeps externally-created changes visible without requiring a window blur/refocus cycle, while avoiding repeated no-op refreshes for gitignored files and unrelated background churn. Root panel streams also retain local watchers for sibling worktrees discovered from git worktree list --porcelain; stale/prunable worktree paths that no longer exist on disk are filtered before watcher retention, and sibling filesystem events force-publish a root local update so the panel can reload sibling Actionable rows even when the root checkout status fingerprint is unchanged. Local watcher subscriptions are acquired with stream-scoped finalizers before the initial status load, so a failed stream setup still releases every watcher retained for the subscription.

apps/server/src/utils/CanonicalPath.ts owns canonicalization for existing repository paths. It uses Node's native realpath first so Windows 8.3 aliases expand, falls back to the Effect filesystem realpath, and returns the input when neither can resolve it. GitVcsDriverCore.ts uses this identity for Git common-directory caches, while GitManager.ts uses it for status caches and worktree path comparisons.

The panel only shows the full Loading repository state... placeholder when no cached or loaded snapshot exists for the active thread/cwd. When a cached snapshot exists, the panel renders that previous repository state immediately and shows the existing refresh indicator while panelSnapshot and remote fetch work catch up, even if the fetched snapshot fingerprint matches the cache. Cached panel state is process-local rather than local-storage persisted and is bounded to recent contexts; it includes the panel snapshot, collapsed/expanded sections, branch and stash detail maps, compare-base overrides, selected paths, working-tree enrichment data, and loaded file diffs. Each environment panel is keyed by the same environment, thread, cwd, and worktree identity as its cache entry, so changing any repository context remounts its hook state before the destination cache can be written or refreshed. In-flight diff loading rows are intentionally not restored from cache, so a reopened panel does not get stuck showing an old loading row without a live request. Refresh requests that arrive while another refresh is active are coalesced, with full refreshes taking priority, and the queued refresh is still drained if the active request fails or is interrupted. Panel actions acquire their action key synchronously and ignore a duplicate invocation until the first mutation settles, preventing repeated commits, pushes, pulls, or destructive actions from racing before React renders the disabled state.

Actionable

Actionable is the default operational overview. It lists only work that needs attention:

  • A dirty Working tree row, shown first and omitted when the tree is clean.
  • Local branches that are local-only, ahead, behind, diverged, or otherwise require action.
  • Same-name local branches that are behind likely fork branches on other remotes.
  • Local branches with open change requests from the discovered source-control provider, including GitHub, GitLab, Azure DevOps, Bitbucket, Forgejo/Gitea and GitCafe, whose base branch exists on the matching remote and is ahead of the local branch.
  • Stashes.
  • Other checked-out worktree branch labels when available.

Checked-out worktree labels are resolved from git worktree list --porcelain first. When that output is unavailable or empty, the service falls back to git branch --format worktree placeholders where supported; older Git versions that do not support %(worktreepath) fall back to the branch format without paths instead of failing the whole panel snapshot.

When the panel is opened from the root checkout and other non-root worktrees are active, dirty sibling worktrees are shown as separate Actionable entries keyed by their checked-out branch and worktree path. These entries are populated by running the same status and diff snapshot commands in the sibling worktree cwd, so staged, unstaged, untracked, and conflicted files remain scoped to the checkout where they exist. Sibling worktree rows use the same batched file enrichment path as the root working tree, keyed by worktree cwd so same relative paths in different checkouts cannot share stale rename or untracked-stat details. Sibling worktree rows are omitted when clean.

Local branches fully synced with their tracking branch are omitted from Actionable unless they are behind another remote branch or an open PR target as described below. They remain visible under Remotes when they track a same-name remote branch.

Actionable branch sync state is intentionally separate from branch comparison state. A local branch can have a configured Git upstream/base such as upstream/main because it was created from that ref, while still being unpublished as origin/<local-branch-name> or another remote branch. The panel treats only same-name remote tracking refs as publish/sync upstreams for branch actions. Different-name refs remain valid compare bases and can still make the branch appear in Actionable, but they do not make the branch behind or diverged for sync purposes. In that state the row's sync action is Publish, and publishing targets the local branch name on the chosen remote rather than the base ref. A configured same-name upstream whose remote ref is gone, because the branch was never pushed or its remote branch was deleted, is not a sync upstream either: the server reports the branch without an upstream, so it appears in Actionable and the unpublished Remotes row with Publish, including when it is the current branch.

When a repository has multiple remotes, the server checks local branches against same-name branches on other remotes. A same-name remote branch is treated as a likely fork only when the refs share ancestry. The Actionable row is shown only when the local branch is behind that remote branch; a local branch that is only ahead of the other remote branch is omitted because it is rarely meant to push directly to that upstream. These fork rows use the other remote branch as their default vs. ... compare base and still show ↑x/↓y counts against that remote branch.

The server discovers change requests for local branches with a remote upstream or a same-named branch on any configured remote through GitManager.branchPullRequest, the cached automatic lookup used by the conversation sidebar. The same-name remote ref covers a push without setting an upstream and an upstream unset after publishing; branches with neither form of remote publication evidence are skipped. This reuses provider selection, head-repository validation, in-flight requests, and failure backoff instead of scanning a capped repository-wide PR/MR list. For an open PR/MR, its repository identity selects the configured remote and the panel compares the local branch against its base branch on that remote. A PR/MR-derived Actionable row is shown only when the local branch is behind that remote base branch; if the branch is already current with, ahead of, or unrelated to the base branch, no Actionable entry is shown. PR-derived rows show the sidebar's PR badge before the ahead/behind counters, with the same status tooltip and link behavior. Plain clicks open the PR in T3 when supported; Ctrl/Cmd-click opens the host. Self-hosted GitLab lookups scope glab mr list --repo with a full project URL instead of an ambiguous scheme-less host/path locator, without duplicating a configured base-URL path prefix that is already present in the remote repository path; GitLab.com keeps the normal namespace/project form. Provider lookup is best-effort: authentication, CLI/API, or unsupported-remote failures omit PR/MR-derived rows without blocking the Git snapshot, but provider-specific errors still preserve structured causes for diagnostics. Change-request and opt-in commit-avatar reads share the pull-request service's provider-and-host rate-limit cooldown, so a classified rate-limit response records its reset time or fallback backoff and suppresses later background panel requests until the pause expires without hiding Git-derived rows. Panel reads and GitHub adapter requests build their cooldown key from the same normalized provider host, retaining explicit non-default ports. A GitHub enterprise pause on host:8443 therefore suppresses panel reads to that endpoint while leaving host and host:9443 independent. Forgejo/Gitea supplies change requests but no commit avatars, so those rows retain the normal avatar fallback.

The federated SourceControlPanel.tsx owns environment coordination. Each SourceControlEnvironmentPanel.tsx mounts useSourceControlPanelController.ts and its state/refresh/action/expansion hooks for Actionable/Remotes expansion, selection, enrichment and the per-environment/thread/cwd cache. apps/web/src/state/sourceControlPanel.ts owns the environment-scoped panel RPC wrapper and presentation-state helper.

The Actionable header has a Fetch action. Automatic fetch-all-remotes work has its own Background activity interval because it is materially more expensive than refreshing the current branch's upstream status. Its performance, balanced, and battery-saver profile values are one minute, five minutes, and disabled respectively; the Source Control settings page can override that interval. An omitted persisted background profile decodes through the shared balanced default, while the panel-specific interval retains its five-minute cadence. Explicit persisted upstream-status overrides, including five minutes, remain overrides and are not reclassified by value. Web and mobile panels retain environment/cwd-scoped git-refs demand only while mounted. Opening the web panel and focusing the mobile route refresh their local snapshots immediately. On subsequent web focus, an enabled interval makes the server-coalesced network request first and refreshes the local snapshot once after it settles; SourceControlPanelService.fetchAllRemotes runs a due fetch only when the owning environment's shared background policy permits its active panel demand, and refreshes VCS status after a fetch that ran. A locked host is the one exception: it does not pause the fetch while a visible, focused or recently interacting client retains that cwd's panel or status demand, because that client shows someone is using the environment remotely. Suspend, thermal, battery, and low-power constraints still pause it. A zero interval prevents open, focus, and periodic panel network fetches, and web focus refreshes locally without making the request. The explicit Fetch action is forced, bypasses both the interval and background policy, and remains available in every profile.

Web, desktop, and mobile panel APIs await an immediate client activity report before every non-forced all-remotes fetch. This ordering applies to opening, focus, and periodic requests: the server must acknowledge current visibility and retained repository demand before evaluating its background policy. Relying on the debounced activity report can make an opening fetch arrive first, get skipped, and leave secondary remotes stale until the next interval. Explicit Fetch does not wait for activity reporting. The automatic path still honors the configured interval and server policy; publishing demand does not bypass either.

Items are sorted by operational urgency, then recency. An unclean working tree is always first. Branch urgency is based on conflicts/diverged, behind, unpushed, dirty, and stale states. Branch and commit rows include succinct relative dates such as 5 minutes ago, yesterday, 4 days ago, and last week.

Working tree

The Working tree row expands to a compact changed-file list. Hovering anywhere on the row reveals its branch, full worktree path, file count, and aggregate added/removed line indicators. There is no staged-versus-unstaged grouping in the panel UI. Each changed file has a selector, and newly appearing changed files are selected by default.

Dirty sibling worktree rows expand to the same compact changed-file list and selection model. File diff, right-panel File preview, open-in-editor, stash, commit, and discard operations for those rows target the sibling worktree path, not the panel's root cwd. The File surface can carry a source-control cwd override for these rows while normal file surfaces remain workspace-relative. File surface ids encode the (cwd, relativePath) tuple unambiguously, so colons or other valid path characters cannot make two different checkouts replace each other's tabs. When an unavailable Source Control surface falls back to a visible File surface, the preview key, cwd, reveal metadata, pending state, and follow-up file opens all use that visible surface rather than the hidden active descriptor. This prevents same relative paths in different checkouts from sharing diff state or executing Git actions against the wrong worktree.

The working-tree subsection header shows a tri-state checkbox before the selection summary. Checked means all files are selected, unchecked means none are selected, and partial means some files are selected; clicking a partial or unchecked checkbox selects all files, while clicking a checked checkbox unselects all files. The summary reads x of y files selected and shows selected-file +x/-y line stats immediately after the label when non-zero. When staged and unstaged entries for the same file are merged into one displayed row, the displayed stats sum both sides instead of taking only the larger staged or unstaged count. These numbers are aggregate staged-plus-unstaged churn, not a de-duplicated net diff against HEAD; for example, a selected file with +2/-1 staged and +3/-4 unstaged is shown as +5/-5.

The working-tree header actions are:

  • Commit selected files.
  • Stash selected files.
  • Discard selected files, with confirmation.

Commit selected and stash selected generate their messages by default. Holding Shift while pressing either action opens the same optional-message dialog path used by the existing Git action control: the commit field is labeled Commit message (optional), its placeholder is Leave empty to auto-generate, and a blank message still uses the generation flow. The stash dialog behaves the same way for stash messages. Generated selected-file stash summaries apply literal pathspec semantics to diff and status input, exclude unrelated paths, and include both staged and unstaged tracked changes for an all-mode stash.

Branch sync actions such as push, pull, fetch, publish, and undo latest commit are intentionally not rendered on the Working tree row. They belong to branch rows, where the target branch is explicit.

File rows are compact. They show a one-letter status indicator such as A, D, M, or R, line change counts, and hover/focus-only action buttons. +x uses the added-line color and -y uses the removed-line color. Zero counts are hidden. Hovering anywhere on a file row reveals its full repository-relative path and line-change indicators; rename tooltips include the original path as well. The full-width row trigger keeps the card available across the whole item, while a panel-aligned virtual anchor keeps every left-positioned file card on the same horizontal axis as the top-level item cards regardless of list nesting. Expanding a file row opens an inline diff for that file change in working-tree, commit, stash, branch, and compare file lists. Inline file diffs apply syntax highlighting on their initial expansion; asynchronous worker completion updates the mounted viewer so users do not need to collapse and reopen the row to consume highlighted output. Review patches for tracked, untracked, commit, stash, branch, and compare rows pin repository-relative a/ and b/ prefixes and three lines of hunk context while disabling Git color, external diff helpers, and text conversion, so repository or user configuration cannot reshape or replace the patch consumed by the internal renderer. Rename diff requests preserve both the destination path and original path, so R rows compare against the deleted source side instead of rendering the destination as a whole new file. Copy diff requests omit the still-live source path, so copied rows render the destination instead of being interpreted as renames. Mixed staged-plus-unstaged working-tree rows open the unstaged diff by default because it is the still-editable part of the row; staged-only rows open the staged diff. The shared DiffPanel file tree selects and reveals files inside the diff. Its explicit filename primary action reuses the thread's default File surface through apps/web/src/diffFileActions.ts, while Source Control previews from a foreign sibling worktree retain the explicit cwd that keeps that checkout's file in a separate tab. The row actions are:

  • Open file in the right-panel File surface.
  • Open in VS Code through the preferred editor or host bridge.
  • Discard changes, with confirmation.

Untracked directories are expanded to file-level rows instead of being shown as a single folder row. Untracked files get A rows with line stats computed from a /dev/null comparison. The server also runs rename detection for unstaged untracked destinations through a temporary Git index, so staged and unstaged renames both collapse matching old/new paths into a single R row when Git can match them. If Git cannot match the similarity threshold, entries remain file-level A and D rows rather than a folder row.

The virtualized working-tree file list shares the Actionable section's scroll area. Web and mobile queue enrichment for every eligible file in the current snapshot, including collapsed and offscreen working trees, so aggregate line counts do not depend on which rows render. Enrichment stays batched and separate from the initial snapshot. Git diff statistics use repository-relative paths; the server converts them to the panel cwd before joining them to status paths.

The Working tree context menu includes selected-file commit and stash actions plus a separated destructive Discard selected changes action.

The web panel shares the default apps/web/src/components/Sidebar.tsx rich-tooltip presentation and timing for informational row targets: the sidebar's ThreadHoverCardPopup glass cards from apps/web/src/components/ThreadHoverCard.tsx open to the left after a short delay, close immediately, and switch instantly between adjacent targets. Federated environment headers use the same styled tooltip primitives for their complete cwd and do not rely on browser-native title tooltips. The opt-in apps/web/src/components/LegacySidebar.tsx implementation does not define this convention. Action buttons nested inside branch, commit, and file rows retain their terse top-positioned label tooltips, while those rich item cards opt out of Base UI's normal parent-close behavior only for a nested trigger inside the same row, so both remain visible as the pointer moves across that row's actions without affecting unrelated open tooltips elsewhere on the page.

Branch rows

Branch rows are compact tree items used both in Actionable and under Remotes. They show branch identity, sync indicators, head labels, and a relative activity date. Clickable branch, commit, remote, stash, working-tree, and file rows use a pointer cursor together with their keyboard button semantics. Hovering anywhere on a branch row reveals its full name plus available upstream, worktree, sync-state, and readable activity-time details. Anchoring the tooltip to the full row keeps its left-positioned card outside the panel content instead of overlapping the row's right side. Long branch refs and worktree paths use constrained metadata columns with forced wrapping so the animated tooltip viewport does not clip their trailing text. The sync-state rules are isolated in SourceControlPanel.logic.ts so Actionable and Remotes use the same definition of published, behind, ahead, diverged, and local-only.

Ahead and behind status is rendered as ↑x and ↓y; zero sides are hidden. ↑x uses the same green as added-line indicators. ↓y uses the warning/yellow download color. If a branch has both indicators against its same-name sync upstream it is diverged; no separate diverged badge is needed. Ahead/behind counts against a different compare base may still appear in fork/change-request rows or expanded branch details, but those counts do not drive the branch sync action.

Synced local branches shown under Remotes use a muted target icon before the branch label. Local-only or not-fully-synced branches use the same branch row model and expose the same expandable details wherever they appear.

Idle branch action buttons appear only on row hover/focus and are absolutely positioned over the right side of the row. During an operation and its follow-up refresh, the affected working-tree, branch, commit, stash, or remote item replaces its controls with an always-visible action label at the top right. It reuses the conversation sidebar's Working icon and color, with labels such as Committing, Stashing, Undoing, Rebasing, or Discarding. Hovering those controls keeps the branch tooltip visible instead of replacing it with a nested action tooltip. Row-level keyboard expansion only handles key events targeted at the row itself, so pressing Enter or Space on a nested action button does not also expand or collapse the row. Supported branch actions include:

  • Switch to branch.
  • Fetch, pull, push, publish, or smart sync, using state-specific icons.
  • Delete branch.
  • Undo latest commit when the current branch has commits not yet synced upstream.
  • Merge branch into the current branch.
  • Rebase current branch onto branch, using the git-pull-request-arrow icon.

Smart sync handles diverged branches by prompting for force pull, normal merge sync, or force push. Normal merge sync is available only when the diverged row is the currently checked-out branch, matching Git's working-tree merge semantics; the merge button is disabled for non-current diverged branch rows. Non-current diverged branch rows keep force pull and force push available without implicitly changing the user's checkout. Modifier-key tooltips stay terse; for example pull can note Shift: reset and Option: fetch, while push and publish note Shift: force. On web, a Shift-click forced sync asks for confirmation first and names the side it overwrites: a force push replaces the remote branch, and a reset to the upstream discards local commits, plus uncommitted changes when that branch is checked out. Diverged rows still go through the dialog choice, and a forced sync that would only fetch skips the prompt. Fetch-before-sync obtains an authoritative full snapshot after the fetch and uses the refreshed ahead/behind state for the same action, so newly fetched commits can pull immediately and an unchanged branch is not fetched twice.

When a branch row represents a branch checked out in another worktree, branch sync and undo operations target that branch's worktree path instead of the panel root cwd. This applies to publish, push, pull, fetch, force-pull, force-push, the current-branch merge-sync path, and undoing an individual or latest commit, so Git operations run from the checkout that owns the branch. Undo uses a soft reset in that checkout and leaves the reverted changes in its working tree. Checkout and deletion stay disabled for any branch already checked out in the current or a sibling worktree. A local branch whose registered worktree directory was deleted without git worktree remove is not shown with that path, and deleting it prunes the stale registration first; if Git still refuses because the branch is unmerged or used by a worktree, the panel reports that reason with the branch name and worktree path instead of the generic Git failure.

Branch details

Expanding a branch reveals branch details:

  • vs. ..., a non-expandable compare-base row.
  • X Ahead
  • Y Behind
  • History
  • Changes

Ahead, Behind and History are separate, initially collapsed subsections. Expanding the branch loads their first-page data and counts; opening a subsection renders its commits. A loaded vs. ... label alone does not imply that a commit list is expanded.

Every expanded branch shows the vs. ... row first. Its default base is the branch's configured upstream/base when available, otherwise the repository default comparison ref. The repository default comparison ref remains available even when it is the currently checked-out branch, so current-default-branch rows still have a stable compare target. The default ref uses VCS status default-branch detection when the checked-out branch is the repository default, so repositories whose default branch is named something like develop do not fall back to an unrelated non-current branch. This base can be a different-name ref such as upstream/main; in that case it is a comparison base only, not proof that the branch has been published. Actionable same-name fork rows default this base to the other remote branch they are tracking for updates. Actionable PR/MR-derived rows default this base to the found change request's remote base branch. Clicking the row opens a searchable ref picker so the user can choose another compare base. Compare rows do not show count prefixes or extra choose labels. Empty ahead and behind subsections are hidden. Ahead and behind labels include the count directly in the title and use the same colored upload/download icons as branch sync indicators. History is collapsed by default and loads commits in pages of 10. When more commits are available, a load-more row appends the next page inline until no more history remains.

Branch detail loading is keyed by the rendered detail surface, not only by branch name. Canonical branch rows keep the branch name and fullRefName entries synchronized, while actionable fork rows use an isolated fork-details:<local>:<remote> key so paging or loading one comparison base does not overwrite the branch-owned details for another base. Requests are sequenced per rendered detail key, so a late response for an earlier compare-base choice cannot replace the newer selection's commits or counts. Paginated commit responses additionally retain their requested comparison base and are discarded when the current detail has moved to another base, preventing old pages from mixing into the new history. Commit pages append in server order and de-duplicate by SHA, which keeps reloads and overlapping pages from duplicating rows. Commit parsing retains valid empty subjects and embedded tabs so history counts remain consistent with pagination. If cached expanded tree state is restored without a matching branch detail payload, the next panel snapshot refresh hydrates the missing expanded branch details even when the snapshot fingerprint itself is unchanged.

The branch-level Changes row summarizes the selected comparison as file count and line stats before expanding to the changed-file list.

Branch-level X Ahead and Y Behind rows follow the selected comparison base in the expanded details. Local-only or unpublished branches still support the vs. ... compare-base row, including configured base refs such as upstream/main; their branch sync action remains publish unless they track a same-name remote branch.

Ahead, behind, history, and changes file lists use the shared compact file-change row model.

Commit rows

Commit rows appear in branch history and ahead/behind lists. They show an author avatar, commit message, branch/tag labels, line-change indicators, and a relative date. Showing a user avatar image for commit authors is required and intended behavior, but provider avatar lookup is opt-in per source-control provider and disabled by default. When enabled for a supported repository remote, the server resolves the provider account avatar URL for the commit author (for example GitHub account avatar_url, GitLab Avatar API URLs, Azure DevOps commit author imageUrl, or Bitbucket commit author avatar link) and sends it as authorAvatarUrl; the web panel falls back to compact initials only when the setting is disabled, the provider/avatar URL is unavailable, or image loading fails. Azure DevOps change-request listing and commit lookup pass the organization encoded by the HTTPS or SSH repository remote explicitly to the Azure CLI; commit lookup uses the stable Commits Get 7.1 API version, so ambient CLI defaults cannot silently redirect either lookup to another organization. The panel must not silently regress to initials-only rows when the option is enabled, and provider avatar lookup must not be replaced by generated local avatars or unofficial email-derived third-party avatar URLs. GitLab's official Avatar API is allowed because it is the provider's documented lookup path; that endpoint may return external avatar-service URLs such as Gravatar or Libravatar, and those URLs are passed through because enabling the setting is an explicit opt-in to provider avatar lookup. The short SHA is available from the commit tooltip and context-menu copy action rather than the row label.

Commit labels are de-duplicated:

  • A commit that is head of a local branch and its synced upstream shows the local branch label with a muted target icon.
  • A commit that is head of a local branch but not synced upstream shows the local branch label.
  • A commit that is only head of the upstream-tracking branch shows a muted target icon before the branch name. Branch/tag labels classify remote refs using the repository's known remote names, not a slash heuristic, so local branch names such as feature/login remain local labels.
  • Tag labels use a tag icon before the tag name.

Commit tooltips use the shared rich tooltip card and are structured panels with author name and avatar, short SHA, relative and readable commit time, branch/tag labels, message, and line-change indicators. The card remains visible while hovering the commit's action buttons.

Commit rows expand to their changed files. Hover/focus-only commit actions include:

  • Revert commit.
  • Rebase current branch onto commit.
  • Checkout as detached HEAD.
  • Create branch from commit.

Stashes

Stashes are listed as Actionable tree rows. Each stash shows its message, ref, branch context when available, and relative date. Hovering the message reveals the full message, ref, branch context, and readable creation time. Expanding a stash loads and shows the stash's changed files using the same compact file-change row model used by commits and compare results. Restored stash expansion and cached stash details are keyed by the stash commit hash when Git reports one, falling back to the positional ref only when no hash is available, so stash@{n} renumbering after stash create/drop operations does not show details for the wrong stash.

Each web stash row's apply, pop, and drop actions share that stash's synchronous mutation key, and while any stash action runs, the actions on every stash row are disabled. Web and mobile mutation requests carry the selected stash SHA when available; the server rejects a positional ref that now identifies another stash. Apply uses the verified commit SHA, and pop rechecks the position before dropping the applied entry. App-owned create, apply, pop, and drop operations share a lock keyed by the canonical Git common directory, including callers in linked worktrees. This lock does not coordinate external Git processes. Requests without a SHA retain positional behavior for compatibility.

Idle stash row actions appear on hover/focus and include:

  • Apply stash.
  • Pop stash.
  • Drop stash.

Creating a stash is done from the dirty Working tree row through Stash selected. If cached expanded tree state is restored without a matching stash detail payload, the next panel snapshot refresh hydrates the missing expanded stash details instead of leaving the expanded row empty until the user collapses and reopens it.

Remotes

Remotes remains a separate section because it is the most useful way to inspect remote activity at a glance. The section header exposes:

  • Fetch all remotes.
  • Add remote, via a modal form.

Each remote row shows the remote name and fetch URL. Hovering the remote name reveals the complete fetch and push URLs plus its branch count. Idle remote action buttons appear on hover/focus and include fetch and remove.

When local-only or unpublished branches exist, Remotes also shows an unpublished tree row with those branches and an x branch(es) secondary label. Publishing one branch sets its same-name remote upstream. If the repository has multiple remotes, the panel prompts for the remote to publish to. If the repository has no remotes, the web panel opens the shared Publish repository dialog instead of attempting to push to an assumed origin.

Expanding a remote lists actual remote branches; pseudo-ref rows such as the remote name itself are de-duplicated. Remote branch rows use the same branch item model as Actionable, including local tracking state, ↑x/↓y sync indicators, synced-local target icons, selectable compare bases, branch details, and branch actions.

Git operations

The panel routes all repository mutations through server-side RPC methods and refreshes status after operations. Implemented operation groups include:

  • Snapshot and detail loading: panel snapshot, same-name fork ancestry checks, cached per-branch PR/MR discovery and comparisons against the matching configured remote's base branch, branch details, branch commit pages, stash details, compare data, and file-change details.
  • Working tree operations: selected-file commit, selected-file stash, discard selected files, discard individual changed files, read/open file data, stage/unstage helpers kept at the service boundary for compatibility.
  • Branch operations: fetch branch, pull, push, publish, switch, delete, undo latest commit, merge branch into current branch, and rebase current branch onto another branch or commit.
  • Commit operations: revert commit, checkout detached HEAD, and create branch from commit.
  • Stash operations: apply, pop, and drop.
  • Remote operations: list, add, remove, fetch one remote, fetch one branch ref, periodic Actionable fetch, and fetch all remotes.

Git command timeout selection is shared by the panel service, core Git driver, and generic VCS adapter. The selector identifies the Git subcommand after supported global options: fetch, pull, and push receive a 300-second default because network transfer and remote-side processing can legitimately exceed the 30-second local-command budget; commit receives a 600-second default because commit hooks can run long local checks, and all other Git subcommands retain the 30-second default. A caller-supplied numeric timeout remains authoritative, preserving deliberately shorter boundaries such as bounded background status refreshes, while an explicit null disables the core Git-driver timeout. Both behaviors use the shared GitCommandTimeoutOverride input type so helper and driver call sites cannot disagree about the override contract.

Ref-affecting panel mutations invalidate both layers shared by branch consumers when the command settles. On the server, apps/server/src/sourceControl/SourceControlPanelActions.ts wraps commit, pull, push, branch delete/undo/revert/checkout/create/merge/rebase, fetch, and remote add/remove commands in a non-failing finalizer that calls GitVcsDriver.invalidateRefs; apps/server/src/sourceControl/SourceControlPanelService.ts applies the same finalizer to coordinated fetch-all-remotes operations. That boundary invalidates the shared listRefs snapshot in apps/server/src/vcs/GitVcsDriverCore.ts after success, failure, interruption, or partial mutation. Matching command atoms in packages/client-runtime/src/state/vcs.ts use onSettled to invalidate shared client ref state under the same outcomes, so web and mobile branch consumers request the post-mutation snapshot instead of retaining the server cache's freshness window. Pull-request details also consume this snapshot to classify stacked bases; focused cross-feature coverage caches a filtered view of the shared snapshot, creates a branch through the panel action boundary, and verifies the next read observes that branch. Working-tree stage, unstage, and discard operations; stash operations; diff, compare, and detail reads; and other display-only operations remain excluded because they do not change the refs represented by listRefs. This shared-ref invalidation is additive to the existing VCS-status and authoritative-panel-snapshot reconciliation, and its non-failing finalizer cannot replace or mask the original mutation error. Separately, every successful panel mutation, including working-tree and stash operations, forks a VCS status refresh from SourceControlPanelService without delaying its result; branch, remote, and fetch-all fetches skip the upstream refresh they just performed.

Non-current branch fetches are scoped to the selected branch. Operation busy state is keyed per action target so fetching one branch does not disable equivalent actions on other branches or remote entries. Publish/push handling on the server only reuses a configured upstream when that upstream resolves to the same branch name; otherwise it pushes <local-branch>:refs/heads/<local-branch> on the selected/default remote so a base ref such as upstream/main cannot become the push target. Branch deletion preserves the selected identity: remote rows send their full row name and remote name, and the server resolves only that remote before choosing the short branch name for git push <remote> --delete. Requests without a remote name resolve only local branches, deleted through the shared GitWorkflowService.deleteLocalBranch; a missing selected target is rejected without falling through to another kind. Snapshot branch and upstream names strip their fixed Git namespaces instead of using ambiguity-dependent short names, so colliding local and remote names retain their actual deletion targets. Upstream remote identity is carried separately, preserving local tracking while remote pairing, sync eligibility, fork comparisons, and peer coordination require the configured remote to match. The server rejects deletion when the resolved local branch is current or has a worktree path, preserving the checked-out-worktree guard even if a non-web client bypasses the disabled action. Before deleting, it prunes prunable worktree registrations, then refuses a branch that Git's worktree list still shows checked out (naming that worktree's path) and, unless forced, a branch not merged into its upstream, or into HEAD when it has no resolvable upstream, matching Git's own -d rule. localBranchDeleteRefusal in SourceControlPanelActions.ts owns that decision.

On web, a normal ahead-branch push coordinates the same logical project across connected environments. Before pushing, it records only peers whose current branch has the same name, has a clean working tree and no local commits ahead of its upstream, and tracks the same normalized remote URL and remote branch. Peers already behind remain eligible so missed updates can catch up. After the source push succeeds, each prepared peer fetches that branch and receives a fast-forward-only pull only if a fresh snapshot still shows the same clean checkout with no local commits. A peer that changed during the push is skipped. Peer reads and mutations are best-effort so a disconnected peer or failed peer fetch cannot turn the already-completed source push into a reported push failure. Force pushes, unpublished branches, diverged branches, different remotes, and disconnected environments are not auto-pulled. Collapsed connected environments remain eligible because the coordinator uses the parent panel's complete target list rather than mounted child sections.

Selected-file commit, stash, and discard operations preserve rename source paths when needed, so selecting an R row sends both the destination path and the original path to the Git operation; copied rows send only their selected destination because the original is still a separate live file. Rename source paths are preserved for normal porcelain status entries and for fallback numstat parsing, including line-based fallback output. Porcelain status paths are decoded from Git's quoted path format before matching numstat output or rendering rows, so non-ASCII and escaped path names use the real repository path. Shared staging, unstaging, temporary intent-to-add, selected-file stash, and every discard classification/mutation command treat selected paths literally. Selected-file commits construct a temporary index from HEAD, or an empty index when HEAD is unborn, stage only the selected paths into that index with literal-pathspec semantics, generate the message and commit without pathspecs through that isolated index, then reset the selected real-index entries to the new HEAD in an interruption-safe success finalizer before temporary-index cleanup. Message generation and commit execution remain cancellable; cancellation during cleanup cannot skip reconciliation of a completed commit. Selected staged deletions retain their removal intent even when the files still exist on disk, as with git rm --cached followed by an ignore rule; adding those paths again would either fail on ignored files or silently undo the removal. Resetting the real-index entries also avoids staging working-tree edits made after the commit snapshot. This keeps mixed selected files complete while excluding deselected changes that were already staged in the user's real index; those deselected changes remain staged after the commit, while generation or commit failures leave the real index untouched. If post-commit real-index synchronization fails or defects, the server reports that the commit already exists, prevents push, and asks the user to reset only the selected paths before pushing the existing commit. Retrying a completed selected-file commit before reconciling the index can stage deletions for newly added files because their stale real-index entries no longer match the new HEAD. Discard operations also split staged and unstaged portions explicitly. Server-side discard handling partitions tracked, untracked, HEAD-backed, and newly added paths before running Git restore/reset/clean commands, so mixed selections such as tracked edits plus untracked files do not cause one path class to prevent the rest of the selected discard from applying. If the tracked unstaged restore step fails, the failure is reported to the panel instead of being swallowed after staged cleanup succeeds. Compare RPC results preserve left-to-right direction, including reversing the patch when the working tree is the left operand and a branch or stash is on the right. Switching a remote ref creates a local tracking branch; configured remote names, including names containing slashes, determine the local branch name. When the remote branch's short name already belongs to a local branch tracking another upstream, Git chooses the next available suffixed local name instead of checking out the remote ref in detached-HEAD state. Merge and rebase operations pass the selected ref after a positional -- separator so option-shaped refs cannot be interpreted as command flags.

Generated commit and stash messages use the registered checkout project's effective Source control writer model and writing-style overrides. Unregistered sibling worktrees inherit the main checkout project's settings; unavailable project lookup leaves environment defaults in effect. When that setting is disabled or its configured writer is not usable in the current provider snapshot, they fall back to the configured Text generation model. Both the panel and the generic Git workflow resolve Source control writing style through the same repository-context policy reader, so Version Control does not add its own writing instructions. Repository-convention mode reads recent commit subjects and the repository's AGENTS.md; Claude writers also read CLAUDE.md. apps/server/src/textGeneration/SourceControlWriting.ts resolves the Git repository root even when a panel opens below it. Its instruction reader checks real paths with path.relative containment so drive roots work, rejects escaping or non-file paths, and bounds instruction files to 20,000 bytes.

Error handling

Rename enrichment for a deleted file can inspect every untracked destination, even when the file list is virtualized. Temporary-index intent-to-add, selected-file commit staging, and post-commit real-index resets send NUL-delimited literal paths through standard input with --pathspec-from-file=- --pathspec-file-nul. Keep these file lists off the command line: thousands of long paths can exceed Windows' process-launch limit and produce spawn ENAMETOOLONG. Generated commit messages read the selected temporary index without repeating the path list in diff arguments; that index already excludes unselected changes. Both the temporary-index add and post-commit real-index synchronization must handle large selections.

The core Git launcher converts synchronous spawn exceptions and spawn-effect defects into GitCommandError. A failed Git launch must remain scoped to its request; allowing it to escape as a connection-wide RPC defect disconnects unrelated subscriptions and can cause a reconnect/retry loop.

An existing Git index lock pauses status reads to avoid repeatedly running uncached clean filters. The status reader waits one second and checks the lock again before reporting an error, allowing brief locks from other Git activity to clear. If the lock clears during that grace period, the same request proceeds normally; otherwise it fails without running status or removing the lock. This applies to repository roots, nested directories, and linked worktrees. A persistent lock requires checking for active Git processes and an open lock-file handle before moving aside a confirmed stale lock. Do not remove an active lock or disable the status guard.

Git action failures surface through the panel error state and existing toast/error paths. Native Version Control checkout owns its failure locally instead of also emitting the shared Git-action toast and connection banner. Web and native mutations reconcile VCS status and an authoritative panel snapshot after success or failure, then preserve the mutation error after reconciliation, so conflicts and other partially applied results become visible without losing the failure message; if mutation and reconciliation both fail, the original mutation error remains the user-facing error. Panel errors are capped to a short scrollable block so large Git output cannot consume the full panel, and the error block has a floating copy action for debugging. Interrupt-only atom command results are treated as cancellation rather than user-visible failures. Native detail and file-diff reads retry one interrupted force-refresh request; repeated detail interruptions remain silent, while a repeated file-diff interruption exits the loading state with a recoverable error. Generated selected-file commits inspect the returned command result, so failed commit generation or execution keeps the dialog path honest by surfacing the failure instead of silently refreshing. Commit progress output can classify known hook failures to enrich a failed Git result, but diagnostic text never interrupts a still-running commit or replaces its authoritative exit status. Destructive actions such as discard selected changes, discard an individual file, delete branch, remove remote, drop stash, force pull, and force push require confirmation or an explicit dialog choice before execution. On web, apps/web/src/components/source-control/useSourceControlPanelActions.tsx routes those confirmations through confirmSourceControlPanelMutation in SourceControlPanel.logic.ts, which calls LocalApi.dialogs.confirm with the destructive variant; there is no native window.confirm fallback, so Version Control shares the queued themed dialog behavior used by the rest of the web app. Creating a branch from a commit collects the branch name through the panel's themed Dialog and Input, never through native window.prompt, so the action also works in Electron's renderer.

The commit-branch dialog remains open and preserves its typed name while the mutation runs or fails, and closes only after the branch is created successfully. Forced current-branch sync forwards the force flag through its push path just like non-current branch sync.

Server-side panel errors preserve a sanitized cause diagnostic when wrapping lower-level failures into GitCommandError, so diagnostics can still distinguish process failures, provider failures, and command output failures even when the UI receives a normalized panel error. Wrapper messages are derived only from stable operation fields and caller-supplied detail, never from cause.message. The preserved cause is bounded to a small plain object before it crosses the server contract, and the existing capped panel error block remains the user-facing size guard. Source-control provider adapters keep provider-specific missing-CLI, authentication, not-found, decode, and API details in their typed errors, then wrap common provider failures through sourceControlProviderError so GitHub, GitLab, Azure DevOps, and Bitbucket expose aligned SourceControlProviderError fields: provider, operation, cwd, sanitized reference/repository, detail, command when available, and a bounded transport-safe cause projection. Typed GitLab CLI errors own GitLab process-failure normalization and preserve structured process failures inside that transport-safe boundary.

The panel keeps version-control actions server-authoritative across browser, desktop, VS Code, and remote clients. Client code does not directly execute Git commands.

Source-control mutation affordances on web and mobile follow the owning environment's source-control:write availability, including confirmation entry points. Read-only inspection remains available. Federated terminal shortcuts use the focused terminal destination's grant, and script shortcut edits compare and recheck settings access on their destination environment.

Verification boundaries

Current source is c6661f493a1998db1d72f316d5f71a3e4eec9dcf, with panel baseline de54a360d80cf7ca7c87aa245dc8ec57bcdeeced directly above fixed base 348f217a853dd4a23d7fa4834dedb05f545c2075. Tracking remains origin/feat/version-control-panel-work at 20bcb7e6106957c991dfb2c4c60c69eb8df9e2a3, 126 ahead / 1 behind. BRANCH_DETAILS.md records compatibility constraints, current checks, runtime ownership and proof links.

493 distinct focused tests pass across 24 files, including modern/older-Git commit and stash writer inheritance, contract path whitespace, destination shortcut grants, panel services, client settlement, native models/requests and selected Git ref/fetch/worktree integration behavior. Server, web and mobile scoped typechecks pass, as do targeted lint, formatting and whitespace checks. Existing non-fatal React Compiler warnings and Effect suggestions remain. No full-repository validation is claimed.

Representative iPhone 17/iOS 26.5 verification passes both entry points, repository summary, branch/commit inspection, numbered syntax-colored diff content, Back preserving expansion, Remotes and close/reopen at the exact source. Native evidence and settled diff screenshot are outside the repository. Windows Android device support is unavailable; iPad, native Electron shell, destructive mutations, provider credentials and pull-to-refresh remain outside this pass.

Integrated web verification passes Changes and launcher entry points, singleton close/reopen, maximize/restore, Remotes, History/Ahead rendering and pagination, and recording/saving/reopening a browser action shortcut. The disposable action and shortcut are removed. HIGH Magi review unanimously approves a P3 test correction: the preceding commit request could satisfy stash assertions if stash generation was skipped. Correction c6661f493a1998db1d72f316d5f71a3e4eec9dcf collects requests, asserts one after commit and two after stash, and checks both inherited writer/style values; the focused 18-test writer suite, targeted lint, formatting and whitespace checks pass. The runtime and integrated web/iOS evidence remain at 4e96abfb3db0cb83583d711ea50fbd18297af2c5; the new commit changes tests only. Final HIGH Magi review reaches unanimous 18/18 consensus at c6661f493a1998db1d72f316d5f71a3e4eec9dcf in turn 3, with no remaining dissent or open findings. Both initial and incremental CodeRabbit reviews complete with zero findings. Future user-visible changes use the current test-t3-app or test-t3-mobile workflow and current main-owned worktrees helpers, fixed web/server ports 5742/13782, isolated state and task-owned teardown. These files describe behavior and evidence; they do not add an approval workflow.