diff --git a/crates/ui-egui/src/app/screenshot.rs b/crates/ui-egui/src/app/screenshot.rs index ce97fc747..c34d022e5 100644 --- a/crates/ui-egui/src/app/screenshot.rs +++ b/crates/ui-egui/src/app/screenshot.rs @@ -157,9 +157,12 @@ impl VectorcraftApp { let pending = self.pending_screenshots.remove(index); self.screenshot_reserved = self.screenshot_reserved.saturating_sub(pending.reserved_bytes); let roots = self.automation_roots.clone(); - let _ = pending - .reply - .send(file_access::confine(roots.as_ref(), || control::save_screenshot(self, &image, pending.path.as_deref(), pending.data))); + let reply = file_access::confine(roots.as_ref(), || { + let mut reply = control::save_screenshot(self, &image, pending.path.as_deref(), pending.data); + control::hide_in_deferred_reply(self, &mut reply, &json!({"path": pending.path})); + reply + }); + let _ = pending.reply.send(reply); } Err(e) => { let error = format!("the screenshot could not be used: {e}"); diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 29c9fbbb5..066d3d82d 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -1027,8 +1027,9 @@ const DOCKING_ACTION: &str = "action"; /// errors included, an error from a panic too: the libraries hidden before it ran and after it. The /// error of a failed request also hides the paths outside the read root that the request sent /// ([`sent_paths`]). A reply with no `/`, `\` or `%` in it is not searched, since every hidden path -/// holds a separator, written or escaped. A deferred reply (a screenshot, a `ui.pointer` reply sent -/// after the tool's edit) holds no library id or path. +/// holds a separator, written or escaped. A screenshot's reply is hidden when it is sent +/// ([`hide_in_deferred_reply`]); a `ui.pointer` reply sent after the tool's edit holds no library id +/// or path. pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { if vectorcraft_engine::file_access::current().is_none() { return dispatch(app, ctx, req); @@ -1056,18 +1057,33 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques } }; let Outcome::Done(mut reply) = outcome else { return outcome }; - if !holds_a_separator(&reply) { - return Outcome::Done(reply); + hide_in_reply(app, &mut reply, &req.params, before); + Outcome::Done(reply) +} + +/// Hides in `reply` the libraries hidden now and in `before`, and for a failed request the paths +/// outside the read root that `sent`, the request's params, holds ([`handle`]). +fn hide_in_reply(app: &VectorcraftApp, reply: &mut Value, sent: &Value, before: Hiding) { + if !holds_a_separator(reply) { + return; } let mut hiding = unreadable_library_ids(app); hiding.merge(before); if reply.get("ok").and_then(Value::as_bool) == Some(false) { - let mut sent = Vec::new(); - sent_paths(&req.params, &mut sent); - hiding.set_hidden(sent, cfg!(windows)); + let mut paths = Vec::new(); + sent_paths(sent, &mut paths); + hiding.set_hidden(paths, cfg!(windows)); + } + hide_library_ids(reply, &hiding); +} + +/// Hides in a reply sent after its request was handled (a screenshot's) what [`handle`] hides in a +/// reply, with `sent` as the request's params. Without a read root in force it leaves the reply as +/// it is. +pub(crate) fn hide_in_deferred_reply(app: &VectorcraftApp, reply: &mut Value, sent: &Value) { + if vectorcraft_engine::file_access::current().is_some() { + hide_in_reply(app, reply, sent, Hiding::default()); } - hide_library_ids(&mut reply, &hiding); - Outcome::Done(reply) } /// The control methods that send synthetic input. diff --git a/crates/ui-egui/src/tests/automation.rs b/crates/ui-egui/src/tests/automation.rs index 01929f578..05811869a 100644 --- a/crates/ui-egui/src/tests/automation.rs +++ b/crates/ui-egui/src/tests/automation.rs @@ -704,6 +704,26 @@ fn an_error_hides_a_sent_path_whether_or_not_a_library_was_loaded_from_it() { let _ = std::fs::remove_dir_all(base); } +/// A screenshot's reply, sent after its request was handled, hides a hidden library's path, and in an +/// error the path outside the read root that the request sent. +#[test] +fn a_screenshot_reply_hides_paths_like_any_reply() { + let (mut app, _tx, base) = confined("deferred-reply"); + let path = s(base.join("outside/b.svg")); + app.ui.color_guide_limit = format!("loaded/{path}"); + let sent = s(base.join("outside/shot.png")); + let roots = AutomationRoots::new(&[&base.join("in")], &[&base.join("out")]).unwrap(); + let mut done = json!({"ok": true, "result": {"note": format!("from {path}")}}); + let mut failed = json!({"ok": false, "error": format!("could not write {sent}")}); + vectorcraft_engine::file_access::confine(roots.as_ref(), || { + crate::control::hide_in_deferred_reply(&app, &mut done, &json!({"path": sent})); + crate::control::hide_in_deferred_reply(&app, &mut failed, &json!({"path": sent})); + }); + assert_eq!(done["result"]["note"], "from b.svg", "{done}"); + assert_eq!(failed["error"], "could not write shot.png", "{failed}"); + let _ = std::fs::remove_dir_all(base); +} + /// A reply whose only trace of a hidden library's path is a `file:` URL with every separator escaped /// shows the file name. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 938833d02..278d69e8d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -154,8 +154,8 @@ the suite) keep the agent to the folders you give it: reopens its panel), and the reply to such a request shows that id and file name wherever the library's id or path appears, an error included. Every reply under the read roots shows a hidden library's id as the id shown and its path as the file name in every string and key, the status text (`ui.status`), dialogs, the command palette query - (`ui.palette_query`), a script's report (`scriptReport`) and the error from an internal fault included, for the - libraries hidden before the request ran and after it. The path is also matched with `%XX` escapes as a `file:` + (`ui.palette_query`), a script's report (`scriptReport`), a screenshot's reply and the error from an internal + fault included, for the libraries hidden before the request ran and after it. The path is also matched with `%XX` escapes as a `file:` URL writes it, and on Windows in any case of its letters. A hidden id or path stays only where it begins another loaded library's whole id with no `.` or `..` part, so a readable library's id that begins with a hidden one stays. The error of a failed request also shows a path outside the read roots that the request sent by its file name, whether or not a library was loaded from it, so that the