supercode-frontend-tui 0.4.6

Attachable terminal frontend primitives for Supercode SDK runtimes.
Documentation
//! The terminal skin executes the canonical live-script receipt without
//! owning a model loop, transcript, or persistence path.

use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use supercode::{
    Agent, ChatMessage, ChatRequest, Config, FrontendElicitationAction, FrontendResponse,
    FrontendRuntimeError, Provider, RpcEngine, RuntimeSubmitError, SdkRuntime, Session,
    SessionFormat, Usage,
};
use supercode_frontend_tui::runtime::TerminalRuntimeView;
use supercode_frontend_tui::terminal::palette::{ColorCapabilities, ColorLevel};

struct BlockingProvider {
    entered: tokio::sync::Notify,
    release: tokio::sync::Notify,
}

struct SharedProvider(Arc<BlockingProvider>);

#[async_trait]
impl Provider for SharedProvider {
    async fn complete(
        &self,
        _request: &ChatRequest,
        on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
    ) -> supercode::Result<(ChatMessage, Usage)> {
        self.0.entered.notify_one();
        self.0.release.notified().await;
        on_delta("same reply");
        Ok((ChatMessage::assistant("same reply"), Usage::default()))
    }
}

#[tokio::test]
async fn terminal_skin_preserves_identity_events_busy_errors_and_persistence() {
    let style = ColorCapabilities::resolve(false, Some("TERMINAL_STYLE_SENTINEL"), None, false);
    assert_eq!(style.level, ColorLevel::Monochrome);
    let provider = Arc::new(BlockingProvider {
        entered: tokio::sync::Notify::new(),
        release: tokio::sync::Notify::new(),
    });
    let persisted = Arc::new(Mutex::new(String::new()));
    let hook = persisted.clone();
    let runtime = RpcEngine::new_named(
        Agent::with_provider(
            Config::builder().system_prompt("sdk conformance").build(),
            Box::new(SharedProvider(provider.clone())),
        ),
        "stable-sdk-session",
        Some(Box::new(move |agent: &supercode::SdkAgent| {
            *hook.lock().unwrap() = serde_json::to_string(agent.history()).unwrap();
        })),
    );
    let view = Arc::new(
        TerminalRuntimeView::attach(runtime.clone(), 50)
            .await
            .unwrap(),
    );
    let mut events = TerminalRuntimeView::attach(runtime.clone(), 50)
        .await
        .unwrap();
    assert_eq!(view.descriptor().session_id, "stable-sdk-session");

    let active = {
        let view = view.clone();
        tokio::spawn(async move { view.submit("same prompt").await })
    };
    provider.entered.notified().await;
    assert_eq!(
        view.submit("busy prompt").await.unwrap_err(),
        FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)
    );
    assert_eq!(
        view.respond(FrontendResponse::Other {
            request_id: 99,
            action: FrontendElicitationAction::Cancel,
            content: None,
        })
        .await
        .unwrap_err(),
        FrontendRuntimeError::UnsupportedAction("respond")
    );
    provider.release.notify_waiters();
    assert_eq!(active.await.unwrap().unwrap(), "same reply");

    let mut observed = Vec::new();
    loop {
        let event = events.next_event().await.unwrap();
        let terminal = event.kind == "turn_succeeded";
        observed.push((event.sequence, event.kind));
        if terminal {
            break;
        }
    }
    assert_eq!(
        observed,
        vec![
            (1, "user_message".into()),
            (2, "turn_started".into()),
            (3, "text_delta".into()),
            (4, "usage".into()),
            (5, "turn_completed".into()),
            (6, "turn_succeeded".into()),
        ]
    );
    let expected = serde_json::to_string(&vec![
        ChatMessage::system("sdk conformance"),
        ChatMessage::user("same prompt"),
        ChatMessage::assistant("same reply"),
    ])
    .unwrap();
    assert_eq!(*persisted.lock().unwrap(), expected);
    let content = Session::from_native_messages(runtime.attach(50).await.unwrap().history)
        .to_jsonl(SessionFormat::Pi)
        .unwrap();
    let export_path = std::env::temp_dir().join(format!(
        "supercode-terminal-export-{}.jsonl",
        std::process::id()
    ));
    std::fs::write(&export_path, &content).unwrap();
    let reloaded = Session::load(&export_path).unwrap();
    assert!(!content.contains("TERMINAL_STYLE_SENTINEL"));
    assert!(!reloaded
        .to_jsonl(SessionFormat::Pi)
        .unwrap()
        .contains("TERMINAL_STYLE_SENTINEL"));
    std::fs::remove_file(export_path).ok();
    runtime.shutdown().await;
}