From b0cfd10b80443d3f67d263ff4db177bb53cc90ed Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Fri, 30 Oct 2026 17:27:21 +0200 Subject: reload pi agents on an old extension: versioned extension, control socket, menu item and reload-agents --- crates/dinggerd/src/integrations.rs | 62 +++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) (limited to 'crates/dinggerd/src/integrations.rs') diff --git a/crates/dinggerd/src/integrations.rs b/crates/dinggerd/src/integrations.rs index b3daf1d..5d9cf7a 100644 --- a/crates/dinggerd/src/integrations.rs +++ b/crates/dinggerd/src/integrations.rs @@ -10,7 +10,28 @@ use serde_json::{json, Value}; /// The embedded pi extension, adapted from herdr's `herdr-agent-state.ts` /// (Apache-2.0, Copyright herdr contributors). +/// It carries a placeholder for its version, which install fills in. const PI_EXTENSION: &str = include_str!("../assets/pi/pi-agent-state.ts"); +const VERSION_PLACEHOLDER: &str = "__DINGGER_EXTENSION_VERSION__"; + +/// The extension's version: a fingerprint of its text (FNV-1a, so it does +/// not change with the Rust release). A running pi reports the version it +/// loaded; one that differs from this runs an old extension. +pub fn pi_extension_version() -> String { + let hash = PI_EXTENSION.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3) + }); + format!("{hash:016x}") +} + +/// The extension as installed: stamped with its version. +fn pi_extension_text() -> String { + PI_EXTENSION.replace(VERSION_PLACEHOLDER, &pi_extension_version()) +} + +fn pi_extension_path() -> Result { + Ok(pi_extension_dir()?.join("dingger-agent-state.ts")) +} fn pi_extension_dir() -> Result { let base = std::env::var_os("PI_CODING_AGENT_DIR") @@ -76,16 +97,40 @@ pub fn install(agent: &str) -> Result<(), String> { fn install_pi() -> Result<(), String> { let dir = pi_extension_dir()?; std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir failed: {err}"))?; - let path = dir.join("dingger-agent-state.ts"); - std::fs::write(&path, PI_EXTENSION).map_err(|err| format!("write failed: {err}"))?; + let path = pi_extension_path()?; + std::fs::write(&path, pi_extension_text()).map_err(|err| format!("write failed: {err}"))?; tracing::info!(path = %path.display(), "installed pi extension"); println!( - "installed {} — new pi sessions report their state directly;\nrestart running pi agents (or reload them) to pick it up", + "installed {} — new pi sessions report their state directly;\nrunning pi agents pick it up when reloaded (`dingger reload-agents`)", path.display() ); Ok(()) } +/// Whether the installed pi extension is this dingger's: only then does a +/// reload bring an agent up to date (otherwise it loads the old one again). +pub fn pi_extension_installed_is_current() -> bool { + pi_extension_path() + .and_then(|path| std::fs::read_to_string(path).map_err(|err| err.to_string())) + .is_ok_and(|installed| installed == pi_extension_text()) +} + +/// At the daemon's start: if the pi extension is installed and differs +/// from this dingger's, update it, so a dingger update needs no reinstall. +/// Only dingger's own file (it says so on its first line). +pub fn update_pi_extension() { + let Ok(path) = pi_extension_path() else { return }; + let Ok(installed) = std::fs::read_to_string(&path) else { return }; + let current = pi_extension_text(); + if installed == current || !installed.starts_with("// installed by dingger") { + return; + } + match std::fs::write(&path, current) { + Ok(()) => tracing::info!(path = %path.display(), version = %pi_extension_version(), "updated pi extension"), + Err(err) => tracing::warn!(path = %path.display(), %err, "failed to update pi extension"), + } +} + /// Claude Code's user settings, where its hooks are configured. fn claude_settings_path() -> Result { std::env::var_os("CLAUDE_CONFIG_DIR") @@ -216,6 +261,17 @@ pub fn claude_report(input: &str, pane: Option<&str>) -> Option { mod tests { use super::*; + #[test] + fn the_installed_extension_carries_its_version_once() { + let version = pi_extension_version(); + assert_eq!(version.len(), 16); + assert_eq!(PI_EXTENSION.matches(VERSION_PLACEHOLDER).count(), 1, "the asset has one placeholder"); + let text = pi_extension_text(); + assert!(!text.contains(VERSION_PLACEHOLDER)); + assert!(text.contains(&format!("\"{version}\"")), "stamped as a string literal"); + assert!(text.starts_with("// installed by dingger"), "update_pi_extension recognizes its own file"); + } + #[test] fn claude_hook_is_added_beside_other_hooks_and_keys_in_order() { let settings: Value = serde_json::from_str( -- cgit v1.2.3