feat(agents): Current branch and time spent in the agent session header

A local-checkout task shows the repository's current branch in its header, and
Changes compares against it; both follow branch switches. The header also shows
the time the agent has spent on the task's turns.

A usage update without a cost (Claude sends one for its rate limits) no longer
clears the session cost.
Цей коміт міститься в:
l0ng-ai committed 2026-10-28 11:57:18 +08:00
1 джерело 6e4d927ff0
коміт 93f87f49c1
7 files changed
+149 -38

No files matched your search

+20 -12
Переглянути файл
@@ -104,17 +104,17 @@ pub(crate) fn commands_of(update: &Value) -> Vec<AgentCommand> {
.collect()
}
/// `usage_update`: `{ used, size, cost?: { amount, currency } }`.
pub(crate) fn usage_of(update: &Value) -> Option<AgentUsage> {
/// `usage_update`: `{ used, size, cost?: { amount, currency } }`. An update without a cost
/// keeps the last one (Claude also sends one for its rate limits, without the cost).
pub(crate) fn usage_of(update: &Value, prev: Option<AgentUsage>) -> Option<AgentUsage> {
let tokens = |key: &str| update.get(key)?.as_f64().map(|n| n.clamp(0.0, f64::from(u32::MAX)) as u32);
let used = tokens("used")?;
let size = tokens("size").filter(|s| *s > 0)?;
Some(AgentUsage {
used,
size,
cost: update.pointer("/cost/amount").and_then(Value::as_f64),
currency: update.pointer("/cost/currency").and_then(Value::as_str).map(str::to_owned),
})
let (Some(used), Some(size)) = (tokens("used"), tokens("size").filter(|s| *s > 0)) else { return prev };
let (cost, currency) = match (update.pointer("/cost/amount").and_then(Value::as_f64), prev) {
(Some(cost), _) => (Some(cost), update.pointer("/cost/currency").and_then(Value::as_str).map(str::to_owned)),
(None, Some(p)) => (p.cost, p.currency),
(None, None) => (None, None),
};
Some(AgentUsage { used, size, cost, currency })
}
fn legacy(
@@ -217,8 +217,16 @@ mod tests {
]}));
assert_eq!(cmds[0].hint.as_deref(), Some("what to review"));
assert_eq!((cmds[1].name.as_str(), cmds[1].hint.as_ref()), ("init", None));
let u = usage_of(&json!({"used": 1200, "size": 200000, "cost": {"amount": 0.42, "currency": "USD"}})).unwrap();
let u = usage_of(&json!({"used": 1200, "size": 200000, "cost": {"amount": 0.42, "currency": "USD"}}), None)
.unwrap();
assert_eq!((u.used, u.cost, u.currency.as_deref()), (1200, Some(0.42), Some("USD")));
assert!(usage_of(&json!({"used": 1, "size": 0})).is_none());
assert!(usage_of(&json!({"used": 1, "size": 0}), None).is_none());
}
#[test]
fn usage_without_a_cost_keeps_the_last_one() {
let u = usage_of(&json!({"used": 100, "size": 1000, "cost": {"amount": 1.5, "currency": "USD"}}), None);
let u = usage_of(&json!({"used": 200, "size": 1000, "_meta": {"_claude/rateLimit": {}}}), u).unwrap();
assert_eq!((u.used, u.cost, u.currency.as_deref()), (200, Some(1.5), Some("USD")));
}
}
+1 -1
Переглянути файл
@@ -915,7 +915,7 @@ impl Task {
true
}
"usage_update" => {
s.task.usage = config::usage_of(update).or(s.task.usage.take());
s.task.usage = config::usage_of(update, s.task.usage.take());
true
}
_ => false,