vtcode-llm 0.181.0

LLM provider abstraction, client implementations, and streaming for VT Code
Documentation
use super::*;

pub(super) fn handle_terminal_create_request(inner: &Arc<CopilotAcpClientInner>, message: &Value) -> Result<()> {
    handle_runtime_request_message(
        inner,
        message,
        parse_terminal_create_request,
        |request, response_tx| {
            CopilotRuntimeRequest::TerminalCreate(PendingTerminalCreateRequest { request, response_tx })
        },
        |response| RpcReply::result(build_terminal_create_result(response)),
        RpcReply::runtime_error("VT Code could not create the requested terminal."),
        RpcReply::runtime_error(
            "VT Code could not create the requested terminal because the Copilot runtime is unavailable.",
        ),
        "terminal_create.respond",
    )
}

pub(super) fn handle_terminal_output_request(inner: &Arc<CopilotAcpClientInner>, message: &Value) -> Result<()> {
    handle_runtime_request_message(
        inner,
        message,
        |params| {
            parse_terminal_request(params, |session_id, terminal_id| CopilotTerminalOutputRequest {
                session_id,
                terminal_id,
            })
        },
        |request, response_tx| {
            CopilotRuntimeRequest::TerminalOutput(PendingTerminalOutputRequest { request, response_tx })
        },
        |response| RpcReply::result(build_terminal_output_result(response)),
        RpcReply::runtime_error("VT Code could not read the requested terminal output."),
        RpcReply::runtime_error(
            "VT Code could not read the requested terminal output because the Copilot runtime is unavailable.",
        ),
        "terminal_output.respond",
    )
}

pub(super) fn handle_terminal_release_request(inner: &Arc<CopilotAcpClientInner>, message: &Value) -> Result<()> {
    handle_runtime_request_message(
        inner,
        message,
        |params| {
            parse_terminal_request(params, |session_id, terminal_id| CopilotTerminalReleaseRequest {
                session_id,
                terminal_id,
            })
        },
        |request, response_tx| {
            CopilotRuntimeRequest::TerminalRelease(PendingTerminalReleaseRequest { request, response_tx })
        },
        |_| RpcReply::result(json!({})),
        RpcReply::runtime_error("VT Code could not release the requested terminal."),
        RpcReply::runtime_error(
            "VT Code could not release the requested terminal because the Copilot runtime is unavailable.",
        ),
        "terminal_release.respond",
    )
}

pub(super) fn handle_terminal_kill_request(inner: &Arc<CopilotAcpClientInner>, message: &Value) -> Result<()> {
    handle_runtime_request_message(
        inner,
        message,
        |params| {
            parse_terminal_request(params, |session_id, terminal_id| CopilotTerminalKillRequest {
                session_id,
                terminal_id,
            })
        },
        |request, response_tx| CopilotRuntimeRequest::TerminalKill(PendingTerminalKillRequest { request, response_tx }),
        |_| RpcReply::result(json!({})),
        RpcReply::runtime_error("VT Code could not kill the requested terminal command."),
        RpcReply::runtime_error(
            "VT Code could not kill the requested terminal command because the Copilot runtime is unavailable.",
        ),
        "terminal_kill.respond",
    )
}

pub(super) fn handle_terminal_wait_for_exit_request(inner: &Arc<CopilotAcpClientInner>, message: &Value) -> Result<()> {
    handle_runtime_request_message(
        inner,
        message,
        |params| {
            parse_terminal_request(params, |session_id, terminal_id| CopilotTerminalWaitForExitRequest {
                session_id,
                terminal_id,
            })
        },
        |request, response_tx| {
            CopilotRuntimeRequest::TerminalWaitForExit(PendingTerminalWaitForExitRequest { request, response_tx })
        },
        |response| RpcReply::result(build_terminal_wait_for_exit_result(response)),
        RpcReply::runtime_error("VT Code could not wait for the requested terminal."),
        RpcReply::runtime_error(
            "VT Code could not wait for the requested terminal because the Copilot runtime is unavailable.",
        ),
        "terminal_wait_for_exit.respond",
    )
}

fn parse_terminal_create_request(params: &Value) -> Result<CopilotTerminalCreateRequest> {
    let session_id = optional_session_id(params);
    let command = required_non_empty_string(params, "command", "copilot terminal/create missing command")?;
    let args = parse_string_array(
        params.get("args"),
        MAX_TERMINAL_ARG_COUNT,
        "copilot terminal args must be strings",
        "copilot terminal/create has too many args",
    )?;
    let env = parse_terminal_env_vars(params.get("env"))?;
    let cwd = params.get("cwd").and_then(Value::as_str).map(PathBuf::from);
    let output_byte_limit = params.get("outputByteLimit").and_then(Value::as_u64).map(|value| {
        usize::try_from(value)
            .unwrap_or(MAX_TERMINAL_OUTPUT_BYTE_LIMIT)
            .min(MAX_TERMINAL_OUTPUT_BYTE_LIMIT)
    });

    Ok(CopilotTerminalCreateRequest {
        session_id,
        command,
        args,
        env,
        cwd,
        output_byte_limit,
    })
}

fn parse_terminal_request<T>(params: &Value, build: impl FnOnce(String, String) -> T) -> Result<T> {
    let session_id = optional_session_id(params);
    let terminal_id = required_non_empty_string(params, "terminalId", "copilot terminal request missing terminalId")?;
    Ok(build(session_id, terminal_id))
}

fn optional_session_id(params: &Value) -> String {
    params.get("sessionId").and_then(Value::as_str).unwrap_or_default().to_string()
}

fn required_non_empty_string(params: &Value, key: &str, error_message: &'static str) -> Result<String> {
    params
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .ok_or_else(|| anyhow!(error_message))
}

fn parse_string_array(
    value: Option<&Value>,
    max_items: usize,
    item_error: &'static str,
    limit_error: &'static str,
) -> Result<Vec<String>> {
    let Some(values) = value.and_then(Value::as_array) else {
        return Ok(Vec::new());
    };

    if values.len() > max_items {
        anyhow::bail!(limit_error);
    }

    values
        .iter()
        .map(|value| value.as_str().map(str::to_string).ok_or_else(|| anyhow!(item_error)))
        .collect()
}

fn parse_terminal_env_vars(value: Option<&Value>) -> Result<Vec<CopilotTerminalEnvVar>> {
    let Some(values) = value.and_then(Value::as_array) else {
        return Ok(Vec::new());
    };

    if values.len() > MAX_TERMINAL_ENV_VAR_COUNT {
        anyhow::bail!("copilot terminal/create has too many env entries");
    }

    values
        .iter()
        .map(|value| {
            let object = value
                .as_object()
                .ok_or_else(|| anyhow!("copilot terminal env entries must be objects"))?;
            let name = object
                .get("name")
                .and_then(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(str::to_string)
                .ok_or_else(|| anyhow!("copilot terminal env entries require a name"))?;
            let value = object
                .get("value")
                .and_then(Value::as_str)
                .map(str::to_string)
                .ok_or_else(|| anyhow!("copilot terminal env entries require a value"))?;
            Ok(CopilotTerminalEnvVar { name, value })
        })
        .collect()
}

fn build_terminal_create_result(response: CopilotTerminalCreateResponse) -> Value {
    json!({
        "terminalId": response.terminal_id,
    })
}

fn build_terminal_output_result(response: CopilotTerminalOutputResponse) -> Value {
    let exit_status = response.exit_status.map(build_terminal_exit_status_json);
    let mut result = serde_json::Map::from_iter([
        ("output".to_string(), Value::String(response.output)),
        ("truncated".to_string(), Value::Bool(response.truncated)),
    ]);
    if let Some(exit_status) = exit_status {
        result.insert("exitStatus".to_string(), exit_status);
    }
    Value::Object(result)
}

fn build_terminal_wait_for_exit_result(response: CopilotTerminalExitStatus) -> Value {
    build_terminal_exit_status_json(response)
}

fn build_terminal_exit_status_json(status: CopilotTerminalExitStatus) -> Value {
    let mut result = serde_json::Map::new();
    result.insert("exitCode".to_string(), status.exit_code.map_or(Value::Null, |value| Value::from(u64::from(value))));
    result.insert("signal".to_string(), status.signal.map_or(Value::Null, Value::String));
    Value::Object(result)
}