Workflows
Source Control Management (SCM)
How the built-in Git extension registers an SCM provider in Cocoon, uses Mountain to run native git commands, and keeps the SCM view in Sky up to date.
SCM in Land is entirely extension-driven. The built-in Git extension in Cocoon owns all Git logic; Mountain provides two services it needs - a native filesystem stat for repository detection and a GitProvider that safely spawns git child processes. Mountain then brokers the resulting resource states to Sky via Tauri events.
On startup Cocoon activates the built-in Git extension. Its
activate()function calls:vscode.scm.createSourceControl("git", "Git");This call is handled by the
ScmProviderservice in Cocoon, which manages state for all registered SCM providers.Cocoon’s
ScmProviderservice sends a$registerScmProvidergRPC request to Mountain, informing it that a provider for"git"is now managed by Cocoon.The Git extension checks for a
.gitdirectory. Since it cannot userequire('fs'), it uses the workspace filesystem API:vscode.workspace.fs.stat(Uri.joinPath(workspaceRoot, ".git"));This
statcall traverses Cocoon’sFileSystemProvider→ gRPC → Mountain →tokio::fs::metadata. A successful result confirms the workspace is a Git repository.
Phase 2 - Populating the SCM view (Cocoon → Mountain → Cocoon)
The Git extension needs changed-file status. It cannot spawn
gitdirectly, so it calls Mountain’svscode.gitAPI which maps to a$gitExecgRPC request:$gitExec { repoPath, args: ["status", "--porcelain", "-z"] }Mountain’s gRPC server receives the
$gitExecrequest and dispatches it to aGitProviderimplementation onMountainEnvironment. The handler safely spawns a native child process:std::process::Command::new("git") .args(["status", "--porcelain", "-z"]) .current_dir(repo_path)It captures stdout and returns the raw string to Cocoon.
The Git extension parses the output (“M src/main.ts”, “A new-file.txt”, etc.), builds a
SourceControlResourceStatearray (each entry carrying a file URI and decoration metadata such as"M"for modified), and sets it on theresourceGroups.Changesresource group.Setting
resourceStateson the source control object triggers Cocoon’sScmProviderto serialize the new states into DTOs and send a$updateScmGroupgRPC notification to Mountain, containing the provider ID, the group ID ("Changes"), and the resource state DTO list.Mountain updates
AppState.ActiveScmProvidersand emits a Tauri event to Sky:sky://scm/update-group { providerId: "git", groupId: "Changes", resources: [...] }
Phase 3 - SCM view rendering (Sky)
- The SCM View component in Wind is listening for
sky://scm/update-group. It receives the resource DTOs, updates its state, and re-renders - the user sees the list of modified files (e.g.M src/main.ts).
Phase 4 - Diff view (Sky → Wind → Cocoon → Mountain)
The user clicks a changed file in the SCM view. The click handler executes a command like
vscode.openwith agit:scheme URI encoding the HEAD revision, e.g.git:/src/main.ts?{"ref":"HEAD"}.EditorServicerecognises thegit:scheme and creates aDiffEditorInputwith two sides:- Modified - loaded from the workspace
file://URI viaIFileService.readFile()(the standard read path described in Opening a File). - Original - requested from the
TextDocumentContentProviderregistered forgit:by the Git extension in Cocoon.
- Modified - loaded from the workspace
Cocoon’s Git extension receives the content request for the
git:URI and sends another$gitExecgRPC request to Mountain:$gitExec { repoPath, args: ["show", "HEAD:src/main.ts"] }Mountain runs the command, captures the original file content, and returns it to Cocoon. Cocoon returns it to Wind as the
git:document content.DiffEditorInputnow holds both sides. The editor opens the diff panel and the user sees the side-by-side comparison of their changes against HEAD.
SCM input state synchronisation
Four SourceControl setters are wired back to Mountain via a $scm:updateSourceControl gRPC call (a sendRequest round-trip, not a fire-and-forget notification) every time the extension sets any of these properties:
| Setter | DTO field | Value sent |
|---|---|---|
inputBox.value | InputBoxValue | { inputBoxValue: V } |
inputBox.placeholder | InputBoxPlaceholder | { inputBoxPlaceholder: V } |
commitTemplate | InputBoxPlaceholder | { commitTemplate: V } |
acceptInputCommand | AcceptInputCommand | { acceptInputCommand: V } |
Changes to inputBox.value in Cocoon must go through the ScmProvider service via the $scm:updateSourceControl call - direct DOM manipulation of the input box will not propagate to the extension.
When Mountain receives $scm:updateSourceControl it emits sky://scm/provider/changed to the Sky renderer with the updated provider state. The InstallScm.ts bridge handler finds the matching shim by numeric handle and applies InputModel.setValue(newValue) (or applyEdits as a fallback) so the workbench commit input reflects the extension-set text, then fires provider.onDidChange so the SCM panel re-renders.
sky://scm/register retry
The sky://scm/register registration in the Sky bridge calls __CEL_SERVICES__.SCM.registerSCMProvider(). Because __CEL_SERVICES__.SCM is populated asynchronously by the Output service accessor, there is a potential race on startup. The bridge retries registration up to 10 times with 200 ms between attempts before falling back to a DOM CustomEvent path, ensuring the SCM viewlet populates even when the workbench service resolves late during startup.
Important
gRPC traffic between Mountain and Cocoon for SCM flows over port 50051 (Mountain Vine server, Mountain → Cocoon direction). The reverse Cocoon → Mountain notification push uses port 50052.