Skip to content

Extract local branch sync logic and scope sync to local mode#65

Merged
juliusmarminge merged 2 commits intomainfrom
codething/1c0075e8
Feb 17, 2026
Merged

Extract local branch sync logic and scope sync to local mode#65
juliusmarminge merged 2 commits intomainfrom
codething/1c0075e8

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 17, 2026

Summary

  • Extract branch auto-sync decision logic into BranchToolbar.logic.ts via deriveSyncedLocalBranch.
  • Restrict automatic thread-branch sync to local mode (skip sync in worktree mode).
  • Keep existing guardrails: no sync when there is no active thread, when thread already has a worktree path, or when branch is already in sync.
  • Add focused unit tests in BranchToolbar.logic.test.ts covering local-mode sync and non-sync cases.

Testing

  • Added Vitest unit tests for deriveSyncedLocalBranch:
  • Syncs to current Git branch in local mode.
  • Does not sync in worktree mode.
  • Does not sync when active thread already targets a worktree path.
  • Lint: Not run.
  • Full test suite: Not run.

Open with Devin

Summary by CodeRabbit

  • Refactor

    • Improved branch synchronization so the app correctly updates a thread’s branch in local mode while avoiding unwanted switching in worktree contexts, respecting active thread/worktree state.
  • Tests

    • Added unit tests covering local-mode sync and scenarios where synchronization is skipped for worktree contexts.

Note

Low Risk
Small refactor that narrows when auto-sync runs and adds focused unit tests; limited surface area and no security/data-impacting changes.

Overview
Thread-branch auto-sync in BranchToolbar is refactored into a new deriveSyncedLocalBranch helper and is now explicitly disabled in worktree mode, preventing automatic branch changes while creating/using worktrees.

Adds Vitest unit coverage for the helper to ensure sync happens only when appropriate (local mode, no worktree path, and branch differs from git current).

Written by Cursor Bugbot for commit 86a2cbc. This will update automatically on new commits. Configure here.

- move branch-sync decision into `BranchToolbar.logic.ts`
- avoid auto-sync when env mode is `worktree` or thread has worktree path
- add unit tests for sync and no-sync cases
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Adds deriveSyncedLocalBranch and its input interface, integrates it into BranchToolbar to replace inline branch-lookup logic, and adds unit tests covering local-sync and worktree bypass scenarios.

Changes

Cohort / File(s) Summary
Branch sync utility & tests
apps/web/src/components/BranchToolbar.logic.ts, apps/web/src/components/BranchToolbar.logic.test.ts
Adds deriveSyncedLocalBranch and DeriveSyncedLocalBranchInput; implements logic to return the current local branch name or null based on activeThreadId, activeWorktreePath, envMode, activeThreadBranch, and queryBranches. Adds tests for local sync, worktree env bypass, and active worktree path bypass.
Component integration
apps/web/src/components/BranchToolbar.tsx
Replaces inline branch-derivation with deriveSyncedLocalBranch call; uses returned syncedBranch to dispatch SET_THREAD_BRANCH and adds envMode to effect dependencies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: extracting branch sync logic into a separate function and scoping that sync to local mode only, which matches the primary objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/1c0075e8

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 17, 2026

Scope branch auto-sync in BranchToolbar to local env mode by extracting deriveSyncedLocalBranch and updating the sync effect in BranchToolbar.tsx

Add deriveSyncedLocalBranch to gate syncing on local mode and absence of a worktree path, and update the BranchToolbar sync effect to use it. Add unit tests for the new logic in BranchToolbar.logic.test.ts.

📍Where to Start

Start with deriveSyncedLocalBranch in BranchToolbar.logic.ts, then review the useEffect in BranchToolbar.tsx.


Macroscope summarized 86a2cbc.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 17, 2026

Greptile Summary

This PR extracts the branch auto-sync decision logic from BranchToolbar.tsx into a pure function deriveSyncedLocalBranch in BranchToolbar.logic.ts, and restricts automatic thread-branch sync to local mode only (previously it would also fire in worktree mode). The refactor is well-structured — the logic is now independently testable, and envMode is correctly added to the useEffect dependency array.

Key observations:

  • The new deriveSyncedLocalBranch function correctly encodes all existing guardrails plus the new envMode !== "local" guard, and the function logic itself is sound.
  • Type unsafety at dispatch site: activeThreadId (string | undefined) is passed as threadId (string) to the SET_THREAD_BRANCH dispatch without a local narrowing guard. The logic function implicitly ensures activeThreadId is defined when it returns a non-null value, but this narrowing is invisible to TypeScript at the call site in BranchToolbar.tsx:101.
  • Missing test coverage: Three guardrails claimed in the PR description ("no active thread", "branch already in sync", queryBranches undefined) are unexercised by the test suite — only the worktreePath and envMode guards are tested.
  • The useEffect dependency array is correctly updated to include envMode.

Confidence Score: 4/5

  • Safe to merge — the behavioural change is correct and the latent type issue is unlikely to cause a runtime error.
  • The core logic change is correct and the new envMode guard prevents unintended branch syncing in worktree mode. The type unsafety (string | undefined passed as string) is protected at runtime by the logic function's guard, so no actual crash is expected. However, the missing test cases for documented guardrails and the unresolved TypeScript narrowing issue are worth addressing before merge.
  • Pay close attention to BranchToolbar.tsx (line 101, type narrowing on activeThreadId) and BranchToolbar.logic.test.ts (missing test cases for "no active thread" and "already in sync" guards).

Important Files Changed

Filename Overview
apps/web/src/components/BranchToolbar.logic.ts New pure function extracting branch sync decision logic. Logic is correct but could expose a type unsafety when activeThreadId (string
apps/web/src/components/BranchToolbar.logic.test.ts Three test cases cover main scenarios, but missing tests for: no active thread (activeThreadId: undefined), branch already in sync, and undefined queryBranches — all of which the PR description claims are guarded.
apps/web/src/components/BranchToolbar.tsx Correctly wires deriveSyncedLocalBranch into the useEffect and adds envMode to the dependency array. Contains a latent type unsafety: activeThreadId (string

Flowchart

flowchart TD
    A[useEffect fires\nwhen deps change] --> B[deriveSyncedLocalBranch]
    B --> C{activeThreadId\ndefined?}
    C -- No --> D[return null]
    C -- Yes --> E{activeWorktreePath\nnon-null?}
    E -- Yes --> D
    E -- No --> F{envMode\n=== 'local'?}
    F -- No --> D
    F -- Yes --> G[find current branch\nin queryBranches]
    G --> H{currentBranch\nfound?}
    H -- No --> D
    H -- Yes --> I{currentBranch.name\n=== activeThreadBranch?}
    I -- Yes --> D
    I -- No --> J[return currentBranch.name]
    D --> K[return: no dispatch]
    J --> L[dispatch SET_THREAD_BRANCH\nthreadId: activeThreadId\nbranch: syncedBranch\nworktreePath: null]
Loading

Last reviewed commit: 3756bbf

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 99 to 103
dispatch({
type: "SET_THREAD_BRANCH",
threadId: activeThreadId,
branch: current.name,
branch: syncedBranch,
worktreePath: null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unsafe threadId type passed to dispatch

activeThreadId is typed string | undefined (derived from activeThread?.id at line 47), but SET_THREAD_BRANCH requires threadId: string. This compiles only because TypeScript doesn't narrow the type across the deriveSyncedLocalBranch call boundary — it doesn't know that a non-null return from that function implies activeThreadId is defined.

The fix is to explicitly assert the non-undefined guarantee that the logic function already encodes, so the narrowing is visible to the type checker at the call site:

Suggested change
dispatch({
type: "SET_THREAD_BRANCH",
threadId: activeThreadId,
branch: current.name,
branch: syncedBranch,
worktreePath: null,
dispatch({
type: "SET_THREAD_BRANCH",
threadId: activeThreadId!,
branch: syncedBranch,
worktreePath: null,
});

Alternatively, guard with if (!activeThreadId) return; before the dispatch call (redundant at runtime, but type-safe and consistent with the pattern used elsewhere in this file at lines 115 and 120).

Comment on lines +20 to +56
describe("deriveSyncedLocalBranch", () => {
it("syncs to git current branch in local mode", () => {
const result = deriveSyncedLocalBranch({
activeThreadId: "thread-1",
activeWorktreePath: null,
envMode: "local",
activeThreadBranch: "feature/demo",
queryBranches: branches,
});

expect(result).toBe("main");
});

it("does not sync when creating a new worktree", () => {
const result = deriveSyncedLocalBranch({
activeThreadId: "thread-1",
activeWorktreePath: null,
envMode: "worktree",
activeThreadBranch: "feature/demo",
queryBranches: branches,
});

expect(result).toBeNull();
});

it("does not sync when thread already targets a worktree path", () => {
const result = deriveSyncedLocalBranch({
activeThreadId: "thread-1",
activeWorktreePath: "/tmp/repo/worktrees/feature-demo",
envMode: "local",
activeThreadBranch: "feature/demo",
queryBranches: branches,
});

expect(result).toBeNull();
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing test cases for documented guardrails

The PR description explicitly states "Keep existing guardrails: no sync when there is no active thread, when thread already has a worktree path, or when branch is already in sync." However, the test suite is missing coverage for:

  1. No active thread (activeThreadId: undefined) — the first guard in the logic (!activeThreadId).
  2. Branch already in sync (activeThreadBranch === currentBranch.name) — the equality check guard.
  3. No branch data (queryBranches: undefined) — defensive path when the query hasn't resolved.

The "does not sync when thread already targets a worktree path" test covers the activeWorktreePath guard, and the "does not sync when creating a new worktree" test covers the envMode guard. The missing cases are easy to add and would fully validate the claimed guardrails:

it("does not sync when there is no active thread", () => {
  const result = deriveSyncedLocalBranch({
    activeThreadId: undefined,
    activeWorktreePath: null,
    envMode: "local",
    activeThreadBranch: "feature/demo",
    queryBranches: branches,
  });
  expect(result).toBeNull();
});

it("does not sync when branch is already in sync", () => {
  const result = deriveSyncedLocalBranch({
    activeThreadId: "thread-1",
    activeWorktreePath: null,
    envMode: "local",
    activeThreadBranch: "main",
    queryBranches: branches,
  });
  expect(result).toBeNull();
});

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/BranchToolbar.tsx`:
- Around line 90-103: The TS2322 error occurs because deriveSyncedLocalBranch
returning a value doesn't convince TypeScript that activeThreadId is a string;
before calling dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId,
... }) add an explicit guard that ensures activeThreadId is defined (e.g., if
(!activeThreadId) return;) so threadId is narrowed to string, then proceed to
dispatch using the already computed syncedBranch; reference
deriveSyncedLocalBranch, activeThreadId, and the dispatch call with type
"SET_THREAD_BRANCH".

Comment thread apps/web/src/components/BranchToolbar.tsx
- Prevent `SET_THREAD_BRANCH` dispatch unless both `activeThreadId` and `syncedBranch` exist
- Avoids updating thread-branch state without an active thread context
@juliusmarminge juliusmarminge merged commit 6d8d39a into main Feb 17, 2026
4 checks passed
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.

1 participant