diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
| commit | 185af91b4d525db6c192a050b53615772fb5fe11 (patch) | |
| tree | 6db92db4042dddcc5fee13d02943447ba9df5f2d /crates/dinggerd | |
| parent | 1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (diff) | |
importance: mark an agent high, normal, or low
within a status, more important agents are listed first, and so jumped to
first; status stays first. a high agent shows a bold ! before its name, a
low agent's name is lightly dimmed with "low" on its second line, and it
makes no sound (it still notifies and waits in line).
in pi, /importance high|normal|low (completes the levels; none once one is
typed out, so Enter runs it). the extension saves it in the session as a
custom entry (dingger-importance), so /reload, restarts, /resume and
restore bring it back; /new starts at normal. it shows a level other than
normal in pi's footer, emits dingger:importance { level } for other
extensions, and sends it with every report. for other agents, `dingger
importance [level] [--pane]`; it refuses a pi that reports itself, whose
saved level would win.
the daemon stays the one writer of pane state: it writes
@dingger_importance (unset for normal), which goes with the agent and
survives a daemon restart. `dingger status` marks high and low.
tests: e2e step 21 (order, the option, no sound for low, the menu bar,
the command, the extension through a harness that now matches pi's
extension API), a DINGGER_SOUND seam so sounds are observable, and a
status-line check that read the counts once (flaky) now polls.
pi-live: /importance in real pi 1.4, its footer and session entry, and the
level after a restore; every pi there now uses the test's agent directory
(a restored pi read the user's). negative controls fail each part.
Diffstat (limited to 'crates/dinggerd')
| -rw-r--r-- | crates/dinggerd/assets/pi/pi-agent-state.ts | 72 | ||||
| -rw-r--r-- | crates/dinggerd/src/hooks.rs | 46 | ||||
| -rw-r--r-- | crates/dinggerd/src/importance.rs | 65 | ||||
| -rw-r--r-- | crates/dinggerd/src/main.rs | 67 | ||||
| -rw-r--r-- | crates/dinggerd/src/notify.rs | 9 | ||||
| -rw-r--r-- | crates/dinggerd/src/tmux.rs | 8 | ||||
| -rw-r--r-- | crates/dinggerd/src/watcher.rs | 65 |
7 files changed, 309 insertions, 23 deletions
diff --git a/crates/dinggerd/assets/pi/pi-agent-state.ts b/crates/dinggerd/assets/pi/pi-agent-state.ts index ff893e7..4c21afe 100644 --- a/crates/dinggerd/assets/pi/pi-agent-state.ts +++ b/crates/dinggerd/assets/pi/pi-agent-state.ts @@ -69,7 +69,34 @@ function updateSessionFile(ctx: any): void { } } -function report(state: AgentState, message?: string): Promise<void> { +// The agent's importance (/importance): it orders agents within a status +// in dingger's list and jump, and a low agent makes no sound. Saved in the +// session (a custom entry, never sent to the model), so it comes back with +// the conversation: on /resume, `pi --session`, and dingger's restore. +// Other extensions can follow it on the event bus: `dingger:importance`, +// `{ level }`, at session start and on every change. +type Importance = "low" | "normal" | "high"; +const importanceLevels: Importance[] = ["high", "normal", "low"]; +const importanceEntry = "dingger-importance"; +let importance: Importance = "normal"; + +function savedImportance(ctx: any): Importance { + try { + const entries = ctx?.sessionManager?.getEntries?.() ?? []; + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry?.type === "custom" && entry.customType === importanceEntry) { + const level = entry.data?.level; + return importanceLevels.includes(level) ? level : "normal"; + } + } + } catch { + // An unreadable session: the default. + } + return "normal"; +} + +function report(state: AgentState | undefined, message?: string): Promise<void> { return sendRequest({ id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, method: "pane.report_agent", @@ -77,8 +104,9 @@ function report(state: AgentState, message?: string): Promise<void> { pane_id: paneId!, source, agent: "pi", - state, + ...(state ? { state } : {}), message, + importance, seq: Date.now() * 1000, ...(sessionFile ? { agent_session_path: sessionFile } : {}), }, @@ -161,6 +189,43 @@ export default function (pi) { publishState(); }); + function showImportance(ctx: any) { + ctx?.ui?.setStatus?.("dingger-importance", importance === "normal" ? undefined : `importance: ${importance}`); + pi.events.emit("dingger:importance", { level: importance }); + } + + pi.registerCommand("importance", { + description: "Set this agent's importance in dingger: high, normal, or low", + // None once a level is typed out, so Enter runs the command at once. + getArgumentCompletions: (prefix: string) => + importanceLevels.includes(prefix.trim() as Importance) + ? null + : importanceLevels + .filter((level) => level.startsWith(prefix.trim())) + .map((level) => ({ value: level, label: level })), + handler: async (args: string, ctx: any) => { + const wanted = args.trim(); + if (!wanted) { + ctx.ui.notify(`importance: ${importance}`, "info"); + return; + } + if (!importanceLevels.includes(wanted as Importance)) { + ctx.ui.notify("importance is high, normal, or low", "error"); + return; + } + if (wanted !== importance) { + importance = wanted as Importance; + pi.appendEntry(importanceEntry, { level: importance }); + showImportance(ctx); + // Without a state: the state stays as last reported. + if (enabled()) { + void report(undefined); + } + } + ctx.ui.notify(`importance: ${importance}`, "info"); + }, + }); + pi.on("session_start", async (event, ctx) => { // TUI only: RPC/JSON/print modes are headless. if (ctx?.mode !== "tui") { @@ -169,6 +234,9 @@ export default function (pi) { // A reload can replace this extension mid-run without emitting another agent_start. agentActive = ctx?.isIdle?.() === false; updateSessionFile(ctx); + // Each session has its own: /new starts at normal, /resume brings its own back. + importance = savedImportance(ctx); + showImportance(ctx); publishState(true); }); diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs index c467c98..8b24659 100644 --- a/crates/dinggerd/src/hooks.rs +++ b/crates/dinggerd/src/hooks.rs @@ -10,6 +10,9 @@ //! or `agent_session_id`, herdr's names), which restore uses to resume it. //! A report without a state only records the session (Claude's //! SessionStart hook, `dingger hook claude`); state stays with the screen. +//! A report may also carry the agent's `importance` (`low`, `normal`, +//! `high`): pi's extension sends it with every report, `dingger importance` +//! alone. //! //! Reports are state-change *events*: the watcher applies every one in //! order, so a turn that starts and finishes between two polls is still a @@ -29,6 +32,8 @@ use std::sync::{Condvar, Mutex, MutexGuard, OnceLock}; use std::time::{Duration, Instant}; use detect_core::AgentState; + +use crate::importance::Importance; use serde::Deserialize; use tracing::{debug, info, warn}; @@ -47,6 +52,8 @@ struct PaneReports { pending: Vec<AgentState>, /// The agent's latest session reference. session: Option<String>, + /// The agent's importance, if reported. + importance: Option<Importance>, /// When the last report arrived. at: Option<Instant>, } @@ -111,6 +118,8 @@ struct HookParams { agent_session_path: Option<String>, #[serde(default)] agent_session_id: Option<String>, + #[serde(default)] + importance: Option<String>, } fn handle_stream(stream: UnixStream) { @@ -137,16 +146,23 @@ fn handle_stream(stream: UnixStream) { continue; } }; + let importance = params.importance.as_deref().and_then(|value| { + let level = Importance::parse(value); + if level.is_none() { + debug!(importance = value, "unknown importance"); + } + level + }); match state { - Some(state) => info!(pane = %params.pane_id, ?state, "hook report"), - None => info!(pane = %params.pane_id, "hook session report"), + Some(state) => info!(pane = %params.pane_id, ?state, ?importance, "hook report"), + None => info!(pane = %params.pane_id, ?importance, "hook report without state"), } - record(params.pane_id, state, params.agent_session_path.or(params.agent_session_id)); + record(params.pane_id, state, params.agent_session_path.or(params.agent_session_id), importance); wake(); } } -fn record(pane_id: String, state: Option<AgentState>, session: Option<String>) { +fn record(pane_id: String, state: Option<AgentState>, session: Option<String>, importance: Option<Importance>) { let mut reports = reports(); let pane = reports.entry(pane_id).or_default(); pane.at = Some(Instant::now()); @@ -157,6 +173,9 @@ fn record(pane_id: String, state: Option<AgentState>, session: Option<String>) { if let Some(session) = session.filter(|s| !s.is_empty() && !s.chars().any(char::is_control)) { pane.session = Some(session); } + if importance.is_some() { + pane.importance = importance; + } } /// Start listening; reports accumulate for the watcher to consume. @@ -204,6 +223,11 @@ pub fn session(pane_id: &str) -> Option<String> { reports().get(pane_id).and_then(|pane| pane.session.clone()) } +/// The pane's agent importance, if reported. +pub fn importance(pane_id: &str) -> Option<Importance> { + reports().get(pane_id).and_then(|pane| pane.importance) +} + /// Drop a pane's reports: the agent was not running when the panes were /// listed at `listed`. A report since then is kept: the agent started /// after the listing (Claude reports its session once, as it starts). @@ -225,7 +249,7 @@ mod tests { #[test] fn a_session_report_records_only_the_session() { - record("%t1".into(), None, Some("/p/s.jsonl".into())); + record("%t1".into(), None, Some("/p/s.jsonl".into()), None); assert_eq!(session("%t1").as_deref(), Some("/p/s.jsonl")); assert_eq!(take_observations("%t1"), None, "state stays with the screen"); assert_eq!(last_state("%t1"), None); @@ -234,10 +258,20 @@ mod tests { #[test] fn a_report_after_the_pane_listing_is_not_dropped() { let listed = Instant::now(); - record("%t2".into(), None, Some("/p/s.jsonl".into())); + record("%t2".into(), None, Some("/p/s.jsonl".into()), None); drop_reports("%t2", listed); assert!(session("%t2").is_some(), "the agent started after the listing"); drop_reports("%t2", Instant::now()); assert_eq!(session("%t2"), None, "listed since, without the agent: dropped"); } + + #[test] + fn importance_is_kept_until_reported_again() { + record("%t3".into(), Some(AgentState::Working), None, Some(Importance::High)); + record("%t3".into(), Some(AgentState::Idle), None, None); + assert_eq!(importance("%t3"), Some(Importance::High), "a report without it keeps it"); + record("%t3".into(), None, None, Some(Importance::Normal)); + assert_eq!(importance("%t3"), Some(Importance::Normal)); + assert_eq!(last_state("%t3"), Some(AgentState::Idle), "a report without state keeps the state"); + } } diff --git a/crates/dinggerd/src/importance.rs b/crates/dinggerd/src/importance.rs new file mode 100644 index 0000000..2480d3e --- /dev/null +++ b/crates/dinggerd/src/importance.rs @@ -0,0 +1,65 @@ +//! An agent's importance, set by the user: high, normal (the default), or +//! low. It orders agents within a status (the list, and so the jump), and +//! a low agent makes no sound. +//! +//! It belongs to the agent's conversation: pi's extension saves it in the +//! session and reports it (`/importance`); other agents get it from +//! `dingger importance`. The daemon writes it to the pane option +//! `@dingger_importance` (unset for normal), which goes with the agent. + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum Importance { + Low, + #[default] + Normal, + High, +} + +impl Importance { + pub fn parse(value: &str) -> Option<Importance> { + match value.trim() { + "low" => Some(Importance::Low), + "normal" => Some(Importance::Normal), + "high" => Some(Importance::High), + _ => None, + } + } + + /// A pane option's value: unset (empty) or anything unknown is normal. + pub fn from_option(value: &str) -> Importance { + Importance::parse(value).unwrap_or_default() + } + + pub fn label(self) -> &'static str { + match self { + Importance::Low => "low", + Importance::Normal => "normal", + Importance::High => "high", + } + } + + /// The pane option's value: normal leaves it unset. + pub fn option_value(self) -> &'static str { + match self { + Importance::Normal => "", + other => other.label(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn levels_order_and_round_trip_through_the_option() { + assert!(Importance::High > Importance::Normal && Importance::Normal > Importance::Low); + for level in [Importance::Low, Importance::Normal, Importance::High] { + assert_eq!(Importance::from_option(level.option_value()), level); + assert_eq!(Importance::parse(level.label()), Some(level)); + } + assert_eq!(Importance::from_option(""), Importance::Normal, "unset is normal"); + assert_eq!(Importance::from_option("urgent"), Importance::Normal); + assert_eq!(Importance::parse("urgent"), None); + } +} diff --git a/crates/dinggerd/src/main.rs b/crates/dinggerd/src/main.rs index b97471c..c50fb80 100644 --- a/crates/dinggerd/src/main.rs +++ b/crates/dinggerd/src/main.rs @@ -1,6 +1,7 @@ //! dingger: agent state watching for tmux panes. mod hooks; +mod importance; mod integrations; mod manifest_update; mod names; @@ -71,6 +72,16 @@ enum Command { }, /// Wake the watcher so focus changes show up immediately (tmux hooks). Poke, + /// Set an agent's importance: it orders agents within a status (the + /// list, and the jump), and a low agent makes no sound. In pi, use + /// /importance instead: pi saves it with the session. + Importance { + /// `high`, `normal`, or `low`; without it, print the current one. + level: Option<String>, + /// The agent's pane (default: the pane this runs in). + #[arg(long)] + pane: Option<String>, + }, /// Restore tmux sessions saved before tmux last stopped (a crash, /// shutdown, or kill-server): windows, panes, layouts, directories. /// Only adds what is missing; a bare session of the same name (what a @@ -177,6 +188,12 @@ fn main() { std::process::exit(1); } } + Command::Importance { level, pane } => { + if let Err(err) = set_importance(level.as_deref(), pane) { + eprintln!("dingger: {err}"); + std::process::exit(1); + } + } Command::Poke => { // Best effort: a stopped watcher just means nothing to wake. let _ = hooks::poke(); @@ -284,6 +301,46 @@ fn raise(client: &terminals::Client) { } } +/// `dingger importance`: report the pane's agent importance to the daemon, +/// which writes it (the daemon is the one writer of pane state). +fn set_importance(level: Option<&str>, pane: Option<String>) -> Result<(), String> { + let target = pane + .or_else(|| std::env::var("TMUX_PANE").ok()) + .ok_or("not in a tmux pane; pass --pane")?; + let info = tmux::run(&[ + "display-message", + "-p", + "-t", + &target, + "#{pane_id}\t#{@agent}\t#{@dingger_hook}\t#{@dingger_importance}", + ]) + .map_err(|_| format!("no pane {target}"))?; + let fields: Vec<&str> = info.trim_end_matches('\n').split('\t').collect(); + let [pane_id, agent, hook, written] = fields[..] else { + return Err(format!("no pane {target}")); + }; + if agent.is_empty() { + return Err(format!("no agent in pane {target}")); + } + let Some(level) = level else { + println!("{}", importance::Importance::from_option(written).label()); + return Ok(()); + }; + let level = importance::Importance::parse(level).ok_or("importance is high, normal, or low")?; + // pi's extension reports the importance saved in the session; a value + // set here would last only until its next report. + if agent == "pi" && !hook.is_empty() { + return Err("this pi reports to dingger: use /importance in it (pi saves it with the session)".into()); + } + let message = serde_json::json!({ + "method": "pane.report_agent", + "params": { "pane_id": pane_id, "importance": level.label() }, + }); + hooks::send(&message.to_string())?; + println!("importance: {}", level.label()); + Ok(()) +} + /// Every agent pane in herdr's priority order (blocked, done, working, /// idle), oldest-waiting first within a status. fn status() -> Result<(), String> { @@ -297,7 +354,8 @@ fn status() -> Result<(), String> { } panes.sort_by_key(|pane| { let status = watcher::Status::from_label(&pane.written_state); - (watcher::list_order(status, pane.written_since.parse().ok()), pane.display_target.clone()) + let importance = importance::Importance::from_option(&pane.written_importance); + (watcher::list_order(status, importance, pane.written_since.parse().ok()), pane.display_target.clone()) }); let place = |pane: &tmux::Pane| pane.display_target.replacen(':', " ยท ", 1); let width = panes.iter().map(|pane| place(pane).chars().count()).max().unwrap_or(5).max(5); @@ -305,7 +363,12 @@ fn status() -> Result<(), String> { for pane in &panes { let state = if pane.written_state.is_empty() { "-" } else { &pane.written_state }; let agent = pane.agent.map(detect_core::agent_label).unwrap_or("-"); - println!("{state:<8} {:<width$} {agent:<7} {}", place(pane), pane.written_name); + let name = match importance::Importance::from_option(&pane.written_importance) { + importance::Importance::High => format!("! {}", pane.written_name), + importance::Importance::Low => format!("{} (low)", pane.written_name), + importance::Importance::Normal => pane.written_name.clone(), + }; + println!("{state:<8} {:<width$} {agent:<7} {}", place(pane), name.trim()); } Ok(()) } diff --git a/crates/dinggerd/src/notify.rs b/crates/dinggerd/src/notify.rs index 555299d..11b37b0 100644 --- a/crates/dinggerd/src/notify.rs +++ b/crates/dinggerd/src/notify.rs @@ -31,6 +31,15 @@ impl NotificationKind { static SOUND_SEQ: AtomicU64 = AtomicU64::new(0); pub fn play_sound(kind: &NotificationKind) { + // Test seam: play through a command instead (tests/e2e.sh). + if let Ok(command) = std::env::var("DINGGER_SOUND") { + let name = match kind { + NotificationKind::NeedsAttention => "request", + NotificationKind::Finished => "done", + }; + let _ = Command::new("sh").args(["-c", &command, name]).stdin(Stdio::null()).spawn(); + return; + } // Sounds are embedded; afplay needs a file, so stage the bytes in the // system temp dir. let path = std::env::temp_dir().join(format!( diff --git a/crates/dinggerd/src/tmux.rs b/crates/dinggerd/src/tmux.rs index f69acd1..9fcccf3 100644 --- a/crates/dinggerd/src/tmux.rs +++ b/crates/dinggerd/src/tmux.rs @@ -28,6 +28,8 @@ pub struct Pane { pub written_hook: String, pub written_name: String, pub written_session: String, + /// `@dingger_importance`: unset for normal. + pub written_importance: String, } @@ -108,14 +110,15 @@ pub fn list_panes() -> Result<Vec<Pane>, String> { "#{pane_index}\t", "#{pane_current_path}\t", "#{@agent_name}\t", - "#{@agent_session}", + "#{@agent_session}\t", + "#{@dingger_importance}", ); let out = run_tmux(&["list-panes", "-a", "-F", format])?; let mut panes: Vec<Pane> = Vec::new(); let mut process_tree = None; for line in out.lines() { let fields: Vec<&str> = line.split('\t').collect(); - if fields.len() < 19 { + if fields.len() < 20 { continue; } let attended = fields[4] == "1" && fields[5] != "0"; @@ -157,6 +160,7 @@ pub fn list_panes() -> Result<Vec<Pane>, String> { written_hook: fields[12].to_string(), written_name: fields[17].to_string(), written_session: fields[18].to_string(), + written_importance: fields[19].to_string(), }); } Ok(panes) diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs index 9198f60..f9c987e 100644 --- a/crates/dinggerd/src/watcher.rs +++ b/crates/dinggerd/src/watcher.rs @@ -31,6 +31,7 @@ use detect_core::{detect_agent_with_osc, AgentState}; use tracing::{debug, info, warn}; use crate::hooks; +use crate::importance::Importance; use crate::notify::{play_sound, send_notification, NotificationKind}; use crate::tmux; @@ -172,15 +173,29 @@ pub fn advance( } /// The order agents are listed in: herdr's status priority; within a -/// status, done and blocked oldest first (the queue: nothing waits -/// forever), working and idle most recent first; then by place. -pub fn list_order(status: Status, since: Option<u64>) -> (std::cmp::Reverse<u8>, i128) { +/// status, the more important first; then done and blocked oldest first +/// (the queue: nothing waits forever), working and idle most recent first; +/// then by place. +pub fn list_order( + status: Status, + importance: Importance, + since: Option<u64>, +) -> (std::cmp::Reverse<u8>, std::cmp::Reverse<Importance>, i128) { let since = since.map(i128::from); let key = match status { Status::Done | Status::Blocked => since.unwrap_or(i128::MAX), _ => since.map(|since| -since).unwrap_or(i128::MAX), }; - (std::cmp::Reverse(status.priority()), key) + (std::cmp::Reverse(status.priority()), std::cmp::Reverse(importance), key) +} + +/// A pane's importance: as its agent last reported it, else as written +/// (it survives a daemon restart); normal without an agent. +pub fn importance_of(pane: &tmux::Pane) -> Importance { + if pane.agent.is_none() { + return Importance::Normal; + } + hooks::importance(&pane.pane_id).unwrap_or_else(|| Importance::from_option(&pane.written_importance)) } pub struct Watcher { @@ -330,6 +345,14 @@ impl Watcher { (None, _) => change("@agent_session", &pane.written_session, String::new()), _ => {} } + // The importance goes with the agent too. + match (pane.agent, hooks::importance(&pane.pane_id)) { + (Some(_), Some(level)) => { + change("@dingger_importance", &pane.written_importance, level.option_value().to_string()) + } + (None, _) => change("@dingger_importance", &pane.written_importance, String::new()), + _ => {} + } if let Err(err) = tmux::set_pane_options(&pane.pane_id, &changes) { warn!(target = %pane.display_target, %err, "failed to write pane options"); } @@ -346,7 +369,8 @@ impl Watcher { if self.notifications && !watching { send_notification(agent, &kind, &format!("{} ({agent})", pane.display_target)); } - if self.sounds { + // A low agent makes no sound; it still notifies and waits in line. + if self.sounds && importance_of(pane) != Importance::Low { play_sound(&kind); } } @@ -355,7 +379,7 @@ impl Watcher { /// status line, menu bar, and focus. The queue is ordered like the menu: /// herdr's priority (blocked before done), oldest first within it. fn publish(&mut self, panes: &[tmux::Pane]) { - let mut queue: Vec<(std::cmp::Reverse<u8>, u64, &str)> = Vec::new(); + let mut queue = Vec::new(); let (mut blocked, mut done, mut working) = (0, 0, 0); for pane in panes { let Some(track) = self.tracks.get(&pane.pane_id) else { @@ -367,16 +391,15 @@ impl Watcher { Status::Working => working += 1, Status::Idle | Status::Unknown => {} } - // The queue: what needs the user, oldest first. + // The queue: what needs the user, in list order. if matches!(track.status(), Status::Blocked | Status::Done) { - let since = track.since.unwrap_or(u64::MAX); - queue.push((std::cmp::Reverse(track.status().priority()), since, pane.pane_id.as_str())); + queue.push((list_order(track.status(), importance_of(pane), track.since), pane.pane_id.as_str())); } } queue.sort(); let queue = queue .iter() - .map(|(_, _, pane_id)| *pane_id) + .map(|(_, pane_id)| *pane_id) .collect::<Vec<_>>() .join(","); @@ -587,7 +610,7 @@ mod tests { ("blocked", Status::Blocked, Some(99)), ("working new", Status::Working, Some(70)), ]; - rows.sort_by_key(|(_, status, since)| list_order(*status, *since)); + rows.sort_by_key(|(_, status, since)| list_order(*status, Importance::Normal, *since)); let names: Vec<_> = rows.iter().map(|(name, ..)| *name).collect(); assert_eq!( names, @@ -596,6 +619,26 @@ mod tests { } #[test] + fn importance_orders_within_a_status_before_time() { + use Importance::*; + let mut rows = [ + ("done old", Status::Done, Normal, Some(10)), + ("done new high", Status::Done, High, Some(90)), + ("done older low", Status::Done, Low, Some(5)), + ("blocked low", Status::Blocked, Low, Some(99)), + ("working high", Status::Working, High, Some(1)), + ("working new", Status::Working, Normal, Some(80)), + ]; + rows.sort_by_key(|(_, status, importance, since)| list_order(*status, *importance, *since)); + let names: Vec<_> = rows.iter().map(|(name, ..)| *name).collect(); + assert_eq!( + names, + ["blocked low", "done new high", "done old", "done older low", "working high", "working new"], + "status first, then importance, then time" + ); + } + + #[test] fn agent_exit_is_unknown() { let done = step(step(PaneTrack::NEW, Working, false), Idle, false); assert_eq!(step(done, Unknown, false).status(), Status::Unknown); |
