app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.11.1 source
19 files changed
+877−40
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+38−1SKILL.md+5bridge.json+1−1install.shpackage.json+1−1src/comhost/com.rs+21src/comhost/mock.rs+13src/dialogs.rs+247src/launchos.rs+136src/lib.rs+1src/quit.rs+52−3src/verbs.rs+73−27src/verbs3.rs+61tests/dialogs.rs+219tests/e2e.rs+6−4uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.11.0+0.11.1
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.11.0"+version = "0.11.1" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.11.0"+version = "0.11.1" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+38−1@@ -506,7 +506,44 @@ not. ## Roadmap -- v0.11.0 (this): **CSWA phase 1 — exam parts as release validation.** Drew:+- v0.11.1 (this): **which verb put that dialog up?** Measured 2026-10-15 on+ adom_drew2:+ - a File ▸ Open dialog appeared during a bridge battery, with Drew away;+ - it was `"Open "`, `#32770`, owned by the SOLIDWORKS main frame, with the+ file name empty;+ - every `swx_stop` after it was `stop_blocked_by_dialog`;+ - the census called it `kind:"unknown"`, `bridgeCaused:false`, and+ `swx_state` once reported `blockingDialogs: []` while it was up.++ Now:+ - **Every verb is bracketed by a dialog census.** One census runs before the+ verb (after it is marked in flight), and one after. A dialog proved absent+ in the first and present in the second appeared DURING that verb.+ - Replies carry `dialogsAppeared`, and the ledger records `dialog_appeared`.+ - Sightings persist (`dialog-sightings.json`) with first-seen and+ last-proved-absent times and the user's last input.+ - Anything first seen outside a bracket stays unattributed.+ - **The census is native and deterministic.** It covers every top-level+ window of SOLIDWORKS and its CEF children: every `#32770`, plus visible+ owned popups. It runs on every `swx_state` call, `screenshot:false`+ included (that path used to skip the census and say `[]`). ab's list is+ only the fallback. No visible modal is dropped: the quit preconditions+ report `moreBlockingDialogs`.+ - **`swx_stop`'s refusal names the dialog:** title, hwnd, class, owner, rect,+ `ageSeconds`, attribution, and whether the bridge may cancel it.+ - **The one dialog the bridge may cancel:** the standard Windows file dialog+ (file-name control 1148/1152 and IDCANCEL), with an EMPTY name read by+ WM_GETTEXT. A bridge verb must provably have raised it: proved absent after+ the verb began, and no user input since. `swx_dismiss_blocking_dialogs+ {close, confirm, reason}` sends IDCANCEL, a no-op, and ledgers+ `dialog_cancelled` with the attribution. Anything with text typed, or of+ unknown origin, is the user's.+ - **Quit rung (b) reads `swCommands_App_Exit` from the installed interop**+ and does not send `RunCommand` unless it reads 2825. A wrong command id+ could raise any dialog.++ 681 tests.+- v0.11.0: **CSWA phase 1 — exam parts as release validation.** Drew: "pull 3-5 random parts from CSWA exams as part of our e2e testing/validation for each release … We need this bridge to be a real, useable tool, for more than the basics." Every new op is built and simulated, and NOT YET MEASURED
SKILL.md+5@@ -92,6 +92,11 @@ BEFORE you do it: 8. **`swx_stop` quits only a session with zero documents open**, and `force` never waives that. Treat a quit as close to a one-way door: relaunching takes minutes and can stop at the sign-in window again.+ A dialog blocking a quit is NAMED: the refusal gives its hwnd, owner, age+ and `attribution` (the bridge verb that raised it, if one provably did). The+ bridge cancels only a standard file dialog with an EMPTY name that it raised+ itself (`swx_dismiss_blocking_dialogs {close, confirm, reason}`); every other+ dialog is the user's to answer. 9. **Never end a session the user is working in, and never pile windows on them.** Every quit and sweep refuses `user_is_active` when SOLIDWORKS is in front with recent input, or documents appeared that the bridge did not open;
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.11.0",+ "version": "0.11.1", "description": "Bridge for SOLIDWORKS desktop CAD — create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", "author": "Adom Inc.", "license": "MIT",
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.11.0",+ "version": "0.11.1", "type": "app", "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.", "tags": [
src/comhost/com.rs+21@@ -1332,6 +1332,27 @@ impl ComBackend { } "b" => { let sw = self.app(false, true)?;+ // THE COMMAND ID IS READ, NOT ASSUMED (0.11.1). RunCommand runs+ // whatever command the number names; a wrong one could raise any+ // dialog (a File ▸ Open blocked every quit on 2026-10-15, origin+ // under investigation). swCommands_e.swCommands_App_Exit is read+ // from the installed interop; unreadable or different from 2825,+ // the rung does not send it.+ let exe = sw.string("GetExecutablePath", &[]).unwrap_or_default();+ let verified = swconst_ids(&exe, &[("swCommands_e", "swCommands_App_Exit")])+ .ok()+ .and_then(|m| m.get("swCommands_App_Exit").copied());+ if verified != Some(crate::quit::SW_COMMANDS_APP_EXIT) {+ detail.push(format!(+ "RunCommand NOT sent: swCommands_App_Exit reads {verified:?} from the installed interop, not {} — \+ a command id that cannot be verified is never run",+ crate::quit::SW_COMMANDS_APP_EXIT+ ));+ drop(sw);+ self.sw = None;+ detail.push("released every COM reference".into());+ return Ok(json!({"rung": rung, "result": "declined", "detail": detail.join("; "), "comReferencesOutstanding": 0}));+ } let called = sw.call( "RunCommand", &[&Var::i4(crate::quit::SW_COMMANDS_APP_EXIT), &Var::bstr("")],
src/comhost/mock.rs+13@@ -1124,6 +1124,19 @@ impl Backend for MockBackend { t.push('\n'); let _ = std::fs::write(&f, t); }+ // A DIALOG THAT APPEARS WHILE A COMMAND RUNS (0.11.1): `dialog-on-<cmd>.json`+ // in the simulated machine's dir holds window rows; the command puts them+ // up (once), as SOLIDWORKS raising a File ▸ Open would.+ if let Ok(dir) = std::env::var(crate::launchos::ENV_FAKE_DIR) {+ let trigger = std::path::Path::new(&dir).join(format!("dialog-on-{cmd}.json"));+ if let Some(rows) = std::fs::read_to_string(&trigger).ok().and_then(|t| serde_json::from_str::<Vec<Value>>(&t).ok()) {+ let wpath = std::path::Path::new(&dir).join("windows.json");+ let mut all: Vec<Value> = std::fs::read_to_string(&wpath).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();+ all.extend(rows);+ let _ = std::fs::write(&wpath, Value::Array(all).to_string());+ let _ = std::fs::remove_file(&trigger);+ }+ } let out = self.dispatch(cmd, args); // The simulated APPLICATION's session outlives the com host, because a // real one does. Written after every command so a respawn resumes from
src/dialogs.rsadded+247@@ -0,0 +1,247 @@+//! WHICH VERB PUT THAT DIALOG UP? (0.11.1)+//!+//! MEASURED 2026-10-15 on adom_drew2: a SOLIDWORKS File ▸ Open dialog (title+//! "Open ", `#32770`, owned by the SOLIDWORKS main frame, file name empty)+//! appeared between 14:57 and 15:00Z, DURING a bridge battery, with Drew away+//! from the machine — and from then on every `swx_stop` was+//! `stop_blocked_by_dialog`. The census called it `kind:"unknown"`,+//! `bridgeCaused:false`: it could not say where it came from, and so the+//! session the bridge had started could never be ended by the bridge.+//!+//! So every verb is now BRACKETED by a census of SOLIDWORKS' dialogs: one+//! before it runs, one after. A dialog first seen in the after-census, that+//! the before-census (taken after the verb was marked in flight) proved+//! ABSENT, appeared DURING that verb — `appearedDuring`. Sightings persist+//! (data root, `dialog-sightings.json`) with the time the dialog was first+//! seen, the last time it was proved absent, and the user's last input. A+//! dialog first seen outside a bracket stays UNATTRIBUTED: the bridge never+//! claims what it cannot prove.+//!+//! Policy is unchanged — the bridge never answers a user's dialog — with ONE+//! carve-out: the standard Windows FILE dialog, with an EMPTY file name, that+//! the bridge PROVABLY provoked (appeared during a bridge verb, no user input+//! since that verb began). Cancelling that is a no-op the bridge owes the+//! user; `swx_dismiss_blocking_dialogs` may do it, ledgered with the+//! attribution. Anything with text typed into it is the user's.++use serde_json::{json, Value};+use std::sync::Mutex;++/// One verb in flight: what, and since when (ms since the epoch).+#[derive(Clone, Debug, PartialEq)]+pub struct InFlight {+ pub id: u64,+ pub verb: String,+ pub started_ms: u64,+}++static IN_FLIGHT: Mutex<Vec<InFlight>> = Mutex::new(Vec::new());+static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);+static FILE_LOCK: Mutex<()> = Mutex::new(());++fn now_ms() -> u64 {+ crate::launchos::epoch_ms() as u64+}++fn sightings_path() -> std::path::PathBuf {+ crate::scratch::root().join("dialog-sightings.json")+}++/// The persisted sightings (hwnd → what is known about its appearance).+pub fn sightings() -> Vec<Value> {+ std::fs::read_to_string(sightings_path()).ok().and_then(|t| serde_json::from_str::<Vec<Value>>(&t).ok()).unwrap_or_default()+}++fn save(v: &[Value]) {+ let _ = std::fs::write(sightings_path(), serde_json::to_string(v).unwrap_or_else(|_| "[]".into()));+}++/// The dialogs SOLIDWORKS is showing now: every top-level window of its+/// process(es) and their CEF children that is a `#32770`, or is a visible,+/// non-tool window OWNED by another SOLIDWORKS window. `None` = could not look.+/// Deterministic: the native window list, every time — never ab's.+pub fn census_windows(os: &dyn crate::launchos::LaunchOs) -> Option<(Vec<crate::launchos::WindowInfo>, Vec<u32>)> {+ let procs = os.processes().ok()?;+ let sw: Vec<&crate::launchos::ProcInfo> = procs.iter().filter(|p| p.is("SLDWORKS.exe")).collect();+ let mut pids: Vec<u32> = Vec::new();+ for p in &sw {+ pids.push(p.pid);+ pids.extend(procs.iter().filter(|c| c.is("swCefSubProc.exe") && c.ppid == Some(p.pid)).map(|c| c.pid));+ }+ let mut all = Vec::new();+ for pid in &pids {+ all.extend(os.windows_of_pid(*pid).ok()?);+ }+ let ours: std::collections::HashSet<i64> = all.iter().map(|w| w.hwnd).collect();+ let rows = all+ .iter()+ .filter(|w| {+ w.is_dialog()+ || (w.visible+ && w.owner.map(|o| ours.contains(&o)).unwrap_or(false)+ && !w.exstyle.map(|e| e & crate::launchos::WS_EX_TOOLWINDOW != 0).unwrap_or(false))+ })+ .cloned()+ .collect();+ Some((rows, sw.iter().map(|p| p.pid).collect()))+}++/// OBSERVE: record every dialog not seen before, attributed to the verbs in+/// flight; strike off sightings whose window is gone. Returns the sightings of+/// the dialogs that are up now.+pub fn observe(os: &dyn crate::launchos::LaunchOs) -> Option<Vec<Value>> {+ let (rows, _) = census_windows(os)?;+ let _g = FILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());+ let mut seen = sightings();+ let last_ok = seen.iter().filter_map(|s| s.get("censusMs").and_then(|v| v.as_u64())).max();+ let now = now_ms();+ let flight: Vec<InFlight> = IN_FLIGHT.lock().unwrap_or_else(|e| e.into_inner()).clone();+ let last_input_ms = os.seconds_since_last_input().ok().map(|s| now.saturating_sub(s * 1000));+ let up: std::collections::HashSet<i64> = rows.iter().map(|w| w.hwnd).collect();+ // Gone windows are forgotten; the census time moves on for the rest.+ seen.retain(|s| s.get("hwnd").and_then(|h| h.as_i64()).map(|h| up.contains(&h)).unwrap_or(false));+ let mut current = Vec::new();+ for w in &rows {+ if let Some(s) = seen.iter_mut().find(|s| s["hwnd"] == w.hwnd && s["title"] == w.title.as_str()) {+ s["lastSeenMs"] = json!(now);+ current.push(s.clone());+ continue;+ }+ // A window under a REUSED hwnd with a new title is a new sighting.+ seen.retain(|s| s["hwnd"] != w.hwnd);+ let s = json!({+ "hwnd": w.hwnd,+ "title": w.title,+ "className": w.class_name,+ "owner": w.owner,+ "firstSeenMs": now,+ "lastSeenMs": now,+ // The last census that could see SOLIDWORKS' windows and did not+ // contain this one: it appeared after that.+ "absentAtMs": last_ok,+ "appearedDuring": flight.iter().map(|f| json!({"verb": f.verb, "startedMs": f.started_ms})).collect::<Vec<_>>(),+ "lastInputMs": last_input_ms,+ });+ current.push(s.clone());+ seen.push(s);+ }+ // Every sighting carries the time of the latest census, so the next new+ // dialog knows when it was last proved absent.+ let mut out = seen;+ out.push(json!({"hwnd": Value::Null, "censusMs": now}));+ out.retain(|s| !s["hwnd"].is_null() || s.get("censusMs").and_then(|v| v.as_u64()) == Some(now));+ save(&out);+ Some(current)+}++/// IS THIS SIGHTING PROVABLY THE BRIDGE'S? It appeared during a bridge verb —+/// proved ABSENT at or after that verb began, first seen while it was in+/// flight — and the user gave no input from that verb's start until `now`+/// (`last_input_now_ms`: the latest input as read now). Returns the verb.+pub fn bridge_caused(s: &Value, last_input_now_ms: Option<u64>) -> Option<String> {+ let absent = s.get("absentAtMs").and_then(|v| v.as_u64())?;+ let input_then = s.get("lastInputMs").and_then(|v| v.as_u64())?;+ let input_now = last_input_now_ms?;+ s.get("appearedDuring")?.as_array()?.iter().find_map(|d| {+ let started = d.get("startedMs").and_then(|v| v.as_u64())?;+ let verb = d.get("verb").and_then(|v| v.as_str())?;+ (started <= absent && input_then < started && input_now < started).then(|| verb.to_string())+ })+}++/// What is known about one dialog's appearance, for a report.+pub fn attribution(hwnd: i64, os: &dyn crate::launchos::LaunchOs) -> Value {+ let Some(s) = sightings().into_iter().find(|s| s["hwnd"] == hwnd) else {+ return json!({"known": false, "why": "no census saw this window appear: it was up before the bridge first looked, so where it came from cannot be said"});+ };+ let now = now_ms();+ let input_now = os.seconds_since_last_input().ok().map(|x| now.saturating_sub(x * 1000));+ let during = s.get("appearedDuring").and_then(|d| d.as_array()).cloned().unwrap_or_default();+ let caused = bridge_caused(&s, input_now);+ json!({+ "known": true,+ "firstSeenMs": s["firstSeenMs"],+ "ageSeconds": s.get("firstSeenMs").and_then(|v| v.as_u64()).map(|f| now.saturating_sub(f) / 1000),+ "appearedDuring": if during.is_empty() { Value::Null } else { json!(during.iter().filter_map(|d| d.get("verb").cloned()).collect::<Vec<_>>()) },+ "appearedBetweenVerbs": during.is_empty(),+ "provedAbsentAtMs": s["absentAtMs"],+ "bridgeCaused": caused.is_some(),+ "bridgeCausedBy": caused,+ "userInputSinceItAppeared": match (input_now, s.get("firstSeenMs").and_then(|v| v.as_u64())) {+ (Some(i), Some(f)) => json!(i >= f),+ _ => Value::Null,+ },+ })+}++/// A verb begins: marked in flight FIRST, then the census — so the census+/// that proves a dialog absent is one taken after the verb began.+pub fn begin(verb: &str) -> u64 {+ let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);+ IN_FLIGHT.lock().unwrap_or_else(|e| e.into_inner()).push(InFlight { id, verb: verb.to_string(), started_ms: now_ms() });+ let os = crate::launchos::os();+ let _ = observe(os.as_ref());+ id+}++/// A verb ends: the census FIRST (a dialog it raised is attributed to it),+/// then it leaves the in-flight list. New dialogs are ledgered.+pub fn end(id: u64) -> Vec<Value> {+ let os = crate::launchos::os();+ let before: std::collections::HashSet<i64> = sightings().iter().filter_map(|s| s.get("hwnd").and_then(|h| h.as_i64())).collect();+ let now = observe(os.as_ref()).unwrap_or_default();+ IN_FLIGHT.lock().unwrap_or_else(|e| e.into_inner()).retain(|f| f.id != id);+ let fresh: Vec<Value> = now.into_iter().filter(|s| s.get("hwnd").and_then(|h| h.as_i64()).map(|h| !before.contains(&h)).unwrap_or(false)).collect();+ for s in &fresh {+ crate::ledger::record("dialog_appeared", json!({"hwnd": s["hwnd"], "title": s["title"], "className": s["className"],+ "appearedDuring": s["appearedDuring"], "owner": s["owner"]}));+ }+ fresh+}++/// Does `dispatch` bracket this verb? Every known verb except the ones that+/// only read the bridge's own state (they never touch SOLIDWORKS).+pub fn brackets(verb: &str) -> bool {+ !matches!(verb, "swx_progress" | "swx_action_ledger" | "swx_verb_times" | "swx_describe")+}++/// The standard Windows FILE dialog with an empty name, provoked by the bridge:+/// the one dialog the bridge may cancel. `Err` says why not.+pub fn may_cancel(hwnd: i64, os: &dyn crate::launchos::LaunchOs) -> Result<(String, Value), String> {+ let s = sightings().into_iter().find(|s| s["hwnd"] == hwnd).ok_or("no census saw it appear: its origin is unknown")?;+ let now = now_ms();+ let input_now = os.seconds_since_last_input().ok().map(|x| now.saturating_sub(x * 1000));+ let verb = bridge_caused(&s, input_now).ok_or(+ "it is not PROVABLY the bridge's: it did not appear during a bridge verb, or the user has given input since that verb began",+ )?;+ let fd = os.file_dialog(hwnd).map_err(|e| format!("whether it is a file dialog could not be read ({e})"))?;+ if !fd.standard {+ return Err("it is not the standard Windows file dialog".into());+ }+ match fd.file_name.as_deref() {+ None => return Err("its file-name box could not be read".into()),+ Some(t) if !t.trim().is_empty() => return Err(format!("its file-name box holds text ({} characters): somebody typed into it", t.chars().count())),+ _ => {}+ }+ Ok((verb, s))+}++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn only_a_dialog_proved_absent_after_a_verb_began_with_no_input_since_is_the_bridges() {+ let s = json!({"absentAtMs": 1000, "lastInputMs": 500, "appearedDuring": [{"verb": "swx_start", "startedMs": 900}]});+ assert_eq!(bridge_caused(&s, Some(500)), Some("swx_start".to_string()));+ // Input after the verb began: the user may have opened it.+ assert_eq!(bridge_caused(&s, Some(950)), None, "input since the verb began");+ assert_eq!(bridge_caused(&json!({"absentAtMs": 1000, "lastInputMs": 950, "appearedDuring": [{"verb": "v", "startedMs": 900}]}), Some(950)), None);+ // Last proved absent BEFORE the verb began: it may predate the verb.+ assert_eq!(bridge_caused(&json!({"absentAtMs": 800, "lastInputMs": 500, "appearedDuring": [{"verb": "v", "startedMs": 900}]}), Some(500)), None);+ // Appeared between verbs, or never proved absent, or input unreadable.+ assert_eq!(bridge_caused(&json!({"absentAtMs": 1000, "lastInputMs": 500, "appearedDuring": []}), Some(500)), None);+ assert_eq!(bridge_caused(&json!({"absentAtMs": null, "lastInputMs": 500, "appearedDuring": [{"verb": "v", "startedMs": 900}]}), Some(500)), None);+ assert_eq!(bridge_caused(&s, None), None, "an unreadable input time proves nothing");+ }+}
src/launchos.rs+136@@ -295,6 +295,16 @@ impl WindowInfo { } } +/// THE STANDARD WINDOWS FILE DIALOG (0.11.1): is this window one (it has the+/// file-name combo, control 1148 — or the old-style edit, 1152 — and a Cancel+/// button, control 2), and what is in its file-name box (`None` = could not be+/// read, never "empty").+#[derive(Clone, Debug, Default, PartialEq)]+pub struct FileDialogState {+ pub standard: bool,+ pub file_name: Option<String>,+}+ /// The readable contents of a dialog: what it says, and what it offers. #[derive(Debug, Clone, Default, PartialEq)] pub struct DialogContents {@@ -532,6 +542,18 @@ pub trait LaunchOs: Send + Sync { None } + /// Is `hwnd` the standard file dialog, and what is typed in it? READ-ONLY.+ fn file_dialog(&self, _hwnd: i64) -> Result<FileDialogState, String> {+ Err("this host cannot read a file dialog".into())+ }++ /// CANCEL a dialog: WM_COMMAND IDCANCEL, as Esc or its Cancel button would.+ /// Used ONLY on a file dialog the bridge provably raised, with an empty file+ /// name (dialogs::may_cancel). `Ok` only when the window is gone after.+ fn cancel_dialog(&self, _hwnd: i64) -> Result<(), String> {+ Err("this host cannot cancel a dialog".into())+ }+ /// This process's own pid — recorded on a launch attempt so a LATER bridge /// can tell "somebody is supervising this" from "this was left behind". fn self_pid(&self) -> u32 {@@ -1327,6 +1349,33 @@ impl LaunchOs for FakeOs { /// The scenario's row for `hwnd` may carry `ownerEnabledReads: [false, /// false, true]` — one value per re-read, the last repeating; otherwise its /// `ownerEnabled` holds for every read.+ /// `file-dialogs.json`: {"<hwnd>": {"standard": bool, "fileName": "..." | null}}.+ fn file_dialog(&self, hwnd: i64) -> Result<FileDialogState, String> {+ let all = self.read_json("file-dialogs.json");+ let row = all.get(hwnd.to_string()).ok_or("simulated: not a scripted file dialog")?;+ Ok(FileDialogState {+ standard: row.get("standard").and_then(|v| v.as_bool()).unwrap_or(false),+ file_name: row.get("fileName").and_then(|v| v.as_str()).map(String::from),+ })+ }++ /// Records the cancel (`cancelled-dialogs.jsonl`) and takes the window out+ /// of `windows.json`, as Windows would; `cancel-dialog-fails` leaves it up.+ fn cancel_dialog(&self, hwnd: i64) -> Result<(), String> {+ use std::io::Write;+ if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(self.dir.join("cancelled-dialogs.jsonl")) {+ let _ = writeln!(f, "{}", json!({"hwnd": hwnd}));+ }+ if self.dir.join("cancel-dialog-fails").exists() {+ return Err("simulated: the dialog was still up after IDCANCEL".into());+ }+ if let Some(rows) = self.read_json("windows.json").as_array() {+ let kept: Vec<Value> = rows.iter().filter(|r| r.get("hwnd").and_then(|h| h.as_i64()) != Some(hwnd)).cloned().collect();+ let _ = std::fs::write(self.dir.join("windows.json"), Value::Array(kept).to_string());+ }+ Ok(())+ }+ fn owner_enabled_of(&self, hwnd: i64) -> Option<bool> { let rows = self.read_json("windows.json"); let row = rows.as_array()?.iter().find(|r| r.get("hwnd").and_then(|h| h.as_i64()) == Some(hwnd))?.clone();@@ -2026,6 +2075,93 @@ mod win { } } + /// The file-name combo (control 1148, `cmb13`) or the old-style edit+ /// (1152, `edt1`), and the Cancel button (IDCANCEL, 2), found among ALL+ /// descendants; the name read with WM_GETTEXT, which Windows marshals+ /// across processes (GetWindowText would not read another process's edit).+ fn file_dialog(&self, hwnd: i64) -> Result<FileDialogState, String> {+ use windows::core::BOOL;+ use windows::Win32::Foundation::{LPARAM, WPARAM};+ use windows::Win32::UI::WindowsAndMessaging::{+ EnumChildWindows, GetClassNameW, GetDlgCtrlID, GetParent, SendMessageTimeoutW, SMTO_ABORTIFHUNG, WM_GETTEXT,+ WM_GETTEXTLENGTH,+ };+ static KIDS: std::sync::Mutex<Vec<i64>> = std::sync::Mutex::new(Vec::new());+ unsafe extern "system" fn kid(h: HWND, _: LPARAM) -> BOOL {+ if let Ok(mut g) = KIDS.lock() {+ g.push(h.0 as i64);+ }+ BOOL(1)+ }+ let _serialised = ENUM_LOCK.lock().unwrap_or_else(|e| e.into_inner());+ KIDS.lock().unwrap_or_else(|e| e.into_inner()).clear();+ unsafe {+ let _ = EnumChildWindows(Some(HWND(hwnd as *mut std::ffi::c_void)), Some(kid), LPARAM(0));+ }+ let kids = KIDS.lock().unwrap_or_else(|e| e.into_inner()).clone();+ if kids.is_empty() {+ return Err("the dialog reported no child controls".into());+ }+ let class_of = |h: i64| -> String {+ let mut c = [0u16; 128];+ let n = unsafe { GetClassNameW(HWND(h as *mut std::ffi::c_void), &mut c) };+ String::from_utf16_lossy(&c[..n.max(0) as usize])+ };+ let id_of = |h: i64| unsafe { GetDlgCtrlID(HWND(h as *mut std::ffi::c_void)) };+ let combo = kids.iter().copied().find(|h| id_of(*h) == 1148 && class_of(*h).starts_with("ComboBox"));+ let old_edit = kids.iter().copied().find(|h| id_of(*h) == 1152 && class_of(*h) == "Edit");+ let cancel = kids.iter().any(|h| id_of(*h) == 2 && class_of(*h) == "Button");+ let edit = old_edit.or_else(|| {+ let c = combo?;+ kids.iter().copied().find(|h| {+ class_of(*h) == "Edit" && {+ let mut p = unsafe { GetParent(HWND(*h as *mut std::ffi::c_void)) }.ok();+ let mut found = false;+ for _ in 0..4 {+ match p {+ Some(x) if x.0 as i64 == c => {+ found = true;+ break;+ }+ Some(x) => p = unsafe { GetParent(x) }.ok(),+ None => break,+ }+ }+ found+ }+ })+ });+ let standard = (combo.is_some() || old_edit.is_some()) && cancel;+ let file_name = edit.and_then(|e| unsafe {+ let h = HWND(e as *mut std::ffi::c_void);+ let mut len: usize = 0;+ let r = SendMessageTimeoutW(h, WM_GETTEXTLENGTH, WPARAM(0), LPARAM(0), SMTO_ABORTIFHUNG, 500, Some(&mut len));+ if r.0 == 0 {+ return None;+ }+ let mut buf = vec![0u16; len + 1];+ let mut got: usize = 0;+ let r = SendMessageTimeoutW(h, WM_GETTEXT, WPARAM(buf.len()), LPARAM(buf.as_mut_ptr() as isize), SMTO_ABORTIFHUNG, 500, Some(&mut got));+ (r.0 != 0).then(|| String::from_utf16_lossy(&buf[..got.min(len)]))+ });+ Ok(FileDialogState { standard, file_name })+ }++ fn cancel_dialog(&self, hwnd: i64) -> Result<(), String> {+ use windows::Win32::Foundation::{LPARAM, WPARAM};+ use windows::Win32::UI::WindowsAndMessaging::{IsWindow, PostMessageW, WM_COMMAND};+ let h = HWND(hwnd as *mut std::ffi::c_void);+ // IDCANCEL (2), BN_CLICKED (0) in the high word: what Esc does.+ unsafe { PostMessageW(Some(h), WM_COMMAND, WPARAM(2), LPARAM(0)) }.map_err(|e| format!("PostMessage failed: {e}"))?;+ for _ in 0..30 {+ std::thread::sleep(std::time::Duration::from_millis(100));+ if !unsafe { IsWindow(Some(h)) }.as_bool() {+ return Ok(());+ }+ }+ Err("the dialog was still up 3 s after IDCANCEL".into())+ }+ fn owner_enabled_of(&self, hwnd: i64) -> Option<bool> { use windows::Win32::UI::WindowsAndMessaging::{GetWindow, GetWindowLongPtrW, GWL_STYLE, GW_OWNER}; unsafe {
src/lib.rs+1@@ -47,6 +47,7 @@ pub mod presence; pub mod quit; pub mod readiness; pub mod render;+pub mod dialogs; pub mod disk; pub mod selftest; pub mod scratch;
src/quit.rs+52−3@@ -411,6 +411,16 @@ pub fn button_like_captions(c: &crate::launchos::DialogContents) -> Vec<String> pub struct DialogCensus { pub blocking: Option<BlockingDialog>, pub notifications: Vec<BlockingDialog>,+ /// EVERY OTHER blocking dialog (0.11.1): `blocking` is the first, which+ /// decides a refusal; a census that reports never drops the rest.+ pub more_blocking: Vec<BlockingDialog>,+}++impl DialogCensus {+ /// Every blocking dialog, the first first.+ pub fn all_blocking(&self) -> Vec<&BlockingDialog> {+ self.blocking.iter().chain(self.more_blocking.iter()).collect()+ } } /// How many times, and how far apart, a disabled owner is re-read before a@@ -458,7 +468,7 @@ fn settle_owner(w: &WindowInfo, contents: Option<&crate::launchos::DialogContent } pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs) -> DialogCensus {- let mut out = DialogCensus { blocking: None, notifications: Vec::new() };+ let mut out = DialogCensus { blocking: None, notifications: Vec::new(), more_blocking: Vec::new() }; for w in windows.iter().filter(|w| w.is_dialog() && w.visible) { let contents = os.dialog_contents(w.hwnd).ok(); // A body that could not be read is judged as unread (0.9.12).@@ -499,6 +509,8 @@ pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs) if blocking { if out.blocking.is_none() { out.blocking = Some(d);+ } else {+ out.more_blocking.push(d); } } else { out.notifications.push(d);@@ -547,9 +559,42 @@ impl BlockingDialog { "ownerDisabledTransiently": self.owner_settle.as_ref().and_then(|o| o.get("ownerDisabledTransiently").cloned()).unwrap_or(json!(false)), "ownerReads": self.owner_settle.as_ref().and_then(|o| o.get("reads").cloned()).unwrap_or(Value::Null), "neverAnswer": true,+ // WHICH WINDOW, AND WHERE IT CAME FROM (0.11.1): an operator facing a+ // refusal sees exactly which dialog, whose it is, how long it has been+ // up, and whether a bridge verb raised it.+ "className": self.window.as_ref().map(|w| w.class_name.clone()),+ "owner": self.window.as_ref().and_then(|w| w.owner),+ "rect": self.window.as_ref().and_then(|w| w.rect),+ "visible": self.window.as_ref().map(|w| w.visible),+ "attribution": crate::dialogs::attribution(self.hwnd, crate::launchos::os().as_ref()), }) } + /// `to_json`, plus what the bridge may do about it: the standard file+ /// dialog it provably raised, with an empty name, is the one it may cancel.+ pub fn report(&self, os: &dyn crate::launchos::LaunchOs) -> Value {+ let mut v = self.to_json();+ let fd = os.file_dialog(self.hwnd).ok();+ v["fileDialog"] = match &fd {+ Some(f) => json!({"standard": f.standard, "fileNameEmpty": f.file_name.as_ref().map(|n| n.trim().is_empty())}),+ None => Value::Null,+ };+ match crate::dialogs::may_cancel(self.hwnd, os) {+ Ok((verb, _)) => {+ v["bridgeMayCancel"] = json!(true);+ v["cancelBecause"] = json!(format!(+ "the standard file dialog with an EMPTY file name, raised during {verb} with no user input since: swx_dismiss_blocking_dialogs {{close:true, confirm:true, reason}} cancels it (Cancel changes nothing)"+ ));+ }+ Err(why) => {+ v["bridgeMayCancel"] = json!(false);+ v["notCancellableBecause"] = json!(why);+ v["whoAnswers"] = json!("the user cancels it");+ }+ }+ v+ }+ /// READ WHAT IT SAYS. Read-only; a failure leaves `contents` as `None`. pub fn read_contents(&mut self, os: &dyn crate::launchos::LaunchOs) { self.contents = os.dialog_contents(self.hwnd).ok();@@ -876,9 +921,13 @@ pub fn check_preconditions( return Err(Refusal::Known { code: d.error_code(), message: format!(- "SOLIDWORKS is showing a dialog ({}): \"{}\" — this bridge never answers a \+ "SOLIDWORKS is showing a dialog ({}): \"{}\" (hwnd {}, class {}, owned by {}) — this bridge never answers a \ dialog, so the quit stops here",- d.kind, d.title+ d.kind,+ d.title,+ d.hwnd,+ d.window.as_ref().map(|w| w.class_name.as_str()).unwrap_or("?"),+ d.window.as_ref().and_then(|w| w.owner).map(|o| o.to_string()).unwrap_or_else(|| "no window".into()) ), }); }
src/verbs.rs+73−27@@ -717,35 +717,55 @@ fn swx_state(ctx: &Ctx, args: &Value) -> Value { if docs.is_null() { docs = json!([]); }- // The RENDER-ME half: a background screenshot plus the dialog census, which- // is what makes one call enough to see what SOLIDWORKS is showing. Both go- // through ab's generic verbs and neither is allowed to fail the verb.+ // THE DIALOG CENSUS (0.11.1): NATIVE and DETERMINISTIC — every top-level+ // window of SOLIDWORKS (and its CEF children), judged, every time, whether or+ // not a screenshot is wanted. MEASURED 2026-10-15: at 19:30Z this verb said+ // `blockingDialogs: []` while a File ▸ Open dialog (hwnd 205698) was up+ // before and after — the census went through ab's flat window list, and was+ // SKIPPED entirely (reporting a known-empty []) whenever screenshot:false.+ // ab's list is the fallback only where the native one cannot be read. let want_shot = arg_bool(args, "screenshot", true); let hwnd_i64 = main_hwnd.as_i64(); let mut windows_error: Option<String> = None;- let (screenshot, dialogs) = if want_shot && (detection.running || crate::is_mock()) {- // AN UNREACHABLE ab IS NOT "NO DIALOGS". `unwrap_or_default()` turned a- // failed window enumeration into an empty census, and `blockingDialogs:- // []` is what a caller reads as "nothing is in the way" right before it- // drives a SOLIDWORKS that is sitting on a modal.- let windows = match crate::abwin::list_windows(Duration::from_secs(20)) {- Ok(w) => Some(w),- Err(e) => {- windows_error = Some(e);- None+ let os = crate::launchos::os();+ let mut census_source = "native";+ let mut other_windows: Vec<Value> = Vec::new();+ let mut notifications: Vec<Value> = Vec::new();+ // A census that finds NO SOLIDWORKS process while COM says one is running+ // cannot vouch for anything: it is not "no dialogs".+ let sw_seen_running = detection.running || !main_hwnd.is_null();+ let dialogs: Option<Vec<Value>> = match crate::dialogs::census_windows(os.as_ref()).filter(|(_, pids)| !pids.is_empty() || !sw_seen_running) {+ Some((rows, _)) => {+ let judged = crate::quit::dialog_census(&rows, os.as_ref());+ notifications = judged.notifications.iter().map(|d| d.report(os.as_ref())).collect();+ // Owned popups that are not dialogs: listed, never judged away.+ other_windows = rows.iter().filter(|w| !w.is_dialog()).map(|w| w.to_json()).collect();+ Some(judged.all_blocking().iter().map(|d| d.report(os.as_ref())).collect())+ }+ None if detection.running || crate::is_mock() => {+ census_source = "ab";+ // AN UNREACHABLE ab IS NOT "NO DIALOGS": null, never [].+ match crate::abwin::list_windows(Duration::from_secs(20)) {+ Ok(w) => {+ let sw_pid = ctx.call("app_state", json!({}), 20).ok().and_then(|s| s.get("processId").and_then(|p| p.as_i64()));+ Some(crate::abwin::dialog_census(&w, hwnd_i64, sw_pid))+ }+ Err(e) => {+ windows_error = Some(e);+ None+ } }- };- let sw_pid = ctx- .call("app_state", json!({}), 20)- .ok()- .and_then(|s| s.get("processId").and_then(|p| p.as_i64()));- let census = windows- .as_ref()- .map(|w| crate::abwin::dialog_census(w, hwnd_i64, sw_pid));- let shot = hwnd_i64.map(|h| crate::abwin::screenshot(h, Duration::from_secs(45)));- (shot.unwrap_or(Value::Null), census)+ }+ None => {+ census_source = "none";+ windows_error = Some("the window census could not be taken".into());+ None+ }+ };+ let screenshot = if want_shot && (detection.running || crate::is_mock()) {+ hwnd_i64.map(|h| crate::abwin::screenshot(h, Duration::from_secs(45))).unwrap_or(Value::Null) } else {- (Value::Null, Some(Vec::new()))+ Value::Null }; ok(json!({ "running": detection.running,@@ -759,6 +779,9 @@ fn swx_state(ctx: &Ctx, args: &Value) -> Value { "blockingDialogs": dialogs.clone().map(Value::from).unwrap_or(Value::Null), "blockingDialogsKnown": dialogs.is_some(), "blockingDialogsError": windows_error,+ "dialogCensusSource": census_source,+ "dialogNotifications": notifications,+ "ownedWindows": other_windows, "screenshot": screenshot, "timings": crate::ledger::stages(), "focusEvents": [],@@ -1178,6 +1201,8 @@ struct StopEvidence { /// Windows the census judged NOT blocking — notifications, reported with /// their evidence and never answered (0.9.4). notifications: Vec<crate::quit::BlockingDialog>,+ /// Every blocking dialog after the first (0.11.1): reported, never dropped.+ more_dialogs: Vec<crate::quit::BlockingDialog>, /// The pids the dialog census actually covered — SOLIDWORKS plus its CEF /// subprocesses, which is where a 3DEXPERIENCE sign-in window lives. dialog_pids: Vec<u32>,@@ -1228,6 +1253,7 @@ impl StopEvidence { let mut census_readable = false; let mut dialog = None; let mut notifications: Vec<crate::quit::BlockingDialog> = Vec::new();+ let mut more_dialogs: Vec<crate::quit::BlockingDialog> = Vec::new(); if let Some(id) = identity.as_ref() { if let Some(pids) = quit::dialog_census_pids(census.as_ref(), id) { let mut all_read = true;@@ -1253,6 +1279,7 @@ impl StopEvidence { let judged = quit::dialog_census(&windows, os); dialog = judged.blocking; notifications = judged.notifications;+ more_dialogs = judged.more_blocking; } } // P5: responding, without COM.@@ -1269,6 +1296,7 @@ impl StopEvidence { identity, dialog, notifications,+ more_dialogs, dialog_pids, census_readable, responding,@@ -1301,7 +1329,10 @@ impl StopEvidence { "listedDocuments": self.listed_docs, "documentsTruncated": self.docs_truncated, "unsavedDocuments": self.unsaved,- "dialog": self.dialog.as_ref().map(|d| d.to_json()),+ // WHICH dialog, whose, how old, and whether a bridge verb raised it+ // (0.11.1) — and whether the bridge may cancel it.+ "dialog": self.dialog.as_ref().map(|d| d.report(crate::launchos::os().as_ref())),+ "moreBlockingDialogs": self.more_dialogs.iter().map(|d| d.report(crate::launchos::os().as_ref())).collect::<Vec<_>>(), "notifications": self.notifications.iter().map(|d| d.to_json()).collect::<Vec<_>>(), "dialogCensusReadable": self.census_readable, "dialogCensusPids": self.dialog_pids,@@ -1331,7 +1362,11 @@ fn stop_refusal_hint(code: &str) -> &'static str { "stop_blocked_by_dialog" => "SOLIDWORKS is asking something. This bridge never answers a dialog — closing a stuck \ modal through the window manager took the whole application down on 2026-09-02. \- swx_state takes a background screenshot; show the user and let them answer.",+ preconditions.dialog names it (hwnd, class, owner, ageSeconds, attribution). ONE \+ exception: the standard file dialog with an EMPTY file name that a bridge verb \+ provably raised (bridgeMayCancel:true) — swx_dismiss_blocking_dialogs {close:true, \+ confirm:true, reason} cancels it, a no-op. Otherwise the user cancels it: swx_state \+ takes a background screenshot to show them.", "stop_preconditions_unknown" => "Nothing was attempted. Every precondition must be MEASURED, and an unanswerable one \ is not permission — quitting on a guess is how a session ends up half-closed with the \@@ -3164,7 +3199,7 @@ pub const CATALOG: &[(&str, &str, &str)] = &[ ("swx_kill", "process", "force-kill SOLIDWORKS (reason + confirm; discards unsaved work)"), ("swx_com_respawn", "process", "respawn ONLY the bridge's own COM host child (reason): every COM reference the bridge holds is released; SOLIDWORKS is never called; refused while a verb is in flight"), ("swx_window_info", "read", "SOLIDWORKS' windows and its main hwnd (read-only, nothing raised)"),- ("swx_dismiss_blocking_dialogs", "exec", "census of blocking SOLIDWORKS dialogs; close:true is opt-in, gated, and never touches an unattributed window"),+ ("swx_dismiss_blocking_dialogs", "exec", "census of blocking SOLIDWORKS dialogs (native: every window of the process, with where each came from); close:true is opt-in, gated, and never touches an unattributed window — the one dialog it may CANCEL without the write opt-in is a standard file dialog with an empty name that a bridge verb provably raised"), ("swx_close_window", "exec", "close one window by hwnd (reason + confirm)"), ("swx_check_recovery", "read", "is SOLIDWORKS showing the Document Recovery dialog?"), ("swx_action_ledger", "read", "append-only record of what the bridge did in the background"),@@ -3479,7 +3514,18 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value { // WHAT WAS OPEN BEFORE THE VERB — so the baseline grows only by what the // verb itself opened or created (0.9.10, adversary pass 46). let before = if changes_documents { crate::presence::snapshot(ctx) } else { None };+ // EVERY VERB IS BRACKETED BY A DIALOG CENSUS (0.11.1): a SOLIDWORKS dialog+ // that appears while it runs is attributed to it (`dialogs::observe`).+ let bracket = (is_known(command) && crate::dialogs::brackets(command)).then(|| crate::dialogs::begin(command)); let mut out = dispatch_after_gate(ctx, command, args);+ if let Some(id) = bracket {+ let fresh = crate::dialogs::end(id);+ if !fresh.is_empty() {+ if let Value::Object(m) = &mut out {+ m.insert("dialogsAppeared".into(), json!(fresh));+ }+ }+ } if let (Some(summary), Value::Object(m)) = (at_gate, &mut out) { m.entry("readiness").or_insert(summary); }
src/verbs3.rs+61@@ -3339,8 +3339,63 @@ pub fn dismiss_blocking_dialogs(ctx: &Ctx, args: &Value) -> Value { return g; } }+ // THE NATIVE CENSUS FIRST (0.11.1): SOLIDWORKS' own windows, judged, with+ // where each one came from. The ONE dialog this bridge may cancel is in it:+ // the standard file dialog with an EMPTY file name that a bridge verb+ // provably raised (dialogs::may_cancel) — Cancel is a no-op, and it is the+ // bridge's own mess. Everything else is the user's to answer.+ let os = crate::launchos::os();+ let native: Option<Vec<crate::quit::BlockingDialog>> = crate::dialogs::census_windows(os.as_ref()).map(|(rows, _)| {+ let c = crate::quit::dialog_census(&rows, os.as_ref());+ let mut all: Vec<crate::quit::BlockingDialog> = c.blocking.into_iter().chain(c.more_blocking).collect();+ all.extend(c.notifications);+ all+ });+ let only_hwnds: Vec<i64> = args.get("hwnds").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|v| v.as_i64()).collect()).unwrap_or_default();+ let mut cancelled: Vec<Value> = Vec::new();+ let mut cancel_refused: Vec<Value> = Vec::new();+ if close {+ let reason = arg_str(args, "reason").unwrap_or("").to_string();+ for d in native.iter().flatten() {+ if !only_hwnds.is_empty() && !only_hwnds.contains(&d.hwnd) {+ continue;+ }+ match crate::dialogs::may_cancel(d.hwnd, os.as_ref()) {+ Ok((verb, sighting)) => {+ let r = os.cancel_dialog(d.hwnd);+ let attribution = crate::dialogs::attribution(d.hwnd, os.as_ref());+ crate::ledger::record(+ "dialog_cancelled",+ json!({"hwnd": d.hwnd, "title": d.title, "raisedDuring": verb, "sighting": sighting,+ "ok": r.is_ok(), "error": r.as_ref().err(), "reason": reason}),+ );+ cancelled.push(json!({"hwnd": d.hwnd, "title": d.title, "ok": r.is_ok(), "error": r.err(),+ "raisedDuring": verb, "attribution": attribution,+ "how": "WM_COMMAND IDCANCEL — what Esc or its Cancel button does; nothing is opened or changed"}));+ }+ Err(why) => cancel_refused.push(json!({"hwnd": d.hwnd, "title": d.title, "why": why})),+ }+ }+ }+ let native_report: Option<Vec<Value>> = crate::dialogs::census_windows(os.as_ref()).map(|(rows, _)| {+ let c = crate::quit::dialog_census(&rows, os.as_ref());+ c.all_blocking().iter().map(|d| d.report(os.as_ref())).chain(c.notifications.iter().map(|d| d.report(os.as_ref()))).collect()+ }); let (hwnd, pid) = window_identity(ctx); let all = match crate::abwin::list_windows(AB_TIMEOUT) {+ Err(e) if native_report.is_some() => {+ return ok(json!({+ "dialogs": native_report,+ "dialogsKnown": true,+ "censusSource": "native",+ "cancelled": cancelled,+ "cancelRefused": cancel_refused,+ "closed": [],+ "abError": e,+ "_hint": "The native census (SOLIDWORKS' own windows) is complete; ab could not be \+ reached, so nothing beyond a bridge-raised empty file dialog could be closed.",+ }))+ } Err(e) => { return ok(json!({ // NULL, not []. An empty census reads as "nothing is in the@@ -3357,10 +3412,13 @@ pub fn dismiss_blocking_dialogs(ctx: &Ctx, args: &Value) -> Value { Ok(w) => w, }; let dialogs = crate::abwin::dialog_census(&all, hwnd, pid);+ // A dialog cancelled above is gone; the ab list was read after it.+ let dialogs: Vec<Value> = dialogs.into_iter().filter(|d| !cancelled.iter().any(|c| c["hwnd"] == d["hwnd"] && c["ok"] == true)).collect(); if !close { return ok(json!({ "dialogs": dialogs, "dialogCount": dialogs.len(),+ "nativeDialogs": native_report, "closed": [], "mainHwnd": hwnd, "_hint": "REPORT ONLY. Pass close:true (plus confirm:true and a reason) to close \@@ -3472,6 +3530,9 @@ pub fn dismiss_blocking_dialogs(ctx: &Ctx, args: &Value) -> Value { } ok(json!({ "dialogs": dialogs,+ "nativeDialogs": native_report,+ "cancelled": cancelled,+ "cancelRefused": cancel_refused, "closed": closed, "refused": refused, "_hint": "Only dialogs PROVABLY owned by SOLIDWORKS are closed (`ownerConfirmed:true`). \
tests/dialogs.rsadded+219@@ -0,0 +1,219 @@+//! WHO PUT THAT DIALOG UP? (0.11.1) — the File ▸ Open of 2026-10-15.+//!+//! MEASURED on adom_drew2: title "Open " (trailing space), `#32770`, hwnd+//! 205698, owned by the SOLIDWORKS main frame, style 0x96CC02C4, exstyle+//! 0x10101, rect (0,0)-(853,533), file name EMPTY. It appeared between 14:57+//! and 15:00Z during a bridge battery with Drew away, and blocked every+//! swx_stop after it; the census called it `kind:"unknown"`,+//! `bridgeCaused:false`, and swx_state once reported `blockingDialogs: []`+//! while it was up. Everything here runs in the simulator.++mod common;++use common::{Bridge, TempDir};+use serde_json::{json, Value};++fn s(v: &Value) -> String {+ serde_json::to_string_pretty(v).unwrap_or_default()+}++/// The window as measured, owned by the simulated main frame (6622700) and+/// with that frame DISABLED — a modal.+fn file_open(hwnd: i64) -> Value {+ json!({"hwnd": hwnd, "className": "#32770", "title": "Open ", "visible": true,+ "owner": 6622700, "ownerEnabled": false, "style": 0x96CC_02C4u64, "exStyle": 0x10101u64, "rect": [0, 0, 853, 533]})+}++struct Machine {+ dir: TempDir,+}++impl Machine {+ fn new(tag: &str) -> Machine {+ let dir = TempDir::new(tag);+ std::fs::write(dir.join("quit-script.json"), json!({"a": "exited"}).to_string()).unwrap();+ std::fs::write(dir.join("sldworks.json"), json!({"responding": true, "hwnd": 6622700, "privateMB": 900, "handles": 4000}).to_string()).unwrap();+ std::fs::write(dir.join("windows.json"), "[]").unwrap();+ // Nobody has touched the machine for an hour.+ std::fs::write(dir.join("last-input-secs"), "3600").unwrap();+ Machine { dir }+ }+ fn write(&self, name: &str, v: Value) {+ std::fs::write(self.dir.join(name), v.to_string()).unwrap();+ }+ fn boot(&self) -> Bridge {+ let b = Bridge::start_in(+ TempDir::new("dialogs-scratch"),+ &[+ ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),+ ("SWX_ALLOW_USER_WRITES", "1"),+ ("SWX_QUIT_SETTLE_MS", "80"),+ ("SWX_QUIT_EXIT_WAIT_SECS", "3"),+ ("SWX_OWNER_SETTLE_MS", "10"),+ ],+ );+ b.call("swx_start", json!({}));+ b+ }+ fn windows(&self) -> Vec<Value> {+ serde_json::from_str::<Vec<Value>>(&std::fs::read_to_string(self.dir.join("windows.json")).unwrap()).unwrap()+ }+}++fn stop(b: &Bridge) -> Value {+ b.call("swx_stop", json!({"userAuthorized": true, "confirm": true, "reason": "test: end the session", "maxRung": "a"}))+}++fn cancel(b: &Bridge) -> Value {+ b.call("swx_dismiss_blocking_dialogs", json!({"close": true, "confirm": true, "reason": "test: cancel the bridge's own Open dialog"}))+}++#[test]+fn the_census_is_native_deterministic_and_never_drops_a_visible_modal() {+ let m = Machine::new("dlg-census");+ let mut second = file_open(205699);+ second["title"] = json!("SOLIDWORKS");+ m.write("windows.json", json!([file_open(205698), second]));+ let b = m.boot();+ let mut seen = Vec::new();+ for shot in [true, false, false] {+ let r = b.call("swx_state", json!({"screenshot": shot}));+ let hw: Vec<i64> = r["blockingDialogs"].as_array().cloned().unwrap_or_default().iter().filter_map(|d| d["hwnd"].as_i64()).collect();+ seen.push(hw.clone());+ check!(+ b,+ &format!("swx_state (screenshot:{shot}): BOTH visible modals, from the native census, every time"),+ r["dialogCensusSource"] == "native" && r["blockingDialogsKnown"] == true && hw == vec![205698, 205699],+ s(&r)+ );+ }+ let d = &b.call("swx_state", json!({"screenshot": false}))["blockingDialogs"][0];+ check!(+ b,+ "...each with hwnd, title, class, owner, rect and visibility",+ d["hwnd"] == 205698 && d["title"] == "Open " && d["className"] == "#32770" && d["owner"] == 6622700+ && d["rect"] == json!([0, 0, 853, 533]) && d["visible"] == true,+ s(d)+ );+ let st = stop(&b);+ check!(+ b,+ "swx_stop refuses on the first, and REPORTS the second too",+ st["errorCode"] == "stop_blocked_by_dialog" && st["preconditions"]["moreBlockingDialogs"][0]["hwnd"] == 205699,+ s(&st)+ );+}++#[test]+fn a_file_dialog_a_bridge_verb_raised_is_attributed_and_the_bridge_may_cancel_it() {+ let m = Machine::new("dlg-ours");+ let b = m.boot();+ // The File ▸ Open goes up while swx_docs runs.+ m.write("dialog-on-docs.json", json!([file_open(205698)]));+ m.write("file-dialogs.json", json!({"205698": {"standard": true, "fileName": ""}}));+ let docs = b.call("swx_docs", json!({}));+ check!(+ b,+ "swx_docs says a dialog appeared while it ran, and which",+ docs["dialogsAppeared"][0]["hwnd"] == 205698 && docs["dialogsAppeared"][0]["appearedDuring"][0]["verb"] == "swx_docs",+ s(&docs)+ );+ let st = stop(&b);+ let d = &st["preconditions"]["dialog"];+ check!(+ b,+ "swx_stop's refusal names the dialog: title, hwnd, owner, age and attribution — bridge-caused, by swx_docs",+ st["errorCode"] == "stop_blocked_by_dialog"+ && st["error"].as_str().unwrap_or("").contains("hwnd 205698")+ && st["error"].as_str().unwrap_or("").contains("owned by 6622700")+ && d["attribution"]["bridgeCaused"] == true+ && d["attribution"]["bridgeCausedBy"] == "swx_docs"+ && d["attribution"]["ageSeconds"].is_u64()+ && d["bridgeMayCancel"] == true+ && d["fileDialog"]["fileNameEmpty"] == true,+ s(&st)+ );+ let c = cancel(&b);+ check!(+ b,+ "swx_dismiss_blocking_dialogs CANCELS it (IDCANCEL), without the process-level opt-in it needs for anything else",+ c["cancelled"][0]["hwnd"] == 205698 && c["cancelled"][0]["ok"] == true && c["cancelled"][0]["raisedDuring"] == "swx_docs",+ s(&c)+ );+ check!(b, "...the window is gone", m.windows().is_empty(), "");+ let led = b.call("swx_action_ledger", json!({"limit": 50}));+ check!(+ b,+ "...and the cancel is ledgered with its attribution",+ led["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "dialog_cancelled" && e["detail"]["raisedDuring"] == "swx_docs")).unwrap_or(false)+ && led["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "dialog_appeared")).unwrap_or(false),+ s(&led)+ );+ let after = stop(&b);+ check!(b, "and swx_stop goes through", after["success"] == true && after["stopped"] == true, s(&after));+}++#[test]+fn a_dialog_with_text_typed_or_input_since_or_of_unknown_origin_is_never_cancelled() {+ // 1. Text typed into the file name: the user's.+ let m = Machine::new("dlg-typed");+ let b = m.boot();+ m.write("dialog-on-docs.json", json!([file_open(205698)]));+ m.write("file-dialogs.json", json!({"205698": {"standard": true, "fileName": "crate-v3"}}));+ b.call("swx_docs", json!({}));+ let c = cancel(&b);+ check!(+ b,+ "typed text: refused, and the window stays",+ c["cancelled"].as_array().map(|a| a.is_empty()).unwrap_or(false)+ && c["cancelRefused"][0]["why"].as_str().unwrap_or("").contains("typed")+ && m.windows().len() == 1+ && !m.dir.join("cancelled-dialogs.jsonl").exists(),+ s(&c)+ );+ let st = stop(&b);+ check!(b, "...and swx_stop says the user cancels it", st["preconditions"]["dialog"]["whoAnswers"] == "the user cancels it", s(&st));++ // 2. The user gave input after the verb began.+ let m = Machine::new("dlg-input");+ let b = m.boot();+ m.write("dialog-on-docs.json", json!([file_open(205698)]));+ m.write("file-dialogs.json", json!({"205698": {"standard": true, "fileName": ""}}));+ b.call("swx_docs", json!({}));+ std::fs::write(m.dir.join("last-input-secs"), "0").unwrap();+ let c = cancel(&b);+ check!(b, "input since: refused", c["cancelled"].as_array().map(|a| a.is_empty()).unwrap_or(false) && m.windows().len() == 1, s(&c));++ // 3. Up before the bridge ever looked: origin unknown.+ let m = Machine::new("dlg-before");+ m.write("windows.json", json!([file_open(205698)]));+ m.write("file-dialogs.json", json!({"205698": {"standard": true, "fileName": ""}}));+ let b = m.boot();+ let st = stop(&b);+ check!(+ b,+ "a dialog up before the first census: unattributed, never claimed",+ st["preconditions"]["dialog"]["attribution"]["bridgeCaused"] == false && st["preconditions"]["dialog"]["bridgeMayCancel"] == false,+ s(&st["preconditions"]["dialog"])+ );+ let c = cancel(&b);+ check!(b, "...and never cancelled", c["cancelled"].as_array().map(|a| a.is_empty()).unwrap_or(false) && m.windows().len() == 1, s(&c));++ // 4. Not the standard file dialog.+ let m = Machine::new("dlg-other");+ let b = m.boot();+ let mut other = file_open(205700);+ other["title"] = json!("Rebuild Errors");+ m.write("dialog-on-docs.json", json!([other]));+ m.write("file-dialogs.json", json!({"205700": {"standard": false, "fileName": ""}}));+ b.call("swx_docs", json!({}));+ let c = cancel(&b);+ check!(+ b,+ "another dialog (even raised by a bridge verb, nothing typed): never cancelled",+ c["cancelled"].as_array().map(|a| a.is_empty()).unwrap_or(false)+ && c["cancelRefused"][0]["why"].as_str().unwrap_or("").contains("not the standard Windows file dialog")+ && m.windows().len() == 1,+ s(&c)+ );+}
tests/e2e.rs+6−4@@ -1160,12 +1160,14 @@ fn every_verb_over_http_against_the_real_binary() { r["blockingDialogs"].is_array() || r["blockingDialogs"].is_null(), s(&r) );+ // 0.11.1: the census is NATIVE (the process's own windows), so ab being+ // unreachable no longer leaves it unknown; where the native census cannot be+ // read and ab is the fallback, an unreachable ab is still UNKNOWN. check!( b,- "...and an unreachable ab makes it UNKNOWN, never an empty 'nothing in the way'",- r["blockingDialogs"].is_null()- && r["blockingDialogsKnown"] == false- && r["blockingDialogsError"].is_string(),+ "...native and known, or (ab fallback, ab unreachable) UNKNOWN — never an empty 'nothing in the way' it could not see",+ (r["dialogCensusSource"] == "native" && r["blockingDialogsKnown"] == true && r["blockingDialogs"].is_array())+ || (r["blockingDialogs"].is_null() && r["blockingDialogsKnown"] == false && r["blockingDialogsError"].is_string()), s(&r) ); check!(b, "state carries this machine's measured timings", r["timings"].is_object(), s(&r));
uninstall.sh