Skip to content

feat(desktop): paginated branch picker with checkout + open actions#3397

Merged
Kitenite merged 25 commits intomainfrom
workspace-branch-review
Apr 14, 2026
Merged

feat(desktop): paginated branch picker with checkout + open actions#3397
Kitenite merged 25 commits intomainfrom
workspace-branch-review

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 13, 2026

Summary

Reworks the v2 new-workspace modal's branch picker so it scales to thousands of branches and exposes direct actions for existing workspaces. Adds a pure-checkout path alongside the existing prompt-driven fork flow.

What changed

Host-service (workspace-creation.ts)

  • searchBranches: cursor pagination, refresh flag gated by a 30s TTL per project (avoids thrashing git fetch on every keystroke), server-side filter (branch / worktree), reflog-based recency ordering.
  • Per-row metadata now includes isLocal / isRemote / worktreePath / recency / isCheckedOut. worktreePath is limited to Superset-managed worktrees under <repoPath>/.worktrees/ so the primary clone's branch doesn't get treated as "has workspace".
  • isCheckedOut is derived from the full git worktree list (including primary) — used by the UI to disable Checkout when a branch is already checked out elsewhere.
  • New checkout procedure: git worktree add <path> <branch> (no -b, no branch-name dedup). Resolves local vs origin/<branch> automatically; fetches the latest when only the remote ref exists. Same cloud-workspace registration + setup-script + rollback path as create.

Renderer

  • useBranchContext is now a useInfiniteQuery. Types (BranchFilter, BranchRow) are derived from the host-service zod schema via inferRouterInputs / inferRouterOutputs — single source of truth, no duplicate enums.
  • CompareBaseBranchPicker gets a 2-tab strip (Branch / Worktree) and an IntersectionObserver sentinel for infinite scroll. Client no longer filters — the server does.
  • Per-row hover action button, tab-specific: Check out on Branch (disabled with tooltip when isCheckedOut), Open on Worktree (navigates to the existing workspace). Clicking the row body still sets the base branch, preserving today's prompt-driven fork flow.
  • New useCheckoutDashboardWorkspace hook — sibling of useCreateDashboardWorkspace.

Design doc

  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md covers the data model, pagination strategy, and the action-intent design.

Known limitations (punted to follow-ups)

  • Project-not-cloned case: if searchBranches is called for a project that has no local projects row in the host-service DB, it still returns empty. Discussed; left for a separate PR that either auto-sets-up the project on modal open or falls back to GitHub API.
  • Prompt carry-over on Open: hitting Open on a worktree row with a typed prompt currently drops the prompt. Design doc includes a path (pending-seed zustand store + wiring the v2 ChatPane's initialLaunchConfig); deferred per review.
  • Recency section headers ("Recent" / "Other") — server already emits in recency order; UI grouping is a small follow-up.

Test plan

  • Open modal on a repo with local branches, remote-only branches, and at least one Superset workspace. Verify the Branch tab hides worktree'd branches and the Worktree tab shows only them.
  • Verify click-on-row still sets the base branch (unchanged behavior) and the submit flow creates a new branch derived from the prompt.
  • On a row where the branch is checked out in the main clone (typically main), confirm the Check out button is disabled with the tooltip.
  • Use Check out on a branch without a worktree — new workspace opens, branch is reused (no new branch created), setup script runs if configured.
  • Use Open on the Worktree tab — modal closes, navigates to the existing workspace.
  • Search with the query field — results filter server-side; scroll loads more pages.
  • On a repo with thousands of branches, confirm the first page renders quickly and scrolling continues to paginate.

Summary by cubic

Reworked the v2 new‑workspace branch picker to handle large repos and added per‑row actions, with fork/checkout/adopt unified under a pending flow and safer git‑ref handling. Also extracted a picker controller and linked‑context hooks to simplify PromptGroup and reduce plumbing.

  • New Features

    • Branch picker: Branch/Worktree tabs, server search with cursor pagination and infinite scroll, remote badges, and actions — Check out (Branch), Open/Create (Worktree). Row click still selects the base branch.
    • Host service: searchBranches with pagination and 30s refresh TTL, server filter (branch/worktree), and richer metadata (isLocal, isRemote, recency, worktreePath, hasWorkspace, isCheckedOut). Returns items + nextCursor.
    • Flows: Fork/Checkout/Adopt run via the pending page (typed intent, retry, warnings), and navigation waits for the new workspace to sync.
    • Checkout/Adopt: Checkout reuses an existing branch (remote‑only creates a local tracking branch via --track -b). Adopt registers a fresh cloud row for an existing .worktrees/<branch> and replaces any stale local mapping; idempotent.
    • Docs: All v2 creation design moved to apps/desktop/V2_WORKSPACE_CREATION.md (folded previous fallback doc); removed packages/host-service/WORKSPACE_CREATION_FALLBACK.md.
  • Bug Fixes

    • Git ref safety: Discriminated ResolvedRef using full refnames; resolveRef accepts origin/<branch>; added tests and packages/host-service/GIT_REFS.md. Lint script forbids short‑name prefix checks.
    • Start point: Prefer a local branch over remote‑tracking; picker sends a baseBranchSource hint so the server uses the exact start point the user selected.
    • Worktree accuracy: Client’s cloud‑synced collection is the source of truth for “has workspace”; Open is host‑scoped with a branch‑only fallback; adopt orphan worktrees; adopt always creates a fresh cloud row and pending waits for sync.
    • UX/a11y and flow hardening: Row actions show on focus; disabled Check out uses aria-disabled; infinite scroll guarded; actions respect a typed workspace name; typed pending‑row schema and pure intent‑payload builders with tests; reset pending‑page refs on pendingId change; clear progress on early errors; lint distinguishes real ripgrep errors.

Written for commit 1b8d8f1. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Server-backed branch search with cursored infinite scroll, query+filter (Branch vs Worktree) and per-page refresh
    • Branch picker UI: search input, Branch/Worktree tabs, remote badges, checked-out/worktree indicators, and infinite-load sentinel
    • Per-row actions: Check out, Open existing workspace, or Create/Adopt worktree; navigates to workspace on success with toast errors on failure
    • New checkout/adopt flows with safer conflict handling and rollback
  • Documentation

    • Added v2 branch discovery design doc describing client/server behavior and APIs

…actions

Rework the v2 new-workspace modal's branch picker to handle thousands of
branches and surface existing workspaces directly.

- Host-service searchBranches: cursor pagination, refresh flag gated by
  30s TTL, server-side filter (branch/worktree), reflog-based recency,
  git-worktree-derived worktreePath, isCheckedOut flag.
- New checkout procedure: git worktree add <path> <existing-branch>
  (no -b, no dedup). Auto-resolves local vs origin/<branch>.
- Renderer: useInfiniteQuery + IntersectionObserver for infinite scroll;
  tabs for Branch vs Worktree filters; hover-reveal action buttons —
  Check out on Branch tab (disabled when isCheckedOut), Open on Worktree
  tab (navigates to existing workspace). Click row body still sets base
  branch for the prompt-driven fork flow.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds server-side paginated branch discovery with richer per-branch metadata and branch/worktree filtering; client infinite-query integration with Branch/Worktree tabs and per-row checkout/open/adopt actions; new host-service checkout/adopt TRPC mutations; and React hooks and UI wiring.

Changes

Cohort / File(s) Summary
Design Document
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md
New design doc specifying workspaceCreation.searchBranches contract, cursor pagination, refresh TTL, BranchRow shape, server discovery/sorting/filtering, and client behavior.
Branch Context Hook
apps/desktop/.../hooks/useBranchContext/useBranchContext.ts, apps/desktop/.../hooks/useBranchContext/index.ts
Switched from useQuery to useInfiniteQuery; added query + filter inputs; export BranchRow/BranchFilter types; return flattened branches, defaultBranch, and pagination helpers.
CompareBaseBranchPicker
apps/desktop/.../CompareBaseBranchPicker/CompareBaseBranchPicker.tsx
Externalized branchSearch/branchFilter props; accepts full BranchRow[] (adds isRemote/recency/worktreePath/isCheckedOut); adds Branch vs Worktree tabs, per-row actions (Check out/Open/Create), disabled/tooltip for checked-out branches, and infinite-scroll sentinel.
PromptGroup / Form Wiring
apps/desktop/.../PromptGroup/PromptGroup.tsx
Introduced branchSearch/branchFilter state and pagination wiring; use new defaultBranch fallback; implemented onCheckoutBranch, onOpenExisting, and onAdoptWorktree flows (UUID pendingId, modal close, host hooks, navigation, toast errors).
Checkout & Adopt Hooks
apps/desktop/.../hooks/useCheckoutDashboardWorkspace/..., apps/desktop/.../hooks/useAdoptWorktree/..., .../index.ts
Added useCheckoutDashboardWorkspace and useAdoptWorktree hooks and input types; resolve host URL (local vs relay), create host-service client and call workspaceCreation.checkout / workspaceCreation.adopt; added barrel exports.
TRPC Workspace Creation Router
packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts
searchBranches redesigned: cursor pagination (base64url offset), reduced limit, refresh TTL with conditional git fetch --prune, unified refs (heads + remotes), worktree inspection, reflog recency, filter by branch/worktree, deterministic sorting, items shape with isRemote/recency/worktreePath/isCheckedOut and nextCursor. Added checkout and adopt mutations with worktree creation/adoption, cloud registration, local DB inserts, rollback and idempotency handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client / UI
    participant HostService as Host Service
    participant Git as Git Repository
    participant DB as Local DB

    Client->>HostService: searchBranches(projectId, hostUrl, query, filter, cursor?, refresh)
    activate HostService
    alt refresh=true & TTL elapsed
        HostService->>Git: git fetch --prune --quiet --no-tags
        Git-->>HostService: fetch complete
    end
    HostService->>Git: git for-each-ref (refs/heads + refs/remotes/origin)
    Git-->>HostService: refs
    HostService->>Git: git worktree list --porcelain
    Git-->>HostService: worktree list & checked-out branches
    HostService->>Git: git log -g --grep-reflog=checkout:
    Git-->>HostService: reflog entries
    HostService->>DB: query local workspace rows
    DB-->>HostService: workspace rows
    HostService->>HostService: merge refs, apply filter/query, sort, slice -> {items, defaultBranch, nextCursor}
    HostService-->>Client: { items, defaultBranch, nextCursor }
    deactivate HostService

    Client->>Client: render items
    Client->>Client: user scroll -> IntersectionObserver triggers load
    Client->>HostService: searchBranches(..., cursor=nextCursor)
Loading
sequenceDiagram
    participant Client as Client / UI
    participant HostService as Host Service
    participant Git as Git Repository
    participant Cloud as Cloud Backend
    participant DB as Local DB

    Client->>Client: User clicks "Checkout" (generate pendingId)
    Client->>HostService: checkout({ pendingId, projectId, workspaceName, branch, composer?, linkedContext? })
    activate HostService
    HostService->>Git: ensure repo present (clone/pull)
    Git-->>HostService: repo ready
    HostService->>Git: resolve ref (refs/heads/<branch> or refs/remotes/origin/<branch>)
    Git-->>HostService: ref resolved/missing
    alt need origin fetch
        HostService->>Git: git fetch origin <branch>
        Git-->>HostService: fetched
    end
    HostService->>Git: git worktree add <path> <ref>
    Git-->>HostService: worktree created
    HostService->>Cloud: register cloud workspace
    Cloud-->>HostService: cloud workspace id
    HostService->>DB: insert local workspaces row
    DB-->>HostService: local row created
    alt setup requested
        HostService->>HostService: start setup terminal
    end
    HostService-->>Client: { workspace, terminals?, warnings? }
    deactivate HostService
    Client->>Client: close modal, navigate to workspace route
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through refs and pages wide,

Found branches, worktrees side by side,
Checkout, open, adopt — a spry little feat,
Cursor and reflog keep my hop neat,
V2 now blossoms with each tiny seat.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title 'feat(desktop): paginated branch picker with checkout + open actions' clearly and specifically summarizes the main changes: pagination, branch picker UI, and new checkout/open action features.
Description check ✅ Passed The pull request description is comprehensive and follows the template structure with all required sections properly filled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch workspace-branch-review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR reworks the v2 new-workspace modal's branch picker from a flat single-query list into a paginated, server-filtered two-tab UI with per-row actions (Check out / Open). The host-service searchBranches procedure gains cursor pagination, a 30-second TTL-guarded git fetch, reflog-based recency ordering, and richer per-row metadata (isLocal, isRemote, worktreePath, isCheckedOut). A new checkout procedure handles branch reuse without the fork path's dedup logic. The renderer migrates to useInfiniteQuery with an IntersectionObserver sentinel for infinite scroll, and types are derived from the host-service schema via inferRouterInputs/inferRouterOutputs.

Key changes and findings:

  • Server (workspace-creation.ts): Well-structured pagination, TTL guard, and rollback paths. One dead-code condition in listWorktreeBranches where currentPath === worktreesRoot can never match (worktrees are nested inside .worktrees, not at it).
  • Search performance: branchSearch state is un-debounced — every keypress triggers a new useInfiniteQuery with a fresh key, running multiple git commands (git worktree list, git log -g, git for-each-ref) per keystroke via IPC. A 300-500 ms debounce would eliminate most of this overhead.
  • Type duplication: CompareBaseBranchPicker.tsx redefines BranchRow as a local interface, contradicting the PR's stated single-source-of-truth design. It should import the exported BranchRow from useBranchContext.
  • Checkout flow: Clean parallel to create, with correct rollback on cloud registration failure. Known limitations (prompt carry-over on Open, project-not-cloned case) are clearly documented.

Confidence Score: 4/5

Safe to merge — checkout/rollback paths are solid with no data-loss risk; the P1 performance issue (un-debounced search) is a notable UX concern but not a correctness blocker.

The host-service changes are well-structured with correct TTL guard, rollback on failure, and careful ref resolution. The renderer migration to useInfiniteQuery is clean. The un-debounced search causes excessive IPC traffic per keystroke, the IntersectionObserver can cascade page loads in small viewports, and the duplicated BranchRow interface is a maintainability concern. None of these block primary user paths or introduce data loss, and known limitations are clearly documented.

PromptGroup.tsx (missing search debounce) and CompareBaseBranchPicker.tsx (duplicate BranchRow type, IntersectionObserver cascade)

Important Files Changed

Filename Overview
packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Adds paginated searchBranches with TTL-guarded git fetch, reflog recency, and a new checkout procedure; one dead-code condition in listWorktreeBranches, rollback paths are solid.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx Wires up branchSearch/filter state, handleCheckout, and handleOpenExisting; un-debounced search state is lifted here and passed to the picker.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx Two-tab picker with IntersectionObserver infinite scroll and per-row hover actions; local BranchRow interface duplicates the exported type from useBranchContext.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts Migrates from useQuery to useInfiniteQuery with correct cursor handling; branchSearch state is not debounced, causing a new query per keystroke.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts Clean thin wrapper over the host-service checkout mutation; correctly handles both local and remote host targets.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant P as PromptGroup
    participant H as useBranchContext
    participant HS as host-service
    participant G as git

    U->>P: opens modal / types search
    P->>H: query=branchSearch, filter, page=undefined
    H->>HS: searchBranches(refresh:true, cursor:undefined)
    HS->>G: git fetch --prune (TTL-gated 30s)
    HS->>G: git worktree list --porcelain
    HS->>G: git log -g (reflog recency)
    HS->>G: git for-each-ref refs/heads + refs/remotes/origin
    G-->>HS: raw output
    HS-->>H: defaultBranch, items[0..49], nextCursor
    H-->>P: branches[], hasNextPage=true

    U->>P: scrolls near bottom
    P->>H: fetchNextPage()
    H->>HS: searchBranches(refresh:false, cursor:encoded)
    HS->>G: worktree list + log -g + for-each-ref
    HS-->>H: items[50..99], nextCursor
    H-->>P: branches[] grows

    U->>P: clicks Check out
    P->>HS: checkout(pendingId, branch, workspaceName)
    HS->>G: git show-ref refs/heads/branch
    alt local ref exists
        G-->>HS: ok, ref=branch
    else remote-only
        HS->>G: git show-ref refs/remotes/origin/branch
        HS->>G: git fetch origin branch
        G-->>HS: ok, ref=origin/branch
    end
    HS->>G: git worktree add worktreePath ref
    HS->>HS: ensureV2Host + v2Workspace.create (cloud)
    HS->>HS: local DB insert workspaces
    HS-->>P: workspace.id
    P->>P: navigate /v2-workspace/$workspaceId

    U->>P: clicks Open (Worktree tab)
    P->>P: lookup workspaceId via workspaceByBranch
    P->>P: closeModal + navigate
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx, line 327-339 (link)

    P1 Un-debounced search triggers expensive git operations per keypress

    branchSearch is passed directly into useBranchContext as query, which is part of the useInfiniteQuery key. Every keystroke creates a brand-new cache key, causing an immediate IPC call to the host-service. Each first-page request runs three git commands — git worktree list --porcelain, git log -g (reflog), and git for-each-ref — before filtering. That's ~60–90 ms of git I/O per character typed.

    Adding a 300–500 ms debounce in PromptGroup before passing the value to useBranchContext would reduce this to at most a couple of requests per burst of typing.

    const [branchSearch, setBranchSearch] = useState("");
    const [debouncedBranchSearch, setDebouncedBranchSearch] = useState("");
    
    useEffect(() => {
      const id = setTimeout(() => setDebouncedBranchSearch(branchSearch), 300);
      return () => clearTimeout(id);
    }, [branchSearch]);
    
    // pass debouncedBranchSearch to useBranchContext:
    const { branches, defaultBranch, ... } = useBranchContext(
      projectId, hostTarget, debouncedBranchSearch, branchFilter,
    );
    // keep passing branchSearch (not debounced) to onBranchSearchChange so the input feels instant

Reviews (1): Last reviewed commit: "feat(desktop): paginated branch picker w..." | Re-trigger Greptile

Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
Copy link
Copy Markdown
Contributor

@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: 5

🤖 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/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md`:
- Around line 171-180: The fenced mockup blocks containing lines like "⎇
feature-foo  [remote]             3d ago   [✓ when selected]" and the
hover/focus mockup with "⎇ feature-foo  [remote]             3d ago   [Check
out]" should be given a language tag to satisfy markdownlint MD040; update each
triple-backtick fence in BRANCH_DISCOVERY_DESIGN.md to start with ```text so
both mockup blocks are labeled as plain text.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts`:
- Around line 50-62: The queryFn in useBranchContext currently only destructures
{ pageParam } and doesn't forward TanStack Query's AbortSignal, so update the
queryFn to destructure the signal (e.g., async ({ pageParam, signal })) and pass
that signal into the tRPC call to cancel in-flight requests; specifically,
modify the call to client.workspaceCreation.searchBranches.query to include the
signal from the queryFn parameters so searches are aborted when the query key
changes.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx`:
- Around line 183-215: The row action buttons in CompareBaseBranchPicker are
only exposed via the "group-hover" class, making them unreachable to keyboard
users; update the JSX for the Open and Check out controls (the elements that
call onOpenExisting and onCheckoutBranch) to be focusable when the row is
focused by adding focus-visible/focus-within visibility classes instead of
relying solely on group-hover (e.g., replace "hidden group-hover:inline-flex"
with classes that show on hover or focus), and replace the non-focusable span
used as a TooltipTrigger for the disabled state with a focusable element (a
button or a TooltipTrigger with asChild wrapping a button) that includes
aria-disabled when branch.isCheckedOut so the tooltip is reachable via keyboard
and screen readers while preserving the disabled styling.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`:
- Around line 152-176: The handler handleOpenExisting currently looks up a
workspace by branch using workspaceByBranch, which is unstable when duplicate
branch names exist; change the API to accept a stable identifier (workspace id
or worktreePath) from the picker rows and stop resolving by branch name. Update
handleOpenExisting to receive workspaceId (or worktreePath) directly, remove
workspaceByBranch.get usage and toast branch-missing logic, and use that id when
calling navigate({ to: "/v2-workspace/$workspaceId", params: { workspaceId } });
also update the component prop onOpenExisting and any call sites that call
handleOpenExisting to pass the row's workspace id/worktreePath instead of
branchName. Ensure workspaceByBranch can be removed or adjusted if no longer
needed.

In
`@packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts`:
- Around line 776-816: The code resolving refs treats an origin-only branch as
if `git worktree add <path> origin/<branch>` will auto-create a local tracking
branch; instead, when ref startsWith("origin/") (the resolved ref variable) call
git.worktree add with explicit tracking flags so a local branch is created (use
`--track -b <branch> <worktreePath> origin/<branch>` semantics) and update the
preceding comment to remove the incorrect claim about auto-tracking for
origin/<branch>; modify the block that currently does git.raw(["worktree",
"add", worktreePath, ref]) to detect origin/* and invoke git.raw with the
`--track` and `-b` flags using the plain branch name, keeping existing
fetch/error handling (refer to variables ref, branch, and worktreePath).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b7c8e74-8f69-42aa-9e7e-dc993a317c65

📥 Commits

Reviewing files that changed from the base of the PR and between e9a272c and 9954cd8.

📒 Files selected for processing (8)
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts
  • packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts

Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai 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 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx:156">
P2: Open-by-branch lookup ignores host, so it can open the wrong workspace when the same branch exists on multiple hosts.</violation>
</file>

<file name="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts">

<violation number="1" location="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts:123">
P2: Silent `catch` swallows `git worktree list` errors without logging. If this fails, every branch's `isCheckedOut` will be `false`, enabling the Checkout button for already-checked-out branches. Add a `console.warn` with context so the failure is observable.

(Based on your team's feedback about handling async errors explicitly and not silently swallowing failures.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx:22">
P2: This local `BranchRow` interface manually duplicates the shape already derived from the host-service zod schema in `useBranchContext.ts` (via `inferRouterOutputs`). If the server type changes, this copy silently diverges.

Import the canonical type instead and add `BranchRow` to the barrel export in `hooks/useBranchContext/index.ts`:

```ts
// index.ts
export { type BranchFilter, type BranchRow, useBranchContext } from "./useBranchContext";

// CompareBaseBranchPicker.tsx
import type { BranchFilter, BranchRow } from "../../../hooks/useBranchContext";
```</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/BRANCH_DISCOVERY_DESIGN.md:77">
P1: The claim that `git worktree add <path> origin/<branch>` auto-creates a local tracking branch is incorrect. Per git documentation, the auto-create convenience only triggers when the **short** branch name is passed (e.g., `branch`, not `origin/branch`) and no `-b`/`-B`/`--detach` flag is given. Passing the fully-qualified `origin/<branch>` treats it as a commit-ish, which may result in a detached HEAD rather than a proper local tracking branch.

The `checkout` procedure should use `--track -b <branch> <path> origin/<branch>` for remote-only branches to ensure a local tracking branch is created, and this doc should be updated accordingly.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Remote-only Check out: use `git worktree add --track -b <branch>` so the
  worktree lands on a proper local tracking branch instead of detached HEAD.
  Auto-track only fires for the short name; passing `origin/<branch>` is
  treated as a commit-ish.
- Open existing: scope the branch→workspace map by host id so the Worktree
  tab doesn't navigate to a workspace on the wrong host when the same
  branch exists on multiple hosts.
- Picker a11y: reveal row action buttons on focus-within, not just hover.
  Swap the disabled span for a focusable button with aria-disabled so
  keyboard users can reach the "already checked out" tooltip.
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 14, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Captures the design for full-resource workspace deletion — worktree on
disk, optional branch, cloud row, local host-DB row. Host-service
orchestrates in that order (hard-to-reverse side first).

Covers failure modes, UX decisions (delete-branch opt-in, confirm only
when dirty), and what replaces the current cloud-only v2Workspace.delete.

Not implemented here — separate PR picks this up.
The old short-circuit returned `existingLocal.id` whenever a local
`workspaces` row matched the (project, branch) pair, without calling
cloud. After a prior workspace was hard-deleted in the cloud, the host
sqlite row leaked — so re-adopting the worktree echoed a phantom id
whose cloud row no longer existed. Renderer navigated to it, Electric
never synced anything back, sidebar/target page showed "not found"
indefinitely.

Fix: drop the local-idempotency shortcut. Always call `v2Workspace.create`;
the new cloudRow.id is authoritative. If a stale local row exists for
this (project, branch), replace it with the fresh mapping.

Also: pending page now waits for the newly-created cloud row to sync
into the Electric-backed `v2Workspaces` collection before navigating,
with a 3s fallback. Fast intents (adopt) were beating the sync to the
punch and landing on /v2-workspace/<id> before the row was visible.

Docs: replaced the draft delete design with the canonical version —
v2 delete unifies through `workspaceCleanup.destroy`; implementation
owned by a follow-up PR.
…hrows, harden lint script

Three PR-review follow-ups:

- Pending page: reset `firedRef` / `navigatedRef` when `pendingId` changes
  under a mounted component. Previously, navigating from one pending
  page to another kept the flags stuck at true → the second page never
  fired its mutation and sat in "creating" forever.
- Checkout procedure: `clearProgress(pendingId)` on the empty-branch
  and `safeResolveWorktreePath` error paths. Without this, a stale
  pending row lingered until the 5-minute sweeper removed it.
- check-git-ref-strings.sh: distinguish ripgrep exit codes. Exit 1
  (no matches) still passes; exit 2+ (unreadable file, bad regex) now
  fails loudly. `2>/dev/null` previously masked real scan failures as
  clean passes.
…payload builders

Addresses two code-smell findings from the PR review:

- pendingWorkspaceSchema had `hostTarget`, `linkedIssues`, `linkedPR` as
  `z.unknown()`, forcing `as`-casts at every read site and hiding
  malformed rows until a consumer crashed. Replaced with structured zod
  shapes (discriminatedUnion for hostTarget, typed object schemas for
  linkedIssues/linkedPR). Drops the casts in the pending page and the
  submit hook, and any malformed row now fails zod parsing instead of
  reaching the dispatch.

- Intent dispatch logic in useFireIntent (the switch that translates
  pending-row state into mutation inputs per intent) was untested and
  tangled up with IO. Extracted the payload-building into pure helpers
  in buildIntentPayload.ts and added a contract suite (11 tests, all
  passing) covering:
    - mapLinkedContextFromPending: internal filtering, github filtering,
      skip-missing-fields, linkedPR passthrough, empty-input
    - buildForkPayload: full fork shape, empty-prompt → undefined,
      attachments plumbing, host-tracking hostTarget survives
    - buildCheckoutPayload: branch + runSetupScript only
    - buildAdoptPayload: minimal shape

Pending page's useFireIntent becomes a thin wrapper: build payload,
call mutation, update collection. Bonus cleanup: the `hostUrl`
derivation block collapses from a nested ternary with 3 string casts
to a plain discriminant switch now that the type is known.
Merges three separate design docs in the modal directory into one
`DESIGN.md` covering the full v2 workspace-creation architecture:

- Branch discovery (data shape, server flow, client flow) — was
  BRANCH_DISCOVERY_DESIGN.md
- Actions per row (Fork / Check out / Open / Create, authority for
  hasWorkspace)
- Workspace creation flow (three intents, pending-row schema,
  pending-page dispatch, baseBranchSource plumbing, per-mutation
  details) — was PENDING_FLOW.md
- Workspace delete (follow-up PR spec with host-service ownership
  principle) — was WORKSPACE_DELETE_DESIGN.md
- Invariants + enforcement (authority decisions, tests, lint, type
  guarantees)

Cross-links to `packages/host-service/GIT_REFS.md` which stays
separate — it's a cross-cutting pattern doc, not modal-specific.

Code-comment pointers updated from the old filenames to the new
consolidated DESIGN.md with section references.
…op root

The doc covers a multi-module subsystem (modal + pending page +
host-service procedures) — it doesn't belong nested deep inside the
modal directory where 'DESIGN.md' is also too generic to be findable.

Moved to apps/desktop/V2_WORKSPACE_CREATION.md, parallel to AGENTS.md
and CLAUDE.md, where system-level architecture docs live in this repo.
Name says what it covers. Code-comment pointers updated to use the new
path. Internal cross-link to GIT_REFS.md fixed for the new depth.
…ATION

The fallback doc was the original design proposal for resolveStartPoint
+ --no-track + targeted single-ref fetch. All shipped in this PR. The
spec/diff sections are now stale — implementation lives in code.

Kept the prior-art comparison (VS Code, T3Code, GitHub Desktop, v1) as
an appendix in V2_WORKSPACE_CREATION.md — that's the part future
contributors might re-derive without reference. Updated the comparison
table to reflect what we actually shipped (local-first instead of
remote-first; targeted single-ref fetch). Added the rationale for
each rejection (why not VS Code's upstream lookup, why not T3Code's
gh-merge-base, etc.).

Removed packages/host-service/WORKSPACE_CREATION_FALLBACK.md.
…rom PromptGroup

PromptGroup was 592 lines mixing form state, agent prefs, branch picker
state + handlers, link handlers, hotkey wiring, and JSX. Split the
picker controller and link handlers into co-located hooks; PromptGroup
is now 360 lines focused on form composition.

- useBranchPickerController: owns picker state (search, filter), the
  branch-context query, host-id resolution + workspace lookup, and the
  three per-row action handlers (Open / Check out / Adopt). Returns a
  `pickerProps` bag the caller spreads into <CompareBaseBranchPicker>.
  Hides ~70 lines of plumbing behind one hook call.
- useLinkedContext: bundles addLinkedIssue / addLinkedGitHubIssue /
  removeLinkedIssue / setLinkedPR / removeLinkedPR. Pure delegation
  over updateDraft.
- Collapse the PromptGroup / PromptGroupInner indirection. There was
  no error boundary, provider, or conditional in the wrapper — just a
  pass-through. Removed.

Behavior unchanged. Typecheck + lint clean; 11 pending-page tests pass.
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