lean_ctx/core/patterns/
ollama.rs1use crate::core::compressor::strip_ansi;
9
10pub fn compress(cmd: &str, output: &str) -> Option<String> {
11 let trimmed = output.trim();
12 if trimmed.is_empty() {
13 return Some("ollama: ok".to_string());
14 }
15
16 if cmd.contains(" list") || cmd.contains(" ls") {
17 return Some(compress_table(trimmed, "model(s)"));
18 }
19 if cmd.contains(" ps") {
20 return Some(compress_table(trimmed, "running"));
21 }
22 if cmd.contains(" pull") || cmd.contains(" push") {
23 return Some(compress_transfer(trimmed));
24 }
25
26 None
28}
29
30fn compress_table(output: &str, noun: &str) -> String {
33 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
34 if lines.len() < 2 {
35 return output.to_string();
36 }
37 let header = split_cols(lines[0]);
38 let drop = header.iter().position(|c| c.eq_ignore_ascii_case("ID"));
39
40 let mut rows: Vec<String> = Vec::new();
41 for line in &lines[1..] {
42 let mut cols = split_cols(line);
43 if let Some(i) = drop
44 && i < cols.len()
45 {
46 cols.remove(i);
47 }
48 rows.push(cols.join(" "));
49 }
50 format!("ollama: {} {}\n{}", rows.len(), noun, rows.join("\n"))
51}
52
53fn compress_transfer(output: &str) -> String {
55 let mut layers = 0usize;
56 let mut success = false;
57 let mut errors: Vec<String> = Vec::new();
58
59 for raw in output.lines() {
60 let line = strip_ansi(raw);
61 let line = line.trim();
62 if line.is_empty() {
63 continue;
64 }
65 if (line.starts_with("pulling") || line.starts_with("pushing")) && line.contains('%') {
66 layers += 1;
67 } else if line == "success" {
68 success = true;
69 } else if line.contains("Error") || line.contains("error") {
70 errors.push(line.to_string());
71 }
72 }
73
74 if !errors.is_empty() {
75 return format!("ollama: FAILED\n {}", errors.join("\n "));
76 }
77 if success {
78 return format!("ollama: success ({layers} layers)");
79 }
80 format!("ollama: {layers} layers")
81}
82
83fn split_cols(line: &str) -> Vec<String> {
86 let mut cols = Vec::new();
87 let mut cur = String::new();
88 let mut spaces = 0;
89 for ch in line.trim().chars() {
90 if ch == ' ' {
91 spaces += 1;
92 continue;
93 }
94 if spaces >= 2 && !cur.is_empty() {
95 cols.push(std::mem::take(&mut cur));
96 } else if spaces == 1 && !cur.is_empty() {
97 cur.push(' ');
98 }
99 spaces = 0;
100 cur.push(ch);
101 }
102 if !cur.is_empty() {
103 cols.push(cur);
104 }
105 cols
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 const LIST: &str = "NAME ID SIZE MODIFIED\nllama3.2:latest a80c4f17acd5 2.0 GB 3 days ago\nqwen2.5-coder:7b 2b0496514337 4.7 GB 2 weeks ago\n";
113
114 #[test]
115 fn list_drops_id_keeps_name_size() {
116 let r = compress("ollama list", LIST).unwrap();
117 assert!(r.contains("2 model(s)"), "{r}");
118 assert!(r.contains("llama3.2:latest"), "{r}");
119 assert!(r.contains("2.0 GB"), "{r}");
120 assert!(r.contains("3 days ago"), "keeps multi-word column: {r}");
121 assert!(!r.contains("a80c4f17acd5"), "drops ID hash: {r}");
122 }
123
124 #[test]
125 fn pull_collapses_progress() {
126 let out = "pulling manifest\npulling aabbccdd... 100% ▕████████▏ 2.0 GB\npulling 1234efgh... 100% ▕██▏ 1.2 KB\nverifying sha256 digest\nwriting manifest\nsuccess\n";
127 let r = compress("ollama pull llama3.2", out).unwrap();
128 assert_eq!(r, "ollama: success (2 layers)");
129 }
130
131 #[test]
132 fn run_is_not_compressed() {
133 assert!(
134 compress(
135 "ollama run llama3.2 'hi'",
136 "The capital of France is Paris."
137 )
138 .is_none()
139 );
140 }
141
142 #[test]
143 fn empty_is_ok() {
144 assert_eq!(compress("ollama list", "").unwrap(), "ollama: ok");
145 }
146}