Unified project/machine model: lane machine selection, push divergence guard, absolute machine names - #912
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (41)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Four defects found while designing the unified project/machine model, plus the locked sidebar retune. Lane cache was written under the binding key (`remote:<targetId>:<projectId>`) but read back by `rootPath`, so every remote project missed its own warm cache. Fixing the key exposed a second, larger bug underneath: on a remote cold start `setProjectBinding` runs before `setProject` and already claims the root, so neither the root nor the state key "changes" and the warm-restore branch never ran at all. Lane hydration now also triggers for an unhydrated lane list on a known key, and per-lane UI context resets only on a real project change so an in-place `setProject` can't clear lanes already onscreen. Cache retention protected open remote tabs by binding key while recents contributed only `rootPath`, so a closed-but-recent remote project lost its warm cache on the next project switch. Added `shared/projectIdentity.ts` as the single definition of the binding key — the format previously existed in three places free to drift, which is how the read/write mismatch arose. PR badge could be pushed off the lane header: a `flex-shrink: 0` rule outranked the Tailwind `shrink` utility on the lane name, and the toggle button had no `overflow-hidden`, so a long name spilled over the trailing badge cluster and got clipped. The branch is gone from the lane header entirely — it duplicated the lane name and was the second flexible text node causing the contention; it moves to the header tooltip. Sidebar weight was inverted: a 26px provider mark (least informative — most rows share a provider) outweighed the 13px title, and the status line, the highest-signal text in the row, was the quietest at 10px/50%. Mark 26→18px, title 13→14px, status 10→12px at 65%. Regression tests pin the remote warm-restore path, the binding-key helpers, and that `refreshLanes` keeps previous lanes visible when the runtime call fails — the offline contract that until now held only by accident of an `await` sitting before the first `set()`. Co-Authored-By: Claude <noreply@anthropic.com>
Foundation for one-tab-per-repo with the machine as a switchable dimension inside it. `shared/projectIdentity.ts` gains `projectRefStateKey`, and main's `recentProjectKey` now delegates to it rather than carrying its own copy of the `remote:<targetId>:<projectId>` format. Local recents now carry `gitOriginUrl`, read straight out of the repo's git config. This is a plain file read rather than `git remote get-url` on purpose: the recents path already opens a SQLite database per project, so a few hundred bytes of config is noise, whereas spawning git once per recent would not be. Linked worktrees resolve to the main repo's config, since a worktree has no remotes of its own. `projectTabGrouping.ts` collapses open local and remote tabs into one group per repository, joined on the normalized git origin. Two rules keep the join honest: a project with no resolvable origin is never merged with anything, so unrelated origin-less folders can't collapse together; and at most one checkout per machine joins a group, because a lane worktree shares its parent's origin and merging them would produce a single tab that cannot represent both. Co-Authored-By: Claude <noreply@anthropic.com>
…ine names Three pieces of the unified project/machine model. A lane owns its machine (`lanes.worktree_path` is an absolute path on one machine) and chats inherit theirs through `laneId`, so lane creation becomes the only place a machine is chosen. Lane creation gains a "Create on" selector: This Mac plus connected machines only, free disk headroom per row, machines without the repo disabled rather than hidden, and a link out to Connections. `laneMachines.ts` derives all of it from the connection snapshot the renderer already holds — no polling, no new IPC — and memoizes the origin normalization so the URL parse never runs per render. The section renders only when more than one machine is connected, so a single-machine setup is unchanged. LaneCombobox groups lanes by machine with its own "Auto-create lane here" per group; below two machines it is the same flat list as before. Push divergence guard: `laneDivergence.ts` is pure and warns only when another machine holds the same branch at a different commit *and* is ahead. It stays silent when shas match, when the other machine is behind (pushing fast-forwards it), and when its head is unknown — an unknown state is not evidence, and a false alarm here just teaches people to click through. It short-circuits on an empty machine list before doing any work, so the common path costs one length check. "Remote" is retired as a machine descriptor in the Work tab. Once the machine a tab is bound to can itself change, "remote" has no fixed referent, so machines are named absolutely. The word survives only in Connections, where it means "not this physical Mac" — an actual physical fact. `RemoteProjectOpenDialog` is deleted: it existed solely to warn that a separate remote tab was being created, which one-tab-per-repo makes meaningless. The risk it was really guarding is now caught at push time instead. The composer gains a machine chip — dashed and inert while a lane is settled, a solid picker only while the lane is still auto-create, which is the one moment no machine is chosen yet. The Chats tab's machine becomes explicit rather than a silent footnote that flipped with whichever project tab you last focused, and CTO now states that it always runs on This Mac. Docs updated for the deleted dialog and the new grouping/divergence modules. Co-Authored-By: Claude <noreply@anthropic.com>
5b60517 to
2b33c81
Compare
Two features from #912 shipped with no production caller. This wires them. **Tab merge.** `groupProjectTabs` had zero importers — the join existed, the merge didn't. TopBar now renders one tab per repository group with the current machine named inline and a dropdown listing every machine in the group. That dropdown is load-bearing: a merged group hides the non-active machine's tab, so without it a checkout would be stranded with no way back. The join key was also absent from the path the renderer actually uses. `IPC.projectListRecent` is served by `toShallowRecentProjectSummary`, which never set `gitOriginUrl` — so even fully wired, every tab would have arrived with an undefined origin and nothing would ever have merged. **Origin parsing was returning wrong data.** The hand-rolled regex ended a section only on a `[` at column 0, so an indented ` [remote "upstream"]` returned *upstream's* URL — silently merging two unrelated repos into one tab. It also kept surrounding quotes and trailing `;`/`#` comments, and missed `[REMOTE "origin"]` (git section names are case-insensitive). Section scanning now delegates to the already-tested `readOriginRemoteUrlFromGitConfig`; only value undecoration is layered on top. The worktree walk is structural instead of a substring search for `worktrees`, so `~/worktrees/myrepo` no longer truncates to `~`, and a submodule inside a linked worktree reports its own origin rather than the superproject's. New `recentProjectSummary.test.ts` covers all of it — the file that decides whether two repos are the same repo previously had no tests. **Divergence guard.** `LaneSummary` has no `headSha`, so `toMachineBranchState` always produced null and the rule skipped every candidate — it could never return a warning. The rule is now grounded in `ahead`, which every `LaneSummary`/`lane_state_snapshots` record genuinely carries: a machine with commits ahead on your branch holds work that moving the upstream tip would strand. Head shas only ever silence a warning now, never suppress one by being absent, and the doc comment that promised a fallback the code didn't have is gone. The two-path delivery registry with zero producers is deleted; one documented seam remains. Found by /quality. Co-Authored-By: Claude <noreply@anthropic.com>
Two features from #912 shipped with no production caller. This wires them. **Tab merge.** `groupProjectTabs` had zero importers — the join existed, the merge didn't. TopBar now renders one tab per repository group with the current machine named inline and a dropdown listing every machine in the group. That dropdown is load-bearing: a merged group hides the non-active machine's tab, so without it a checkout would be stranded with no way back. The join key was also absent from the path the renderer actually uses. `IPC.projectListRecent` is served by `toShallowRecentProjectSummary`, which never set `gitOriginUrl` — so even fully wired, every tab would have arrived with an undefined origin and nothing would ever have merged. **Origin parsing was returning wrong data.** The hand-rolled regex ended a section only on a `[` at column 0, so an indented ` [remote "upstream"]` returned *upstream's* URL — silently merging two unrelated repos into one tab. It also kept surrounding quotes and trailing `;`/`#` comments, and missed `[REMOTE "origin"]` (git section names are case-insensitive). Section scanning now delegates to the already-tested `readOriginRemoteUrlFromGitConfig`; only value undecoration is layered on top. The worktree walk is structural instead of a substring search for `worktrees`, so `~/worktrees/myrepo` no longer truncates to `~`, and a submodule inside a linked worktree reports its own origin rather than the superproject's. New `recentProjectSummary.test.ts` covers all of it — the file that decides whether two repos are the same repo previously had no tests. **Divergence guard.** `LaneSummary` has no `headSha`, so `toMachineBranchState` always produced null and the rule skipped every candidate — it could never return a warning. The rule is now grounded in `ahead`, which every `LaneSummary`/`lane_state_snapshots` record genuinely carries: a machine with commits ahead on your branch holds work that moving the upstream tip would strand. Head shas only ever silence a warning now, never suppress one by being absent, and the doc comment that promised a fallback the code didn't have is gone. The two-path delivery registry with zero producers is deleted; one documented seam remains. Found by /quality. Co-Authored-By: Claude <noreply@anthropic.com>
* Never rebind the app to a same-named but different repository
The create-lane machine picker resolved another machine's checkout by git
origin and then, unconditionally, fell back to matching on the repo folder
name. A folder name is not an identity: a local `~/src/api`
(github.com/acme/api) matched an unrelated `api` (github.com/other/api) on
another machine, rendered as a valid choice, and selecting it called
switchRemoteProject with that project id — rebinding the whole app tab to the
wrong repository and then creating a lane in it.
Two changes. A name candidate whose known origin contradicts the known origin
of the current repo is now rejected outright: matching names with differing
origins are proof the repos are *different*, not the same. And a match is
tagged with how it was established, so a name-only hit reports `repoMatch:
"unknown"` rather than `"matched"` — it stays visible as a candidate, because
absence of proof is not proof of absence, but it no longer claims to be
verified.
Found by /quality Track A. The failing assertion this changes had encoded the
bug: it asserted a folder-name match reported as proven.
Co-Authored-By: Claude <noreply@anthropic.com>
* Give "this machine" one definition instead of five
The id for the machine ADE runs on existed in five places under three names —
laneMachines, projectTabGrouping, AgentChatComposer, PersonalChatsPage, and a
hardcoded literal in LaneGitActionsPane — with **two different values**
("this-mac" and "local"), kept in sync by comments.
That is not cosmetic. The push-divergence guard decides whether a branch lives
on *another* machine by comparing these ids. The moment one producer supplies
"local" where the consumer expects "this-mac", the self-filter stops matching
and ADE warns that This Mac has diverged from itself.
`shared/machineIdentity.ts` is now the only definition. Existing export names
are kept as aliases so call sites are unchanged.
Found by /quality Track B.
Co-Authored-By: Claude <noreply@anthropic.com>
* Merge project tabs by repo, and make the divergence guard able to fire
Two features from #912 shipped with no production caller. This wires them.
**Tab merge.** `groupProjectTabs` had zero importers — the join existed, the
merge didn't. TopBar now renders one tab per repository group with the current
machine named inline and a dropdown listing every machine in the group. That
dropdown is load-bearing: a merged group hides the non-active machine's tab, so
without it a checkout would be stranded with no way back.
The join key was also absent from the path the renderer actually uses.
`IPC.projectListRecent` is served by `toShallowRecentProjectSummary`, which
never set `gitOriginUrl` — so even fully wired, every tab would have arrived
with an undefined origin and nothing would ever have merged.
**Origin parsing was returning wrong data.** The hand-rolled regex ended a
section only on a `[` at column 0, so an indented ` [remote "upstream"]`
returned *upstream's* URL — silently merging two unrelated repos into one tab.
It also kept surrounding quotes and trailing `;`/`#` comments, and missed
`[REMOTE "origin"]` (git section names are case-insensitive). Section scanning
now delegates to the already-tested `readOriginRemoteUrlFromGitConfig`; only
value undecoration is layered on top. The worktree walk is structural instead
of a substring search for `worktrees`, so `~/worktrees/myrepo` no longer
truncates to `~`, and a submodule inside a linked worktree reports its own
origin rather than the superproject's. New `recentProjectSummary.test.ts`
covers all of it — the file that decides whether two repos are the same repo
previously had no tests.
**Divergence guard.** `LaneSummary` has no `headSha`, so `toMachineBranchState`
always produced null and the rule skipped every candidate — it could never
return a warning. The rule is now grounded in `ahead`, which every
`LaneSummary`/`lane_state_snapshots` record genuinely carries: a machine with
commits ahead on your branch holds work that moving the upstream tip would
strand. Head shas only ever silence a warning now, never suppress one by being
absent, and the doc comment that promised a fallback the code didn't have is
gone. The two-path delivery registry with zero producers is deleted; one
documented seam remains.
Found by /quality.
Co-Authored-By: Claude <noreply@anthropic.com>
* Wire the per-machine lane picker and stop two silent repo switches
**Lane picker grouping was unreachable** — `LaneCombobox` supported grouping
lanes by machine with a per-machine "Auto-create lane here", but no call site
passed `machines`, so it always short-circuited to the flat list. Wired at the
chat draft lane selector, gated so a single-machine setup makes no extra IPC
calls and renders byte-for-byte as before.
**The auto-create id contract had already drifted.** `isAutoCreateLaneOptionId`
accepts both the bare id and the per-machine `<id>:<machineId>` form, but three
call sites still compared with `===`. Once grouping went live, a per-machine id
would have fallen through and been treated as a real lane id.
**"This Mac" could switch you to an unrelated repository.** When the tab was
bound to another machine, selecting This Mac fell back to
`openProjectTabRoots[0]` — the first open local tab in insertion order, with no
relation to the repo on screen — and switched to it. The premise of this whole
model is that the machine is a dimension *of one repo's tab*; that fallback
silently changed the repo instead. Both copies of the logic now resolve the
local counterpart by identity and refuse to switch when there isn't one, saying
so, rather than landing you somewhere arbitrary.
**Cancelling the create-lane dialog left the app rebound.** Picking a machine
mutates the active binding (lane creation is routed by it); closing without
creating left the app on the other machine with no sign a dialog did it. The
binding is captured on open and restored on cancel; an intentional create keeps
the switch.
Removed the now-dead `remoteRuntimeCheckLocalWork` IPC — channel, handler,
preload binding, type, timeout entry, browser mock, and test stubs — left with
no callers when `RemoteProjectOpenDialog` was deleted.
Found by /quality.
Co-Authored-By: Claude <noreply@anthropic.com>
* Show every machine's chats in the Work sidebar, and feed the push guard
The Work sidebar now unions lanes and chats across every connected machine,
independent of which machine the tab is bound to. That was the point of the
whole model: you should see what is in flight anywhere without switching tabs.
The tab's machine still drives Lanes/PRs/Files/Git — the execution context is
unchanged; only the sidebar became global.
Machines are tagged onto lanes, never onto chats: a lane owns its machine
because `worktree_path` is an absolute path on exactly one machine, and chats
inherit through `laneId`. No schema change.
A machine that drops keeps its lanes on screen, dimmed and read-only, rather
than having them disappear. That is stated in the merge logic instead of being
an accident of where an `await` sits — if a wifi blip reflowed half the sidebar
away, this would be worse than the two tabs it replaces.
The machine marker follows the rule that it only appears when work is *not*
here: a bare glyph by default, the machine's name when it is offline, when two
or more distinct foreign machines are visible at once, or when the lane carries
a same-branch warning. Monochrome — the lane accent already owns colour.
No polling. Reads are driven by the existing connection-snapshot and
lane/session change events, coalesced, bounded to four machines in parallel
with per-machine timeouts and generation-guarded cancellation, over the
already-allowlisted `remoteRuntime.callAction` path. Local lanes paint first;
foreign rows append as they arrive, so a slow machine cannot stall the sidebar.
This also supplies what the push divergence guard was missing. It has been
running on a permanently empty list; it now reads the union at click time, so
it still costs nothing until you actually press push.
Co-Authored-By: Claude <noreply@anthropic.com>
* Route each chat to its own machine instead of to the window
Clicking a chat whose lane lives on another machine now streams from that
machine without rebinding the tab. Previously every project-scoped call went
through `callProjectRuntimeActionIfBound` to whichever machine the *window* was
bound to, so a union sidebar could show you a chat it could not open — or could
only open by dragging Lanes, PRs and Files along with it.
A chat's machine is its lane's machine; `chatMachineRouting` resolves it from
lane ownership over state the renderer already holds. The primitive already
existed — `callPinnedRuntimeAction`, used until now only for detached launches
— and is promoted to the default path for chat-scoped calls. Everything is
additive: `pin` is an optional trailing argument, and a chat on the active
binding passes no argument at all and allocates nothing, so the common path is
byte-for-byte what it was.
Fixes the guard that would have silently swallowed this. `canMutatePinnedProjectUi`
dropped UI updates whenever a pin differed from the active binding, which meant
"stale detached launch" before and means "chat on another machine" now — it
would have discarded updates for precisely the chats this enables. It now asks
whether the pinned binding is still open rather than whether it is the active
one.
Co-Authored-By: Claude <noreply@anthropic.com>
* Consolidate lane test files and pin the pinned-UI guard
Test steward pass. The machine-selection feature had fractured into four small
files in `components/lanes/` — a folder already carrying 20 test files for 39
sources. `CreateLaneDialogMachines` is folded into `CreateLaneDialog.test.tsx`
(both render the same component), and the LaneCombobox grouping test moves next
to `LaneCombobox.tsx` in `components/terminals/`, where its subject lives; it
was only parked under `lanes/` by a file-ownership constraint while the feature
was being built.
Adds the one gate finding from /quality that had no test: the
`canMutatePinnedProjectUi` change. Every other gate item was already pinned —
name-only repo matches, the git-config parser cases, the structural worktree
walk, offline lane retention, and per-chat routing not rebinding the tab. This
one I made by hand and left uncovered, which is exactly the gap that let dead
code ship in the first place.
Co-Authored-By: Claude <noreply@anthropic.com>
* Update internal docs for the unified project/machine model
Documents one-tab-per-repo with a machine dropdown, the cross-machine union
Work sidebar, per-chat runtime routing, machine selection at lane creation, and
the shared machine/project identity modules. Source file maps updated for the
added modules. Removes the dirty-local-work confirmation dialog and
`remoteRuntimeCheckLocalWork`, both deleted from the code.
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix consolidated create-lane dialog tests
* ship: iteration 1 — fix CLI deadline and review findings
* ship: iteration 1 — address Codex routing and refresh findings
* ship: iteration 2 — fix desktop typecheck and review findings
* ship: iteration 3 — fix cross-machine ownership and CI
* ship: iteration 4 — finish per-chat runtime routing
* ship: iteration 5 — address terminal routing reviews
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implements most of the unified project/machine model. The organizing idea: a lane owns its machine (
lanes.worktree_pathis an absolute path on one machine) and chats inherit theirs throughlaneId— so lane creation becomes the only place a machine is chosen, and no schema change is needed to know where a chat runs.Defects fixed
Remote projects never restored their warm lane cache.
refreshLaneswrites it under the binding key butsetProjectread it back byrootPath. Fixing the key exposed a bigger bug underneath: on remote cold startsetProjectBindingruns first and already claims the root, so nothing "changed" and the warm-restore branch never ran at all. Hydration now also triggers for an unhydrated lane list on a known key, and per-lane UI context resets only on a real project change.Closed remote projects lost their cache — retention protected open remote tabs by binding key while recents contributed only
rootPath.PR badge could be clipped out of the lane header — a
flex-shrink: 0rule outranked the Tailwindshrinkutility and the toggle button had nooverflow-hidden, so a long lane name overflowed and took the trailing badge with it. The branch is gone from the header entirely: it duplicated the lane name and was the second flexible text node causing the contention.Sidebar weight was inverted — a 26px provider mark (least informative; most rows share a provider) outweighed the 13px title, while the status line, the highest-signal text in the row, was quietest at 10px/50%. Now 18px / 14px / 12px at 65%.
Repo identity
shared/projectIdentity.tsis the single definition of the binding key — theremote:<targetId>:<projectId>format previously existed in three places free to drift, which is exactly how the read/write mismatch arose. Local recents now carrygitOriginUrlvia a plain git-config read (this path already opens SQLite per project; spawninggitper recent would not be noise).projectTabGrouping.tscollapses open tabs into one group per repo on normalized origin. Two rules keep it honest: no-origin projects never merge, and at most one checkout per machine joins a group — a lane worktree shares its parent's origin, and merging them would make a tab that can't represent both.Lane machine selection
A "Create on" selector: This Mac plus connected machines only, free disk headroom per row, machines lacking the repo disabled rather than hidden, and a link to Connections. All derived from the connection snapshot the renderer already holds — no polling, no new IPC — with origin normalization memoized so the parse never runs per render. Renders only above one machine, so single-machine setups are unchanged. LaneCombobox groups by machine with its own "Auto-create lane here" per group.
Push divergence guard
laneDivergence.tsis pure and warns only when another machine holds the same branch at a different commit and is ahead. Silent when shas match, when the other machine is behind (you'd fast-forward it), and when its head is unknown — an unknown state isn't evidence, and a false alarm here just teaches people to click through. Short-circuits on an empty machine list, so the common path costs one length check. This is the only interruption in the whole design, at the one moment work can actually be lost."Remote" retired from the Work tab
Once the machine a tab is bound to can change, "remote" has no fixed referent. Machines are named absolutely; the word survives only in Connections where it means "not this physical Mac" — a real physical fact.
RemoteProjectOpenDialogis deleted (548 lines): it existed solely to warn that a separate remote tab was being created, which one-tab-per-repo makes meaningless.Composer gains a machine chip — dashed and inert while a lane is settled, a solid picker only while the lane is auto-create. The Chats tab's machine is explicit instead of silently flipping with whichever project tab you last focused. CTO states that it always runs on This Mac.
Not in this PR
canMutatePinnedProjectUiand changes machine resolution for every project-scoped call — it wants the app driven end-to-end before merge, not just green unit tests.personalChatsCallderives its machine from the window binding, so it still follows tab focus. Closing that needs an explicit machine argument in main.Verification
npm --prefix apps/desktop run typecheck— cleannpm --prefix apps/ade-cli run typecheck— cleannpm run test:desktop:sharded— 8 shards, ~8,990 passed, 2 skipped, exit 0npm --prefix apps/ade-cli run test— 104 files, 2,291 passednpm --prefix apps/desktop run lint— 0 errors🤖 Generated with Claude Code