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
@@ -8,6 +8,8 @@ use serde_json::json;
 use crate::CadApp;
 use crate::theme::Tokens;
 
+/// Draw the open dialog (`app.ui.dialog`), the MTEXT editor and Quick Properties for this frame; a
+/// dialog closed by its own button is cleared.
 pub fn show(app: &mut CadApp, ctx: &egui::Context) {
     crate::mtext_editor::show(app, ctx);
     crate::quick::quick_properties(app, ctx);
@@ -21,6 +23,7 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {
         "parameters" => crate::parametric::parameters_dialog(app, ctx, &mut open),
         "dsettings" => dsettings(app, ctx, &mut open),
         "units" => units(app, ctx, &mut open),
+        "saveas" => crate::saveas::dialog(app, ctx, &mut open),
         "hatch" => crate::hatchdlg::dialog(app, ctx, &mut open),
         "aliasedit" => crate::aliasdlg::dialog(app, ctx, &mut open),
         "about" => about(ctx, &mut open),
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -1365,3 +1365,13 @@ menu	Modify	Edita
 	Check for Updates…	Verifica aggiornamenti…
 	CADCraft is up to date.	CADCraft è aggiornato.
 	Could not check for updates.	Impossibile verificare gli aggiornamenti.
+# Save As dialog: file type, version, binary DXF.
+	Save As (file picker)	Salva con nome (selezione file)
+	File type	Tipo di file
+	Save...	Salva...
+	Binary DXF	DXF binario
+	An export of the drawing as shown: the drawing keeps its own file.	Un'esportazione del disegno com'è mostrato: il disegno mantiene il proprio file.
+	Units of new drawings	Unità dei nuovi disegni
+	Imperial (inches)	Imperiali (pollici)
+	Metric (millimetres)	Metriche (millimetri)
+	New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE).	Nuovo e il primo disegno all'avvio usano queste unità, a meno che non sia impostato un modello predefinito (QNEWTEMPLATE).
diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs
--- a/crates/ui-egui/src/i18n/tests.rs
+++ b/crates/ui-egui/src/i18n/tests.rs
@@ -59,6 +59,8 @@ fn translation_gaps() {
     }
 }
 
+/// The command labels, menu names, tool names and snap mode names that `language`'s catalog has no
+/// row for.
 fn missing_commands_menus_tools_and_snap_labels(language: &str) -> Vec<String> {
     let catalog = catalog(language).unwrap();
     let mut sources = BTreeSet::new();
@@ -81,6 +83,7 @@ fn missing_commands_menus_tools_and_snap_labels(language: &str) -> Vec<String> {
     sources.into_iter().filter(|s| !s.is_empty() && !catalog.contains_key(&("", *s))).map(str::to_string).collect()
 }
 
+/// The `tl!`/`tf!` literals of the listed UI source files that `language`'s catalog has no row for.
 fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
     let catalog = catalog(language).unwrap();
     let sources = [
@@ -92,6 +95,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
         include_str!("../palettes/quickcalc.rs"),
         include_str!("../palettes/patterns.rs"),
         include_str!("../dialogs.rs"),
+        include_str!("../saveas.rs"),
         include_str!("../hatchdlg.rs"),
         include_str!("../aliasdlg.rs"),
         include_str!("../mtext_editor.rs"),
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -1371,3 +1371,9 @@
 	Check for Updates…	Перевірити оновлення…
 	CADCraft is up to date.	Встановлено найновішу версію CADCraft.
 	Could not check for updates.	Не вдалося перевірити оновлення.
+# Save As dialog: file type, version, binary DXF.
+	Save As (file picker)	Зберегти як (вибір файлу)
+	File type	Тип файлу
+	Save...	Зберегти...
+	Binary DXF	Двійковий DXF
+	An export of the drawing as shown: the drawing keeps its own file.	Експорт креслення як воно показане: креслення зберігає власний файл.
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
@@ -46,6 +46,7 @@ pub mod palettes;
 pub mod parametric;
 pub mod plotstyles;
 pub mod quick;
+pub mod saveas;
 pub mod shortcut_menus;
 pub mod theme;
 #[cfg(not(target_arch = "wasm32"))]
@@ -159,6 +160,7 @@ impl SaveFormat {
 }
 
 impl Default for UiState {
+    /// The interface as a first start shows it: every panel and bar on, the default save format and version.
     fn default() -> Self {
         UiState {
             docking: docking::default_workspace(),
@@ -223,6 +225,8 @@ pub struct CadApp {
     pub status: Option<(String, f64)>,
     /// Drawings being read on worker threads ([`opening`]).
     pub opening: opening::Opening,
+    /// The Save As dialog's choices while it is open ([`saveas`]).
+    pub saveas: Option<saveas::SaveAsState>,
     pub integrated_titlebar: bool,
     control_rx: Option<Receiver<ControlRequest>>,
     /// Fresh surface and identity snapshot (see [`capture`]).
@@ -296,6 +300,7 @@ impl CadApp {
             cmd: cmdline::CmdLine::default(),
             status: None,
             opening: opening::Opening::default(),
+            saveas: None,
             integrated_titlebar: false,
             control_rx: None,
             capture_geometry: None,
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -23,6 +23,7 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
     ("ui.open", "Open...", &[], Some("Cmd+O")),
     ("ui.newtemplate", "New from Template...", &["File", "New from Template..."], None),
     ("ui.saveas", "Save As...", &[], None),
+    ("ui.saveas.pick", "Save As (file picker)", &[], None),
     ("ui.sample", "Open Sample Drawing", &["Help", "Open Sample Drawing"], None),
     ("ui.toggle.toolsets", "Tool Sets", &["Window", "Tool Sets"], Some("Cmd+3")),
     ("ui.panel.properties", "Properties Inspector", &["Window", "Properties Inspector"], Some("Cmd+1")),
@@ -85,6 +86,8 @@ pub fn run_ui_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Resu
     crate::i18n::with_language(app.language(), || run_ui_command_inner(app, id, params))
 }
 
+/// [`run_ui_command`] in the interface language: the UI-only commands, and the file commands that
+/// ask for a file (Open, Save As and its dialog, Export…) when they come without parameters.
 fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
     if let Some(result) = crate::spacemouse::route(app, id, params) {
         return Some(result);
@@ -197,15 +200,25 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
             }
             Ok(Value::Null)
         }
+        // Save As from the menu, the tool bar, a shortcut or typed: the Save As dialog (file type,
+        // version, binary DXF) before the host's file picker.
         "ui.saveas" | "saveas" if no_path => {
+            crate::saveas::open(app);
+            Ok(Value::Null)
+        }
+        // The picker alone, in the default format and version: QSAVE of a drawing with no file
+        // yet, and the close prompt's Save, which must finish in this call.
+        "ui.saveas.pick" if no_path => {
             let title = app.session.state().map(|s| s.title.clone()).ok();
             let name = app.ui.save_format.suggested_name(title.as_deref());
             if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name)) {
+                // The default format is the format written, whatever extension the picker gave.
+                let p = crate::saveas::with_extension(&p, app.ui.save_format.as_str());
                 return Some(app.session.execute("saveas", &saveas_params(&app.ui, &app.session, &p)).map_err(|e| e.to_string()));
             }
             Ok(Value::Null)
         }
-        "qsave" if no_path && app.session.state().is_ok_and(|s| s.path.is_none()) => return run_ui_command(app, "ui.saveas", &Value::Null),
+        "qsave" if no_path && app.session.state().is_ok_and(|s| s.path.is_none()) => return run_ui_command(app, "ui.saveas.pick", &Value::Null),
         // EXPORTPDF from the menu or typed: ask where to save the PDF (PLOT and PRINT open the Plot
         // dialog, `managers::route`). JSON calls (scripts, control channel, MCP) always carry
         // params, never get here and never open a dialog.
diff --git a/crates/ui-egui/src/saveas.rs b/crates/ui-egui/src/saveas.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/saveas.rs
@@ -0,0 +1,225 @@
+//! The Save As dialog: the file type, the DXF/DWG version and binary DXF, chosen before the
+//! host's file picker, which cannot report the type the user picked. "Save..." opens the picker
+//! for the chosen type and runs `saveas` (images: an export, as SAVEAS to an image does).
+//!
+//! QSAVE of a drawing that has no file yet skips this dialog and goes straight to the picker
+//! (`ui.saveas.pick`): the close prompt's "Save" relies on that save finishing in the same call.
+
+use egui::{RichText, vec2};
+use serde_json::{Value, json};
+
+use crate::theme::Tokens;
+use crate::{CadApp, SaveFormat, SaveVersion};
+
+/// A file type the dialog saves as.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum FileType {
+    Dxf,
+    Dwg,
+    Dwt,
+    Svg,
+    Png,
+    Pdf,
+}
+
+impl FileType {
+    pub const ALL: [FileType; 6] = [FileType::Dxf, FileType::Dwg, FileType::Dwt, FileType::Svg, FileType::Png, FileType::Pdf];
+
+    /// The file extension, without the dot.
+    pub fn ext(self) -> &'static str {
+        match self {
+            FileType::Dxf => "dxf",
+            FileType::Dwg => "dwg",
+            FileType::Dwt => "dwt",
+            FileType::Svg => "svg",
+            FileType::Png => "png",
+            FileType::Pdf => "pdf",
+        }
+    }
+
+    /// The name the dialog shows (translated; the format names stay as they are).
+    pub fn label(self) -> &'static str {
+        match self {
+            FileType::Dxf => crate::tl!("Drawing (DXF)"),
+            FileType::Dwg => crate::tl!("Drawing (DWG)"),
+            FileType::Dwt => crate::tl!("Drawing Template (DWT)"),
+            FileType::Svg => "SVG",
+            FileType::Png => "PNG",
+            FileType::Pdf => "PDF",
+        }
+    }
+
+    /// The type of an extension.
+    pub fn from_ext(ext: &str) -> Option<FileType> {
+        let ext = ext.trim().trim_start_matches('.');
+        FileType::ALL.into_iter().find(|t| t.ext().eq_ignore_ascii_case(ext))
+    }
+
+    /// A drawing file whose version the dialog asks for (DXF, DWG).
+    fn versioned(self) -> bool {
+        matches!(self, FileType::Dxf | FileType::Dwg)
+    }
+}
+
+/// The dialog's choices until Save.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SaveAsState {
+    pub file_type: FileType,
+    pub version: SaveVersion,
+    pub binary: bool,
+}
+
+impl SaveAsState {
+    /// The choices a drawing starts from: its own file type and version (an untitled drawing:
+    /// the default save format and version) and its binary flag.
+    pub fn for_app(app: &CadApp) -> SaveAsState {
+        let st = app.session.state().ok();
+        let untitled = st.as_ref().is_none_or(|s| s.path.is_none());
+        let title = st.as_ref().map(|s| s.title.clone()).unwrap_or_default();
+        let from_title = std::path::Path::new(&title).extension().and_then(|e| FileType::from_ext(&e.to_string_lossy()));
+        let default_type = match app.ui.save_format {
+            SaveFormat::Dxf => FileType::Dxf,
+            SaveFormat::Dwg => FileType::Dwg,
+        };
+        // A drawing from a file keeps its type (a template stays a template); an untitled one,
+        // or one from an image, takes the default save format.
+        let file_type = from_title.filter(|t| t.versioned() || (*t == FileType::Dwt && !untitled)).unwrap_or(default_type);
+        let own =
+            app.session.doc().ok().and_then(|d| d.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str).and_then(SaveVersion::for_acadver));
+        let version = match (untitled, own) {
+            (false, Some(v)) => v,
+            _ => SaveVersion::parse(&app.ui.save_version).unwrap_or_default(),
+        };
+        SaveAsState { file_type, version, binary: st.as_ref().is_some_and(|s| s.binary_dxf) && file_type == FileType::Dxf }
+    }
+
+    /// The `saveas` parameters for `path`.
+    pub fn params(&self, path: &str) -> Value {
+        let mut p = json!({ "path": path });
+        if self.file_type.versioned()
+            && let Some(o) = p.as_object_mut()
+        {
+            o.insert("version".into(), json!(self.version.year()));
+            if self.binary && self.file_type == FileType::Dxf {
+                o.insert("binary".into(), json!(true));
+            }
+        }
+        p
+    }
+
+    /// The file name the picker suggests: the drawing's name with the chosen extension.
+    pub fn suggested_name(&self, title: &str) -> String {
+        let stem = std::path::Path::new(title).file_stem().map(|s| s.to_string_lossy().to_string()).filter(|s| !s.trim().is_empty());
+        format!("{}.{}", stem.unwrap_or_else(|| "Drawing".into()), self.file_type.ext())
+    }
+
+    /// Binary is for DXF only.
+    fn settle(&mut self) {
+        if self.file_type != FileType::Dxf {
+            self.binary = false;
+        }
+    }
+}
+
+/// Open the dialog for the active drawing.
+pub fn open(app: &mut CadApp) {
+    app.saveas = Some(SaveAsState::for_app(app));
+    app.ui.dialog = Some("saveas".into());
+}
+
+/// `path` with the chosen type's extension: kept when the picker gave it (any case), added
+/// otherwise (`plan.dwg` saved as DXF is `plan.dwg.dxf`), so the type chosen is the type written.
+pub(crate) fn with_extension(path: &str, ext: &str) -> String {
+    let has = std::path::Path::new(path).extension().is_some_and(|e| e.eq_ignore_ascii_case(ext));
+    if has { path.to_string() } else { format!("{path}.{ext}") }
+}
+
+/// Ask the host where to save and run `saveas` with the dialog's choices. `Some(true)` once
+/// saved, `Some(false)` when the picker was cancelled or the save failed (the dialog stays),
+/// `None` without a picker (said on the command line).
+pub fn save(app: &mut CadApp) -> Option<bool> {
+    let st = app.saveas.clone().unwrap_or_else(|| SaveAsState::for_app(app));
+    let title = app.session.state().map(|s| s.title.clone()).unwrap_or_default();
+    let Some(pick) = app.services.pick_save.as_ref() else {
+        app.session
+            .echo(r#"SAVEAS: there is no file dialog here; run it with parameters, e.g. saveas {"path": "plan.dxf", "version": 2018}"#.to_string());
+        return None;
+    };
+    let Some(path) = pick(&st.suggested_name(&title)) else { return Some(false) };
+    Some(app.run("saveas", st.params(&with_extension(&path, st.file_type.ext()))).is_ok())
+}
+
+/// A dimmed, wrapping note.
+fn note(ui: &mut egui::Ui, text: &str) {
+    ui.add(egui::Label::new(RichText::new(text).color(Tokens::get().text_dim)).wrap());
+}
+
+/// The Save As window: file type, version and binary DXF, then Save… (the picker) or Cancel. The
+/// choices live in `app.saveas` until the window closes.
+pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
+    if app.saveas.is_none() {
+        app.saveas = Some(SaveAsState::for_app(app));
+    }
+    let Some(mut st) = app.saveas.clone() else { return };
+    let (mut save_clicked, mut cancel) = (false, false);
+    egui::Window::new(crate::tl!("Save As"))
+        .id(egui::Id::unique("saveas_dialog"))
+        .open(open)
+        .collapsible(false)
+        .resizable(false)
+        .pivot(egui::Align2::CENTER_CENTER)
+        .default_pos(ctx.content_rect().center())
+        .show(ctx, |ui| {
+            ui.set_max_width(440.0);
+            egui::Grid::new("saveas_grid").num_columns(2).spacing(vec2(12.0, 6.0)).show(ui, |ui| {
+                ui.label(crate::tl!("File type"));
+                egui::ComboBox::from_id_salt("saveas_type").selected_text(st.file_type.label()).width(260.0).show_ui(ui, |ui| {
+                    for ft in FileType::ALL {
+                        ui.selectable_value(&mut st.file_type, ft, ft.label());
+                    }
+                });
+                ui.end_row();
+                if st.file_type.versioned() {
+                    ui.label(crate::tl!("Version"));
+                    egui::ComboBox::from_id_salt("saveas_version").selected_text(version_label(st.version)).width(260.0).show_ui(ui, |ui| {
+                        for v in SaveVersion::ALL {
+                            ui.selectable_value(&mut st.version, v, version_label(v));
+                        }
+                    });
+                    ui.end_row();
+                }
+                if st.file_type == FileType::Dxf {
+                    ui.label("");
+                    ui.checkbox(&mut st.binary, crate::tl!("Binary DXF"));
+                    ui.end_row();
+                }
+            });
+            st.settle();
+            if !st.file_type.versioned() && st.file_type != FileType::Dwt {
+                note(ui, crate::tl!("An export of the drawing as shown: the drawing keeps its own file."));
+            }
+            ui.separator();
+            ui.horizontal(|ui| {
+                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                    save_clicked = ui.button(crate::tl!("Save...")).clicked();
+                    cancel = ui.button(crate::tl!("Cancel")).clicked();
+                });
+            });
+        });
+    app.saveas = Some(st);
+    // Closed once saved, or when there is no picker to ask (the command line says how to save).
+    if cancel || (save_clicked && save(app) != Some(false)) {
+        *open = false;
+    }
+    if !*open {
+        app.saveas = None;
+    }
+}
+
+/// A version's label: "AutoCAD 2018".
+pub fn version_label(v: SaveVersion) -> String {
+    format!("AutoCAD {}", v.year())
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/crates/ui-egui/src/saveas/tests.rs b/crates/ui-egui/src/saveas/tests.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/saveas/tests.rs
@@ -0,0 +1,119 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use cadcraft_engine::Session;
+use serde_json::json;
+
+use super::*;
+use crate::Services;
+
+/// An app whose file picker answers `answer` and records every name it was offered.
+fn app_with_picker(answer: Option<String>) -> (CadApp, Rc<RefCell<Vec<String>>>) {
+    let asked = Rc::new(RefCell::new(Vec::new()));
+    let log = Rc::clone(&asked);
+    let services = Services {
+        pick_open: None,
+        pick_save: Some(Box::new(move |name: &str| {
+            log.borrow_mut().push(name.to_string());
+            answer.clone()
+        })),
+        ..Services::default()
+    };
+    (CadApp::new(Session::new(), services), asked)
+}
+
+/// Save As opens the dialog, not the picker, on the drawing's own type and version (a template
+/// stays a template), and its choices become the `saveas` parameters.
+#[test]
+fn save_as_opens_the_dialog_with_the_drawings_own_type_and_version() {
+    let (mut app, asked) = app_with_picker(Some("x.dxf".into()));
+    app.start("ui.saveas");
+    assert_eq!(app.ui.dialog.as_deref(), Some("saveas"));
+    assert!(asked.borrow().is_empty(), "the picker waits for Save...");
+    let st = app.saveas.clone().unwrap();
+    assert_eq!((st.file_type, st.version, st.binary), (FileType::Dxf, SaveVersion::R2018, false));
+    // A drawing opened as a 2000 DWG.
+    let mut d = cadcraft_doc::Drawing::new_metric();
+    d.header.set_str("ACADVER", "AC1015");
+    app.session.open_drawing(d, "plan.dwg", Some("/tmp/plan.dwg".into()));
+    open(&mut app);
+    let st = app.saveas.clone().unwrap();
+    assert_eq!((st.file_type, st.version), (FileType::Dwg, SaveVersion::R2000));
+    assert_eq!(st.suggested_name("plan.dwg"), "plan.dwg");
+    assert_eq!(st.params("/tmp/plan.dwg"), json!({ "path": "/tmp/plan.dwg", "version": "2000" }));
+    // A template from a file stays a template.
+    app.session.open_drawing(cadcraft_doc::Drawing::new_metric(), "base.dwt", Some("/tmp/base.dwt".into()));
+    open(&mut app);
+    assert_eq!(app.saveas.as_ref().map(|s| s.file_type), Some(FileType::Dwt));
+}
+
+/// Choices that don't go together settle (binary is for DXF), images and templates carry no
+/// version, and the chosen extension is always the one written.
+#[test]
+fn choices_settle_and_become_saveas_parameters() {
+    let mut st = SaveAsState { file_type: FileType::Dwg, version: SaveVersion::R2004, binary: true };
+    st.settle();
+    assert!(!st.binary, "binary is for DXF");
+    assert_eq!(st.params("a.dwg"), json!({ "path": "a.dwg", "version": "2004" }));
+    // Images and templates carry no version.
+    let st = SaveAsState { file_type: FileType::Pdf, version: SaveVersion::R2018, binary: true };
+    assert_eq!(st.params("a.pdf"), json!({ "path": "a.pdf" }));
+    let st = SaveAsState { file_type: FileType::Dwt, ..st };
+    assert_eq!(st.params("a.dwt"), json!({ "path": "a.dwt" }));
+    let st = SaveAsState { file_type: FileType::Dxf, version: SaveVersion::R2004, binary: true };
+    assert_eq!(st.params("a.dxf"), json!({ "path": "a.dxf", "version": "2004", "binary": true }));
+    assert_eq!(st.suggested_name("Plan.dwg"), "Plan.dxf");
+    assert_eq!(st.suggested_name(""), "Drawing.dxf");
+    assert_eq!(FileType::from_ext(".DWG"), Some(FileType::Dwg));
+    assert_eq!(with_extension("/tmp/plan", "dxf"), "/tmp/plan.dxf");
+    assert_eq!(with_extension("/tmp/plan.DXF", "dxf"), "/tmp/plan.DXF");
+    assert_eq!(with_extension("/tmp/plan.dwg", "dxf"), "/tmp/plan.dwg.dxf", "the chosen type is the type written");
+    assert!(SaveVersion::ALL.iter().all(|v| version_label(*v).starts_with("AutoCAD ")));
+}
+
+/// Save… offers the picker a name with the chosen extension and keeps the dialog when it is
+/// cancelled; without a picker the command line says how to save.
+#[test]
+fn save_asks_the_picker_with_the_chosen_type_and_keeps_the_dialog_when_cancelled() {
+    let (mut app, asked) = app_with_picker(None);
+    open(&mut app);
+    if let Some(st) = app.saveas.as_mut() {
+        st.file_type = FileType::Svg;
+    }
+    assert_eq!(save(&mut app), Some(false));
+    assert_eq!(*asked.borrow(), vec!["Drawing1.svg".to_string()]);
+    // Without a picker the command line says how to save.
+    let mut app = CadApp::new(Session::new(), Services::default());
+    assert_eq!(save(&mut app), None);
+    assert!(app.session.log.iter().any(|l| l.contains("no file dialog")));
+}
+
+/// QSAVE of a drawing with no file skips the dialog: the close prompt's Save must finish at once.
+#[test]
+fn qsave_of_an_untitled_drawing_goes_straight_to_the_picker() {
+    let (mut app, asked) = app_with_picker(None);
+    app.start("qsave");
+    assert_eq!(*asked.borrow(), vec!["Drawing1.dxf".to_string()]);
+    assert!(app.ui.dialog.is_none());
+}
+
+/// The dialog draws for every file type and keeps the choice across frames.
+#[test]
+fn the_dialog_renders_its_choices() {
+    let (mut app, _) = app_with_picker(None);
+    for ft in FileType::ALL {
+        open(&mut app);
+        if let Some(st) = app.saveas.as_mut() {
+            st.file_type = ft;
+        }
+        let ctx = egui::Context::default();
+        let input =
+            egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 800.0))), ..Default::default() };
+        for _ in 0..2 {
+            let mut output = ctx.run_ui(input.clone(), |ui| app.ui(ui));
+            output.textures_delta.clear();
+        }
+        assert_eq!(app.ui.dialog.as_deref(), Some("saveas"), "{ft:?}");
+        assert_eq!(app.saveas.as_ref().map(|s| s.file_type), Some(ft));
+    }
+}
diff --git a/crates/ui-egui/tests/it/dialog_dispatch.rs b/crates/ui-egui/tests/it/dialog_dispatch.rs
--- a/crates/ui-egui/tests/it/dialog_dispatch.rs
+++ b/crates/ui-egui/tests/it/dialog_dispatch.rs
@@ -39,6 +39,7 @@ fn drafting_settings_menu_opens_the_dialog() {
     assert_eq!(app.session.settings.gridmajor, 10);
 }
 
+/// The Properties menu item shows the Properties palette.
 #[test]
 fn properties_menu_shows_the_palette() {
     let (mut app, _) = app();
@@ -49,6 +50,8 @@ fn properties_menu_shows_the_palette() {
     assert!(app.ui.show_palettes);
 }
 
+/// File commands with JSON parameters never open a picker; typed or from a menu they ask, Save As
+/// with its dialog first and QSAVE of an untitled drawing with the picker straight away.
 #[test]
 fn json_file_commands_never_open_pickers() {
     let (mut app, picks) = app();
@@ -57,14 +60,18 @@ fn json_file_commands_never_open_pickers() {
     }
     app.cmdline("open {}");
     assert_eq!(picks.get(), 0);
-    // Typed, chosen from the menu or the ui.* shortcut commands: the pickers.
+    // Typed, chosen from the menu or the ui.* shortcut commands: the pickers — Save As first
+    // its dialog (file type, version), QSAVE of an untitled drawing the picker straight away.
     for c in ["open", "saveas", "qsave"] {
         app.start(c);
     }
-    assert_eq!(picks.get(), 3);
+    assert_eq!(picks.get(), 2);
+    assert_eq!(app.ui.dialog.as_deref(), Some("saveas"));
+    app.ui.dialog = None;
     app.run("ui.open", json!({})).unwrap();
     app.run("ui.saveas", json!({})).unwrap();
-    assert_eq!(picks.get(), 5);
+    assert_eq!(picks.get(), 3);
+    assert_eq!(app.ui.dialog.as_deref(), Some("saveas"));
 }
 
 #[test]
diff --git a/crates/ui-egui/tests/save_version.rs b/crates/ui-egui/tests/save_version.rs
--- a/crates/ui-egui/tests/save_version.rs
+++ b/crates/ui-egui/tests/save_version.rs
@@ -11,10 +11,13 @@ fn picked() -> std::path::PathBuf {
     std::env::temp_dir().join(format!("cadcraft-ui-save-version-{}.dxf", std::process::id()))
 }
 
+/// A writer that records the version and binary flag it was asked for instead of a drawing.
 fn with_options(_: &Drawing, _: &str, o: &SaveAs) -> Result<Vec<u8>, String> {
     Ok(format!("{}|{}", o.version.map(|v| v.year()).unwrap_or("-"), o.binary).into_bytes())
 }
 
+/// The default save version is offered to untitled drawings by the Save As dialog, written by its
+/// Save… and by an untitled QSAVE (in the default format), and kept in the preferences.
 #[test]
 fn default_save_version_applies_to_new_drawings_and_persists() {
     set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"plain".to_vec()), plot: None });
@@ -25,14 +28,28 @@ fn default_save_version_applies_to_new_drawings_and_persists() {
     assert!(app.run("ui.saveversion", json!({ "version": "2005" })).is_err());
     assert_eq!(app.run("ui.saveversion", json!({ "version": 2010 })).ok(), Some(json!({ "saveVersion": "2010" })));
 
-    // The Save dialog saves an untitled drawing in the default version …
+    // The Save As dialog offers an untitled drawing's default version, and its Save... runs the
+    // picker and saves in it …
     app.run("ui.saveas", Value::Null).unwrap();
+    assert_eq!(app.ui.dialog.as_deref(), Some("saveas"));
+    assert_eq!(cadcraft_ui_egui::saveas::save(&mut app), Some(true));
     assert_eq!(std::fs::read(picked()).unwrap(), b"2010|false");
-    // … and a drawing that has a file in its own.
+    // … and a drawing that has a file starts from its own version.
     app.run("ui.saveas", Value::Null).unwrap();
-    assert_eq!(std::fs::read(picked()).unwrap(), b"plain");
+    assert_eq!(cadcraft_ui_egui::saveas::save(&mut app), Some(true));
+    assert_eq!(std::fs::read(picked()).unwrap(), b"2010|false");
     let _ = std::fs::remove_file(picked());
 
+    // QSAVE of an untitled drawing (the picker alone): a name picked with another extension
+    // gets the default format's, so DXF is what is written.
+    let dwg = std::env::temp_dir().join(format!("cadcraft-ui-save-version-{}.dwg", std::process::id()));
+    let services = Services { pick_save: Some(Box::new(move |_: &str| Some(dwg.to_string_lossy().to_string()))), ..Services::default() };
+    let mut untitled = CadApp::new(Session::new(), services);
+    untitled.start("qsave");
+    let written = std::env::temp_dir().join(format!("cadcraft-ui-save-version-{}.dwg.dxf", std::process::id()));
+    assert!(written.exists(), "saved as {written:?}");
+    let _ = std::fs::remove_file(&written);
+
     // Menu items, preferences (bad values ignored) and resetting the palettes.
     app.start("ui.saveversion.2004");
     assert_eq!(app.ui.save_version, "2004");
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where CADCraft falls short of AutoCAD
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (DIMTFILL limits in the `*D` block, EXPLODE and DXF: F7 updated) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (the Save As dialog picks the version per file: FF3 updated) · **Target:** Autodesk AutoCAD 2027
 
 Every known shortfall, one row each, ranked within each section by user impact. This is the work
 list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -75,7 +75,7 @@ Detail: [file-format-parity.md](file-format-parity.md).
 |---|---|---|---|
 | FF1 | **[alpha blocker]** Round-trip what we don't model. DXF done (#528): unknown entities and objects (with their CLASS records), foreign xdata (hyperlinks included), extension dictionaries, persistent reactors, DICTIONARYVAR and XRECORDs, layer states, colour book colours, references remapped on save; IMAGE, VIEW/UCS, groups, block descriptions, true colours, transparency (#201, #204, #208, #209, #213, #223); WIPEOUT and gradients (#147, #162). Left: through DWG, acadrust drops custom-class entities and objects, xdata on table records and blocks, entity colour book names and reactors; UCS elevation; MTEXT background colours; unknown entities follow only layer edits, and renamed layers and blocks lose their foreign data | High | 6–10 |
 | FF2 | Multileader leftovers (native MULTILEADER read/write landed, #459): several landings per multileader (extra leader roots are folded into the lines), per-object overrides (leader colour and linetype, arrowhead symbol, text frame), annotative scale contexts; check the text placement of our MULTILEADER output in AutoCAD | Medium | 3–5 |
-| FF3 | Save-version leftovers (DXF/DWG 2000–2018, binary DXF, SAVEAS `version`/`binary` and the default save version landed, #421): no R14 DWG or R12 DXF output; the Save dialog can't pick the version per file (the file picker doesn't report the chosen type; File > Default Save Format sets it); no command-line SAVEAS format prompt | Low | 2–4 |
+| FF3 | Save-version leftovers (DXF/DWG 2000–2018, binary DXF, SAVEAS `version`/`binary` and the default save version landed, #421; the Save As dialog picks the file type, version and binary DXF per file before the picker): no R14 DWG or R12 DXF output; no command-line SAVEAS format prompt | Low | 1–3 |
 | FF4 | Code-page leftovers (pre-2007 DXF read in their `$DWGCODEPAGE` landed, #505): Johab (ANSI_1361) and DOS 855/857/860–865/869 read as ANSI_1252; saving declares `ANSI_1252` instead of the file's code page (the text is pure ASCII, so still correct); acadrust approximates DOS code pages in pre-2007 DWG (DOS437 as 866, DOS850 as 1252) | Low | 1–2 |
 | FF5 | **[alpha blocker]** Real-file corpus (`storytold/cadcraft-corpus`), open → save → reopen tests, black-box check in AutoCAD | High | 10–15 + human |
 | FF6 | **[alpha blocker]** DWG throughput: a 10 MB DWG took ~2 min to fail (#57) | High for real projects | 6–10 |
@@ -150,6 +150,7 @@ Detail: [localization-parity.md](localization-parity.md).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | Save As is a dialog: file type (DXF, DWG, DWT, SVG, PNG, PDF), version and binary DXF chosen per file before the file picker, which can't report them: FF3 updated |
 | 2026-10-25 | minor | DIMTFILL limits: with DIMTFILL 1 (the drawing's background color) the written `*D` block and EXPLODE keep no background mask (a WIPEOUT over the text box would), and a true-color DIMTFILLCLR is written to DXF as the nearest ACI: F7 updated |
 | 2026-10-24 | minor | COMPAREEXPORT (typed, or Export on the comparison bar) opens the comparison as a new untitled drawing, `compare_<this> vs <other>.dwg`: this drawing's objects in one group and the other drawing's differing objects in a second group, in the comparison colors (green, red, gray for objects in both), block contents and style colors included: F22 narrowed |
 | 2026-10-24 | minor | DIMTFILL and DIMTFILLCLR: a dimension's text background (none, the drawing's background color, or a color) hides what is drawn under the text; SETVAR, DIMOVERRIDE and style fields, DIMSTYLE groups 69 and 70 in DXF from R2007 (CADCraft xdata before), and a SOLID in the `*D` block and from EXPLODE for DIMTFILL 2: F7 narrowed |
