Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 5 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ fn answer(command: Command, json: bool, home: &Path, data: &Path) -> Result<Stri
};
shown.retain(|account| all || !account.hidden);
let fit = match (hours, shown.first()) {
(Some(hours), Some(first)) => 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);
Expand All @@ -423,7 +423,7 @@ fn answer(command: Command, json: bool, home: &Path, data: &Path) -> Result<Stri
let heading = format!(
"{} · {}\n",
account.full_title(),
status::limit_text(limit, now)
status::limit_text(limit, status.at)
);
let named = limit.named();
let column = shares
Expand Down Expand Up @@ -698,7 +698,7 @@ fn doctor(db: &rusqlite::Connection, home: &Path, data: &Path, now: i64) -> 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()));
}
Expand Down Expand Up @@ -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<String> {
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()),
Expand All @@ -852,7 +851,7 @@ fn statusline(home: &Path, data: &Path, account: Option<&str>) -> Option<String>
.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
Expand Down
7 changes: 3 additions & 4 deletions src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,7 @@ fn check_limits(server: &Server, arguments: Value) -> Result<String> {
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
Expand All @@ -503,12 +502,12 @@ fn check_limits(server: &Server, arguments: Value) -> Result<String> {
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)
))
}
Expand Down
12 changes: 6 additions & 6 deletions src/status/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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}"),
}
}
Expand Down
29 changes: 29 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading