16ef3779e810c24026019dc60b50a5cec7bebc08
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d076427ee0 |
Realign undo with the evening rulings — repo-nested and identity anchors
Repo-nested boards bind native undo in every tier ( |
||
|
|
71664dab02 |
Give card windows their own undo stacks and coarsen the close
Phase B of the two-level undo card: every card-window gesture — comment
post/delete/edit, body Edit sessions, style and details changes —
registers fine-grained on the window's own stack (window.undoManager
answers with it; board ⌘Z never sees mid-session card steps; an empty
window stack beeps, never falls through). Window close folds the stack
into one coarse values-based board step ("Edit card 'X'") — per-target
per-field later-wins merge, so foreign mid-session writes stay out by
construction, a no-net-change session registers nothing, and any stale
component skips the whole step. The comments/.trash purge defers with
the coarse step via a step-retirement seam on the providers: it runs
when the step leaves the board stack or the board session ends; the git
provider retires dropped steps on register, which keeps Pro's
purge-at-close-flush structural with no tier check. Interim on git
boards: gestures still auto-commit per debounce until phase C's
close-flush commit.
2432 tests in 418 suites green.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
|
||
|
|
bec75e4282 |
Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
142c6e75fe |
Implement undo and redo as forward commits
GitHistoryProvider is the second HistoryProviding implementation: its stack IS HEAD's first-parent ancestry, reseeded on load (redo empty), re-synced to HEAD before every crossing so agents' self-commits become the top and ⌘Z steps back exactly one commit; any arriving commit clears redo (a heal-only window deliberately does not). Restores are forward commits through the ordinary signature path — GitRestoreOperation materializes only the current-vs-target diff as working-tree writes and resolves no reset/checkout symbol at all; heal commits are transparent in-session (pointer passes over, restores exclude heal-owned paths, identity carried on landed windows via PlannedCommit.kind → GitLandedCommit). Subjects "Undo:/Redo: <crossed subject>"; menu labels never nest in-session; the root commit is not a step (crossing it would restore the empty tree). Provider binding flips: makeHistoryProvider(store, tier, git) — free binds native everywhere, Pro binds the git provider on git boards and NOTHING on mode-none/repo-nested (the pair disables through existing validation); add-git mid-session live-binds via HistoryStore.didAddGit → bindHistoryProvider (the flip only ever adds). SessionSettleGate is the reusable Save All / Discard / Cancel step: restores whose diff touches an open Edit session or raw-source buffer gate on it (Save All applies with validation — a refused buffer cancels the whole restore focused on the offender; Discard reverts via CardBodyEditSession.discardBuffer and reconciles against the working tree, deliberately skipping the second flush); untouched sessions ride through undisturbed. Built for the branch-switch card to reuse. BoardStore gains the async performWholesale sibling. CardHistorySection fills the m6 EmptyView slot: read-only, newest first, follows the card across lane moves by folder-component match (the UUID is the identity — no rename detection), absent off git mode and off Pro. 2332 tests / 403 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
3c07c26fda |
Build the auto-commit engine
Every settled change on a git-mode board commits, debounced 2s past drag/typing churn, staged whole-root with .gitignore respected. GitCommitOperation reaches the vendored libgit2 directly (same 1.9.2 pin SwiftGitX resolves — importable, not duplicated) for signature-capable commits; add-git's config materialization is gone, identity resolves at commit time (repo-local config, else derived default) per the 2026-07-31 ruling in 06. CommitAttribution classifies per file off EchoLedger receipts: user identity on app-mediated windows, Lanework External <[email protected]> on foreign, the modified-by refinement (<slug>@agents.lanework .invalid) when every foreign file agrees, heal-marked receipts split into their own commit — window split foreign → heal → user. Edit-session granularity: ~700ms saves stay uncommitted, staging excludes open session folders (closure-resolved so mid-session moves stage around the new location), session end nudges the debounce so each session lands exactly one body commit. Flush-before-overwrite gates on known-foreign windows and commits synchronously ahead of the write; close/quit flush the pipeline via CloseFlushCoordinator's committerFlush. index.lock backs off briefly then re-debounces silently; clean tree no-ops; genuine failures ride the standing history-suspension banner and retry next debounce. Abnormal repo states (detached HEAD, merge/rebase/cherry-pick in progress) hold the engine with a 15s re-check; unborn HEAD commits "Initial board state" whole-tree; dirty tree at open catches up through the same engine. Message seam (CommitMessageComposing) ships interim — the semantic composer is the next card. Discovery diffs HEAD against an in-memory index with rename detection (git status alone never pairs a bare mv), and a failed survey reads as "could not look", never "nothing changed". 46 new tests / 8 suites, all real repositories via bundled libgit2. 2240 tests / 383 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
189af238a1 |
Build HistoryStore — opt-in git and mode detection
The pro-m1 foundation card. SwiftGitX 0.4.0 (bundled libgit2, the pathfinder's pin) joins the one target; new Kanban/Git/ holds BoardGitMode (pure nearest-.git-wins detection, .git-as-file counts, NSString ancestor walk), HistoryStore (@MainActor @Observable; compose() is the tier gate — free tier gets no object, no detection, no stat), GitRepository (scope-confined SwiftGitX handles: create = init + HEAD forced to main + whole-tree "Initial board state" commit; branch reads incl. unborn/detached; path-history ranks), GitIdentity (derived default as a pure function + repo-local config reader — not libgit2's merged ladder), and GitPathHistory (Mutex-guarded lazy ranker). beginSession composes the git state beside the tier and feeds BoardStore.makeIdentityHistoryRanker; git-mode loads pass the git-backed IdentityHistoryRanker to BoardLoader. The popover's git slot resolves a pure five-way matrix: free tier unchanged (absent / BoardGitNote), Pro mode-aware — Add Git on mode none, honest prose on repo-nested, read-only branch line on git. Provider binding unchanged: both tiers still bind native until the undo/redo card. 42 new tests across 8 suites, all repositories built through bundled libgit2; InertGitTests untouched and green. 2194 tests / 375 suites. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
9588f7b1f0 |
Comments, phase 3 — search, the thread find, announcements, and a11y
Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
092300c7d2 |
Collapse the edition split — one target, twins merged, verify-editions retired
Phase 1 of the 2026-07-30 one-app pivot (DESIGN
|
||
|
|
f7c8088783 |
Lanes delete into the trash — rendering, grammar, drag, clipboard, a11y
Phase 2 completes the lanes-in-trash card. TrashEntry merges the trash's two kinds by rank in exactly ONE place (ItemPath.resolve's own merge deleted in favor of it — the three-merge-points finding shrinks instead of growing). TrashLaneRowView renders the opaque row — tertiary plate, level-default lane glyph never the lane's own icon, title + card count, no accents, no expansion; the column badge counts rendered rows. Selection grammar: kind-homogeneous trash selections — ranges skip the other kind, ⇧-extension stops at the kind boundary, plain arrows walk the merged order, marquee stays card-only (now load-bearing: rows register frames for arrows), Select All card-scoped; successor-on-purge crosses kinds like navigation as the interim for open Gap 7b5cbc90. Drag: TrashDrop accepts lane sessions (drop on shown trash deletes), restoreLanes routes a trash-sourced strip drop as an arrival-ranked within-board move with an undo step. Clipboard: ⌘X/⌘V lane restore via opaque lane subjects; fixed boardRoot(ofLaneFolder:) returning .trash as the root — a same-board restore looked like an import and would have reminted the lane it was restoring (pinned by test). A11y: row = one flattened "title, deleted lane, N cards" element with Delete/Reveal actions; BoardDiff crossings read lanes as deleted/restored, shown-trash churn digested at row level. Agent guide stays v7 — the literal already teaches lanes-trash-by-move and kind stamping; drift-guard pins those lines. README trash paragraph notes lanes. Both schemes 1893 tests / 322 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
8014bde7c6 |
Lanes delete into the trash — storage, loader, writer, and undo
Phase 1 of the lanes-in-trash card (2026-07-29 ruling, docs led the
code): lane delete is a move into .trash/ with the subtree intact,
arriving at top trash rank — no destructive delete remains outside
the trash.
TrashedLane opaque unit (id/schema/title/order/heldCards) beside
trash cards — deliberately not a Lane, so no card-shaped surface can
believe an empty subtree. Loader's trash walk trusts the kind VALUE
(lane → opaque unit w/ held-card count counted at the loader's own
unit; card → ordinary card; absent/unrecognized → UUID-children
shape, empty-kindless falls to card per 01's honest limit). Writer:
moveIntoTrash generalized with kind passed never derived (an empty
lane would re-derive as card), deleteLaneToTrash mints against the
whole-container rank ladder. Retired: migrateTombstonedLane (lane
deleted: now ignored — loads live, bytes inert, tolerate-tier
warning), removeLane, captureSubtree/recreateSubtree and the
subtree-snapshot machinery. Undo inverse = move back to captured
strip position, redo replays at captured trash rank. Purge walks
lane subtrees; TrashModel.Freight phrases confirms with lane freight
("…and its 5 cards"). ItemPath gains .trashLane; resolve interleaves
the trash by rank; SearchFilter matches lane rows by title only.
Trashed-lane card windows dismiss and pending cuts void via the
ordinary vanish rule — no new plumbing.
Phase 2 (rendering, selection grammar, drag, a11y, agent guide)
follows. Both schemes 1858 tests / 318 suites green.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
785ef5fe14 |
Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (ruling |
||
|
|
69084fdff7 |
Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
0463540aea |
Dedupe duplicate ids and heal them silently
The crash-class gap the integrity design pass found (DESIGN/01 - Fractal layout rules; 02 - Live-reload resilience): the loader had no board-wide dedupe at all, so two hand-copied folders sharing a UUID put two equal ItemIDs into one snapshot - which SwiftUI's ForEach does not tolerate. Built to the day's re-rulings, both landing mid-flight: the user-gated Repair banner retired ( |
||
|
|
3a9db2e78b |
Build the integrity service - IntegrityRules and the HealScheduler
The 2026-07-29 integrity design pass, consolidated (DESIGN/01 -
Validation and healing; DESIGN/02 - Components): IntegrityRules
(Storage, pure) is the one home for the identity predicate and
canonical form (BoardWriter.canonicalIdentity deleted, ItemID and the
loader forward to it), the per-field rulebook, uneditable shapes,
per-kind index validation, the reserved-name tables, and the trash
kind discriminator (values trusted - kind: lane/card explicit,
unrecognized falls to shape). LoadResult's ad-hoc channels fold into
one typed Defect stream (looseCardFiles / legacyTombstone /
claimedNameSquatted, per-defect heal signatures); the old accessors
survive as computed views.
HealScheduler (LiveStore) states the six-step heal pattern once -
resting-clear, lock gate, isWritableFile gate (now covering all four
heals), signature memo armed-before-attempt with explicit
clear-on-success, disk re-verify in each write half, one banner-posture
table (BannerCenter keeps all phrasing). The three hand-rolled healers
run on it with behavior preserved - including the
relocation-notice-despite-partial-failure quirk, deliberately. Heals
run at the reload tail AND at registry acquire, closing the
migration-never-fires-at-open asymmetry. Displacement runs first: a
squatted .trash would otherwise fail the migration and arm its memo
against an unchanged picture.
Claimed-name squatters (ruled today,
|
||
|
|
89d4d983e6 |
Wire the open-time writability probe and read-only lock
Closes the gap found at m10: enterUnwritableLock existed with zero call
sites. WritabilityProbe classifies the cause volume-first - a board on a
read-only DMG is also permission-denied by access(2), and "you don't
have permission" would send the user to a Get Info panel that cannot
help - with a pure classify(volumeIsReadOnly:isWritable:) truth table
and a two-syscall probe that rebuilds its URL to defeat NSURL resource
caching. ReadOnlyLockReason.unwritableLocation now carries the cause;
BannerCenter phrases the two ("this board's volume is read-only" vs
"you don't have permission to change this folder").
The probe wires once in BoardStoreRegistry.acquire, immediately after
the store loads - every open path funnels through it, and running
before the loose-file relocation and agent-guide hooks makes the
skipped-with-log guide write true by construction (its isWritableFile
pre-check demotes to second line of defense). The board still opens:
lock, not refusal.
The reconciling re-probe is now symmetric per 02's settled text - a
volume gone read-only mid-session raises the lock at the next probe
(sibling locks settle first, so a root returning read-only lands the
honest lock); the stale "deliberately one-way" comment and its pinning
test are gone. Save as Template's carve-out predicate extracted to a
testable allowsSave (behavior unchanged); Duplicate stays disabled.
11 tests added. 1649 green on both schemes.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
5880838e66 |
Build the EchoLedger - per-file write provenance for announcements
User-ruled 2026-07-29: the ledger builds now in base, pre-release (DESIGN/02 - Components - EchoLedger; DESIGN/10 - Live board announcements). Receipts drop inside BoardWriter's four disk primitives (atomic replace, folder move, removal, attachment copy) into a @TaskLocal ledger that BoardStore.performWrite binds for the bracket's duration - no call-site bookkeeping, and performWholesale deliberately binds nothing per 02's bracket exemption. Classification is a pure function of two snapshots: an item whose folder, index.md bytes, or attachment listing differs is an observed change; disk matching the receipt is app-mediated (receipt consumed), no receipt or mismatch is foreign. Byte-identical foreign overwrites classify app-mediated (unobservable, accepted); a foreign edit over a fresh app write classifies foreign. The announcer now consumes per-file facts on every reload origin - the WatchOrigin gate is gone (ReloadFacts.origin removed outright; nothing read it after the gate fell). Reconciling sweeps announce their receipt-less findings as foreign, closing both interim holes (debounce-window absorption, reconcile silence). The vanishing-focus rung gates on the ledger too: "deleted externally" would be a lie about an app-mediated delete, and the subject's own verdict decides. Divergence flagged: attachment imports hash the landed file right after FileManager.copyItem rather than during the copy (the bytes do not stream through the app); an unreadable read-back records nothing, the direction that biases toward foreign. 30 ledger tests added, announcer suite reworked to the ruling. 1638 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
28ca2c3f50 |
Realign code with the 2026-07-29 accessibility rulings
Three ruled behavior changes (DESIGN/10, resolution session 2026-07-29): - The board-change digest covers the trash while View > Show Trash is on: BoardDiff.between gains includingTrash, keying its card index by ItemPath so foreign purges, restores, and Empty Trash join the digest; crossings of the trash boundary still read deleted/restored, never moved, on both sides of the toggle. BoardStore.land passes the store's own isTrashVisible - no new injection seam. - A vanished head with surviving co-selection is still named: naming and recovery are independent axes, so BoardAnnouncer's vanished-focus rung fires on all branches while the survivors-veto now gates only the recovery half (recovery implies vanished, no longer both-or-neither). - Banner-row buttons are literal FKA Tab stops: BannerRow.controls is the row's testable button inventory, BannerRowView renders from it with .focusable() on each button; the combined VoiceOver element stays unconditional - custom actions and Tab stops are independent surfaces. 19 tests added, 2 expectations updated to the rulings. 1607 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
c339b4cecf |
Implement live accessibility announcements
The board speaks when files change under the user, per DESIGN/10 § Live board announcements. BoardDiff is the pure snapshot summarizer (identity sets for cards/lanes added/edited/moved/deleted — ids, not tallies, so pro-m1's semantic commit engine can build on it; edited = rendered content only, moved beats edited, implied events don't steal the subject). BoardAnnouncer is the decision seam: focusOutcome computes the vanishing-focus sentence and the walk-up-then-sideways recovery (next lane by order, else previous, board container only when none remain, never the trash); speech(for:) is the one-sentence precedence ladder — raised condition > bracket completion > cleared condition > vanished focus > digest — foreign-only for the last two rungs, so app-mediated echoes stay silent. BoardStore.land assembles ReloadFacts and posts exactly one sentence per reload through the injectable announce outlet (AccessibilityAnnouncer, medium priority, never interrupting). Selection recovery layers on top of ItemReferenceSet re-resolution — survivors veto, the emptied selection lands on the vanished item's lane and re-arms ⌘N's active-lane memory. performWholesale(announcing:) arms a completion phrase consumed by the closing reload — nil on every base bracket today; pro-m1 fills git phrasings. Locks raised outside the reload path (vanished root, unwritable location) announce through the same ladder, and the banner strip is a labeled "Board status" container whose row labels are the announced sentences (AccessibilityPhrases.bannerLabel — one string for eye and ear). Announcements classify at reload granularity (WatchOrigin) as a deliberate interim: DESIGN/02's EchoLedger (per-file classification, the announcer's specified input, git-free) was scheduled with the auto-committer that the edition split moved to pro-m1 — filed on the Redesign board for a ruling. 1533 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b3812ed928 |
Build AgentGuide — versioned CLAUDE.md maintenance
The app-owned agent guide at every board root (DESIGN/08 ▸ The agent guide): version-gated by a first-line marker (v5, superseding the pathfinder's v4 guides on real boards), rewritten when missing or older, byte-for-byte untouched when current or newer. A markerless CLAUDE.md is displaced to CLAUDE.user.md when that name is free — never clobbered — and the guide write is skipped with a log when it isn't. Symlinks, folders, and read-only volumes are skipped in silence; the write rides performWrite's bracket as an app-mediated Writer operation (new WriteOperation.agentGuide), so the echo lands appMediated and the Pro-era committer can attribute it honestly later. Hooked at store acquire (beside the loose-file relocation, after the watcher exists) and on every successful reload — the guide self-heals from foreign deletion or rollback, pre-wiring 06's acknowledged undo bounce. The refresh memo arms before each attempt and clears on a successful write, so a failing write can't hot-loop and a foreign deletion stays healable. First-line-only marker parsing (no Regex); guide content is one swappable literal, finalized under the next card. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
53bc71f7fb |
Materialize the trash — store, undo, and the container universe
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone, replaced by ItemContainer — a UUID set plus the container side it lives on, presence the whole test, one selection boundary instead of the old liveness law. Deletion stages by place: board cards move to the trash at a store-minted head rank, trash-side delete is permanent behind its confirmation, Delete Immediately skips the trash from anywhere, lane delete captures the subtree and removes the folder. Restore has no method at all — moveCards resolves members in either container, so drag-out and cut-paste are the ordinary moves 13 calls them, registering ordinary Move steps. The delete inverse moves the card back to its captured lane and rank; redo replays the captured trash rank, a value the gesture actually wrote; lane undo recreates the subtree byte-faithfully in session. Purges register nothing — where 13's trash section contradicts its own Rules on that, Rules wins, filed for ruling. Staleness collapsed to present-or-absent: a container is a path, so a foreign restore fails the delete step's expectation structurally. Legacy tombstones migrate on the loose-file tail hook, cards oldest-first so minting above top reproduces the retired newest-first column, lanes returning live, one folded loss row naming both directions. Put Back, restoreByDrag, receiveRestoredCards, TrashEntry, and the kind machinery are deleted; the trash column renders the container correctly with its full face rework left to phase 3. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
50669489cb |
Implement staleness validation and skip-with-banner
Every crossing validates its expectations before writing: each step
carries per-item HistoryExpectations — folder, effective ancestor-walked
liveness, and exactly the fields the gesture set — and a mismatch pops
the step, posts the signpost ('Undo skipped — Fix login changed outside
Lanework'), and falls through to the next. Validation reads disk, not
the in-memory snapshot: the snapshot is by construction one reload
behind every app write, so a rapid second undo would false-skip against
the pre-state — disk is what current can honestly mean at press time.
Stale and failed part ways: a stale step is one the board moved past,
so dropping it loses nothing; a failed one is refused by a usually
momentary condition, so it stays put and the crossing stops with only
performWrite's own error row — which forced the provider off
NSUndoManager onto two plain arrays, since a popped group cannot be put
back. The read-only lock disables Undo/Redo through the adapter while
the stack survives to resume on clear. Delete and restore validate
presence alone — a machine timestamp is not a decision — and a
malformed field matches nothing, since it is a shape the app never
writes.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
2148ebb379 |
Register inverse operations at the Writer boundary
The store is the Writer boundary, so it computes and registers inverses: a weak history sink bound at session composition, one HistoryStep per gesture at exactly the brackets that were already one performWrite each — multi-card moves, style batches, width pairs, and multi-row restores each undo as one plurally-titled step, and the Edit session registers once at the flip from the bytes disk held before its first landed write, debounce ticks registering nothing. Crossings run through performWrite, so an undo brackets the watcher, echoes through the reload, and reaches every window; every closure captures values, never snapshots. The inventory follows 13 exactly: moves return to origin lane and order, renames restore or remove the title key, restyles and resizes restore field values or absence, tombstones and restores swap with captured timestamps, and an undone create is a real removal — no trace — with redo re-materializing the same UUID from bytes captured at gesture time. Purge, attachments, repair, bookkeeping, checkbox flips, raw Apply, and the whole arrival family register nothing, each exclusion documented where it lives. Step names speak 06's verb vocabulary through the new HistoryPhrase. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
40322247e0 |
Build the style, details, and actions sidebar sections
The sidebar completes: the shared style editor gains a second anchor — StyleEditorLayout carries the geometry (the popover keeps its settled 268/14/7/8 untouched as the default; the sidebar packs columns to its width with no inner scroller) while every well, the batch display, the arrow grammar, and the one applyStyle bracket stay the shared component's. The card anchor is fixed, not tracking: the target is this card, and the fate walk retires the window when the card goes. Details renders every unknown frontmatter key read-only in file order — Card.document already carried them — showing the author's own bytes where the raw span is a value and the engine's rendering for block scalars and empties; reserved enhanced-schema keys are ordinary unknowns, and no keys means no section. Actions: Delete rides the same tombstone bytes as Backspace and drop-on-trash through a one-line seam, says nothing about selection, and lets the fate walk dismiss; Reveal in Finder resolves through the attachment scope so the two paths cannot disagree. History reserves its m7 slot without drawing a header no base board can honor. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
46397c740e |
Build the attachments sidebar section
The card's complete file inventory: compact QuickLook-thumbnail rows over Card.attachments — no reference tracking, subfolders tolerated and unsurfaced — with a quiet header add affordance and the drop hint empty state. The whole window is the file-drop surface, Edit mode included (the editor's drag types were already filtered; now tested), sharing the board's folder-refusal semantics literally: FinderDrop moved verbatim into its own file so both windows run the same partition and loss row. Dragged text still lands at the caret and is inert elsewhere — the window delegate accepts file payloads only. Rows open on double-click or Return, drag out their file URL, and Remove is a bracketed write through FileManager.trashItem — the system Trash, never a hard delete, returning the in-Trash URL so the promise is testable; the attachment listing is the guard, so traversal and subfolder names refuse in one line. Keyboard-native per 05: the section is one Tab stop, arrows walk rows by name, Space toggles the shared QuickLook panel, Backspace removes. File > Add Attachment (shift-cmd-A) comes alive through the same import path. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
40c0a75c24 |
Build the Raw Source outlet
The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole content area for the literal on-disk index.md in a plain monospaced editor with Cancel/Apply. Raw source is window-level state, not a third body mode — entry rides setMode(.preview), which flushes the Edit session by construction, then reads the file fresh; exit reveals Preview, and an empty body after Apply does not reopen Edit (openIfNeeded already ran). Apply validates the proposed bytes through the loader's own card checks — parseDocument's strict UTF-8/BOM rejection, schema, order — deliberately skipping the uneditable-shape refusal, since a flow-mapping card is exactly what the hatch repairs; invalid bytes alert in place with the loader's own error and no bracket opens. The write is byte-for-byte with no modified stamp and no modified-by clear, per 01's explicit carve-out — the verbatim contract outranks stamping — and identical bytes write nothing. Escape cancels, cmd-Return applies, toggle-off applies too, and cmd-E disables while raw is active via a testable predicate. Tombstoned targets refuse as vanished: a foreign delete is never reverted by a stale buffer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
e989c1f26e |
Build Edit mode with debounced, byte-honest saves
The editing surface: the same hosted TextKit-1 text view gains an editable branch with a per-keystroke line-scanner highlighter — chosen over a parser re-parse because a mid-typing buffer is usually invalid Markdown and 05 wants the delimiters themselves dimmed; apply only sets attributes, so presentation-never-transforms is structural. Saves ride a ~700ms injectable debounce through BoardWriter.writeBody — toggleTaskMarker's idiom widened to the body span, frontmatter bytes untouched, refusing to write when disk already holds that body, which enforces all three gates (untouched, reverted, echo) at the layer that owns the bytes with one isDirty predicate above it. Mode grammar lands whole: ⌘E toggles with a checkmark, Return in Preview enters, Escape returns, and every flip flushes first; window close flushes through the existing retry/save-copy/discard modal, and the dismissal flush deliberately reaches a tombstoned card. Dirty-buffer-wins: disk always follows the snapshot, the buffer only when clean, both surfaces render the buffer. Undo is the editor's own session-scoped NSUndoManager; endEditSession names the pro-m1 one-commit-per-session boundary. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
6dc84176fb |
Build Preview mode rendering
The card body's resting state: swift-markdown (pinned 0.8.0, smart typography off — Preview renders the bytes on disk) parsed into a pure BodyMarkup model with UTF-8 source offsets, rendered on one hosted TextKit 1 NSTextView — chosen because find-in-text is NSTextFinder, checkbox clicks reuse AppKit character hit-testing, links are .link attributes, and NSTextTable's automatic layout is exactly the columns-sized-to-contents rule. The GFM subset renders per 05; HTML stays verbatim code-styled text; relative images resolve against the card folder while remote URLs are never fetched, drawing a quiet chip instead. Task checkboxes are live: a click flips exactly one byte through a fresh-read, refuse-uneditable, stamp, atomic-replace write — the app's only offset-addressed write, so a moved target refuses as staleTarget and what the user saw decides the direction, netting one toggle on a double-click. Empty bodies open in Edit per CardBodyMode's opening rule, applied once; the Edit surface itself stays an honest read-only stub until its card. FindCommand prefers the card body's find over board search when a card window is focused. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
af1860debf |
Relocate loose card files into attachments
01's Lanework-owns-the-board carve-out: a regular file beside a card's index.md belongs in attachments/, and the app moves it there. The loader detects read-only — a new LoadResult.looseCardFiles channel, separate from the stray-tolerance warnings because it says the opposite thing — skipping directories, symlinks, hidden entries, and the reserved names compared case-insensitively (on APFS, Index.md IS the index). The relocation rides one performWrite bracket at the tail of every successful reload, which makes lock deferral free: the reload that lifts a read-only lock is the reload that relocates. A lane/card/filename memo keeps a failing relocation from hot-looping — one one-shot, then silence until disk changes. The notice rides the loss-row class, phrasing folded by BannerCenter (one file, one card's files, a multi-card sweep), naming original filenames per the importAttachment rule. Paste normalizes at the import boundary: staged snapshots' loose files land in the pasted card's attachments silently, every arrival path declaring its side via an explicit normalizingLooseFiles parameter — drag paths decline and fall back to the destination's own carve-out. checkIsCardFolder closes the hole where a lane's notes.txt would have been relocated: card depth is exact, UUID under UUID. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
5c0c0e5619 |
Realign search, clipboard, and lane-hover code with the second batch
Creation now clears the search by mechanism, not gesture: one seam (noteUserCreation) states 04's rule once, called from the placeholder funnel, paste — cards and lanes, after the staleness guard so a stale paste clears nothing — and Finder file-drop creation; the attach path deliberately doesn't clear, and cross-board arrivals and New Lane stay outside the seam (a transfer isn't creation; a lane can't be born invisible). An open inline rename now survives the filter hiding its card: the model already kept the editor, but the field renders in the card's slot, so renderedCards keeps the renaming card's slot exactly as long as the editor is open — the query stands throughout, and commit or Escape lets the predicate apply in the same pass. Verified conformant and newly pinned: query-emptied lanes keep their slot with a 0 badge, pasteboard staleness (takeover before paste and mid-staging both no-op), out-transition reachability and the strip pre-divide hold by construction with comments citing their rulings. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
524488122f |
Land Finder file drops positionally, header release topmost
04's settled clauses were mostly shipped already — the create landing resolved through DropSlotMath.cardSlot with one nominal shadow per importable file — but a release on the lane header fell through to the card zones, which clamp inward, so a scrolled lane could propose behind the header stripe. FileDropZones now folds header, attach hit-test, and card-slot resolution into one pure seam asked in that order, the header answering topmost per the ruling; lane headers register their frames for it. FinderDrop.shadowCount names the floor-at-one rule. New tests pin the header boundary, a differential against cardSlot's own zones (same zones, not similar), and a store-level differential proving a file landing takes the very ranks a card move there takes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
33bf425f25 |
Drop a card on the shown trash to delete it
04's ruling makes the drag the pointer's delete gesture: the shown trash column accepts live same-board card drags, the shadow pinned topmost — honest, since the trash sorts by deleted newest-first — and release tombstones through the same write path as Backspace, extracted so the two gestures cannot drift. DropTarget grew a container case for the quasi-lane (it has no lane id by construction); lane drags, cross-board arrivals, option-copies (re-checked at release, the one input that can flip without a callback), trashed-side payloads, hidden trash, and the read-only lock all refuse — and a refusal falls through to the strip retarget, never cancelling the drag. The settle draws the tombstoned rows in the trash under the cards' own GUIDs, so the echo is an invisible content swap and nothing winks out for a round trip. Selection needs no surgery: the reload's resolve rule ejects tombstoned members as the vanish it is, pinned by a test contrasting both gestures. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
1487b391ad |
Refuse folder drops at hover, skip them in mixes
04's settled ruling: the attachment model is flat top-level files, so a drag containing only folders never engages — no highlight, no proposal, the standard incompatible-payload cursor — and a mixed drag proposes for its files only, importing them at the drop while a loss row names the skipped folders. Hover reads the providers' registered types (anything conforming to public.directory refuses, packages included); commit re-partitions authoritatively from the filesystem, so a synthetic payload that hides its type still can't land a folder. The create path now only ever fires with at least one importable file — the mint-fail-remove dance is gone from the folder case and stays reserved for genuine mid-batch failures. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
7be4eec9fd |
Unify the trashed universe on the trash rows
02 rules the trashed side has exactly one definition: the set with trash rows — a card carrying its own deleted: under a tombstoned lane is in neither universe, so an anchor or selection can never survive on an item that renders nowhere. The membership rule now lives once, in Liveness.walk, and ItemReferenceSet.idUniverse, TrashModel.entries, paths, and emptyTrashTargets all derive from it — the old lane-OR-card logic that admitted subsumed cards to the trashed side is gone, and the two universes deliberately no longer partition the board. Put Back, Delete Immediately, and Empty Trash outcomes are unchanged: a tombstoned lane still moves and purges whole, its nested tombstones with it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
756e936291 |
Add the loss-row banner class and retone degraded paste
02 ratified a warning-tone class for non-failure losses — content that didn't arrive though nothing failed: a degraded paste, folders skipped from a Finder drop, their future kin. Loss rows take the one-shot's dismissable-untimed lifecycle (a loss the user didn't notice is the harm) and rank below the true failures, above commit and attachment notices. BannerCenter grows LossBanner, postLoss, and the skipped-folders phrasing; the degraded-paste notice moves off its signpost onto the new class, ending its too-quiet ranking. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
cf87b72092 |
Implement live search filtering
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 |
||
|
|
7eee0934ee |
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 |
||
|
|
8f116934b4 |
Implement Finder file drops
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 |
||
|
|
90cf82d740 |
Implement drag & drop with the locality model
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 |
||
|
|
f2d9f3ad07 |
Give the registry live display-state write-through and record-before-load
BoardRecord carries icon/iconColor; recordOpen/recordClose stamp them with the display name, and a displayStateDelegate on the store (wired in BoardWindowHost beside onFrameChanged) syncs all three through BoardRegistry.syncDisplayState on every successful reload — welcome rows now wear the board's own icon and follow in-app renames live. recordOpen now runs before the load with the folder name as a brand-new record's provisional display name, so a first open that fails fail-fast still lands in recents carrying the failure row-level (02's rule); an existing record's cached name survives a failing retry, and the welcome fallback list remains only for failures naming no record at all. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
edc094a5f4 |
Realign the width and lane-move commands — three fresh settlements
DESIGN edits landing from the parallel session, honored: - Caret chords yield to any focused text control (04 ▸ Grammar): Move Left/Right ⌘←/⌘→ and the width pair ⌥⌘←/⌥⌘→ now disable while the board popover is open — its fields are the one non-inline text surface a board window has today; the search field and card-window fields extend the rule with their own cards. - The width pair batches over a multi-lane selection (03 ▸ Lane, the styling precedent): each selected lane steps one unit through one bracket (stepLaneWidths); floor members hold on a decrease; the context-menu stepper stays single-lane by nature. - A width write landing on 1 removes the width key (03 ▸ Lane, the remove-at-default family), every mechanism alike — writeLaneWidths is the one commit point; a hand-written width: 1 is preserved by the unchanged guard until the app itself next edits width. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
21a5a6dbfd |
Build the drop-slot model and the drop commits — drag & drop, first half
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 |
||
|
|
4035ba7986 |
Implement the keyboard grammar and full command map
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 |
||
|
|
2e229735b1 |
Implement the selection model
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 |
||
|
|
5ebf9fb90c |
Implement the motion language
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 |
||
|
|
2090d742b9 |
Build the trash quasi-lane
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 |
||
|
|
aa6aaf2a11 |
Build the board popover — title widget, rename, styling, git slot
The window-title widget arrives as a leading titlebar accessory — a quiet chevron on board windows only, installed and removed by the window controller's own attach lifecycle — anchoring the one board-level surface as a transient popover (the board window deliberately grows no toolbar item for it). Inside: board rename editing frontmatter title only (the folder is never renamed; an empty commit removes the key and the window title falls back to the folder name), the embedded shared style editor permanently targeting the board, and the labeled Git section that this milestone only reserves — a mode-none explanation and a disabled stub where m7's add-git, branch, remote, and authentication controls land. Cmd-I (File > Board Info) toggles it per window through a focused scene value, kept apart from board-scoped transient state since a titlebar popover belongs to one window, not to the board. Escape reverts a dirty rename field and falls through to dismiss otherwise; a foreign rename resyncs the field only while unfocused. 12 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
c6298c2e41 |
Build the styling system and shared style editor
One style-editor component, anchor-agnostic: a background grid (None well plus the 12 palette colors) and a curated symbol grid (the pathfinder's five-dozen set, leading well removing the icon key for the level default), selection-aware across cards, lanes, and the board itself. Batch edits compute per-dimension state — uniform, mixed (no well selected), or an off-palette value labeled verbatim outside the grids — and choosing a well applies to the whole target set as one write bracket, skipping no-ops per field. The popover tracks its target set live per the freshly ratified rule: targets re-resolve by UUID on every reload, a vanished target leaves the set, an emptied set dismisses the editor, and nothing ever silently retargets to the board. Anchors landing now: Board > Style (Opt-Cmd-S) and the card/lane context menus, which also carry the quick-style recents row (app-wide, persisted, capped at six, None never recorded) and the lane's width control twinning the menu chords. The styling system's other two renders arrive with it: a lane's background paints the C7 top-edge band, the board's paints the window content background — malformed values paint nothing and stay byte-identical on disk. 31 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b35566e0fe |
Build lane chrome — title bar, badge, inline rename
The lane title bar becomes real: leading SF Symbol (hand-written names render leniently, unknown ones fall back to the level default), title or secondary untitled placeholder, a quiet count badge that counts exactly the cards the body renders (so the m5 search filter is followed by construction), and a new-card button. The whole bar is the reorder drag surface — no grip — with click-vs-movement splitting select from drag; a pure proposal function maps the drag to an insertion index and release commits through the Writer's same-parent degenerate reorder, compacting and retrying when midpoint precision runs out. Clicking never edits: inline rename is Return on the sole selected card or Board > Rename for either kind, a third transient editor beside the placeholder that tracks its target by UUID, commits on focus loss, discards silently when the target vanishes, and removes the title key on an empty commit. The new-card placeholder renders at last — the settled Cmd-N target rule (pure, tested) files it after the anchor card, at a selected lane's bottom, or into the last-active lane; Return commits and re-selects the lane, Cmd-Return also opens the card window, and a failed create discards the overlay. New Card / New Lane / Rename land in the menus with focused-editor and read-only validation; rename gets its own WriteOperation case in the banner vocabulary. 59 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
ff3ba298f0 |
Build the full-visibility lane layout
The lane strip replaces the placeholder board: window width divides
across the lanes' width units (no horizontal scroll, no minimum width,
degenerate compression accepted), a lane of n units flowing its cards
into n round-robin masonry columns via a measurement-cached Layout.
Width has two deliberately opposite controls, both landing here: the
right-edge drag (ported verbatim from the pathfinder's ColumnResize)
freezes the 1x standard at drag start, snaps between integer widths
with the asymmetric shadow-leads tick and 10pt re-entry, grows the
window one standard width per snap so siblings keep their exact
pixels, and rubber-bands at the screen's visible frame — uncapped
otherwise; the Increase/Decrease Lane Width items (new Board menu,
Cmd-Opt-arrows) are the stepper's keyboard face and re-divide the
existing window width instead, never touching the window. Width
changes write through the new .resize WriteOperation ("Couldn't
resize…" in the banner vocabulary, which grows with the surfaces by
design); malformed width values render as one unit and stay untouched
on disk. 27 new tests port the pathfinder's resize-math suite onto
the uncapped range and pin the write path's fidelity.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
23e761120f |
Give transient UI state an explicit home in the store
TransientBoardState, one per store, holds state by how a reload treats it: item-referencing sets (selection, drag membership, pending cut) share one shape and one constraint rule — members must exist in the current universe — applied in two directions by one primitive, so the search filter's hidden-cards rule and reload survival are one rule expressed once; derived state is stored as its inputs only (the query, never its result set); and the new-card placeholder is a lane-anchored overlay with no UUID until commit, discarded when its lane vanishes or tombstones, handed off when the created card's UUID appears. Trash visibility rides along per-open, never persisted. The decision is written back into DESIGN/02 § Changes from Kanban — the TBD is closed. Full suite 352 tests in 64 suites green. Two findings filed. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A |