a3s 0.10.3

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use serde_json::json;

use crate::cli::args::{MemoryArgs, MemoryCommand, OutputMode};
use crate::cli::context::InvocationContext;
use crate::cli::output::{render_value, usage_error};

pub(super) fn run(args: MemoryArgs, context: &InvocationContext) -> anyhow::Result<()> {
    if context.output_mode() == OutputMode::Jsonl {
        return Err(usage_error(
            "Code memory commands do not support JSONL output",
        ));
    }
    let path = crate::commands::config::memory_directory(context)?;
    match args.command {
        MemoryCommand::List(args) => list(path, args.query.as_deref().unwrap_or_default(), context),
        MemoryCommand::Stats => stats(path, context),
        MemoryCommand::Path => show_path(path, context),
    }
}

fn list(path: std::path::PathBuf, query: &str, context: &InvocationContext) -> anyhow::Result<()> {
    let data = crate::tui::memutil::load_panel_data(&path);
    let entries = data
        .entries
        .iter()
        .filter(|entry| {
            let content = memory_content(&data, entry);
            let haystack = format!("{} {} {}", entry.id, entry.tags.join(" "), content);
            matches_query(&haystack, query)
        })
        .collect::<Vec<_>>();
    let values = entries
        .iter()
        .map(|entry| {
            json!({
                "id": entry.id,
                "type": if entry.memory_type.is_empty() { "memory" } else { &entry.memory_type },
                "importance": entry.importance,
                "timestamp": entry.timestamp,
                "tags": entry.tags,
                "content": memory_content(&data, entry),
            })
        })
        .collect::<Vec<_>>();
    render_value(
        context.output_mode(),
        "code.memory.list",
        json!({"path": path, "query": query, "entries": values}),
        || {
            println!(
                "{} memory entr{} in {}",
                entries.len(),
                if entries.len() == 1 { "y" } else { "ies" },
                path.display()
            );
            for entry in entries {
                let content = memory_content(&data, entry);
                println!(
                    "{}\t{}\t{:.2}\t{}\t{}",
                    trim_column(&entry.id, 8),
                    if entry.memory_type.is_empty() {
                        "memory"
                    } else {
                        entry.memory_type.as_str()
                    },
                    entry.importance,
                    entry.timestamp.format("%Y-%m-%d"),
                    trim_column(&content.replace('\n', " "), 120)
                );
            }
        },
    )
}

fn stats(path: std::path::PathBuf, context: &InvocationContext) -> anyhow::Result<()> {
    let data = crate::tui::memutil::load_panel_data(&path);
    let stats = &data.graph.stats;
    let value = json!({
        "path": path,
        "entries": data.entries.len(),
        "graph": {
            "events": stats.events,
            "entities": stats.entities,
            "relations": stats.relations,
            "aliases": stats.aliases,
            "llmExtracted": stats.llm_extracted,
            "consolidated": stats.consolidated,
            "conflicts": stats.conflicts,
        },
        "tiers": {
            "short": stats.short,
            "mid": stats.mid,
            "long": stats.long,
            "forgetCandidates": stats.forget_candidates,
        },
    });
    render_value(context.output_mode(), "code.memory.stats", value, || {
        println!("memory: {}", path.display());
        println!("entries: {}", data.entries.len());
        println!(
            "graph: {} event(s) · {} entity(ies) · {} relation(s) · {} alias(es)",
            stats.events, stats.entities, stats.relations, stats.aliases
        );
        println!(
            "tiers: short {} · mid {} · long {} · forget candidates {}",
            stats.short, stats.mid, stats.long, stats.forget_candidates
        );
    })
}

fn show_path(path: std::path::PathBuf, context: &InvocationContext) -> anyhow::Result<()> {
    render_value(
        context.output_mode(),
        "code.memory.path",
        json!({"path": path, "exists": path.is_dir()}),
        || println!("{}", path.display()),
    )
}

fn memory_content(
    data: &crate::tui::memutil::MemPanelData,
    entry: &crate::tui::memutil::MemEntry,
) -> String {
    data.details
        .get(&entry.id)
        .map(|detail| detail.content.trim().to_string())
        .filter(|content| !content.is_empty())
        .unwrap_or_else(|| entry.content_lower.clone())
}

fn matches_query(text: &str, query: &str) -> bool {
    let query = query.trim().to_ascii_lowercase();
    query.is_empty() || text.to_ascii_lowercase().contains(&query)
}

fn trim_column(value: &str, width: usize) -> String {
    let mut output = value.chars().take(width).collect::<String>();
    if value.chars().count() > width && width >= 1 {
        output.pop();
        output.push('~');
    }
    output
}