The board's live title+body filter per 04-interactions.md § Search:
- SearchFilter — a pure value folding the query once (case- and
diacritic-insensitive substring, locale-stable); title OR body matches,
attachment filenames never searched; only the literal empty string is
inactive.
- One universe: the filter threads through SelectionGrammar's order lists
as a defaulted parameter, so ranges, Select All, arrow navigation, the
marquee, drop zones, count badges, and the shown trash all read the same
filtered set by construction; lanes are deliberately never filtered out
(an emptied lane keeps its slot with a 0 badge). Hidden cards leave the
selection through the existing constrain primitive, run on every query
change and as the last line of the reload resolve; the delete successor
is filtered so ⌫ never selects a hidden neighbour.
- The field: an NSSearchField-backed toolbar item (the toolbar's sole
default item); Edit ▸ Find ⌘F focuses it through a focused-value
presentation; stock field-editor dispatch — Return swallowed, Tab is the
keep-filter path to the board, board commands stay enabled except the
caret-chord pair, now one shared caretChordsYield expression.
- Escape is staged: clear the non-empty query (focus stays), hand an empty
field back to the board, clear an active search from board focus —
before Escape's clear-selection meaning.
- Creating a card clears the search (the placeholder funnel); a rename
deliberately gets no carve-out; filter reflow rides the content spring
keyed narrowly on the query.
903 unit tests (24 new).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
⌘X/⌘C/⌘V for cards and lanes per 04-interactions.md § Clipboard:
- ClipboardStore stages full folder snapshots eagerly at the gesture into
Application Support (at most the current copy; sweep at launch and on
each copy purges what the pasteboard no longer references; a copy made
before quitting pastes whole after restart) and writes the pasteboard a
JSON manifest — every entry embedding its index.md, lane entries their
cards' too — plus plain-text titles.
- Cut is Finder-style deferred: items dim in place off pendingCut, void on
pasteboard takeover (changeCount, no timers), source-board close, or
per-item external tombstoning; the first armed paste moves the surviving
originals whole (tombstoned interior cards land in the destination's
trash), a second paste materializes copies from staging.
- Paste anchors by the shared flatten-order rule (NewCardTarget's anchor,
extracted); a tombstoned selection never anchors; lane paste reaches the
right end and stays enabled on a zero-lane board; paste into the source
board is the within-board lane duplicate; copies keep created, take
fresh GUIDs, and strip tombstoned cards; trash-sourced copies strip
deleted: at materialization; ⌘X is disabled on the trash side.
- A degraded paste is loud, never silent: staging gone → the embedded
index.md fallback lands content-intact, attachments absent, and a
BannerCenter-phrased row names what was lost.
- The standard Edit items validate through conditionally-attached
onCommand handlers, so AppKit's enablement mirrors the availability
predicates; text fields keep their own clipboard while focused.
879 unit tests (68 new).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Files from Finder land on the board per 04-interactions.md § Drag & drop:
- Dropped on a card, they copy into its attachments/ (any type, multi-file),
the face highlighting while hovered; the hovered card is resolved by
hit-testing the same analytic masonry frames the card zones are built
from, so attach-beats-create adds no drop region and cannot drift from
the dispatch.
- Dropped on lane empty space, one card per file — filename minus extension
as the title (a blank stem omits the key), fresh GUID, rank at the drop
position through the ordinary insertion machinery, the file attached —
all in one bracket; a failed import removes the just-minted card, so
creating-then-abandoning never leaves an empty card behind.
- Every board drop surface now declares .fileURL beside the two board
types (the single-target-dispatch rule); tombstoned surfaces are inert;
file sessions ride a distinct session mode with their own watchdog and
no hysteresis, leaving the board-drag machinery untouched.
- Also: four empty fixture directories pinned with .keep files so git
preserves them, and a test-only visibility fix in DragSessionTests.
811 unit tests (12 new).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The second half: system drag sessions over phase 1's model, per
DRAG-REORDER.md and 04-interactions.md § Drag & drop.
- Card faces, lane headers, and trash rows drag as NSItemProvider sessions
(two exported UTTypes, JSON payload in flatten order, plain-text titles as
the secondary representation) — replacing m4's custom lane-reorder gesture
and trash drag-out wholesale; the app-wide DragSession carries the members,
the frozen dragged sizes, the live proposal, and the effective operation.
- Three drop delegates (lane masonry, strip, window fallback), each accepting
both types and routing internally per the single-target-dispatch rule; the
cursor is the physical mouse converted to strip space; proposals come from
DropSlotMath with hysteresis threaded through, and the lane-strip proposal
clamps in front of the shown trash.
- Locality picks the default — move within a board, copy across, the badge
tracking live; ⌥ forces copy (ignored on within-board lane drags), ⌘
forces move; trash rows restore within their board (positional), copy out
across boards by default, ⌘ forcing the true restore-move.
- N contiguous shadows with reflow keyed on the proposal; the
committed-overlay hold renders the dropped arrangement until the reload
echo lands (1.5 s dissolution deadline for refused writes); the
re-grounding trio: geometry re-derives per render, proposals re-validate
by liveness at release, an emptied drag cancels itself.
- Edge autoscroll (ticking driver over DragAutoScrollMath, re-targeting per
step), the mouse-up-gated late-event cleanup, and the polling watchdog —
the pathfinder's lifecycle traps, ported.
- Store: moveLanes and multi-card restoreByDrag join the one-bracket drop
commits.
784 unit tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The pathfinder's drag-reorder model, ported and generalized (DRAG-REORDER.md
travels with it, rewritten for lanes, the interior masonry, multi-drag,
cross-board sessions, the re-grounding trio, and the committed-overlay hold):
- DropSlotMath — resting-layout zones from analytic lane arithmetic and the
pure masonry placement (MasonryLayout now lays out through the same
MasonryPlacement the drag reads, so geometry cannot drift), span-capped
triggers sized to the dragged run's future footprint, hysteresis holds with
the fresh-entry fallback, boundary ties, own-slot no-ops; nil means hold.
- DragAutoScrollMath — the activation bands and velocity ramp, pure.
- The drop commits, one performWrite bracket each: moveCards/copyCards within
a board (insertion ranks touch only the dragged cards; renumber fallback);
receiveCards/receiveLanes/receiveRestoredCards on the destination store for
cross-board copy and ⌘-move with the import-boundary remint, lane copies
stripping tombstoned cards while moves carry them; restoreByDrag is now
positional, writing order only when the drop names a new one.
Gestures, sessions, previews, and delegates are the second half.
773 unit tests (87 new since the keyboard grammar).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The board's fixed grammar keys and the menu-backed chords of
04-interactions.md § Keyboard, per the Command Nexus inventory:
- Spatial arrow navigation (NavigationMath.nearest over the marquee
registry's frames — one geometry source), walking across interior
masonry columns, lanes, and into the shown trash; ⇧-arrows extend via
the same range function as ⇧-click and go inert at the liveness and
kind boundaries; ⌥-jumps with the ⌥↑ lane-domain escalation and ↓
descent; the empty selection seeds at the first lane's first card;
selection scrolls into view.
- selectionHead — the navigation cursor beside the anchor, set by every
click, moved by every arrow, dropped by the reload vanish rule.
- Board ▸ Open Card ⌘↩ (the one command enabled mid-edit: commits the
placeholder or rename and opens), Move Up/Move Down ⌥⌘↑/⌥⌘↓
(within-lane sort, gather-then-step, rank-permuting writes in one
bracket), Move Left/Move Right ⌘←/⌘→ (sole lane, one slot, never the
trash) — all validating and acting off one shared answer.
- Delete now selects the Finder-style successor sibling from the
pre-write snapshot, so repeated ⌫ walks down a lane; external
vanishing still only shrinks the selection.
- handleReturn rejects modified Returns; the trash column renders
eagerly so every row stays registered for navigation and the marquee.
686 unit tests (27 new).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The full pointer grammar of 04-interactions.md § Selection, stated once
as a pure function (SelectionGrammar) and reached through one store
funnel from every click surface — card face, lane header, lane empty
space, trash row:
- Plain click replaces and anchors; the lane surfaces (header and empty
space alike, per the settled one-lane-click-behavior rule) toggle off
on a sole-membership repeat.
- ⌘-click toggles within a homogeneous set; crossing any axis — cards
XOR lanes, live XOR trashed, card entries XOR lane entries in the
trash — degrades to a replace, so no click can produce a mixed
selection.
- ⇧-click ranges from the anchor in the (side, kind) order list: flatten
order for cards, lane order for lanes, the trash's deterministic sort
filtered to kind — the pointer twin of the keyboard's boundary rule
(the keyboard goes inert, the pointer skips).
- The rubber band (MarqueeSession/MarqueeMath) arms from lane empty
space, the board backdrop, and the trash column; side frozen at the
origin, trash bands homogeneous by topmost kind, frames self-registered
in strip space, geometric begin guard, never animated.
- Fast plain double-click opens the card window (⌘↩'s pointer twin);
Select All answers the standard Edit menu item via the responder
chain, trash- and kind-respecting.
- The range anchor lives in TransientBoardState beside the selection and
obeys the same reload vanish rule.
659 unit tests (28 new in SelectionGrammarTests).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
One named home for every curve and duration — the two-voice split 03
fixes (snappy structural: drag reflow 0.18, delete 0.25, lane resize
0.2; smooth content-reflow 0.28), the scale-and-fade appear/disappear
transitions (cards 0.8, lanes 0.9), and a Reduce Motion variant on
every accessor (animations go instant, transitions go crossfade). A
post-migration grep holds the invariant: zero motion literals outside
Motion.swift. The animate-vs-snap split lands where Lanework's
one-way flow puts it: the store's reload seam. An app-mediated echo
applies its snapshot inside the structural transaction; foreign and
reconciling reloads — and every bracket-ending wholesale reload —
assign bare, because live-reload is the board becoming what's on
disk, not an event to perform. A window that merged foreign events
into an app-mediated span animates, deliberately: the ratified merge
rule makes it indistinguishable from a pure echo, and a test pins
that reading so a future mixed case fails loudly. The lane-reorder
reflow keys on the drop proposal alone (pointer tracking stays 1:1),
the resize session freezes its Reduce Motion answer at drag start so
the unit tick and the window resize can never disagree, and the m5
drag / m5 search / m7 undo voices are named seams waiting for their
call sites. 13 new tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Deletion becomes a two-stage, Finder-style story. File > Delete and
plain Backspace tombstone the live selection; View > Show Trash (no
chord — Shift-Cmd-T stays with the system's tab bar) reveals the
quasi-lane: trailing, one fixed width unit consumed only while shown,
hatched dimmed header, count badge, no new-card button, exempt from
resize and reorder alike. Its contents are a pure view over the
snapshot — the deterministic sort (deleted newest first, folder-name
ties, unparseable stamps oldest) interleaves card rows with a
tombstoned lane's single entry, whose count names what Put Back
returns; the ancestor walk is absolute, so an own-flag card beneath a
tombstoned lane has no row and recovery is deliberately two steps.
Put Back twins Delete on Cmd-Backspace with validation enabling
exactly one; restore fidelity is byte-perfect because nothing ever
moved. Delete Immediately confirms exactly where loss is real (every
board is mode-none today; the predicate names the git carve-out for
m7), Empty Trash always confirms with the true whole-board count,
and dragging a tombstoned card onto a live lane restores it there —
positional drops and cross-board locality arrive with m5's machinery.
The banner's delete phrasing drops "move to the trash" per the naming
constraint: board deletion says Delete, "Move to Trash" stays
reserved for the system Trash. 47 new tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY