Iteration 4b: save / save as / new / load with project switching

Adds the rest of the track-2 lifecycle commands (ADR-0015 §11)
and the project-switching machinery they need at runtime.

Temp vs named distinction: replaced the fragile naming heuristic
with an explicit `[temp]` marker in the directory pattern
(`<YYYYMMDD>-[temp]-<word>-<word>-<word>`). validate_user_name
already rejects brackets, so user-typed names can never collide
with a temp marker. The status bar shows `[TEMP] <Display Name>`
for temp projects; the prettifier strips both the date and the
marker so display names are clean.

save / save as: temp project's `save` opens a path-entry modal
(acts as save as); named project's `save` reports "already
auto-saved; use `save as`". `save as` always prompts. Relative
names resolve under <data-root>/projects/; absolute paths used
as-is. Copy excludes the per-process lock file; everything else
(.db, yaml, csvs, history.log) is copied.

new: closes current project, creates a fresh auto-named temp,
switches.

load: opens a picker. List sub-mode shows projects in the active
data root, sorted newest-first by project.yaml mtime; arrow keys
navigate, Enter loads, `b` switches to a path-entry sub-mode for
projects elsewhere, Esc cancels. Empty data root jumps straight
to path entry.

Runtime: `Session` holds Option<Project> + Option<Database> so
project switches can drop old (releasing lock + stopping worker)
before opening new -- required for the "load my own current
project" case. `perform_switch` handles Load / SaveAs / NewTemp
uniformly.

Tests: 332 passing (270 lib + 9 + 5 + 6 + 16 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-08 06:23:46 +00:00
parent ba93d3c7d8
commit f2198275f0
9 changed files with 1376 additions and 44 deletions
+18
View File
@@ -72,4 +72,22 @@ pub enum AppEvent {
RebuildFailed {
error: String,
},
/// Runtime has gathered the list of available projects
/// for the load picker. App opens the picker modal.
LoadPickerReady {
entries: Vec<crate::app::LoadPickerEntry>,
},
/// A project switch (load / new / save-as) succeeded.
/// Carries the new display name + temp flag so App can
/// update the status bar.
ProjectSwitched {
display_name: String,
is_temp: bool,
},
/// A project switch failed in a non-fatal way (target
/// already exists, path unreadable, …). Surfaced as an
/// error in the output panel.
ProjectSwitchFailed {
error: String,
},
}