mobius 0.15.0

A small, modular Rust framework for building coding agents
Documentation
use super::{Basis, Entry, MANIFEST, Scope, Snapshot, text};
use crate::Result;
use crate::middleware::{FrontendEventSink, MiddlewareCommandOutput};
use crate::protocol::{
    FrontendAction, FrontendActionListItem, FrontendEditor, FrontendEvent, FrontendListItemState,
    FrontendSlot, FrontendSymbol, FrontendTone, FrontendWidget, FrontendWidgetContent, Op,
};

pub(super) fn surface_widgets(snapshot: &Snapshot) -> Vec<FrontendWidget> {
    vec![global_widget(&snapshot.global)]
}

pub(super) fn global_widget(entries: &[Entry]) -> FrontendWidget {
    frontend_widget(
        "navigation",
        FrontendSlot::Navigation,
        text::WIDGET_TEXT,
        action_list_content(text::WIDGET_GLOBAL_TITLE, Scope::Global, entries),
    )
}

pub(super) fn swarm_widget(entries: &[Entry]) -> FrontendWidget {
    frontend_widget(
        "swarm",
        FrontendSlot::Navigation,
        text::WIDGET_TEXT,
        action_list_content(text::WIDGET_SWARM_TITLE, Scope::Swarm, entries),
    )
}

fn frontend_widget(
    id: &str,
    slot: FrontendSlot,
    text: &str,
    content: FrontendWidgetContent,
) -> FrontendWidget {
    FrontendWidget {
        id: id.into(),
        slot,
        text: text.into(),
        tone: FrontendTone::Neutral,
        symbol: Some(FrontendSymbol::Brain),
        icon_only: false,
        progress: None,
        content: Some(content),
        action: Some(Op::CapabilityCommand {
            capability: MANIFEST.id.into(),
            command: "scratchpad".into(),
            arguments: "refresh".into(),
            input: None,
            target: None,
        }),
    }
}

fn action_list_content(title: &str, scope: Scope, entries: &[Entry]) -> FrontendWidgetContent {
    FrontendWidgetContent::ActionList {
        title: title.into(),
        actions: vec![FrontendAction {
            id: "add".into(),
            label: match scope {
                Scope::Swarm => text::ACTION_ADD_SWARM,
                Scope::Global => text::ACTION_ADD_GLOBAL,
            }
            .into(),
            symbol: FrontendSymbol::Custom("plus".into()),
            tone: FrontendTone::Neutral,
            op: Op::CapabilityCommand {
                capability: MANIFEST.id.into(),
                command: "scratchpad".into(),
                arguments: "add".into(),
                input: Some(String::new()),
                target: None,
            },
            editor: Some(FrontendEditor {
                title: match scope {
                    Scope::Swarm => text::EDITOR_SWARM_TITLE,
                    Scope::Global => text::EDITOR_GLOBAL_TITLE,
                }
                .into(),
                label: text::EDITOR_LABEL.into(),
                description: match scope {
                    Scope::Swarm => text::EDITOR_SWARM_DESCRIPTION,
                    Scope::Global => text::EDITOR_GLOBAL_DESCRIPTION,
                }
                .into(),
                submit_label: text::EDITOR_SUBMIT.into(),
            }),
        }],
        items: entries
            .iter()
            .rev()
            .map(|entry| action_list_item(scope, entry))
            .collect(),
    }
}

pub(super) fn action_list_item(scope: Scope, entry: &Entry) -> FrontendActionListItem {
    let scope_name = scope_name(scope);
    let actions = vec![
        list_action(
            entry,
            "edit",
            FrontendSymbol::Edit,
            text::ACTION_EDIT,
            FrontendTone::Neutral,
            format!("edit {scope_name} {}", entry.id),
            Some(&entry.note),
        ),
        list_action(
            entry,
            "delete",
            FrontendSymbol::Delete,
            text::ACTION_DELETE,
            FrontendTone::Error,
            format!("forget {scope_name} {}", entry.id),
            None,
        ),
    ];
    FrontendActionListItem {
        id: entry.id.clone(),
        text: entry.note.clone(),
        state: FrontendListItemState::Plain,
        actions,
    }
}

fn list_action(
    entry: &Entry,
    id: &str,
    symbol: FrontendSymbol,
    label: &str,
    tone: FrontendTone,
    arguments: String,
    input: Option<&str>,
) -> FrontendAction {
    FrontendAction {
        editor: None,
        id: format!("{id}:{}", entry.id),
        label: label.into(),
        symbol,
        tone,
        op: Op::CapabilityCommand {
            capability: MANIFEST.id.into(),
            command: "scratchpad".into(),
            arguments,
            input: input.map(str::to_owned),
            target: None,
        },
    }
}

pub(super) fn widget_events(snapshot: &Snapshot) -> Vec<FrontendEvent> {
    surface_widgets(snapshot)
        .into_iter()
        .map(|item| FrontendEvent::Widget {
            capability: MANIFEST.id.into(),
            item,
        })
        .collect()
}

pub(super) fn publish_widgets(frontend: &FrontendEventSink, snapshot: &Snapshot) -> Result<()> {
    for event in widget_events(snapshot) {
        frontend(event)?;
    }
    Ok(())
}

pub(super) fn parse_scope(scope: &str) -> Option<Scope> {
    match scope {
        "swarm" => Some(Scope::Swarm),
        "global" => Some(Scope::Global),
        _ => None,
    }
}

const fn scope_name(scope: Scope) -> &'static str {
    match scope {
        Scope::Swarm => "swarm",
        Scope::Global => "global",
    }
}

pub(super) fn usage() -> MiddlewareCommandOutput {
    MiddlewareCommandOutput::render(MANIFEST.id, text::COMMAND_USAGE, FrontendTone::Warning)
}

pub(super) fn format_snapshot(snapshot: &Snapshot) -> String {
    let mut sections = Vec::new();
    if let Some(swarm) = &snapshot.swarm {
        sections.push(format!(
            "{}\n{}",
            text::MESSAGE_SWARM_HEADING,
            format_entries(swarm)
        ));
    }
    sections.push(format!(
        "{}\n{}",
        text::MESSAGE_GLOBAL_HEADING,
        format_entries(&snapshot.global)
    ));
    sections.join("\n\n")
}

fn format_entries(entries: &[Entry]) -> String {
    if entries.is_empty() {
        return text::MESSAGE_NO_NOTES.into();
    }
    entries
        .iter()
        .map(|entry| format!("[{}] {}\n  {}", entry.id, entry.note, entry_metadata(entry)))
        .collect::<Vec<_>>()
        .join("\n")
}

fn entry_metadata(entry: &Entry) -> String {
    format!(
        "{} ยท created at Unix time {}",
        basis_label(&entry.basis),
        entry.created_at
    )
}

fn basis_label(basis: &Basis) -> &'static str {
    match basis {
        Basis::AgentObservation => text::MESSAGE_AGENT_OBSERVATION,
        Basis::UserConfirmed => text::MESSAGE_USER_CONFIRMED,
    }
}