Skip to content

Add File Shelf, a drag-and-drop staging area for files - #30

Open
havokentity wants to merge 2 commits into
mainfrom
feat/file-shelf
Open

Add File Shelf, a drag-and-drop staging area for files#30
havokentity wants to merge 2 commits into
mainfrom
feat/file-shelf

Conversation

@havokentity

Copy link
Copy Markdown
Owner

Wave 2 Phase A.

Implementation: Add File Shelf, a drag-and-drop staging area for files
Review fixes: File Shelf: make the icon clickable and stop re-drops forking a row

Built by an isolated agent, then adversarially reviewed by an independent agent that checked the branch out and ran swift build + the full suite itself. Every review finding was fixed, with a regression test added for each high and medium.

🤖 Generated with Claude Code

havokentity and others added 2 commits July 21, 2026 13:26
Moving a file between two apps, or between two Spaces, currently means keeping
a Finder window parked somewhere just to hold it. File Shelf is that holding
area without the window: drop files onto the menu bar icon or into the popover,
then drag them straight back out wherever you land.

Shelving deliberately does not copy bytes. Each entry is a security-scoped
bookmark to the original file, which buys three things a list of paths could
not: parking a 40 GB video costs the same as parking a note, the shelf survives
a relaunch, and an entry keeps working after the file is renamed or moved —
exactly the tidying people do while something is parked on its way elsewhere.
Dragging out hands over the real file URL, so the receiving app attaches or
opens the original rather than a duplicate.

A file that has been deleted, or that lives on a disk that is currently
unplugged, stays on the shelf and is drawn as unavailable. The alternative —
pruning it — makes the shelf look like it lost the thing the user parked, and
an unplugged disk comes back on its own.

De-duplication compares device and inode rather than paths, so the same file
reached through a symlink, or dropped again after a rename, collapses onto the
row it already has. Bookmarks are resolved with `.withoutUI` and
`.withoutMounting`: without them, a routine refresh of an entry on an unplugged
disk pops a system "please insert the disk" panel out of a background menu-bar
app.

The menu-bar icon is a drop target in its own right, which is the point of the
tool — park a file without opening anything. AppKit creates NSStatusBarButton
and its dragging-destination methods are overrides rather than delegate
callbacks, so the only way in is a transparent child view that registers for
file URLs and hands every mouse event straight back to the button underneath;
without that pass-through the overlay would be a dead zone over the icon.

The rules that decide what the shelf holds — ordering, de-duplication, the cap,
bookmark resolution and the on-disk format — live in FileShelfKit as pure
functions over values, and the tests drive real bookmarks against files in a
temporary directory. A stubbed resolver would have asserted nothing about the
one question that matters: whether a bookmark actually follows a rename.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three things the drop overlay and the shelf logic were getting wrong.

The overlay that turns the status button into a drag target wins hit-testing
over the button, and NSView answers false to acceptsFirstMouse where
NSStatusBarButton answers true. This tool is LSUIElement and never becomes the
active app, so every click on its icon is a first-mouse click: AppKit was free
to spend that click on activation instead of delivering it, and the
mouseDown pass-through that opens the popover never ran. The only other two
status-button subviews in this package already override this for exactly the
same reason.

De-duplication compared a dropped file against identities cached on `items`,
which were computed at the last resolve — startup, or the last popover open.
A device/inode pair does not survive a safe save, and a drop straight onto the
menu-bar icon never opens the popover, so shelving a file, editing it in any
app that writes atomically, and dropping it again found no match and minted a
second row. Nothing collapsed the two afterwards. shelve() now re-resolves
first, through the same helper refresh() uses, so a drop is judged against the
shelf as it is rather than as it was.

Finally, .missing was unreachable. A bookmark to a deleted file, or to one on
an unmounted volume, does not resolve at all — .withoutMounting sees to the
second — so every failure landed on .unresolvable, whose copy tells the user
the entry has to be shelved again. Someone who merely unplugged an external
disk would have removed a row that was about to start working by itself. The
two cases are now told apart by reading the bookmark's own cached resource
values, which survive while the file does not; junk data carries none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant