//! Runtime host surface (`host` feature). //! //! [`HostApi`] is the `acadrust`-typed adapter a plugin uses at *dispatch* time //! — document access, entity creation, XDATA, undo, and the command line. It is //! the stable counterpart to the dependency-free manifest/ribbon contract: a //! plugin's `dispatch` receives `&mut dyn HostApi` rather than the host's //! concrete session type, so an out-of-tree add-on compiles against this crate //! alone. //! //! Per-tab plugin state is keyed by `manifest.id`. The trait exposes it in an //! object-safe `Any` form; use the [`plugin_state`], [`plugin_state_mut`] and //! [`ensure_plugin_state`] helpers for the ergonomic typed access. use std::any::Any; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::manifest::PluginManifest; use crate::ribbon::CadModule; // Re-export the acadrust crate and the types that appear in the HostApi trait // so out-of-tree plugins can use them without adding their own acadrust // dependency (which would risk an ABI-mismatching version). pub use acadrust; pub use acadrust::{CadDocument, EntityType, Handle}; pub use acadrust::xdata::ExtendedDataRecord; use crate::ipc::protocol::{PluginRequest, PluginResponse}; /// Thread-safe handle that can issue host requests from plugin worker threads. /// Out-of-process V4 plugins implement this; in-process hosts may return `None`. pub trait PluginRequestSender: Send + Sync { fn request(&self, req: PluginRequest) -> Result; } /// Error returned when a plugin worker thread cannot issue a host request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginRequestError(pub String); impl std::fmt::Display for PluginRequestError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PluginRequestError: {}", self.0) } } impl std::error::Error for PluginRequestError {} /// Log level carried by [`PluginNotification::Log`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub enum LogLevel { Debug, Info, Warn, Error, } /// A notification the host sends to a plugin. These are best-effort, /// full-duplex messages correlated with an optional `command_id`. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum HostNotification { InputLine { line: String }, Cancel, DocumentChanged { version: u64 }, SelectionChanged { handles: Vec }, Raw(Vec), /// V4 snapshot changed for a specific tab. Discriminant 5. DocumentChangedV4 { tab_id: u64, version: u64 }, /// V4 tab closed notification. Discriminant 6. DocumentTabClosed { tab_id: u64 }, /// V4 selection changed for a specific tab. Discriminant 7. SelectionChangedV4 { tab_id: u64, handles: Vec }, /// Fallback for notification variants added in future minor revisions. /// Carries the raw bincode payload so an older peer can ignore it without /// failing deserialization. Unknown(Vec), } impl Serialize for HostNotification { fn serialize(&self, serializer: S) -> Result { let mut bytes = Vec::new(); match self { HostNotification::InputLine { line } => { bytes.push(0); bincode::serialize_into(&mut bytes, line) .map_err(serde::ser::Error::custom)?; } HostNotification::Cancel => bytes.push(1), HostNotification::DocumentChanged { version } => { bytes.push(2); bincode::serialize_into(&mut bytes, version) .map_err(serde::ser::Error::custom)?; } HostNotification::SelectionChanged { handles } => { bytes.push(3); bincode::serialize_into(&mut bytes, handles) .map_err(serde::ser::Error::custom)?; } HostNotification::Raw(data) => { bytes.push(4); bincode::serialize_into(&mut bytes, data) .map_err(serde::ser::Error::custom)?; } HostNotification::DocumentChangedV4 { tab_id, version } => { bytes.push(5); bincode::serialize_into(&mut bytes, tab_id) .map_err(serde::ser::Error::custom)?; bincode::serialize_into(&mut bytes, version) .map_err(serde::ser::Error::custom)?; } HostNotification::DocumentTabClosed { tab_id } => { bytes.push(6); bincode::serialize_into(&mut bytes, tab_id) .map_err(serde::ser::Error::custom)?; } HostNotification::SelectionChangedV4 { tab_id, handles } => { bytes.push(7); bincode::serialize_into(&mut bytes, tab_id) .map_err(serde::ser::Error::custom)?; bincode::serialize_into(&mut bytes, handles) .map_err(serde::ser::Error::custom)?; } HostNotification::Unknown(raw) => bytes.extend_from_slice(raw), } bytes.serialize(serializer) } } impl<'de> Deserialize<'de> for HostNotification { fn deserialize>(deserializer: D) -> Result { let bytes = Vec::::deserialize(deserializer)?; if bytes.is_empty() { return Err(serde::de::Error::custom("empty HostNotification")); } let discriminant = bytes[0]; let rest = &bytes[1..]; match discriminant { 0 => bincode::deserialize(rest) .map(|line| HostNotification::InputLine { line }) .map_err(serde::de::Error::custom), 1 => Ok(HostNotification::Cancel), 2 => bincode::deserialize(rest) .map(|version| HostNotification::DocumentChanged { version }) .map_err(serde::de::Error::custom), 3 => bincode::deserialize(rest) .map(|handles| HostNotification::SelectionChanged { handles }) .map_err(serde::de::Error::custom), 4 => bincode::deserialize(rest) .map(HostNotification::Raw) .map_err(serde::de::Error::custom), 5 => bincode::deserialize(rest) .map(|(tab_id, version)| HostNotification::DocumentChangedV4 { tab_id, version }) .map_err(serde::de::Error::custom), 6 => bincode::deserialize(rest) .map(|tab_id| HostNotification::DocumentTabClosed { tab_id }) .map_err(serde::de::Error::custom), 7 => bincode::deserialize(rest) .map(|(tab_id, handles)| HostNotification::SelectionChangedV4 { tab_id, handles }) .map_err(serde::de::Error::custom), _ => Ok(HostNotification::Unknown(bytes)), } } } /// A notification a plugin sends to the host. These are best-effort, /// full-duplex messages correlated with an optional `command_id`. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum PluginNotification { Output { text: String }, Error { text: String }, Prompt { text: String }, Progress { percent: u8 }, Log { level: LogLevel, text: String }, Raw(Vec), /// V4 REPL status update. Discriminant 6. ReplStatus { status: String, message: String }, /// Fallback for notification variants added in future minor revisions. /// Carries the raw bincode payload so an older peer can ignore it without /// failing deserialization. Unknown(Vec), } impl Serialize for PluginNotification { fn serialize(&self, serializer: S) -> Result { let mut bytes = Vec::new(); match self { PluginNotification::Output { text } => { bytes.push(0); bincode::serialize_into(&mut bytes, text) .map_err(serde::ser::Error::custom)?; } PluginNotification::Error { text } => { bytes.push(1); bincode::serialize_into(&mut bytes, text) .map_err(serde::ser::Error::custom)?; } PluginNotification::Prompt { text } => { bytes.push(2); bincode::serialize_into(&mut bytes, text) .map_err(serde::ser::Error::custom)?; } PluginNotification::Progress { percent } => { bytes.push(3); bincode::serialize_into(&mut bytes, percent) .map_err(serde::ser::Error::custom)?; } PluginNotification::Log { level, text } => { bytes.push(4); bincode::serialize_into(&mut bytes, level) .map_err(serde::ser::Error::custom)?; bincode::serialize_into(&mut bytes, text) .map_err(serde::ser::Error::custom)?; } PluginNotification::Raw(data) => { bytes.push(5); bincode::serialize_into(&mut bytes, data) .map_err(serde::ser::Error::custom)?; } PluginNotification::ReplStatus { status, message } => { bytes.push(6); bincode::serialize_into(&mut bytes, status) .map_err(serde::ser::Error::custom)?; bincode::serialize_into(&mut bytes, message) .map_err(serde::ser::Error::custom)?; } PluginNotification::Unknown(raw) => bytes.extend_from_slice(raw), } bytes.serialize(serializer) } } impl<'de> Deserialize<'de> for PluginNotification { fn deserialize>(deserializer: D) -> Result { let bytes = Vec::::deserialize(deserializer)?; if bytes.is_empty() { return Err(serde::de::Error::custom("empty PluginNotification")); } let discriminant = bytes[0]; let rest = &bytes[1..]; match discriminant { 0 => bincode::deserialize(rest) .map(|text| PluginNotification::Output { text }) .map_err(serde::de::Error::custom), 1 => bincode::deserialize(rest) .map(|text| PluginNotification::Error { text }) .map_err(serde::de::Error::custom), 2 => bincode::deserialize(rest) .map(|text| PluginNotification::Prompt { text }) .map_err(serde::de::Error::custom), 3 => bincode::deserialize(rest) .map(|percent| PluginNotification::Progress { percent }) .map_err(serde::de::Error::custom), 4 => { let (level, text): (LogLevel, String) = bincode::deserialize(rest) .map_err(serde::de::Error::custom)?; Ok(PluginNotification::Log { level, text }) } 5 => bincode::deserialize(rest) .map(PluginNotification::Raw) .map_err(serde::de::Error::custom), 6 => bincode::deserialize(rest) .map(|(status, message)| PluginNotification::ReplStatus { status, message }) .map_err(serde::de::Error::custom), _ => Ok(PluginNotification::Unknown(bytes)), } } } /// An add-on package's entry point: its manifest, optional ribbon tab, and /// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins /// implement the same trait from this crate, so an out-of-tree add-on targets /// the stable contract rather than the host binary. pub trait BuiltinPlugin: Send + Sync { fn manifest(&self) -> &'static PluginManifest; fn ribbon(&self) -> Box; fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool; /// Long-running REPL code execution (API v4). The plugin may start the /// work on another thread and call `respond` when finished; the runner /// will forward the result to the host. The supplied `host` is the active /// document tab's API surface, so the REPL session is tied to a tab. /// /// Returning `false` means the plugin does not support code execution. fn start_execute_code( &mut self, _host: &mut dyn HostApi, _command_id: u64, _code: &str, _source: CommandSource, _respond: Box, ) -> bool { false } /// State-only callback for host-to-plugin notifications. Added in API v4; /// the V4 runner guarantees it is only called for plugins that report /// API major 4 or newer. fn on_notification(&mut self, _command_id: Option, _notification: HostNotification) {} } /// A point-driven interactive command a plugin starts via /// [`HostApi::start_interactive`]. The host shows the prompt, collects points — /// clicked in the viewport, or fed as coordinates over the `--serve` automation /// API — and commits the entities the command yields, exactly like a built-in /// tool. This is the plugin-facing slice of the host's command machinery; it /// covers click-to-place placement without exposing the host's internal command /// trait. pub trait InteractiveCommand: Send { /// Prompt for the next point. fn prompt(&self) -> String; /// A point was supplied (clicked or typed `x,y[,z]`). Returns the next step. fn on_point(&mut self, pt: [f64; 3]) -> CommandStep; /// Enter pressed with no point — e.g. to finish a multi-point command. fn on_enter(&mut self) -> CommandStep { CommandStep::Cancel } /// When `true`, the next input picks an existing **entity** (the user clicks /// on it; over `--serve`, a handle is supplied) rather than a free point — /// the host then calls [`on_object_pick`](Self::on_object_pick). Use this to /// reference existing geometry (e.g. connect a pipe between two structures). fn needs_object_pick(&self) -> bool { false } /// An existing entity was picked: its `handle` and the pick point. Read the /// entity's data (XDATA / geometry) via `HostApi`, keyed by the handle. fn on_object_pick(&mut self, _handle: Handle, _pt: [f64; 3]) -> CommandStep { CommandStep::Cancel } } /// The outcome of an [`InteractiveCommand`] step. #[derive(Debug)] #[cfg_attr(feature = "host", derive(serde::Serialize, serde::Deserialize))] pub enum CommandStep { /// Need another point; keep the command active. NeedPoint, /// Commit an entity to the document and keep collecting points. Commit(EntityType), /// Commit an entity and end the command. CommitAndEnd(EntityType), /// End the command without committing. Done, /// Cancel the command. Cancel, } /// Export a `BuiltinPlugin` from a `cdylib` so the host can load it at runtime. /// /// Emits the two C symbols the loader looks for: `ocs_plugin_api_version` /// (checked before anything else, so an ABI-incompatible build is rejected /// without running its code) and `ocs_plugin_register` (constructs the plugin /// and hands ownership to the host as a boxed trait object). /// /// ```ignore /// ocs_plugin_api::export_plugin!(MyPlugin::new()); /// ``` #[macro_export] macro_rules! export_plugin { ($ctor:expr) => { #[no_mangle] pub extern "C" fn ocs_plugin_api_version() -> u32 { $crate::API_VERSION } #[no_mangle] pub extern "C" fn ocs_plugin_register( ) -> *mut ::std::boxed::Box { // The constructor runs across a C ABI boundary; a panic unwinding // past it is undefined behavior. Contain it and report failure as a // null pointer, which the host loader treats as "registration // failed" rather than crashing the runner process. match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { let plugin: ::std::boxed::Box = ::std::boxed::Box::new($ctor); ::std::boxed::Box::into_raw(::std::boxed::Box::new(plugin)) })) { ::std::result::Result::Ok(ptr) => ptr, ::std::result::Result::Err(_) => ::std::ptr::null_mut(), } } }; } /// The plugin-facing runtime surface for one active document tab. pub trait HostApi { /// Index of the tab this session targets. fn tab_index(&self) -> usize; // ── Document ──────────────────────────────────────────────────────────── fn document(&self) -> &CadDocument; /// Mutable access to the active document. /// /// For an **out-of-process** plugin this borrows a *local snapshot*: edits /// to existing entities made through it are NOT sent back to the host and /// are silently discarded. To modify or delete entities from any plugin, /// use [`add_entity`](Self::add_entity), [`update_entity`](Self::update_entity) /// and [`remove_entity`](Self::remove_entity), which are committed to the /// host document over IPC. fn document_mut(&mut self) -> &mut CadDocument; /// Add an entity to the active document, returning its handle. fn add_entity(&mut self, entity: EntityType) -> Handle; /// Replace the existing entity that carries `entity`'s handle, preserving /// its identity (handle and owning block). Returns `false` when no entity /// has that handle. This is the sanctioned way to commit in-place edits /// from an out-of-process plugin — mutating `document_mut()` does not work /// across the process boundary. fn update_entity(&mut self, entity: EntityType) -> bool { let handle = entity.common().handle; match self.document_mut().get_entity_mut(handle) { Some(slot) => { *slot = entity; true } None => false, } } /// Delete the entity with `handle` (and any derived render caches). Returns /// `true` when an entity was removed. fn remove_entity(&mut self, handle: Handle) -> bool { self.document_mut().remove_entity(handle).is_some() } /// Mark the scene geometry dirty so it is re-tessellated next frame. fn bump_geometry(&mut self); // ── XDATA ─────────────────────────────────────────────────────────────── /// Read the XDATA record for `app_name` on entity `handle`, if any. fn read_record(&self, handle: Handle, app_name: &str) -> Option<&ExtendedDataRecord>; /// Attach `record` to entity `handle`, replacing any existing record for the /// same application and registering the APPID. Returns `false` if the entity /// does not exist. fn write_record(&mut self, handle: Handle, record: ExtendedDataRecord) -> bool; /// Remove the XDATA record for `app_name` from entity `handle`. Returns /// `true` if a record was removed. fn remove_record(&mut self, handle: Handle, app_name: &str) -> bool; // ── Undo / dirty ──────────────────────────────────────────────────────── fn push_undo(&mut self, label: &str); fn set_dirty(&mut self); // ── Command line ──────────────────────────────────────────────────────── fn push_info(&mut self, msg: &str); fn push_output(&mut self, msg: &str); fn push_error(&mut self, msg: &str); /// Start a plugin-defined interactive (click-to-place) command on the active /// tab. The host drives it through its normal point-collection flow. fn start_interactive(&mut self, command: Box); // ── Per-tab plugin state (object-safe; use the typed helpers below) ────── fn plugin_state_any(&self, plugin_id: &str) -> Option<&(dyn Any + Send + Sync)>; fn plugin_state_any_mut(&mut self, plugin_id: &str) -> Option<&mut (dyn Any + Send + Sync)>; /// Get the state for `plugin_id`, inserting `init()`'s result if absent. fn ensure_plugin_state_any( &mut self, plugin_id: &'static str, init: &mut dyn FnMut() -> Box, ) -> &mut (dyn Any + Send + Sync); // ── DocumentReader (added in API v3; appended at the end to keep vtable // indices stable for API v2 plugins) ───────────────────────────────────── /// Read-only, zero-copy view of the active document. For out-of-process /// plugins this is backed by host-owned shared memory; for in-process /// plugins it wraps `document()`. fn document_reader(&self) -> Box; /// Open (or refresh) the host-side shared document view and return the /// information the plugin needs to map it. In-process hosts implement this; /// out-of-process plugin proxies return `None`. fn document_view(&mut self) -> Option { None } // ── Notifications (added in API v4; appended at the end to keep vtable // indices stable for V2/V3 plugins) ─────────────────────────────────────── /// Send a best-effort notification from the plugin to the host. /// /// In-process plugins can override this to forward to the host event loop; /// out-of-process plugins send a V4 notification frame. fn notify_plugin(&mut self, _command_id: Option, _notification: PluginNotification) {} /// Poll for a host-to-plugin notification, if any. /// /// Returns the optional `command_id` used to correlate the notification /// with a running command, and the notification payload. Long-running /// plugins should call this periodically to drain the bounded queue. fn try_recv_notification( &mut self, ) -> Option<(Option, HostNotification)> { None } /// Returns a thread-safe handle that can issue host requests from worker /// threads. Out-of-process V4 plugins implement this; in-process hosts may /// return `None`. fn plugin_request_sender(&self) -> Option> { None } // ── V4 tab/document identity (added for REPL; appended at the end) ─────── /// Stable tab identifier for the active document tab. fn tab_id(&self) -> u64 { self.tab_index() as u64 } /// Open (or refresh) the host-side V4 shared document view for `tab_id` /// and return the mapping information. In-process hosts implement this; /// out-of-process plugin proxies return `None`. fn document_view_v4(&mut self, tab_id: u64) -> Option { let _ = tab_id; None } /// Close the host-side V4 shared document view for `tab_id`. fn close_document_view_v4(&mut self, tab_id: u64) { let _ = tab_id; } // ── Batch entities (added after API v4; appended at the very end so older // plugins compiled without it keep stable vtable indices) ──────────────── /// Add multiple entities to the active document, returning their handles. /// The default implementation calls [`add_entity`](Self::add_entity) for each /// entity; hosts should override it for batch efficiency. fn add_entities(&mut self, entities: Vec) -> Vec { entities.into_iter().map(|e| self.add_entity(e)).collect() } } /// Simplified, read-only entity kind exposed by [`DocumentReader`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReaderEntityKind { Point, Line, Circle, Arc, Polyline, Text, Other, } /// A 3D point returned by [`DocumentReader`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ReaderPoint { pub x: f64, pub y: f64, pub z: f64, } /// A read-only view of one entity, borrowed from a [`DocumentReader`]. pub struct ReaderEntity<'a> { /// Entity handle in the host document. pub handle: Handle, /// Simplified entity type. pub kind: ReaderEntityKind, /// Name of the layer the entity lives on. pub layer_name: &'a str, /// If the entity is a point, its coordinates. pub point: Option, } /// Read-only, zero-copy view of a CAD document. /// /// For out-of-process plugins this is backed by host-owned shared memory. The /// plugin receives only references into that mapping, so the document model is /// not copied into the plugin's heap. pub trait DocumentReader { /// Total number of entities in the document. fn entity_count(&self) -> usize; /// Iterate over all entities without allocating a full `CadDocument`. fn for_each_entity(&self, f: &mut dyn FnMut(ReaderEntity<'_>)); /// Look up a layer name by handle. fn layer_name(&self, handle: Handle) -> Option<&str>; /// Look up an APPID name by handle. fn app_id_name(&self, handle: Handle) -> Option<&str>; } impl ReaderEntityKind { /// Map a concrete `EntityType` to the simplified reader kind. pub fn from_entity(entity: &EntityType) -> Self { match entity { EntityType::Point(_) => ReaderEntityKind::Point, EntityType::Line(_) => ReaderEntityKind::Line, EntityType::Circle(_) => ReaderEntityKind::Circle, EntityType::Arc(_) => ReaderEntityKind::Arc, EntityType::Polyline(_) | EntityType::Polyline2D(_) | EntityType::Polyline3D(_) | EntityType::LwPolyline(_) => ReaderEntityKind::Polyline, EntityType::Text(_) | EntityType::MText(_) => ReaderEntityKind::Text, _ => ReaderEntityKind::Other, } } } /// In-process `DocumentReader` implementation that wraps a borrowed `CadDocument`. pub struct CadDocumentReader<'a>(pub &'a CadDocument); impl<'a> DocumentReader for CadDocumentReader<'a> { fn entity_count(&self) -> usize { self.0.entities().count() } fn for_each_entity(&self, f: &mut dyn FnMut(ReaderEntity<'_>)) { for entity in self.0.entities() { let kind = ReaderEntityKind::from_entity(entity); let layer_name = entity.common().layer.as_str(); let point = match entity { EntityType::Point(p) => Some(ReaderPoint { x: p.location.x, y: p.location.y, z: p.location.z, }), _ => None, }; f(ReaderEntity { handle: entity.common().handle, kind, layer_name, point, }); } } fn layer_name(&self, handle: Handle) -> Option<&str> { self.0 .layers .iter() .find(|layer| layer.handle == handle) .map(|layer| layer.name.as_str()) } fn app_id_name(&self, handle: Handle) -> Option<&str> { self.0 .app_ids .iter() .find(|app_id| app_id.handle == handle) .map(|app_id| app_id.name.as_str()) } } /// Typed read of per-tab plugin state stored under `plugin_id`. pub fn plugin_state<'a, T: Any + Send + Sync>( host: &'a dyn HostApi, plugin_id: &str, ) -> Option<&'a T> { host.plugin_state_any(plugin_id)?.downcast_ref::() } /// Typed mutable access to per-tab plugin state stored under `plugin_id`. pub fn plugin_state_mut<'a, T: Any + Send + Sync>( host: &'a mut dyn HostApi, plugin_id: &str, ) -> Option<&'a mut T> { host.plugin_state_any_mut(plugin_id)?.downcast_mut::() } /// Typed get-or-insert of per-tab plugin state stored under `plugin_id`. pub fn ensure_plugin_state<'a, T: Any + Send + Sync>( host: &'a mut dyn HostApi, plugin_id: &'static str, init: impl FnOnce() -> T, ) -> &'a mut T { let mut init = Some(init); let any = host.ensure_plugin_state_any(plugin_id, &mut || { Box::new((init.take().expect("init called once"))()) }); any.downcast_mut::() .expect("plugin state type mismatch for plugin_id") } #[cfg(feature = "host")] mod repl { use serde::{Deserialize, Serialize}; /// Source surface that submitted a REPL code snippet. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub enum CommandSource { CommandLine, Script, Editor, } /// Outcome of a REPL `ExecuteCode` request. #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] pub struct ExecutionResult { pub success: bool, pub output: Option, pub error: Option, pub error_type: Option, pub traceback: Option, pub line_number: Option, pub column_number: Option, pub duration_ms: f64, } impl ExecutionResult { /// Create a new execution result. pub fn new( success: bool, output: Option, error: Option, error_type: Option, traceback: Option, line_number: Option, column_number: Option, duration_ms: f64, ) -> Self { Self { success, output, error, error_type, traceback, line_number, column_number, duration_ms, } } } /// Spawn a thread that runs `f` and calls `respond` with the returned /// `ExecutionResult`. If `f` panics, `respond` is called with a panic error /// result instead, so the host never waits indefinitely for a callback that /// will never arrive. pub fn execute_code_guard( respond: Box, f: F, ) -> std::thread::JoinHandle<()> where F: FnOnce() -> ExecutionResult + Send + 'static, { std::thread::spawn(move || { let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { Ok(result) => result, Err(payload) => { let msg = payload .downcast_ref::<&str>() .copied() .or_else(|| payload.downcast_ref::().map(|s| s.as_str())) .unwrap_or("execute_code panicked"); ExecutionResult { success: false, output: None, error: Some(format!("panic: {}", msg)), error_type: Some("panic".to_string()), traceback: None, line_number: None, column_number: None, duration_ms: 0.0, } } }; respond(result); }) } } #[cfg(feature = "host")] pub use repl::{execute_code_guard, CommandSource, ExecutionResult};