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
+13
View File
@@ -30,10 +30,23 @@ public enum BoardCloseCause: Sendable, Equatable {
@MainActor
public protocol CardSessionFlushing: AnyObject {
func endSession() async
/// Whether this window is holding content the file does not have a dirty Edit buffer, or an
/// open raw-source outlet that has been typed in.
///
/// Beside `endSession()` because it is the same fact from the other end: this is what the flush
/// *would* write, asked before running it. Its one caller is File Save as Template's
/// validation, which under the unwritable-location read-only lock stays live only "unless an
/// open Edit or raw-source session holds unsaved content the suspended saves can't flush"
/// (02-architecture.md Live-reload resilience; 09-templates.md Save as Template) a
/// template that silently missed those keystrokes would break 09's never-misses-keystrokes
/// guarantee, which outranks the item's availability.
var holdsUnsavedContent: Bool { get }
}
public extension CardSessionFlushing {
func endSession() async {}
var holdsUnsavedContent: Bool { false }
}
// MARK: - CloseFlushCoordinator