use std::collections::BTreeSet;
use supercode::FrontendOperationDescriptor;
use supercode::FrontendOperationInvocation;
use supercode::FrontendOperationKind;
use supercode::FrontendRuntimeDescriptor;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComposerCapabilities {
operations: Vec<FrontendOperationDescriptor>,
pub can_submit: bool,
pub can_steer: bool,
pub can_interrupt: bool,
pub can_respond: bool,
pub can_detach: bool,
}
impl ComposerCapabilities {
pub fn from_descriptor(descriptor: &FrontendRuntimeDescriptor) -> Self {
Self {
operations: descriptor
.operations
.iter()
.filter(|operation| {
operation.kind == FrontendOperationKind::Prompt && operation.command.is_some()
})
.cloned()
.collect(),
can_submit: descriptor.actions.submit,
can_steer: descriptor.actions.steer,
can_interrupt: descriptor.actions.interrupt,
can_respond: descriptor.actions.respond,
can_detach: descriptor.actions.detach,
}
}
pub fn operations(&self) -> &[FrontendOperationDescriptor] {
&self.operations
}
pub fn invocation_for_command(&self, input: &str) -> Option<FrontendOperationInvocation> {
let trimmed = input.trim_start();
let rest = trimmed.strip_prefix('/')?;
let (name, arguments) = rest
.split_once(char::is_whitespace)
.map_or((rest, ""), |(name, arguments)| (name, arguments.trim()));
self.operations.iter().find_map(|operation| {
(operation.kind == FrontendOperationKind::Prompt
&& operation
.command
.as_ref()
.map(|command| command.name.as_str())
== Some(name))
.then(|| FrontendOperationInvocation::Prompt {
operation_id: operation.id.clone(),
arguments: arguments.to_string(),
})
})
}
pub fn available_actions(&self) -> BTreeSet<String> {
let mut actions = self
.operations
.iter()
.map(|operation| format!("operation:{}", operation.id))
.collect::<BTreeSet<_>>();
if self.can_submit {
actions.insert("submit".into());
}
if self.can_steer {
actions.insert("steer".into());
}
if self.can_interrupt {
actions.insert("interrupt".into());
}
if self.can_respond {
actions.insert("respond".into());
}
if self.can_detach {
actions.insert("detach".into());
}
actions
}
}
#[cfg(test)]
mod tests {
use supercode::FrontendActions;
use supercode::FrontendCommandDescriptor;
use supercode::FrontendConnectionState;
use supercode::FrontendDisplayCapabilities;
use supercode::FrontendRuntimeDescriptor;
use supercode::FrontendTurnState;
use supercode::FRONTEND_RUNTIME_SCHEMA_VERSION;
use super::*;
fn descriptor(actions: FrontendActions) -> FrontendRuntimeDescriptor {
FrontendRuntimeDescriptor {
schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
session_id: "test-session".into(),
source_harness: None,
emulation_profile: Some("custom".into()),
active_modules: vec![
"tools_search".into(),
"model_catalog".into(),
"session_tree".into(),
"subagents".into(),
"tui".into(),
"reduction".into(),
"permissions".into(),
"mcp".into(),
],
commands: vec![FrontendCommandDescriptor {
name: "review".into(),
description: None,
argument_hint: None,
}],
operations: vec![FrontendOperationDescriptor {
id: "prompt:review".into(),
kind: FrontendOperationKind::Prompt,
command: Some(FrontendCommandDescriptor {
name: "review".into(),
description: None,
argument_hint: Some("[arguments]".into()),
}),
}],
actions,
display: FrontendDisplayCapabilities {
event_kinds: vec![],
opaque_fallback: true,
},
model: "test-model".into(),
turn_state: FrontendTurnState::Idle,
connection_state: FrontendConnectionState::Connected,
extensions: Default::default(),
}
}
#[test]
fn active_modules_do_not_create_frontend_actions() {
let mut descriptor = descriptor(FrontendActions {
submit: false,
interrupt: false,
steer: false,
respond: false,
detach: false,
close: true,
});
descriptor.operations.clear();
let capabilities = ComposerCapabilities::from_descriptor(&descriptor);
assert!(capabilities.available_actions().is_empty());
}
#[test]
fn available_actions_are_descriptor_backed() {
let capabilities = ComposerCapabilities::from_descriptor(&descriptor(FrontendActions {
submit: true,
interrupt: true,
steer: true,
respond: true,
detach: true,
close: true,
}));
assert_eq!(
capabilities.available_actions(),
BTreeSet::from([
"operation:prompt:review".to_string(),
"detach".to_string(),
"interrupt".to_string(),
"respond".to_string(),
"steer".to_string(),
"submit".to_string(),
])
);
}
#[test]
fn schema_v1_descriptor_never_falls_back_to_legacy_commands_or_modules() {
let value = serde_json::json!({
"schema_version": 1,
"session_id": "old-runtime",
"source_harness": null,
"emulation_profile": "cc-parity",
"active_modules": ["model_catalog", "session_tree", "subagents", "reduction"],
"commands": [{"name":"legacy-only", "description":null}],
"actions": {
"submit": true, "interrupt": true, "steer": true,
"respond": false, "detach": true, "close": false
},
"display": {"event_kinds":[], "opaque_fallback":true},
"model": "test",
"turn_state": "idle",
"connection_state": "connected"
});
let descriptor: FrontendRuntimeDescriptor = serde_json::from_value(value).unwrap();
assert!(descriptor.operations.is_empty());
assert!(descriptor.commands[0].argument_hint.is_none());
let capabilities = ComposerCapabilities::from_descriptor(&descriptor);
assert_eq!(
capabilities.available_actions(),
BTreeSet::from([
"detach".to_string(),
"interrupt".to_string(),
"steer".to_string(),
"submit".to_string(),
])
);
assert!(capabilities.operations().is_empty());
}
}