AI bridge: fix cross-realm reply drop + add diagnostics

read_editor_buffer came back empty in testing: the reply to the chat
iframe was dropped because event.source (the iframe's Window) is from
another JS realm, so dyn_into::<Window>() returned None.

- reply via unchecked_ref::<Window>() (postMessage works cross-realm even
  though instanceof Window does not), plus a DOM-query fallback that posts
  straight to the '#ai-assistant-editor-embed iframe' contentWindow.
- console.log at each step ([cmms-ai-bridge] …); on-page command-line
  error if save_to_bytes fails.
- Cargo.toml: web-sys HtmlIFrameElement.
This commit is contained in:
AI Assistant 2026-09-03 13:04:43 +10:00
commit 89e014afa1
3 changed files with 72 additions and 35 deletions

View file

@ -92,6 +92,7 @@ web-sys = { version = "=0.3.85", features = [
"Document", "Document",
"Element", "Element",
"HtmlAnchorElement", "HtmlAnchorElement",
"HtmlIFrameElement",
"Blob", "Blob",
"File", "File",
"FileSystemDirectoryHandle", "FileSystemDirectoryHandle",

View file

@ -877,19 +877,26 @@ impl OpenCADStudio {
// Reply with the active drawing as ASCII DXF (the editor's // Reply with the active drawing as ASCII DXF (the editor's
// export format regardless of the on-disk format). An empty // export format regardless of the on-disk format). An empty
// reply tells the CMMS read tool the buffer isn't ready. // reply tells the CMMS read tool the buffer isn't ready.
let dxf = self let dxf = match self.tabs.get(self.active_tab).map(|tab| {
.tabs crate::io::save_to_bytes(
.get(self.active_tab) &tab.scene.document,
.and_then(|tab| { "dxf",
crate::io::save_to_bytes( tab.scene.document.version,
&tab.scene.document, )
"dxf", }) {
tab.scene.document.version, Some(Ok(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
) Some(Err(e)) => {
.ok() self.command_line.push_error(
}) crate::tf!("AI Assistant: could not read the drawing: {e}").as_ref(),
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) );
.unwrap_or_default(); String::new()
}
None => String::new(),
};
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
"[cmms-ai-bridge] poll: want_read, replying {} bytes DXF",
dxf.len()
)));
crate::sys::reply_ai_editor_buffer(&dxf); crate::sys::reply_ai_editor_buffer(&dxf);
} }
if let Some(dxf) = apply { if let Some(dxf) = apply {

View file

@ -560,7 +560,11 @@ mod ai_editor_bridge {
struct State { struct State {
listener: Option<Closure<dyn FnMut(web_sys::MessageEvent)>>, listener: Option<Closure<dyn FnMut(web_sys::MessageEvent)>>,
reply_to: Option<web_sys::Window>, // The chat iframe's Window, kept as a raw JsValue: it comes from
// another realm (the <iframe>), so `instanceof Window` / `dyn_into`
// would fail - we only ever call `.postMessage()` on it, which works
// cross-realm.
reply_to: Option<JsValue>,
want_read: bool, want_read: bool,
pending_apply: Option<String>, pending_apply: Option<String>,
} }
@ -574,6 +578,10 @@ mod ai_editor_bridge {
}) }; }) };
} }
fn log(msg: &str) {
web_sys::console::log_1(&JsValue::from_str(&format!("[cmms-ai-bridge] {msg}")));
}
fn str_field(obj: &JsValue, key: &str) -> Option<String> { fn str_field(obj: &JsValue, key: &str) -> Option<String> {
js_sys::Reflect::get(obj, &JsValue::from_str(key)) js_sys::Reflect::get(obj, &JsValue::from_str(key))
.ok()? .ok()?
@ -601,7 +609,8 @@ mod ai_editor_bridge {
&JsValue::from_str("source"), &JsValue::from_str("source"),
) )
.ok() .ok()
.and_then(|s| s.dyn_into::<web_sys::Window>().ok()); .filter(|s| !s.is_null() && !s.is_undefined());
log(&format!("get-buffer received (source={})", source.is_some()));
STATE.with(|state| { STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
state.reply_to = source; state.reply_to = source;
@ -613,6 +622,7 @@ mod ai_editor_bridge {
return; return;
} }
if let Some(new_source) = str_field(&data, "new_source") { if let Some(new_source) = str_field(&data, "new_source") {
log(&format!("apply received ({} bytes)", new_source.len()));
STATE.with(|state| { STATE.with(|state| {
state.borrow_mut().pending_apply = Some(new_source); state.borrow_mut().pending_apply = Some(new_source);
}); });
@ -625,6 +635,7 @@ mod ai_editor_bridge {
.add_event_listener_with_callback("message", cb.as_ref().unchecked_ref()); .add_event_listener_with_callback("message", cb.as_ref().unchecked_ref());
state.listener = Some(cb); state.listener = Some(cb);
}); });
log("message listener installed");
} }
/// Drain any queued bridge requests: `(a read was asked for, a buffer to apply)`. /// Drain any queued bridge requests: `(a read was asked for, a buffer to apply)`.
@ -639,27 +650,45 @@ mod ai_editor_bridge {
} }
/// Post the current drawing (ASCII DXF) back to the chat iframe that asked. /// Post the current drawing (ASCII DXF) back to the chat iframe that asked.
/// Also broadcasts to the embedded chat iframe found in the DOM, in case
/// `event.source` was unusable.
pub fn reply_buffer(dxf: &str) { pub fn reply_buffer(dxf: &str) {
STATE.with(|state| { let message = js_sys::Object::new();
let Some(target) = state.borrow_mut().reply_to.take() else { let _ = js_sys::Reflect::set(
return; &message,
}; &JsValue::from_str("type"),
let message = js_sys::Object::new(); &JsValue::from_str("cmms-ai-editor-buffer"),
let _ = js_sys::Reflect::set( );
&message, let _ = js_sys::Reflect::set(
&JsValue::from_str("type"), &message,
&JsValue::from_str("cmms-ai-editor-buffer"), &JsValue::from_str("buffer"),
); &JsValue::from_str(dxf),
let _ = js_sys::Reflect::set( );
&message, let origin = web_sys::window()
&JsValue::from_str("buffer"), .and_then(|w| w.location().origin().ok())
&JsValue::from_str(dxf), .unwrap_or_else(|| "*".to_string());
);
let origin = web_sys::window() let mut posted = false;
.and_then(|w| w.location().origin().ok()) if let Some(target) = STATE.with(|state| state.borrow_mut().reply_to.take()) {
.unwrap_or_else(|| "*".to_string()); if target
let _ = target.post_message(&message, &origin); .unchecked_ref::<web_sys::Window>()
}); .post_message(&message, &origin)
.is_ok()
{
posted = true;
}
}
// Fallback: post straight to the embedded chat iframe's contentWindow.
if let Some(frame_win) = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.query_selector("#ai-assistant-editor-embed iframe").ok().flatten())
.and_then(|el| el.dyn_into::<web_sys::HtmlIFrameElement>().ok())
.and_then(|f| f.content_window())
{
let _ = frame_win.post_message(&message, &origin);
posted = true;
}
log(&format!("reply_buffer: {} bytes, delivered={}", dxf.len(), posted));
} }
} }