aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/main.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-27 14:12:28 +0200
committerIgor Soarez <igor@soarez.org>2026-10-27 14:12:28 +0200
commit50c433e82c368b9861a89deecb74e5f29fa50353 (patch)
treefe6d87b79387f6c53ef9bf615b7b530daf9a8d3a /crates/dinggerd/src/main.rs
parent9f3a567d4bb1b7b6e42a744936f993bcd437882d (diff)
mark as unread: make an idle agent done again, to come back to it
like "mark as unread" in mail: an agent you have seen (idle) becomes done again, last among done agents of its importance, with no sound or notification. it stays done until you come back to it: leave its tmux window and return, or jump to it. marked while you look at it, it holds until you look away (`held`); a new turn clears it. only idle agents can be marked. - daemon: `pane.mark_unread` on the hook socket; the watcher applies it after the tick's observations (mark_unread, advance's hold rule). - `dingger unread [--pane]`: refuses done, working, blocked, and panes without an agent; returns once the agent shows as done. - pi: /unread (refused while the agent works or is blocked). - panel: right-click an idle agent in the full list, "Mark as unread". - search: ⌘U or right-click marks the selected idle agent; the footer offers ⌘U when it applies; search stays open. tests: unit (hold, jump, new turn, idle only), e2e step 22 (queue position, hold, look away and back, jump, refusals, no sound, menu, the extension), pi-live in real pi 1.4 (a mark, not a turn: an unknown command would also end done), swift selftest (menu only for idle, footer).
Diffstat (limited to 'crates/dinggerd/src/main.rs')
-rw-r--r--crates/dinggerd/src/main.rs51
1 files changed, 51 insertions, 0 deletions
diff --git a/crates/dinggerd/src/main.rs b/crates/dinggerd/src/main.rs
index c50fb80..2c36198 100644
--- a/crates/dinggerd/src/main.rs
+++ b/crates/dinggerd/src/main.rs
@@ -82,6 +82,14 @@ enum Command {
#[arg(long)]
pane: Option<String>,
},
+ /// Mark an idle agent unread: it shows as done again, last among done
+ /// agents of its importance, until you come back to it. In pi, /unread
+ /// does the same.
+ Unread {
+ /// 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
@@ -194,6 +202,12 @@ fn main() {
std::process::exit(1);
}
}
+ Command::Unread { pane } => {
+ if let Err(err) = mark_unread(pane) {
+ eprintln!("dingger: {err}");
+ std::process::exit(1);
+ }
+ }
Command::Poke => {
// Best effort: a stopped watcher just means nothing to wake.
let _ = hooks::poke();
@@ -341,6 +355,43 @@ fn set_importance(level: Option<&str>, pane: Option<String>) -> Result<(), Strin
Ok(())
}
+/// `dingger unread`: ask the daemon to mark the pane's idle agent unread,
+/// and wait until it shows as done.
+fn mark_unread(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 read = || -> Result<(String, String, String), String> {
+ let info = tmux::run(&["display-message", "-p", "-t", &target, "#{pane_id}\t#{@agent}\t#{@agent_state}"])
+ .map_err(|_| format!("no pane {target}"))?;
+ match info.trim_end_matches('\n').split('\t').collect::<Vec<_>>()[..] {
+ [id, agent, state] => Ok((id.to_string(), agent.to_string(), state.to_string())),
+ _ => Err(format!("no pane {target}")),
+ }
+ };
+ let (pane_id, agent, state) = read()?;
+ match (agent.as_str(), state.as_str()) {
+ ("", _) => return Err(format!("no agent in pane {target}")),
+ (_, "idle") => {}
+ (_, "done") => return Err("it is unread already (done)".into()),
+ (_, state) => return Err(format!("only an idle agent can be marked unread; this one is {state}")),
+ }
+ let message = serde_json::json!({ "method": "pane.mark_unread", "params": { "pane_id": pane_id } });
+ hooks::send(&message.to_string())?;
+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
+ while std::time::Instant::now() < deadline {
+ match read()?.2.as_str() {
+ "done" => {
+ println!("marked unread");
+ return Ok(());
+ }
+ "idle" => std::thread::sleep(std::time::Duration::from_millis(50)),
+ other => return Err(format!("not marked: it turned {other}")),
+ }
+ }
+ Err("not marked: the daemon did not apply it".into())
+}
+
/// Every agent pane in herdr's priority order (blocked, done, working,
/// idle), oldest-waiting first within a status.
fn status() -> Result<(), String> {