aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-05 19:45:08 +0200
committerIgor Soarez <igor@soarez.org>2026-11-05 19:45:08 +0200
commit9575ead9c637a2dc482c00f417a1d784d46e076a (patch)
treea47abc5722d4f978e7c94428e06634e88101152a /crates/dinggerd
parent5842b093cc502b46076423e4f8efdbbc568f86aa (diff)
name pi agents by their reported session name, not the terminal title; scroll a panel list taller than the screen
Diffstat (limited to 'crates/dinggerd')
-rw-r--r--crates/dinggerd/assets/pi/pi-agent-state.ts13
-rw-r--r--crates/dinggerd/src/hooks.rs30
-rw-r--r--crates/dinggerd/src/watcher.rs7
3 files changed, 47 insertions, 3 deletions
diff --git a/crates/dinggerd/assets/pi/pi-agent-state.ts b/crates/dinggerd/assets/pi/pi-agent-state.ts
index ceac45b..0575ba9 100644
--- a/crates/dinggerd/assets/pi/pi-agent-state.ts
+++ b/crates/dinggerd/assets/pi/pi-agent-state.ts
@@ -119,6 +119,9 @@ let draft = "";
// How full the context window is, in percent, as pi's footer shows it;
// "" when unknown (right after compacting, before the next reply).
let context = "";
+// The session's name ("" for none): dingger shows it as the agent's name,
+// whatever the terminal title says.
+let sessionName = "";
const draftDir = path.join(path.dirname(socketPath), "drafts");
// Enough to find the agent by it; the file keeps all of it.
const reportedDraftChars = 500;
@@ -212,6 +215,7 @@ async function report(state: AgentState | undefined, message?: string, end: RunE
...(controlListening ? { control: controlPath } : {}),
draft: draft.slice(0, reportedDraftChars),
context,
+ name: sessionName,
seq: Date.now() * 1000,
...(sessionFile ? { agent_session_path: sessionFile } : {}),
},
@@ -563,8 +567,9 @@ export default function (pi) {
// Each session has its own: /new starts at normal, /resume brings its own back.
importance = savedImportance(ctx);
announceImportance();
- // The report below carries it.
+ // The reports below carry them.
checkContext(ctx, true);
+ sessionName = pi.getSessionName?.() ?? "";
publishState(true);
});
@@ -588,6 +593,12 @@ export default function (pi) {
});
}
+ // /name, or a name given by an extension.
+ pi.on("session_info_changed", (event) => {
+ sessionName = event?.name ?? "";
+ void report(undefined);
+ });
+
pi.on("agent_settled", (event, ctx) => {
latestCtx = ctx;
if (ctx?.isIdle?.() !== true) {
diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs
index aeb3285..c9825dd 100644
--- a/crates/dinggerd/src/hooks.rs
+++ b/crates/dinggerd/src/hooks.rs
@@ -32,6 +32,9 @@
//! Or that it failed, with the `error` as one readable line: the agent is
//! done, and the alert says it failed and why.
//!
+//! A pi report carries the session's `name` ("" for none): the agent's
+//! name, whatever its terminal title says.
+//!
//! It also carries the `draft` in the agent's input box ("" for none), for
//! 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 (""
@@ -83,6 +86,8 @@ struct PaneReports {
draft: Option<String>,
/// How full its context window is: whole percent ("" when unknown).
context: Option<String>,
+ /// The agent's session name, as reported ("" for none).
+ name: Option<String>,
/// The error that ended its last failed run, as one line.
error: Option<String>,
/// When the last report arrived.
@@ -189,6 +194,8 @@ struct HookParams {
aborted: Option<bool>,
#[serde(default)]
error: Option<String>,
+ #[serde(default)]
+ name: Option<String>,
}
fn handle_stream(stream: UnixStream) {
@@ -259,6 +266,7 @@ fn handle_stream(stream: UnixStream) {
context: params.context,
aborted: params.aborted == Some(true),
error: params.error,
+ name: params.name,
};
record(params.pane_id, state, reported);
wake();
@@ -278,6 +286,7 @@ struct Reported {
aborted: bool,
/// The error the run this report ends failed with.
error: Option<String>,
+ name: Option<String>,
}
/// A reported state, and whether it ends a run the user cancelled.
@@ -344,6 +353,9 @@ 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(name) = reported.name {
+ pane.name = Some(draft_line(&name));
+ }
if let Some(context) = reported.context {
pane.context = Some(context_percent(&context));
}
@@ -358,6 +370,12 @@ fn context_percent(value: &str) -> String {
}
}
+/// The pane's agent's session name ("" for none), if its extension
+/// reports it.
+pub fn name(pane_id: &str) -> Option<String> {
+ reports().get(pane_id).and_then(|pane| pane.name.clone())
+}
+
/// The error the pane's agent's last failed run ended with.
pub fn error(pane_id: &str) -> Option<String> {
reports().get(pane_id).and_then(|pane| pane.error.clone())
@@ -528,6 +546,18 @@ mod tests {
}
#[test]
+ fn a_reported_name_is_kept_as_one_line_until_reported_again() {
+ let named = |name: &str| Reported { name: Some(name.into()), ..Default::default() };
+ assert_eq!(super::name("%t9"), None, "an extension that does not report it");
+ record("%t9".into(), None, named("Fix the\nsidecar"));
+ assert_eq!(super::name("%t9").as_deref(), Some("Fix the sidecar"));
+ record("%t9".into(), None, Reported::default());
+ assert_eq!(super::name("%t9").as_deref(), Some("Fix the sidecar"), "kept by a report without one");
+ record("%t9".into(), None, named(""));
+ assert_eq!(super::name("%t9").as_deref(), Some(""), "unnamed");
+ }
+
+ #[test]
fn a_failed_run_ends_in_a_failed_idle_observation_with_its_error() {
let failed = |error: &str| Reported { error: Some(error.into()), ..Default::default() };
record("%t8".into(), Some(AgentState::Working), failed("not the end of a run"));
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs
index 9bad1fe..1cd282b 100644
--- a/crates/dinggerd/src/watcher.rs
+++ b/crates/dinggerd/src/watcher.rs
@@ -503,8 +503,11 @@ impl Watcher {
change("@dingger_since", &pane.written_since, track.since.map(|s| s.to_string()).unwrap_or_default());
change("@dingger_error", &pane.written_error, error.to_string());
change("@agent", &pane.written_agent, pane.agent.map(detect_core::agent_label).unwrap_or("").to_string());
- let name = crate::names::display_name(pane.agent, &pane.pane_title, &pane.cwd, host_name())
- .unwrap_or_default();
+ // The name its extension reports ("" for an unnamed session), else
+ // what the terminal title says (anyone can set a title).
+ let name = hooks::name(&pane.pane_id).unwrap_or_else(|| {
+ crate::names::display_name(pane.agent, &pane.pane_title, &pane.cwd, host_name()).unwrap_or_default()
+ });
change("@agent_name", &pane.written_name, name);
// The session reference, for restore; it goes with the agent.
match (pane.agent, hooks::session(&pane.pane_id)) {