use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use bcode_client::BcodeClient;
use bcode_ipc::Event as BcodeEvent;
use bcode_plugin_sdk::tui::{
PluginSessionViewSubscription, PluginSessionViewSubscriptionRequest, PluginSessionViewUpdate,
PluginStructuredGenerationFuture, PluginStructuredGenerationRequest, PluginTask, PluginTuiHost,
PluginTuiHostError, PluginTuiSurfaceUpdate, PluginTuiSurfaceUpdateReceiver,
PluginWorkflowAuthoringApplyFuture, PluginWorkflowAuthoringCatalogFuture,
PluginWorkflowAuthoringDraft, PluginWorkflowAuthoringDraftFuture,
PluginWorkflowAuthoringEditFuture, PluginWorkflowAuthoringEditResult,
PluginWorkflowAuthoringForkFuture, PluginWorkflowAuthoringPreviewFuture,
PluginWorkflowAuthoringPublishFuture, PluginWorkflowAuthoringPublishResult,
PluginWorkflowAuthoringRevision, PluginWorkflowAuthoringRevisionFuture,
PluginWorkflowAuthoringStartFuture, PluginWorkflowAuthoringValidationFuture,
PluginWorkflowControlAction, PluginWorkflowControlFuture, PluginWorkflowControlResult,
PluginWorkflowGeneratedCandidate, PluginWorkflowGeneratedCandidateAcceptance,
PluginWorkflowGeneratedCandidateAcceptanceFuture, PluginWorkflowInspection,
PluginWorkflowInspectionFuture, PluginWorkflowLaunchCatalogFuture,
PluginWorkflowLaunchDetailFuture, PluginWorkflowLookup, PluginWorkflowLookupFuture,
PluginWorkflowPackageExportStartRequest, PluginWorkflowStartFuture, PluginWorkflowStartRequest,
PluginWorkflowStartResponse, PluginWorkflowStatus, PluginWorkflowSummary,
PluginWorkflowTemplateInstantiationFuture, PluginWorkflowTemplateInstantiationRequest,
PluginWorkflowTemplateStartRequest,
};
use bcode_session_models::SessionId;
use bcode_session_view::SessionView;
use bcode_session_view_models::PermissionView;
use bcode_session_view_models::SessionConnectionViewStatus;
use bmux_tui_runtime::InvalidationSignal;
use tokio::sync::mpsc;
const DEFAULT_PLUGIN_SESSION_VIEW_BUFFER: usize = 32;
const MAX_PLUGIN_SESSION_VIEW_BUFFER: usize = 256;
pub fn compose_surface_input(
surface: &mut dyn bcode_plugin_sdk::tui::PluginTuiSurface,
event: &bmux_tui::event::Event,
host: &dyn PluginTuiHost,
) -> bcode_plugin_sdk::tui::PluginTuiEventResult {
let editor = surface.focused_text_field_event(event, host);
if editor.outcome == bmux_tui::event::EventOutcome::Ignored {
surface.handle_event_result(event, host)
} else {
editor
}
}
#[derive(Debug, Clone)]
struct BcodePluginTuiHost {
handle: tokio::runtime::Handle,
redraw: InvalidationSignal,
active_tasks: Arc<AtomicUsize>,
launch: FreshSessionSettings,
client: BcodeClient,
}
#[derive(Debug, Clone, Default)]
pub struct FreshSessionSettings {
pub keymap: bcode_plugin_sdk::tui::keymap::KeymapRegistry,
pub working_directory: std::path::PathBuf,
pub provider: Option<String>,
pub model: Option<String>,
pub agent: Option<String>,
pub effort: Option<String>,
pub summary: Option<String>,
}
fn workflow_start_request(
request: PluginWorkflowStartRequest,
) -> Result<bcode_workflow::WorkflowStartRequest, PluginTuiHostError> {
let parent_scope = request.parent_session_id.to_string();
if request.binding.scope_key != parent_scope {
return Err(PluginTuiHostError::InvalidRequest(
"workflow binding scope must match the active parent session".to_string(),
));
}
Ok(bcode_workflow::WorkflowStartRequest {
identity: request.identity,
definition: request.definition,
run_id: request.run_id,
workspace_snapshot: None,
parent_session_id: request.parent_session_id,
input: request.input,
binding: bcode_workflow_store::WorkflowRunBinding {
owner_plugin_id: request.binding.owner_plugin_id,
workflow_kind: request.binding.workflow_kind,
scope_key: request.binding.scope_key,
display_label: request.binding.display_label,
single_active: request.binding.single_active,
},
limits: workflow_run_limits(&request.limits),
})
}
fn workflow_package_export_start_request(
request: PluginWorkflowPackageExportStartRequest,
) -> bcode_workflow::StartWorkflowPackageExportRequest {
bcode_workflow::StartWorkflowPackageExportRequest {
package_export: request.package_export,
run_id: request.run_id,
parent_session_id: request.parent_session_id,
workspace_snapshot: request.workspace_snapshot,
parent_session_generation: request.parent_session_generation,
configuration: request.configuration,
input: request.input,
}
}
fn workflow_run_limits(
policy: &bcode_workflow::WorkflowRunLimitPolicy,
) -> bcode_workflow_store::WorkflowRunLimits {
bcode_workflow_store::WorkflowRunLimits {
deadline_at_ms: policy.maximum_duration_ms.map(|duration| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|now| u64::try_from(now.as_millis()).unwrap_or(u64::MAX))
.unwrap_or_default()
.saturating_add(duration)
}),
node_execution_cap: policy.node_execution_cap,
concurrency_cap: policy.concurrency_cap,
cycle_cap: policy.cycle_cap,
retry_cap: policy.retry_cap,
recursion_depth_cap: policy.recursion_depth_cap,
descendant_cap: policy.descendant_cap,
}
}
impl PluginTuiHost for BcodePluginTuiHost {
fn keymap_action(&self, scope: &str, stroke: bmux_keyboard::KeyStroke) -> Option<String> {
self.launch
.keymap
.action_for_key(scope, stroke)
.map(str::to_owned)
}
fn keymap_is_overridden(&self, scope: &str, stroke: bmux_keyboard::KeyStroke) -> bool {
self.launch.keymap.is_overridden_key(scope, stroke)
}
fn keymap_keys(&self, scope: &str, action: &str) -> Vec<String> {
self.launch
.keymap
.keys_for_action(scope, action)
.into_iter()
.map(str::to_owned)
.collect()
}
fn validate_judgement_setup(
&self,
selection: bcode_model::judgement::SetupSelection,
) -> bcode_plugin_sdk::tui::PluginJudgementValidationFuture {
let client = self.client.clone();
Box::pin(async move {
client
.validate_judgement_setup(&selection)
.await
.map_err(|_| {
PluginTuiHostError::Internal("judgement setup validation unavailable".into())
})
})
}
fn judgement_setup(&self) -> bcode_plugin_sdk::tui::PluginJudgementSetupFuture {
let client = self.client.clone();
Box::pin(async move {
client.judgement_setup().await.map_err(|_| {
PluginTuiHostError::Internal("judgement setup unavailable; retry discovery".into())
})
})
}
fn prepare_fresh_session(
&self,
existing: Option<SessionId>,
) -> bcode_plugin_sdk::tui::PluginCreateSessionFuture {
let client = self.client.clone();
let launch = self.launch.clone();
Box::pin(async move {
let Some(session_id) = existing else {
return client
.create_session_in_working_directory(None, launch.working_directory)
.await
.map(|session| session.id)
.map_err(|error| PluginTuiHostError::Internal(error.to_string()));
};
super::effects::apply_submit_runtime_selections(
&client,
session_id,
launch.provider,
launch.model,
launch.agent,
launch.effort,
launch.summary,
)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
Ok(session_id)
})
}
fn usage_report(
&self,
query: bcode_usage_models::UsageQuery,
) -> bcode_plugin_sdk::tui::PluginUsageReportFuture {
let client = self.client.clone();
Box::pin(async move {
client.usage_report(query).await.map_err(|_| {
PluginTuiHostError::Internal("usage query failed; refresh or recollect".into())
})
})
}
fn collect_usage(
&self,
session_id: SessionId,
query: bcode_session_models::SessionUsageQuery,
) -> bcode_plugin_sdk::tui::PluginUsageCollectFuture {
let client = self.client.clone();
Box::pin(async move {
client.collect_usage(session_id, query).await.map_err(|_| {
PluginTuiHostError::Internal("usage collection failed; restart collection".into())
})
})
}
fn spawn(&self, task: PluginTask) {
let redraw = self.redraw.clone();
let active_tasks = Arc::clone(&self.active_tasks);
active_tasks.fetch_add(1, Ordering::AcqRel);
drop(self.handle.spawn(async move {
task.await;
active_tasks.fetch_sub(1, Ordering::AcqRel);
redraw.request();
}));
}
fn spawn_blocking(&self, task: Box<dyn FnOnce() + Send + 'static>) {
let redraw = self.redraw.clone();
let active_tasks = Arc::clone(&self.active_tasks);
active_tasks.fetch_add(1, Ordering::AcqRel);
drop(self.handle.spawn_blocking(move || {
task();
active_tasks.fetch_sub(1, Ordering::AcqRel);
redraw.request();
}));
}
fn request_redraw(&self) {
self.redraw.request();
}
fn copy_text(&self, text: String) -> Result<(), PluginTuiHostError> {
let mut clipboard = arboard::Clipboard::new()
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
clipboard
.set_text(text)
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
}
fn workflow_delegation_preflight(
&self,
plugin_id: String,
) -> bcode_plugin_sdk::tui::PluginWorkflowDelegationPreflightFuture {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_delegation_preflight(plugin_id)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn start_workflow(&self, request: PluginWorkflowStartRequest) -> PluginWorkflowStartFuture {
let client = self.client.clone();
Box::pin(async move {
let started = client
.start_workflow(workflow_start_request(request)?)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
Ok(PluginWorkflowStartResponse {
run_id: started.run.run_id,
runtime_work_id: started.runtime_work_id.to_string(),
})
})
}
fn associated_workflow(&self, lookup: PluginWorkflowLookup) -> PluginWorkflowLookupFuture {
let client = self.client.clone();
Box::pin(async move {
client
.associated_workflow_run(workflow_lookup(lookup))
.await
.map(|run| run.map(workflow_summary))
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn inspect_associated_workflow(
&self,
lookup: PluginWorkflowLookup,
limit: usize,
) -> PluginWorkflowInspectionFuture {
let client = self.client.clone();
Box::pin(async move {
client
.inspect_associated_workflow_run(workflow_lookup(lookup), limit)
.await
.and_then(|inspection| inspection.map(workflow_inspection).transpose())
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn control_associated_workflow(
&self,
lookup: PluginWorkflowLookup,
action: PluginWorkflowControlAction,
) -> PluginWorkflowControlFuture {
let client = self.client.clone();
Box::pin(async move {
let (run, changed) = client
.control_associated_workflow_run(
workflow_lookup(lookup),
match action {
PluginWorkflowControlAction::Pause => {
bcode_workflow::WorkflowRunControlAction::Pause
}
PluginWorkflowControlAction::Resume => {
bcode_workflow::WorkflowRunControlAction::Resume
}
PluginWorkflowControlAction::Cancel => {
bcode_workflow::WorkflowRunControlAction::Cancel
}
},
)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
Ok(PluginWorkflowControlResult {
run: run.map(workflow_summary),
changed,
})
})
}
fn workflow_authoring_catalog(&self) -> PluginWorkflowAuthoringCatalogFuture {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_authoring_catalog()
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn workflow_scheduling_page(
&self,
request: bcode_workflow::WorkflowSchedulingPageRequest,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
bcode_workflow::WorkflowSchedulingPage<
bcode_workflow_view_models::WorkflowSchedulingView,
>,
PluginTuiHostError,
>,
> + Send,
>,
> {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_scheduling_page(request)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn workflow_launch_catalog(
&self,
request: bcode_workflow::WorkflowLaunchCatalogRequest,
) -> PluginWorkflowLaunchCatalogFuture {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_launch_catalog(request)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn workflow_launch_detail(
&self,
request: bcode_workflow::WorkflowLaunchDetailRequest,
) -> PluginWorkflowLaunchDetailFuture {
let client = self.client.clone();
Box::pin(async move {
let (cancel, interrupted) = tokio::sync::oneshot::channel::<()>();
let task = tokio::spawn(async move {
Box::pin(client.workflow_launch_detail_until(request, async {
let _ = interrupted.await;
}))
.await
});
let result = task.await;
drop(cancel);
result
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?
.ok_or_else(|| PluginTuiHostError::Internal("workflow detail interrupted".into()))
})
}
fn session_working_document(
&self,
request: bcode_session_models::SessionWorkingDocumentRequest,
) -> bcode_plugin_sdk::tui::PluginWorkingDocumentFuture {
let client = self.client.clone();
Box::pin(async move {
client
.session_working_document(request)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn generate_structured_output(
&self,
request: PluginStructuredGenerationRequest,
) -> PluginStructuredGenerationFuture {
self.generate_observable_structured_output(
request,
bcode_plugin_sdk::tui::PluginStructuredGenerationControl::default(),
)
}
fn generate_observable_structured_output(
&self,
request: PluginStructuredGenerationRequest,
control: bcode_plugin_sdk::tui::PluginStructuredGenerationControl,
) -> PluginStructuredGenerationFuture {
let client = self.client.clone();
Box::pin(async move {
client
.generate_structured_output(request, control)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn accept_generated_workflow_candidate(
&self,
candidate: PluginWorkflowGeneratedCandidate,
) -> PluginWorkflowGeneratedCandidateAcceptanceFuture {
let client = self.client.clone();
Box::pin(async move {
let producer = candidate.document.producer.clone();
if producer.kind != bcode_workflow::WorkflowProducerKind::Generated {
return Err(PluginTuiHostError::InvalidRequest(
"generated candidate provenance is required".to_string(),
));
}
if let Some(target) = candidate.target {
return client
.update_workflow_draft(bcode_workflow::UpdateWorkflowDraftRequest {
workflow_id: target.workflow_id,
draft_id: target.draft_id,
expected_generation: target.expected_generation,
document: candidate.document,
producer,
})
.await
.map(|result| match result {
bcode_workflow::WorkflowDraftUpdateResult::Updated(draft) => {
PluginWorkflowGeneratedCandidateAcceptance::Updated(Box::new(
plugin_workflow_authoring_draft(*draft),
))
}
bcode_workflow::WorkflowDraftUpdateResult::Conflict(conflict) => {
PluginWorkflowGeneratedCandidateAcceptance::Conflict {
expected_generation: conflict.expected_generation,
current_generation: conflict.current_generation,
}
}
})
.map_err(|error| PluginTuiHostError::Internal(error.to_string()));
}
let (_, draft) = client
.create_authored_workflow(bcode_workflow::CreateAuthoredWorkflowRequest {
document: candidate.document,
draft_id: candidate.draft_id,
})
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
Ok(PluginWorkflowGeneratedCandidateAcceptance::Created(
Box::new(plugin_workflow_authoring_draft(draft)),
))
})
}
fn instantiate_workflow_template(
&self,
request: PluginWorkflowTemplateInstantiationRequest,
) -> PluginWorkflowTemplateInstantiationFuture {
let client = self.client.clone();
Box::pin(async move {
let (_, draft) = client
.instantiate_workflow_template(
bcode_workflow::WorkflowTemplateInstantiationRequest {
owner_plugin_id: request.owner_plugin_id,
template_id: request.template_id,
template_version: request.template_version,
workflow_id: request.workflow_id,
draft_id: request.draft_id,
},
)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))?;
Ok(plugin_workflow_authoring_draft(draft))
})
}
fn workflow_authoring_draft(
&self,
workflow_id: String,
draft_id: String,
) -> PluginWorkflowAuthoringDraftFuture {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_draft(workflow_id, draft_id)
.await
.map(|draft| draft.map(plugin_workflow_authoring_draft))
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn fork_workflow_authoring_revision(
&self,
workflow_id: String,
revision: u64,
draft_id: String,
producer: bcode_workflow::WorkflowProducerProvenance,
) -> PluginWorkflowAuthoringForkFuture {
let client = self.client.clone();
Box::pin(async move {
client
.fork_workflow_draft(bcode_workflow::ForkWorkflowDraftRequest {
workflow_id,
source: bcode_workflow::WorkflowDraftForkSource::Revision { revision },
draft_id,
producer,
})
.await
.map(plugin_workflow_authoring_draft)
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn workflow_authoring_revision(
&self,
workflow_id: String,
revision: u64,
) -> PluginWorkflowAuthoringRevisionFuture {
let client = self.client.clone();
Box::pin(async move {
client
.workflow_revision(workflow_id, revision)
.await
.map(|revision| revision.map(plugin_workflow_authoring_revision))
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn apply_workflow_authoring_edits(
&self,
workflow_id: String,
draft_id: String,
batch: bcode_workflow::WorkflowAuthoringEditBatch,
producer: bcode_workflow::WorkflowProducerProvenance,
) -> PluginWorkflowAuthoringEditFuture {
let client = self.client.clone();
Box::pin(async move {
client
.apply_workflow_draft_edits(bcode_workflow::ApplyWorkflowDraftEditsRequest {
workflow_id,
draft_id,
batch,
producer,
})
.await
.map(plugin_workflow_authoring_edit_result)
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn validate_workflow_authoring(
&self,
document: bcode_workflow::WorkflowAuthoringDocument,
) -> PluginWorkflowAuthoringValidationFuture {
let client = self.client.clone();
Box::pin(async move {
client
.validate_workflow_authoring(document)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn apply_workflow_source(
&self,
source_format: bcode_workflow::WorkflowSourceFormat,
source: String,
draft_id: String,
) -> PluginWorkflowAuthoringApplyFuture {
let client = self.client.clone();
Box::pin(async move {
client
.apply_workflow_source(source_format, source, draft_id)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn preview_workflow_authoring(
&self,
document: bcode_workflow::WorkflowAuthoringDocument,
configuration: Option<serde_json::Value>,
) -> PluginWorkflowAuthoringPreviewFuture {
let client = self.client.clone();
Box::pin(async move {
client
.preview_workflow_compilation(document, configuration)
.await
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn publish_workflow_authoring_draft(
&self,
workflow_id: String,
draft_id: String,
expected_generation: u64,
activate: bool,
) -> PluginWorkflowAuthoringPublishFuture {
let client = self.client.clone();
Box::pin(async move {
client
.publish_workflow_draft(bcode_workflow::PublishWorkflowDraftRequest {
workflow_id,
draft_id,
expected_generation,
configuration: None,
activate,
expected_active_revision: None,
control: bcode_workflow::WorkflowComputationControl::default(),
})
.await
.map(plugin_workflow_authoring_publish_result)
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn start_authored_workflow_revision(
&self,
workflow_id: String,
revision: u64,
parent_session_id: SessionId,
workspace_snapshot: Option<String>,
configuration: Option<serde_json::Value>,
) -> PluginWorkflowAuthoringStartFuture {
let client = self.client.clone();
Box::pin(async move {
client
.start_authored_workflow(bcode_workflow::StartAuthoredWorkflowRequest {
selection: bcode_workflow::AuthoredWorkflowRunSelection::Revision {
workflow_id,
revision,
},
run_id: None,
parent_session_id,
workspace_snapshot,
parent_session_generation: None,
configuration,
input: None,
})
.await
.map(|started| PluginWorkflowStartResponse {
run_id: started.started.run.run_id,
runtime_work_id: started.started.runtime_work_id.to_string(),
})
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn start_workflow_package_export(
&self,
request: PluginWorkflowPackageExportStartRequest,
) -> PluginWorkflowAuthoringStartFuture {
let client = self.client.clone();
Box::pin(async move {
client
.start_workflow_package_export(workflow_package_export_start_request(request))
.await
.map(|started| PluginWorkflowStartResponse {
run_id: started.started.started.run.run_id,
runtime_work_id: started.started.started.runtime_work_id.to_string(),
})
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn start_workflow_template(
&self,
request: PluginWorkflowTemplateStartRequest,
) -> PluginWorkflowAuthoringStartFuture {
let client = self.client.clone();
Box::pin(async move {
client
.start_workflow_template(bcode_workflow::WorkflowTemplateStartRequest {
owner_plugin_id: request.owner_plugin_id,
template_id: request.template_id,
template_version: request.template_version,
run_id: request.run_id,
workspace_snapshot: request.workspace_snapshot,
parent_session_id: request.parent_session_id,
configuration: request.configuration,
limits: workflow_run_limits(&request.limits),
})
.await
.map(|started| PluginWorkflowStartResponse {
run_id: started.run.run_id,
runtime_work_id: started.runtime_work_id.to_string(),
})
.map_err(|error| PluginTuiHostError::Internal(error.to_string()))
})
}
fn subscribe_session_view(
&self,
request: PluginSessionViewSubscriptionRequest,
) -> Result<PluginSessionViewSubscription, PluginTuiHostError> {
let buffer = request
.buffer
.clamp(1, MAX_PLUGIN_SESSION_VIEW_BUFFER)
.max(DEFAULT_PLUGIN_SESSION_VIEW_BUFFER.min(MAX_PLUGIN_SESSION_VIEW_BUFFER));
let (sender, receiver) = mpsc::channel(buffer);
let client = self.client.clone();
let redraw = self.redraw.clone();
drop(self.handle.spawn(async move {
Box::pin(stream_plugin_session_view(client, request, sender, redraw)).await;
}));
Ok(PluginSessionViewSubscription { receiver })
}
}
pub fn root_host(
redraw: InvalidationSignal,
client: BcodeClient,
launch: FreshSessionSettings,
active_tasks: Arc<AtomicUsize>,
) -> impl PluginTuiHost {
BcodePluginTuiHost {
handle: tokio::runtime::Handle::current(),
redraw,
active_tasks,
launch,
client,
}
}
fn plugin_workflow_authoring_draft(
draft: bcode_workflow::WorkflowDraftSnapshot,
) -> PluginWorkflowAuthoringDraft {
PluginWorkflowAuthoringDraft {
workflow_id: draft.identity.workflow_id,
draft_id: draft.identity.draft_id,
base_revision: draft.base_revision,
generation: draft.generation,
document: draft.document,
producer: draft.producer,
}
}
fn plugin_workflow_authoring_revision(
revision: bcode_workflow::WorkflowRevisionSnapshot,
) -> PluginWorkflowAuthoringRevision {
PluginWorkflowAuthoringRevision {
workflow_id: revision.identity.workflow_id,
revision: revision.identity.revision,
document: revision.document,
}
}
fn plugin_workflow_authoring_edit_result(
result: bcode_workflow::WorkflowDraftEditResult,
) -> PluginWorkflowAuthoringEditResult {
match result {
bcode_workflow::WorkflowDraftEditResult::Updated(draft) => {
PluginWorkflowAuthoringEditResult::Updated(Box::new(plugin_workflow_authoring_draft(
*draft,
)))
}
bcode_workflow::WorkflowDraftEditResult::Conflict(conflict) => {
PluginWorkflowAuthoringEditResult::Conflict {
expected_generation: conflict.expected_generation,
current_generation: conflict.current_generation,
}
}
bcode_workflow::WorkflowDraftEditResult::Rejected { diagnostics } => {
PluginWorkflowAuthoringEditResult::Rejected { diagnostics }
}
}
}
fn plugin_workflow_authoring_publish_result(
result: bcode_workflow::WorkflowPublicationResult,
) -> PluginWorkflowAuthoringPublishResult {
match result {
bcode_workflow::WorkflowPublicationResult::Published {
revision,
active_revision,
} => PluginWorkflowAuthoringPublishResult::Published {
revision: revision.identity.revision,
activated: active_revision == Some(revision.identity.revision),
},
bcode_workflow::WorkflowPublicationResult::Conflict(conflict) => {
PluginWorkflowAuthoringPublishResult::Conflict {
expected_generation: conflict.expected_generation,
current_generation: conflict.current_generation,
}
}
}
}
fn workflow_lookup(lookup: PluginWorkflowLookup) -> bcode_workflow::WorkflowRunBindingLookup {
bcode_workflow::WorkflowRunBindingLookup {
owner_plugin_id: lookup.owner_plugin_id,
workflow_kind: lookup.workflow_kind,
scope_key: lookup.scope_key,
}
}
const fn workflow_status(status: bcode_workflow_store::RunStatus) -> PluginWorkflowStatus {
match status {
bcode_workflow_store::RunStatus::Running => PluginWorkflowStatus::Running,
bcode_workflow_store::RunStatus::Paused => PluginWorkflowStatus::Paused,
bcode_workflow_store::RunStatus::Completed => PluginWorkflowStatus::Completed,
bcode_workflow_store::RunStatus::Failed => PluginWorkflowStatus::Failed,
bcode_workflow_store::RunStatus::Cancelled => PluginWorkflowStatus::Cancelled,
bcode_workflow_store::RunStatus::RepairRequired => PluginWorkflowStatus::RepairRequired,
}
}
fn workflow_summary(run: bcode_workflow_store::WorkflowRunSummary) -> PluginWorkflowSummary {
PluginWorkflowSummary {
run_id: run.run_id,
definition_id: run.definition_id,
definition_version: run.definition_version,
status: workflow_status(run.status),
cancellation_requested: run.cancellation_requested_at_ms.is_some(),
created_at_ms: run.created_at_ms,
updated_at_ms: run.updated_at_ms,
}
}
fn workflow_inspection(
inspection: bcode_ipc::WorkflowRunInspection,
) -> Result<PluginWorkflowInspection, bcode_client::ClientError> {
Ok(PluginWorkflowInspection {
run: workflow_summary(inspection.run),
definition: inspection.definition.definition.into_definition(),
waits: inspection
.waits
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
attempts: inspection
.attempts
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
events: inspection
.events
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
grants: inspection
.grants
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
resource_leases: inspection
.resource_leases
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
outputs: inspection
.outputs
.into_iter()
.map(|value| {
serde_json::to_value(value)
.map_err(|_| bcode_client::ClientError::UnexpectedResponse)
})
.collect::<Result<_, _>>()?,
child_session_ids: inspection
.child_sessions
.into_iter()
.map(|session| session.id)
.collect(),
})
}
async fn stream_plugin_session_view(
client: BcodeClient,
request: PluginSessionViewSubscriptionRequest,
sender: mpsc::Sender<PluginSessionViewUpdate>,
redraw: InvalidationSignal,
) {
if let Err(error) = Box::pin(stream_plugin_session_view_inner(
client,
request,
sender.clone(),
redraw.clone(),
))
.await
{
let _ = sender
.send(PluginSessionViewUpdate::Disconnected {
message: error.to_string(),
})
.await;
redraw.request();
}
}
async fn attach_plugin_session_view(
client: &BcodeClient,
connection: &mut bcode_client::ClientConnection,
request: &PluginSessionViewSubscriptionRequest,
) -> Result<SessionView, bcode_client::ClientError> {
let attached = connection
.attach_session_projection_window_with_input_history(
request.session_id,
request.projection.clone(),
)
.await?;
let runtime = attached.runtime_selection;
let mut view = SessionView::new();
view.set_session_summary(attached.session);
view.set_runtime_selection(
runtime.provider_plugin_id,
runtime.requested_model_id.or(runtime.model_id),
runtime.effective_model_id,
runtime.reasoning_effort,
runtime.reasoning_summary,
None,
);
view.set_agent_id(runtime.agent_id);
view.set_reasoning_presentation_policy(request.reasoning_policy);
if let Some(window) = attached.projection_window.as_ref() {
view.set_history_window_metadata(
window.source_range.map(|range| range.start_sequence),
window.source_range.map(|range| range.end_sequence),
window.has_older,
window.has_newer,
);
}
view.apply_history(&attached.history);
for record in &attached.submissions {
view.apply_submission_record(record);
}
let permissions = client_permission_views(
client.list_permissions().await.unwrap_or_default(),
request.session_id,
);
view.set_pending_permissions(permissions);
if let Ok(runtime_work) = client.list_runtime_work(request.session_id).await {
view.set_runtime_work_snapshots(&runtime_work);
}
if let Ok(interactions) =
super::effects::load_pending_interactions(client, request.session_id).await
{
view.set_pending_interactions(interactions);
}
view.set_connection_status(SessionConnectionViewStatus::Attached);
Ok(view)
}
fn client_permission_views(
permissions: Vec<bcode_ipc::PermissionSummary>,
session_id: SessionId,
) -> Vec<PermissionView> {
permissions
.into_iter()
.filter(|permission| permission.is_addressed_to(session_id))
.map(|permission| PermissionView {
permission_id: permission.permission_id,
session_id: Some(permission.session_id),
tool_call_id: permission.tool_call_id,
tool_name: permission.tool_name,
arguments_json: permission.arguments_json,
batch: permission
.batch
.map(|batch| bcode_session_view_models::PermissionBatchView {
batch_id: batch.batch_id,
call_index: batch.call_index,
call_count: batch.call_count,
}),
agent_id: permission.agent_id,
title: Some("Permission requested".to_string()),
policy_source: permission.policy_source,
detail: permission.policy_reason,
resolved: false,
approved: None,
can_remember: permission.can_remember_policy,
})
.collect()
}
async fn send_plugin_session_snapshot(
sender: &mpsc::Sender<PluginSessionViewUpdate>,
redraw: &InvalidationSignal,
view: &SessionView,
) -> bool {
if sender
.send(PluginSessionViewUpdate::Snapshot(Box::new(
view.snapshot().clone(),
)))
.await
.is_err()
{
return false;
}
redraw.request();
true
}
async fn stream_plugin_session_view_inner(
client: BcodeClient,
request: PluginSessionViewSubscriptionRequest,
sender: mpsc::Sender<PluginSessionViewUpdate>,
redraw: InvalidationSignal,
) -> Result<(), bcode_client::ClientError> {
let session_id = request.session_id;
let mut connection = client.connect("bcode-plugin-tui-session-view").await?;
let mut view = attach_plugin_session_view(&client, &mut connection, &request).await?;
if !send_plugin_session_snapshot(&sender, &redraw, &view).await {
return Ok(());
}
let mut reconnect_delay = std::time::Duration::from_millis(100);
loop {
let needs_resync = match match tokio::time::timeout(
std::time::Duration::from_millis(250),
connection.recv_event(),
)
.await
{
Ok(event) => event,
Err(_) if sender.is_closed() => return Ok(()),
Err(_) => continue,
} {
Ok(BcodeEvent::SessionViewResyncRequired {
session_id: required,
}) if required == session_id => true,
Ok(event) => {
let changed = match event {
BcodeEvent::Session(event) | BcodeEvent::RuntimeWork(event)
if event.session_id == session_id =>
{
view.apply_event(&event);
true
}
BcodeEvent::SessionLive(event) if event.session_id == session_id => {
view.apply_live_event(&event);
true
}
BcodeEvent::Session(_)
| BcodeEvent::SessionLive(_)
| BcodeEvent::RuntimeWork(_)
| BcodeEvent::Workflow(_)
| BcodeEvent::SessionViewResyncRequired { .. }
| BcodeEvent::SessionCatalogUpdated { .. } => false,
};
if changed && !send_plugin_session_snapshot(&sender, &redraw, &view).await {
return Ok(());
}
false
}
Err(_error) => true,
};
if !needs_resync {
continue;
}
view.set_connection_status(SessionConnectionViewStatus::Reconnecting);
if !send_plugin_session_snapshot(&sender, &redraw, &view).await {
return Ok(());
}
drop(connection);
loop {
if sender.is_closed() {
return Ok(());
}
if let Ok(mut next_connection) = client.connect("bcode-plugin-tui-session-view").await
&& let Ok(next_view) =
attach_plugin_session_view(&client, &mut next_connection, &request).await
{
view = next_view;
if !send_plugin_session_snapshot(&sender, &redraw, &view).await {
return Ok(());
}
connection = next_connection;
reconnect_delay = std::time::Duration::from_millis(100);
break;
}
tokio::time::sleep(reconnect_delay).await;
reconnect_delay = (reconnect_delay * 2).min(std::time::Duration::from_secs(2));
}
}
}
fn workflow_event_refreshes_selected_detail(
selected_run_id: Option<&str>,
changed_run_id: &str,
) -> bool {
selected_run_id == Some(changed_run_id)
}
fn workflow_catalog_selection(
selected_run_id: Option<&str>,
catalog: &bcode_workflow_view_models::WorkflowCatalogView,
) -> Option<String> {
selected_run_id
.filter(|run_id| catalog.runs.iter().any(|run| run.run_id == *run_id))
.map(str::to_string)
.or_else(|| catalog.runs.first().map(|run| run.run_id.clone()))
}
#[allow(clippy::too_many_lines)]
async fn stream_plugin_workflow_views(
client: BcodeClient,
sender: mpsc::Sender<PluginTuiSurfaceUpdate>,
mut requests: mpsc::Receiver<super::chat_loop::WorkflowViewRequest>,
redraw: InvalidationSignal,
) {
const CATALOG_PAGE_SIZE: usize = 100;
const RUN_DETAIL_LIMIT: usize = 1_000;
let mut watcher = match client.watch_workflow_runs().await {
Ok(watcher) => watcher,
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
})
.await;
redraw.request();
return;
}
};
let mut catalog_request = bcode_workflow_view_models::WorkflowCatalogRequest {
limit: CATALOG_PAGE_SIZE,
cursor: None,
filter: bcode_workflow_view_models::WorkflowCatalogFilter::All,
sort: bcode_workflow_view_models::WorkflowCatalogSort::UpdatedAt,
group: bcode_workflow_view_models::WorkflowCatalogGroup::None,
search: None,
};
let catalog = match client.workflow_catalog_view(catalog_request.clone()).await {
Ok(catalog) => catalog,
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
})
.await;
redraw.request();
return;
}
};
let mut selected_run_id = catalog.runs.first().map(|run| run.run_id.clone());
if sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalog(catalog))
.await
.is_err()
{
return;
}
if let Some(run_id) = selected_run_id.clone() {
match client.workflow_run_view(run_id, RUN_DETAIL_LIMIT).await {
Ok(view) => {
if sender
.send(PluginTuiSurfaceUpdate::WorkflowRun(Box::new(view)))
.await
.is_err()
{
return;
}
}
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
})
.await;
redraw.request();
return;
}
}
}
redraw.request();
loop {
tokio::select! {
requested = requests.recv() => {
let Some(requested) = requested else {
return;
};
match requested {
super::chat_loop::WorkflowViewRequest::SelectRun(run_id) => {
if selected_run_id.as_deref() == Some(run_id.as_str()) {
continue;
}
selected_run_id = Some(run_id.clone());
if sender
.send(PluginTuiSurfaceUpdate::WorkflowRunLoading {
run_id: run_id.clone(),
})
.await
.is_err()
{
return;
}
match client.workflow_run_view(run_id.clone(), RUN_DETAIL_LIMIT).await {
Ok(view) => {
if sender.send(PluginTuiSurfaceUpdate::WorkflowRun(Box::new(view))).await.is_err() {
return;
}
redraw.request();
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowRunError {
run_id,
message: error.to_string(),
}).await;
redraw.request();
}
}
}
super::chat_loop::WorkflowViewRequest::UpdateQuery {
filter,
sort,
group,
search,
} => {
let stale = true;
if sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalogLoading { stale })
.await
.is_err()
{
return;
}
catalog_request = bcode_workflow_view_models::WorkflowCatalogRequest {
limit: CATALOG_PAGE_SIZE,
cursor: None,
filter,
sort,
group,
search,
};
match client.workflow_catalog_view(catalog_request.clone()).await {
Ok(catalog) => {
selected_run_id = catalog.runs.first().map(|run| run.run_id.clone());
if sender.send(PluginTuiSurfaceUpdate::WorkflowCatalog(catalog)).await.is_err() {
return;
}
if let Some(run_id) = selected_run_id.clone() {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowRunLoading {
run_id: run_id.clone(),
}).await;
match client.workflow_run_view(run_id.clone(), RUN_DETAIL_LIMIT).await {
Ok(view) => {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowRun(Box::new(view))).await;
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowRunError {
run_id,
message: error.to_string(),
}).await;
}
}
}
redraw.request();
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowCatalogError {
message: error.to_string(),
}).await;
redraw.request();
}
}
}
super::chat_loop::WorkflowViewRequest::LoadMore(cursor) => {
if sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalogLoading { stale: true })
.await
.is_err()
{
return;
}
let mut page_request = catalog_request.clone();
page_request.cursor = Some(cursor);
match client.workflow_catalog_view(page_request).await {
Ok(page) => {
if sender.send(PluginTuiSurfaceUpdate::WorkflowCatalogPage(page)).await.is_err() {
return;
}
redraw.request();
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::WorkflowCatalogError {
message: error.to_string(),
}).await;
redraw.request();
}
}
}
}
}
event = watcher.next_event() => match event {
Ok(bcode_client::WorkflowRunWatchEvent::Changed(event)) => {
let catalog = match client.workflow_catalog_view(catalog_request.clone()).await {
Ok(catalog) => catalog,
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
}).await;
redraw.request();
return;
}
};
if sender.send(PluginTuiSurfaceUpdate::WorkflowCatalog(catalog)).await.is_err() {
return;
}
if workflow_event_refreshes_selected_detail(
selected_run_id.as_deref(),
&event.run_id,
) {
match client.workflow_run_view(event.run_id, RUN_DETAIL_LIMIT).await {
Ok(view) => {
if sender.send(PluginTuiSurfaceUpdate::WorkflowRun(Box::new(view))).await.is_err() {
return;
}
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
}).await;
redraw.request();
return;
}
}
}
redraw.request();
}
Ok(bcode_client::WorkflowRunWatchEvent::ResyncRequired) => {
if sender
.send(PluginTuiSurfaceUpdate::Resyncing)
.await
.is_err()
{
return;
}
if sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalogLoading { stale: true })
.await
.is_err()
{
return;
}
let replacement_watcher = match client.watch_workflow_runs().await {
Ok(watcher) => watcher,
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
})
.await;
redraw.request();
return;
}
};
let catalog = match client.workflow_catalog_view(catalog_request.clone()).await {
Ok(catalog) => catalog,
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalogError {
message: error.to_string(),
})
.await;
redraw.request();
continue;
}
};
selected_run_id = workflow_catalog_selection(
selected_run_id.as_deref(),
&catalog,
);
if sender
.send(PluginTuiSurfaceUpdate::WorkflowCatalog(catalog))
.await
.is_err()
{
return;
}
if let Some(run_id) = selected_run_id.clone() {
if sender
.send(PluginTuiSurfaceUpdate::WorkflowRunLoading {
run_id: run_id.clone(),
})
.await
.is_err()
{
return;
}
match client.workflow_run_view(run_id.clone(), RUN_DETAIL_LIMIT).await {
Ok(view) => {
if sender
.send(PluginTuiSurfaceUpdate::WorkflowRun(Box::new(view)))
.await
.is_err()
{
return;
}
}
Err(error) => {
let _ = sender
.send(PluginTuiSurfaceUpdate::WorkflowRunError {
run_id,
message: error.to_string(),
})
.await;
}
}
}
watcher = replacement_watcher;
redraw.request();
}
Ok(bcode_client::WorkflowRunWatchEvent::UnsupportedVersion { version }) => {
let _ = sender.send(PluginTuiSurfaceUpdate::Disconnected {
message: format!("unsupported workflow live-event version {version}"),
}).await;
redraw.request();
return;
}
Err(error) => {
let _ = sender.send(PluginTuiSurfaceUpdate::Disconnected {
message: error.to_string(),
}).await;
redraw.request();
return;
}
}
}
}
}
pub fn subscribe_workflow_views(
client: BcodeClient,
redraw: InvalidationSignal,
) -> (
PluginTuiSurfaceUpdateReceiver,
mpsc::Sender<super::chat_loop::WorkflowViewRequest>,
) {
let (sender, receiver) = mpsc::channel(64);
let (request_sender, requests) = mpsc::channel(8);
tokio::spawn(stream_plugin_workflow_views(
client, sender, requests, redraw,
));
(receiver, request_sender)
}
#[cfg(test)]
mod tests {
use super::{
PluginWorkflowPackageExportStartRequest, PluginWorkflowStartRequest, SessionId,
workflow_catalog_selection, workflow_event_refreshes_selected_detail,
workflow_package_export_start_request, workflow_run_limits, workflow_start_request,
};
#[test]
fn bmux_invalidation_signal_coalesces_plugin_redraw_requests() {
let redraw = bmux_tui_runtime::InvalidationSignal::new();
redraw.request();
redraw.request();
assert!(redraw.take());
assert!(!redraw.take());
assert_eq!(redraw.requests(), 2);
assert_eq!(redraw.coalesced(), 1);
}
#[test]
fn plugin_surface_host_preserves_requested_workflow_limits() {
let session_id = SessionId::new();
let workflow = bcode_workflow::WorkflowBuilder::new(
"budget-test",
bcode_workflow::Step::task("body", |value: bool, _context| async move { Ok(value) }),
)
.build()
.expect("workflow");
let spec = bcode_workflow::WorkflowSpec::new("budget-test", &workflow).expect("spec");
let mut request = PluginWorkflowStartRequest::typed(
&spec,
&false,
session_id,
bcode_plugin_sdk::tui::PluginWorkflowBinding {
owner_plugin_id: "test".to_string(),
workflow_kind: "budget-test".to_string(),
scope_key: session_id.to_string(),
display_label: None,
single_active: true,
},
None,
)
.expect("request");
request.limits.cycle_cap = u32::MAX;
request.limits.node_execution_cap = u64::from(u32::MAX) * 8;
let expected = workflow_run_limits(&request.limits);
let adapted = workflow_start_request(request).expect("adapt");
assert_eq!(adapted.limits, expected);
let wire = bcode_ipc::Request::StartWorkflow(adapted);
let encoded = bcode_ipc::encode_request(&wire).expect("encode");
assert_eq!(bcode_ipc::decode_request(&encoded).expect("decode"), wire);
}
#[test]
fn plugin_surface_host_adapts_portable_package_export_start() {
let parent_session_id = SessionId::new();
let request =
workflow_package_export_start_request(PluginWorkflowPackageExportStartRequest {
package_export: bcode_workflow::WorkflowPackageExportIdentity {
package_id: "example/package".to_string(),
export: "main".to_string(),
package_lock_digest_sha256: Some("a".repeat(64)),
},
run_id: Some("run-1".to_string()),
parent_session_id,
workspace_snapshot: Some("workspace".to_string()),
parent_session_generation: Some(1),
configuration: None,
input: Some(serde_json::json!({"subject": "change"})),
});
assert_eq!(request.package_export.package_id, "example/package");
assert_eq!(request.parent_session_id, parent_session_id);
assert_eq!(request.input.expect("input")["subject"], "change");
}
#[test]
fn bounded_resync_preserves_exact_selection_or_uses_documented_first_fallback() {
let catalog = bcode_workflow_view_models::WorkflowCatalogView {
basic_discovery: None,
version: bcode_workflow_view_models::WORKFLOW_VIEW_VERSION,
runs: ["run-a", "run-b"]
.into_iter()
.map(|run_id| bcode_workflow_view_models::WorkflowRunListItem {
run_id: run_id.to_string(),
display_title: run_id.to_string(),
binding_label: None,
definition_id: "definition".to_string(),
definition_version: 1,
authored_source: None,
definition_disposition:
bcode_workflow_view_models::WorkflowDefinitionDisposition::CompiledOnly,
parent_run_id: None,
descendant_count: 0,
progress: bcode_workflow_view_models::WorkflowRunProgress::default(),
attention: bcode_workflow_view_models::WorkflowAttentionSummary::default(),
status: bcode_workflow_view_models::WorkflowRunStatus::Running,
created_at_ms: 1,
updated_at_ms: 1,
})
.collect(),
next_cursor: None,
has_more: false,
filter: bcode_workflow_view_models::WorkflowCatalogFilter::All,
sort: bcode_workflow_view_models::WorkflowCatalogSort::UpdatedAt,
group: bcode_workflow_view_models::WorkflowCatalogGroup::None,
search: None,
};
assert_eq!(
workflow_catalog_selection(Some("run-b"), &catalog).as_deref(),
Some("run-b")
);
assert_eq!(
workflow_catalog_selection(Some("removed"), &catalog).as_deref(),
Some("run-a")
);
assert_eq!(
workflow_catalog_selection(None, &catalog).as_deref(),
Some("run-a")
);
}
#[test]
fn repeated_workflow_events_refresh_only_exact_selected_detail() {
for _ in 0..10_000 {
assert!(workflow_event_refreshes_selected_detail(
Some("selected-run"),
"selected-run"
));
assert!(!workflow_event_refreshes_selected_detail(
Some("selected-run"),
"historical-run"
));
assert!(!workflow_event_refreshes_selected_detail(
None,
"historical-run"
));
}
}
}