tact 0.6.2

Terminal interface for Nanocodex
use crate::{
    app::config::{ReasoningEffort, ReasoningMode},
    core::ConfiguredAgent,
    tui::{pane::PaneId, worker::AuxiliaryError},
};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;

pub(crate) type HandoffResult = Result<PreparedHandoff, AuxiliaryError>;
pub(crate) type HandoffTask = JoinHandle<HandoffCompletion>;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct HandoffIdentity {
    pub(crate) pane: PaneId,
    pub(crate) pane_generation: u64,
    controller_generation: u64,
}

pub(crate) struct HandoffCompletion {
    pub(crate) identity: HandoffIdentity,
    pub(crate) result: HandoffResult,
}

pub(crate) struct PreparedHandoff {
    pub(crate) prompt: String,
    pub(crate) effort: ReasoningEffort,
    pub(crate) reasoning_mode: ReasoningMode,
    pub(crate) fast_mode: bool,
    pub(crate) configured: ConfiguredAgent,
}

struct ActiveHandoff {
    identity: HandoffIdentity,
    cancellation: CancellationToken,
    task: HandoffTask,
}

pub(crate) struct HandoffController {
    next_generation: u64,
    active: Option<ActiveHandoff>,
}

impl HandoffController {
    pub(crate) const fn new() -> Self {
        Self {
            next_generation: 0,
            active: None,
        }
    }

    pub(crate) fn start(
        &mut self,
        pane: PaneId,
        pane_generation: u64,
        spawn: impl FnOnce(HandoffIdentity, CancellationToken) -> HandoffTask,
    ) -> Option<HandoffIdentity> {
        if self.active.is_some() {
            return None;
        }

        let identity = HandoffIdentity {
            pane,
            pane_generation,
            controller_generation: self.next_generation,
        };
        self.next_generation = self.next_generation.saturating_add(1);
        let cancellation = CancellationToken::new();
        let task = spawn(identity, cancellation.clone());
        self.active = Some(ActiveHandoff {
            identity,
            cancellation,
            task,
        });
        Some(identity)
    }

    pub(crate) fn task_mut(&mut self) -> Option<&mut HandoffTask> {
        self.active.as_mut().map(|handoff| &mut handoff.task)
    }

    pub(crate) fn cancel(&mut self) -> Option<HandoffIdentity> {
        let handoff = self.active.as_ref()?;
        handoff.cancellation.cancel();
        Some(handoff.identity)
    }

    pub(crate) fn complete(&mut self, identity: HandoffIdentity) -> bool {
        let matches = self
            .active
            .as_ref()
            .is_some_and(|handoff| handoff.identity == identity);
        if matches {
            self.active = None;
        }
        matches
    }
}

#[cfg(test)]
mod tests {
    use super::{HandoffCompletion, HandoffController, HandoffIdentity};
    use crate::tui::pane::PaneId;

    fn pending_handoff(
        identity: HandoffIdentity,
        _: tokio_util::sync::CancellationToken,
    ) -> super::HandoffTask {
        tokio::spawn(async move {
            std::future::pending::<()>().await;
            HandoffCompletion {
                identity,
                result: Err(crate::tui::worker::AuxiliaryError::Cancelled),
            }
        })
    }

    #[tokio::test]
    async fn controller_rejects_overlap_and_cancels_its_operation() {
        let mut controller = HandoffController::new();
        let identity = controller
            .start(PaneId::Main, 4, pending_handoff)
            .expect("the first handoff should start");

        assert!(controller.start(PaneId::Main, 4, pending_handoff).is_none());
        assert_eq!(controller.cancel(), Some(identity));
        assert!(controller.task_mut().is_some());
    }
}