aether-wisp 0.3.1

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use acp_utils::notifications::{ContextUsageParams, SubAgentEvent, SubAgentProgressParams, SubAgentToolRequest};
use agent_client_protocol::schema as acp;
use tui::testing::TestTerminal;

use super::common::*;

#[tokio::test]
async fn test_sub_agent_progress_notification_triggers_render() {
    let terminal = TestTerminal::new(TEST_WIDTH, 40);
    let mut renderer = Renderer::new(terminal, TEST_AGENT.to_string(), &[], (TEST_WIDTH, 40));
    renderer.initial_render().unwrap();

    let params = SubAgentProgressParams {
        parent_tool_id: "p1".to_string(),
        task_id: "t1".to_string(),
        agent_name: "explorer".to_string(),
        event: SubAgentEvent::ToolCall {
            request: SubAgentToolRequest {
                id: "c1".to_string(),
                name: "grep".to_string(),
                arguments: "{}".to_string(),
            },
        },
    };
    renderer.on_sub_agent_progress(params).unwrap();

    let lines = renderer.writer().get_lines();
    assert!(!lines.is_empty());
}

#[tokio::test]
async fn test_context_usage_notification_updates_nominal_display() {
    let terminal = TestTerminal::new(TEST_WIDTH, 40);
    let mut renderer = Renderer::new(terminal, TEST_AGENT.to_string(), &[], (TEST_WIDTH, 40));
    renderer.initial_render().unwrap();

    let params = ContextUsageParams {
        usage_ratio: Some(0.75),
        context_limit: Some(200_000),
        input_tokens: 150_000,
        output_tokens: 0,
        cache_read_tokens: None,
        cache_creation_tokens: None,
        reasoning_tokens: None,
        total_input_tokens: 0,
        total_output_tokens: 0,
        total_cache_read_tokens: 0,
        total_cache_creation_tokens: 0,
        total_reasoning_tokens: 0,
    };
    renderer.on_context_usage(params).unwrap();

    let lines = renderer.writer().get_lines();
    assert!(
        lines.iter().any(|l| l.contains("150k / 200k")),
        "Status line should show nominal context usage.\nBuffer:\n{}",
        lines.join("\n")
    );
}

#[tokio::test]
async fn test_context_usage_notification_with_unknown_limit_clears_meter() {
    let terminal = TestTerminal::new(TEST_WIDTH, 40);
    let mut renderer = Renderer::new(terminal, TEST_AGENT.to_string(), &[], (TEST_WIDTH, 40));
    renderer.initial_render().unwrap();

    let nominal = ContextUsageParams {
        usage_ratio: Some(0.67),
        context_limit: Some(150_000),
        input_tokens: 100_000,
        output_tokens: 0,
        cache_read_tokens: None,
        cache_creation_tokens: None,
        reasoning_tokens: None,
        total_input_tokens: 0,
        total_output_tokens: 0,
        total_cache_read_tokens: 0,
        total_cache_creation_tokens: 0,
        total_reasoning_tokens: 0,
    };
    renderer.on_context_usage(nominal).unwrap();

    let cleared = ContextUsageParams {
        usage_ratio: None,
        context_limit: None,
        input_tokens: 0,
        output_tokens: 0,
        cache_read_tokens: None,
        cache_creation_tokens: None,
        reasoning_tokens: None,
        total_input_tokens: 0,
        total_output_tokens: 0,
        total_cache_read_tokens: 0,
        total_cache_creation_tokens: 0,
        total_reasoning_tokens: 0,
    };
    renderer.on_context_usage(cleared).unwrap();

    let lines = renderer.writer().get_lines();
    assert!(
        !lines.iter().any(|l| l.contains("ctx")),
        "Context segment should not be shown when limit is unknown.\nBuffer:\n{}",
        lines.join("\n")
    );
}

#[tokio::test]
async fn test_context_cleared_notification_resets_conversation() {
    let terminal = TestTerminal::new(TEST_WIDTH, 40);
    let mut renderer = Renderer::new(terminal, TEST_AGENT.to_string(), &[], (TEST_WIDTH, 40));
    renderer.initial_render().unwrap();

    renderer
        .on_session_update(acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(acp::ContentBlock::Text(
            acp::TextContent::new("hello world"),
        ))))
        .unwrap();

    let lines = renderer.writer().get_lines();
    assert!(lines.iter().any(|l| l.contains("hello world")), "Content should be visible before clear");

    renderer.on_context_cleared().unwrap();

    let lines = renderer.writer().get_lines();
    assert!(
        !lines.iter().any(|l| l.contains("hello world")),
        "Content should be cleared after context_cleared.\nBuffer:\n{}",
        lines.join("\n")
    );
}

#[tokio::test]
async fn test_on_tick_requests_render_while_completed_entries() {
    let terminal = TestTerminal::new(TEST_WIDTH, 40);
    let mut renderer = Renderer::new(terminal, TEST_AGENT.to_string(), &[], (TEST_WIDTH, 40));
    renderer.initial_render().unwrap();

    renderer
        .on_session_update(acp::SessionUpdate::Plan(acp::Plan::new(vec![acp::PlanEntry::new(
            "1",
            acp::PlanEntryPriority::Medium,
            acp::PlanEntryStatus::Completed,
        )])))
        .unwrap();

    renderer.on_tick().await.unwrap();

    let lines = renderer.writer().get_lines();
    assert!(!lines.is_empty());
}