diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,7 +2,7 @@
 
 **Stage: alpha** · next: beta, ~20 points and ~120–210 h away
 
-> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
+> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-21 · **Change:** minor (native Open and Save As share the last selected directory (#1078); native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
 
 DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
 format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
 
 | Date | What landed |
 |---|---|
+| 2026-10-21 | Native Open and Save As share the last selected directory in UI preferences, falling back to the active presentation, recent files, then the OS default; cancelling preserves the selection (#1078) |
 | 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
 | 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
 | 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
@@ -239,6 +240,7 @@ app's JSON control channel (`deckcraft --control PORT`).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-21 | minor | Native file dialog directory memory (#1078); feature scores and milestone scope unchanged |
 | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged |
 | 2026-10-15 | trivial | Progress log: chart package parts survive a native save. Scores and milestone names unchanged |
 | 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged |
diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs
--- a/apps/deckcraft/src/main.rs
+++ b/apps/deckcraft/src/main.rs
@@ -93,8 +93,9 @@ fn save_prefs(app: &SlideApp) {
 
 fn services() -> Services {
     Services {
-        pick_open: Some(Box::new(|purpose: &str| {
+        pick_open: Some(Box::new(|purpose: &str, directory| {
             let d = rfd::FileDialog::new();
+            let d = if let Some(directory) = directory { d.set_directory(directory) } else { d };
             let d = match purpose {
                 "picture" => d.add_filter("Pictures", &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"]),
                 "audio" => d.add_filter(
@@ -111,7 +112,11 @@ fn services() -> Services {
             };
             d.pick_file().map(|p| p.to_string_lossy().to_string())
         })),
-        pick_save: Some(Box::new(|name: &str| rfd::FileDialog::new().set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string()))),
+        pick_save: Some(Box::new(|name: &str, directory| {
+            let d = rfd::FileDialog::new().set_file_name(name);
+            let d = if let Some(directory) = directory { d.set_directory(directory) } else { d };
+            d.save_file().map(|p| p.to_string_lossy().to_string())
+        })),
         read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
         write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
         clipboard_image: Some(Box::new(pasteboard::image_png)),
diff --git a/crates/ui-egui/src/backstage_tests.rs b/crates/ui-egui/src/backstage_tests.rs
--- a/crates/ui-egui/src/backstage_tests.rs
+++ b/crates/ui-egui/src/backstage_tests.rs
@@ -46,7 +46,7 @@ fn saving_into(dir: &std::path::Path) -> (Services, Rc<RefCell<Vec<String>>>) {
     let log = asked.clone();
     let dir = dir.to_path_buf();
     let services = Services {
-        pick_save: Some(Box::new(move |suggested: &str| {
+        pick_save: Some(Box::new(move |suggested: &str, _| {
             log.borrow_mut().push(suggested.to_string());
             Some(dir.join(suggested).to_string_lossy().to_string())
         })),
@@ -464,7 +464,7 @@ fn a_file_that_cant_be_opened_says_why() {
     let path = dir.join("Notes.pptx");
     std::fs::write(&path, b"not a presentation").unwrap();
     let picked = path.to_string_lossy().to_string();
-    let services = Services { pick_open: Some(Box::new(move |_: &str| Some(picked.clone()))), ..Default::default() };
+    let services = Services { pick_open: Some(Box::new(move |_: &str, _| Some(picked.clone()))), ..Default::default() };
     let mut h = harness(SlideApp::new(Session::new(), services));
     h.state_mut().run("app.backstage", json!({"page": "open"})).unwrap();
     h.run_steps(2);
diff --git a/crates/ui-egui/src/browsetheme_tests.rs b/crates/ui-egui/src/browsetheme_tests.rs
--- a/crates/ui-egui/src/browsetheme_tests.rs
+++ b/crates/ui-egui/src/browsetheme_tests.rs
@@ -22,7 +22,7 @@ fn browse_button_applies_the_picked_files_theme() {
     let log = asked.clone();
     let p2 = path.to_string_lossy().to_string();
     let services = Services {
-        pick_open: Some(Box::new(move |purpose: &str| {
+        pick_open: Some(Box::new(move |purpose: &str, _| {
             log.borrow_mut().push(purpose.to_string());
             Some(p2.clone())
         })),
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -217,8 +217,8 @@ fn album_item_label(v: &Value) -> String {
 }
 
 fn pick_album_picture(app: &mut SlideApp) -> Option<Value> {
-    let path = if let Some(pick) = app.services.pick_open.as_mut() {
-        pick("picture")
+    let path = if app.services.pick_open.is_some() {
+        app.pick_open_file("picture")
     } else {
         app.set_status("Pass pictures to insert.photoAlbum");
         None
@@ -3401,7 +3401,7 @@ mod tests {
         let suggested = std::rc::Rc::new(RefCell::new(String::new()));
         let seen = suggested.clone();
         let services = crate::Services {
-            pick_save: Some(Box::new(move |s: &str| {
+            pick_save: Some(Box::new(move |s: &str, _| {
                 *seen.borrow_mut() = s.to_string();
                 Some(picked.clone())
             })),
@@ -3459,7 +3459,7 @@ mod tests {
         std::fs::create_dir_all(&dir).unwrap();
         let path = dir.join("slide.png");
         let picked = path.to_string_lossy().to_string();
-        let services = crate::Services { pick_save: Some(Box::new(move |_: &str| Some(picked.clone()))), ..Default::default() };
+        let services = crate::Services { pick_save: Some(Box::new(move |_: &str, _| Some(picked.clone()))), ..Default::default() };
         let mut harness = egui_kittest::Harness::builder().with_size(vec2(1200.0, 900.0)).build_ui_state(
             |ui, app: &mut SlideApp| {
                 app.logic(ui.ctx());
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -49,7 +49,8 @@ pub use control::{ControlRequest, ControlResponse};
 
 pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
 pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
-pub type PickFn = Box<dyn FnMut(&str) -> Option<String>>;
+/// Purpose or suggested name, starting directory → selected path. `None` keeps the OS default.
+pub type PickFn = Box<dyn FnMut(&str, Option<&std::path::Path>) -> Option<String>>;
 /// `(purpose, request id)`. The host shows a picker and later pushes an [`InboxEvent`]
 /// carrying that same id. Completing a cancelled or unknown id does nothing: the file is
 /// not applied to whichever document happens to be active.
@@ -200,9 +201,9 @@ const STATUS_LINGER_MS: f64 = 6000.0;
 /// Platform services injected by the host (desktop or web).
 #[derive(Default)]
 pub struct Services {
-    /// Open-file dialog for a purpose (`open`, `picture`, `audio`, `video`) → path.
+    /// Open-file dialog for a purpose (`open`, `picture`, `audio`, `video`) and directory → path.
     pub pick_open: Option<PickFn>,
-    /// Save dialog with a suggested name → path.
+    /// Save dialog with a suggested name and directory → path.
     pub pick_save: Option<PickFn>,
     pub read: Option<ReadFn>,
     pub write: Option<WriteFn>,
@@ -258,6 +259,8 @@ pub struct UiState {
     pub zoom: Option<f32>,
     pub sorter_zoom: f32,
     pub recent: Vec<String>,
+    /// Last directory selected through a native file dialog, shared by Open and Save As.
+    pub file_dialog_directory: Option<std::path::PathBuf>,
     pub presenter_view: bool,
     pub ui_scale: f32,
     /// Format Shape pane: tab (`fill`, `effects`, `size`, `picture`, `text`).
@@ -296,6 +299,7 @@ impl Default for UiState {
             zoom: None,
             sorter_zoom: 1.0,
             recent: vec![],
+            file_dialog_directory: None,
             presenter_view: true,
             ui_scale: 1.0,
             format_tab: "fill".into(),
@@ -752,8 +756,8 @@ impl SlideApp {
             }
         };
         let suggested = self.suggested_name(ext);
-        if let Some(pick) = self.services.pick_save.as_mut() {
-            let Some(path) = pick(&suggested) else { return false };
+        if self.services.pick_save.is_some() {
+            let Some(path) = self.pick_save_file(&suggested) else { return false };
             let mut params = json!({"path": path, "format": format, "all": all});
             if let (Some(o), Some(x)) = (params.as_object_mut(), extra.as_object()) {
                 o.extend(x.clone());
@@ -1169,9 +1173,42 @@ impl SlideApp {
         Ok(json!({"playing": true, "slide": slide, "duration": duration}))
     }
 
+    /// Valid dialog selection first, then the active presentation, then recent files in order.
+    fn file_dialog_directory(&self) -> Option<std::path::PathBuf> {
+        self.ui.file_dialog_directory.clone().filter(|p| p.is_dir()).or_else(|| {
+            self.session
+                .active()
+                .and_then(|d| d.path.as_deref())
+                .and_then(file_parent_directory)
+                .or_else(|| self.ui.recent.iter().find_map(|p| file_parent_directory(p)))
+        })
+    }
+
+    fn remember_file_directory(&mut self, path: &str) {
+        if let Some(directory) = file_parent_directory(path) {
+            self.ui.file_dialog_directory = Some(directory);
+        }
+    }
+
+    pub(crate) fn pick_open_file(&mut self, purpose: &str) -> Option<String> {
+        let directory = self.file_dialog_directory();
+        let pick = self.services.pick_open.as_mut()?;
+        let path = pick(purpose, directory.as_deref())?;
+        self.remember_file_directory(&path);
+        Some(path)
+    }
+
+    fn pick_save_file(&mut self, name: &str) -> Option<String> {
+        let directory = self.file_dialog_directory();
+        let pick = self.services.pick_save.as_mut()?;
+        let path = pick(name, directory.as_deref())?;
+        self.remember_file_directory(&path);
+        Some(path)
+    }
+
     fn pick_and_open(&mut self) {
-        if let Some(pick) = self.services.pick_open.as_mut() {
-            if let Some(path) = pick("open") {
+        if self.services.pick_open.is_some() {
+            if let Some(path) = self.pick_open_file("open") {
                 self.open_path_in_background(&path);
             }
         } else if self.services.open_async.is_some() {
@@ -1228,11 +1265,11 @@ impl SlideApp {
     /// Home › New Slide › Reuse Slides…: pick a presentation and insert all its slides after the
     /// current one (desktop only: the browser picker opens what it picks as a document).
     fn pick_and_reuse(&mut self, keep: bool) {
-        let Some(pick) = self.services.pick_open.as_mut() else {
+        if self.services.pick_open.is_none() {
             self.set_status("Reuse Slides needs the desktop app".to_string());
             return;
-        };
-        let Some(path) = pick("open") else { return };
+        }
+        let Some(path) = self.pick_open_file("open") else { return };
         match self.session.execute("slide.reuse", &json!({"path": path, "keepFormatting": keep})) {
             Ok(r) => self.set_status(format!("Inserted {} slides from {path}", r["slides"])),
             Err(e) => self.set_status(e.to_string()),
@@ -1245,8 +1282,8 @@ impl SlideApp {
 
     /// Pick a file and run `cmd` with it (`name` + `data`, or `path`) added to `extra`.
     fn pick_and_run(&mut self, purpose: &str, cmd: &str, extra: &Value) {
-        if let Some(pick) = self.services.pick_open.as_mut() {
-            if let Some(path) = pick(purpose) {
+        if self.services.pick_open.is_some() {
+            if let Some(path) = self.pick_open_file(purpose) {
                 let mut args = if extra.is_object() { extra.clone() } else { json!({}) };
                 let r = match self.services.read.as_ref().map(|r| r(&path)) {
                     Some(Ok(bytes)) => {
@@ -1282,8 +1319,8 @@ impl SlideApp {
         let name = self.session.active().map(|d| d.title()).unwrap_or_else(|| "Presentation".into());
         let ext = if format == "pptx" { "pptx" } else { "deckcraft" };
         let suggested = format!("{name}.{ext}");
-        if let Some(pick) = self.services.pick_save.as_mut() {
-            if let Some(path) = pick(&suggested) {
+        if self.services.pick_save.is_some() {
+            if let Some(path) = self.pick_save_file(&suggested) {
                 match self.session.execute("file.saveAs", &json!({"path": path})) {
                     Ok(_) => {
                         self.set_status(format!("Saved {path}"));
@@ -1973,6 +2010,13 @@ impl SlideApp {
     }
 }
 
+/// An existing parent directory, resolving relative file names against the working directory.
+fn file_parent_directory(path: &str) -> Option<std::path::PathBuf> {
+    let path = std::path::Path::new(path);
+    let absolute = if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir().ok()?.join(path) };
+    absolute.parent().filter(|p| p.is_dir()).map(std::path::Path::to_path_buf)
+}
+
 /// Engine key name for an egui key.
 pub fn key_name(key: egui::Key) -> &'static str {
     use egui::Key::*;
@@ -2262,7 +2306,7 @@ mod tests {
         // Desktop: the file dialog returns a path.
         let blue = svg("blue");
         let services = Services {
-            pick_open: Some(Box::new(|_: &str| Some("b.svg".into()))),
+            pick_open: Some(Box::new(|_: &str, _| Some("b.svg".into()))),
             read: Some(Box::new(move |_: &str| Ok(blue.clone()))),
             ..Default::default()
         };
diff --git a/crates/ui-egui/src/opening.rs b/crates/ui-egui/src/opening.rs
--- a/crates/ui-egui/src/opening.rs
+++ b/crates/ui-egui/src/opening.rs
@@ -213,6 +213,116 @@ mod tests {
         path
     }
 
+    /// #1078: native Open and Save As share their last selected directory.
+    #[test]
+    fn open_and_save_as_share_directory_and_cancellation_preserves_it() {
+        let source = saved_as("dialog-source.deckcraft", 1);
+        let a = std::path::Path::new(&source).parent().unwrap().to_path_buf();
+        let b = a.join("dialog-save");
+        std::fs::create_dir_all(&b).unwrap();
+        let destination = b.join("copy.deckcraft").to_string_lossy().into_owned();
+        let picked = source.clone();
+        let services = crate::Services {
+            pick_open: Some(Box::new(move |purpose, directory| {
+                assert_eq!(purpose, "open");
+                assert_eq!(directory, None);
+                Some(picked.clone())
+            })),
+            ..Default::default()
+        };
+        let mut app = SlideApp::new(Session::new(), services);
+        app.pick_and_open();
+        wait(&mut app);
+        assert_eq!(app.session.active().unwrap().path.as_deref(), Some(source.as_str()));
+        assert_eq!(app.ui.file_dialog_directory.as_ref(), Some(&a));
+
+        let expected = a.clone();
+        let picked = destination.clone();
+        app.services.pick_save = Some(Box::new(move |name, directory| {
+            assert_eq!(name, "dialog-source.deckcraft");
+            assert_eq!(directory, Some(expected.as_path()));
+            Some(picked.clone())
+        }));
+        app.save_as_dialog("deckcraft");
+        assert_eq!(app.session.active().unwrap().path.as_deref(), Some(destination.as_str()));
+        assert_eq!(app.ui.file_dialog_directory.as_ref(), Some(&b));
+        assert!(std::path::Path::new(&destination).is_file());
+
+        // Switching documents does not displace the last dialog selection; neither does Cancel.
+        app.session.active_mut().unwrap().path = Some(source.clone());
+        let expected = b.clone();
+        app.services.pick_open = Some(Box::new(move |purpose, directory| {
+            assert_eq!(purpose, "open");
+            assert_eq!(directory, Some(expected.as_path()));
+            None
+        }));
+        let expected = b.clone();
+        app.services.pick_save = Some(Box::new(move |_, directory| {
+            assert_eq!(directory, Some(expected.as_path()));
+            None
+        }));
+        app.pick_and_open();
+        app.save_as_dialog("deckcraft");
+        assert_eq!(app.ui.file_dialog_directory.as_ref(), Some(&b));
+        assert_eq!(app.session.active().unwrap().path.as_deref(), Some(source.as_str()));
+        std::fs::remove_file(source).unwrap();
+        std::fs::remove_file(destination).unwrap();
+        std::fs::remove_dir(b).unwrap();
+    }
+
+    #[test]
+    fn file_dialogs_fall_back_to_active_then_recent_directories() {
+        let root = std::env::temp_dir();
+        let expected = root.clone();
+        let services = crate::Services {
+            pick_open: Some(Box::new(move |_, directory| {
+                assert_eq!(directory, Some(expected.as_path()));
+                None
+            })),
+            ..Default::default()
+        };
+        let mut app = SlideApp::new(Session::with_new(), services);
+        app.session.active_mut().unwrap().path = Some(root.join("active.pptx").to_string_lossy().into_owned());
+        let current = std::env::current_dir().unwrap();
+        app.ui.recent = vec![current.join("recent.pptx").to_string_lossy().into_owned()];
+        app.pick_and_open();
+        assert_eq!(app.ui.file_dialog_directory, None, "a fallback is not a selection");
+
+        // A missing remembered directory is ignored; relative recent files resolve against cwd.
+        let missing = root.join(format!("deckcraft-dialog-missing-{}", std::process::id()));
+        assert!(!missing.exists());
+        app.ui.file_dialog_directory = Some(missing.clone());
+        app.session.active_mut().unwrap().path = Some(missing.join("active.pptx").to_string_lossy().into_owned());
+        app.ui.recent = vec![missing.join("recent.pptx").to_string_lossy().into_owned(), "relative.pptx".into()];
+        app.services.pick_save = Some(Box::new(move |_, directory| {
+            assert_eq!(directory, Some(current.as_path()));
+            None
+        }));
+        app.save_as_dialog("deckcraft");
+        assert_eq!(app.ui.file_dialog_directory, Some(missing));
+    }
+
+    #[test]
+    fn file_dialogs_without_valid_directories_keep_the_os_default() {
+        let mut app = SlideApp::new(Session::with_new(), crate::Services::default());
+        let missing = std::env::temp_dir().join(format!("deckcraft-dialog-default-missing-{}", std::process::id()));
+        assert!(!missing.exists());
+        app.ui.file_dialog_directory = Some(missing.clone());
+        app.session.active_mut().unwrap().path = Some(missing.join("active.pptx").to_string_lossy().into_owned());
+        app.ui.recent = vec![missing.join("recent.pptx").to_string_lossy().into_owned()];
+        app.services.pick_open = Some(Box::new(|_, directory| {
+            assert_eq!(directory, None);
+            None
+        }));
+        app.services.pick_save = Some(Box::new(|_, directory| {
+            assert_eq!(directory, None);
+            None
+        }));
+        app.pick_and_open();
+        app.save_as_dialog("deckcraft");
+        assert_eq!(app.ui.file_dialog_directory, Some(missing));
+    }
+
     /// #862: a PowerPoint Show handed over by the system (command line, double-click) starts its
     /// slide show at once, full screen and from the first slide.
     #[test]
diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs
--- a/crates/ui-egui/src/panes.rs
+++ b/crates/ui-egui/src/panes.rs
@@ -641,8 +641,7 @@ fn fx_slider(
 
 /// Asks for a picture file and makes it the selected shapes' fill.
 fn pick_fill_picture(app: &mut SlideApp) {
-    if let Some(pick) = app.services.pick_open.as_mut()
-        && let Some(path) = pick("picture")
+    if let Some(path) = app.pick_open_file("picture")
         && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path))
     {
         run(app, "shape.fill", json!({"picture": deckcraft_engine::cmd::base64_encode(&bytes)}));
@@ -834,8 +833,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) {
                     run(app, "design.background", json!({"gradient": crate::fillui::gradient_json(&g)}));
                 }
                 "picture" => {
-                    if let Some(pick) = app.services.pick_open.as_mut()
-                        && let Some(path) = pick("picture")
+                    if let Some(path) = app.pick_open_file("picture")
                         && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path))
                     {
                         let _ = app.run("design.background", json!({"picture": deckcraft_engine::cmd::base64_encode(&bytes)}));
@@ -894,8 +892,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) {
         }
         Some(Fill::Picture(pf)) => {
             if ui.button("Insert picture from file…").clicked()
-                && let Some(pick) = app.services.pick_open.as_mut()
-                && let Some(path) = pick("picture")
+                && let Some(path) = app.pick_open_file("picture")
                 && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path))
             {
                 let _ = app.run("design.background", json!({"picture": deckcraft_engine::cmd::base64_encode(&bytes)}));
diff --git a/crates/ui-egui/src/photo_album_tests.rs b/crates/ui-egui/src/photo_album_tests.rs
--- a/crates/ui-egui/src/photo_album_tests.rs
+++ b/crates/ui-egui/src/photo_album_tests.rs
@@ -49,7 +49,7 @@ fn insert_ribbon_opens_the_dialog_without_editing() {
 fn create_inserts_the_album_in_one_step() {
     let bytes = png();
     let services = Services {
-        pick_open: Some(Box::new(|_| Some("/tmp/cat.png".into()))),
+        pick_open: Some(Box::new(|_, _| Some("/tmp/cat.png".into()))),
         read: Some(Box::new(move |_| Ok(bytes.clone()))),
         ..Default::default()
     };
diff --git a/crates/ui-egui/src/settings.rs b/crates/ui-egui/src/settings.rs
--- a/crates/ui-egui/src/settings.rs
+++ b/crates/ui-egui/src/settings.rs
@@ -120,17 +120,21 @@ mod tests {
         assert_eq!(prefs.author.chars().count(), 256);
         // A zero or negative zoom means "fit".
         std::fs::write(d.join(UI_FILE), r#"{"zoom": 0}"#).unwrap();
-        assert_eq!(load(&d).0.unwrap().zoom, None);
+        let ui = load(&d).0.unwrap();
+        assert_eq!(ui.zoom, None);
+        assert_eq!(ui.file_dialog_directory, None);
     }
 
     #[test]
     fn saving_round_trips_and_leaves_no_temporary_file() {
         let d = dir("save");
-        let ui = UiState { zoom: Some(1.5), recent: vec!["/a.pptx".into()], ..Default::default() };
+        let ui = UiState { zoom: Some(1.5), recent: vec!["/a.pptx".into()], file_dialog_directory: Some(d.clone()), ..Default::default() };
         let prefs = Prefs { author: "Ann".into(), ..Default::default() };
         save(&d.join("nested"), &ui, &prefs);
         let (u, p) = load(&d.join("nested"));
-        assert_eq!(u.unwrap().zoom, Some(1.5));
+        let u = u.unwrap();
+        assert_eq!(u.zoom, Some(1.5));
+        assert_eq!(u.file_dialog_directory, Some(d.clone()));
         assert_eq!(p.unwrap().author, "Ann");
         let names: Vec<String> =
             std::fs::read_dir(d.join("nested")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect();
