Skip to content

[Bug]: Remote-only projects cannot be assigned to local sidebar groups #13699

Closed

Description

@jrdncstr

Operating system

macOS

Orca version

1.4.179

Details

Short summary

A project cannot be assigned to an existing local sidebar group when its only worktree is on a remote Orca runtime. The same assignment works after a local worktree for that canonical project exists.

What happened?

Using either project menu to move a remote-only project into a local sidebar group appears to succeed, but the project remains ungrouped. No visible error explains the failed assignment.

The project's worktree remains available on the connected remote runtime. The failure concerns sidebar organization, not remote execution.

How can we reproduce it?

  1. Connect a remote Orca runtime that contains a project with no matching local worktree.
  2. Create or select an existing local sidebar group.
  3. Open the remote-only project's menu and select Move to group.
  4. Select the local group.
  5. Observe that the project remains ungrouped even though the action appeared to succeed.
  6. Add a local worktree for the same canonical project.
  7. Repeat the group assignment and observe that it now works.

Expected behavior

Sidebar grouping should be user-local organization. A canonical project should be assignable to a local group regardless of which host owns its worktrees. Group assignment should not change execution ownership.

Investigation notes

The renderer combines host-specific repos into a canonical project header and merges project-group catalogs from all connected hosts. The current assignment path sends the representative repo ID plus the selected group ID, then routes the mutation to the runtime that owns that repo.

A remote runtime does not own the selected local group. Its persistence layer treats the unknown group ID as null and returns a valid repo mutation, so the renderer reports success while the project remains ungrouped. A matching local repo masks the problem because local group placement is inherited by equivalent runtime repo records.

Relevant areas:

  • src/shared/project-host-setup-projection.ts
  • src/renderer/src/store/slices/repos.ts
  • src/renderer/src/components/sidebar/WorktreeList.tsx
  • src/renderer/src/components/sidebar/WorktreeContextMenu.tsx
  • src/main/persistence.ts

This may require maintainer guidance on whether user-local sidebar placement should belong to the canonical project rather than a host-owned repo, while keeping host-owned folder groups and execution state separate.

Activity

  1. manuaudio commented on Aug 11, 2026

    @manuaudio
    Contributor

    Confirmed, and the silent-null step you inferred is a single ternary. Verified on upstream/main at 20848169e0.

    The exact line

    src/main/persistence.ts:4636-4644:

    moveProjectToGroup(repoId: string, groupId: string | null, order?: number): Repo | null {
      const repo = this.state.repos.find((entry) => entry.id === repoId)
      if (!repo) {
        return null
      }
      const normalizedGroupId =
        groupId && (this.state.projectGroups ?? []).some((group) => group.id === groupId)
          ? groupId
          : null

    A group id that this store has never heard of takes the : null branch, and the method then does repo.projectGroupId = normalizedGroupId and return this.hydrateRepo(repo) — a fully-formed Repo. The only failure signal the method can produce is null, and that is already spoken for by "repo not found" at :4638. So "assigned to group G" and "silently ungrouped because G is unknown here" are the same return value. The renderer is not ignoring an error; there is no error to ignore.

    Why a fix can't just reject unknown ids

    The same : null branch is the legitimate ungroup path, and it has real callers:

    • src/renderer/src/components/sidebar/WorktreeContextMenu.tsx:601 — void moveProjectToGroup(repo.id, null)
    • src/renderer/src/components/sidebar/WorktreeList.tsx:6196 — void moveProjectToGroup(repo.id, null)

    and there is a third way to arrive at null that never passed through a user's intent at all — src/main/runtime/rpc/methods/repo.ts:145:

    repo: await runtime.moveProjectToGroup(params.repo, params.groupId ?? null, params.order)

    An omitted groupId on the wire becomes null before persistence is reached. So one sentinel currently carries three distinct meanings:

    Input Meaning Correct outcome
    null from the two ungroup call sites deliberate ungroup ungroup, success
    groupId absent on the RPC unspecified ungroup, success (today)
    a group id unknown to this store the bug should not report success

    Only the third is a failure, and it is indistinguishable from the first two at the point where the decision is made. Any guard that returns an error for "unknown group" without first separating explicit-null from unknown-id will break both ungroup buttons — the minimal change is to make the "unknown id" case reachable as its own outcome rather than to tighten the existing branch.

    Where the host split becomes visible

    moveProjectToGroup has exactly two non-test store call sites — src/main/ipc/repos.ts:1618 and :1729 — plus the runtime wrapper at src/main/runtime/orca-runtime.ts:18305-18314, which forwards to this.store.moveProjectToGroup(repo.id, groupId, order) on whichever runtime owns the repo. That wrapper is the point where a locally-created group id reaches a store whose state.projectGroups was never given it, which is exactly the crossing you described.

    On the open question

    Your framing — sidebar placement is user-local organization, not execution ownership — is the part that needs a maintainer call, and I'd only add one observation in support of it: projectGroupId is stored on the repo record (repo.projectGroupId, set at :4646), i.e. in the per-host store that also holds execution state. So "which group is this project in" is currently host-owned by construction, not by accident of routing. If grouping should be user-local, the ordering value has the same problem — repo.projectGroupOrder at :4647-4650, via getNextProjectGroupOrder(siblingRepos, normalizedGroupId), is computed against that host's sibling repos, so even a successful cross-host assignment would order the project against a different sibling set than the one the user is looking at.

    Happy to send a PR for whichever direction is chosen; the silent-success half is fixable on its own regardless of where placement ends up living.

  2. AmethystLiang commented on Aug 11, 2026

    @AmethystLiang
    Contributor

    thanks for reporting. looking into it

  3. AmethystLiang commented on Aug 11, 2026

    @AmethystLiang
    Contributor

    thanks for reporting. looking into it

  4. Coriou commented on Sep 17, 2026

    @Coriou

    Same silent-null, opposite direction.

    Paired local + remote runtime. Groups live on host A. Two git projects live only on host B.

    Move (drag or menu) looks successful. projectGroupId stays null. projectGroupOrder still updates to the drop index. No toast.

    Tried the exact same moves from Orca running on host A (the group owner) — identical no-op. So this isn’t “Active Server was Local”. moveProjectToGroup follows the project owner; host A’s group IDs aren’t in that catalog; persistence coerces them to null and still returns the repo.

    The coerce is in FolderWorkspacePersistenceOperations.moveProjectToGroup. Related: #15006, #10755. Fix PRs still open: #16315, #7023, #11970.

  5. added theissue type on Sep 19, 2026
  6. mkirouac commented on Oct 15, 2026

    @mkirouac

    Still reproducible on 1.4.205 (Windows desktop, Linux remote runtimes). I created a local group, right-clicked a remote-owned project, chose Move to group, and picked the local group: nothing happens, with no error or toast.

  7. dustinbyershax commented on Oct 17, 2026

    @dustinbyershax

    Still reproducible on 1.4.229 (macOS desktop paired with a macOS runtime over WebSocket, both 1.4.229), using drag-and-drop.

    • Remote-only repo registered with orca repo add --environment <server> --path <abs>. It has no git remote, so it has no matching local project.
    • Dragged it onto a local folder-scan group (createdFrom: "folder-scan"). The repo stayed at the top level with no toast.
    • orca repo list --environment <server> --json before the drag: the repo had no projectGroupId key. After the drag: "projectGroupId": null, "projectGroupOrder": 0. Same silent coercion described above.

    Server repos whose git remote matches a local project already show up inside the local group through the merged project header. So the only workaround I've found is to keep a local checkout of every remote repo you want grouped.

  8. nwparker commented on Oct 26, 2026

    @nwparker
    Contributor

    Thanks @jrdncstr, and everyone who confirmed this. The silent no-op is fixed: since #30127 (v1.4.230), Orca tells you when a project can't join a group on another host instead of pretending the move worked. Putting a remote-only project into a local group, so groups span hosts, is tracked as a feature in #29200. Closing in favor of that issue; reopen if you still get a silent no-op on a current build.

  9. manuaudio commented on Oct 26, 2026

    @manuaudio
    Contributor

    Confirmed. #30127 resolves the silent-success path I traced, while #29200 keeps the cross-host grouping request separate. That split matches the distinction in my earlier notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinggitos:macosIssues affecting macOS

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions