aether-wisp 0.7.3

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use crate::command::{Command, CommandResult, FilesystemCommand, TerminalCommand};
use crate::runtime::{agent, files};
use acp_utils::client::AcpClientHandle;
use crossterm::{execute, style::Print};
use std::io;

use super::git_review::GitReviewRuntime;
use super::tasks::{ReadTask, TaskSupervisor};

pub struct CommandDispatcher {
    client_handle: AcpClientHandle,
    tasks: TaskSupervisor,
    git_review: GitReviewRuntime,
}

impl CommandDispatcher {
    pub fn new(client_handle: AcpClientHandle) -> Self {
        Self {
            git_review: GitReviewRuntime::new(client_handle.clone()),
            client_handle,
            tasks: TaskSupervisor::default(),
        }
    }

    pub fn dispatch(&mut self, command: Command) -> Option<CommandResult> {
        match command {
            Command::Agent(command) => agent::execute(&self.client_handle, command, &mut self.tasks),
            Command::GitReview(command) => self.git_review.dispatch(command, &mut self.tasks),
            Command::Filesystem(command) => {
                let key = match &command {
                    FilesystemCommand::IndexFiles { .. } => Some(ReadTask::FileIndex),
                    FilesystemCommand::PrepareSubmission { .. } => Some(ReadTask::AttachmentPreparation),
                    FilesystemCommand::ListThemes => Some(ReadTask::ThemeList),
                    FilesystemCommand::ListReviewThemes => Some(ReadTask::ReviewThemeList),
                    FilesystemCommand::ApplyTheme { .. } => None,
                };
                let work = async move { files::execute(command).await };
                if let Some(key) = key {
                    self.tasks.spawn_read(key, work);
                } else {
                    self.tasks.spawn_mutation(work);
                }
                None
            }
            Command::Terminal(command) => execute_terminal(&command),
        }
    }

    pub fn has_pending_tasks(&self) -> bool {
        !self.tasks.is_empty() || self.git_review.is_active()
    }

    pub async fn next_result(&mut self) -> Option<CommandResult> {
        loop {
            if !self.git_review.is_active() && self.tasks.is_empty() {
                return None;
            }
            tokio::select! {
                state = self.git_review.changed() => {
                    match state {
                        Some(state) => return Some(CommandResult::GitReview(state)),
                        None => self.git_review.close(),
                    }
                }
                result = std::future::poll_fn(|cx| self.tasks.poll_result(cx)), if !self.tasks.is_empty() => {
                    if let Some(result) = result {
                        return Some(result);
                    }
                }
            }
        }
    }

    pub async fn shutdown(&mut self) {
        self.git_review.close();
        self.client_handle.disconnect().await;
        self.tasks.shutdown().await;
    }
}

fn execute_terminal(command: &TerminalCommand) -> Option<CommandResult> {
    let TerminalCommand::RingBell = command;
    execute!(io::stdout(), Print("\x07")).err().map(|error| CommandResult::TerminalFailed(error.to_string()))
}