1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//! REPL status line renderer (Claude-Code parity).
//!
//! PMAT-CODE-STATUS-LINE-001: Claude Code renders a one-line status
//! strip at the bottom of the REPL showing model name, permission
//! mode, session cost, git branch, and cwd-relative directory. This
//! module ships the data struct + pure render function so TUI/REPL
//! call sites don't duplicate the formatting rules.
//!
//! # Example
//!
//! ```rust
//! use aprender_orchestrate::agent::status_line::StatusLine;
//!
//! let line = StatusLine {
//! model: "opus-4-7".into(),
//! mode: "bypassPermissions".into(),
//! cost_usd: 0.0234,
//! branch: Some("main".into()),
//! cwd_short: Some("~/src/aprender".into()),
//! };
//! let rendered = line.render();
//! assert!(rendered.contains("opus-4-7"));
//! assert!(rendered.contains("bypassPermissions"));
//! assert!(rendered.contains("$0.02"));
//! assert!(rendered.contains("main"));
//! ```
use std::path::Path;
use super::permission::PermissionMode;
/// Data captured by the REPL and rendered as a one-line status strip.
///
/// All fields are owned strings so the renderer is pure and
/// trivially testable without borrowing from the live session.
#[derive(Debug, Clone, PartialEq)]
pub struct StatusLine {
/// Model identifier (e.g. `opus-4-7`, `qwen2.5-coder-7b`).
pub model: String,
/// Permission-mode canonical identifier
/// (`default`/`plan`/`acceptEdits`/`bypassPermissions`).
pub mode: String,
/// Session cost in USD. Formatted to 2 decimal places in the
/// output.
pub cost_usd: f64,
/// Git branch, if the REPL is inside a repo.
pub branch: Option<String>,
/// Home-relative cwd display (e.g. `~/src/aprender`).
pub cwd_short: Option<String>,
}
impl StatusLine {
/// Render as a single-line string using the Claude-Code column
/// order: `model | [mode] | $cost | branch | cwd`.
///
/// Missing optional fields are elided (no empty cells).
pub fn render(&self) -> String {
let mut parts: Vec<String> = Vec::with_capacity(5);
parts.push(self.model.clone());
parts.push(format!("[{}]", self.mode));
parts.push(format!("${:.2}", self.cost_usd));
if let Some(branch) = self.branch.as_deref() {
parts.push(branch.to_string());
}
if let Some(cwd) = self.cwd_short.as_deref() {
parts.push(cwd.to_string());
}
parts.join(" | ")
}
/// Build a [`StatusLine`] from the live session primitives.
///
/// Pure — caller passes in already-resolved values so this is
/// trivially testable without touching git or the filesystem.
pub fn build(
model: impl Into<String>,
mode: PermissionMode,
cost_usd: f64,
branch: Option<String>,
cwd_short: Option<String>,
) -> Self {
Self { model: model.into(), mode: mode.to_string(), cost_usd, branch, cwd_short }
}
}
/// Collapse `$HOME` prefix in `path` to `~/` for display.
///
/// Returns `path.to_string_lossy()` when no home prefix matches.
pub fn short_cwd(path: &Path, home: Option<&Path>) -> String {
if let Some(home) = home {
if let Ok(stripped) = path.strip_prefix(home) {
if stripped.as_os_str().is_empty() {
return "~".to_string();
}
return format!("~/{}", stripped.display());
}
}
path.display().to_string()
}
#[cfg(test)]
mod tests;