aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-03 13:05:57 +0200
committerIgor Soarez <igor@soarez.org>2026-11-03 13:05:57 +0200
commitbd1eb9ef9b3b89ff5825eb8bf9b2e978167c9ad0 (patch)
treeac3ea2a1f0f2243c692ba5e5fab251df6c9f077b /crates/dinggerd/src
parent8146d1db419e8bdacd75920da6f3761abf724853 (diff)
show each pi agent's context use: a meter in the panel and search, text in the menu
Diffstat (limited to 'crates/dinggerd/src')
-rw-r--r--crates/dinggerd/src/hooks.rs43
-rw-r--r--crates/dinggerd/src/main.rs8
-rw-r--r--crates/dinggerd/src/tmux.rs8
-rw-r--r--crates/dinggerd/src/watcher.rs7
4 files changed, 60 insertions, 6 deletions
diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs
index 1718ff8..82fa7bd 100644
--- a/crates/dinggerd/src/hooks.rs
+++ b/crates/dinggerd/src/hooks.rs
@@ -28,7 +28,9 @@
//! on an old extension once it is idle and off screen.
//!
//! It also carries the `draft` in the agent's input box ("" for none), for
-//! marking a draft you left behind; never logged.
+//! marking a draft you left behind; never logged. And `context`: how full
+//! the agent's context window is, in percent, as pi's footer shows it (""
+//! when unknown, as right after compacting).
//!
//! Each line is answered with `{"ok":true}`.
//!
@@ -74,6 +76,8 @@ struct PaneReports {
control: Option<String>,
/// The draft in the agent's input box, as one line ("" for none).
draft: Option<String>,
+ /// How full its context window is: whole percent ("" when unknown).
+ context: Option<String>,
/// When the last report arrived.
at: Option<Instant>,
}
@@ -172,6 +176,8 @@ struct HookParams {
control: Option<String>,
#[serde(default)]
draft: Option<String>,
+ #[serde(default)]
+ context: Option<String>,
}
fn handle_stream(stream: UnixStream) {
@@ -239,6 +245,7 @@ fn handle_stream(stream: UnixStream) {
extension: params.extension,
control: params.control,
draft: params.draft,
+ context: params.context,
};
record(params.pane_id, state, reported);
wake();
@@ -253,6 +260,7 @@ struct Reported {
extension: Option<String>,
control: Option<String>,
draft: Option<String>,
+ context: Option<String>,
}
/// The longest draft kept: enough to find an agent by it.
@@ -298,6 +306,24 @@ fn record(pane_id: String, state: Option<AgentState>, reported: Reported) {
if let Some(draft) = reported.draft {
pane.draft = Some(draft_line(&draft));
}
+ if let Some(context) = reported.context {
+ pane.context = Some(context_percent(&context));
+ }
+}
+
+/// A reported context use as whole percent ("12.6" is "13"); "" for
+/// unknown, or anything not a percentage.
+fn context_percent(value: &str) -> String {
+ match value.trim().parse::<f64>() {
+ Ok(percent) if percent.is_finite() && percent >= 0.0 => format!("{:.0}", percent.min(999.0)),
+ _ => String::new(),
+ }
+}
+
+/// How full the pane's agent's context window is ("" when unknown), if its
+/// extension reports it.
+pub fn context(pane_id: &str) -> Option<String> {
+ reports().get(pane_id).and_then(|pane| pane.context.clone())
}
/// Start listening; reports accumulate for the watcher to consume.
@@ -441,6 +467,21 @@ mod tests {
}
#[test]
+ fn context_use_is_kept_as_whole_percent_until_reported_again() {
+ let context = |text: &str| Reported { context: Some(text.into()), ..Default::default() };
+ assert_eq!(super::context("%t6"), None, "an extension that does not report it");
+ record("%t6".into(), None, context("12.6"));
+ assert_eq!(super::context("%t6").as_deref(), Some("13"));
+ record("%t6".into(), None, Reported::default());
+ assert_eq!(super::context("%t6").as_deref(), Some("13"), "kept by a report without one");
+ record("%t6".into(), None, context(""));
+ assert_eq!(super::context("%t6").as_deref(), Some(""), "unknown, after compacting");
+ for (value, want) in [("0", "0"), ("0.4", "0"), ("97.5", "98"), ("1e9", "999"), ("-3", ""), ("NaN", ""), ("x", "")] {
+ assert_eq!(context_percent(value), want, "{value}");
+ }
+ }
+
+ #[test]
fn a_draft_is_kept_as_one_short_line_until_reported_again() {
let draft = |text: &str| Reported { draft: Some(text.into()), ..Default::default() };
record("%t5".into(), Some(AgentState::Idle), Reported::default());
diff --git a/crates/dinggerd/src/main.rs b/crates/dinggerd/src/main.rs
index 99c10a7..935da7d 100644
--- a/crates/dinggerd/src/main.rs
+++ b/crates/dinggerd/src/main.rs
@@ -448,7 +448,8 @@ fn mark_unread(pane: Option<String>) -> Result<(), String> {
/// Every agent pane in herdr's priority order (blocked, done, working,
/// idle), oldest-waiting first within a status. ✎ marks a draft left in
-/// the agent's input box. COST is what a pi agent spent today (UTC); the
+/// the agent's input box. CTX is how full a pi agent's context window is.
+/// COST is what a pi agent spent today (UTC); the
/// last line totals today, this week, and this month, closed agents included.
fn status() -> Result<(), String> {
let mut panes: Vec<_> = tmux::list_panes()?
@@ -467,7 +468,7 @@ fn status() -> Result<(), String> {
});
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);
- println!("{:<8} {:<width$} {:<7} {:>8} NAME", "STATUS", "WHERE", "AGENT", "COST");
+ println!("{:<8} {:<width$} {:<7} {:>8} {:>4} NAME", "STATUS", "WHERE", "AGENT", "COST", "CTX");
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("-");
@@ -478,7 +479,8 @@ fn status() -> Result<(), String> {
importance::Importance::Normal => pane.written_name.clone(),
};
let cost = if pane.written_cost.is_empty() { String::new() } else { format!("${}", pane.written_cost) };
- println!("{state:<8} {:<width$} {agent:<7} {cost:>8} {draft}{}", place(pane), name.trim());
+ let context = if pane.written_context.is_empty() { String::new() } else { format!("{}%", pane.written_context) };
+ println!("{state:<8} {:<width$} {agent:<7} {cost:>8} {context:>4} {draft}{}", place(pane), name.trim());
}
let global = |name: &str| tmux::show_global(name).unwrap_or_default();
let spent = [("today", "@dingger_cost_today"), ("this week", "@dingger_cost_week"), ("this month", "@dingger_cost_month")]
diff --git a/crates/dinggerd/src/tmux.rs b/crates/dinggerd/src/tmux.rs
index 136260e..80adbb5 100644
--- a/crates/dinggerd/src/tmux.rs
+++ b/crates/dinggerd/src/tmux.rs
@@ -36,6 +36,8 @@ pub struct Pane {
pub written_draft: String,
/// What the agent's session spent today (UTC), in dollars.
pub written_cost: String,
+ /// How full the agent's context window is, in whole percent.
+ pub written_context: String,
}
@@ -121,14 +123,15 @@ pub fn list_panes() -> Result<Vec<Pane>, String> {
"#{@dingger_extension}\t",
"#{@dingger_control}\t",
"#{@dingger_draft}\t",
- "#{@dingger_cost}",
+ "#{@dingger_cost}\t",
+ "#{@dingger_context}",
);
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() < 24 {
+ if fields.len() < 25 {
continue;
}
let attended = fields[4] == "1" && fields[5] != "0";
@@ -175,6 +178,7 @@ pub fn list_panes() -> Result<Vec<Pane>, String> {
written_control: fields[21].to_string(),
written_draft: fields[22].to_string(),
written_cost: fields[23].to_string(),
+ written_context: fields[24].to_string(),
});
}
Ok(panes)
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs
index 1aec88e..77b9259 100644
--- a/crates/dinggerd/src/watcher.rs
+++ b/crates/dinggerd/src/watcher.rs
@@ -28,6 +28,7 @@
//! and its control socket, for reloading agents on an old one
//! - `@dingger_draft` the draft in the agent's input box, as one line
//! - `@dingger_cost` what the agent's session spent today (see `cost`)
+//! - `@dingger_context` how full its context window is, in whole percent
use std::collections::HashMap;
@@ -527,6 +528,12 @@ impl Watcher {
(None, _) => change("@dingger_draft", &pane.written_draft, String::new()),
_ => {}
}
+ // And how full its context window is.
+ match (pane.agent, hooks::context(&pane.pane_id)) {
+ (Some(_), Some(context)) => change("@dingger_context", &pane.written_context, context),
+ (None, _) => change("@dingger_context", &pane.written_context, 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");
}