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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
use std::path::{Path, PathBuf};
use crate::agent::tools::background::BackgroundStore;
use crate::agent::tools::bg_shell::BackgroundShellStore;
use crate::session::Session;
pub struct StatusLine;
fn fmt_tokens(n: u64) -> String {
if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{}k", n / 1000)
} else {
n.to_string()
}
}
/// Find the current git branch for `start`, walking up parent
/// directories until we hit a `.git` entry (file for worktrees,
/// directory for the main checkout) or the filesystem root. Returns
/// `None` when the directory isn't inside a git working tree, or
/// when `.git/HEAD` is unreadable / detached / malformed (the status
/// line is informational, not a git porcelain — we just omit the
/// segment in those cases).
fn git_branch(start: &Path) -> Option<String> {
let head_path = find_git_head(start)?;
let head = std::fs::read_to_string(head_path).ok()?;
let head = head.trim();
head.strip_prefix("ref: refs/heads/").map(|b| b.to_string())
}
fn find_git_head(start: &Path) -> Option<PathBuf> {
let mut cur: PathBuf = start.to_path_buf();
loop {
let dot_git = cur.join(".git");
if dot_git.is_dir() {
return Some(dot_git.join("HEAD"));
}
if dot_git.is_file() {
// Worktree pointer: `gitdir: <path>` → HEAD lives there.
let txt = std::fs::read_to_string(&dot_git).ok()?;
let gitdir = txt.trim().strip_prefix("gitdir: ")?;
return Some(PathBuf::from(gitdir).join("HEAD"));
}
if !cur.pop() {
return None;
}
}
}
/// Cached wrapper around [`git_branch`]. `StatusLine::render` runs on every
/// keystroke, and the raw `.git/HEAD` directory walk it did there is
/// synchronous filesystem I/O — repeated per painted frame, it froze the UI on
/// slow storage / large repos (dirge-vuzz). The branch only changes on
/// checkout, so cache it per working-dir for a few seconds: the FS walk now
/// runs at most once every `TTL`, not once per frame. (The background
/// `gitstatus` poller already refreshes on its own cadence; this just keeps the
/// status line's own lookup off the hot path.)
fn cached_git_branch(start: &Path) -> Option<String> {
use std::sync::Mutex;
use std::time::{Duration, Instant};
const TTL: Duration = Duration::from_secs(3);
static CACHE: Mutex<Option<(Instant, PathBuf, Option<String>)>> = Mutex::new(None);
let mut guard = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some((at, dir, branch)) = guard.as_ref()
&& dir.as_path() == start
&& at.elapsed() < TTL
{
return branch.clone();
}
let fresh = git_branch(start);
*guard = Some((Instant::now(), start.to_path_buf(), fresh.clone()));
fresh
}
impl StatusLine {
#[allow(clippy::too_many_arguments)]
pub fn render(
session: &Session,
is_running: bool,
_spinner_tick: u64,
loop_label: Option<&str>,
prompt_name: Option<&str>,
perm_mode: Option<&str>,
bg_store: Option<&BackgroundStore>,
shell_store: Option<&BackgroundShellStore>,
sandbox_badge: Option<&'static str>,
) -> String {
let state = if is_running { "running" } else { "ready" };
let wd_path = Path::new(&session.working_dir);
let dir = wd_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&session.working_dir);
// Append `:branch` when the working dir is inside a git
// working tree. Detached HEAD / non-git dirs show just the
// project name.
let project_label = match cached_git_branch(wd_path) {
Some(b) => format!("{}:{}", dir, b),
None => dir.to_string(),
};
// Denominator is the EFFECTIVE context window (`min(model window,
// context_target)`) — a true fullness meter that reads 0–100% and
// doesn't overflow. The previous denominator was the fold-trigger
// budget (~75% of the window), so once real usage passed 75% the gauge
// showed a confusing >100% (e.g. `90k/75k (120%)`). The loop still
// folds well before the window fills — ~75% (normal) and ~90%
// (turn-start) — so a compact `fold`/`fold!` marker flags when a fold
// is near/imminent instead of pushing the percentage past 100
// (dirge-l4rp, dirge-cx7t).
let ctx =
crate::agent::agent_loop::context_manager::effective_ctx_max(session.context_window);
let used = session.total_estimated_tokens;
let pct = (used * 100).checked_div(ctx).unwrap_or(0);
let pct_str = if pct >= 90 {
format!("{pct}% fold!")
} else if pct >= 75 {
format!("{pct}% fold")
} else {
format!("{pct}%")
};
// TODO(cost-tracking): `session.total_cost` is always 0.0
// because dirge doesn't yet have a per-provider pricing
// table — `AgentEvent::Done` emits `cost: 0.0` unconditionally
// (see `src/agent/runner.rs::run_stream`). Until that's wired,
// the cost segment is suppressed entirely to avoid showing a
// misleading "$0.0000". When pricing lands, restore the
// conditional formatter that was here previously.
let cost_str = String::new();
let compact_badge = if session.compactions.is_empty() {
String::new()
} else {
format!(" cmp:{}", session.compactions.len())
};
let loop_badge = match loop_label {
Some(label) => format!(" [{}]", label),
None => String::new(),
};
let prompt_badge = match prompt_name {
Some(name) => format!(" [{}]", name),
None => String::new(),
};
let perm_badge = match perm_mode {
Some(m) if m != "standard" => format!(" | mode:{}", m),
_ => String::new(),
};
// Active background work, counted per kind. Each badge is shown
// only when non-zero, like the other conditional badges, so the
// bar stays quiet during normal single-agent work.
let active_agents = bg_store.map(|s| s.running_count()).unwrap_or(0);
let active_shells = shell_store.map(|s| s.running_count()).unwrap_or(0);
let agents_badge = if active_agents > 0 {
format!(" | agents:{}", active_agents)
} else {
String::new()
};
let shells_badge = if active_shells > 0 {
format!(" | shells:{}", active_shells)
} else {
String::new()
};
let sandbox_badge_str = match sandbox_badge {
Some(label) => format!(" | sbx:{}", label),
None => String::new(),
};
// dirge: a *distinct* glance id — `short_id`'s fixed 8 chars rendered
// every `compacted-<uuid>` session as "compacte". Full id via
// `/sessions current`.
let session_badge = format!(
" session:{}",
crate::text::session_glance_id(session.id.as_str())
);
format!(
"{}{} | {}{} | {}/{} ({}) | {}msgs | {}{}{}{}{}{}{}{}",
project_label,
cost_str,
session.model,
loop_badge,
fmt_tokens(used),
fmt_tokens(ctx),
pct_str,
session.messages.len(),
state,
compact_badge,
sandbox_badge_str,
prompt_badge,
perm_badge,
agents_badge,
shells_badge,
session_badge,
)
}
}
#[cfg(test)]
mod tests {
use super::{StatusLine, cached_git_branch, git_branch};
use crate::agent::tools::background::BackgroundStore;
use crate::agent::tools::bg_shell::BackgroundShellStore;
use crate::session::Session;
use std::path::Path;
/// The cache returns the same branch as the direct lookup (whatever it is —
/// a branch name, or `None` under a detached HEAD in CI), and a second
/// call within the TTL returns the same value (cache hit) [dirge-vuzz].
#[test]
fn cached_git_branch_matches_direct_and_caches() {
let p = Path::new(".");
let direct = git_branch(p);
let cached = cached_git_branch(p);
assert_eq!(direct, cached);
assert_eq!(cached_git_branch(p), cached);
}
/// A subagent store with `agents` running subagents (each needs a
/// live handle to count, so attach a never-ending spawned task).
fn agent_store(agents: usize) -> BackgroundStore {
let store = BackgroundStore::new();
for n in 0..agents {
let id = format!("a{n}");
store.insert(id.clone());
if tokio::runtime::Handle::try_current().is_ok() {
store.attach_handle(&id, tokio::spawn(std::future::pending::<()>()));
}
}
store
}
/// A shell store with `shells` running shells.
fn shell_store(shells: usize) -> BackgroundShellStore {
let store = BackgroundShellStore::new();
for n in 0..shells {
store.register(format!("s{n}"), "cmd".to_string());
}
store
}
fn render(agents: usize, shells: usize) -> String {
let session = Session::new("openrouter", "test-model", 100_000);
let a = agent_store(agents);
let s = shell_store(shells);
StatusLine::render(
&session,
false,
0,
None,
None,
None,
Some(&a),
Some(&s),
None,
)
}
#[tokio::test]
async fn badges_hidden_when_nothing_active() {
let line = render(0, 0);
assert!(
!line.contains("agents:"),
"no agents badge expected: {line}"
);
assert!(
!line.contains("shells:"),
"no shells badge expected: {line}"
);
}
#[tokio::test]
async fn agents_and_shells_counted_separately() {
let line = render(2, 3);
assert!(line.contains("agents:2"), "expected agents:2 in: {line}");
assert!(line.contains("shells:3"), "expected shells:3 in: {line}");
}
/// dirge-cx7t: the gauge denominator is the full effective window, so the
/// percentage reads 0–100% (not the old >100% past 75%), with a `fold`
/// marker flagging when a fold is near/imminent.
#[tokio::test]
async fn gauge_uses_full_window_and_marks_fold() {
let mut session = Session::new("openrouter", "test-model", 100_000);
// Comfortable: 50k of a 100k window → 50%, no marker, never >100%.
session.total_estimated_tokens = 50_000;
let line = StatusLine::render(&session, false, 0, None, None, None, None, None, None);
assert!(line.contains("/100k (50%)"), "full-window 50%: {line}");
assert!(!line.contains("fold"), "no fold marker at 50%: {line}");
// Normal-fold zone (≥75%): the `fold` hint appears, still ≤100%.
session.total_estimated_tokens = 80_000;
let line = StatusLine::render(&session, false, 0, None, None, None, None, None, None);
assert!(line.contains("(80% fold)"), "fold hint at 80%: {line}");
// Turn-start fold zone (≥90%): `fold!` — and crucially NOT the old
// confusing 120% (90k against the 75k budget).
session.total_estimated_tokens = 90_000;
let line = StatusLine::render(&session, false, 0, None, None, None, None, None, None);
assert!(line.contains("(90% fold!)"), "fold! at 90%: {line}");
assert!(
!line.contains("120%"),
"must not overflow past 100%: {line}"
);
}
/// The session id is shown at the end of the status line so the
/// user can copy it for `--session <id>` resume from the banner.
#[tokio::test]
async fn session_id_appears_in_status_line() {
let session = Session::new("openrouter", "test-model", 100_000);
let line = StatusLine::render(&session, false, 0, None, None, None, None, None, None);
let expected = format!(" session:{}", crate::text::short_id(session.id.as_str()));
assert!(line.contains(&expected), "session id not in status: {line}");
}
}