supercode-frontend-tui 0.5.82

Attachable terminal frontend primitives for Volter Harness SDK runtimes.
Documentation
//! Thin terminal consumer of the SDK-owned frontend runtime contract.

use std::sync::Arc;

use supercode_harness::frontend::FrontendAttachment;
use supercode_harness::frontend::FrontendEvent;
use supercode_harness::frontend::FrontendOperationInvocation;
use supercode_harness::frontend::FrontendOperationResult;
use supercode_harness::frontend::FrontendResponse;
use supercode_harness::frontend::FrontendRuntime;
use supercode_harness::frontend::FrontendRuntimeDescriptor;
use supercode_harness::frontend::FrontendRuntimeError;
use supercode_harness::ChatMessage;

use crate::composer::ComposerAction;
use crate::composer::ComposerModel;
use crate::transcript::TranscriptModel;

/// One terminal attachment to either a local or authenticated remote runtime.
///
/// Dropping this value detaches the terminal. It does not own or stop the SDK
/// runtime, continuation loop, scheduler, or persistence.
pub struct TerminalRuntimeView {
    runtime: Arc<dyn FrontendRuntime>,
    attachment: FrontendAttachment,
}

impl TerminalRuntimeView {
    pub async fn attach(
        runtime: Arc<dyn FrontendRuntime>,
        history_limit: usize,
    ) -> Result<Self, FrontendRuntimeError> {
        let attachment = runtime.attach(history_limit).await?;
        Ok(Self {
            runtime,
            attachment,
        })
    }

    pub fn descriptor(&self) -> &FrontendRuntimeDescriptor {
        &self.attachment.descriptor
    }

    /// Clone the protocol-neutral controller without duplicating the live
    /// attachment. Long-running actions can be spawned while this view keeps
    /// consuming runtime events.
    pub fn controller(&self) -> Arc<dyn FrontendRuntime> {
        self.runtime.clone()
    }

    pub fn history(&self) -> &[ChatMessage] {
        &self.attachment.history
    }

    pub fn history_cursor(&self) -> u64 {
        self.attachment.history_cursor
    }

    /// Build the normalized transcript at this attachment's atomic
    /// history/live boundary.
    pub fn transcript_model(&self) -> TranscriptModel {
        TranscriptModel::from_history(self.history(), self.history_cursor())
    }

    pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
        self.attachment.next_event().await
    }

    /// Return the next finite replay item without waiting for live runtime
    /// traffic. Full-screen consumers drain this snapshot before enabling
    /// input so resolved historical requests are projected atomically.
    pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
        self.attachment.next_replay_event()
    }

    /// Receive and apply the next non-duplicate runtime event.
    pub async fn update_transcript(
        &mut self,
        transcript: &mut TranscriptModel,
    ) -> Result<bool, FrontendRuntimeError> {
        let event = self.next_event().await?;
        Ok(transcript.apply_event(&event))
    }

    /// Receive one event and apply it to both terminal projections. The same
    /// lossless SDK event drives transcript and composer state.
    pub async fn update_ui(
        &mut self,
        transcript: &mut TranscriptModel,
        composer: &mut ComposerModel,
    ) -> Result<bool, FrontendRuntimeError> {
        let event = self.next_event().await?;
        let transcript_changed = transcript.apply_event(&event);
        let composer_changed = composer.apply_event(&event);
        Ok(transcript_changed || composer_changed)
    }

    /// Route a pure composer action through the protocol-neutral runtime.
    /// Typed responses complete their existing transcript request cell only
    /// after the runtime accepts the exactly-once response.
    pub async fn dispatch_composer_action(
        &self,
        action: ComposerAction,
        composer: &mut ComposerModel,
        transcript: &mut TranscriptModel,
    ) -> Result<(), FrontendRuntimeError> {
        dispatch_runtime_action(self.runtime.as_ref(), action, composer, transcript).await
    }

    pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, FrontendRuntimeError> {
        self.runtime.submit(prompt.into()).await
    }

    pub async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
        self.runtime.interrupt().await
    }

    pub async fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
        self.runtime.steer(prompt.into()).await
    }

    pub async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
        self.runtime.respond(response).await
    }

    pub async fn invoke(
        &self,
        operation: FrontendOperationInvocation,
    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
        self.runtime.invoke(operation).await
    }
}

async fn dispatch_runtime_action(
    runtime: &dyn FrontendRuntime,
    action: ComposerAction,
    composer: &mut ComposerModel,
    transcript: &mut TranscriptModel,
) -> Result<(), FrontendRuntimeError> {
    match action {
        ComposerAction::Submit(prompt) => {
            if let Err(error) = runtime.submit(prompt).await {
                if !is_interrupted_submit_error(&error) {
                    composer.record_dispatch_failure("submit");
                }
                return Err(error);
            }
        }
        ComposerAction::Invoke(operation) => {
            if let Err(error) = runtime.invoke(operation).await {
                composer.record_dispatch_failure("invoke");
                return Err(error);
            }
        }
        ComposerAction::Steer(prompt) => runtime.steer(prompt).await?,
        ComposerAction::Interrupt => {
            runtime.interrupt().await?;
        }
        ComposerAction::Respond {
            response,
            request,
            resolution,
        } => {
            let request_id = match &response {
                FrontendResponse::Approval { request_id, .. }
                | FrontendResponse::Elicitation { request_id, .. }
                | FrontendResponse::Other { request_id, .. } => *request_id,
            };
            if let Err(error) = runtime.respond(response).await {
                composer.restore_request(request);
                return Err(error);
            }
            transcript.resolve_frontend_request(request_id, &resolution);
        }
    }
    Ok(())
}

pub(crate) fn is_interrupted_submit_error(error: &FrontendRuntimeError) -> bool {
    matches!(
        error,
        FrontendRuntimeError::Submit(supercode_harness::server::RuntimeSubmitError::Interrupted)
    )
}