feat: persist & restore per-project input mode (#14)

The input mode always started in simple; a learner who quit in advanced
had to re-toggle every launch. Store the mode per-project in project.yaml
(project.mode:, optional, default simple) and restore it on every open.

Mode is live UI state, not schema: the worker stamps the current mode
into project.yaml on every write, so a later command rewrites the live
value rather than clobbering it — no db round-trip needed. The mode is
persisted on unload (quit + project switch) so the mode you leave a
project in is always what reopens; the `mode` command also persists
immediately. A switch saves the outgoing mode, then restores the
incoming project's stored mode.

New --mode simple|advanced CLI flag (precedence --mode > stored >
simple; combines with --resume). A teacher can ship a project that
opens in advanced mode and export it to students (the mode travels in
the zip).

ADR-0015 Amendment 1; ADR-0003 note; help banner; requirements L1b.
This commit is contained in:
claude@clouddev1
2026-06-02 06:47:34 +00:00
parent ae57c6fc82
commit 4cd574b909
16 changed files with 769 additions and 14 deletions
+55 -1
View File
@@ -736,6 +736,7 @@ impl App {
display_name,
is_temp,
history_entries,
mode,
} => {
self.note_system(crate::t!(
"project.switched_ok",
@@ -746,6 +747,9 @@ impl App {
self.tables.clear();
self.current_table = None;
self.seed_history(history_entries);
// Restore the switched-to project's stored input
// mode (ADR-0015 mode-restore amendment, issue #14).
self.mode = mode;
Vec::new()
}
AppEvent::ProjectSwitchFailed { error } => {
@@ -1311,7 +1315,9 @@ impl App {
ModeValue::Advanced => "advanced",
};
self.handle_mode_command(&format!("mode {arg}"));
Vec::new()
// Persist the new mode so it is restored on the next
// open (ADR-0015 mode-restore amendment, issue #14).
vec![Action::PersistMode(self.mode)]
}
AppCommand::Messages { value } => {
let raw = match value {
@@ -2906,6 +2912,54 @@ mod tests {
assert!(!app.output.is_empty());
}
// ---- ADR-0015 mode-restore amendment (issue #14) ----
#[test]
fn mode_command_changes_mode_and_emits_persist_action() {
// The `mode` command flips the live mode AND emits
// `PersistMode` so the runtime records it to project.yaml.
let mut app = App::new();
app.mode = Mode::Advanced;
type_str(&mut app, "mode simple");
let actions = submit(&mut app);
assert_eq!(app.mode, Mode::Simple, "live mode flipped");
assert_eq!(
actions,
vec![Action::PersistMode(Mode::Simple)],
"the mode change is persisted",
);
}
#[test]
fn mode_command_via_one_shot_escape_persists_advanced() {
// Reaching `mode advanced` from simple via the `:` one-shot
// (ADR-0003) still emits the persist action for advanced.
let mut app = App::new();
assert_eq!(app.mode, Mode::Simple);
type_str(&mut app, ":mode advanced");
let actions = submit(&mut app);
assert_eq!(app.mode, Mode::Advanced);
assert!(
actions.contains(&Action::PersistMode(Mode::Advanced)),
"expected PersistMode(Advanced), got {actions:?}",
);
}
#[test]
fn project_switched_event_restores_the_stored_mode() {
// A switch carries the target project's stored mode; the
// App adopts it ("loading triggers the mode switch").
let mut app = App::new();
assert_eq!(app.mode, Mode::Simple);
app.update(AppEvent::ProjectSwitched {
display_name: "Other".to_string(),
is_temp: false,
history_entries: Vec::new(),
mode: Mode::Advanced,
});
assert_eq!(app.mode, Mode::Advanced);
}
#[test]
fn bare_create_table_emits_friendly_parse_error() {
let mut app = App::new();