Repository navigation
Description
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?
- Connect a remote Orca runtime that contains a project with no matching local worktree.
- Create or select an existing local sidebar group.
- Open the remote-only project's menu and select Move to group.
- Select the local group.
- Observe that the project remains ungrouped even though the action appeared to succeed.
- Add a local worktree for the same canonical project.
- 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.tssrc/renderer/src/store/slices/repos.tssrc/renderer/src/components/sidebar/WorktreeList.tsxsrc/renderer/src/components/sidebar/WorktreeContextMenu.tsxsrc/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
manuaudio commentedon Aug 11, 2026
Confirmed, and the silent-
nullstep you inferred is a single ternary. Verified onupstream/mainat20848169e0.The exact line
src/main/persistence.ts:4636-4644:A group id that this store has never heard of takes the
: nullbranch, and the method then doesrepo.projectGroupId = normalizedGroupIdandreturn this.hydrateRepo(repo)— a fully-formedRepo. The only failure signal the method can produce isnull, 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
: nullbranch 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
nullthat never passed through a user's intent at all —src/main/runtime/rpc/methods/repo.ts:145:An omitted
groupIdon the wire becomesnullbefore persistence is reached. So one sentinel currently carries three distinct meanings:nullfrom the two ungroup call sitesgroupIdabsent on the RPCOnly 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-
nullfrom 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
moveProjectToGrouphas exactly two non-test store call sites —src/main/ipc/repos.ts:1618and:1729— plus the runtime wrapper atsrc/main/runtime/orca-runtime.ts:18305-18314, which forwards tothis.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 whosestate.projectGroupswas 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:
projectGroupIdis 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.projectGroupOrderat:4647-4650, viagetNextProjectGroupOrder(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.
AmethystLiang commentedon Aug 11, 2026
thanks for reporting. looking into it
AmethystLiang commentedon Aug 11, 2026
thanks for reporting. looking into it
Coriou commentedon Sep 17, 2026
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.
projectGroupIdstaysnull.projectGroupOrderstill 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”.
moveProjectToGroupfollows the project owner; host A’s group IDs aren’t in that catalog; persistence coerces them tonulland still returns the repo.The coerce is in
FolderWorkspacePersistenceOperations.moveProjectToGroup. Related: #15006, #10755. Fix PRs still open: #16315, #7023, #11970.feat(orca): check docs and known issues before guessing at a failure
refactor(orca): thin router to the official skills plus operator judg…
mkirouac commentedon Oct 15, 2026
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.
dustinbyershax commentedon Oct 17, 2026
Still reproducible on 1.4.229 (macOS desktop paired with a macOS runtime over WebSocket, both 1.4.229), using drag-and-drop.
orca repo add --environment <server> --path <abs>. It has no git remote, so it has no matching local project.createdFrom: "folder-scan"). The repo stayed at the top level with no toast.orca repo list --environment <server> --jsonbefore the drag: the repo had noprojectGroupIdkey. 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.
fix(project-groups): say so when a project cannot join a group on ano…
fix(project-groups): say so when a project cannot join a group on ano…
nwparker commentedon Oct 26, 2026
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.
manuaudio commentedon Oct 26, 2026
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.