Implement the hybrid clipboard with deferred cut

⌘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
This commit is contained in:
2026-07-27 22:18:58 -04:00
parent 8f116934b4
commit 7eee0934ee
18 changed files with 2749 additions and 59 deletions
+7 -1
View File
@@ -10,7 +10,13 @@ import Foundation
/// (`ItemReferenceSet`), so the kind of a selection is always re-derived from the snapshot. Storing
/// it would be a second answer to a question the snapshot can always answer, and one that a reload
/// could falsify.
public enum SelectionKind: Sendable, Equatable {
///
/// The **one** place a kind is written down is the clipboard manifest, which has no snapshot to
/// re-derive it from "the cards-XOR-lanes selection rule means the clipboard holds cards or lanes,
/// never both" (04-interactions.md Clipboard). Hence `String`-backed and `Codable`: those raw
/// spellings are pasteboard API, decoded after a relaunch, and they are the case names so there is
/// no second vocabulary to keep in step.
public enum SelectionKind: String, Codable, Sendable, Equatable {
case card
case lane
}