diff --git a/crates/engine/src/cmd/blocks/bedit.rs b/crates/engine/src/cmd/blocks/bedit.rs
--- a/crates/engine/src/cmd/blocks/bedit.rs
+++ b/crates/engine/src/cmd/blocks/bedit.rs
@@ -2,6 +2,8 @@
 //! drawing's own tab, its base point at the origin; the block's objects are edited with the
 //! ordinary commands; BSAVE saves them into the definition, so every reference to the block in the
 //! drawing shows the change; BCLOSE goes back to the drawing, saving or discarding what isn't saved.
+//! BEDIT while the Block Editor is open switches it to another block: changes not saved yet are
+//! saved or discarded first (asked at the command line; `save` in JSON).
 //! The state is [`crate::blockedit`].
 
 use cadcraft_doc::{Drawing, EntityKind, Handle};
@@ -16,10 +18,9 @@ pub(super) fn specs() -> Vec<CommandSpec> {
         CommandSpec::new("bedit", "Block Editor", run_bedit)
             .menu(&["Tools", "Block Editor"])
             .alias(&["be", "-bedit"])
-            .params("{name? (\"<Current Drawing>\": the drawing itself, a dynamic block's own drawing) | handle? (a block reference: edits its block); default the one selected block reference, create?: bool (a name no block has yet starts a new block; BSAVE creates it)} → {block, new}")
-            .enabled(not_editing)
+            .params("{name? (\"<Current Drawing>\": the drawing itself, a dynamic block's own drawing) | handle? (a block reference: edits its block); default the one selected block reference, create?: bool (a name no block has yet starts a new block; BSAVE creates it), save?: bool (in the Block Editor, switching to another block: save (true) or discard (false) the changes not saved yet; required when there are some)} → {block, new, saved?}")
             .noundo()
-            .interactive(|_| Ok(Box::new(BeditM))),
+            .interactive(|_| Ok(Box::new(BeditM::default()))),
         CommandSpec::new("bsave", "Save Block", run_bsave).params("{} → {block}").enabled(editing).noundo(),
         CommandSpec::new("bsaveas", "Save Block As", run_bsaveas)
             .params("{name, replace?: bool (redefine an existing block of that name)} → {block}")
@@ -53,16 +54,6 @@ fn editing_a_block(s: &Session) -> std::result::Result<(), String> {
     }
 }
 
-fn not_editing(s: &Session) -> std::result::Result<(), String> {
-    match s.state() {
-        Ok(st) => match st.block_editor() {
-            Some(name) => Err(format!("The Block Editor is already open on \"{name}\"; close it first (BCLOSE).")),
-            None => Ok(()),
-        },
-        Err(e) => Err(e.to_string()),
-    }
-}
-
 /// Commands that leave model space, which the Block Editor is: not available while it is open.
 const NOT_IN_EDITOR: &[&str] = &[
     "layout",
@@ -106,8 +97,50 @@ fn dynamic_definition(d: &Drawing, name: &str) -> Option<String> {
     (kind == "BLOCK_RECORD").then(|| d.block(block).map(|b| b.name.clone())).flatten()
 }
 
-/// Open the Block Editor on block `name` and frame its objects.
-fn open(s: &mut Session, name: &str, create: bool) -> Result<Value> {
+/// Open the Block Editor on block `name` and frame its objects. While it is open on another
+/// block it switches: the changes not saved yet are saved first with `save` Some(true),
+/// discarded with Some(false); with None there must be none. The block to switch to is checked
+/// before anything is saved or closed, so a refused switch leaves the editor as it was.
+fn open(s: &mut Session, name: &str, create: bool, save: Option<bool>) -> Result<Value> {
+    let mut saved = None;
+    if let Some(current) = s.state()?.block_editor().map(str::to_string) {
+        if current.eq_ignore_ascii_case(name.trim()) {
+            s.echo(format!("The Block Editor is already editing \"{current}\"."));
+            return Ok(json!({ "block": current, "new": false }));
+        }
+        let host = s.state()?.block_edit.as_ref().map(|b| b.host.doc.clone()).ok_or_else(|| bad("bedit", "the Block Editor is not open"))?;
+        if !name.trim().eq_ignore_ascii_case(crate::blockedit::CURRENT_DRAWING) {
+            match host.block(name) {
+                None if !create => return Err(bad("bedit", format!("Block \"{name}\" not found."))),
+                None => {}
+                Some(_) => {
+                    if let Some(why) = crate::blockedit::refusal(&host, name) {
+                        return Err(bad("bedit", why));
+                    }
+                }
+            }
+        }
+        let save = match (s.state()?.block_unsaved(), save) {
+            (false, _) => false,
+            (true, Some(save)) => save,
+            (true, None) => {
+                return Err(bad(
+                    "bedit",
+                    format!("Block \"{current}\" has changes not saved; pass `save: true` to save them or `save: false` to discard them"),
+                ));
+            }
+        };
+        saved = close(s, save)?.get("saved").cloned();
+    }
+    let mut r = open_closed(s, name, create)?;
+    if let (Some(v), Some(o)) = (saved, r.as_object_mut()) {
+        o.insert("saved".into(), v);
+    }
+    Ok(r)
+}
+
+/// [`open`] with the Block Editor closed.
+fn open_closed(s: &mut Session, name: &str, create: bool) -> Result<Value> {
     let new = create && !name.eq_ignore_ascii_case(crate::blockedit::CURRENT_DRAWING) && s.doc()?.block(name).is_none();
     s.state_mut()?.open_block_editor(name, create)?;
     let name = s.state()?.block_editor().unwrap_or(name).to_string();
@@ -153,7 +186,10 @@ fn run_bedit(s: &mut Session, p: &Value) -> Result<Value> {
     if create && !current && s.doc()?.block(name).is_none() && !super::valid_block_name(name) {
         return Err(bad("bedit", format!("\"{name}\" is not a valid block name")));
     }
-    open(s, name, create)
+    if p.get("save").is_some_and(|v| !v.is_null() && !v.is_boolean()) {
+        return Err(bad("bedit", "`save` must be true or false"));
+    }
+    open(s, name, create, p.get("save").and_then(Value::as_bool))
 }
 
 fn run_bsave(s: &mut Session, _p: &Value) -> Result<Value> {
@@ -206,7 +242,25 @@ fn run_bclose(s: &mut Session, p: &Value) -> Result<Value> {
 // ---------------- interactive ----------------
 
 /// BEDIT: the selected block reference's block, else a block name typed or a reference picked.
-struct BeditM;
+/// In the Block Editor with changes not saved yet, it asks whether to save them before switching.
+#[derive(Default)]
+struct BeditM {
+    /// The block to switch to, waiting for the answer to "Save changes?".
+    pending: Option<String>,
+}
+
+impl BeditM {
+    /// Go to block `name`: at once, or after asking about the changes not saved yet.
+    fn go(&mut self, s: &mut Session, name: &str) -> Result<Step> {
+        let st = s.state()?;
+        let other = st.block_editor().is_some_and(|b| !b.eq_ignore_ascii_case(name.trim()));
+        if other && st.block_unsaved() {
+            self.pending = Some(name.trim().to_string());
+            return Ok(Step::Continue);
+        }
+        open(s, name, false, Some(false)).map(|_| Step::Done)
+    }
+}
 
 impl Interactive for BeditM {
     fn name(&self) -> &'static str {
@@ -214,14 +268,30 @@ impl Interactive for BeditM {
     }
     fn begin(&mut self, s: &mut Session) -> Result<Step> {
         match selected_reference(s) {
-            Some(name) => open(s, &name, false).map(|_| Step::Done),
+            Some(name) => self.go(s, &name),
             None => Ok(Step::Continue),
         }
     }
-    fn prompt(&self, _s: &Session) -> Prompt {
+    fn prompt(&self, s: &Session) -> Prompt {
+        if self.pending.is_some() {
+            let name = s.state().ok().and_then(|st| st.block_editor().map(str::to_string)).unwrap_or_default();
+            return Prompt::new(format!("Save changes to block \"{name}\"?"), KW).kw(&["Yes", "No"]).default("Y");
+        }
         Prompt::new("Enter block name or select a block reference [?]", Accept { point: true, number: false, text: true, select: true, enter: true })
     }
     fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
+        if let Some(name) = self.pending.clone() {
+            let answer = match i {
+                Input::Enter => "Y".to_string(),
+                Input::Keyword(k) | Input::Text(k) => k,
+                _ => return Ok(Step::Continue),
+            };
+            return match answer.trim().chars().next().map(|c| c.to_ascii_uppercase()) {
+                Some('Y') => open(s, &name, false, Some(true)).map(|_| Step::Done),
+                Some('N') => open(s, &name, false, Some(false)).map(|_| Step::Done),
+                _ => Err(EngineError::Other("Enter Yes or No.".into())),
+            };
+        }
         match i {
             Input::Text(t) if t.trim() == "?" => {
                 let names: Vec<String> = {
@@ -231,14 +301,14 @@ impl Interactive for BeditM {
                 s.echo(if names.is_empty() { "No blocks to edit.".to_string() } else { names.join(", ") });
                 Ok(Step::Continue)
             }
-            Input::Text(t) | Input::Keyword(t) => open(s, t.trim(), false).map(|_| Step::Done),
+            Input::Text(t) | Input::Keyword(t) => self.go(s, t.trim()),
             Input::Pick(hs) => {
                 let name = {
                     let d = s.doc()?;
                     hs.iter().find_map(|h| block_of(d, *h))
                 };
                 match name {
-                    Some(n) => open(s, &n, false).map(|_| Step::Done),
+                    Some(n) => self.go(s, &n),
                     None => Err(EngineError::Other("Select a block reference.".into())),
                 }
             }
diff --git a/crates/engine/tests/block_editor.rs b/crates/engine/tests/block_editor.rs
--- a/crates/engine/tests/block_editor.rs
+++ b/crates/engine/tests/block_editor.rs
@@ -140,7 +140,8 @@ fn what_the_block_editor_refuses() {
     assert!(s.execute("bclose", &json!({})).is_err(), "not open");
 
     s.execute("bedit", &json!({ "name": "Door" })).unwrap();
-    assert!(s.execute("bedit", &json!({ "name": "Door" })).is_err(), "already open");
+    s.execute("bedit", &json!({ "name": "DOOR" })).unwrap();
+    assert_eq!(editor(&s).as_deref(), Some("Door"), "the block already open stays open");
     let r = s.execute("layout.set", &json!({ "name": "Layout1" }));
     assert!(r.is_err_and(|e| e.to_string().contains("Block Editor")));
     assert_eq!(s.layout_space(), cadcraft_engine::doc::Space::Model);
@@ -370,3 +371,102 @@ fn the_current_drawing_opens_in_the_block_editor() {
     s.undo().unwrap();
     assert_eq!(s.doc().unwrap().model.len(), 1);
 }
+
+/// BEDIT in the Block Editor switches to another block, as AutoCAD's does: changes not saved yet
+/// are saved or discarded first (`save`), and a block it can't open leaves the editor as it was.
+#[test]
+fn bedit_in_the_block_editor_switches_blocks() {
+    let mut s = Session::new();
+    door(&mut s);
+    let l = line(&mut s, [0.0, 50.0], [5.0, 50.0]);
+    s.execute("block", &json!({ "name": "Frame", "base": [0, 50], "handles": [l] })).unwrap();
+    let model = s.doc().unwrap().model.len();
+
+    s.execute("bedit", &json!({ "name": "Door" })).unwrap();
+    line(&mut s, [0.0, 0.0], [0.0, 3.0]);
+    // Changes not saved: JSON must say what to do with them.
+    let e = s.execute("bedit", &json!({ "name": "Frame" })).unwrap_err().to_string();
+    assert!(e.contains("save"), "{e}");
+    assert_eq!(editor(&s).as_deref(), Some("Door"));
+    assert!(s.state().unwrap().block_unsaved(), "nothing was lost");
+    // A block that isn't there, or `save` that isn't a bool: refused, nothing changes.
+    assert!(s.execute("bedit", &json!({ "name": "Nope", "save": true })).is_err());
+    assert!(s.execute("bedit", &json!({ "name": "Frame", "save": "yes" })).is_err());
+    assert_eq!(editor(&s).as_deref(), Some("Door"));
+    assert!(s.state().unwrap().block_unsaved());
+
+    // Saved, then switched.
+    let r = s.execute("bedit", &json!({ "name": "frame", "save": true })).unwrap();
+    assert_eq!((r["block"].as_str(), r["saved"].as_bool()), (Some("Frame"), Some(true)));
+    assert_eq!(editor(&s).as_deref(), Some("Frame"));
+    assert_eq!(lines_of(&s, "Door").len(), 2, "Door keeps the new line");
+
+    // Discarded, then switched; with nothing to save `save` isn't needed.
+    line(&mut s, [1.0, 1.0], [2.0, 2.0]);
+    s.execute("bedit", &json!({ "name": "Door", "save": false })).unwrap();
+    assert_eq!(lines_of(&s, "Frame").len(), 1, "Frame's change was discarded");
+    s.execute("bedit", &json!({ "name": "Frame" })).unwrap();
+    assert_eq!(editor(&s).as_deref(), Some("Frame"));
+    // A new block, and the drawing itself.
+    s.execute("bedit", &json!({ "name": "Gate", "create": true })).unwrap();
+    assert_eq!(editor(&s).as_deref(), Some("Gate"));
+    s.execute("bedit", &json!({ "name": "<Current Drawing>" })).unwrap();
+    s.execute("bclose", &json!({ "save": false })).unwrap();
+    assert!(editor(&s).is_none());
+    assert_eq!(s.doc().unwrap().model.len(), model, "the drawing as it was");
+}
+
+/// Typed, BEDIT in the Block Editor asks whether to save the changes before it switches.
+#[test]
+fn bedit_typed_in_the_block_editor_asks_to_save_first() {
+    let mut s = Session::new();
+    door(&mut s);
+    let l = line(&mut s, [0.0, 50.0], [5.0, 50.0]);
+    s.execute("block", &json!({ "name": "Frame", "base": [0, 50], "handles": [l] })).unwrap();
+    s.execute("bedit", &json!({ "name": "Door" })).unwrap();
+
+    // Nothing changed: it switches at once.
+    s.script(
+        "BEDIT
+Frame
+",
+    )
+    .unwrap();
+    assert!(s.running.is_none());
+    assert_eq!(editor(&s).as_deref(), Some("Frame"));
+
+    line(&mut s, [1.0, 1.0], [2.0, 2.0]);
+    s.script(
+        "BEDIT
+Door
+",
+    )
+    .unwrap();
+    assert_eq!(s.current_prompt().unwrap().message, "Save changes to block \"Frame\"?");
+    // Escape keeps the editor on Frame with its change.
+    s.input(cadcraft_engine::Input::Cancel).unwrap();
+    assert_eq!(editor(&s).as_deref(), Some("Frame"));
+    assert!(s.state().unwrap().block_unsaved());
+
+    s.script(
+        "BEDIT
+Door
+Y
+",
+    )
+    .unwrap();
+    assert!(s.running.is_none());
+    assert_eq!(editor(&s).as_deref(), Some("Door"));
+    assert_eq!(lines_of(&s, "Frame").len(), 2, "saved before switching");
+
+    line(&mut s, [3.0, 3.0], [4.0, 4.0]);
+    s.script(
+        "BEDIT
+Frame
+N
+",
+    )
+    .unwrap();
+    assert_eq!(editor(&s).as_deref(), Some("Frame"));
+    assert_eq!(lines_of(&s, "Door").len(), 1, "discarded");
+}
diff --git a/crates/ui-egui/src/bedit.rs b/crates/ui-egui/src/bedit.rs
--- a/crates/ui-egui/src/bedit.rs
+++ b/crates/ui-egui/src/bedit.rs
@@ -1,7 +1,9 @@
 //! The Edit Block Definition dialog: BEDIT typed or started from a menu or the Tool Sets with no
 //! block reference selected. It lists the drawing's blocks with a preview and description; a name
 //! no block has yet starts a new block. OK runs BEDIT with that name; the dialog itself never
-//! changes the drawing. JSON calls (`bedit {name}`) and a selected block reference skip it.
+//! changes the drawing. In the Block Editor it switches to the block chosen, first asking whether
+//! to save the changes not saved yet. JSON calls (`bedit {name}`) and a selected block reference
+//! skip it.
 
 use std::sync::Arc;
 
@@ -24,6 +26,8 @@ pub struct State {
     pub name: String,
     /// Why the last OK failed, shown until the name changes.
     pub error: Option<String>,
+    /// OK in the Block Editor with changes not saved: asking whether to save them first.
+    pub ask_save: bool,
     /// The preview of the block named, for (drawing uid, revision, block name).
     preview: Option<((u64, u64, String), Arc<DisplayList>)>,
 }
@@ -86,22 +90,19 @@ pub fn authoring_dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool)
 }
 
 /// BEDIT typed or from a menu without parameters opens the dialog, unless exactly one block
-/// reference is selected (it opens at once) or the Block Editor is open (BEDIT says why not).
+/// reference is selected (it opens at once). In the Block Editor it starts on the block edited.
 pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
     if !matches!(id, "bedit" | "be") || !params.is_null() {
         return None;
     }
     let st = app.session.state().ok()?;
-    if st.block_edit.is_some() {
-        return None;
-    }
     let refs = st.selection.iter().filter(|h| st.doc.entity(**h).is_some_and(|e| matches!(e.kind, EntityKind::Insert(_)))).count();
     if refs == 1 && st.selection.len() == 1 {
         return None;
     }
     // Start on the block last edited if it is still there, else the first one.
     let names = editable_blocks(&st.doc);
-    let last = app.bedit.name.clone();
+    let last = st.block_editor().map(str::to_string).unwrap_or_else(|| app.bedit.name.clone());
     let name = if names.iter().any(|n| n.eq_ignore_ascii_case(&last)) { last } else { names.first().cloned().unwrap_or_default() };
     app.bedit = State { name, ..State::default() };
     app.ui.dialog = Some("bedit".into());
@@ -164,6 +165,10 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
     let list = existing.as_deref().and_then(|n| preview(app, n));
     let description = existing.as_deref().and_then(|n| d.block(n)).map(|b| b.description.clone()).unwrap_or_default();
     let (mut ok, mut cancel) = (false, false);
+    if app.bedit.ask_save {
+        save_question(app, ctx, open);
+        return;
+    }
     egui::Window::new(crate::tl!("Edit Block Definition"))
         .id(egui::Id::unique("Edit Block Definition"))
         .open(open)
@@ -236,14 +241,67 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
     }
 }
 
-/// OK: open the Block Editor on the name given (a new name starts a new block). Returns whether
-/// it opened; if not, the dialog shows why and stays open.
+/// In the Block Editor with changes not saved: whether to save them before switching to the
+/// block chosen. Yes saves, No discards, Cancel goes back to the list.
+fn save_question(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
+    let current = app.session.state().ok().and_then(|st| st.block_editor().map(str::to_string)).unwrap_or_default();
+    let mut answer: Option<Option<bool>> = None;
+    egui::Window::new(crate::tl!("Edit Block Definition"))
+        .id(egui::Id::unique("Edit Block Definition"))
+        .collapsible(false)
+        .resizable(false)
+        .pivot(egui::Align2::CENTER_CENTER)
+        .default_pos(ctx.content_rect().center())
+        .show(ctx, |ui| {
+            ui.set_max_width(420.0);
+            ui.label(crate::tf!("Save changes to block \"{name}\"?", name = current));
+            ui.add_space(8.0);
+            ui.horizontal(|ui| {
+                if ui.button(crate::tl!("Yes")).clicked() {
+                    answer = Some(Some(true));
+                }
+                if ui.button(crate::tl!("No")).clicked() {
+                    answer = Some(Some(false));
+                }
+                if ui.button(crate::tl!("Cancel")).clicked() {
+                    answer = Some(None);
+                }
+            });
+        });
+    let Some(answer) = answer else { return };
+    app.bedit.ask_save = false;
+    if let Some(save) = answer
+        && run(app, Some(save))
+    {
+        *open = false;
+    }
+}
+
+/// OK: open the Block Editor on the name given (a new name starts a new block). In the Block
+/// Editor with changes not saved, it asks first whether to save them. Returns whether it opened;
+/// if not, the dialog shows why (or asks) and stays open.
 pub fn confirm(app: &mut CadApp) -> bool {
     let name = app.bedit.name.trim().to_string();
     if name.is_empty() {
         return false;
     }
-    match app.run("bedit", json!({ "name": name, "create": true })) {
+    let ask = app.session.state().is_ok_and(|st| st.block_unsaved() && st.block_editor().is_some_and(|b| !b.eq_ignore_ascii_case(&name)));
+    if ask {
+        app.bedit.ask_save = true;
+        return false;
+    }
+    run(app, None)
+}
+
+/// Run BEDIT on the name given, saving (`Some(true)`) or discarding (`Some(false)`) the Block
+/// Editor's changes not saved yet. Returns whether it went through; if not, the dialog shows why.
+fn run(app: &mut CadApp, save: Option<bool>) -> bool {
+    let name = app.bedit.name.trim().to_string();
+    let mut p = json!({ "name": name, "create": true });
+    if let (Some(save), Some(o)) = (save, p.as_object_mut()) {
+        o.insert("save".into(), json!(save));
+    }
+    match app.run("bedit", p) {
         Ok(_) => true,
         Err(e) => {
             app.bedit.error = Some(e);
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,4 @@
 	Check for Updates…	Перевірити оновлення…
 	CADCraft is up to date.	Встановлено найновішу версію CADCraft.
 	Could not check for updates.	Не вдалося перевірити оновлення.
+	Save changes to block "{name}"?	Зберегти зміни в блоці «{name}»?
diff --git a/crates/ui-egui/tests/bedit_dialog.rs b/crates/ui-egui/tests/bedit_dialog.rs
--- a/crates/ui-egui/tests/bedit_dialog.rs
+++ b/crates/ui-egui/tests/bedit_dialog.rs
@@ -95,9 +95,10 @@ fn a_selected_reference_and_json_calls_skip_the_dialog() {
     app.start("bedit");
     assert!(app.ui.dialog.is_none());
     assert_eq!(app.session.state().unwrap().block_editor(), Some("Window"));
-    // In the editor BEDIT says why it can't run instead of opening the dialog.
+    // In the editor BEDIT opens the dialog, to switch to another block.
     app.start("bedit");
-    assert!(app.ui.dialog.is_none());
+    assert_eq!(app.ui.dialog.as_deref(), Some("bedit"));
+    app.ui.dialog = None;
     app.run("bclose", json!({})).unwrap();
 
     app.run("bedit", json!({ "name": "door" })).unwrap();
@@ -175,3 +176,50 @@ fn opening_a_drawing_with_authoring_elements_offers_the_block_editor() {
     assert!(app.ui.dialog.is_none());
     let _ = std::fs::remove_dir_all(&dir);
 }
+
+/// In the Block Editor, BEDIT opens the dialog on the block being edited; OK on another block
+/// switches to it, first asking whether to save the changes not saved yet (Cancel goes back).
+#[test]
+fn in_the_block_editor_the_dialog_switches_blocks_asking_to_save_first() {
+    use egui_kittest::Harness;
+    use egui_kittest::kittest::Queryable;
+
+    let mut app = app_with_blocks();
+    app.run("bedit", json!({ "name": "door" })).unwrap();
+    app.start("bedit");
+    assert_eq!((app.ui.dialog.as_deref(), app.bedit.name.as_str()), (Some("bedit"), "door"), "starts on the block edited");
+    // Nothing changed: OK switches at once.
+    app.bedit.name = "Window".into();
+    assert!(bedit::confirm(&mut app));
+    assert_eq!(app.session.state().unwrap().block_editor(), Some("Window"));
+
+    app.run("line", json!({ "points": [[0, 0], [0, 4]] })).unwrap();
+    app.start("bedit");
+    app.bedit.name = "door".into();
+    assert!(!bedit::confirm(&mut app), "asks first");
+    assert!(app.bedit.ask_save);
+    let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(ui.ctx());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(2);
+    assert!(h.query_by_label_contains("Save changes to block \"Window\"").is_some());
+    h.get_by_label("Cancel").click();
+    h.run_steps(2);
+    assert!(!h.state().bedit.ask_save, "back to the list");
+    assert_eq!(h.state().session.state().unwrap().block_editor(), Some("Window"));
+    assert!(h.state().session.state().unwrap().block_unsaved(), "nothing lost");
+
+    assert!(!bedit::confirm(h.state_mut()));
+    h.run_steps(2);
+    h.get_by_label("Yes").click();
+    h.run_steps(2);
+    let st = h.state().session.state().unwrap();
+    assert_eq!(st.block_editor(), Some("door"));
+    assert!(h.state().ui.dialog.is_none(), "the dialog closed");
+    let window = h.state().session.doc().unwrap().block("Window").map(|b| b.entities.len());
+    assert_eq!(window, Some(2), "Window saved with its new line before switching");
+}
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-26 · **Change:** minor (BEDIT in the Block Editor switches blocks: F3 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
@@ -26,7 +26,7 @@ do them first.
 |---|---|---|---|---|
 | F1 | **[alpha blocker]** Prompt options that still print "not available yet": -ATTEDIT one-at-a-time Position, Height, Angle and Style, colour books in -ATTEDIT Color, -RENAME of materials, plot styles and viewport configurations. SPLINEDIT Join, Edit vertex Add and Elevate order (#559), PEDIT Edit vertex and the other SPLINEDIT options (#558) done: Edit vertex Tangent directions last for the PEDIT session only (LWPOLYLINE stores none). MTEXT Dynamic/Static columns done (#547). SPLINE, MTEXT and MLEADER options done (#408): SPLINE Object fits the polyline's vertices (no spline frame is kept) (STRETCH polygon, CP and WP, and fence selection done; TRIM/EXTEND, PLINE arc mode and Length, XLINE done (#397); OFFSET, ROTATE and FILLET options done earlier) | `cmd/blocks/attedit.rs`, `cmd/props/rename.rs` | Low: the most-used commands are done | 1–3 |
 | F2 | XREF editing in place and clipping: XCLIP is missing. REFEDIT, REFSET and REFCLOSE edit a block reference in place (working set, faded rest of the block, everything else locked, save or discard; #813); left: editing an xref in place (its source drawing is not written back; XOPEN opens it), picking nested references, a dimmed look for the locked drawing and a banner with Save and Discard, attribute sync of other references (ATTSYNC). XATTACH, the External References palette, -XREF (Bind/Detach/Path/Unload/Reload/Overlay), XBIND, XOPEN, overlays, relative paths and loading at open are done (#525). Unloaded state and xref-layer overrides last only for the session | `cmd/blocks/xref.rs` | Medium: xrefs can't be edited in place or clipped | 15–25 |
-| F3 | **[alpha blocker]** (dynamic-block authoring is beta) No dynamic-block authoring: parameters and actions can't be added in the Block Editor. Dynamic blocks from other files are read and their references edited (visibility, distances, stretch and move, lookups, grips). BEDIT (with the Edit Block Definition dialog, which also starts new blocks), BSAVE, BSAVEAS and BCLOSE edit a static block definition, and every reference updates; REFEDIT edits a reference in place; no authoring palettes yet | issue #185 offers help | High: standard libraries are dynamic | 40–65 |
+| F3 | **[alpha blocker]** (dynamic-block authoring is beta) No dynamic-block authoring: parameters and actions can't be added in the Block Editor. Dynamic blocks from other files are read and their references edited (visibility, distances, stretch and move, lookups, grips). BEDIT (with the Edit Block Definition dialog, which also starts new blocks and, in the Block Editor, switches to another block after asking to save), BSAVE, BSAVEAS and BCLOSE edit a static block definition, and every reference updates; REFEDIT edits a reference in place; no authoring palettes yet | issue #185 offers help | High: standard libraries are dynamic | 40–65 |
 | F4 | Autosave leftovers: no autosave in the web build (browser storage); autosave files are DXF, not DWG; no separate RECOVER mode in the file readers (SAVETIME, Drawing Recovery, AUDIT and RECOVER landed with #406; AUDITCTL `.adt` log landed) | `engine/src/autosave.rs`, `engine/src/audit.rs` | Low-medium (web users) | 2–4 |
 | F5 | Associative arrays (ASsociative, Levels, polar ROWs, ARRAYEDIT, ARRAYCLOSE) landed (#448); left: grip editing of arrays (count/spacing grips), ARRAYPATH is not associative, ARRAYEDIT has no item deletion (CTRL+click) and no Key point base points, levels only shift Z (no 3D view) | `cmd/array.rs`, `cmd/array/edit.rs` | Medium | 4–6 |
 | F7 | Dimensions: no DIMSTYLE manager dialog; no smart DIM; DIMALTTD/DIMALTTZ, DIMUPT and DIMTXTDIRECTION not modelled; jog height and break size not style settings; DIMBREAK breaks don't follow moved objects; with DIMTFILL 1 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 (style variables, DIMJOGGED, DIMJOGLINE, DIMBREAK, DIMEDIT, DIMINSPECT and QDIM modes done, #507; the DIMTFILL/DIMTFILLCLR text background done) | `doc/src/tables.rs`, `cmd/annotate_dimedit.rs`, `cmd/annotate_qdim.rs` | Medium | 10–15 |
@@ -151,6 +151,7 @@ Detail: [localization-parity.md](localization-parity.md).
 | Date | Change | Summary |
 |---|---|---|
 | 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-26 | minor | BEDIT in the Block Editor (typed, Edit Block, or the Edit Block Definition dialog) switches to another block as AutoCAD's does, asking whether to save the changes not saved yet; the JSON form takes `save`: F3 evidence |
 | 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 |
 | 2026-10-22 | minor | APPLOAD typed or from Tools ▸ Load Application opens a `.lsp` file browser starting in the last-used folder (kept in the preferences); a picked file loads interactively like a typed name (routines that ask for input wait), the web says to drop the file (no file system), and the JSON form `{path}` reads the path as given and returns the commands the file defined, never opening a dialog (#1820): E1 narrowed |
