Skip to content

feat: add Sapling SCM support#295

Merged
tomasz-tomczyk merged 11 commits intomainfrom
feat/sapling-scm-support
Apr 22, 2026
Merged

feat: add Sapling SCM support#295
tomasz-tomczyk merged 11 commits intomainfrom
feat/sapling-scm-support

Conversation

@tomasz-tomczyk
Copy link
Copy Markdown
Owner

Summary

  • Extract a VCS interface from git-specific code, implement GitVCS (zero behavior change for git users)
  • Implement SaplingVCS backend using sl CLI for all operations
  • Auto-detect Sapling repos (.sl/ directory) with --vcs flag override
  • Handle Sapling's lack of staging area (staged/unstaged scopes hidden automatically)
  • Generalize frontend labels ("vcs mode" instead of "git mode")

Closes #288

Known limitations

  • No staging area: Sapling has no index — staged/unstaged scopes are hidden
  • Stacked diffs: Per-stack review is out of scope for v1
  • --vcs flag: Only applies to the serve command, not crit stop/crit status (tracked as TODO)

Test plan

  • All existing tests pass (git behavior unchanged)
  • Sapling parser unit tests pass (status + diff-stat formats)
  • SaplingVCS unit tests pass (interface compliance, scope hiding, commit log parsing)
  • Manual: crit in a Sapling repo detects and opens review UI
  • Manual: crit --vcs git forces git backend in colocated repos

@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

Pre-release binaries for testing

Built from commit 80e2009 on feat/sapling-scm-support. Download for your platform:

Platform Binary
macOS (Apple Silicon) crit-darwin-arm64
macOS (Intel) crit-darwin-amd64
Linux (x86_64) crit-linux-amd64
Linux (ARM64) crit-linux-arm64

Binaries are attached as assets on the v0.0.0-sapling-rc1 pre-release.

Quick install

# macOS (Apple Silicon)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc1/crit-darwin-arm64 -o crit && chmod +x crit

# Linux (x86_64)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc1/crit-linux-amd64 -o crit && chmod +x crit

Or build from source:

go install github.com/tomasz-tomczyk/crit@feat/sapling-scm-support

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 18, 2026

  1. auto detection is not working in all cases. I have a diff ready.
  2. default behavior for git branch review (the entire branch) is really getting at me.
    e.g. I have this branch checked out and my own commit on top. crit would show me all 1300 lines instead of just what I added. at minimum, it should be possible to select the base revision for the diff. ideally, we will also do something intelligent automatically. I don't mind sending a diff for this one.

I think it will be best if you land this diff first, unless you want to fix the detection problem yourself.

@tomasz-tomczyk tomasz-tomczyk force-pushed the feat/sapling-scm-support branch from 80e2009 to 6eec910 Compare April 18, 2026 11:21
@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

Updated: rc2 binaries available

Rebased on latest main (732e844), all review findings fixed. New binaries:

v0.0.0-sapling-rc2 pre-release

# macOS (Apple Silicon)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc2/crit-darwin-arm64 -o crit && chmod +x crit

# macOS (Intel)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc2/crit-darwin-amd64 -o crit && chmod +x crit

# Linux (x86_64)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc2/crit-linux-amd64 -o crit && chmod +x crit

# Linux (ARM64)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc2/crit-linux-arm64 -o crit && chmod +x crit

Or build from source: go install github.com/tomasz-tomczyk/crit@feat/sapling-scm-support

@omry — would love to hear how it works for you! If auto-detection isn't picking up your Sapling repo, crit --vcs sl should force it.

@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

  1. auto detection is not working in all cases. I have a diff ready.

Sure! Happy for you to fork off of this branch? Or you can share the diff and I can have Claude figure it out 😅

default behavior for git branch review (the entire branch) is really getting at me.
e.g. I have this branch checked out and my own commit on top. crit would show me all 1300 lines instead of just whatI added. at minimum, it should be possible to select the base revision for the diff. ideally, we will also do something intelligent automatically. I don't mind sending a diff for this one.

We do have --base-branch... we also have a commit picker but that's only visible on feature branches so not sure how it works for Sapling.

Is this something we should revisit with #300 do you think? GitHub just added stacked PRs so I wanted to see if it's relevant for git implementation too.

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 18, 2026

  1. auto detection is not working in all cases. I have a diff ready.

Sure! Happy for you to fork off of this branch? Or you can share the diff and I can have Claude figure it out 😅

Hehe, sure.
I'll share my diff here once I test on top of your update.

default behavior for git branch review (the entire branch) is really getting at me.
e.g. I have this branch checked out and my own commit on top. crit would show me all 1300 lines instead of just whatI added. at minimum, it should be possible to select the base revision for the diff. ideally, we will also do something intelligent automatically. I don't mind sending a diff for this one.

We do have --base-branch... we also have a commit picker but that's only visible on feature branches so not sure how it works for Sapling.

not quiet sure if this is the thing that bugs me (or that my solution is really the right one):
my understanding is that currently crit is meant to review uncommitted changes.
It seems to assume that a feature branch with multiple stacked commits should all be reviewed.
For example. I tested it on git and it looks the same.
If I create a branch and make a commit, then have some uncommitted changes and I run crit - it will show me the diff from the base of the branch (including both the committed and uncommitted changes). Is this intentional?

Is this something we should revisit with #300 do you think?

I think it's not related to the sapling support. we can discuss it separately.

GitHub just added stacked PRs so I wanted to see if it's relevant for git implementation too.

They did!?
Wow, that timing :)
I signed up to the waiting list.

This is definitely making #300 much more relevant though and we can discuss it there in light of the upcoming GitHub support.

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 18, 2026

Here is the diff (this is from sl so not exactry a git diff).
Claude should be able to deal with it.
Ask it to import as a commit to your stack and to review.

changeset:   8e73ba3660362c99f616a30e4e38fa416738d403  (@)
parent:      0ef8be177cc6b20db673dab6289450fc257c2ee5
user:        Omry Yadan <omry@yadan.net>
date:        Sat, 18 Apr 2026 21:31:52 +0800

    fix: detect Sapling repos that store metadata under .git/sl

    Recognize .git/sl as Sapling metadata during VCS auto-detection so mixed
    Git/Sapling repositories select the Sapling backend by default.

    Also include the detected VCS in `crit status` output to make backend
    selection easier to debug.

diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
 /crit
 /crit.exe
 
 # Review session files (generated at runtime)
 *.comments.json
 *.review.md
diff --git a/main.go b/main.go
--- a/main.go
+++ b/main.go
@@ -2021,8 +2021,10 @@
 		os.Exit(1)
 	}
 
+	vcsName := ""
 	branch := ""
 	if vcs := DetectVCS(""); vcs != nil {
+		vcsName = vcs.Name()
 		branch = vcs.CurrentBranch()
 	}
 
@@ -2050,15 +2052,16 @@
 	}
 
 	if jsonOutput {
-		printStatusJSON(branch, revPath, revExists, matchedSession)
+		printStatusJSON(vcsName, branch, revPath, revExists, matchedSession)
 		return
 	}
 
-	printStatusHuman(branch, revPath, revExists, matchedSession)
+	printStatusHuman(vcsName, branch, revPath, revExists, matchedSession)
 }
 
-func printStatusJSON(branch, revPath string, revExists bool, session *sessionEntry) {
+func printStatusJSON(vcsName, branch, revPath string, revExists bool, session *sessionEntry) {
 	result := map[string]interface{}{
+		"vcs":                vcsName,
 		"branch":             branch,
 		"review_file":        revPath,
 		"review_file_exists": revExists,
@@ -2096,7 +2099,10 @@
 	}
 }
 
-func printStatusHuman(branch, revPath string, revExists bool, session *sessionEntry) {
+func printStatusHuman(vcsName, branch, revPath string, revExists bool, session *sessionEntry) {
+	if vcsName != "" {
+		fmt.Printf("VCS:         %s\n", vcsName)
+	}
 	if branch != "" {
 		fmt.Printf("Branch:      %s\n", branch)
 	}
diff --git a/sapling_test.go b/sapling_test.go
--- a/sapling_test.go
+++ b/sapling_test.go
@@ -1,6 +1,10 @@
 package main
 
-import "testing"
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
 
 // Compile-time interface compliance check.
 var _ VCS = &SaplingVCS{}
@@ -185,3 +189,31 @@
 		}
 	}
 }
+
+func TestHasSLDirFrom_DetectsDotSL(t *testing.T) {
+	root := t.TempDir()
+	child := filepath.Join(root, "nested", "repo")
+	if err := os.MkdirAll(filepath.Join(root, ".sl"), 0o755); err != nil {
+		t.Fatalf("mkdir .sl: %v", err)
+	}
+	if err := os.MkdirAll(child, 0o755); err != nil {
+		t.Fatalf("mkdir child: %v", err)
+	}
+	if !hasSLDirFrom(child) {
+		t.Fatal("expected hasSLDirFrom to detect .sl metadata")
+	}
+}
+
+func TestHasSLDirFrom_DetectsDotGitSL(t *testing.T) {
+	root := t.TempDir()
+	child := filepath.Join(root, "nested", "repo")
+	if err := os.MkdirAll(filepath.Join(root, ".git", "sl"), 0o755); err != nil {
+		t.Fatalf("mkdir .git/sl: %v", err)
+	}
+	if err := os.MkdirAll(child, 0o755); err != nil {
+		t.Fatalf("mkdir child: %v", err)
+	}
+	if !hasSLDirFrom(child) {
+		t.Fatal("expected hasSLDirFrom to detect .git/sl metadata")
+	}
+}
diff --git a/vcs.go b/vcs.go
--- a/vcs.go
+++ b/vcs.go
@@ -102,7 +102,8 @@
 		return &SaplingVCS{}
 	}
 
-	// Auto-detect: check for .sl/ first since Sapling repos on top of git have both.
+	// Auto-detect: check for Sapling metadata first since Sapling repos on top of
+	// git can have both backends available.
 	if hasSLDir() {
 		return &SaplingVCS{}
 	}
@@ -114,16 +115,24 @@
 	return nil
 }
 
-// hasSLDir checks whether a .sl/ directory exists at or above the current directory.
+// hasSLDir checks whether Sapling metadata exists at or above the current
+// directory. Sapling repos may store metadata either in .sl/ or in .git/sl/.
 func hasSLDir() bool {
 	dir, err := os.Getwd()
 	if err != nil {
 		return false
 	}
+	return hasSLDirFrom(dir)
+}
+
+func hasSLDirFrom(dir string) bool {
 	for {
 		if info, err := os.Stat(filepath.Join(dir, ".sl")); err == nil && info.IsDir() {
 			return true
 		}
+		if info, err := os.Stat(filepath.Join(dir, ".git", "sl")); err == nil && info.IsDir() {
+			return true
+		}
 		parent := filepath.Dir(dir)
 		if parent == dir {
 			break

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 18, 2026

One thing I noticed:
Running sl in a git repository, even without doing anything - creates a .git/sl directory.
After this diff, crit will start to default to sl for this repo.

Maybe worth some output when detecting a git repo as sl repo (.git/sl exists) to reduce user confusion.

@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

tomasz-tomczyk commented Apr 18, 2026

my understanding is that currently crit is meant to review uncommitted changes.
It seems to assume that a feature branch with multiple stacked commits should all be reviewed.
For example. I tested it on git and it looks the same.

If I understand what you're saying right, yes this is intended. In git terms, when on a featue branch, all changes from that branch will be shown. There are toggles in the header to switch back to just unstaged view or you can select specific commit from what has been comitted.

Not 100% sure how this translates to Sapling though and might need some UX adaptation.

If I create a branch and make a commit, then have some uncommitted changes and I run crit - it will show me the diff from the base of the branch (including both the committed and uncommitted changes). Is this intentional?

Yes. Can switch in the header to just show unstaged.

@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

rc3 — improved detection + config support

@omry thanks for the diff! Applied your changes with one modification to the .git/sl behavior:

What changed

Your diff (applied):

  • hasSLDirFrom() extraction for testability
  • VCS name shown in crit status output (human + JSON)

Modified behavior for .git/sl:

  • .sl/ directory → auto-selects Sapling (unchanged, this is a real Sapling repo)
  • .git/sl/ directory → stays on git, prints a hint: Sapling detected. Use --vcs sl or set "vcs": "sl" in config

The reason: running sl once in a git repo creates .git/sl/, so auto-selecting Sapling there would surprise git users who just tried it once.

New: "vcs" config field

You can now set your preferred VCS in .crit.config.json (project-level or global):

{
  "vcs": "sl"
}

This is a preference, not a hard requirement — if sl isn't in PATH, it falls back to git with a warning.

Precedence: --vcs flag > config vcs > auto-detect.

Binaries

v0.0.0-sapling-rc3

# macOS (Apple Silicon)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc3/crit-darwin-arm64 -o crit && chmod +x crit

# macOS (Intel)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc3/crit-darwin-amd64 -o crit && chmod +x crit

# Linux (x86_64)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc3/crit-linux-amd64 -o crit && chmod +x crit

# Linux (ARM64)
curl -L https://github.com/tomasz-tomczyk/crit/releases/download/v0.0.0-sapling-rc3/crit-linux-arm64 -o crit && chmod +x crit

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 18, 2026

If I understand what you're saying right, yes this is intended. In git terms, when on a featue branch, all changes from that branch will be shown. There are toggles in the header to switch back to just unstaged view or you can select specific commit from what has been comitted.

Not 100% sure how this translates to Sapling though and might need some UX adaptation.

ah, it looks like it's always disabled in sapling:
Generally speaking, sapling has no staging area.
So interesting stuff is either uncommitted (sl status will show that) or committed (sl without arguments shows it).

I suggest the following borrowing terminology from sl status:

M = modified
A = added
R = removed
C = clean
! = missing (deleted by a non-sl command, but still tracked)
? = not tracked
I = ignored

Modified should include M, A, R and !. (diff content via sl diff)
Committed should be the files in the active commit (diff content via sl show).
Later when we do stacked diffs, we can also move up and down the stack (sl prev, sl next).

image

Comment thread vcs.go
// Check if Sapling metadata exists under .git/sl — this means sl was used
// here but it's primarily a git repo. Hint but don't switch automatically.
if hasGitSLDir() {
fmt.Fprintf(os.Stderr, "Hint: Sapling detected. Use --vcs sl or set \"vcs\": \"sl\" in config to use Sapling.\n")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest hint and behavior:

This Git Repository has Sapling metadata. 
- Use --vcs sl to override Crit to use Sapling
- Set \"vcs\": \"sl\" to always use Sapling and suppress this hint
- Set \"vcs\": \"git\" to always use Git and suppress this hint

Comment thread config.go
AuthUserName string `json:"auth_user_name,omitempty"`
AuthUserEmail string `json:"auth_user_email,omitempty"`
CleanupOnApprove *bool `json:"cleanup_on_approve,omitempty"`
VCS string `json:"vcs,omitempty"` // preferred VCS backend: "git", "sl"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should not be a preference.
If a user tells you to use Sapling for a repo and you can't find it, falling back to git is wrong. it will just make things worse.

Error out saying Sapling binary sl is not found in the path.

@tomasz-tomczyk
Copy link
Copy Markdown
Owner Author

So interesting stuff is either uncommitted (sl status will show that) or committed (sl without arguments shows it).

Right, so if I was to add commit picker to crit on your default branch, I suppose then this could work? Might have to put off SL integration until that is done then

@omry
Copy link
Copy Markdown
Contributor

omry commented Apr 19, 2026

So interesting stuff is either uncommitted (sl status will show that) or committed (sl without arguments shows it).

Right, so if I was to add commit picker to crit on your default branch, I suppose then this could work? Might have to put off SL integration until that is done then

Base commit picker would be nice, but I think the minimum for sl support is to allow me to select modified (aka dirty) files.
this is similar to the functionality you have for git showing unstaged.

secondary: I think the current default is not useful (at least not for me, on sl).
It would be good if I could change the default view type (to Dirty in my case).

tomasz-tomczyk and others added 10 commits April 22, 2026 16:22
Parse sl status (Mercurial format) and sl diff --stat output into
crit's FileChange and NumstatEntry types. (#288)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#288)

Phase 1 of Sapling SCM support: extract a VCS interface from existing
git-specific code so multiple VCS backends can be supported.

- vcs.go: VCS interface with all operations + DetectVCS() auto-detection
- git_vcs.go: GitVCS struct delegating to existing git.go functions
- git_vcs_test.go: interface compliance and basic method tests
- session.go: VCS field on Session, NewSessionFromVCS constructor,
  VCSName in SessionInfo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r.go (#288)

Replace direct IsGitRepo()/git.go function calls with DetectVCS() and
VCS interface methods throughout the codebase:

- main.go: add --vcs flag, use DetectVCS in createSession, runReview,
  resolveServerConfig, serveSessionKey, runStop, runStatus, runConfig
- session.go: use VCS in NewSessionFromFiles, ChangeBaseBranch,
  EnsureFileEntry, GetCommits; resolveGitContext returns VCS
- watch.go: use VCS for WorkingTreeFingerprint, RefreshDiffs, DiffNumstat
- server.go: use VCS in handleBranches and handleFilesList
- github.go: use DetectVCS in resolveReviewPath, resolveReviewPathFromDaemon
- share.go: use DetectVCS in loadShareConfig
- git.go: add .sl to skipDirs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full implementation of the VCS interface for Sapling. Uses sl CLI
for all operations. Handles bookmark-based branching, lack of staging
area, and Mercurial-format status output. Wire into DetectVCS. (#288)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Thread baseBranch through serverConfig to VCS.SetDefaultBranchOverride()
- Route scoped queries (scopedHunks, availableScopes, GetSessionInfoScoped,
  loadScopedFileState, computeScopedDiffHunks) through VCS interface
- Pass VCS to ensureLoaded and populateEagerFile for lazy/eager file diffs
- Add DetectVCS("git") test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add "vcs" field to config (e.g. "vcs": "sl" in .crit.config.json)
- Precedence: --vcs flag > config vcs > auto-detect
- Config VCS falls back gracefully if backend unavailable (sl not in PATH)
- Detect .git/sl as hint (not auto-select) — prints suggestion to use --vcs sl
- Show VCS name in crit status output (human + JSON)
- Extract hasSLDirFrom() for testability

Co-authored-by: Omry Yadan <omry@yadan.net>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route FileContentAtRef through VCS interface (was direct git bypass),
use configured VCS for author fallback, guard empty baseRef, use
--change for initial commit support, add vcs to config docs/README.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tomasz-tomczyk tomasz-tomczyk force-pushed the feat/sapling-scm-support branch from b257361 to 4374a7c Compare April 22, 2026 15:44
@tomasz-tomczyk tomasz-tomczyk marked this pull request as ready for review April 22, 2026 15:44
gofmt formatting, nilerr nolint for intentional graceful degradation,
gocyclo nolint for ChangeBaseBranch (inherent rollback complexity).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tomasz-tomczyk tomasz-tomczyk merged commit e759b46 into main Apr 22, 2026
4 checks passed
@tomasz-tomczyk tomasz-tomczyk deleted the feat/sapling-scm-support branch April 22, 2026 16:01
tomasz-tomczyk pushed a commit that referenced this pull request Apr 29, 2026
Replaces the gate-and-abort push flow with a partition-and-export model.
Today, a single stale or unanchored comment aborts the entire push; now
crit push splits comments into three buckets at submit time:

- postable     -> pushed to GitHub via gh review API as before
- full_stack   -> kept local, written to ~/.crit/exports/<pr>-<ts>.md
- unmapped     -> kept local (empty HeadSHA in range mode, or stale
                  HeadSHA), written to the same export file

Postable comments still flow through createGHReview unchanged. The
orphan buckets get a markdown dump grouped by reason ("Full-stack-only",
"Stale or unanchored") so the user has a paste-able record. A one-line
summary on stdout reports both halves: "Posted N comments. M comments
exported to <path>."

The --dry-run path now prints the bucket plan ("Push plan for PR #295:
12 postable, 2 full-stack, 1 stale.") plus per-bucket detail with
truncated comment bodies.

UX changes
- The "Switch to Layer diff before posting a platform review" abort is
  gone. Full-stack comments are silently bucketed instead.
- A single stale comment no longer kills the whole push.
- gh failures only exit 1 when there are no orphans to fall back on;
  if we wrote an export file, exit 0 (something useful happened).

Removed helpers: applyPushGates, filterCommentsForPush,
commentPassesPushGates, displayPushDryRun. Kept: resolveCurrentPRHead,
parsePushFlags, postPushReplies. Added: bucketCommentsForPush,
renderOrphanMarkdown, writeOrphanExport, summarizeBuckets,
detailedDryRun, bucketsToGHComments, exportsDir.

Tests
- New push_buckets_test.go covering pure layer, mixed scope, stale,
  no-anchor, working-tree, resolved-skipped, GitHub-anchored-skipped,
  markdown rendering, export file creation, summary formatting,
  dry-run sectioning, GH comment shaping, deterministic ordering.
- Converted the four old TestApplyPushGates_* tests in
  focus_session_test.go to bucket assertions covering the same
  semantic concerns (full-stack diversion, stale head, no anchor,
  working-tree legacy filter).
- Scrubbed remaining 'plannotator' references that survived in
  spec/plan markdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tomasz-tomczyk pushed a commit that referenced this pull request Apr 29, 2026
Drop the old focus-picker popover (Working tree / Your stack / Other PRs /
Branches) in favour of a narrower, more focused UI inspired by stacked-PR
review tools that only handle in-stack navigation.

Frontend
- New stack breadcrumb in the header: main > #294 > #295 (reviewing) >
  #296, rendered inline (not a popover). Default-branch click flips
  diff_scope to full_stack on the current focus; other entries swap focus
  via /api/focus. Visibility uses stack.length > 1 (not is_stacked) so
  local-only stacks and Sapling-style commit chains also get the
  breadcrumb. Long stacks collapse the middle into an ellipsis.
- New Working tree pill, visible whenever focus is range in git mode.
- Layer/full-stack toggle now appears whenever default_sha is resolved,
  not only for is_stacked focus.
- Other PRs / Remote branches sections deleted from the frontend; backend
  /api/picker still returns them but the frontend stops consuming them.
- focus-changed SSE handler fixed to parse the wrapper.content payload
  correctly (the old handler relied on page reloads in tests to mask this).

Backend (picker.go)
- detectStack adds tier-3 fallback for naked-commit ancestors on the topic
  chain (commits reachable from HEAD but not the default branch). Uses
  first-line commit subject as the label so Sapling-style git stacks of
  unbranched commits surface in the picker.
- Topic-chain filter prevents long-lived feature branches from drowning
  the breadcrumb in default-branch ancestors.

CLI
- Help text now lists --pr <num|url> and --range <baseSHA>..<headSHA> as
  the entry points into range mode.

Tests
- Rewrote focus-picker.rangemode.spec.ts and focus-picker-wt-entry for
  the breadcrumb. Added breadcrumb-current-marker,
  breadcrumb-default-branch, breadcrumb-ellipsis, breadcrumb-local-stack,
  breadcrumb-no-pr specs.
- Updated focus-switch.rangemode for the working-tree pill.
- loading.filemode + nogit.nogit assert the breadcrumb and pill stay
  hidden in non-VCS modes.
- range-loading header-label test now checks the breadcrumb reviewing
  marker instead of the removed #focusPickerLabel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sapling scm support

2 participants