Build the template chooser and Save as Template

The chooser completes its three tiers: bundled by template order, then
keyed user templates, then keyless boards by display name — and a
malformed user template still lists, by folder name with the loader's
own sentence on the row, never failing its neighbours. The store is
re-scanned on every presentation and on app activation, the Reveal
round trip made honest without watching a folder 09 deliberately
leaves unwatched; Reveal lives in the chooser's header and mints the
store on first press. Save as Template repeats Duplicate's sequence —
progress row with Cancel, flush, detached cancellable copy — through
the engine: mint the store, read the next user order before the copy
can count itself, Finder-ladder the name, copy excluding .git and
.trash/, then stamp the whole template: mapping on the landed copy
through updateIndex, with no bracket because the copy lives outside
every watched board. Folder attributes deliberately don't carry — the
one lock the command stays live under is the read-only-DMG one, and
carrying its mode bits would mint a read-only template in the user's
own store; the command gates instead on the real hazard, unsaved card
content. A signpost names the template only when the ladder renamed
it. One name ladder now serves Duplicate and the store.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 19:39:18 -04:00
parent e7b48d2d53
commit 3aa80db2a4
17 changed files with 1358 additions and 90 deletions
+16
View File
@@ -59,6 +59,19 @@ final class CardWindowSession: CardSessionFlushing {
/// Write-failure surfacing: "the one modal moment on the write-failure path").
let bufferGuard: DirtyBufferGuard
/// The window's raw-source outlet, wired in by the host once the window exists.
///
/// A closure rather than a stored reference, `CardBodyEditSession.save`'s precedent: the outlet
/// is window state living beside this object (`CardWindowHost.rawSource`) rather than inside it,
/// and a session that reached into the view's state would be the wrong direction. `nil` a
/// window that has not joined its board holds nothing, which is true.
var rawSourceHoldsUnsavedText: (@MainActor () -> Bool)?
/// The Edit buffer's dirty text or a typed-in raw-source outlet see `CardSessionFlushing`.
var holdsUnsavedContent: Bool {
body.isDirty || rawSourceHoldsUnsavedText?() == true
}
private var hasEnded = false
/// Both `let`s, wired to each other through a local the guard's two closures need the buffer,
@@ -430,6 +443,9 @@ struct CardWindowHost: View {
store: store,
cardID: cardID
)
// The other half of the outlet's wiring: the session answers for this window's unsaved
// content, and the outlet is the half that does not live inside it (`CardWindowSession`).
session.rawSourceHoldsUnsavedText = { [rawSource] in rawSource.holdsUnsavedText }
Self.configureAttachments(attachments, store: store, cardID: cardID)
}