Skip to content

Source Control (Git) Panel

The Source Control panel is a VSCode-style SCM sidebar mounted on the session detail page (/tools/agents/sessions/<id>). It lets you stage, commit, push, pull, view diffs, and browse history for any git repositories inside the session's working directory — without leaving the chat.

Source

Backend: internal/agents/scm/git.go (git shell-outs), scan.go (multi-repo discovery). Handler: internal/tools/agents/scm.go, scm_watch.go (SSE watcher). Frontend SPA: fe/agents/scm/ — Svelte 5, Monaco editor, mounted as an island into the session page. Endpoints: /tools/agents/api/sessions/{id}/git/*.

Prerequisite

git must be on PATH on the host running wick. The panel shells out to the real git binary — it does not bundle one. Existing SSH / PAT credentials and ~/.gitconfig settings apply as-is.

Opening the panel

The session detail page has a right-edge rail with three tabs: Context, Process, and Source. Click Source (or the tab label) to open the Source Control panel.

The panel opens as a slide-over overlay by default. To dock it, click the pin icon in the panel header. When pinned, the chat content area reflows to the left to make room — the panel does not overlap it.

StateBehaviour
UnpinnedOverlay on top of the chat; click outside or press Esc to close.
PinnedDocked alongside the chat; chat content pushes left.

Pin state and panel width are persisted in localStorage. The panel is resizable: drag the left handle to any width between 240 px and 640 px (default 260 px).

Layout (full mode)

In full mode the panel uses a two-column layout:

  • Left column (220 px) — file list grouped into Staged and Changes sections, commit message input, and branch bar at the bottom.
  • Right column (flex) — Monaco diff editor as the primary surface. The first changed file is selected automatically on load.

The active repo selection lives on the session, not in the browser: it is stored in the session's meta.json (ScmRepo), so it survives a different browser, is scoped per session rather than shared across all of them, and — crucially — is visible to the agent. The agent's system prompt names it as active_repo, and the Source connector lets it re-read or move that selection.

Editing a file also moves the selection: when the filesystem watcher sees a write inside a repository other than the active one, the panel follows it there. What the agent is working in and what the panel shows stay in agreement without anyone clicking.

Multi-repo support

On open, the panel recursively scans the session cwd for git repositories. The scan skips heavy directories (node_modules, vendor, dist, .cache, etc.) and caps at a fixed depth. If more than one repository is found, a collapsible Repositories section appears above the file list, showing each repo's name, branch, and ahead/behind count. Click a row to switch the active repo; all other sections apply to the active repo.

The list is capped at roughly 5 visible rows with vertical scroll so it can't push the rest of the panel down. When there are more than 5 repositories, a Search repositories filter input appears above the list, matching against repo name and path.

Collapsed, the Repositories header names the active repo, so you can tell which one you are looking at without expanding the section.

File list

The left column shows two flat sections:

SectionWhat it contains
Staged (N)Files added to the index (git add).
Changes (N)Modified tracked files and untracked new files.

Click a file row to open it in the diff editor. The active file is highlighted.

Each file row shows a status badge (M, A, D, ?) on the right edge.

Diff editor

Clicking a file opens it inline in the right-column Monaco diff editor — no modal. The diff is git-correct:

File stateWhat is diffed
StagedHEAD ↔ index (staged content)
Unstaged (tracked)Index ↔ working tree
Untracked (new file)Empty ↔ working tree

Unchanged regions are collapsed by default with a "N hidden lines" expand bar (3-line context, same as VSCode). Click the bar to expand.

The diff renders in unified (inline) mode by default. Click the split-view icon in the diff header to toggle side-by-side mode.

Editing files

The diff editor is directly editable — no "Edit" button required. Start typing in the modified (right) side and a Save button appears automatically in the diff header. Click Save to write the file to disk. Click Revert edit to abandon unsaved changes without touching the file.

Per-file actions (diff header)

ButtonAction
StageStage the current file (git add).
UnstageUnstage the current file (git restore --staged).
DiscardDiscard working-tree changes (see warning below).
SaveWrite in-editor edits to disk (visible only when there are unsaved edits).
Revert editAbandon in-editor edits without touching the file (visible only when there are unsaved edits).
Split-view iconToggle unified ↔ side-by-side diff layout.

Discard is destructive

Discard cannot be undone. For tracked files it runs git restore <file>; for untracked files it runs git clean -f <file>. A confirmation dialog appears before the operation runs.

On mobile or when the panel is opened as a slide-over overlay, a compact sidebar mode is used instead. The file list, commit box, and branch bar stack vertically. Clicking a file opens a full-screen diff modal (Monaco diff editor, same editing and save behavior as full mode).

Commit

The commit message input is at the bottom of the left column. Type a message and press Enter (or click Commit (N)) to commit all staged files. The button shows the staged file count and is disabled when staging is empty or the message is blank.

Branches

The branch bar at the bottom of the left column shows the current branch name and the ahead/behind count (↑N ↓N). Click the branch name to open a dropdown with:

  • a filter input to search local and remote branches
  • local branches (current highlighted)
  • remote branches
ActionHow
Checkout localClick a local branch name.
Checkout remoteClick a remote branch — creates a local tracking branch automatically.
Create + checkoutType a new name in the bottom input and click +.

Pull and Push buttons are below the branch picker.

Which credential push and pull use

The panel shells out to plain git, which has no credential helper of its own. Against a private HTTPS remote that always failed — so Push and Pull were decorative for exactly the repositories people care about.

The credentials already exist: the Git CLI connector holds them, and it is how the agent pushes. So the panel borrows one, along with that connector's branch policy and its per-run audit trail — a panel push shows up in the connector's run history with source: app and the signed-in user's id.

Choosing one

The first network operation opens a picker. Native git is preselected, because running as the machine already does is the option that changes nothing. Alongside it are the Git CLI instances you personally can reach — the same tag filter the connector manager applies, so the panel never hands out a credential you could not otherwise use.

ChoiceWhat runsPolicy
Native gitThe host's own git, with whatever credentials are on the machineNone — no protected branches, no force-push guard
A Git CLI instanceThe connector's push / pull op under your user idThe connector's full policy applies

An instance whose label contains the remote's host is marked likely — a guess from the label, and labelled as one, since the connector stores a username and a token, not a host.

How the choice is remembered

Stored on the session, keyed by user:

  • Per session, not per repo — a session's repositories are one body of work, and answering the same question for each of 55 checkouts is a chore nobody asked for.
  • Per user — a session can be shared (a Slack thread is open to everyone in the channel), and a credential is the one thing that must never be inherited from whoever pushed first. If you have no access to the instance your colleague picked, your push is refused with a message saying so, not silently run under their identity.

There is no "remember this" checkbox: the answer is always remembered, and Clear in the dialog is how you go back to being asked. Reopen the picker any time with the key button in the branch bar. A small dot on that button means this session pushes through a connector; native git shows no dot, because nothing about it is unusual.

A remembered instance that is later deleted, disabled, or revoked from you is treated as no choice at all — the panel asks again rather than reporting a push that went nowhere.

A refusal is not a failure

The connector answers a policy denial normallyok:false with a deny verdict — rather than as a transport error. Code that only checks the transport error reports a blocked pull as a success, which is exactly what happened here: the panel announced "Pulled" while git never ran and the branch stayed 65 commits behind. The panel now reads the verdict first and shows the connector's own reason.

History

The History tab inside the panel shows a commit log for the active repository. Each row displays:

ColumnContent
SHAShort commit hash (7 chars).
SubjectFirst line of the commit message.
AuthorCommit author name.
DateRelative date (e.g. "2 hours ago").

Click a commit row to expand it and see the list of files changed in that commit. Click a file in the expanded list to open the Monaco diff viewer for that file (parent commit ↔ this commit).

Live updates

The panel subscribes to the server-sent event stream for the session. A server-side filesystem watcher monitors the session cwd and pushes a git_status event over SSE whenever the working tree changes. The Changes section and the Source rail tab badge both update in real time — no polling, no manual refresh required.

The rail tab badge shows the count of changed files (staged + unstaged) in the active repository — the same number the Changes section lists. It used to sum every discovered repo, so a session with 55 clones showed 99+ on the badge while the panel below it showed 14, and the two could never be reconciled.

Endpoint reference

All endpoints are scoped to /tools/agents/api/sessions/{id}/git/ and require the same RequireToolAccess middleware + session-ownership check as the rest of the agents tool.

MethodPathPurpose
GET.../git/statusFull git status snapshot for all discovered repos.
GET.../git/diffFile diff (query: path, staged, commit).
POST.../git/stageStage files. Body: {paths: [...]}.
POST.../git/unstageUnstage files.
POST.../git/discardDiscard working-tree changes (destructive).
POST.../git/commitCommit staged files. Body: {message}.
GET.../git/branchesList local + remote branches.
POST.../git/checkoutCheckout or create a branch.
POST.../git/pullPull from upstream.
POST.../git/pushPush to upstream.
GET.../git/logCommit history.
GET.../git/log/diffDiff for a specific commit.
GET.../git/activeThe session's active repository, and whether it was explicitly chosen.
GET.../git/connectorsGit CLI instances this caller may push through, plus the one remembered.
POST.../git/connectorsRemember (or clear) the credential for this session. Body: {connector_id}.

See also

Built with ❤️ by a developer, for developers.