diff --git a/CHANGELOG.md b/CHANGELOG.md index 7834667..0cb01e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ `read_session` and the handoff, not by every tool its text names. One that ran `rg tools.apply_patch src` was named `exec: exec_command, apply_patch`, as if it changed files. +- **`check_limits`' verdict, `statusline` and `usage --limit`'s first + line** count time from the minute the status is as of, as `status` + does. Late in a minute, `check_limits` could say a limit resets "14:00 + (in 1h 59m)" above a line that says "14:00 (in 2h)", and `statusline` + could say "out in 59m" while `status` said "13:00 (in 1h)". ## 0.1.4 diff --git a/src/main.rs b/src/main.rs index 9b6b738..0518fb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -397,7 +397,7 @@ fn answer(command: Command, json: bool, home: &Path, data: &Path) -> Result Some(status::fit(first, hours, now)?), + (Some(hours), Some(first)) => Some(status::fit(first, hours, status.at)?), _ => None, }; let text = status::text(&status, &shown, None); @@ -423,7 +423,7 @@ fn answer(command: Command, json: bool, home: &Path, data: &Path) -> Result Resu // An account no agent here is signed in to has nothing to fix. for account in &status.accounts { if account.state.why() != Some(status::Stale::SignedOut) - && let Some(stale) = status::stale(account, false, now) + && let Some(stale) = status::stale(account, false, status.at) { notes.push(format!("{}: {stale}.", account.full_title())); } @@ -835,8 +835,7 @@ fn guard( /// so it's quick; none when nothing's known. fn statusline(home: &Path, data: &Path, account: Option<&str>) -> Option { let db = db::open(data).ok()?; - let now = now(); - let status = status::status(&db, now).ok()?; + let status = status::status(&db, now()).ok()?; let id = match account { // A line is better than none: of several named, the first. Some(name) => Some(status::named(&status, name).ok()?.first()?.id.clone()), @@ -852,7 +851,7 @@ fn statusline(home: &Path, data: &Path, account: Option<&str>) -> Option .iter() .find(|account| account.in_use && !account.hidden)?, }; - Some(status::line(account, now)) + Some(status::line(account, status.at)) } /// The account of the session a harness names in the JSON it writes to a diff --git a/src/mcp.rs b/src/mcp.rs index c43967e..56f5220 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -476,8 +476,7 @@ fn check_limits(server: &Server, arguments: Value) -> Result { all: bool, } let input: Input = input(arguments)?; - let now = crate::now(); - let status = status::status(&server.db, now)?; + let status = status::status(&server.db, crate::now())?; let mine = server.caller.account(&server.db, &server.home, &status)?; let yours = mine.map(|mine| mine.id.as_str()); let shown: Vec<&status::Account> = status @@ -503,12 +502,12 @@ fn check_limits(server: &Server, arguments: Value) -> Result { return Ok("No account's limits have been read yet: no agent here is signed in to one Turnscope reads.".to_owned()); }; let fit = match input.hours { - Some(hours) => format!("{}\n", status::fit(first, hours, now)?), + Some(hours) => format!("{}\n", status::fit(first, hours, status.at)?), None => String::new(), }; Ok(format!( "{fit}{}\n\n{}", - status::verdict(first, now), + status::verdict(first, status.at), status::text(&status, &shown, yours) )) } diff --git a/src/status/text.rs b/src/status/text.rs index ab68236..3237686 100644 --- a/src/status/text.rs +++ b/src/status/text.rs @@ -2,7 +2,7 @@ //! each account and limit as lines, whether more hours of work fit, and the //! one sentence that leads an answer. The app says it in its own words. -use super::{Account, Limit, Outlook, Spread, Stale, State, Status, Unknown}; +use super::{Account, Horizon, Limit, Moment, Outlook, Spread, Stale, State, Status, Unknown}; use crate::agents; use crate::time::{self, HOUR, MINUTE, when}; @@ -414,14 +414,14 @@ fn short(limit: &Limit, now: i64) -> String { None => format!("{}%", limit.left_percent), }; // Soon, as a countdown; later, as the clock says it. - let at = |at: i64| match at - now < 3 * HOUR { - true => format!("in {}", time::span(at - now)), - false => time::clock(at, now), + let at = |moment: Moment| match moment.horizon { + Horizon::Soon => format!("in {}", time::span(moment.at - now)), + _ => time::clock(moment.at, now), }; match limit.outlook { - Outlook::UsedUp { back: Some(back) } => format!("{name} used up, back {}", at(back.at)), + Outlook::UsedUp { back: Some(back) } => format!("{name} used up, back {}", at(back)), Outlook::UsedUp { back: None } => format!("{name} used up"), - Outlook::RunsOut { likely, .. } => format!("{name} {left}, out {}", at(likely.at)), + Outlook::RunsOut { likely, .. } => format!("{name} {left}, out {}", at(likely)), _ => format!("{name} {left}"), } } diff --git a/tests/cli.rs b/tests/cli.rs index c0403f3..dff3a5b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -440,6 +440,35 @@ fn a_status_line_says_the_account_of_the_session_it_is_given() { ); } +#[test] +fn an_answer_tells_time_by_the_minute_its_status_is_as_of() { + let data = store(); + // 40 seconds into the minute; Claude's 5 hours runs out at 13:00 and + // resets at 14:00. + let late = (NOW + 40_000).to_string(); + let mut mcp = command(data.path(), &["mcp"]) + .env("TURNSCOPE_NOW", &late) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let request = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_limits","arguments":{"account":"claude"}}}"#; + writeln!(mcp.stdin.take().unwrap(), "{request}").unwrap(); + let reply: serde_json::Value = + serde_json::from_slice(&mcp.wait_with_output().unwrap().stdout).unwrap(); + let text = reply["result"]["content"][0]["text"].as_str().unwrap(); + // In the verdict, and in the 5 hours' own line. + assert_eq!(text.matches("resets 14:00 (in 2h)").count(), 2, "{text}"); + let line = command(data.path(), &["statusline", "--account", "claude"]) + .env("TURNSCOPE_NOW", &late) + .output() + .unwrap(); + assert_eq!( + String::from_utf8_lossy(&line.stdout).trim(), + "Claude Max · 5h 25%, out in 1h · weekly 56%" + ); +} + #[test] fn an_account_with_no_read_for_half_an_hour_shows_as_of_its_last() { let data = store();