aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
committerIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
commit185af91b4d525db6c192a050b53615772fb5fe11 (patch)
tree6db92db4042dddcc5fee13d02943447ba9df5f2d /crates
parent1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (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')
-rw-r--r--crates/dinggerd/assets/pi/pi-agent-state.ts72
-rw-r--r--crates/dinggerd/src/hooks.rs46
-rw-r--r--crates/dinggerd/src/importance.rs65
-rw-r--r--crates/dinggerd/src/main.rs67
-rw-r--r--crates/dinggerd/src/notify.rs9
-rw-r--r--crates/dinggerd/src/tmux.rs8
-rw-r--r--crates/dinggerd/src/watcher.rs65
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);