Every board root keys once at its store — the hover path stops walking the filesystem

`DragLocality.isSameBoard` resolved symlinks on both URLs, which stats every
path component. It ran five-plus times per `dropUpdated` and again per lane per
body evaluation through `renderedCards` → `hiddenMembers` — order of 50–100
stat calls per mouse-move, and worse on iCloud-backed paths. A pickup stall
sample had it on ~47 of 943 stacks.

`BoardRootKey` mints that canonical spelling once, where the root is, and every
locality comparison downstream is an `==` on two strings. The drag carriers hold
keys rather than URLs, so re-deriving under the cursor is no longer expressible.
`rootURL` keeps the user's spelling — the folder name is the display-name
fallback, and canonicalizing there would visibly rename a board opened through
a pin. The key follows the folder through `relocate(to:)`: a key frozen at open
would collide with a new board opened at the vacated path, and two boards
comparing as one is a cross-board drag silently behaving as a move.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 17:55:48 -04:00
parent 8a8ec4dfd1
commit ef423bb9d2
8 changed files with 232 additions and 124 deletions
+3 -3
View File
@@ -788,7 +788,7 @@ struct LaneView: View {
/// Where a card drag would land **in this lane**, or `nil` when the proposal is elsewhere.
private var cardProposal: Int? {
drops.session.laneProposal(onBoardRooted: store.rootURL, laneID: lane.id)
drops.session.laneProposal(onBoardRooted: store.rootKey, laneID: lane.id)
}
/// The shadow run this lane opens, or `nil` when no proposal names it the masonry's one
@@ -805,7 +805,7 @@ struct LaneView: View {
if let position = cardProposal {
return ShadowRun(position: position, heights: drops.session.cardHeights)
}
if let proposal = drops.session.fileLaneProposal(onBoardRooted: store.rootURL, laneID: lane.id) {
if let proposal = drops.session.fileLaneProposal(onBoardRooted: store.rootKey, laneID: lane.id) {
return ShadowRun(
position: proposal.index,
heights: Array(repeating: LaneDropRegistry.nominalCardHeight, count: proposal.count)
@@ -892,7 +892,7 @@ struct LaneView: View {
private var renderedCards: [Card] {
Self.rendered(
lane.cards,
hiddenByDrag: drops.session.hiddenMembers(onBoardRooted: store.rootURL),
hiddenByDrag: drops.session.hiddenMembers(onBoardRooted: store.rootKey),
filter: store.searchFilter,
renaming: store.transient.renameEditor?.targetID
)