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
+19 -19
View File
@@ -147,7 +147,7 @@ struct BoardDropContext {
/// disagree.
func revalidateProposal() {
guard let proposal = session.proposal,
DragLocality.isSameBoard(proposal.boardRoot, store.rootURL),
proposal.boardRoot == store.rootKey,
let laneID = proposal.laneID
else { return }
guard !store.snapshot.lanes.contains(where: { $0.id == laneID }) else { return }
@@ -179,7 +179,7 @@ struct BoardDropContext {
/// `receiveLanes` keep the index space they always had.
func retargetLanes() {
guard session.isDraggingLanes, let cursor = stripCursor() else { return }
let hidden = session.hiddenMembers(onBoardRooted: store.rootURL)
let hidden = session.hiddenMembers(onBoardRooted: store.rootKey)
let resting = store.snapshot.lanes.filter { !hidden.contains($0.id) }
let restingUnits = resting.map { LaneLayoutMath.displayUnits(of: $0) }
let slot = DropSlotMath.laneSlot(
@@ -188,11 +188,11 @@ struct BoardDropContext {
draggedUnits: session.laneUnits,
standard: standard(),
gap: gap,
current: session.stripProposal(onBoardRooted: store.rootURL)
current: session.stripProposal(onBoardRooted: store.rootKey)
)
guard let slot else { return } // a dead region: hold the current proposal
let index = min(max(0, slot), restingUnits.count)
session.propose(DropTarget(boardRoot: store.rootURL, container: .strip, index: index))
session.propose(DropTarget(boardRoot: store.rootKey, container: .strip, index: index))
}
/// Where a **card** session would land in `laneID`'s masonry.
@@ -215,7 +215,7 @@ struct BoardDropContext {
}
guard let grid = registry.grids[laneID] else { return }
let hidden = session.hiddenMembers(onBoardRooted: store.rootURL)
let hidden = session.hiddenMembers(onBoardRooted: store.rootKey)
let rendered = lane.cards.filter { !hidden.contains($0.id) }
let heights = rendered.map { registry.heights[$0.id] ?? LaneDropRegistry.nominalCardHeight }
let placement = MasonryPlacement(
@@ -232,10 +232,10 @@ struct BoardDropContext {
// The run's footprint at the landing spot: the first dragged card's frozen height, which
// is the trigger rect the cursor is over (the rest stack below it).
draggedHeight: session.cardHeights.first ?? LaneDropRegistry.nominalCardHeight,
current: session.laneProposal(onBoardRooted: store.rootURL, laneID: laneID)
current: session.laneProposal(onBoardRooted: store.rootKey, laneID: laneID)
)
guard let slot else { return } // a dead region: hold
session.propose(DropTarget(boardRoot: store.rootURL, container: .lane(laneID), index: slot))
session.propose(DropTarget(boardRoot: store.rootKey, container: .lane(laneID), index: slot))
}
/// The strip's fall-through for card sessions: which lane is under the cursor, analytically.
@@ -280,8 +280,8 @@ struct BoardDropContext {
return TrashDrop.accepts(
kind: session.kind,
container: session.container,
isWithinBoard: DragLocality.isSameBoard(sourceRoot, store.rootURL),
operation: session.resolveOperation(destinationRoot: store.rootURL),
isWithinBoard: sourceRoot == store.rootKey,
operation: session.resolveOperation(destinationRoot: store.rootKey),
isTrashShown: store.transient.isTrashVisible,
acceptsMutations: store.acceptsBoardMutations
)
@@ -304,7 +304,7 @@ struct BoardDropContext {
return
}
session.propose(DropTarget(
boardRoot: store.rootURL,
boardRoot: store.rootKey,
container: .trash,
index: TrashDrop.landingIndex
))
@@ -390,7 +390,7 @@ struct BoardDropContext {
placement: placement,
heights: heights,
nominalHeight: LaneDropRegistry.nominalCardHeight,
current: session.fileLaneProposal(onBoardRooted: store.rootURL, laneID: laneID)?.index
current: session.fileLaneProposal(onBoardRooted: store.rootKey, laneID: laneID)?.index
)
switch landing {
@@ -399,13 +399,13 @@ struct BoardDropContext {
case let .attach(index):
guard rendered.indices.contains(index) else { return }
session.proposeFile(FileDropTarget(
boardRoot: store.rootURL,
boardRoot: store.rootKey,
landing: .attach(cardID: rendered[index].id),
fileCount: count
))
case let .create(index):
session.proposeFile(FileDropTarget(
boardRoot: store.rootURL,
boardRoot: store.rootKey,
landing: .create(laneID: laneID, index: index),
fileCount: count
))
@@ -463,7 +463,7 @@ struct BoardDropContext {
func commitFileDrop(_ info: DropInfo) -> Bool {
guard acceptsFileDrops,
let target = session.fileTarget,
DragLocality.isSameBoard(target.boardRoot, store.rootURL)
target.boardRoot == store.rootKey
else {
session.proposeFile(nil)
return false
@@ -503,9 +503,9 @@ struct BoardDropContext {
/// live as the cursor crosses a board boundary (04-interactions.md Drag and drop).
func dropProposal() -> DropProposal {
guard let proposal = session.proposal,
DragLocality.isSameBoard(proposal.boardRoot, store.rootURL)
proposal.boardRoot == store.rootKey
else { return DropProposal(operation: .cancel) }
let operation = session.resolveOperation(destinationRoot: store.rootURL)
let operation = session.resolveOperation(destinationRoot: store.rootKey)
return DropProposal(operation: operation == .copy ? .copy : .move)
}
@@ -541,7 +541,7 @@ struct BoardDropContext {
}
revalidateProposal()
guard let target = session.proposal,
DragLocality.isSameBoard(target.boardRoot, store.rootURL)
target.boardRoot == store.rootKey
else {
cancelDrop()
return false
@@ -570,8 +570,8 @@ struct BoardDropContext {
}
let ids = survivors.map { session.members[$0] }
let folders = survivors.map { session.folders[$0] }
let within = DragLocality.isSameBoard(sourceRoot, store.rootURL)
let operation = session.resolveOperation(destinationRoot: store.rootURL)
let within = sourceRoot == store.rootKey
let operation = session.resolveOperation(destinationRoot: store.rootKey)
switch kind {
case .lanes:
+5 -5
View File
@@ -170,7 +170,7 @@ struct BoardView: View {
// discards itself the moment a snapshot lands because holding a moment longer would draw
// the arrangement twice.
.onChange(of: store.snapshotGeneration) { _, generation in
appModel.dragSession.handOff(root: store.rootURL, generation: generation)
appModel.dragSession.handOff(root: store.rootKey, generation: generation)
}
.trashPurgeAlert(store: store, confirmations: confirmations)
// The board's own anchor for the Style popover the surface a board-targeted session hangs
@@ -602,9 +602,9 @@ struct BoardView: View {
private var arrivingLaneUnits: Int {
let session = appModel.dragSession
guard session.isDraggingLanes,
session.stripProposal(onBoardRooted: store.rootURL) != nil,
session.stripProposal(onBoardRooted: store.rootKey) != nil,
let source = session.sourceRoot,
!DragLocality.isSameBoard(source, store.rootURL)
source != store.rootKey
else { return 0 }
return session.laneUnits.reduce(0, +)
}
@@ -612,7 +612,7 @@ struct BoardView: View {
/// The strip's current lane-drop proposal the reflow's narrow animation key, and where the
/// shadow run opens.
private var stripProposal: Int? {
appModel.dragSession.stripProposal(onBoardRooted: store.rootURL)
appModel.dragSession.stripProposal(onBoardRooted: store.rootKey)
}
/// One position in the strip: a lane, or one of the drag's N contiguous shadows.
@@ -635,7 +635,7 @@ struct BoardView: View {
/// the shadows opened at the proposal.
private var stripSlots: [StripSlot] {
let session = appModel.dragSession
let hidden = session.hiddenMembers(onBoardRooted: store.rootURL)
let hidden = session.hiddenMembers(onBoardRooted: store.rootKey)
var slots = boardLanes.filter { !hidden.contains($0.id) }.map(StripSlot.lane)
guard let index = stripProposal else { return slots }
let shadows = session.laneUnits.enumerated().map { StripSlot.shadow(index: $0.offset, units: $0.element) }
+1 -1
View File
@@ -720,7 +720,7 @@ struct CardFaceView: View {
/// The trash), and the trash column's own delegate clears the file highlight rather than
/// proposing one so this is a second, structural statement of the same rule.
private var isFileHovered: Bool {
role.container == .board && drops.session.fileAttachTarget(onBoardRooted: store.rootURL) == card.id
role.container == .board && drops.session.fileAttachTarget(onBoardRooted: store.rootKey) == card.id
}
/// **Board-only** "everything edit-shaped is disabled on trash selections Rename"
+27 -32
View File
@@ -31,7 +31,7 @@ struct DropTarget: Equatable, Sendable {
case trash
}
var boardRoot: URL
var boardRoot: BoardRootKey
var container: Container
var index: Int
@@ -133,7 +133,7 @@ struct FileDropTarget: Equatable, Sendable {
/// The board under the cursor only that board's delegates may commit, and only its lanes draw
/// the shadows, exactly as `DropTarget.boardRoot` works for our own sessions.
var boardRoot: URL
var boardRoot: BoardRootKey
var landing: Landing
@@ -175,7 +175,7 @@ struct FileDropTarget: Equatable, Sendable {
struct CommittedHold: Equatable, Sendable {
/// The board whose next applied snapshot retires this hold.
var boardRoot: URL
var boardRoot: BoardRootKey
/// That board's `snapshotGeneration` at the moment of the commit.
var generation: Int
@@ -188,8 +188,8 @@ struct CommittedHold: Equatable, Sendable {
/// Whether a snapshot applied on `root` at `generation` retires this hold. A snapshot on another
/// board says nothing about this one, and the *same* generation is the one already on screen at
/// the commit.
func isRetired(byRoot root: URL, generation: Int) -> Bool {
DragLocality.isSameBoard(root, boardRoot) && generation > self.generation
func isRetired(byRoot root: BoardRootKey, generation: Int) -> Bool {
root == boardRoot && generation > self.generation
}
}
@@ -204,16 +204,12 @@ struct CommittedHold: Equatable, Sendable {
/// is already the default. The operation is therefore a function of (source board, board under the
/// cursor, modifiers) sampled every frame, not a decision taken at pickup, which is what lets the
/// badge track live as the cursor crosses a boundary.
///
/// Which board a root names is `BoardRootKey`'s question, and it is asked as an `==`: the two sides
/// of every comparison below are keys their own stores minted once (`BoardStore.rootKey`), never
/// paths re-resolved under the cursor.
enum DragLocality {
/// Whether two roots name the same board. Symlinks are resolved first a board reached through
/// a pinned symlink is the same board as the one reached directly (01-storage-format.md's
/// symlink pins) and the standardized path is the comparison key.
static func isSameBoard(_ lhs: URL, _ rhs: URL) -> Bool {
lhs.resolvingSymlinksInPath().standardizedFileURL.path
== rhs.resolvingSymlinksInPath().standardizedFileURL.path
}
/// The effective operation, live.
///
/// Two carve-outs, both 04-interactions.md's:
@@ -314,7 +310,7 @@ final class DragSession {
/// The board the drag started in. Root and store are kept separately because the store may go
/// away with its window mid-drag while the root the left-hand side of the locality
/// comparison stays perfectly usable.
private(set) var sourceRoot: URL?
private(set) var sourceRoot: BoardRootKey?
@ObservationIgnored private(set) weak var sourceStore: BoardStore?
@@ -408,27 +404,26 @@ final class DragSession {
/// **The hold freezes the answer**, because `resolveOperation` refuses to move once a release
/// has settled: a settled copy keeps its originals on screen and a settled move keeps them
/// lifted, in both cases until the echo snapshot lands and the real faces take over.
func hiddenMembers(onBoardRooted root: URL) -> Set<ItemID> {
guard isActive, container == .board, let sourceRoot,
DragLocality.isSameBoard(root, sourceRoot)
func hiddenMembers(onBoardRooted root: BoardRootKey) -> Set<ItemID> {
guard isActive, container == .board, let sourceRoot, root == sourceRoot
else { return [] }
return operation == .copy ? [] : memberSet
}
/// The proposal's index when it names this board's strip, else `nil` the lane strip's shadow
/// run position.
func stripProposal(onBoardRooted root: URL) -> Int? {
func stripProposal(onBoardRooted root: BoardRootKey) -> Int? {
guard kind == .lanes, let proposal, proposal.container == .strip,
DragLocality.isSameBoard(proposal.boardRoot, root)
proposal.boardRoot == root
else { return nil }
return proposal.index
}
/// The proposal's index when it names `laneID` on this board, else `nil` the masonry's shadow
/// run position, in the lane's logical card order.
func laneProposal(onBoardRooted root: URL, laneID: ItemID) -> Int? {
func laneProposal(onBoardRooted root: BoardRootKey, laneID: ItemID) -> Int? {
guard kind == .cards, let proposal, proposal.container == .lane(laneID),
DragLocality.isSameBoard(proposal.boardRoot, root)
proposal.boardRoot == root
else { return nil }
return proposal.index
}
@@ -443,9 +438,9 @@ final class DragSession {
/// **Either kind proposes here** (lanes extended 2026-07-29), so there is no kind clause: what
/// makes the proposal legal is `TrashDrop.accepts`, asked at hover and again at release, and a
/// second copy of its answer written here could only ever disagree with it.
func trashProposal(onBoardRooted root: URL) -> Int? {
func trashProposal(onBoardRooted root: BoardRootKey) -> Int? {
guard isActive, let proposal, proposal.container == .trash,
DragLocality.isSameBoard(proposal.boardRoot, root)
proposal.boardRoot == root
else { return nil }
return proposal.index
}
@@ -468,9 +463,9 @@ final class DragSession {
/// The card an external file drag is hovering **on this board**, or `nil` the attach
/// highlight's one input (`CardFaceView`).
func fileAttachTarget(onBoardRooted root: URL) -> ItemID? {
func fileAttachTarget(onBoardRooted root: BoardRootKey) -> ItemID? {
guard let fileTarget,
DragLocality.isSameBoard(fileTarget.boardRoot, root),
fileTarget.boardRoot == root,
case let .attach(cardID) = fileTarget.landing
else { return nil }
return cardID
@@ -487,9 +482,9 @@ final class DragSession {
/// The floor restates `FinderDrop.shadowCount`'s, at the render end rather than in place of it: a
/// proposal that somehow carried a zero would otherwise open a run of no shadows at all, which is
/// a landing spot the user cannot see.
func fileLaneProposal(onBoardRooted root: URL, laneID: ItemID) -> (index: Int, count: Int)? {
func fileLaneProposal(onBoardRooted root: BoardRootKey, laneID: ItemID) -> (index: Int, count: Int)? {
guard let fileTarget,
DragLocality.isSameBoard(fileTarget.boardRoot, root),
fileTarget.boardRoot == root,
case let .create(lane, index) = fileTarget.landing,
lane == laneID
else { return nil }
@@ -576,7 +571,7 @@ final class DragSession {
self.container = container
self.mixesKinds = mixesKinds
self.sourceStore = source
self.sourceRoot = source.rootURL
self.sourceRoot = source.rootKey
self.proposal = nil
self.operation = .move
// The reload-resolved drag set: vanished members leave it silently, which is what
@@ -609,14 +604,14 @@ final class DragSession {
/// pure function is.
@discardableResult
func resolveOperation(
destinationRoot: URL,
destinationRoot: BoardRootKey,
modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags
) -> TransferOperation {
guard let kind, let sourceRoot, hold == nil else { return operation }
let resolved = DragLocality.operation(
kind: kind,
container: container,
isWithinBoard: DragLocality.isSameBoard(sourceRoot, destinationRoot),
isWithinBoard: sourceRoot == destinationRoot,
modifiers: modifiers
)
if resolved != operation { operation = resolved }
@@ -656,7 +651,7 @@ final class DragSession {
sourceStore?.transient.dragMembers = .empty
watchdog?.cancel()
watchdog = nil
let hold = CommittedHold(boardRoot: store.rootURL, generation: store.snapshotGeneration)
let hold = CommittedHold(boardRoot: store.rootKey, generation: store.snapshotGeneration)
self.hold = hold
let timeout = holdTimeout
holdTimeoutTask?.cancel()
@@ -684,7 +679,7 @@ final class DragSession {
///
/// Called from every board window's own snapshot-generation watch, which is why the hold names
/// the board it belongs to a reload on some other board says nothing about this one.
func handOff(root: URL, generation: Int) {
func handOff(root: BoardRootKey, generation: Int) {
guard let hold, hold.isRetired(byRoot: root, generation: generation) else { return }
end()
}
+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
)
+1 -1
View File
@@ -194,7 +194,7 @@ struct TrashLaneView: View {
/// Where the delete gesture's shadow run opens in this column always the topmost row or `nil`
/// when no proposal names the trash, which is every other moment of the app's life.
private var proposal: Int? {
drops.session.trashProposal(onBoardRooted: store.rootURL)
drops.session.trashProposal(onBoardRooted: store.rootKey)
}
/// The slots the column lays out: the rows, with the delete gesture's shadow run opened at the