daat-locus 0.4.0

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use std::path::PathBuf;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::DashboardState;

#[derive(Clone, Debug)]
pub enum DashboardControlCommand {
    RunSleep,
    ClearConversation,
    InterruptRuntime,
    RestartDaemon,
    ReloadSkills,
    RunWorkflow {
        workflow_id: String,
        input: serde_json::Value,
    },
    SetSkillAutoUse {
        path: PathBuf,
        enabled: bool,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DashboardPendingUserInputMoveDirection {
    Up,
    Down,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DashboardAction {
    RunSleep,
    ClearConversation,
    InterruptRuntime,
    RestartDaemon,
    ReloadSkills,
    RunWorkflow {
        workflow_id: String,
        input: serde_json::Value,
    },
    SetSkillAutoUse {
        path: PathBuf,
        enabled: bool,
    },
    DismissPendingUserInput {
        event_id: Uuid,
    },
    ClearPendingUserInputs,
    UpdatePendingUserInput {
        event_id: Uuid,
        incoming_text: String,
    },
    MovePendingUserInput {
        event_id: Uuid,
        direction: DashboardPendingUserInputMoveDirection,
    },
    MovePendingUserInputToPosition {
        event_id: Uuid,
        target_position: usize,
    },
    PreemptPendingUserInput {
        event_id: Uuid,
    },
}

pub const fn dashboard_action_is_manager_owned(action: &DashboardAction) -> bool {
    matches!(action, DashboardAction::RestartDaemon)
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DashboardActionResult {
    pub success: bool,
    pub message: String,
    #[serde(default)]
    pub detail: Option<String>,
}

impl DashboardActionResult {
    fn ok(message: impl Into<String>) -> Self {
        Self {
            success: true,
            message: message.into(),
            detail: None,
        }
    }

    fn error(message: impl Into<String>) -> Self {
        Self {
            success: false,
            message: message.into(),
            detail: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DashboardCommandAttachment {
    pub placeholder: String,
    pub name: String,
    pub path: PathBuf,
    pub media_type: String,
}

#[async_trait]
pub trait DashboardCommandRunner: Send + Sync {
    async fn run_command(
        &self,
        command: &str,
        attachments: Vec<DashboardCommandAttachment>,
        state: &DashboardState,
    ) -> String;
    async fn run_action(
        &self,
        action: DashboardAction,
        state: &DashboardState,
    ) -> DashboardActionResult;
}

pub fn execute_dashboard_action(
    action: DashboardAction,
    control_tx: &tokio::sync::mpsc::UnboundedSender<DashboardControlCommand>,
) -> DashboardActionResult {
    match action {
        DashboardAction::RunSleep => match control_tx.send(DashboardControlCommand::RunSleep) {
            Ok(()) => DashboardActionResult::ok("queued sleep run"),
            Err(err) => DashboardActionResult::error(format!("failed to queue sleep run: {err}")),
        },
        DashboardAction::ClearConversation => {
            match control_tx.send(DashboardControlCommand::ClearConversation) {
                Ok(()) => DashboardActionResult::ok("queued runtime clear"),
                Err(err) => DashboardActionResult::error(format!("failed to queue clear: {err}")),
            }
        }
        DashboardAction::InterruptRuntime => {
            match control_tx.send(DashboardControlCommand::InterruptRuntime) {
                Ok(()) => DashboardActionResult::ok("queued runtime interrupt"),
                Err(err) => {
                    DashboardActionResult::error(format!("failed to queue interrupt: {err}"))
                }
            }
        }
        DashboardAction::RestartDaemon => {
            match control_tx.send(DashboardControlCommand::RestartDaemon) {
                Ok(()) => DashboardActionResult::ok("queued daemon restart"),
                Err(err) => {
                    DashboardActionResult::error(format!("failed to queue daemon restart: {err}"))
                }
            }
        }
        DashboardAction::ReloadSkills => {
            match control_tx.send(DashboardControlCommand::ReloadSkills) {
                Ok(()) => DashboardActionResult::ok("queued skills reload"),
                Err(err) => {
                    DashboardActionResult::error(format!("failed to queue skills reload: {err}"))
                }
            }
        }
        DashboardAction::RunWorkflow { workflow_id, input } => {
            match control_tx.send(DashboardControlCommand::RunWorkflow { workflow_id, input }) {
                Ok(()) => DashboardActionResult::ok("queued workflow run"),
                Err(err) => {
                    DashboardActionResult::error(format!("failed to queue workflow run: {err}"))
                }
            }
        }
        DashboardAction::SetSkillAutoUse { path, enabled } => {
            match control_tx.send(DashboardControlCommand::SetSkillAutoUse { path, enabled }) {
                Ok(()) => {
                    let action = if enabled { "enable" } else { "disable" };
                    DashboardActionResult::ok(format!("queued skills auto-use {action}"))
                }
                Err(err) => {
                    DashboardActionResult::error(format!("failed to queue skills auto-use: {err}"))
                }
            }
        }
        DashboardAction::DismissPendingUserInput { .. }
        | DashboardAction::ClearPendingUserInputs
        | DashboardAction::UpdatePendingUserInput { .. }
        | DashboardAction::MovePendingUserInput { .. }
        | DashboardAction::MovePendingUserInputToPosition { .. }
        | DashboardAction::PreemptPendingUserInput { .. } => {
            DashboardActionResult::error("pending user input actions require a target session")
        }
    }
}