Commit Graph
16 Commits
Author SHA1 Message Date
rzen 797d020d01 Materialize the trash — faces, menus, and grammar
Phase 3 finishes the pivot at the surface. One card face serves two
containers: CardFaceView extracted with a role — board or trash — so
stripe, tint, chip, selection stroke, cut dim, marquee registration,
and drag are shared by construction, the trash side differing only in
its absences: no Open, no rename, no Style, no file-hover highlight,
and a Delete that goes through the confirmation host. The column
rewrote around the lanes' own single-column masonry so drag reflow
reads as positional slides; chrome stays the hatched header, symbol,
and count — 11 gives Empty Trash to the File menu alone. Two real
grammar bugs die here: plain Backspace on a trash selection purged
without the confirmation the menu raises, and the context menu's
Delete resolved against the standing selection, so right-clicking a
trash card under a board selection silently did nothing — it now
stages the clicked set explicitly. Open, Rename, Style, and Empty
Trash validation became testable store seams; the column is one named
accessibility container of ordinary card elements. The tombstone era
is swept: deleteItem, restoreItem, stripTombstonedChildren — dead
since lane copies stopped nesting trash — the restore verb, the
unreachable put-back banner row, and every quasi-lane doc comment.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 18:18:39 -04:00
rzen 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
2026-07-28 17:47:56 -04:00
rzen 4cf5f09d93 Materialize the trash — storage layer
Phase 1 of the trash pivot: the file format learns .trash/. The loader
parses the reserved root container — cards only, one shared parseCard
for both containers so fail-fast, attachments, and verbatim documents
are literally the same code; absent means empty; symlinks and
lane-shaped nestings fall out as strays by construction. BoardModel
grows snapshot.trash as a plain rank-ordered card list — the container
has no identity to carry. Legacy deleted: keys keep flowing through
the retiring flag path so every tombstone consumer stays green, and
are additionally reported through LoadResult.legacyTombstones in the
loose-file idiom for phase 2's migration scheduling — nothing vanishes
from view before its folder has actually moved, which is also 01's
lock-deferral posture. Writer primitives land value-passing: move to
trash with caller-minted rank and the deliberate modified stamp,
tombstone migrations that surgically remove the key, physical lane
removal, per-card and whole-container purge that leaves strays
verbatim, and byte-faithful whole-subtree capture/recreate for lane
undo. Board-wide identity now spans the trash, so an import colliding
with a trashed UUID remints instead of colliding. The watcher already
delivered .trash events — isGitInternal tests a component, not a dot —
now stated and pinned rather than relied on.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 15:55:40 -04:00
rzen 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
2026-07-28 14:54:42 -04:00
rzen 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
2026-07-28 11:52:37 -04:00
rzen 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
2026-07-28 11:25:25 -04:00
rzen 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
2026-07-28 10:46:02 -04:00
rzen 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
2026-07-28 09:59:18 -04:00
rzen 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
2026-07-28 09:09:31 -04:00
rzen 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
2026-07-28 07:17:27 -04:00
rzen 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
2026-07-27 22:18:58 -04:00
rzen 4b97ecf3f0 Build the welcome screen
The welcome window becomes the real thing: Xcode-style, hidden title
bar with background drag, branding and actions left, recents right —
rows carrying the board symbol, name, location, and the registry's
cached lane/card counts (stamped at close, never a scan at welcome
time), sorted by last opened. Launch failures surface row-level per
02: a failure joins its recents row as a warning caption, an
unresolvable bookmark renders unavailable with Forget its one
affordance, and only a failure with no row to carry it falls back to
a compact list; a board opening again heals its row. New Board
(Opt-Cmd-N) opens the Pages-style template chooser — shipped with
the single Basic template and the m9 seams marked — flowing through
the save panel into createBoard/createLane and straight into a board
window. Open Recent gains its submenu with Clear Menu (byte-identical
to forgetting every row, pinned by test), and File > Duplicate forks
the frontmost board to a Finder-style copy sibling: pending work
flushes first through the close flush's step two alone (sessions stay
open — 09's stated exception), every GUID and tombstone carries (the
whole-board carve-out from copies-remint), and the copy opens in its
own window while the original stays put. 36 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 16:50:39 -04:00
rzen 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
2026-07-27 15:38:34 -04:00
rzen 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
2026-07-27 08:52:24 -04:00
rzen 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
2026-07-27 08:16:14 -04:00
rzen 747dea552d Surface write failures — banners, locks, one modal
The banner surface as one vocabulary (Kanban/LiveStore/BannerCenter,
Kanban/UI/BannerStripView): a pure precedence rule — in-progress pinned
above the collapse (ratified mid-build), lock > breakage > one-shot
write failures > commit+attachment, signposts last — with all
user-facing phrasing owned here via exhaustive switches over the closed
WriteOperation enum; free-form English survives only in diagnostics.
performWrite posts its failures before rethrowing, so no one-shot can
bypass the strip; refusals under lock post nothing.

The lock vocabulary completes: vanishedRoot and unwritableLocation join
bracketedReloadFailed, each with its own clearing rule (unwritable
clears only on a reconciling reload's writability re-probe). The
registry now owns root recovery: bookmark re-resolution absorbs renames
transparently, a dead root locks read-only and re-arms FSEvents on the
gone path so the root's return round-trips back through rootChanged,
re-minting and re-keying on the way. DirtyBufferGuard is the one modal
moment, retry / save a copy / discard, no fourth button.

36 new tests; full suite 333 tests in 62 suites green. Five findings
filed on the Redesign board.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 20:41:48 -04:00