use bcode_plugin::{PluginHost, PluginLoadError, PluginRuntimeHost, StaticBundledPlugin};
use bcode_plugin_sdk::tui::{
BoxedPluginTuiSurface, PluginTuiArtifactChunk, PluginTuiRegistry, PluginTuiSurfaceOpenRequest,
};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Instant;
static NEXT_PRESENTATION_IDENTITY: AtomicU64 = AtomicU64::new(1);
const MAX_DYNAMIC_VISUAL_CACHE_ENTRIES: usize = 512;
const DYNAMIC_VISUAL_QUEUE_CAPACITY: usize = 64;
const DYNAMIC_VISUAL_COMPLETION_CAPACITY: usize =
MAX_DYNAMIC_VISUAL_CACHE_ENTRIES + DYNAMIC_VISUAL_QUEUE_CAPACITY;
const DYNAMIC_VISUAL_SERVICE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
const MAX_DYNAMIC_VISUAL_COMPLETIONS_PER_POLL: usize = 16;
#[derive(Debug, Clone)]
enum PresentationPluginBackend {
Synchronous(Arc<PluginHost>),
Runtime(PluginRuntimeHost),
}
impl PresentationPluginBackend {
fn visual_adapters(
&self,
schema: &str,
schema_version: u32,
surface: &str,
producer_plugin_id: Option<&str>,
) -> Vec<bcode_plugin::PluginVisualAdapterRoute> {
match self {
Self::Synchronous(host) => {
host.visual_adapters(schema, schema_version, surface, producer_plugin_id)
}
Self::Runtime(runtime) => runtime.registry().visual_adapters(
schema,
schema_version,
surface,
producer_plugin_id,
),
}
}
fn keymap_actions_page(
&self,
after: Option<(&str, &str, &str)>,
limit: usize,
) -> bcode_plugin_sdk::keymap::KeymapActionPage {
match self {
Self::Synchronous(host) => host.keymap_actions_page(after, limit),
Self::Runtime(runtime) => runtime.registry().keymap_actions_page(after, limit),
}
}
fn tool_presentation(
&self,
tool_name: &str,
) -> Option<(&str, &bcode_plugin::PluginToolPresentationDeclaration)> {
match self {
Self::Synchronous(host) => host.tool_presentation(tool_name),
Self::Runtime(runtime) => runtime.tool_presentation(tool_name),
}
}
fn composer_contributions(
&self,
) -> Vec<bcode_plugin_sdk::composer::PluginOwnedComposerContribution> {
match self {
Self::Synchronous(host) => {
let plugins = host
.loaded_plugins()
.iter()
.map(|plugin| bcode_plugin::RegisteredPlugin {
manifest_path: std::path::PathBuf::new(),
manifest: plugin.manifest().clone(),
})
.collect::<Vec<_>>();
bcode_plugin::plugin_composer_contributions(&plugins)
}
Self::Runtime(runtime) => runtime.registry().composer_contributions(),
}
}
fn has_service(&self, plugin_id: &str, interface_id: &str) -> bool {
match self {
Self::Synchronous(host) => host.has_service(plugin_id, interface_id),
Self::Runtime(runtime) => {
runtime
.registry()
.manifests()
.get(plugin_id)
.is_some_and(|manifest| {
manifest
.services
.iter()
.any(|service| service.interface_id == interface_id)
})
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct DynamicVisualKey {
plugin_id: String,
adapter_id: String,
invocation_id: String,
schema: String,
schema_version: u32,
payload_revision: u64,
payload_identity: String,
width: u16,
diff_layout: String,
working_directory: Option<std::path::PathBuf>,
theme_fingerprint: u64,
}
#[derive(Debug)]
struct DynamicVisualRequest {
key: DynamicVisualKey,
request: bcode_plugin_sdk::tui_visual::RenderTuiVisualRequest,
}
#[derive(Debug)]
enum DynamicVisualJob {
Render(DynamicVisualRequest),
Artifact {
plugin_id: String,
invocation_id: String,
request: bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunkRequest,
},
}
#[derive(Debug)]
struct DynamicVisualCompletion {
key: Option<DynamicVisualKey>,
invocation_id: String,
invalidate_invocation_cache: bool,
result: Result<bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse, String>,
}
#[derive(Debug)]
struct DynamicVisualCoordinator {
request_sender: Option<SyncSender<DynamicVisualJob>>,
completion_receiver: Receiver<DynamicVisualCompletion>,
worker: Option<JoinHandle<()>>,
requested: BTreeSet<DynamicVisualKey>,
cache: BTreeMap<
DynamicVisualKey,
Result<bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse, String>,
>,
}
impl DynamicVisualCoordinator {
fn new(backend: PresentationPluginBackend) -> Self {
let (request_sender, request_receiver) =
std::sync::mpsc::sync_channel::<DynamicVisualJob>(DYNAMIC_VISUAL_QUEUE_CAPACITY);
let (completion_sender, completion_receiver) = std::sync::mpsc::sync_channel::<
DynamicVisualCompletion,
>(DYNAMIC_VISUAL_COMPLETION_CAPACITY);
let worker = std::thread::Builder::new()
.name("bcode-tui-visual-adapter".to_owned())
.spawn(move || {
while let Ok(job) = request_receiver.recv() {
let completion = match job {
DynamicVisualJob::Render(job) => {
let result = invoke_dynamic_visual_service::<
_,
bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse,
>(
&backend,
&job.key.plugin_id,
bcode_plugin_sdk::tui_visual::OP_RENDER_TUI_VISUAL,
&job.request,
)
.and_then(|response| {
response.validate()?;
Ok(response)
});
DynamicVisualCompletion {
invocation_id: job.key.invocation_id.clone(),
key: Some(job.key),
invalidate_invocation_cache: false,
result,
}
}
DynamicVisualJob::Artifact {
plugin_id,
invocation_id,
request,
} => {
let result = invoke_dynamic_visual_service::<_, serde_json::Value>(
&backend,
&plugin_id,
bcode_plugin_sdk::tui_visual::OP_DELIVER_TUI_VISUAL_ARTIFACT,
&request,
)
.map(|_| {
bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse {
version:
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_CONTRACT_VERSION,
render_mode: String::new(),
anchors: Vec::new(),
title: None,
timeout_ms: None,
rows: Vec::new(),
}
});
DynamicVisualCompletion {
key: None,
invocation_id,
invalidate_invocation_cache: result.is_ok(),
result,
}
}
};
if completion_sender.send(completion).is_err() {
break;
}
}
})
.ok();
Self {
request_sender: worker.as_ref().map(|_| request_sender),
completion_receiver,
worker,
requested: BTreeSet::new(),
cache: BTreeMap::new(),
}
}
fn response(
&self,
key: &DynamicVisualKey,
) -> Option<&bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse> {
self.cache.get(key)?.as_ref().ok()
}
fn request(&mut self, request: DynamicVisualRequest) {
if self.cache.len().saturating_add(self.requested.len()) >= MAX_DYNAMIC_VISUAL_CACHE_ENTRIES
|| self.cache.contains_key(&request.key)
|| !self.requested.insert(request.key.clone())
{
return;
}
let Some(sender) = self.request_sender.as_ref() else {
return;
};
match sender.try_send(DynamicVisualJob::Render(request)) {
Ok(()) => {}
Err(
TrySendError::Full(DynamicVisualJob::Render(request))
| TrySendError::Disconnected(DynamicVisualJob::Render(request)),
) => {
self.requested.remove(&request.key);
}
Err(error) => {
debug_assert!(matches!(
error,
TrySendError::Full(DynamicVisualJob::Artifact { .. })
| TrySendError::Disconnected(DynamicVisualJob::Artifact { .. })
));
}
}
}
fn artifact(
&self,
plugin_id: String,
invocation_id: String,
request: bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunkRequest,
) -> bool {
self.request_sender.as_ref().is_some_and(|sender| {
sender
.try_send(DynamicVisualJob::Artifact {
plugin_id,
invocation_id,
request,
})
.is_ok()
})
}
fn poll(&mut self) -> BTreeSet<String> {
let mut dirty = BTreeSet::new();
for _ in 0..MAX_DYNAMIC_VISUAL_COMPLETIONS_PER_POLL {
match self.completion_receiver.try_recv() {
Ok(completion) => {
if completion.invalidate_invocation_cache {
self.cache
.retain(|key, _| key.invocation_id != completion.invocation_id);
}
dirty.insert(completion.invocation_id);
if let Some(key) = completion.key {
self.requested.remove(&key);
if self.cache.len() >= MAX_DYNAMIC_VISUAL_CACHE_ENTRIES
&& let Some(oldest) = self.cache.keys().next().cloned()
{
self.cache.remove(&oldest);
}
self.cache.insert(key, completion.result);
}
}
Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
}
}
dirty
}
}
fn invoke_dynamic_visual_service<Q, R>(
backend: &PresentationPluginBackend,
plugin_id: &str,
operation: &str,
request: &Q,
) -> Result<R, String>
where
Q: serde::Serialize + Sync,
R: serde::de::DeserializeOwned,
{
match backend {
PresentationPluginBackend::Synchronous(host) => host
.invoke_service_json(
plugin_id,
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID,
operation,
request,
)
.map_err(|error| error.to_string()),
PresentationPluginBackend::Runtime(runtime) => {
let runtime_handle = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.map_err(|error| error.to_string())?;
runtime_handle
.block_on(runtime.invoke_service_json_scoped_with_timeout(
plugin_id,
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID,
operation,
request,
bcode_plugin::PluginInvocationScope::Global,
DYNAMIC_VISUAL_SERVICE_TIMEOUT,
))
.map_err(|error| error.to_string())
}
}
}
impl Drop for DynamicVisualCoordinator {
fn drop(&mut self) {
self.request_sender = None;
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginVisualTiming {
pub operation: &'static str,
pub plugin_id: String,
pub schema: String,
pub duration_micros: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginVisualDiagnostic {
pub plugin_id: String,
pub name: String,
pub value: u64,
}
#[derive(Debug)]
pub struct PluginTuiPresentation {
composer_identity: u64,
backend: PresentationPluginBackend,
visual_adapter_config: bcode_config::TuiVisualAdapterConfig,
registry_factories: BTreeMap<String, bcode_plugin_sdk::tui::PluginTuiRegistryFactory>,
configuration_failures: Mutex<BTreeSet<String>>,
configuration: Mutex<bcode_plugin_sdk::tui::PluginTuiPresentationConfiguration>,
registries: Mutex<BTreeMap<String, Arc<PluginTuiRegistry>>>,
visual_revisions: Mutex<BTreeMap<String, u64>>,
artifact_delivery_failures: Mutex<BTreeSet<String>>,
dirty_visuals: Mutex<BTreeSet<String>>,
visual_generation: AtomicU64,
full_generation: AtomicU64,
timings: Mutex<Vec<PluginVisualTiming>>,
dynamic_visuals: Mutex<DynamicVisualCoordinator>,
}
#[derive(Debug, Clone)]
pub struct RoutedTuiVisual {
pub route: bcode_plugin::PluginVisualAdapterRoute,
pub render_mode: bcode_plugin_sdk::tui::PluginTuiVisualRenderMode,
pub rows: Vec<bmux_tui::prelude::Line>,
pub header: bcode_plugin_sdk::tui::PluginTuiTranscriptHeader,
pub anchors: Vec<bcode_plugin_sdk::tui_visual::TuiVisualAnchor>,
pub selection: Vec<(usize, bcode_plugin_sdk::tui::PluginTuiSelectionRow)>,
}
impl PluginTuiPresentation {
#[must_use]
pub fn new(host: PluginHost) -> Self {
Self::with_config(host, bcode_config::TuiVisualAdapterConfig::default())
}
#[must_use]
pub fn with_config(
host: PluginHost,
visual_adapter_config: bcode_config::TuiVisualAdapterConfig,
) -> Self {
Self::from_backend(
PresentationPluginBackend::Synchronous(Arc::new(host)),
visual_adapter_config,
test_tui_extensions(),
)
}
#[must_use]
pub fn with_runtime_config_and_extensions(
runtime: PluginRuntimeHost,
visual_adapter_config: bcode_config::TuiVisualAdapterConfig,
extensions: &[bcode_plugin_sdk::tui::StaticPluginTuiExtension],
) -> Self {
Self::from_backend(
PresentationPluginBackend::Runtime(runtime),
visual_adapter_config,
extensions,
)
}
#[must_use]
pub fn from_shared(host: Arc<PluginHost>) -> Self {
Self::from_backend(
PresentationPluginBackend::Synchronous(host),
bcode_config::TuiVisualAdapterConfig::default(),
test_tui_extensions(),
)
}
fn from_backend(
backend: PresentationPluginBackend,
visual_adapter_config: bcode_config::TuiVisualAdapterConfig,
extensions: &[bcode_plugin_sdk::tui::StaticPluginTuiExtension],
) -> Self {
let registry_factories = extensions
.iter()
.map(|extension| {
(
extension.plugin_id().to_owned(),
extension.registry_factory(),
)
})
.collect();
let dynamic_visuals = Mutex::new(DynamicVisualCoordinator::new(backend.clone()));
Self {
composer_identity: NEXT_PRESENTATION_IDENTITY.fetch_add(1, Ordering::Relaxed),
backend,
visual_adapter_config,
registry_factories,
configuration_failures: Mutex::new(BTreeSet::new()),
configuration: Mutex::new(
bcode_plugin_sdk::tui::PluginTuiPresentationConfiguration::default(),
),
registries: Mutex::new(BTreeMap::new()),
visual_revisions: Mutex::new(BTreeMap::new()),
artifact_delivery_failures: Mutex::new(BTreeSet::new()),
dirty_visuals: Mutex::new(BTreeSet::new()),
visual_generation: AtomicU64::new(0),
full_generation: AtomicU64::new(0),
timings: Mutex::new(Vec::new()),
dynamic_visuals,
}
}
pub fn selection_row(
&self,
identity: &str,
offset: usize,
) -> Option<bcode_plugin_sdk::tui::PluginTuiSelectionRow> {
let (plugin, identity) = identity.split_once(':')?;
let mut row = self
.registry(plugin)?
.visual_selection_row(identity, offset)?;
if !row.is_valid() {
return None;
}
row.identity = format!("{plugin}:{}", row.identity);
Some(row)
}
pub fn retain_content_position(&self, identity: &str, offset: usize) {
let Some((plugin, identity)) = identity.split_once(':') else {
return;
};
if let Some(registry) = self.registry(plugin) {
registry.retain_visual_content_position(identity, offset);
}
}
pub fn clear_content_positions(&self) {
if let Ok(registries) = self.registries.lock() {
for registry in registries.values() {
registry.clear_visual_content_positions();
}
}
}
pub fn keymap_actions(
&self,
) -> Result<
Vec<bcode_plugin_sdk::tui::keymap::KeymapAction>,
bcode_plugin_sdk::tui::keymap::KeymapError,
> {
let mut registry = bcode_plugin_sdk::tui::keymap::KeymapRegistry::default();
let mut after = None;
loop {
let page = self.backend.keymap_actions_page(
after
.as_ref()
.map(|(owner, scope, id): &(String, String, String)| {
(owner.as_str(), scope.as_str(), id.as_str())
}),
100,
);
for owned in page.actions {
registry.register(owned.action)?;
}
if page.next.is_none() {
break;
}
after = page.next;
}
for plugin in self.registry_factories.keys() {
let loaded = match &self.backend {
PresentationPluginBackend::Synchronous(host) => host.has_plugin(plugin),
PresentationPluginBackend::Runtime(runtime) => {
runtime.registry().manifests().contains_key(plugin)
}
};
if loaded && let Some(contributor) = self.registry(plugin) {
registry.register_all(contributor.keymap_actions().iter().cloned())?;
}
}
Ok(registry.actions().cloned().collect())
}
pub fn content_event(&self, identity: &str, event: &bmux_tui::event::Event) -> bool {
let Some((plugin, identity)) = identity.split_once(':') else {
return false;
};
let changed = self
.registry(plugin)
.is_some_and(|registry| registry.visual_content_event(identity, event));
if changed {
self.full_generation.fetch_add(1, Ordering::Relaxed);
}
changed
}
pub fn content_event_with_host(
&self,
identity: &str,
event: &bmux_tui::event::Event,
host: &dyn bcode_plugin_sdk::tui::PluginTuiHost,
) -> bool {
let Some((plugin, identity)) = identity.split_once(':') else {
return false;
};
let changed = self
.registry(plugin)
.is_some_and(|registry| registry.visual_content_event_with_host(identity, event, host));
if changed {
self.full_generation.fetch_add(1, Ordering::Relaxed);
}
changed
}
#[must_use]
pub fn tool_presentation(
&self,
tool_name: &str,
) -> Option<(&str, &bcode_plugin::PluginToolPresentationDeclaration)> {
self.backend.tool_presentation(tool_name)
}
#[must_use]
pub(crate) const fn composer_identity(&self) -> u64 {
self.composer_identity
}
#[must_use]
pub fn revision(&self) -> u64 {
self.full_generation.load(Ordering::Relaxed)
}
#[must_use]
pub fn visual_generation(&self) -> u64 {
self.visual_generation.load(Ordering::Relaxed)
}
#[must_use]
pub fn visual_revision(&self, invocation_id: &str) -> u64 {
self.visual_revisions
.lock()
.ok()
.and_then(|revisions| revisions.get(invocation_id).copied())
.unwrap_or(0)
}
#[cfg(test)]
pub fn bump_visual_revision_for_test(&self, invocation_id: &str) {
if let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(invocation_id.to_owned()).or_default();
*revision = revision.wrapping_add(1);
}
self.mark_visual_dirty(invocation_id);
}
fn mark_visual_dirty(&self, invocation_id: &str) {
self.visual_generation.fetch_add(1, Ordering::Relaxed);
if let Ok(mut dirty) = self.dirty_visuals.lock() {
dirty.insert(invocation_id.to_owned());
}
}
pub fn drain_dirty_visuals_bounded(&self, limit: usize) -> BTreeSet<String> {
self.dirty_visuals.lock().map_or_else(
|_| BTreeSet::new(),
|mut dirty| {
let mut drained = BTreeSet::new();
for _ in 0..limit {
let Some(invocation_id) = dirty.pop_first() else {
break;
};
drained.insert(invocation_id);
}
drained
},
)
}
pub fn drain_dirty_visuals(&self) -> BTreeSet<String> {
self.dirty_visuals
.lock()
.map_or_else(|_| BTreeSet::new(), |mut dirty| std::mem::take(&mut *dirty))
}
#[cfg(test)]
pub fn install_registry_for_test(&self, plugin_id: &str, registry: PluginTuiRegistry) {
if let Ok(mut registries) = self.registries.lock() {
registries.insert(plugin_id.to_owned(), Arc::new(registry));
self.full_generation.fetch_add(1, Ordering::Relaxed);
}
}
pub fn configure(
&self,
configuration: bcode_plugin_sdk::tui::PluginTuiPresentationConfiguration,
) -> Result<bool, String> {
configuration.validate()?;
let mut current = self
.configuration
.lock()
.map_err(|_| "presentation configuration unavailable".to_owned())?;
let mut failures = self
.configuration_failures
.lock()
.map_err(|_| "presentation configuration status unavailable".to_owned())?;
if *current == configuration && failures.is_empty() {
return Ok(false);
}
let registries = self
.registries
.lock()
.map_err(|_| "presentation registries unavailable".to_owned())?;
self.full_generation.fetch_add(1, Ordering::Relaxed);
self.visual_generation.fetch_add(1, Ordering::Relaxed);
failures.clear();
for (plugin_id, registry) in &*registries {
if registry.configure(&configuration).is_err() {
failures.insert(plugin_id.clone());
}
}
drop(registries);
*current = configuration;
drop(current);
if failures.is_empty() {
Ok(true)
} else {
Err("visual adapter rejected presentation configuration".to_owned())
}
}
pub(crate) fn activate_composer(
&self,
document: &bcode_plugin_sdk::composer::ComposerDocument,
) -> Option<(
bcode_plugin_sdk::composer::PluginOwnedComposerContribution,
bcode_plugin_sdk::composer::ComposerActivation,
)> {
self.backend
.composer_contributions()
.into_iter()
.find_map(|route| {
let registry = self.registry(&route.plugin_id)?;
let activation = registry
.composer_registry()
.activate(&route.contribution.activation_kind, document)
.ok()??;
Some((route, activation))
})
}
pub(crate) fn reactivate_composer(
&self,
route: &bcode_plugin_sdk::composer::PluginOwnedComposerContribution,
document: &bcode_plugin_sdk::composer::ComposerDocument,
) -> Option<bcode_plugin_sdk::composer::ComposerActivation> {
if !self.backend.composer_contributions().contains(route) {
return None;
}
self.registry(&route.plugin_id)?
.composer_registry()
.activate(&route.contribution.activation_kind, document)
.ok()?
}
#[must_use]
pub fn registry(&self, plugin_id: &str) -> Option<Arc<PluginTuiRegistry>> {
let configuration = self.configuration.lock().ok()?;
if self.configuration_failures.lock().ok()?.contains(plugin_id) {
return None;
}
let mut registries = self.registries.lock().ok()?;
if let Some(registry) = registries.get(plugin_id).cloned() {
return Some(registry);
}
let registry = Arc::new((self.registry_factories.get(plugin_id)?)());
registry.configure(&configuration).ok()?;
registries.insert(plugin_id.to_owned(), Arc::clone(®istry));
drop(registries);
Some(registry)
}
#[must_use]
pub fn visual_routes(
&self,
schema: &str,
schema_version: u32,
producer_plugin_id: Option<&str>,
) -> Vec<bcode_plugin::PluginVisualAdapterRoute> {
self.visual_adapter_config
.order_routes(self.backend.visual_adapters(
schema,
schema_version,
"tui",
producer_plugin_id,
))
.into_iter()
.filter(|route| {
self.registry(&route.plugin_id).is_some_and(|registry| {
registry.supports_visual_adapter(&route.adapter_id, &route.schema)
}) || self.backend.has_service(
&route.plugin_id,
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID,
)
})
.collect()
}
#[must_use]
pub fn visual_route(
&self,
schema: &str,
schema_version: u32,
producer_plugin_id: Option<&str>,
) -> Option<bcode_plugin::PluginVisualAdapterRoute> {
self.visual_routes(schema, schema_version, producer_plugin_id)
.into_iter()
.next()
}
pub fn poll_dynamic_visuals(&self) -> bool {
let dirty = self
.dynamic_visuals
.lock()
.map_or_else(|_| BTreeSet::new(), |mut coordinator| coordinator.poll());
let changed = !dirty.is_empty();
for invocation_id in dirty {
if let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(invocation_id.clone()).or_default();
*revision = revision.wrapping_add(1);
}
self.mark_visual_dirty(&invocation_id);
}
changed
}
#[must_use]
#[allow(clippy::too_many_arguments)] pub fn routed_visual_with_host(
&self,
invocation_id: &str,
payload_revision: u64,
schema: &str,
schema_version: u32,
producer_plugin_id: Option<&str>,
payload: &serde_json::Value,
context: &bcode_plugin_sdk::tui::PluginTuiVisualRenderContext,
host: &dyn bcode_plugin_sdk::tui::PluginTuiHost,
) -> Option<RoutedTuiVisual> {
self.routed_visual_impl(
invocation_id,
payload_revision,
schema,
schema_version,
producer_plugin_id,
payload,
context,
Some(host),
)
}
#[must_use]
#[allow(clippy::too_many_arguments)] pub fn routed_visual(
&self,
invocation_id: &str,
payload_revision: u64,
schema: &str,
schema_version: u32,
producer_plugin_id: Option<&str>,
payload: &serde_json::Value,
context: &bcode_plugin_sdk::tui::PluginTuiVisualRenderContext,
) -> Option<RoutedTuiVisual> {
self.routed_visual_impl(
invocation_id,
payload_revision,
schema,
schema_version,
producer_plugin_id,
payload,
context,
None,
)
}
#[allow(clippy::too_many_arguments)] fn routed_visual_impl(
&self,
invocation_id: &str,
payload_revision: u64,
schema: &str,
schema_version: u32,
producer_plugin_id: Option<&str>,
payload: &serde_json::Value,
context: &bcode_plugin_sdk::tui::PluginTuiVisualRenderContext,
host: Option<&dyn bcode_plugin_sdk::tui::PluginTuiHost>,
) -> Option<RoutedTuiVisual> {
for route in self.visual_routes(schema, schema_version, producer_plugin_id) {
if let Some(registry) = self.registry(&route.plugin_id)
&& let Some(bcode_plugin_sdk::tui::PluginTuiVisualProjection {
rows,
anchors,
selection,
}) = host.map_or_else(
|| {
registry.visual_projection(
&route.adapter_id,
&route.schema,
payload,
context,
)
},
|host| {
registry.visual_projection_with_host(
&route.adapter_id,
&route.schema,
payload,
context,
host,
)
},
)
{
let render_mode = registry
.visual_render_mode(&route.adapter_id, &route.schema, payload)
.unwrap_or_else(|| manifest_render_mode(route.render_mode));
let header = registry
.visual_transcript_header(&route.adapter_id, &route.schema, payload)
.unwrap_or_default();
return Some(self.native_routed_visual(
invocation_id,
route,
render_mode,
header,
rows,
anchors,
selection,
));
}
if !self.backend.has_service(
&route.plugin_id,
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID,
) {
continue;
}
let key = DynamicVisualKey {
plugin_id: route.plugin_id.clone(),
adapter_id: route.adapter_id.clone(),
invocation_id: invocation_id.to_owned(),
schema: schema.to_owned(),
schema_version,
payload_revision,
payload_identity: payload.to_string(),
width: context.width(),
diff_layout: format!("{:?}", context.diff_layout()),
working_directory: context.working_directory().map(ToOwned::to_owned),
theme_fingerprint: context.theme().as_ref().map_or(0, theme_fingerprint),
};
let response = {
let dynamic = self.dynamic_visuals.lock().ok()?;
dynamic.response(&key).cloned()
};
if let Some(response) = response {
return Some(RoutedTuiVisual {
render_mode: serialized_render_mode(&response, route.render_mode),
route,
rows: serialized_visual_rows(&response, context.theme().as_ref()),
anchors: response.anchors.clone(),
selection: Vec::new(),
header: bcode_plugin_sdk::tui::PluginTuiTranscriptHeader {
title: response.title,
timeout_ms: response.timeout_ms,
},
});
}
let Some(mut dynamic) = self.dynamic_visuals.lock().ok() else {
continue;
};
dynamic.request(DynamicVisualRequest {
key,
request: bcode_plugin_sdk::tui_visual::RenderTuiVisualRequest {
version: bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_CONTRACT_VERSION,
adapter_id: route.adapter_id.clone(),
invocation_id: invocation_id.to_owned(),
schema: schema.to_owned(),
schema_version,
payload: payload.clone(),
context: bcode_plugin_sdk::tui_visual::SerializedTuiVisualContext {
width: context.width(),
diff_layout: format!("{:?}", context.diff_layout()),
working_directory: context.working_directory().map(ToOwned::to_owned),
theme_fingerprint: context.theme().as_ref().map_or(0, theme_fingerprint),
},
},
});
}
None
}
#[allow(clippy::too_many_arguments)] fn native_routed_visual(
&self,
invocation_id: &str,
route: bcode_plugin::PluginVisualAdapterRoute,
render_mode: bcode_plugin_sdk::tui::PluginTuiVisualRenderMode,
header: bcode_plugin_sdk::tui::PluginTuiTranscriptHeader,
mut rows: Vec<bmux_tui::prelude::Line>,
anchors: Vec<bcode_plugin_sdk::tui_visual::TuiVisualAnchor>,
selection: Vec<(usize, bcode_plugin_sdk::tui::PluginTuiSelectionRow)>,
) -> RoutedTuiVisual {
if self
.artifact_delivery_failures
.lock()
.is_ok_and(|failures| failures.contains(invocation_id))
{
rows.push(bmux_tui::prelude::Line::from(
"Artifact replay failed; showing last valid output. Recording requires inspection.",
));
}
RoutedTuiVisual {
route,
render_mode,
rows,
header,
anchors,
selection,
}
}
#[must_use]
pub fn accepts_visual(
&self,
producer_plugin_id: &str,
schema: &str,
schema_version: u32,
) -> bool {
let producer = Some(producer_plugin_id);
self.visual_route(schema, schema_version, producer)
.is_some()
}
#[must_use]
pub fn accepts_artifact_reference(
&self,
producer_plugin_id: &str,
schema: &str,
schema_version: u32,
reference_key: &str,
content_type: Option<&str>,
) -> bool {
let producer = Some(producer_plugin_id);
let Some(route) = self.visual_route(schema, schema_version, producer) else {
return false;
};
self.registry(&route.plugin_id).is_some_and(|registry| {
registry.visual_accepts_artifact_reference(
&route.adapter_id,
&route.schema,
reference_key,
content_type,
)
}) || self.backend.has_service(
&route.plugin_id,
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID,
)
}
pub fn drain_diagnostics(&self) -> Vec<PluginVisualDiagnostic> {
const MAX_DIAGNOSTICS: usize = 64;
let Ok(registries) = self.registries.lock() else {
return Vec::new();
};
registries
.iter()
.flat_map(|(plugin_id, registry)| {
registry
.drain_visual_diagnostics()
.into_iter()
.filter(|diagnostic| valid_diagnostic_name(&diagnostic.name))
.map(|diagnostic| PluginVisualDiagnostic {
plugin_id: plugin_id.clone(),
name: diagnostic.name,
value: diagnostic.value,
})
})
.take(MAX_DIAGNOSTICS)
.collect()
}
pub fn drain_timings(&self) -> Vec<PluginVisualTiming> {
self.timings
.lock()
.map_or_else(|_| Vec::new(), |mut timings| std::mem::take(&mut *timings))
}
pub fn record_visual_timing(
&self,
operation: &'static str,
plugin_id: &str,
schema: &str,
started: Instant,
) {
self.record_timing(PluginVisualTiming {
operation,
plugin_id: plugin_id.to_owned(),
schema: schema.to_owned(),
duration_micros: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
});
}
fn record_timing(&self, timing: PluginVisualTiming) {
if let Ok(mut timings) = self.timings.lock() {
const MAX_PENDING_TIMINGS: usize = 256;
if timings.len() < MAX_PENDING_TIMINGS {
timings.push(timing);
}
}
}
pub fn viewport_input(
&self,
invocation_id: &str,
producer: &str,
schema: &str,
schema_version: u32,
viewport: bmux_tui::geometry::Size,
) -> Option<bcode_tool::ToolInvocationInput> {
let route = self.visual_route(schema, schema_version, Some(producer))?;
self.registry(&route.plugin_id)?.visual_viewport_input(
&route.adapter_id,
invocation_id,
schema,
viewport.width,
viewport.height,
)
}
pub fn mark_artifact_unavailable(&self, invocation_id: &str) {
let changed = self
.artifact_delivery_failures
.lock()
.is_ok_and(|mut failures| {
failures.len() < 256 && failures.insert(invocation_id.to_owned())
});
if changed {
if let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(invocation_id.to_owned()).or_default();
*revision = revision.wrapping_add(1);
}
self.mark_visual_dirty(invocation_id);
}
}
pub(crate) fn clear_artifact_failure(&self, invocation_id: &str) {
let changed = self
.artifact_delivery_failures
.lock()
.is_ok_and(|mut failures| failures.remove(invocation_id));
if changed {
if let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(invocation_id.to_owned()).or_default();
*revision = revision.wrapping_add(1);
}
self.mark_visual_dirty(invocation_id);
}
}
pub fn deliver_artifact_chunk(&self, chunk: &PluginTuiArtifactChunk) -> Result<bool, String> {
let producer = Some(chunk.producer_plugin_id.as_str());
let Some(route) = self.visual_route(&chunk.schema, chunk.schema_version, producer) else {
return Ok(false);
};
if let Some(registry) = self.registry(&route.plugin_id)
&& registry.supports_visual_adapter(&route.adapter_id, &route.schema)
{
let started = Instant::now();
let delivered = match registry.visual_artifact_chunk(&route.adapter_id, chunk) {
Ok(delivered) => delivered,
Err(error) => {
if let Ok(mut failures) = self.artifact_delivery_failures.lock()
&& failures.len() < 256
{
failures.insert(chunk.tool_call_id.clone());
}
if let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(chunk.tool_call_id.clone()).or_default();
*revision = revision.wrapping_add(1);
}
self.mark_visual_dirty(&chunk.tool_call_id);
return Err(error);
}
};
self.record_timing(PluginVisualTiming {
operation: "artifact_delivery",
plugin_id: route.plugin_id.clone(),
schema: route.schema,
duration_micros: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
});
if delivered && let Ok(mut revisions) = self.visual_revisions.lock() {
let revision = revisions.entry(chunk.tool_call_id.clone()).or_default();
*revision = revision.wrapping_add(1);
}
if delivered {
self.mark_visual_dirty(&chunk.tool_call_id);
}
return Ok(delivered);
}
let queued = self.dynamic_visuals.lock().is_ok_and(|coordinator| {
coordinator.artifact(
route.plugin_id,
chunk.tool_call_id.clone(),
bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunkRequest {
version: bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_CONTRACT_VERSION,
adapter_id: route.adapter_id,
chunk: serialized_artifact_chunk(chunk),
},
)
});
Ok(queued)
}
}
#[cfg(test)]
fn test_tui_extensions() -> &'static [bcode_plugin_sdk::tui::StaticPluginTuiExtension] {
static EXTENSIONS: std::sync::OnceLock<Vec<bcode_plugin_sdk::tui::StaticPluginTuiExtension>> =
std::sync::OnceLock::new();
EXTENSIONS.get_or_init(|| {
vec![
bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"bcode.filesystem",
bcode_filesystem_plugin::filesystem_tui_registry,
),
bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"bcode.git",
bcode_git_plugin::git_tui_registry,
),
bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"bcode.question",
bcode_question_plugin::question_tui_registry,
),
bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"bcode.shell",
bcode_shell_plugin::shell_tui_registry,
),
bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"bcode.vim-edit",
bcode_vim_edit_plugin::vim_edit_tui_registry,
),
]
})
}
#[cfg(not(test))]
const fn test_tui_extensions() -> &'static [bcode_plugin_sdk::tui::StaticPluginTuiExtension] {
&[]
}
fn serialized_artifact_chunk(
chunk: &PluginTuiArtifactChunk,
) -> bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunk {
bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunk {
tool_call_id: chunk.tool_call_id.clone(),
artifact_id: chunk.artifact_id.clone(),
reference_key: chunk.reference_key.clone(),
producer_plugin_id: chunk.producer_plugin_id.clone(),
schema: chunk.schema.clone(),
schema_version: chunk.schema_version,
content_type: chunk.content_type.clone(),
offset: chunk.offset,
total_bytes: chunk.total_bytes,
revision: chunk.revision,
finalized: chunk.finalized,
bytes: chunk.bytes.clone(),
}
}
const fn manifest_render_mode(
mode: bcode_plugin::PluginVisualAdapterRenderMode,
) -> bcode_plugin_sdk::tui::PluginTuiVisualRenderMode {
match mode {
bcode_plugin::PluginVisualAdapterRenderMode::Inline => {
bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::Inline
}
bcode_plugin::PluginVisualAdapterRenderMode::TranscriptBlock => {
bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::TranscriptBlock
}
bcode_plugin::PluginVisualAdapterRenderMode::FullBlock => {
bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::FullBlock
}
}
}
fn serialized_render_mode(
response: &bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse,
default: bcode_plugin::PluginVisualAdapterRenderMode,
) -> bcode_plugin_sdk::tui::PluginTuiVisualRenderMode {
match response.render_mode.as_str() {
"inline" => bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::Inline,
"transcript_block" => bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::TranscriptBlock,
"full_block" => bcode_plugin_sdk::tui::PluginTuiVisualRenderMode::FullBlock,
_ => manifest_render_mode(default),
}
}
fn theme_fingerprint(theme: &bcode_plugin_sdk::tui::PluginTuiTheme) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
format!("{theme:?}").hash(&mut hasher);
hasher.finish()
}
fn serialized_visual_rows(
response: &bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse,
theme: Option<&bcode_plugin_sdk::tui::PluginTuiTheme>,
) -> Vec<bmux_tui::prelude::Line> {
use bcode_plugin_sdk::tui_visual::{
SerializedTuiColor, SerializedTuiModifier, SerializedTuiStyleRole,
};
use bmux_tui::prelude::{Color, Line, Modifier, Span, Style};
response
.rows
.iter()
.map(|row| {
Line::from_spans(
row.spans
.iter()
.map(|span| {
let color = match span.foreground {
SerializedTuiColor::Reset => Color::Default,
SerializedTuiColor::Black => Color::Black,
SerializedTuiColor::Red => Color::Red,
SerializedTuiColor::Green => Color::Green,
SerializedTuiColor::Yellow => Color::Yellow,
SerializedTuiColor::Blue => Color::Blue,
SerializedTuiColor::Magenta => Color::Magenta,
SerializedTuiColor::Cyan => Color::Cyan,
SerializedTuiColor::Gray | SerializedTuiColor::White => Color::White,
SerializedTuiColor::DarkGray => Color::BrightBlack,
SerializedTuiColor::LightRed => Color::BrightRed,
SerializedTuiColor::LightGreen => Color::BrightGreen,
SerializedTuiColor::LightYellow => Color::BrightYellow,
SerializedTuiColor::LightBlue => Color::BrightBlue,
SerializedTuiColor::LightMagenta => Color::BrightMagenta,
SerializedTuiColor::LightCyan => Color::BrightCyan,
};
let compatibility_style = Style::new().fg(color);
let role_style = span.role.and_then(|role| {
theme.map(|theme| match role {
SerializedTuiStyleRole::Text => theme.text,
SerializedTuiStyleRole::Muted => theme.muted,
SerializedTuiStyleRole::Accent | SerializedTuiStyleRole::Info => {
theme.focused
}
SerializedTuiStyleRole::Success
| SerializedTuiStyleRole::DiffAdded => theme.diff.added,
SerializedTuiStyleRole::Warning
| SerializedTuiStyleRole::DiffHunk => theme.diff.hunk,
SerializedTuiStyleRole::Error
| SerializedTuiStyleRole::DiffRemoved => theme.diff.removed,
})
});
let mut style = role_style.unwrap_or(compatibility_style);
for modifier in &span.modifiers {
style = style.add_modifier(match modifier {
SerializedTuiModifier::Bold => Modifier::BOLD,
SerializedTuiModifier::Dim => Modifier::DIM,
SerializedTuiModifier::Italic => Modifier::ITALIC,
SerializedTuiModifier::Underlined => Modifier::UNDERLINE,
});
}
Span::styled(span.text.clone(), style)
})
.collect::<Vec<_>>(),
)
})
.collect()
}
fn valid_diagnostic_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 64
&& name.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_')
})
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn tui_registry(plugin_id: &str) -> Option<PluginTuiRegistry> {
let registry = super::bundled_tui_extensions()
.into_iter()
.find(|extension| extension.plugin_id() == plugin_id)
.map(bcode_plugin_sdk::tui::StaticPluginTuiExtension::registry);
#[cfg(test)]
let registry = registry.or_else(|| match plugin_id {
"bcode.filesystem" => Some(bcode_filesystem_plugin::filesystem_tui_registry()),
"bcode.git" => Some(bcode_git_plugin::git_tui_registry()),
"bcode.question" => Some(bcode_question_plugin::question_tui_registry()),
"bcode.shell" => Some(bcode_shell_plugin::shell_tui_registry()),
"bcode.vim-edit" => Some(bcode_vim_edit_plugin::vim_edit_tui_registry()),
_ => None,
});
registry
}
pub fn load_default_host_with_static_bundled(
selection: &bcode_plugin::PluginSelection,
static_plugins: &[StaticBundledPlugin],
extensions: &[bcode_plugin_sdk::tui::StaticPluginTuiExtension],
) -> Result<PluginHost, PluginLoadError> {
PluginHost::load_defaults_with_static_bundled(selection, static_plugins)
.or_else(|_| load_presentation_fallback(selection, static_plugins, extensions))
}
fn load_presentation_fallback(
selection: &bcode_plugin::PluginSelection,
static_plugins: &[StaticBundledPlugin],
extensions: &[bcode_plugin_sdk::tui::StaticPluginTuiExtension],
) -> Result<PluginHost, PluginLoadError> {
let selected = bcode_plugin::filter_selected_static_plugins(static_plugins, selection)?;
let visual_plugins = selected
.into_iter()
.filter(|(manifest, _)| {
extensions
.iter()
.find(|extension| extension.plugin_id() == manifest.id)
.is_some_and(|extension| {
let registry = extension.registry();
manifest.composer_contributions.iter().any(|contribution| {
registry
.composer_registry()
.supports(&contribution.activation_kind)
}) || manifest.visual_adapters.iter().any(|adapter| {
(adapter.surfaces.is_empty()
|| adapter.surfaces.iter().any(|surface| surface == "tui"))
&& registry.supports_visual_adapter(&adapter.id, &adapter.schema)
})
})
})
.collect::<Vec<_>>();
Ok(PluginHost::load_static_plugins_best_effort(&visual_plugins))
}
pub fn load_default_presentation_with_static_bundled(
selection: &bcode_plugin::PluginSelection,
visual_adapter_config: bcode_config::TuiVisualAdapterConfig,
static_plugins: &[StaticBundledPlugin],
extensions: &[bcode_plugin_sdk::tui::StaticPluginTuiExtension],
) -> Result<PluginTuiPresentation, PluginLoadError> {
load_default_host_with_static_bundled(selection, static_plugins, extensions).map(|host| {
PluginTuiPresentation::with_runtime_config_and_extensions(
host.into(),
visual_adapter_config,
extensions,
)
})
}
pub fn load_default_runtime_with_static_bundled(
static_plugins: &[StaticBundledPlugin],
) -> Result<PluginRuntimeHost, PluginLoadError> {
PluginRuntimeHost::load_defaults_with_static_bundled(
&bcode_plugin::PluginSelection::all_enabled(),
static_plugins,
)
}
pub async fn open_plugin_tui_surface(
runtime: &PluginRuntimeHost,
plugin_id: &str,
surface_kind: &str,
request: PluginTuiSurfaceOpenRequest,
) -> Result<BoxedPluginTuiSurface, PluginLoadError> {
if !runtime
.plugin_ids()
.iter()
.any(|loaded| loaded == plugin_id)
{
return Err(PluginLoadError::PluginNotLoaded(plugin_id.to_string()));
}
let registry = tui_registry(plugin_id)
.ok_or_else(|| PluginLoadError::PluginNotLoaded(plugin_id.to_string()))?;
if runtime
.registry()
.tui_surface(plugin_id, surface_kind)
.is_none()
{
return Err(PluginLoadError::TuiSurfaceOpen {
plugin_id: plugin_id.to_string(),
message: format!("plugin does not declare TUI surface kind '{surface_kind}'"),
});
}
registry
.open(surface_kind, request)
.await
.map_err(|error| PluginLoadError::TuiSurfaceOpen {
plugin_id: plugin_id.to_string(),
message: error.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use bmux_tui::prelude::Line;
const DYNAMIC_TEST_MANIFEST: &str = r#"
id = "test.dynamic-visual"
name = "Dynamic Visual Test Plugin"
version = "0.0.1"
[[services]]
interface_id = "bcode.tui-visual-adapter/v1"
name = "Dynamic TUI Visual Adapter"
class = "service"
[[visual_adapters]]
id = "success"
schema = "bcode.shell.run"
min_schema_version = 1
max_schema_version = 1
service_interface_id = "bcode.tui-visual-adapter/v1"
surfaces = ["tui"]
priority = 100
producer_default = false
render_mode = "transcript_block"
[[visual_adapters]]
id = "malformed"
schema = "bcode.shell.run"
min_schema_version = 1
max_schema_version = 1
service_interface_id = "bcode.tui-visual-adapter/v1"
surfaces = ["tui"]
priority = 99
producer_default = false
render_mode = "transcript_block"
[[visual_adapters]]
id = "failure"
schema = "bcode.shell.run"
min_schema_version = 1
max_schema_version = 1
service_interface_id = "bcode.tui-visual-adapter/v1"
surfaces = ["tui"]
priority = 98
producer_default = false
render_mode = "transcript_block"
[[visual_adapters]]
id = "timeout"
schema = "bcode.shell.run"
min_schema_version = 1
max_schema_version = 1
service_interface_id = "bcode.tui-visual-adapter/v1"
surfaces = ["tui"]
priority = 97
producer_default = false
render_mode = "transcript_block"
[concurrency]
type = "concurrent"
[runtime]
type = "native"
abi_version = 5
library = "libdynamic_visual_test.dylib"
"#;
#[derive(Default)]
struct DynamicVisualTestPlugin {
artifact_revisions: BTreeMap<String, u64>,
}
impl bcode_plugin_sdk::RustPlugin for DynamicVisualTestPlugin {
fn invoke_service(
&mut self,
context: bcode_plugin_sdk::NativeServiceContext,
) -> bcode_plugin_sdk::ServiceResponse {
match context.request.operation.as_str() {
bcode_plugin_sdk::tui_visual::OP_RENDER_TUI_VISUAL => {
let Ok(request) = context
.request
.payload_json::<bcode_plugin_sdk::tui_visual::RenderTuiVisualRequest>(
) else {
return bcode_plugin_sdk::ServiceResponse::error(
"invalid_request",
"render request did not decode",
);
};
match request.adapter_id.as_str() {
"failure" => bcode_plugin_sdk::ServiceResponse::error(
"test_failure",
"dynamic adapter failed",
),
"malformed" => bcode_plugin_sdk::ServiceResponse::json(
&bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse {
version: 999,
anchors: Vec::new(),
render_mode: "transcript_block".to_owned(),
title: None,
timeout_ms: None,
rows: Vec::new(),
},
)
.expect("encode malformed response"),
"timeout" => {
while !context.cancellation.is_cancelled() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
bcode_plugin_sdk::ServiceResponse::error(
"cancelled",
"dynamic adapter was cancelled",
)
}
"success" => {
let artifact_revision = self
.artifact_revisions
.get(&request.invocation_id)
.copied()
.unwrap_or_default();
bcode_plugin_sdk::ServiceResponse::json(
&bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse {
version:
bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_CONTRACT_VERSION,
render_mode: "transcript_block".to_owned(),
title: Some("Dynamic shell".to_owned()),
anchors: Vec::new(),
timeout_ms: Some(321),
rows: vec![bcode_plugin_sdk::tui_visual::SerializedTuiRow {
spans: vec![bcode_plugin_sdk::tui_visual::SerializedTuiSpan {
text: request.payload.get("cache_probe").map_or_else(
|| format!("dynamic:{}:artifact-{artifact_revision}", request.adapter_id),
|_| format!("{}:{}:{:?}", request.payload, request.context.diff_layout, request.context.working_directory),
),
role: Some(
bcode_plugin_sdk::tui_visual::SerializedTuiStyleRole::Success,
),
foreground:
bcode_plugin_sdk::tui_visual::SerializedTuiColor::Green,
modifiers: vec![
bcode_plugin_sdk::tui_visual::SerializedTuiModifier::Bold,
],
}],
}],
},
)
.expect("encode dynamic response")
}
_ => bcode_plugin_sdk::ServiceResponse::error(
"unknown_adapter",
"adapter id was not exact",
),
}
}
bcode_plugin_sdk::tui_visual::OP_DELIVER_TUI_VISUAL_ARTIFACT => {
let Ok(request) = context
.request
.payload_json::<bcode_plugin_sdk::tui_visual::SerializedTuiArtifactChunkRequest>(
) else {
return bcode_plugin_sdk::ServiceResponse::error(
"invalid_request",
"artifact request did not decode",
);
};
let revision = self
.artifact_revisions
.entry(request.chunk.tool_call_id)
.or_default();
*revision = revision.wrapping_add(1);
bcode_plugin_sdk::ServiceResponse::json(&serde_json::json!({"accepted": true}))
.expect("encode artifact response")
}
_ => bcode_plugin_sdk::ServiceResponse::error(
"unsupported_operation",
"unsupported dynamic visual operation",
),
}
}
}
fn dynamic_visual_test_plugin() -> StaticBundledPlugin {
StaticBundledPlugin::new(
DYNAMIC_TEST_MANIFEST,
bcode_plugin_sdk::static_plugin_vtable!(DynamicVisualTestPlugin, DYNAMIC_TEST_MANIFEST),
)
}
const COMPOSER_FALLBACK_MANIFEST: &str = r#"
id = "test.composer-fallback"
name = "Composer fallback fixture"
version = "0.0.1"
[[services]]
class = "service"
name = "Suggestions"
interface_id = "bcode.composer/v1"
[[composer_contributions]]
id = "generic"
activation_kind = "test.generic"
service_interface_id = "bcode.composer/v1"
priority = 1
[runtime]
type = "native"
abi_version = 5
library = "libcomposer_fixture.dylib"
"#;
fn composer_fallback_registry() -> PluginTuiRegistry {
struct Contributor;
impl bcode_plugin_sdk::composer::PluginComposerContributor for Contributor {
fn kind(&self) -> &'static str {
"test.generic"
}
fn activate(
&self,
document: &bcode_plugin_sdk::composer::ComposerDocument,
) -> Option<bcode_plugin_sdk::composer::ComposerActivation> {
document.allows_activation().then(|| {
bcode_plugin_sdk::composer::ComposerActivation {
range: bcode_plugin_sdk::composer::ComposerTextRange {
start: 0,
end: document.text.len(),
},
query: document.text.clone(),
}
})
}
}
let mut registry = PluginTuiRegistry::default();
registry
.register_composer_contributor(Box::new(Contributor))
.unwrap();
registry
}
#[test]
fn presentation_fallback_retains_composer_only_activation() {
let bundled = [StaticBundledPlugin::new(
COMPOSER_FALLBACK_MANIFEST,
bcode_plugin_sdk::static_plugin_vtable!(
DynamicVisualTestPlugin,
COMPOSER_FALLBACK_MANIFEST
),
)];
let extensions = [bcode_plugin_sdk::tui::StaticPluginTuiExtension::new(
"test.composer-fallback",
composer_fallback_registry,
)];
let host = load_presentation_fallback(
&bcode_plugin::PluginSelection::all_enabled(),
&bundled,
&extensions,
)
.unwrap();
let presentation = PluginTuiPresentation::with_runtime_config_and_extensions(
host.into(),
bcode_config::TuiVisualAdapterConfig::default(),
&extensions,
);
let document = bcode_plugin_sdk::composer::ComposerDocument {
document_id: "fixture".into(),
revision: 0,
text: "generic".into(),
cursor: 7,
selection: None,
};
let (route, activation) = presentation
.activate_composer(&document)
.expect("retained contributor activation");
drop(presentation);
assert_eq!(route.plugin_id, "test.composer-fallback");
assert_eq!(activation.query, "generic");
let unsupported = load_presentation_fallback(
&bcode_plugin::PluginSelection::all_enabled(),
&bundled,
&[],
)
.unwrap();
let unsupported = PluginTuiPresentation::with_runtime_config_and_extensions(
unsupported.into(),
bcode_config::TuiVisualAdapterConfig::default(),
&extensions,
);
assert!(unsupported.activate_composer(&document).is_none());
drop(unsupported);
}
#[test]
fn presentation_fallback_retains_compatible_native_adapters() {
let bundled = [StaticBundledPlugin::new(
include_str!("../../../plugins/shell-plugin/bcode-plugin.toml"),
bcode_shell_plugin::static_plugin(),
)];
let host = load_presentation_fallback(
&bcode_plugin::PluginSelection::all_enabled(),
&bundled,
test_tui_extensions(),
)
.unwrap();
assert!(
host.loaded_plugins()
.iter()
.any(|plugin| plugin.manifest().id == "bcode.shell")
);
let presentation = PluginTuiPresentation::from_shared(Arc::new(host));
assert!(presentation.registry("bcode.shell").is_some());
drop(presentation);
}
#[test]
fn presentation_fallback_does_not_admit_plugins_without_compatible_registry() {
let bundled = [dynamic_visual_test_plugin()];
let host = load_presentation_fallback(
&bcode_plugin::PluginSelection::all_enabled(),
&bundled,
&[],
)
.unwrap();
assert!(host.loaded_plugins().is_empty());
}
fn dynamic_test_presentation(
preferred_adapter: &str,
disabled_adapters: &[&str],
) -> PluginTuiPresentation {
let bundled = [
dynamic_visual_test_plugin(),
StaticBundledPlugin::new(
include_str!("../../../plugins/shell-plugin/bcode-plugin.toml"),
bcode_shell_plugin::static_plugin(),
),
];
let selected = bcode_plugin::filter_selected_static_plugins(
&bundled,
&bcode_plugin::PluginSelection::all_enabled(),
)
.expect("select dynamic visual test plugins");
let runtime: PluginRuntimeHost = PluginHost::load_static_plugins(&selected)
.expect("load dynamic visual test plugins")
.into();
PluginTuiPresentation::with_runtime_config_and_extensions(
runtime,
bcode_config::TuiVisualAdapterConfig {
preferred: vec![preferred_adapter.to_owned()],
disabled: disabled_adapters
.iter()
.map(|adapter| (*adapter).to_owned())
.collect(),
},
test_tui_extensions(),
)
}
fn test_plugin_theme(fingerprint: u64) -> bcode_plugin_sdk::tui::PluginTuiTheme {
use bmux_tui::prelude::{Color, Modifier, Style};
let style = Style::new();
let added = Style::new().fg(Color::Green);
let removed = Style::new().fg(Color::Red);
let hunk = Style::new().fg(Color::Yellow);
let syntax = bcode_plugin_sdk::tui::PluginTuiSyntaxColor::rgb(
u8::try_from(fingerprint).unwrap_or(u8::MAX),
2,
3,
);
bcode_plugin_sdk::tui::PluginTuiTheme {
component_theme_version: bcode_plugin_sdk::tui::PLUGIN_TUI_COMPONENT_THEME_VERSION,
canvas: style,
text: style.fg(Color::White),
muted: style.fg(Color::BrightBlack),
border: style,
focused: style.fg(Color::Cyan),
selection: style.add_modifier(Modifier::REVERSED),
source: bcode_plugin_sdk::tui::PluginTuiSourceTheme {
source: style,
border: style,
gutter: style,
truncated: style,
},
diff: bcode_plugin_sdk::tui::PluginTuiDiffTheme {
text: style,
muted: style,
title: style,
label: style,
added,
removed,
hunk,
added_row: style,
removed_row: style,
added_emphasis: style,
removed_emphasis: style,
},
syntax: bcode_plugin_sdk::tui::PluginTuiSyntaxTheme {
text: syntax,
comment: syntax,
keyword: syntax,
function: syntax,
variable: syntax,
string: syntax,
number: syntax,
type_name: syntax,
operator: syntax,
punctuation: syntax,
heading: syntax,
link: syntax,
raw: syntax,
},
}
}
#[test]
fn serialized_semantic_role_precedes_legacy_color_with_readable_fallback() {
use bcode_plugin_sdk::tui_visual::{
RenderTuiVisualResponse, SerializedTuiColor, SerializedTuiRow, SerializedTuiSpan,
SerializedTuiStyleRole,
};
use bmux_tui::prelude::{Color, Style};
let response = RenderTuiVisualResponse {
anchors: Vec::new(),
version: 2,
render_mode: "inline".to_owned(),
title: None,
timeout_ms: None,
rows: vec![SerializedTuiRow {
spans: vec![SerializedTuiSpan {
text: "status".to_owned(),
role: Some(SerializedTuiStyleRole::Success),
foreground: SerializedTuiColor::Red,
modifiers: Vec::new(),
}],
}],
};
let themed = serialized_visual_rows(&response, Some(&test_plugin_theme(7)));
assert_eq!(themed[0].spans[0].style.fg, Some(Color::Green));
let fallback = serialized_visual_rows(&response, None);
assert_eq!(fallback[0].spans[0].style, Style::new().fg(Color::Red));
}
#[test]
fn serialized_visual_cache_key_includes_theme_identity() {
let presentation = dynamic_test_presentation("success", &[]);
let key = |theme_fingerprint| DynamicVisualKey {
plugin_id: "test.dynamic-visual".to_owned(),
adapter_id: "success".to_owned(),
invocation_id: "call-theme".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
payload_revision: 0,
payload_identity: "{}".to_owned(),
width: 80,
diff_layout: "Unified".to_owned(),
working_directory: None,
theme_fingerprint,
};
assert_ne!(key(1), key(2));
let context = |fingerprint| {
bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
)
.with_theme(test_plugin_theme(fingerprint))
};
let first = dynamic_test_visual_with_context(&presentation, "call-theme", &context(1));
if first.is_none() {
wait_for_dynamic_completion(&presentation);
}
assert!(
dynamic_test_visual_with_context(&presentation, "call-theme", &context(1)).is_some()
);
let changed = dynamic_test_visual_with_context(&presentation, "call-theme", &context(2));
drop(presentation);
if let Some(visual) = changed {
assert_eq!(visual.route.plugin_id, "bcode.shell");
}
}
#[test]
fn serialized_visual_cache_separates_generation_payload_and_render_context() {
use bcode_plugin_sdk::tui::{PluginTuiDiffLayout, PluginTuiVisualRenderContext};
let presentation = dynamic_test_presentation("success", &[]);
let payload = |generation, preview| {
serde_json::json!({
"cache_probe": true,
"generation": generation,
"preview": preview,
"_bcode_artifact": {"artifact_id": format!("draft-{generation}")},
})
};
let cases = [
(payload(1, "first"), PluginTuiDiffLayout::Unified, None),
(payload(2, "second"), PluginTuiDiffLayout::Unified, None),
(payload(2, "changed"), PluginTuiDiffLayout::Unified, None),
(
payload(2, "changed"),
PluginTuiDiffLayout::Unified,
Some(std::path::PathBuf::from("/tmp/changed")),
),
(
payload(2, "changed"),
PluginTuiDiffLayout::SideBySide,
Some(std::path::PathBuf::from("/tmp/changed")),
),
];
for (payload, layout, cwd) in &cases {
let context = PluginTuiVisualRenderContext::new(80, *layout, cwd.clone());
let render = || {
presentation.routed_visual(
"cache-boundary",
1,
"bcode.shell.run",
1,
None,
payload,
&context,
)
};
if let Some(fallback) = render() {
assert_ne!(fallback.route.plugin_id, "test.dynamic-visual");
}
wait_for_dynamic_completion(&presentation);
let visual = render().expect("completed serialized projection");
assert_eq!(visual.route.plugin_id, "test.dynamic-visual");
assert_eq!(
routed_text(&visual),
format!("{payload}:{layout:?}:{cwd:?}")
);
assert_eq!(
routed_text(&render().expect("idle cache hit")),
routed_text(&visual)
);
}
let context = PluginTuiVisualRenderContext::new(80, PluginTuiDiffLayout::Unified, None);
let visual = presentation
.routed_visual(
"cache-boundary",
1,
"bcode.shell.run",
1,
None,
&cases[0].0,
&context,
)
.expect("retained exact cache entry");
drop(presentation);
assert_eq!(routed_text(&visual), format!("{}:Unified:None", cases[0].0));
}
fn dynamic_test_visual_with_context(
presentation: &PluginTuiPresentation,
invocation_id: &str,
context: &bcode_plugin_sdk::tui::PluginTuiVisualRenderContext,
) -> Option<RoutedTuiVisual> {
presentation.routed_visual(
invocation_id,
0,
"bcode.shell.run",
1,
None,
&serde_json::json!({
"arguments": {"command": "printf hello", "cwd": "/tmp/project"}
}),
context,
)
}
fn dynamic_test_visual(
presentation: &PluginTuiPresentation,
invocation_id: &str,
) -> Option<RoutedTuiVisual> {
presentation.routed_visual(
invocation_id,
0,
"bcode.shell.run",
1,
Some("bcode.shell"),
&serde_json::json!({
"arguments": {
"command": "printf hello",
"cwd": "/tmp/project"
}
}),
&bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
Some(std::path::PathBuf::from("/tmp/project")),
),
)
}
fn routed_text(visual: &RoutedTuiVisual) -> String {
visual
.rows
.iter()
.flat_map(|line| &line.spans)
.map(|span| span.content.as_str())
.collect()
}
fn wait_for_dynamic_completion(presentation: &PluginTuiPresentation) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if presentation.poll_dynamic_visuals() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
panic!("dynamic visual completion timed out");
}
fn wait_for_dynamic_text(
presentation: &PluginTuiPresentation,
invocation_id: &str,
expected: &str,
) -> RoutedTuiVisual {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
let mut last_text = None;
while std::time::Instant::now() < deadline {
let _ = presentation.poll_dynamic_visuals();
if let Some(visual) = dynamic_test_visual(presentation, invocation_id) {
let text = routed_text(&visual);
if text == expected {
return visual;
}
last_text = Some(text);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
panic!("dynamic visual text did not converge to {expected}; last text: {last_text:?}");
}
fn hello_dynamic_library_path() -> std::path::PathBuf {
let executable = std::env::current_exe().expect("current test executable path");
let directory = executable.parent().expect("test executable parent");
let prefix = format!("{}bcode_hello_plugin", std::env::consts::DLL_PREFIX);
std::fs::read_dir(directory)
.expect("test dependency directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.find(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with(&prefix) && name.ends_with(std::env::consts::DLL_SUFFIX)
})
})
.expect("hello plugin dynamic library")
}
fn discovered_hello_presentation() -> PluginTuiPresentation {
let root = tempfile::tempdir().expect("plugin discovery root");
let plugin_dir = root.path().join("hello");
std::fs::create_dir_all(&plugin_dir).expect("plugin directory");
let manifest_path = plugin_dir.join("bcode-plugin.toml");
let mut manifest: toml::Value = toml::from_str(include_str!(
"../../../examples/hello-plugin/bcode-plugin.toml"
))
.expect("hello manifest");
let library = hello_dynamic_library_path();
manifest["runtime"]["library"] = toml::Value::String(
library
.file_name()
.expect("library name")
.to_string_lossy()
.into_owned(),
);
std::fs::write(
&manifest_path,
toml::to_string(&manifest).expect("manifest TOML"),
)
.expect("plugin manifest");
std::fs::copy(
&library,
plugin_dir.join(
library
.file_name()
.expect("hello plugin dynamic library name"),
),
)
.expect("plugin library");
let registered = bcode_plugin::discover_plugins_in_roots(&[root.path().to_path_buf()])
.expect("discover user plugin");
let runtime: PluginRuntimeHost = PluginHost::load_registered_plugins(®istered)
.expect("load discovered user plugin")
.into();
PluginTuiPresentation::with_runtime_config_and_extensions(
runtime,
bcode_config::TuiVisualAdapterConfig {
preferred: vec!["example.hello/hello-shell-request-card".to_owned()],
disabled: BTreeSet::new(),
},
&[],
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn discovered_dynamic_user_adapter_renders_shell_request_and_result() {
let presentation = discovered_hello_presentation();
for (schema, adapter_id) in [
("bcode.tool.request.shell.run", "hello-shell-request-card"),
("bcode.shell.run", "hello-shell-card"),
] {
let invocation_id = format!("call-{adapter_id}");
let initial = presentation.routed_visual(
&invocation_id,
1,
schema,
1,
Some("bcode.shell"),
&serde_json::json!({"command": "printf hello"}),
&bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
),
);
assert!(initial.is_none(), "dynamic work must be off-frame");
wait_for_dynamic_completion(&presentation);
assert!(
presentation
.dynamic_visuals
.lock()
.expect("visual coordinator")
.cache
.values()
.all(Result::is_ok),
"dynamic adapter errors: {:?}",
presentation
.dynamic_visuals
.lock()
.expect("visual coordinator")
.cache
.values()
.filter_map(|result| result.as_ref().err())
.collect::<Vec<_>>()
);
let routed = presentation
.routed_visual(
&invocation_id,
1,
schema,
1,
Some("bcode.shell"),
&serde_json::json!({"command": "printf hello"}),
&bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
),
)
.expect("discovered dynamic visual");
assert_eq!(routed.route.plugin_id, "example.hello");
assert_eq!(routed.route.adapter_id, adapter_id);
assert_eq!(routed.header.title.as_deref(), Some("Hello user shell"));
assert_eq!(
routed_text(&routed),
format!("hello dynamic {invocation_id}")
);
}
drop(presentation);
}
#[test]
fn preferred_dynamic_adapter_overrides_producer_native_adapter_after_completion() {
let presentation = dynamic_test_presentation(
"test.dynamic-visual/success",
&[
"test.dynamic-visual/malformed",
"test.dynamic-visual/failure",
"test.dynamic-visual/timeout",
],
);
let initial = dynamic_test_visual(&presentation, "call-success")
.expect("producer-native fallback while dynamic visual runs");
assert_eq!(initial.route.plugin_id, "bcode.shell");
wait_for_dynamic_completion(&presentation);
let routed =
dynamic_test_visual(&presentation, "call-success").expect("completed dynamic visual");
let dynamic_cache = presentation
.dynamic_visuals
.lock()
.expect("dynamic visual cache");
assert_eq!(
routed.route.adapter_reference(),
"test.dynamic-visual/success",
"cached dynamic results: {:?}",
dynamic_cache.cache
);
drop(dynamic_cache);
drop(presentation);
assert_eq!(routed.header.title.as_deref(), Some("Dynamic shell"));
assert_eq!(routed.header.timeout_ms, Some(321));
assert_eq!(routed_text(&routed), "dynamic:success:artifact-0");
}
#[test]
fn malformed_failed_and_timed_out_dynamic_adapters_fall_through() {
for adapter_id in ["malformed", "failure", "timeout"] {
let disabled = ["success", "malformed", "failure", "timeout"]
.into_iter()
.filter(|candidate| *candidate != adapter_id)
.map(|candidate| format!("test.dynamic-visual/{candidate}"))
.collect::<Vec<_>>();
let disabled = disabled.iter().map(String::as_str).collect::<Vec<_>>();
let presentation =
dynamic_test_presentation(&format!("test.dynamic-visual/{adapter_id}"), &disabled);
let initial = dynamic_test_visual(&presentation, &format!("call-{adapter_id}"))
.expect("native fallback while dynamic visual runs");
assert_eq!(initial.route.plugin_id, "bcode.shell");
wait_for_dynamic_completion(&presentation);
let fallback = dynamic_test_visual(&presentation, &format!("call-{adapter_id}"))
.expect("native fallback after dynamic failure");
drop(presentation);
assert_eq!(fallback.route.plugin_id, "bcode.shell", "{adapter_id}");
}
}
#[test]
fn dynamic_visual_cache_and_pending_work_remain_bounded() {
let presentation = dynamic_test_presentation(
"test.dynamic-visual/success",
&[
"test.dynamic-visual/malformed",
"test.dynamic-visual/failure",
"test.dynamic-visual/timeout",
],
);
for index in 0..(MAX_DYNAMIC_VISUAL_CACHE_ENTRIES + 64) {
let _ = dynamic_test_visual(&presentation, &format!("call-bound-{index}"));
}
let coordinator = presentation
.dynamic_visuals
.lock()
.expect("dynamic coordinator");
assert!(
coordinator
.cache
.len()
.saturating_add(coordinator.requested.len())
<= MAX_DYNAMIC_VISUAL_CACHE_ENTRIES
);
assert!(coordinator.requested.len() <= MAX_DYNAMIC_VISUAL_CACHE_ENTRIES);
drop(coordinator);
}
#[test]
fn dynamic_artifact_update_invalidates_only_affected_invocation() {
let presentation = dynamic_test_presentation(
"test.dynamic-visual/success",
&[
"test.dynamic-visual/malformed",
"test.dynamic-visual/failure",
"test.dynamic-visual/timeout",
],
);
for invocation_id in ["call-owner", "call-other"] {
let _ = dynamic_test_visual(&presentation, invocation_id);
wait_for_dynamic_completion(&presentation);
let visual =
dynamic_test_visual(&presentation, invocation_id).expect("cached dynamic visual");
assert_eq!(routed_text(&visual), "dynamic:success:artifact-0");
let _ = presentation.drain_dirty_visuals();
}
assert!(
presentation
.deliver_artifact_chunk(&PluginTuiArtifactChunk {
tool_call_id: "call-owner".to_owned(),
artifact_id: "artifact-owner".to_owned(),
reference_key: "shell_recording".to_owned(),
producer_plugin_id: "bcode.shell".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
content_type: Some(
"application/x-bcode-shell-recording; version=3".to_owned(),
),
offset: 0,
total_bytes: 3,
revision: 1,
finalized: false,
bytes: b"abc".to_vec(),
})
.expect("queue dynamic artifact")
);
wait_for_dynamic_completion(&presentation);
assert_eq!(presentation.visual_revision("call-owner"), 2);
assert_eq!(presentation.visual_revision("call-other"), 1);
assert_eq!(
presentation.drain_dirty_visuals(),
BTreeSet::from(["call-owner".to_owned()])
);
let during_refresh = dynamic_test_visual(&presentation, "call-owner")
.expect("native fallback or refreshed dynamic visual");
assert!(
during_refresh.route.plugin_id == "bcode.shell"
|| routed_text(&during_refresh) == "dynamic:success:artifact-1"
);
let refreshed =
wait_for_dynamic_text(&presentation, "call-owner", "dynamic:success:artifact-1");
assert_eq!(routed_text(&refreshed), "dynamic:success:artifact-1");
let unchanged =
dynamic_test_visual(&presentation, "call-other").expect("unaffected cached visual");
drop(presentation);
assert_eq!(routed_text(&unchanged), "dynamic:success:artifact-0");
}
#[test]
fn lazy_registry_receives_current_configuration_before_first_use() {
use bcode_plugin_sdk::tui::{
PluginTuiPresentationConfiguration, PluginTuiVisualAdapter,
PluginTuiVisualRenderContext,
};
struct ConfiguredAdapter(std::sync::atomic::AtomicBool);
impl PluginTuiVisualAdapter for ConfiguredAdapter {
fn supports(&self, _kind: &str) -> bool {
self.0.load(Ordering::Relaxed)
}
fn rows(
&self,
_kind: &str,
_payload: &serde_json::Value,
_context: &PluginTuiVisualRenderContext,
) -> Vec<Line> {
Vec::new()
}
fn configure(
&self,
configuration: &PluginTuiPresentationConfiguration,
) -> Result<(), String> {
self.0.store(
configuration.config["enabled"].as_bool().unwrap_or(false),
Ordering::Relaxed,
);
Ok(())
}
}
fn factory() -> PluginTuiRegistry {
let mut registry = PluginTuiRegistry::default();
registry.register_visual_adapter(
["configured"],
Box::new(ConfiguredAdapter(std::sync::atomic::AtomicBool::new(false))),
);
registry
}
let mut presentation = test_presentation();
presentation
.registry_factories
.insert("lazy-test".to_owned(), factory);
presentation
.configure(PluginTuiPresentationConfiguration {
version: 1,
config: serde_json::json!({"enabled": true}),
})
.unwrap();
let registry = presentation.registry("lazy-test").unwrap();
assert!(registry.supports_visual_adapter("configured", "test"));
assert!(Arc::ptr_eq(
®istry,
&presentation.registry("lazy-test").unwrap()
));
drop(presentation);
}
#[test]
fn presentation_configuration_updates_retained_registry_and_invalidates_once() {
use bcode_plugin_sdk::tui::{
PluginTuiPresentationConfiguration, PluginTuiVisualAdapter,
PluginTuiVisualRenderContext,
};
struct ConfigAdapter(Arc<Mutex<Vec<serde_json::Value>>>);
impl PluginTuiVisualAdapter for ConfigAdapter {
fn supports(&self, _kind: &str) -> bool {
true
}
fn rows(
&self,
_kind: &str,
_payload: &serde_json::Value,
_context: &PluginTuiVisualRenderContext,
) -> Vec<bmux_tui::prelude::Line> {
Vec::new()
}
fn configure(
&self,
configuration: &PluginTuiPresentationConfiguration,
) -> Result<(), String> {
if configuration.config.get("reject").is_some() {
return Err("private diagnostic".to_owned());
}
self.0.lock().unwrap().push(configuration.config.clone());
Ok(())
}
}
let presentation = test_presentation();
let calls = Arc::new(Mutex::new(Vec::new()));
let mut registry = PluginTuiRegistry::default();
registry.register_visual_adapter(["test"], Box::new(ConfigAdapter(Arc::clone(&calls))));
presentation.install_registry_for_test("test", registry);
presentation.install_registry_for_test("unrelated", PluginTuiRegistry::default());
let retained = presentation.registry("test").unwrap();
let revision = presentation.revision();
let configuration = PluginTuiPresentationConfiguration {
version: 1,
config: serde_json::json!({"enabled": false}),
};
assert!(presentation.configure(configuration.clone()).unwrap());
assert_eq!(presentation.revision(), revision + 1);
assert!(!presentation.configure(configuration).unwrap());
assert_eq!(calls.lock().unwrap().len(), 1);
assert!(Arc::ptr_eq(
&retained,
&presentation.registry("test").unwrap()
));
assert!(
presentation
.configure(PluginTuiPresentationConfiguration {
version: 2,
config: serde_json::json!({})
})
.is_err()
);
assert_eq!(presentation.revision(), revision + 1);
let error = presentation
.configure(PluginTuiPresentationConfiguration {
version: 1,
config: serde_json::json!({"reject": true}),
})
.unwrap_err();
assert!(!error.contains("private diagnostic"));
assert!(presentation.registry("test").is_none());
assert!(presentation.registry("unrelated").is_some());
assert!(
presentation
.configure(PluginTuiPresentationConfiguration::default())
.unwrap()
);
assert!(Arc::ptr_eq(
&retained,
&presentation.registry("test").unwrap()
));
drop(presentation);
}
#[derive(Default)]
struct StatefulTestAdapter {
bytes: Mutex<Vec<u8>>,
chunks: Mutex<Vec<(String, String, u64, u64)>>,
}
impl bcode_plugin_sdk::tui::PluginTuiVisualAdapter for StatefulTestAdapter {
fn supports(&self, kind: &str) -> bool {
kind == "bcode.shell.run"
}
fn accepts_artifact_reference(
&self,
kind: &str,
reference_key: &str,
content_type: Option<&str>,
) -> bool {
kind == "bcode.shell.run"
&& reference_key == "shell_recording"
&& content_type == Some("application/x-bcode-shell-recording; version=3")
}
fn artifact_chunk(&self, chunk: &PluginTuiArtifactChunk) -> Result<(), String> {
self.bytes
.lock()
.map_err(|_| "test adapter state poisoned".to_owned())?
.extend_from_slice(&chunk.bytes);
self.chunks
.lock()
.map_err(|_| "test adapter chunk state poisoned".to_owned())?
.push((
chunk.artifact_id.clone(),
chunk.reference_key.clone(),
chunk.offset,
chunk.revision,
));
Ok(())
}
fn rows(
&self,
_kind: &str,
_payload: &serde_json::Value,
_context: &bcode_plugin_sdk::tui::PluginTuiVisualRenderContext,
) -> Vec<Line> {
let text = self
.bytes
.lock()
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
.unwrap_or_default();
vec![Line::from(text)]
}
}
#[test]
fn rejected_native_recording_is_visible_and_invalidates_layout() {
let presentation = test_presentation();
let result = presentation.deliver_artifact_chunk(&PluginTuiArtifactChunk {
tool_call_id: "broken".to_owned(),
artifact_id: "artifact".to_owned(),
reference_key: "shell_recording".to_owned(),
producer_plugin_id: "bcode.shell".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
content_type: Some("application/x-bcode-shell-recording; version=3".to_owned()),
offset: 0,
total_bytes: 32,
revision: 1,
finalized: true,
bytes: vec![0; 32],
});
assert!(result.is_err());
assert!(presentation.drain_dirty_visuals().contains("broken"));
let context = bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
100,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
);
let visual = presentation
.routed_visual(
"broken",
1,
"bcode.shell.run",
1,
Some("bcode.shell"),
&serde_json::json!({"mode":"terminal"}),
&context,
)
.expect("visual");
assert!(routed_text(&visual).contains("Artifact replay failed"));
let revision = presentation.visual_revision("broken");
presentation.clear_artifact_failure("broken");
assert!(presentation.visual_revision("broken") > revision);
assert!(presentation.drain_dirty_visuals().contains("broken"));
let recovered = presentation
.routed_visual(
"broken",
1,
"bcode.shell.run",
1,
Some("bcode.shell"),
&serde_json::json!({"mode":"terminal"}),
&context,
)
.expect("recovered visual");
drop(presentation);
assert!(!routed_text(&recovered).contains("Artifact replay failed"));
}
#[test]
fn unadvertised_composer_adapter_cannot_activate_and_registry_lifetimes_are_distinct() {
struct Topics;
impl bcode_plugin_sdk::composer::PluginComposerContributor for Topics {
fn kind(&self) -> &'static str {
"example.topic"
}
fn activate(
&self,
document: &bcode_plugin_sdk::composer::ComposerDocument,
) -> Option<bcode_plugin_sdk::composer::ComposerActivation> {
Some(bcode_plugin_sdk::composer::ComposerActivation {
range: bcode_plugin_sdk::composer::ComposerTextRange {
start: 0,
end: document.text.len(),
},
query: document.text.clone(),
})
}
}
let presentation = test_presentation();
let mut registry = PluginTuiRegistry::default();
registry
.register_composer_contributor(Box::new(Topics))
.unwrap();
presentation.install_registry_for_test("bcode.shell", registry);
let document = bcode_plugin_sdk::composer::ComposerDocument {
document_id: "fake-document".into(),
revision: 0,
text: "#topic".into(),
cursor: 6,
selection: None,
};
assert!(presentation.activate_composer(&document).is_none());
let replacement = test_presentation();
assert_ne!(
presentation.composer_identity(),
replacement.composer_identity()
);
drop(presentation);
assert!(replacement.activate_composer(&document).is_none());
drop(replacement);
}
fn test_presentation() -> PluginTuiPresentation {
let bundled = [StaticBundledPlugin::new(
include_str!("../../../plugins/shell-plugin/bcode-plugin.toml"),
bcode_shell_plugin::static_plugin(),
)];
let selected = bcode_plugin::filter_selected_static_plugins(
&bundled,
&bcode_plugin::PluginSelection::all_enabled(),
)
.expect("select test plugin");
let host = PluginHost::load_static_plugins(&selected).expect("load test plugin");
PluginTuiPresentation::new(host)
}
fn deliver_shell_recording_commit(
presentation: &PluginTuiPresentation,
commit: &bcode_shell_plugin::recording::ShellRecordingCommit,
previous_bytes: u64,
revision: u64,
) -> u64 {
let mut bytes = std::fs::read(&commit.path).expect("recording prefix");
bytes.truncate(usize::try_from(commit.committed_bytes).expect("committed length"));
bytes.drain(..usize::try_from(previous_bytes).expect("offset"));
assert!(
presentation
.deliver_artifact_chunk(&PluginTuiArtifactChunk {
tool_call_id: "shell-call".to_owned(),
artifact_id: "shell-artifact".to_owned(),
reference_key: "shell_recording".to_owned(),
producer_plugin_id: "bcode.shell".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
content_type: Some(
"application/x-bcode-shell-recording; version=3".to_owned(),
),
offset: previous_bytes,
total_bytes: commit.committed_bytes,
revision,
finalized: commit.finalized,
bytes,
})
.expect("deliver growing artifact chunk")
);
assert_eq!(presentation.visual_revision("shell-call"), revision);
assert_eq!(
presentation.drain_dirty_visuals(),
BTreeSet::from(["shell-call".to_owned()])
);
commit.committed_bytes
}
#[test]
fn dirty_visual_drain_is_bounded_and_retains_overflow_for_next_tick() {
let presentation = test_presentation();
for index in 0..100 {
presentation.bump_visual_revision_for_test(&format!("call-{index:03}"));
}
let first = presentation.drain_dirty_visuals_bounded(64);
assert_eq!(first.len(), 64);
assert_eq!(first.first().map(String::as_str), Some("call-000"));
assert_eq!(first.last().map(String::as_str), Some("call-063"));
let second = presentation.drain_dirty_visuals_bounded(64);
assert_eq!(second.len(), 36);
assert_eq!(second.first().map(String::as_str), Some("call-064"));
assert_eq!(second.last().map(String::as_str), Some("call-099"));
assert!(presentation.drain_dirty_visuals_bounded(64).is_empty());
drop(presentation);
}
#[test]
fn one_visual_revision_changes_only_its_transcript_signature() {
let presentation = test_presentation();
let first = crate::transcript::tool_request_item(
"call-one",
Some("bcode.shell"),
"shell",
"{}",
None,
);
let second = crate::transcript::tool_request_item(
"call-two",
Some("bcode.shell"),
"shell",
"{}",
None,
);
let first_before =
crate::transcript_projection::test_layout_signature(&first, 80, Some(&presentation));
let second_before =
crate::transcript_projection::test_layout_signature(&second, 80, Some(&presentation));
presentation.bump_visual_revision_for_test("call-one");
let first_after =
crate::transcript_projection::test_layout_signature(&first, 80, Some(&presentation));
let second_after =
crate::transcript_projection::test_layout_signature(&second, 80, Some(&presentation));
drop(presentation);
assert_ne!(first_before, first_after);
assert_eq!(second_before, second_after);
}
#[test]
#[ignore = "manual deterministic performance baseline"]
fn targeted_visual_update_transcript_baseline_report() {
for transcript_len in [10_usize, 500, 2_000] {
let presentation = test_presentation();
let items = (0..transcript_len)
.map(|index| {
crate::transcript::tool_request_item(
&format!("call-{index}"),
Some("bcode.shell"),
"shell",
"{}",
None,
)
})
.collect::<Vec<_>>();
let mut cache = crate::transcript_layout::TranscriptLayoutCache::default();
cache.sync(crate::transcript_layout::TranscriptLayoutSpec {
width: 80,
fingerprint: crate::transcript_layout::TranscriptLayoutFingerprint::new(
"baseline-initial".to_owned(),
),
structural_fingerprint: crate::transcript_layout::TranscriptLayoutFingerprint::new(
"baseline-structure".to_owned(),
),
transcript_len,
pending_len: 0,
transcript_signature: |index| {
crate::transcript_projection::test_layout_signature(
&items[index],
80,
Some(&presentation),
)
},
transcript_rows: |index| vec![Line::from(format!("row-{index}"))].into(),
transcript_invocation_id: |index| Some(format!("call-{index}")),
pending_signature: |index| {
crate::transcript_layout::TranscriptLayoutSignature::new(format!(
"pending-{index}"
))
},
pending_rows: |_| Vec::new().into(),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
presentation.bump_visual_revision_for_test("call-0");
let started = Instant::now();
let stats = cache.sync_visuals(
crate::transcript_layout::TranscriptLayoutFingerprint::new(
"baseline-updated".to_owned(),
),
&std::collections::BTreeSet::from(["call-0".to_owned()]),
|index| {
crate::transcript_projection::test_layout_signature(
&items[index],
80,
Some(&presentation),
)
},
|index| vec![Line::from(format!("row-{index}"))].into(),
);
let elapsed = started.elapsed();
drop(presentation);
println!(
"BCODE_PERF_CASE {}",
serde_json::json!({
"domain": "transcript_visual_update",
"transcript_entries": transcript_len,
"entries_scanned": stats.entries_scanned,
"signatures_changed": stats.signatures_changed,
"entries_rebuilt": stats.entries_rebuilt,
"rows_regenerated": stats.rows_regenerated,
"sync_us": stats.duration_micros,
"wall_us": u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX),
})
);
}
}
#[test]
fn one_visual_update_rebuilds_only_its_entry_across_transcript_sizes() {
for transcript_len in [10_usize, 500, 2_000] {
let presentation = test_presentation();
let items = (0..transcript_len)
.map(|index| {
crate::transcript::tool_request_item(
&format!("call-{index}"),
Some("bcode.shell"),
"shell",
"{}",
None,
)
})
.collect::<Vec<_>>();
let mut cache = crate::transcript_layout::TranscriptLayoutCache::default();
let initial = cache.sync(crate::transcript_layout::TranscriptLayoutSpec {
width: 80,
fingerprint: crate::transcript_layout::TranscriptLayoutFingerprint::new(
"initial".to_owned(),
),
structural_fingerprint: crate::transcript_layout::TranscriptLayoutFingerprint::new(
"structure".to_owned(),
),
transcript_len,
pending_len: 0,
transcript_signature: |index| {
crate::transcript_projection::test_layout_signature(
&items[index],
80,
Some(&presentation),
)
},
transcript_rows: |index| vec![Line::from(format!("row-{index}"))].into(),
transcript_invocation_id: |index| Some(format!("call-{index}")),
pending_signature: |index| {
crate::transcript_layout::TranscriptLayoutSignature::new(format!(
"pending-{index}"
))
},
pending_rows: |_| Vec::new().into(),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(initial.entries_rebuilt, transcript_len);
presentation.bump_visual_revision_for_test("call-0");
let updated = cache.sync_visuals(
crate::transcript_layout::TranscriptLayoutFingerprint::new("updated".to_owned()),
&std::collections::BTreeSet::from(["call-0".to_owned()]),
|index| {
crate::transcript_projection::test_layout_signature(
&items[index],
80,
Some(&presentation),
)
},
|index| vec![Line::from(format!("row-{index}"))].into(),
);
drop(presentation);
assert_eq!(updated.entries_scanned, 1);
assert_eq!(updated.signatures_changed, 1);
assert_eq!(updated.entries_rebuilt, 1);
assert_eq!(updated.rows_regenerated, 1);
}
}
#[test]
fn git_contribution_schema_routes_through_platform_registry() {
let bundled = [StaticBundledPlugin::new(
include_str!("../../../plugins/git-plugin/bcode-plugin.toml"),
bcode_git_plugin::static_plugin(),
)];
let selected = bcode_plugin::filter_selected_static_plugins(
&bundled,
&bcode_plugin::PluginSelection::all_enabled(),
)
.expect("select Git plugin");
let presentation = PluginTuiPresentation::new(
PluginHost::load_static_plugins(&selected).expect("load Git plugin"),
);
let route = presentation
.visual_route("bcode.git.clone_request", 1, Some("bcode.git"))
.expect("Git contribution route");
assert_eq!(route.plugin_id, "bcode.git");
let registry = presentation.registry("bcode.git").expect("Git registry");
drop(presentation);
let rows = registry
.visual_rows(
&route.adapter_id,
"bcode.git.clone_request",
&serde_json::json!({
"url": "https://github.com/bmorphism/bcode",
"ref": "main"
}),
&bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
),
)
.expect("Git contribution rows");
let rendered = rows
.iter()
.flat_map(|line| &line.spans)
.map(|span| span.content.as_str())
.collect::<String>();
assert!(rendered.contains("github.com/bmorphism/bcode"));
assert!(rendered.contains("main"));
}
#[test]
fn growing_shell_artifact_advances_only_owner_revision() {
let presentation = test_presentation();
let producer = "bcode.shell";
let schema = "bcode.shell.run";
let reference_key = "shell_recording";
let content_type = "application/x-bcode-shell-recording; version=3";
assert!(presentation.accepts_artifact_reference(
producer,
schema,
1,
reference_key,
Some(content_type),
));
let directory = tempfile::tempdir().expect("recording temp dir");
let path = directory.path().join("growing.bcsr");
let (commit_sender, commit_receiver) = std::sync::mpsc::channel();
let observer: bcode_shell_plugin::recording::ShellRecordingCommitObserver =
Arc::new(move |commit| {
let _ = commit_sender.send(commit);
});
let mut writer =
bcode_shell_plugin::recording::AsyncShellRecordingWriter::create_with_observer(
&path,
80,
24,
Some(observer),
)
.expect("recording writer");
let mut previous_bytes = deliver_shell_recording_commit(
&presentation,
&commit_receiver
.recv_timeout(std::time::Duration::from_secs(1))
.expect("initial recording commit"),
0,
1,
);
let mut delivered_revisions = 1_u64;
for revision in 1..=32 {
writer
.write_output_with(
revision,
format!("raw-{revision}\n").as_bytes(),
Some(format!("rendered-{revision}\r\n").as_bytes()),
|| {},
)
.expect("recording output");
delivered_revisions = delivered_revisions.saturating_add(1);
previous_bytes = deliver_shell_recording_commit(
&presentation,
&commit_receiver
.recv_timeout(std::time::Duration::from_secs(1))
.expect("growing recording commit"),
previous_bytes,
delivered_revisions,
);
}
writer
.finish(33, Some(0), None, false, false)
.expect("finish recording");
delivered_revisions = delivered_revisions.saturating_add(1);
let final_commit = commit_receiver
.recv_timeout(std::time::Duration::from_secs(1))
.expect("final recording commit");
assert!(final_commit.finalized);
let _final_bytes = deliver_shell_recording_commit(
&presentation,
&final_commit,
previous_bytes,
delivered_revisions,
);
assert_eq!(presentation.visual_revision("other-call"), 0);
assert_eq!(
presentation.drain_timings().len(),
usize::try_from(delivered_revisions).expect("revisions")
);
drop(presentation);
}
#[test]
fn artifact_delivery_routes_only_supported_references_and_invalidates_the_owner() {
let presentation = test_presentation();
let mut registry = PluginTuiRegistry::default();
registry.register_visual_adapter(
["shell-run-terminal-card"],
Box::new(StatefulTestAdapter::default()),
);
presentation
.registries
.lock()
.expect("presentation registries")
.insert("bcode.shell".to_owned(), Arc::new(registry));
assert!(presentation.accepts_artifact_reference(
"bcode.shell",
"bcode.shell.run",
1,
"shell_recording",
Some("application/x-bcode-shell-recording; version=3"),
));
assert!(!presentation.accepts_artifact_reference(
"bcode.shell",
"bcode.shell.run",
1,
"clean_output",
Some("text/plain"),
));
assert!(!presentation.accepts_artifact_reference(
"bcode.shell",
"bcode.shell.run",
1,
"shell_recording",
Some("text/plain"),
));
assert!(
presentation
.deliver_artifact_chunk(&PluginTuiArtifactChunk {
tool_call_id: "call-owner".to_owned(),
artifact_id: "artifact-owner".to_owned(),
reference_key: "shell_recording".to_owned(),
producer_plugin_id: "bcode.shell".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
content_type: Some(
"application/x-bcode-shell-recording; version=3".to_owned(),
),
offset: 0,
total_bytes: 3,
revision: 7,
finalized: false,
bytes: b"abc".to_vec(),
})
.expect("deliver owned artifact chunk")
);
assert_eq!(presentation.visual_revision("call-owner"), 1);
assert_eq!(presentation.visual_revision("call-other"), 0);
assert_eq!(
presentation.drain_dirty_visuals(),
BTreeSet::from(["call-owner".to_owned()])
);
drop(presentation);
}
#[test]
fn presentation_retains_one_registry_for_delivery_and_rendering() {
let presentation = test_presentation();
assert!(presentation.accepts_artifact_reference(
"bcode.shell",
"bcode.shell.run",
1,
"shell_recording",
Some("application/x-bcode-shell-recording; version=3"),
));
assert!(!presentation.accepts_artifact_reference(
"bcode.shell",
"bcode.shell.run",
1,
"clean_output",
Some("text/plain; charset=utf-8"),
));
let mut registry = PluginTuiRegistry::default();
registry.register_visual_adapter(
["shell-run-terminal-card"],
Box::new(StatefulTestAdapter::default()),
);
presentation
.registries
.lock()
.expect("presentation registries")
.insert("bcode.shell".to_owned(), Arc::new(registry));
let first = presentation.registry("bcode.shell").expect("registry");
assert_eq!(presentation.revision(), 0);
assert!(
presentation
.deliver_artifact_chunk(&PluginTuiArtifactChunk {
tool_call_id: "call".to_owned(),
artifact_id: "artifact".to_owned(),
reference_key: "reference".to_owned(),
producer_plugin_id: "bcode.shell".to_owned(),
schema: "bcode.shell.run".to_owned(),
schema_version: 1,
content_type: None,
offset: 0,
total_bytes: 3,
revision: 1,
finalized: false,
bytes: b"abc".to_vec(),
})
.expect("deliver artifact chunk")
);
assert_eq!(presentation.revision(), 0);
assert_eq!(presentation.visual_revision("call"), 1);
assert_eq!(presentation.visual_revision("other-call"), 0);
assert_eq!(
presentation.drain_dirty_visuals(),
BTreeSet::from(["call".to_owned()])
);
let second = presentation.registry("bcode.shell").expect("registry");
drop(presentation);
assert!(Arc::ptr_eq(&first, &second));
let rows = second
.visual_rows(
"shell-run-terminal-card",
"bcode.shell.run",
&serde_json::Value::Null,
&bcode_plugin_sdk::tui::PluginTuiVisualRenderContext::new(
80,
bcode_plugin_sdk::tui::PluginTuiDiffLayout::Unified,
None,
),
)
.expect("stateful adapter rows");
assert_eq!(rows[0].spans[0].content, "abc");
}
}