From 9575ead9c637a2dc482c00f417a1d784d46e076a Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Thu, 5 Nov 2026 19:45:08 +0200 Subject: name pi agents by their reported session name, not the terminal title; scroll a panel list taller than the screen --- crates/dinggerd/assets/pi/pi-agent-state.ts | 13 ++++++++++++- crates/dinggerd/src/hooks.rs | 30 +++++++++++++++++++++++++++++ crates/dinggerd/src/watcher.rs | 7 +++++-- 3 files changed, 47 insertions(+), 3 deletions(-) (limited to 'crates/dinggerd') 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, /// How full its context window is: whole percent ("" when unknown). context: Option, + /// The agent's session name, as reported ("" for none). + name: Option, /// The error that ended its last failed run, as one line. error: Option, /// When the last report arrived. @@ -189,6 +194,8 @@ struct HookParams { aborted: Option, #[serde(default)] error: Option, + #[serde(default)] + name: Option, } 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, + name: Option, } /// A reported state, and whether it ends a run the user cancelled. @@ -344,6 +353,9 @@ fn record(pane_id: String, state: Option, 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 { + 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 { reports().get(pane_id).and_then(|pane| pane.error.clone()) @@ -527,6 +545,18 @@ mod tests { assert_eq!(take_observations("%t7"), Some(vec![Observation::plain(AgentState::Idle)])); } + #[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() }; 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)) { -- cgit v1.2.3