diff --git a/crates/ui-egui/src/discard_ui/tests/jpeg_tests.rs b/crates/ui-egui/src/discard_ui/tests/jpeg_tests.rs
--- a/crates/ui-egui/src/discard_ui/tests/jpeg_tests.rs
+++ b/crates/ui-egui/src/discard_ui/tests/jpeg_tests.rs
@@ -220,10 +220,11 @@ fn recorded_jpeg_saves_keep_their_options_and_replay_without_asking() {
         steps,
         &[
             ("file.saveAs".into(), json!({"path": "p.jpg", "quality": 10, "jpegFormat": "optimized"})),
-            ("file.saveACopy".into(), json!({"path": "copy.jpg", "quality": 6, "jpegFormat": "baseline"})),
+            // The Save a Copy prompt starts at the format just chosen (it is remembered).
+            ("file.saveACopy".into(), json!({"path": "copy.jpg", "quality": 6, "jpegFormat": "optimized"})),
         ]
     );
-    let expected = [("p.jpg".to_string(), asked(pct(10), JpegFormat::Optimized)), ("copy.jpg".to_string(), asked(pct(6), JpegFormat::Baseline))];
+    let expected = [("p.jpg".to_string(), asked(pct(10), JpegFormat::Optimized)), ("copy.jpg".to_string(), asked(pct(6), JpegFormat::Optimized))];
     assert_eq!(writes.borrow().as_slice(), &expected);
     writes.borrow_mut().clear();
     let played = h.state_mut().run("actions.play", json!({"action": 0})).unwrap();
@@ -270,3 +271,77 @@ fn a_save_can_choose_where_the_prompt_starts() {
     assert_eq!(writes.borrow().as_slice(), &[("s.jpg".to_string(), asked(Some(70), JpegFormat::Progressive))]);
     assert_eq!(h.state().session.actions.list[0].steps, [("file.saveAs".into(), json!({"path": "s.jpg", "jpegFormat": "progressive"}))]);
 }
+
+/// The JPEG Options dialog starts at what it was last answered with (#9631): the quality and the
+/// format, for a document that isn't an opened JPEG. Cancel remembers nothing.
+#[test]
+fn the_next_dialog_starts_at_the_last_answer() {
+    let (mut h, writes) = jpeg_app("unused.jpg");
+    let ctx = h.ctx.clone();
+    h.state_mut().save_as(Some("a.jpg".into())).unwrap();
+    h.run_steps(2);
+    h.state_mut().jpeg_options.as_mut().unwrap().quality = 6;
+    h.run_steps(2);
+    h.get_by_label("Progressive").click();
+    h.run_steps(2);
+    h.get_by_label("Cancel").click();
+    h.run_steps(3);
+    assert!(writes.borrow().is_empty());
+    h.state_mut().save_as(Some("b.jpg".into())).unwrap();
+    h.run_steps(2);
+    assert_eq!(h.state().jpeg_options.as_ref().map(|p| (p.quality, p.format)), Some((10, JpegFormat::Baseline)), "a cancelled dialog is not an answer");
+    h.state_mut().jpeg_options.as_mut().unwrap().quality = 6;
+    h.run_steps(2);
+    h.get_by_label("Progressive").click();
+    h.run_steps(2);
+    h.get_by_label("OK").click();
+    h.run_steps(3);
+    crate::menus::invoke(h.state_mut(), &ctx, "file.saveACopy", json!({})).unwrap();
+    h.run_steps(3);
+    assert_eq!(h.state().jpeg_options.as_ref().map(|p| (p.quality, p.format, p.own_quality)), Some((6, JpegFormat::Progressive, None)));
+    h.key_press(Key::Enter);
+    h.run_steps(3);
+    let expected = [("b.jpg".to_string(), asked(pct(6), JpegFormat::Progressive)), ("unused.jpg".to_string(), asked(pct(6), JpegFormat::Progressive))];
+    assert_eq!(writes.borrow().as_slice(), &expected);
+}
+
+/// An opened JPEG still starts at its own quality (File › Save keeps it, #4032) but at the last
+/// format; left on its own quality, the answer doesn't replace the quality remembered for other
+/// documents.
+#[test]
+fn an_opened_jpeg_starts_at_its_own_quality_and_the_last_format() {
+    let (mut h, _writes) = jpeg_app("unused.jpg");
+    h.state_mut().session.prefs.edit(|p| {
+        p.dialogs.insert("file.jpegOptions".into(), json!({"quality": 3, "jpegFormat": "progressive"}));
+    });
+    std::sync::Arc::make_mut(&mut h.state_mut().session.active_mut().unwrap().doc).file_encoding.jpeg_quality = Some(95);
+    h.state_mut().save_as(Some("p.jpg".into())).unwrap();
+    h.run_steps(2);
+    assert_eq!(h.state().jpeg_options.as_ref().map(|p| (p.quality, p.format, p.own_quality)), Some((11, JpegFormat::Progressive, Some(95))));
+    h.key_press(Key::Enter);
+    h.run_steps(3);
+    let saved = h.state().session.prefs().dialogs.get("file.jpegOptions").cloned();
+    assert_eq!(saved, Some(json!({"quality": 3, "jpegFormat": "progressive"})), "the own quality is not an answer");
+}
+
+/// A remembered answer that no longer fits (a hand-edited preferences file) is ignored, value by
+/// value.
+#[test]
+fn a_bad_remembered_answer_is_ignored() {
+    for (saved, expected) in [
+        (json!({"quality": 13, "jpegFormat": "progressive"}), (10, JpegFormat::Progressive)),
+        (json!({"quality": "8", "jpegFormat": "fast"}), (10, JpegFormat::Baseline)),
+        (json!({"quality": -1}), (10, JpegFormat::Baseline)),
+        (json!({"quality": 2.5, "jpegFormat": 1}), (10, JpegFormat::Baseline)),
+        (json!("not an object"), (10, JpegFormat::Baseline)),
+        (json!({"quality": 0, "jpegFormat": "optimized"}), (0, JpegFormat::Optimized)),
+    ] {
+        let (mut h, _writes) = jpeg_app("unused.jpg");
+        h.state_mut().session.prefs.edit(|p| {
+            p.dialogs.insert("file.jpegOptions".into(), saved.clone());
+        });
+        h.state_mut().save_as(Some("p.jpg".into())).unwrap();
+        h.run_steps(2);
+        assert_eq!(h.state().jpeg_options.as_ref().map(|p| (p.quality, p.format)), Some(expected), "{saved}");
+    }
+}
diff --git a/crates/ui-egui/src/jpeg_options_ui.rs b/crates/ui-egui/src/jpeg_options_ui.rs
--- a/crates/ui-egui/src/jpeg_options_ui.rs
+++ b/crates/ui-egui/src/jpeg_options_ui.rs
@@ -3,7 +3,9 @@
 //! 5–7, High 8–9, Maximum 10–12), mapped to the encoder's 1–100 by
 //! `photocraft_engine::file_cmds::jpeg_quality`; Format Options are Baseline ("Standard"), Baseline
 //! Optimized and Progressive. Matte, Scans and the size preview aren't offered yet. Re-saving an
-//! opened JPEG starts at the file's own quality, as File › Save keeps it (#4032).
+//! opened JPEG starts at the file's own quality, as File › Save keeps it (#4032). Otherwise the
+//! dialog starts at the quality and format it was last answered with, also after a restart, as
+//! Photoshop's does (see [`Start::for_active`]).
 //!
 //! The save is parked in [`Prompt`] while the modal is up (egui is immediate-mode) and written
 //! once answered. Cancel drops the save. Scripts skip the prompt by passing `quality` or
@@ -135,14 +137,58 @@ impl Start {
         Start { quality: quality_from_percent(quality), own_quality: None, format: JpegFormat::Baseline }
     }
 
-    /// For the active document: its opened JPEG's own quality (`Document::file_encoding`), or
-    /// else the Export preferences' JPEG quality (the one Quick Export uses).
+    /// For the active document: the Format Options the dialog was last answered with, else
+    /// Baseline; and the quality of its opened JPEG (`Document::file_encoding`, which File › Save
+    /// keeps, #4032), else the quality last answered, else the Export preferences' JPEG quality
+    /// (the one Quick Export uses).
     pub fn for_active(app: &PhotocraftApp) -> Self {
-        match app.session.active().and_then(|st| st.doc.file_encoding.jpeg_quality) {
+        let remembered = remembered(app);
+        let start = match app.session.active().and_then(|st| st.doc.file_encoding.jpeg_quality) {
             Some(q) => Start::own(q),
-            None => Start::percent(app.session.prefs().export.jpeg_quality),
-        }
+            None => match remembered.quality {
+                Some(quality) => Start { quality, own_quality: None, format: JpegFormat::Baseline },
+                None => Start::percent(app.session.prefs().export.jpeg_quality),
+            },
+        };
+        Start { format: remembered.format.unwrap_or(start.format), ..start }
+    }
+}
+
+/// Preferences ▸ remembered dialog settings: JPEG Options writes its last answer under this key
+/// and starts the next dialog from it, as Photoshop does (it keeps it across restarts). There is
+/// no command of this id; the key only names the dialog among the others.
+const REMEMBER_KEY: &str = "file.jpegOptions";
+
+/// What the dialog was last answered with; a value that no longer fits (a hand-edited
+/// preferences file) is left out.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+struct Remembered {
+    quality: Option<u8>,
+    format: Option<JpegFormat>,
+}
+
+fn remembered(app: &PhotocraftApp) -> Remembered {
+    let Some(Value::Object(saved)) = app.session.prefs().dialogs.get(REMEMBER_KEY) else { return Remembered::default() };
+    Remembered {
+        quality: saved.get("quality").and_then(Value::as_u64).and_then(|q| u8::try_from(q).ok()).filter(|&q| q <= MAX_QUALITY),
+        format: saved.get("jpegFormat").and_then(Value::as_str).and_then(JpegFormat::parse),
+    }
+}
+
+/// Records an answered dialog for the next one. A quality left on the opened file's own is not
+/// an answer: the earlier one stays, so saving a series of opened JPEGs doesn't forget what a
+/// new document was last saved at.
+fn remember(app: &mut PhotocraftApp, choice: JpegChoice) {
+    let before = remembered(app);
+    let quality = choice.quality.or(before.quality);
+    let mut saved = serde_json::Map::new();
+    if let Some(q) = quality {
+        saved.insert("quality".into(), q.into());
     }
+    saved.insert("jpegFormat".into(), choice.format.name().into());
+    app.session.prefs.edit(|p| {
+        p.dialogs.insert(REMEMBER_KEY.into(), Value::Object(saved));
+    });
 }
 
 /// A save waiting on the JPEG Options answer.
@@ -250,6 +296,7 @@ pub fn confirm(app: &mut PhotocraftApp, ctx: &egui::Context) {
     // quality out, so a replay keeps the file's own quality too, as File › Save does (#4032).
     let own = p.own_quality.filter(|&q| quality_from_percent(u32::from(q)) == p.quality);
     let choice = JpegChoice { quality: own.is_none().then_some(p.quality), format: p.format };
+    remember(app, choice);
     let step = if p.record == "file.save" {
         // A plain Save writes whichever file the action is played on (#3531), with the answer.
         plain_save_step(Some(choice))
