starweaver-cli 0.10.0

Command-line interface for Starweaver
Documentation
use super::{Value, full_content_lines, preview_line, preview_lines};

pub(in crate::tui::state) fn format_custom_context_event_lines(
    kind: &str,
    payload: &Value,
) -> Option<Vec<String>> {
    let normalized = normalized_event_kind(kind);
    let context_payload = payload.get("payload").unwrap_or(payload);
    if event_kind_matches(
        &normalized,
        &[
            "compact_start",
            "compact_started",
            "compaction_start",
            "compaction_started",
        ],
    ) {
        return Some(format_compaction_started_lines(context_payload));
    }
    if event_kind_matches(
        &normalized,
        &[
            "compact_complete",
            "compact_completed",
            "compaction_complete",
            "compaction_completed",
        ],
    ) {
        return Some(format_compaction_completed_lines(context_payload, payload));
    }
    if event_kind_matches(
        &normalized,
        &[
            "compact_failed",
            "compact_failure",
            "compaction_failed",
            "compaction_failure",
        ],
    ) {
        return Some(vec![format!(
            "Compact failed: {}",
            compact_error_text(context_payload, payload)
        )]);
    }
    if event_kind_matches(
        &normalized,
        &[
            "handoff_start",
            "handoff_started",
            "summary_start",
            "summary_started",
        ],
    ) {
        return Some(format_handoff_started_lines(context_payload));
    }
    if event_kind_matches(
        &normalized,
        &[
            "handoff_complete",
            "handoff_completed",
            "summary_complete",
            "summary_completed",
        ],
    ) {
        return Some(format_handoff_completed_lines(context_payload, payload));
    }
    if event_kind_matches(
        &normalized,
        &[
            "handoff_failed",
            "handoff_failure",
            "summary_failed",
            "summary_failure",
        ],
    ) {
        return Some(vec![format!(
            "Summary failed: {}",
            compact_error_text(context_payload, payload)
        )]);
    }
    if event_kind_matches(&normalized, &["goal_iteration"]) {
        return Some(format_goal_iteration_lines(context_payload));
    }
    if event_kind_matches(&normalized, &["goal_complete", "goal_completed"]) {
        return Some(format_goal_complete_lines(context_payload));
    }
    None
}

pub(in crate::tui::state) fn normalized_event_kind(kind: &str) -> String {
    kind.to_ascii_lowercase().replace(['.', '-'], "_")
}

pub(in crate::tui::state) fn is_subagent_start_event_kind(normalized: &str) -> bool {
    event_kind_matches(normalized, &["subagent_start", "subagent_started"])
}

pub(in crate::tui::state) fn is_subagent_complete_event_kind(normalized: &str) -> bool {
    event_kind_matches(normalized, &["subagent_complete", "subagent_completed"])
}

pub(in crate::tui::state) fn is_subagent_failed_event_kind(normalized: &str) -> bool {
    event_kind_matches(normalized, &["subagent_fail", "subagent_failed"])
}

pub(in crate::tui::state) fn is_subagent_lifecycle_event_kind(kind: &str) -> bool {
    let normalized = normalized_event_kind(kind);
    is_subagent_start_event_kind(&normalized)
        || is_subagent_complete_event_kind(&normalized)
        || is_subagent_failed_event_kind(&normalized)
}

fn event_kind_matches(normalized: &str, candidates: &[&str]) -> bool {
    candidates
        .iter()
        .any(|candidate| normalized == *candidate || normalized.ends_with(&format!("_{candidate}")))
}

pub(in crate::tui::state) fn subagent_event_payload(payload: &Value) -> &Value {
    payload.get("payload").unwrap_or(payload)
}

pub(in crate::tui::state) fn subagent_display_id(payload: &Value) -> String {
    let payload = subagent_event_payload(payload);
    payload
        .get("metadata")
        .and_then(|metadata| payload_string(metadata, &["agent_id", "id"]))
        .or_else(|| {
            payload_string(
                payload,
                &[
                    "agent_id",
                    "id",
                    "name",
                    "subagent",
                    "subagent_name",
                    "agent_name",
                    "agent",
                ],
            )
        })
        .unwrap_or_else(|| "subagent".to_string())
}

pub(in crate::tui::state) fn format_subagent_running_line(payload: &Value) -> String {
    let agent_id = subagent_display_id(payload);
    format!("[{agent_id}] Running...")
}

pub(in crate::tui::state) fn format_subagent_finished_line(kind: &str, payload: &Value) -> String {
    let normalized = normalized_event_kind(kind);
    let payload = subagent_event_payload(payload);
    let agent_id = subagent_display_id(payload);
    let success = if is_subagent_failed_event_kind(&normalized) {
        false
    } else {
        payload
            .get("success")
            .and_then(Value::as_bool)
            .unwrap_or(true)
    };

    let mut line = if success {
        format!("[{agent_id}] Done")
    } else {
        format!("[{agent_id}] Failed")
    };
    if let Some(duration_seconds) = payload_f64(
        payload,
        &["duration_seconds", "duration", "elapsed_seconds"],
    ) {
        line.push_str(" (");
        let duration_text = format!("{duration_seconds:.1}s");
        line.push_str(&duration_text);
        line.push(')');
    }
    if success {
        let metadata = payload.get("metadata");
        if let Some(request_count) = payload_u64(payload, &["request_count", "requests"])
            .or_else(|| {
                metadata.and_then(|value| payload_u64(value, &["request_count", "requests"]))
            })
            .filter(|count| *count > 0)
        {
            line.push_str(" | ");
            line.push_str(&request_count.to_string());
            line.push_str(" reqs");
        }
        if let Some(preview) = payload_string(payload, &["result_preview", "preview", "output"])
            .or_else(|| {
                metadata.and_then(|value| {
                    payload_string(value, &["result_preview", "preview", "output"])
                })
            })
            .filter(|preview| !preview.trim().is_empty())
        {
            line.push_str(" | \"");
            line.push_str(&preview_line(&preview.replace('\n', " ")));
            line.push('"');
        }
    } else if let Some(error) = payload
        .get("metadata")
        .and_then(|metadata| payload_string(metadata, &["error", "message", "reason"]))
        .or_else(|| payload_string(payload, &["error", "message", "reason"]))
    {
        line.push_str(" | ");
        line.push_str(&preview_line(&error));
    }
    line
}

fn format_compaction_started_lines(payload: &Value) -> Vec<String> {
    let message_count = payload_u64(
        payload,
        &[
            "message_count",
            "messages",
            "original_count",
            "original_message_count",
            "original_messages",
        ],
    );
    vec![message_count.map_or_else(
        || "Context compacting...".to_string(),
        |count| format!("Context compacting {count} messages..."),
    )]
}

fn format_compaction_completed_lines(payload: &Value, wrapper: &Value) -> Vec<String> {
    let mut lines = vec!["Context compacted".to_string()];
    let original = payload_u64(
        payload,
        &[
            "original_count",
            "original_message_count",
            "original_messages",
        ],
    );
    let compacted = payload_u64(
        payload,
        &[
            "compacted_count",
            "compacted_message_count",
            "compacted_messages",
            "message_count",
            "messages",
        ],
    );
    match (original, compacted) {
        (Some(original), Some(compacted)) if original > 0 => {
            let reduction = original
                .saturating_sub(compacted)
                .saturating_mul(100)
                .saturating_add(original / 2)
                / original;
            lines.push(format!(
                "  Summary: {original} -> {compacted} messages ({reduction}% reduction)"
            ));
        }
        (_, Some(compacted)) => {
            if let Some(revision) = payload_u64(payload, &["revision"]) {
                lines.push(format!(
                    "  Summary: revision {revision}, {compacted} messages retained"
                ));
            } else {
                lines.push(format!("  Summary: {compacted} messages retained"));
            }
        }
        _ => {
            let preview = payload_string(wrapper, &["preview"])
                .unwrap_or_else(|| "Context compaction completed".to_string());
            lines.push(format!("  Summary: {}", preview_line(&preview)));
        }
    }
    if let Some(summary) = payload_string(
        payload,
        &["summary", "summary_markdown", "content", "handoff_content"],
    ) {
        push_indented_full_content(&mut lines, &summary);
    }
    lines
}

fn format_handoff_started_lines(payload: &Value) -> Vec<String> {
    let message_count = payload_u64(
        payload,
        &[
            "message_count",
            "messages",
            "original_count",
            "original_message_count",
        ],
    );
    vec![message_count.map_or_else(
        || "Summarizing progress...".to_string(),
        |count| format!("Summarizing progress ({count} messages)..."),
    )]
}

fn format_handoff_completed_lines(payload: &Value, wrapper: &Value) -> Vec<String> {
    let mut lines = vec!["Summary complete".to_string()];
    lines.push("  Summary: Progress summarized, continuing with fresh context".to_string());
    let content = payload_string(
        payload,
        &["handoff_content", "content", "summary_markdown", "summary"],
    )
    .or_else(|| payload_string(wrapper, &["preview"]));
    if let Some(content) = content {
        push_indented_full_content(&mut lines, &content);
    }
    lines
}

pub(super) fn push_indented_preview(lines: &mut Vec<String>, content: &str, max_lines: usize) {
    if content.trim().is_empty() {
        return;
    }
    for line in preview_lines(content, max_lines) {
        lines.push(format!("    │ {line}"));
    }
}

pub(super) fn push_indented_full_content(lines: &mut Vec<String>, content: &str) {
    if content.trim().is_empty() {
        return;
    }
    for line in full_content_lines(content) {
        lines.push(format!("    │ {line}"));
    }
}

fn compact_error_text(payload: &Value, wrapper: &Value) -> String {
    payload_string(payload, &["error", "message", "reason"])
        .or_else(|| payload_string(wrapper, &["error", "message", "preview"]))
        .unwrap_or_else(|| "unknown error".to_string())
}

fn format_goal_iteration_lines(payload: &Value) -> Vec<String> {
    let iteration = payload_u64(payload, &["iteration"]).unwrap_or(0);
    let max_iterations = payload_u64(payload, &["max_iterations"]).unwrap_or(0);
    vec![format!("[Goal] Iteration {iteration}/{max_iterations}")]
}

fn format_goal_complete_lines(payload: &Value) -> Vec<String> {
    let reason = payload_string(payload, &["reason"]).unwrap_or_else(|| "unknown".to_string());
    let iteration = payload_u64(payload, &["iteration"]).unwrap_or(0);
    let line = match reason.as_str() {
        "verified" => format!("[Goal] Completed: verified after {iteration} iteration(s)"),
        "max_iterations" => {
            format!("[Goal] Completed: max iterations reached after {iteration} iteration(s)")
        }
        other => format!("[Goal] Completed: {other}"),
    };
    vec![line]
}

fn payload_u64(value: &Value, keys: &[&str]) -> Option<u64> {
    keys.iter().find_map(|key| {
        value.get(*key).and_then(|item| {
            item.as_u64()
                .or_else(|| item.as_i64().and_then(|number| u64::try_from(number).ok()))
                .or_else(|| item.as_str().and_then(|text| text.parse::<u64>().ok()))
        })
    })
}

fn payload_f64(value: &Value, keys: &[&str]) -> Option<f64> {
    keys.iter().find_map(|key| {
        value.get(*key).and_then(|item| match item {
            Value::Number(number) => number.to_string().parse::<f64>().ok(),
            Value::String(text) => text.parse::<f64>().ok(),
            _ => None,
        })
    })
}

pub(super) fn payload_string(value: &Value, keys: &[&str]) -> Option<String> {
    keys.iter().find_map(|key| {
        let item = value.get(*key)?;
        match item {
            Value::String(text) if !text.trim().is_empty() => Some(text.clone()),
            Value::Null => None,
            other if !other.to_string().trim().is_empty() => Some(other.to_string()),
            _ => None,
        }
    })
}

pub(super) fn payload_string_array(value: &Value, key: &str) -> Option<Vec<String>> {
    let item = value.get(key)?;
    let values = match item {
        Value::Array(items) => items
            .iter()
            .filter_map(|item| item.as_str().map(str::to_string))
            .collect::<Vec<_>>(),
        Value::String(text) if !text.trim().is_empty() => vec![text.clone()],
        _ => Vec::new(),
    };
    (!values.is_empty()).then_some(values)
}