//! office-tui — terminal dashboard for a running (graphical or --headless) //! office sim. Read-only cockpit polling the company's work/ folder. //! //! header day/clock + tasks-done + live-meeting + build-gate state //! left rail who's working on what today (task id, status, %) //! tab: board full assignment table + selected task details //! tab: desk chat who said what to whom, face-to-face (work/log/desk.md) //! tab: meetings ended meeting notes (topic + who-said-what) //! tab: mail per-person inbox (incl. build-gate failure mail) //! tab: docs work/project tree + file viewer (c in input mode → cmd) //! command input `assign T3 Grace Kim` etc → work/commands.txt //! //! Run: village/target/debug/office-tui companies/algodojo use office::company::{slug, CompanyCfg}; use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Paragraph, Wrap}, DefaultTerminal, Frame, }; use std::path::{Path, PathBuf}; const TABS: [&str; 7] = ["board", "desk chat", "meetings", "mail", "docs", "commits", "llm"]; const HELP: &str = "tab/1-6 page · ↑↓ select · PgUp/PgDn scroll detail · ←→ person (mail) · c command · q quit"; fn main() { let given = std::env::args() .nth(1) .unwrap_or_else(|| "companies/algodojo".into()); let company_dir = resolve_dir(&given); let cfg = CompanyCfg::load(&company_dir).unwrap_or_else(|e| { eprintln!("error: {e}"); std::process::exit(1); }); let mut term = ratatui::init(); let app = &mut App { company_dir, cfg, tab: 0, sel: [0; 7], person_sel: 0, detail_scroll: [0; 7], doc_files: Vec::new(), meeting_files: Vec::new(), mail_files: Vec::new(), commit_lines: Vec::new(), llm_flights: Vec::new(), input: None, flash: String::new(), }; let res = run(&mut term, app); ratatui::restore(); if let Err(e) = res { eprintln!("tui error: {e}"); } } fn resolve_dir(given: &str) -> PathBuf { let given = Path::new(given); if given.join("company.toml").exists() { return given.to_path_buf(); } let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); for _ in 0..6 { if dir.join(given).join("company.toml").exists() { return dir.join(given); } match dir.parent() { Some(p) => dir = p.to_path_buf(), None => break, } } given.to_path_buf() } fn wks(app: &App) -> PathBuf { app.company_dir.join("work") } // ────────────────────────────────── snapshot ──────────────────────────────── struct TaskRow { id: String, assignee: String, status: String, progress: u32, } struct Snapshot { day: u32, hhmm: String, meeting_live: Option, tasks: Vec, gate: Option<&'static str>, } fn snap(app: &App) -> Snapshot { let (day, minutes, tasks) = read_board(&wks(app)); let hhmm = minutes_to_clock(minutes); let meeting_live = std::fs::read_to_string(wks(app).join("log/meeting_open.md")) .ok() .map(|t| { t.lines() .next() .unwrap_or("") .trim_start_matches('#') .trim() .to_string() }) .filter(|t| !t.is_empty()); let gate = std::fs::read_to_string(wks(app).join("project/verify_report.md")) .ok() .map(|t| if t.contains("FAIL ") { "red" } else { "green" }); Snapshot { day, hhmm, meeting_live, tasks, gate } } fn read_board(root: &Path) -> (u32, f32, Vec) { let text = std::fs::read_to_string(root.join("tasks.json")).unwrap_or_default(); let v: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); let day = v["day"].as_u64().unwrap_or(1) as u32; let sim_minutes = v["sim_minutes"].as_f64().unwrap_or(540.0) as f32; let tasks = v["tasks"] .as_array() .unwrap_or(&Vec::new()) .iter() .map(|t| TaskRow { id: t["id"].as_str().unwrap_or("?").into(), assignee: t["assignee"] .as_str() .unwrap_or("unassigned") .to_string(), status: t["status"].as_str().unwrap_or("Backlog").into(), progress: t["progress"].as_u64().unwrap_or(0) as u32, }) .collect(); (day, sim_minutes, tasks) } fn minutes_to_clock(sim_minutes: f32) -> String { let m = sim_minutes.rem_euclid(1440.0); format!("{:02}:{:02}", (m / 60.0) as u32, (m % 60.0) as u32) } // ─────────────────────────────────── main loop ────────────────────────────── struct App { company_dir: PathBuf, cfg: CompanyCfg, tab: usize, sel: [usize; 7], person_sel: usize, detail_scroll: [u16; 7], doc_files: Vec, meeting_files: Vec, mail_files: Vec, commit_lines: Vec, llm_flights: Vec, input: Option, flash: String, } fn run(term: &mut DefaultTerminal, app: &mut App) -> Result<(), Box> { loop { term.draw(|f| draw(f, app))?; if !event::poll(std::time::Duration::from_millis(1000))? { continue; } if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { let typing = app.input.is_some(); handle_key(app, key.code, typing); } } } } fn handle_key(app: &mut App, code: KeyCode, typing: bool) { match code { KeyCode::Char('c') if !typing => app.input = Some(String::new()), KeyCode::Esc => app.input = None, KeyCode::Enter if typing => submit_command(app), KeyCode::Backspace if typing => { if let Some(s) = app.input.as_mut() { s.pop(); } } KeyCode::Char(ch) if typing => { if let Some(s) = app.input.as_mut() { if s.len() < 200 { s.push(ch); } } } KeyCode::Tab if !typing => { app.tab = (app.tab + 1) % TABS.len(); app.detail_scroll[app.tab] = 0; } KeyCode::Char(d) if !typing && ('1'..='7').contains(&d) => { app.tab = d as usize - '1' as usize; // tabs indexed 0..=5 app.detail_scroll[app.tab] = 0; } KeyCode::Up if !typing => { app.sel[app.tab] = app.sel[app.tab].saturating_sub(1); app.detail_scroll[app.tab] = 0; } KeyCode::Down if !typing => { app.sel[app.tab] += 1; app.detail_scroll[app.tab] = 0; } KeyCode::PageDown if !typing => { app.detail_scroll[app.tab] = (app.detail_scroll[app.tab] + 10).min(500); } KeyCode::PageUp if !typing => { app.detail_scroll[app.tab] = app.detail_scroll[app.tab].saturating_sub(10); } KeyCode::Left if !typing && app.tab == 3 => { app.person_sel = app.person_sel.saturating_sub(1); } KeyCode::Right if !typing && app.tab == 3 => { app.person_sel += 1; } KeyCode::Char('q') if !typing => std::process::exit(0), _ => {} } } fn submit_command(app: &mut App) { let Some(line) = app.input.take() else { return }; let line = line.trim().to_string(); if line.is_empty() { return; } use std::io::Write; match std::fs::OpenOptions::new() .create(true) .append(true) .open(wks(app).join("commands.txt")) { Ok(mut f) => { let _ = writeln!(f, "{line}"); app.flash = format!("→ sent: {line}"); } Err(e) => app.flash = format!("send failed: {e}"), } } // ───────────────────────────────────── draw ───────────────────────────────── fn draw(f: &mut Frame, app: &mut App) { let s = snap(app); app.doc_files = tree_list(&wks(app).join("project")); app.meeting_files = fs_list(&wks(app).join("meetings")); app.commit_lines = office::gitrepo::log(&wks(app).join("project"), 60) .unwrap_or_default(); if let Ok(text) = std::fs::read_to_string(wks(app).join("log/llm.json")) { if let Ok(serde_json::Value::Array(v)) = serde_json::from_str(&text) { app.llm_flights = v; } } let outer = Layout::vertical([ Constraint::Length(3), Constraint::Min(8), Constraint::Length(2), ]) .split(f.area()); let body = Layout::horizontal([Constraint::Length(36), Constraint::Min(30)]).split(outer[1]); draw_header(f, outer[0], app, &s); draw_people(f, body[0], app, &s); draw_pane(f, body[1], app, &s); draw_footer(f, outer[2], app); } fn title_block(title: &str, color: Color) -> Block<'static> { Block::bordered().title(Span::styled( format!(" {title} "), Style::default().fg(color).bold(), )) } fn body_lines(text: &str) -> Vec> { text.lines() .map(|l| { if l.contains("FAIL") { Line::from(Span::styled(l.to_string(), Style::default().fg(Color::Red))) } else if l.contains("PASS") { Line::from(Span::styled(l.to_string(), Style::default().fg(Color::Green))) } else if l.starts_with('#') { Line::from(Span::styled( l.to_string(), Style::default().fg(Color::Cyan).bold(), )) } else { Line::from(l.to_string()) } }) .collect() } fn draw_header(f: &mut Frame, area: Rect, app: &App, s: &Snapshot) { let done = s.tasks.iter().filter(|t| t.status == "Done").count(); let gate = match s.gate { Some("green") => Span::styled("GATE green ", Style::default().fg(Color::Green).bold()), Some("red") => Span::styled("GATE red ", Style::default().fg(Color::Red).bold()), _ => Span::styled("GATE — ", Style::default().fg(Color::DarkGray)), }; let meeting = match &s.meeting_live { Some(topic) => Span::styled( format!("MEETING LIVE: {topic} "), Style::default().fg(Color::Yellow).bold(), ), None => Span::styled("no meeting ", Style::default().fg(Color::DarkGray)), }; let header = Line::from(vec![ Span::styled( app.cfg.company.name.clone(), Style::default().fg(Color::Cyan).bold(), ), Span::styled( format!(" DAY {} {} ", s.day, s.hhmm), Style::default().fg(Color::White).bold(), ), Span::styled( format!("{}/{} done ", done, s.tasks.len()), Style::default().fg(Color::Gray), ), meeting, gate, ]); let tabs = Line::from( TABS.iter() .enumerate() .map(|(i, t)| { let on = i == app.tab; Span::styled( format!("({}) {} ", i + 1, t), Style::default() .fg(if on { Color::Cyan } else { Color::DarkGray }) .add_modifier(if on { Modifier::BOLD } else { Modifier::empty() }), ) }) .collect::>(), ); f.render_widget( Paragraph::new(vec![header]).block(title_block(" office · live sim ", Color::Cyan)), area, ); f.render_widget( tabs, Rect::new(area.x + 1, area.y + 2, area.width.saturating_sub(2), 1), ); } fn rail_name(name: &str) -> String { let parts: Vec<&str> = name.split_whitespace().collect(); match parts.as_slice() { [first, last] => format!("{first} {}", &last[..1]), _ => name.chars().take(14).collect(), } } fn status_icon(status: &str) -> &'static str { match status { "InReview" => "⇄", "Done" => "✔", "ChangesRequested" => "↩", _ => "→", } } fn status_color(status: &str) -> Color { match status { "Done" => Color::DarkGray, "InReview" => Color::Magenta, "ChangesRequested" => Color::Red, "InProgress" => Color::Green, _ => Color::Gray, } } fn draw_people(f: &mut Frame, area: Rect, app: &App, s: &Snapshot) { let mut lines = Vec::new(); for p in &app.cfg.people { let mine: Vec<&TaskRow> = s.tasks.iter().filter(|t| t.assignee == p.name).collect(); let star = if p.is_manager { "★" } else { " " }; let name_style = Style::default() .fg(if p.is_manager { Color::Yellow } else { Color::Cyan }) .add_modifier(Modifier::BOLD); lines.push(Line::from(vec![ Span::styled(format!("{star}"), name_style), Span::styled(format!(" {:13}", rail_name(&p.name)), name_style), Span::styled( format!(" {}", p.role.chars().take(17).collect::()), Style::default().fg(Color::Gray), ), ])); let sub = if mine.is_empty() { Line::from(Span::styled( " · idle / between tasks", Style::default().fg(Color::DarkGray), )) } else { Line::from( mine.iter() .map(|t| { Span::styled( format!( " {}{} {:>3}% ", status_icon(&t.status), t.id, t.progress ), Style::default().fg(status_color(&t.status)), ) }) .collect::>(), ) }; lines.push(sub); } f.render_widget( Paragraph::new(lines).block(title_block( " people · on what today", Color::Cyan, )), area, ); } fn draw_pane(f: &mut Frame, area: Rect, app: &mut App, s: &Snapshot) { let chunks = Layout::vertical([Constraint::Percentage(62), Constraint::Percentage(38)]) .split(area); match app.tab { 0 => draw_board(f, chunks[0], app, s), 1 => draw_desk(f, chunks[0], app), 2 => draw_meetings(f, chunks[0], app), 3 => draw_mail(f, chunks[0], app), 4 => draw_docs(f, chunks[0], app), 5 => draw_commits(f, chunks[0], app), _ => draw_llm(f, chunks[0], app), } let (detail, title) = match app.tab { 0 => (task_detail(app), " details · notes + description"), 1 => (badge_detail(app), " who is in earshot · live"), 2 => (meeting_detail(app), " meeting record"), 3 => (mail_detail(app), " email text"), 4 => (doc_detail(app), " document viewer"), 5 => (commit_detail(app), " commit detail"), _ => (llm_detail(app), " gpu activity prompt+output"), }; f.render_widget( Paragraph::new(body_lines(&detail)) .wrap(Wrap { trim: false }) .scroll((app.detail_scroll[app.tab], 0)) .block(title_block(title, Color::Magenta)), chunks[1], ); } fn select_index(sel: usize, len: usize) -> Option { if len == 0 { return None; } Some(sel.min(len - 1)) } fn rendered( f: &mut Frame, area: Rect, title: &str, items: Vec, sel: Option, color: Color, ) { let painted: Vec = items .into_iter() .enumerate() .map(|(i, item)| { let is_sel = Some(i) == sel; let mut spans = vec![if is_sel { Span::styled("► ", Style::default().fg(Color::Cyan).bold()) } else { Span::raw(" ") }]; spans.push(if is_sel { Span::styled( item, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), ) } else { Span::styled(item, Style::default()) }); Line::from(spans) }) .collect(); f.render_widget(Paragraph::new(painted).block(title_block(title, color)), area); } fn draw_board(f: &mut Frame, area: Rect, app: &App, s: &Snapshot) { let items: Vec = s .tasks .iter() .map(|t| { format!( "{} {:3} {:12} {:>3}% {}", status_icon(&t.status), t.id, rail_name(&t.assignee), t.progress, t.id, ) }) .collect(); rendered( f, area, " board · assignments + status + progress", items, select_index(app.sel[0], s.tasks.len()), Color::Blue, ); } fn task_detail(app: &App) -> String { let idx = select_index(app.sel[0], board_len(app)).unwrap_or(0); board_detail(app, idx) } fn board_len(app: &App) -> usize { let text = std::fs::read_to_string(wks(app).join("tasks.json")).unwrap_or_default(); serde_json::from_str::(&text) .ok() .and_then(|v| v["tasks"].as_array().map(|a| a.len())) .unwrap_or(0) } fn board_detail(app: &App, idx: usize) -> String { let Ok(text) = std::fs::read_to_string(wks(app).join("tasks.json")) else { return "(no tasks.json yet)".into(); }; let Ok(v) = serde_json::from_str::(&text) else { return "(bad tasks.json)".into(); }; let Some(tasks) = v["tasks"].as_array() else { return String::new() }; let Some(t) = tasks.get(idx) else { return String::new() }; let mut out = format!( "{} \"{}\" — {}\nassignee {} · {}% · status {}\n", t["id"].as_str().unwrap_or("?"), t["title"].as_str().unwrap_or(""), t["area"].as_str().unwrap_or(""), t["assignee"].as_str().unwrap_or("unassigned"), t["progress"].as_u64().unwrap_or(0), t["status"].as_str().unwrap_or("?"), ); if let Some(d) = t["due_day"].as_u64() { out.push_str(&format!("due day {d}\n")); } if let Some(concern) = t["concern"].as_str() { out.push_str(&format!("⚠ {concern}\n")); } for note in t["notes"].as_array().unwrap_or(&Vec::new()) { if let Some(n) = note.as_str() { out.push_str(&format!(" · {n}\n")); } } if let Some(desc) = t["description"].as_str() { out.push_str(&format!("\n{desc}")); } out } fn draw_desk(f: &mut Frame, area: Rect, app: &App) { let text = std::fs::read_to_string(wks(app).join("log/desk.md")).unwrap_or_default(); let lines: Vec = text .lines() .rev() .take(300) .filter(|l| !l.is_empty()) .map(render_chat_line) .collect(); f.render_widget( Paragraph::new(lines).block(title_block( " desk chat · face-to-face, latest first", Color::Cyan, )), area, ); } fn render_chat_line(line: &str) -> Line<'static> { let (stamp, rest) = match line.strip_prefix('[').and_then(|a| a.find(']').map(|p| (a, p))) { Some((a, p)) => (a[..p].to_string(), a[p + 1..].trim()), None => (String::new(), line), }; let (from, right) = rest.split_once(" → ").unwrap_or((rest, "")); let (to, msg) = right.split_once(": ").unwrap_or((right, "")); Line::from(vec![ Span::styled(format!("{stamp:>14} "), Style::default().fg(Color::DarkGray)), Span::styled(format!("{from:13}"), Style::default().fg(Color::Cyan)), Span::styled("› ", Style::default().fg(Color::DarkGray)), Span::styled(format!("{:12}", to), Style::default().fg(Color::Magenta)), Span::styled(msg.to_string(), Style::default()), ]) } /// desk-tab detail: latest utterance big, so users see one exchange clearly. fn badge_detail(app: &App) -> String { let text = std::fs::read_to_string(wks(app).join("log/desk.md")).unwrap_or_default(); let latest: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect(); let n = latest.len(); match latest.len() { 0 => "(nobody has spoken yet — standup soon)".into(), _ => { let mut out = format!("({n} utterances logged)\n\n{}", latest[n - 1]); if n > 1 { out.push_str(&format!("\n{}\n{}", latest[n - 2], latest[n.min(6) - 1])); } out } } } fn draw_meetings(f: &mut Frame, area: Rect, app: &mut App) { rendered( f, area, " meetings · ended notes", app.meeting_files.clone(), select_index(app.sel[2], app.meeting_files.len()), Color::Magenta, ); } fn meeting_detail(app: &App) -> String { match select_index(app.sel[2], app.meeting_files.len()) { Some(i) => { let p = wks(app).join("meetings").join(&app.meeting_files[i]); std::fs::read_to_string(&p).unwrap_or_else(|_| "(unreadable)".into()) } None => "(no meetings have ended yet — a live one writes to log/meeting_open.md)".into(), } } fn draw_mail(f: &mut Frame, area: Rect, app: &mut App) { let n = app.cfg.people.len(); app.person_sel = app.person_sel.min(n.saturating_sub(1)); let person = &app.cfg.people[app.person_sel]; let dir = wks(app).join("mail").join(slug(&person.name)); app.mail_files = fs_list(&dir); let title = format!( " mail · {} [{} msgs] (←→ change person)", person.name, app.mail_files.len() ); let files = app.mail_files.clone(); rendered( f, area, &title, files, select_index( app.sel[3], app.mail_files.len(), ), Color::Magenta, ); } fn mail_detail(app: &App) -> String { let person = &app.cfg.people[app.person_sel.min(app.cfg.people.len().saturating_sub(1))]; let dir = wks(app).join("mail").join(slug(&person.name)); let files = fs_list(&dir); match select_index(app.sel[3], files.len()) { Some(i) => std::fs::read_to_string(dir.join(&files[i])) .unwrap_or_else(|_| "(unreadable)".into()), None => format!("no mail yet for {}", person.name), } } fn draw_docs(f: &mut Frame, area: Rect, app: &mut App) { rendered( f, area, " docs · work/project tree", app.doc_files.clone(), select_index(app.sel[4], app.doc_files.len()), Color::Blue, ); } fn doc_detail(app: &App) -> String { match select_index(app.sel[4], app.doc_files.len()) { Some(i) => { let path = wks(app).join("project").join(&app.doc_files[i]); match std::fs::read(&path) { Ok(bytes) => match String::from_utf8(bytes) { Ok(text) if text.len() <= 20000 => text, Ok(text) => { let head: String = text.chars().take(20000).collect(); format!("{head}\n…(truncated)") } Err(_) => "(binary file — open outside the TUI)".into(), }, Err(_) => "(unreadable)".into(), } } None => "(project tree empty — agents haven't committed files yet)".into(), } } /// sorted, flat, recursive file list of dir (relative paths) fn tree_list(dir: &Path) -> Vec { let mut out = Vec::new(); let mut stack = vec![PathBuf::from("")]; while let Some(rel) = stack.pop() { if let Ok(entries) = std::fs::read_dir(dir.join(&rel)) { for e in entries.flatten() { let name = e.file_name().to_string_lossy().to_string(); if name.starts_with('.') { continue; } let child = if rel.as_os_str().is_empty() { PathBuf::from(name) } else { rel.join(&name) }; if e.path().is_dir() { stack.push(child); } else { out.push(child.to_string_lossy().replace('\\', "/")); } } } } out.sort(); out } fn fs_list(dir: &Path) -> Vec { let mut out = Vec::new(); if let Ok(entries) = std::fs::read_dir(dir) { for e in entries.flatten() { let name = e.file_name().to_string_lossy().to_string(); if name.starts_with('.') { continue; } out.push(name.trim_end_matches(".read").to_string()); } } out.sort(); out } fn draw_footer(f: &mut Frame, area: Rect, app: &mut App) { let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(area); let hint = match &app.input { Some(buf) => Line::from(vec![ Span::styled(" c » ", Style::default().fg(Color::Yellow).bold()), Span::styled( buf.clone(), Style::default().fg(Color::White).add_modifier(Modifier::BOLD), ), Span::styled("▏", Style::default().fg(Color::Yellow)), Span::styled(" Enter=send · Esc=cancel", Style::default().fg(Color::DarkGray)), ]), None => Line::from(Span::styled( format!(" {HELP}"), Style::default().fg(Color::DarkGray), )), }; f.render_widget(hint, rows[0]); if !app.flash.is_empty() { f.render_widget( Line::from(Span::styled( app.flash.clone(), Style::default().fg(Color::Green), )), rows[1], ); } } fn draw_commits(f: &mut Frame, area: Rect, app: &mut App) { let items: Vec = app.commit_lines.clone(); rendered( f, area, " commits · who did what, when (authored history)", items, select_index(app.sel[5], app.commit_lines.len()), Color::Yellow, ); } fn commit_detail(app: &App) -> String { if app.commit_lines.is_empty() { return "(no commits yet)".into(); } let idx = select_index(app.sel[5], app.commit_lines.len()).unwrap_or(0); let hash: String = app.commit_lines[idx] .split('›') .next() .unwrap_or_default() .trim() .split(' ') .next() .unwrap_or_default() .to_string(); // show detail via the shared git helper office::gitrepo::show(&wks(app).join("project"), &hash) .unwrap_or_else(|| app.commit_lines[idx].clone()) } // ─────────────────────────────── LLM activity tab ─────────────────────────── const STATUS_ICON_LLM: [(&str, &str, ratatui::style::Color); 5] = [ ("queued", "⏳", ratatui::style::Color::Gray), ("computing", "✱", ratatui::style::Color::Yellow), ("discussing", "✱", ratatui::style::Color::Yellow), ("done", "✔", ratatui::style::Color::Green), ("failed", "✖", ratatui::style::Color::Red), ]; fn llm_row_vol(row: &serde_json::Value) -> (String, &'static str, ratatui::style::Color) { let status = row["status"].as_str().unwrap_or(""); for (tag, icon, color) in STATUS_ICON_LLM { if status.starts_with(tag) { return (status.to_string(), icon, color); } } (status.to_string(), "?", ratatui::style::Color::Gray) } fn stamp_of(row: &serde_json::Value, now: f64) -> String { let sec = row["started"].as_f64().unwrap_or(0.0); let age = (now - sec).max(0.0) as u64; format!("{:>3}s ago", age.min(999)) } fn draw_llm(f: &mut Frame, area: Rect, app: &mut App) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs_f64()) .unwrap_or(0.0); let items: Vec = app .llm_flights .iter() .map(|row| { let (status, icon, _c) = llm_row_vol(row); let person = row["person"].as_str().unwrap_or("?"); let kind = row["kind"].as_str().unwrap_or(""); let model = row["model"].as_str().unwrap_or("—"); let model_short: String = model .split(']') .next() .unwrap_or("queued") .to_string(); format!( "{} {:>7} {:18} {:14} {}", icon, stamp_of(row, now), kind, rail_name(person), if status.starts_with("queued") { "⌁ waiting".to_string() } else { model_short } ) }) .collect(); rendered( f, area, " llm · what the GPUs are generating, for whom (live)", items, select_index(app.sel[6], app.llm_flights.len()), ratatui::style::Color::Yellow, ); } fn llm_detail(app: &App) -> String { match select_index(app.sel[6], app.llm_flights.len()) { Some(i) => { let row = &app.llm_flights[i]; let prompt = row["prompt"].as_str().unwrap_or(""); let reply = row["reply"].as_str().unwrap_or(""); format!( "{} — {} on {}\nStatus: {}\n\n── prompt ──\n{}\n\n── model output ──\n{}", row["person"].as_str().unwrap_or("?"), row["kind"].as_str().unwrap_or(""), row["model"].as_str().unwrap_or("queued"), row["status"].as_str().unwrap_or(""), prompt, reply ) .replace("\\n", "\n") } None => "(no LLM jobs recorded yet)".into(), } }