supercode-frontend-model 0.5.45

Donor-neutral client state and semantic intents for Volter Harness frontends.
use std::collections::BTreeSet;

use supercode_harness::FrontendOperationDescriptor;
use supercode_harness::FrontendOperationInvocation;
use supercode_harness::FrontendOperationKind;
use supercode_harness::FrontendRuntimeDescriptor;

/// Composer controls copied from the SDK runtime descriptor.
///
/// `active_modules` describes the agent semantics behind the runtime. It is
/// intentionally not interpreted as a frontend operation registry: a search
/// tool, session tree, or reduction policy does not by itself provide the
/// terminal with a file picker, session switcher, or reduction RPC.
#[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| {
            if operation
                .command
                .as_ref()
                .map(|command| command.name.as_str())
                != Some(name)
            {
                return None;
            }
            match operation.kind {
                FrontendOperationKind::Prompt => Some(FrontendOperationInvocation::Prompt {
                    operation_id: operation.id.clone(),
                    arguments: arguments.to_string(),
                }),
                // BP-13 (catalog D9 "Mid-session model switching"): the
                // model control is reachable by TYPING `/model <name>` on
                // whatever composer this runtime is attached to — the
                // surface both parity presets actually run on, not only the
                // line-mode REPL.
                FrontendOperationKind::Model => Some(FrontendOperationInvocation::Model {
                    operation_id: operation.id.clone(),
                    model: arguments.to_string(),
                }),
                FrontendOperationKind::Context => Some(FrontendOperationInvocation::Context {
                    operation_id: operation.id.clone(),
                }),
                _ => None,
            }
        })
    }

    /// Stable identifiers for operations with real composer/runtime routes.
    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
    }
}