daat-locus 0.1.1

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use ratatui::{
    style::{Color, Modifier, Style},
    text::{Line, Span},
};
use serde::{Deserialize, Serialize};

use crate::tool_ui::{
    AppAttentionUiAction, AppAttentionUiData, BrowserUiAction, BrowserUiData, glyph,
};

use super::primitives::{Cell, render_text_activity_lines};

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppAttentionActivityCell {
    pub title: String,
    pub body_lines: Vec<String>,
}

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BrowserActivityCell {
    pub title: String,
    pub body_lines: Vec<String>,
    pub url: Option<String>,
    pub line_count: Option<usize>,
    pub ref_count: Option<usize>,
}

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LiveBrowserActivityCell {
    pub title: String,
    pub body_lines: Vec<String>,
    pub url: Option<String>,
}

impl Cell for AppAttentionActivityCell {
    fn render_lines(&self) -> Vec<Line<'static>> {
        render_text_activity_lines(
            glyph::APP_ATTENTION,
            Color::LightBlue,
            &self.title,
            &self.body_lines,
            6,
            None,
        )
    }
}

impl Cell for BrowserActivityCell {
    fn render_lines(&self) -> Vec<Line<'static>> {
        let mut lines = vec![Line::from(vec![
            Span::styled(
                glyph::BROWSER,
                Style::default()
                    .fg(Color::LightBlue)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(
                format!(
                    "Captured URL: {}",
                    self.url
                        .as_deref()
                        .map(compact_browser_url)
                        .unwrap_or_else(|| "unknown".to_string())
                ),
                Style::default()
                    .fg(Color::White)
                    .add_modifier(Modifier::BOLD),
            ),
        ])];
        let mut stats = Vec::new();
        if let Some(line_count) = self.line_count {
            stats.push(format!("{line_count} lines"));
        }
        if let Some(ref_count) = self.ref_count {
            stats.push(format!("{ref_count} refs"));
        }
        if !stats.is_empty() {
            lines.push(Line::from(vec![
                Span::raw("   "),
                Span::styled(stats.join(" ยท "), Style::default().fg(Color::Gray)),
            ]));
        }
        lines
    }
}

impl Cell for LiveBrowserActivityCell {
    fn render_lines(&self) -> Vec<Line<'static>> {
        let title = self
            .url
            .as_deref()
            .map(|url| format!("Opening URL: {}", compact_browser_url(url)))
            .unwrap_or_else(|| self.title.clone());
        let mut lines = vec![Line::from(vec![
            Span::styled(
                glyph::BROWSER,
                Style::default()
                    .fg(Color::LightBlue)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(
                title,
                Style::default()
                    .fg(Color::LightBlue)
                    .add_modifier(Modifier::BOLD),
            ),
        ])];
        for line in self.body_lines.iter().take(1) {
            lines.push(Line::from(vec![
                Span::raw("   "),
                Span::styled(line.clone(), Style::default().fg(Color::Gray)),
            ]));
        }
        lines
    }
}

impl From<AppAttentionUiData> for AppAttentionActivityCell {
    fn from(data: AppAttentionUiData) -> Self {
        match data.action {
            AppAttentionUiAction::Focus => {
                let app = data.app.unwrap_or_else(|| "app".to_string());
                Self {
                    title: format!("Focused App: {app}"),
                    body_lines: Vec::new(),
                }
            }
            AppAttentionUiAction::PutAway => Self {
                title: "put away focused app".to_string(),
                body_lines: Vec::new(),
            },
        }
    }
}

impl From<BrowserUiData> for BrowserActivityCell {
    fn from(data: BrowserUiData) -> Self {
        Self {
            title: data.title,
            body_lines: data.body_lines,
            url: data.url,
            line_count: data.line_count,
            ref_count: data.ref_count,
        }
    }
}

impl From<BrowserUiData> for LiveBrowserActivityCell {
    fn from(data: BrowserUiData) -> Self {
        let title = match data.action {
            BrowserUiAction::OpenPage => data
                .url
                .as_deref()
                .map(|url| format!("Opening URL: {}", compact_browser_url(url)))
                .unwrap_or_else(|| "Opening Page".to_string()),
            BrowserUiAction::Wait => "Waiting for Page".to_string(),
            BrowserUiAction::Click => "Clicking Element".to_string(),
            BrowserUiAction::Fill => "Filling Element".to_string(),
            BrowserUiAction::Back => "Going Back".to_string(),
            BrowserUiAction::Forward => "Going Forward".to_string(),
            BrowserUiAction::Reload => "Reloading Page".to_string(),
            BrowserUiAction::ClosePage => "Closing Page".to_string(),
            BrowserUiAction::Snapshot => "Capturing Page".to_string(),
        };
        Self {
            title,
            body_lines: data.body_lines,
            url: data.url,
        }
    }
}

fn compact_browser_url(url: &str) -> String {
    const MAX_CHARS: usize = 88;
    let compact = url.trim().replace('\n', "");
    let mut chars = compact.chars();
    let head = chars.by_ref().take(MAX_CHARS).collect::<String>();
    if chars.next().is_some() {
        format!("{head}...")
    } else {
        head
    }
}