tandem-tui 0.4.22

Terminal user interface for the Tandem engine
use super::{AgentPane, App};
use std::collections::HashSet;

impl App {
    fn make_paste_marker(id: u32, payload: &str) -> String {
        format!("[Pasted {} chars #{}]", payload.chars().count(), id)
    }

    pub(super) fn register_collapsed_paste(agent: &mut AgentPane, payload: &str) -> String {
        let id = agent.next_paste_id;
        agent.next_paste_id = agent.next_paste_id.saturating_add(1);
        agent.paste_registry.insert(id, payload.to_string());
        Self::make_paste_marker(id, payload)
    }

    pub(super) fn should_collapse_paste(payload: &str) -> bool {
        payload.lines().count() > 2
    }

    pub(super) fn insert_chat_paste(agent: Option<&mut AgentPane>, payload: &str) -> String {
        if !Self::should_collapse_paste(payload) {
            return payload.to_string();
        }
        if let Some(agent) = agent {
            return Self::register_collapsed_paste(agent, payload);
        }
        format!("[Pasted {} chars]", payload.chars().count())
    }

    pub(super) fn normalize_paste_payload(payload: &str) -> String {
        payload.replace("\r\n", "\n").replace('\r', "\n")
    }

    fn parse_marker_id(marker: &str) -> Option<u32> {
        let trimmed = marker.trim();
        if !trimmed.starts_with("[Pasted ") || !trimmed.ends_with(']') {
            return None;
        }
        let hash = trimmed.rfind('#')?;
        let id_str = &trimmed[hash + 1..trimmed.len() - 1];
        id_str.parse::<u32>().ok()
    }

    fn find_paste_token_ranges(text: &str) -> Vec<(usize, usize)> {
        let mut ranges = Vec::new();
        let mut i = 0usize;
        while i < text.len() {
            let rest = &text[i..];
            if rest.starts_with("[Pasted ") {
                if let Some(end_rel) = rest.find(']') {
                    let end = i + end_rel + 1;
                    let token = &text[i..end];
                    if token.contains(" chars") {
                        ranges.push((i, end));
                        i = end;
                        continue;
                    }
                }
            }
            if let Some(ch) = rest.chars().next() {
                i += ch.len_utf8();
            } else {
                break;
            }
        }
        ranges
    }

    fn prev_char_boundary(text: &str, pos: usize) -> usize {
        if pos == 0 {
            return 0;
        }
        let mut i = pos.saturating_sub(1);
        while i > 0 && !text.is_char_boundary(i) {
            i = i.saturating_sub(1);
        }
        i
    }

    pub(super) fn paste_token_range_for_backspace(
        input: &crate::ui::components::composer_input::ComposerInputState,
    ) -> Option<(usize, usize)> {
        let text = input.text();
        let cursor = input.cursor_byte_index().min(text.len());
        if cursor == 0 {
            return None;
        }
        let target = Self::prev_char_boundary(text, cursor);
        Self::find_paste_token_ranges(text)
            .into_iter()
            .find(|(start, end)| target >= *start && target < *end)
    }

    pub(super) fn paste_token_range_for_delete(
        input: &crate::ui::components::composer_input::ComposerInputState,
    ) -> Option<(usize, usize)> {
        let text = input.text();
        let cursor = input.cursor_byte_index().min(text.len());
        if cursor >= text.len() {
            return None;
        }
        Self::find_paste_token_ranges(text)
            .into_iter()
            .find(|(start, end)| cursor >= *start && cursor < *end)
    }

    fn collect_referenced_paste_ids(text: &str) -> HashSet<u32> {
        let mut ids = HashSet::new();
        let mut i = 0usize;
        while i < text.len() {
            let rest = &text[i..];
            if rest.starts_with("[Pasted ") {
                if let Some(end_rel) = rest.find(']') {
                    let end = i + end_rel + 1;
                    if let Some(id) = Self::parse_marker_id(&text[i..end]) {
                        ids.insert(id);
                    }
                    i = end;
                    continue;
                }
            }
            if let Some(ch) = rest.chars().next() {
                i += ch.len_utf8();
            } else {
                break;
            }
        }
        ids
    }

    pub(super) fn prune_agent_paste_registry(agent: &mut AgentPane) {
        let referenced = Self::collect_referenced_paste_ids(agent.draft.text());
        agent.paste_registry.retain(|id, _| referenced.contains(id));
    }

    pub(super) fn expand_paste_markers(text: &str, agent: &AgentPane) -> String {
        let mut out = String::with_capacity(text.len());
        let mut i = 0usize;
        while i < text.len() {
            if text[i..].starts_with("[Pasted ") {
                if let Some(end_rel) = text[i..].find(']') {
                    let end = i + end_rel + 1;
                    let marker = &text[i..end];
                    if let Some(id) = Self::parse_marker_id(marker) {
                        if let Some(payload) = agent.paste_registry.get(&id) {
                            out.push_str(payload);
                            i = end;
                            continue;
                        }
                    }
                }
            }
            if let Some(ch) = text[i..].chars().next() {
                out.push(ch);
                i += ch.len_utf8();
            } else {
                break;
            }
        }
        out
    }

    fn unresolved_paste_ids(text: &str, agent: &AgentPane) -> Vec<u32> {
        let mut unresolved = Vec::new();
        for id in Self::collect_referenced_paste_ids(text) {
            if !agent.paste_registry.contains_key(&id) {
                unresolved.push(id);
            }
        }
        unresolved.sort_unstable();
        unresolved
    }

    pub(super) fn expand_paste_markers_checked(
        text: &str,
        agent: &AgentPane,
    ) -> Result<String, String> {
        let unresolved = Self::unresolved_paste_ids(text, agent);
        if unresolved.is_empty() {
            Ok(Self::expand_paste_markers(text, agent))
        } else {
            Err(format!(
                "Cannot send: pasted token payload missing for id(s): {}. Re-paste and try again.",
                unresolved
                    .iter()
                    .map(|id| id.to_string())
                    .collect::<Vec<_>>()
                    .join(", ")
            ))
        }
    }
}