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
This commit is contained in:
2026-07-28 10:46:02 -04:00
parent 6dc84176fb
commit e989c1f26e
16 changed files with 2195 additions and 78 deletions
+7 -2
View File
@@ -680,8 +680,13 @@ public final class AppModel {
storeFlush: { [weak self] in
await self?.sessions[ref]?.store.awaitQuiescence()
},
// editorFlush / committerFlush stay nil until m6 and m7 have something to flush; the
// slots exist so their order is already decided when they do.
// `editorFlush` stays nil, and now deliberately rather than for want of an editor: the
// card windows' debounced body saves flush in **step 1**, inside each window's
// `endSession()` (`CardWindowSession`), which is both earlier than this slot and where
// 02-architecture.md puts them ("each open Edit session ends with its normal session
// commit", then pending work). The slot stays for a board-level editor with no card
// window of its own the raw-source buffer is the candidate so that the order
// relative to `committerFlush` (m7) is already decided when one arrives.
recordClose: { [weak self] in
guard let self, let session = sessions[ref] else { return }
let counts = Self.liveCounts(of: session.store.snapshot)