aico/commands/
last.rs

1use crate::console::is_stdout_terminal;
2use crate::exceptions::AicoError;
3use crate::models::{DisplayItem, MessagePairJson, MessageWithId};
4use crate::session::Session;
5use std::io::Write;
6
7pub fn run(
8    index_str: String,
9    prompt: bool,
10    verbatim: bool,
11    recompute: bool,
12    json: bool,
13) -> Result<(), AicoError> {
14    let session = Session::load_active()?;
15    let resolved_idx = session.resolve_pair_index(&index_str)?;
16
17    let (user_rec, asst_rec, user_id, asst_id) = session.fetch_pair(resolved_idx)?;
18
19    if json {
20        let output = MessagePairJson {
21            pair_index: resolved_idx,
22            user: MessageWithId {
23                record: user_rec,
24                id: user_id,
25            },
26            assistant: MessageWithId {
27                record: asst_rec,
28                id: asst_id,
29            },
30        };
31
32        let mut stdout = std::io::stdout();
33        let res = if crate::console::is_stdout_terminal() {
34            serde_json::to_writer_pretty(&mut stdout, &output)
35        } else {
36            serde_json::to_writer(&mut stdout, &output)
37        };
38
39        if let Err(e) = res
40            && !e.is_io()
41        {
42            return Err(AicoError::Serialization(e));
43        }
44        let _ = writeln!(stdout);
45        return Ok(());
46    }
47
48    if prompt {
49        if recompute {
50            return Err(AicoError::InvalidInput(
51                "--recompute cannot be used with --prompt.".into(),
52            ));
53        }
54        print!("{}", user_rec.content);
55        return Ok(());
56    }
57
58    if verbatim {
59        print!("{}", asst_rec.content);
60        return Ok(());
61    }
62
63    let is_tty = is_stdout_terminal();
64
65    // 1. Resolve structured content
66    // We parse if recompute is requested OR if derived content is missing (fallback for legacy or broken history)
67    let (unified_diff, display_items, warnings) = match (&asst_rec.derived, recompute) {
68        (Some(derived), false) => (
69            derived.unified_diff.clone(),
70            derived
71                .display_content
72                .clone()
73                .unwrap_or_else(|| vec![DisplayItem::Markdown(asst_rec.content.clone())]),
74            vec![],
75        ),
76        _ => {
77            use crate::diffing::parser::StreamParser;
78
79            let mut parser = StreamParser::new(&session.context_content);
80            let gated_content = if asst_rec.content.ends_with('\n') {
81                asst_rec.content.clone()
82            } else {
83                format!("{}\n", asst_rec.content)
84            };
85            parser.feed(&gated_content);
86
87            let (diff, items, warnings) = parser.final_resolve(&session.root);
88
89            (Some(diff), items, warnings)
90        }
91    };
92
93    // 2. Render based on TTY and intent
94    if is_tty {
95        let width = crate::console::get_terminal_width();
96        let mut engine = crate::ui::markdown_streamer::MarkdownStreamer::new();
97        engine.set_width(width);
98        engine.set_margin(0);
99
100        let mut stdout = std::io::stdout();
101        for item in &display_items {
102            match item {
103                DisplayItem::Markdown(m) => {
104                    let _ = engine.print_chunk(&mut stdout, m);
105                }
106                DisplayItem::Diff(d) => {
107                    let _ = engine.print_chunk(&mut stdout, "\n~~~~~diff\n");
108                    let _ = engine.print_chunk(&mut stdout, d);
109                    let _ = engine.print_chunk(&mut stdout, "\n~~~~~\n");
110                }
111            }
112        }
113        let _ = engine.flush(&mut stdout);
114    } else {
115        // Composable output (pipes) - Piped Output Contract
116        // Strict for Diff mode, Flexible for others.
117        if matches!(asst_rec.mode, crate::models::Mode::Diff) {
118            if let Some(diff) = unified_diff {
119                print!("{}", diff);
120            }
121        } else {
122            // Flexible: Prefer diff if parsed, else fallback to raw content.
123            if let Some(diff) = unified_diff {
124                if !diff.is_empty() {
125                    print!("{}", diff);
126                } else {
127                    print!("{}", asst_rec.content);
128                }
129            } else {
130                print!("{}", asst_rec.content);
131            }
132        }
133    }
134
135    if !warnings.is_empty() {
136        eprintln!("\nWarnings:");
137        for w in warnings {
138            eprintln!("{}", w);
139        }
140    }
141
142    Ok(())
143}