fix(board): a run just started is Running, not Review

Between Start and the agent showing up in its tab — the shell starting,
the command being typed — the run's tab has no agent in it, and a run with
no agent read as finished: the card jumped to Review and back. A run whose
tab is open and which started under two minutes ago now reads as starting
(task::STARTING, an agent yet to take its first turn), which is Running.
Past the grace, a run that never started is over after all. The same for
`tty7 task ls`.
This commit is contained in:
l0ng-ai
2026-09-29 10:08:15 +08:00
parent 5d0c93626c
commit cf9c830c55
3 changed files with 63 additions and 4 deletions
+30 -2
View File
@@ -1208,12 +1208,17 @@ fn run_live(
AgentStatus::Done => 1,
AgentStatus::Idle => 0,
};
tab.root
let live = tab
.root
.pane_ids()
.iter()
.filter_map(|p| states.get(p))
.max_by_key(|s| urgency(s.status))
.map(tty7_core::core::task::Live::from)
.map(tty7_core::core::task::Live::from);
// Its tab is open and nothing has reported yet: still starting.
let fresh = tty7_core::core::machine::unix_now().saturating_sub(run.started)
< tty7_core::core::task::START_GRACE_SECS;
live.or(fresh.then_some(tty7_core::core::task::STARTING))
}
fn column_name(column: tty7_core::core::task::Column) -> &'static str {
@@ -2652,6 +2657,29 @@ mod tests {
assert_eq!(columns, vec!["queued", "needs-input"]);
}
#[test]
fn a_run_whose_agent_has_not_shown_up_yet_is_running_not_over() {
use tty7_core::core::task::{Run, Task};
let mut backend = mock();
let ws = &mut backend.machine.workspaces[0];
let tab = ws.tabs[0].id;
let mut fresh = Task::new("just started");
fresh.push_run(Run {
agent: CLIAgent::Claude,
tab: Some(tab),
session_id: None,
started: tty7_core::core::machine::unix_now(),
worktree: None,
});
ws.tasks = vec![fresh];
let out = json_of(run_cli(
&["tty7", "task", "ls", "api"],
&Context::default(),
&mut backend,
));
assert_eq!(out["tasks"][0]["column"], "running");
}
#[test]
fn task_done_finds_the_task_by_the_start_of_its_id() {
use tty7_core::core::task::Task;
+13
View File
@@ -36,6 +36,19 @@ pub const MAX_PROMPT_BYTES: usize = 16 * 1024;
/// resumed and nobody resumes the tenth attempt at the same thing.
pub const MAX_RUNS: usize = 8;
/// How long a run's tab may take to show its agent before the run counts as
/// never having started: a shell's startup, then the agent's own. Until then
/// a run with its tab open and no agent in it is starting, not over — see
/// [`STARTING`].
pub const START_GRACE_SECS: u64 = 120;
/// What a run that is still starting reads as: an agent that has not taken
/// its first turn, which [`column`] puts in Running.
pub const STARTING: Live = Live {
status: AgentStatus::Idle,
turns: 0,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TaskId(uuid::Uuid);
+20 -2
View File
@@ -366,7 +366,25 @@ impl Tty7App {
let local = self.can_spawn_locally(cx);
let mut cards = Vec::new();
for task in &self.board.tasks {
let column = task::column(task, |run| find(run.tab).map(|f| f.live));
// A run whose tab is open but whose agent has not shown up yet —
// the shell is still starting, the command still being typed —
// is starting, not over. Read as an agent that has not taken its
// first turn, which `column` counts as Running. Past the grace a
// run that never started is over after all.
let now = unix_now();
let starting = |run: &Run| {
run.tab.is_some_and(|id| {
find(Some(id)).is_none()
&& self.tabs.iter().any(|t| t.tree_id.get() == id)
&& now.saturating_sub(run.started) < task::START_GRACE_SECS
})
};
let column = task::column(task, |run| {
find(run.tab)
.map(|f| f.live)
.or_else(|| starting(run).then_some(task::STARTING))
});
let starting_tab = task.runs.last().filter(|r| starting(r)).and_then(|r| r.tab);
// The run this card reports: the newest one still open, else the
// newest one at all.
let open = task.runs.iter().rev().find_map(|r| find(r.tab));
@@ -397,7 +415,7 @@ impl Tty7App {
Column::Queued => task.paused.or(Some(task.created)),
_ => last.map(|r| r.started),
},
tab: open.map(|f| f.id),
tab: open.map(|f| f.id).or(starting_tab),
resumable: open.is_none() && last.is_some_and(|r| r.session_id.is_some()),
paused: task.paused.is_some(),
worktree_gone: local