aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 03:02:45 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 03:04:20 +0300
commit77819797385c5538f624a8881208921342911db0 (patch)
tree931a03aa3825a10d6b6d73b63e4ce80e19ecbf56 /src
parent3acd499ffb1119e8032bbcba3f1f46d4ff5f3da9 (diff)
fix: clarify update restart guidance
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs14
-rw-r--r--src/app/api.rs6
-rw-r--r--src/server/autodetect.rs28
-rw-r--r--src/server/headless.rs8
-rw-r--r--src/session.rs53
-rw-r--r--src/ui/release_notes.rs21
-rw-r--r--src/update.rs103
7 files changed, 169 insertions, 64 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 981212b1..160947ff 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -1932,7 +1932,7 @@ impl AppState {
self.toast = Some(ToastNotification {
kind: ToastKind::UpdateInstalled,
title: format!("v{version} available"),
- context: format!("detach, then run `{install_command}`"),
+ context: crate::update::update_install_instruction(&install_command),
target: None,
});
}
@@ -2795,7 +2795,10 @@ mod tests {
assert!(state.latest_release_notes_available);
let toast = state.toast.as_ref().expect("update toast");
assert_eq!(toast.title, "v0.5.0 available");
- assert_eq!(toast.context, "detach, then run `herdr update`");
+ assert_eq!(
+ toast.context,
+ "detach, run `herdr update`, then follow its restart guidance"
+ );
}
fn mark_agent(state: &mut AppState, ws_idx: usize, tab_idx: usize, pane_id: PaneId) {
@@ -3756,7 +3759,10 @@ mod tests {
let toast = state.toast.as_ref().expect("update toast");
assert_eq!(toast.kind, ToastKind::UpdateInstalled);
assert_eq!(toast.title, "v0.5.0 available");
- assert_eq!(toast.context, "detach, then run `herdr update`");
+ assert_eq!(
+ toast.context,
+ "detach, run `herdr update`, then follow its restart guidance"
+ );
}
#[test]
@@ -3776,7 +3782,7 @@ mod tests {
let toast = state.toast.as_ref().expect("update toast");
assert_eq!(
toast.context,
- "detach, then run `brew update && brew upgrade herdr`"
+ "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready"
);
}
diff --git a/src/app/api.rs b/src/app/api.rs
index d8224506..1442012b 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -152,10 +152,8 @@ impl App {
};
if let Some((version, install_command)) = update_ready {
- let _ = notify(
- &format!("v{version} available"),
- Some(&format!("detach, then run `{install_command}`")),
- );
+ let instruction = crate::update::update_install_instruction(&install_command);
+ let _ = notify(&format!("v{version} available"), Some(&instruction));
} else {
for update in &pane_updates {
let is_active_tab = self
diff --git a/src/server/autodetect.rs b/src/server/autodetect.rs
index c278329d..c012aa03 100644
--- a/src/server/autodetect.rs
+++ b/src/server/autodetect.rs
@@ -85,11 +85,10 @@ fn read_server_status() -> io::Result<Option<crate::api::RuntimeStatus>> {
}
fn validate_running_server_compatibility() -> io::Result<()> {
- let stop_command = crate::session::local_stop_command();
- let attach_command = crate::session::local_attach_command();
let Some(status) = read_server_status()? else {
return Err(io::Error::other(format!(
- "a herdr server is listening, but its status API is unavailable. Try `{stop_command}`; if that fails, stop the old server process manually, then run `{attach_command}` again."
+ "a herdr server is listening, but its status API is unavailable.\n\n{}\nIf that fails, stop the old server process manually.",
+ crate::session::active_restart_after_update_guidance()
)));
};
@@ -98,14 +97,15 @@ fn validate_running_server_compatibility() -> io::Result<()> {
}
Err(io::Error::other(format!(
- "herdr server is running from v{} / protocol {}, but this client is v{} / protocol {}.\nStop the old server with `{stop_command}`, then run `{attach_command}` again.",
+ "Herdr was updated, but this session is still running the old server.\n\nserver: v{} protocol {}\nclient: v{} protocol {}\n\n{}",
status.version.as_deref().unwrap_or("unknown"),
status
.protocol
.map(|value| value.to_string())
.unwrap_or_else(|| "unknown".to_string()),
env!("CARGO_PKG_VERSION"),
- crate::protocol::PROTOCOL_VERSION
+ crate::protocol::PROTOCOL_VERSION,
+ crate::session::active_restart_after_update_guidance()
)))
}
@@ -432,12 +432,13 @@ mod tests {
fn validate_running_server_compatibility_names_session_commands_for_protocol_mismatch() {
let _guard = env_lock().lock().unwrap();
let dir = unique_test_dir("named-protocol");
- std::fs::create_dir_all(&dir).unwrap();
- let path = dir.join("api.sock");
- let listener = UnixListener::bind(&path).unwrap();
- std::env::set_var(crate::api::SOCKET_PATH_ENV_VAR, &path);
+ std::env::set_var("XDG_CONFIG_HOME", &dir);
std::env::set_var(crate::session::SESSION_ENV_VAR, "work");
+ std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR);
crate::session::clear_explicit_session_for_test();
+ let path = crate::session::api_socket_path_for(Some("work"));
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ let listener = UnixListener::bind(&path).unwrap();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = String::new();
@@ -458,15 +459,20 @@ mod tests {
let _ = handle.join();
assert!(
- message.contains("Stop the old server with `herdr session stop work`"),
+ message.contains("Stop the old server to use the new version"),
+ "unexpected error: {message}"
+ );
+ assert!(
+ message.contains("Run `herdr session stop work`"),
"unexpected error: {message}"
);
assert!(
message.contains("then run `herdr session attach work` again"),
"unexpected error: {message}"
);
- std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR);
+ std::env::remove_var("XDG_CONFIG_HOME");
std::env::remove_var(crate::session::SESSION_ENV_VAR);
+ std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR);
crate::session::clear_explicit_session_for_test();
let _ = std::fs::remove_dir_all(dir);
}
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 3d273b1c..2bf8fa15 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -1359,7 +1359,8 @@ impl HeadlessServer {
.map(|toast| format!("{}: {}", toast.title, toast.context))
} else {
Some(format!(
- "v{version} available: detach, then run `{install_command}`"
+ "v{version} available: {}",
+ crate::update::update_install_instruction(&install_command)
))
}
} else {
@@ -5062,7 +5063,10 @@ next_tab = ""
) {
ServerMessage::Notify { kind, message } => {
assert_eq!(kind, protocol::NotifyKind::SystemToast);
- assert_eq!(message, "v9.9.9 available: detach, then run `herdr update`");
+ assert_eq!(
+ message,
+ "v9.9.9 available: detach, run `herdr update`, then follow its restart guidance"
+ );
}
other => panic!("expected system toast notify, got {other:?}"),
}
diff --git a/src/session.rs b/src/session.rs
index 5e407df8..2e9b0d12 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -111,6 +111,33 @@ pub fn stop_command_for(name: Option<&str>) -> String {
}
}
+pub fn restart_after_update_guidance(stop_command: &str, attach_command: Option<&str>) -> String {
+ let restart = match attach_command {
+ Some(command) => format!("Run `{stop_command}`, then run `{command}` again."),
+ None => format!("Run `{stop_command}`, then restart Herdr with the same socket override."),
+ };
+ format!(
+ "Stop the old server to use the new version.\nStopping exits pane processes.\n{restart}"
+ )
+}
+
+pub fn active_restart_after_update_guidance() -> String {
+ if !explicit_session_requested() {
+ if let Ok(socket_path) = std::env::var(crate::api::SOCKET_PATH_ENV_VAR) {
+ return restart_after_update_guidance(
+ &format!(
+ "{}={} herdr server stop",
+ crate::api::SOCKET_PATH_ENV_VAR,
+ socket_path
+ ),
+ None,
+ );
+ }
+ }
+
+ restart_after_update_guidance(&local_stop_command(), Some(&local_attach_command()))
+}
+
pub fn explicit_session_requested() -> bool {
EXPLICIT_SESSION_REQUESTED.load(Ordering::Relaxed)
}
@@ -699,6 +726,32 @@ mod tests {
}
#[test]
+ fn restart_after_update_guidance_names_stop_and_attach_commands() {
+ assert_eq!(
+ restart_after_update_guidance(
+ "herdr session stop work",
+ Some("herdr session attach work")
+ ),
+ "Stop the old server to use the new version.\nStopping exits pane processes.\nRun `herdr session stop work`, then run `herdr session attach work` again."
+ );
+ }
+
+ #[test]
+ fn active_restart_after_update_guidance_respects_socket_override() {
+ let _guard = env_lock().lock().unwrap();
+ std::env::set_var(crate::api::SOCKET_PATH_ENV_VAR, "/tmp/custom-herdr.sock");
+ std::env::remove_var(SESSION_ENV_VAR);
+ clear_explicit_session_for_test();
+
+ assert_eq!(
+ active_restart_after_update_guidance(),
+ "Stop the old server to use the new version.\nStopping exits pane processes.\nRun `HERDR_SOCKET_PATH=/tmp/custom-herdr.sock herdr server stop`, then restart Herdr with the same socket override."
+ );
+
+ std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR);
+ }
+
+ #[test]
fn explicit_session_socket_ignores_inherited_socket_override() {
let _guard = env_lock().lock().unwrap();
let config_home =
diff --git a/src/ui/release_notes.rs b/src/ui/release_notes.rs
index 1e0769e7..9941a216 100644
--- a/src/ui/release_notes.rs
+++ b/src/ui/release_notes.rs
@@ -425,17 +425,14 @@ fn release_notes_preview_panel_height(area_width: u16, install_command: &str, p:
text_height.saturating_add(3)
}
-pub(super) fn release_notes_preview_lines<'a>(
+pub(super) fn release_notes_preview_lines(
_version: &str,
- install_command: &'a str,
+ install_command: &str,
p: &Palette,
-) -> Vec<Line<'a>> {
+) -> Vec<Line<'static>> {
let title_style = Style::default().fg(p.text).add_modifier(Modifier::BOLD);
let text_style = Style::default().fg(p.text);
- let code_style = Style::default()
- .fg(p.accent)
- .bg(p.surface0)
- .add_modifier(Modifier::BOLD);
+ let instruction = crate::update::update_install_instruction(install_command);
vec![
Line::from(vec![
@@ -445,11 +442,7 @@ pub(super) fn release_notes_preview_lines<'a>(
),
Span::styled(" update ready", title_style),
]),
- Line::from(vec![
- Span::styled("detach from this session, then run ", text_style),
- Span::styled(install_command, code_style),
- Span::styled(" in your shell", text_style),
- ]),
+ Line::from(vec![Span::styled(instruction, text_style)]),
]
}
@@ -586,7 +579,7 @@ mod tests {
assert_eq!(line_text(&lines[0]), "● update ready");
assert_eq!(
line_text(&lines[1]),
- "detach from this session, then run herdr update in your shell"
+ "detach, run `herdr update`, then follow its restart guidance"
);
assert_eq!(lines[0].spans[0].style.fg, Some(palette.accent));
assert_eq!(lines[0].spans[1].style.fg, Some(palette.text));
@@ -641,7 +634,7 @@ mod tests {
.unwrap();
let rendered = buffer_text(terminal.backend().buffer(), area);
- assert!(rendered.contains("your shell"), "{rendered}");
+ assert!(rendered.contains("Herdr session"), "{rendered}");
}
#[test]
diff --git a/src/update.rs b/src/update.rs
index 135fccfd..ed421c89 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -516,8 +516,11 @@ enum RunningServerUpdateOutcome {
#[derive(Debug, Clone, PartialEq, Eq)]
struct RunningSessionUpdateOutcome {
session_label: String,
+ target_noun: &'static str,
stop_command: String,
attach_command: Option<String>,
+ server_version: Option<String>,
+ server_protocol: Option<u32>,
outcome: RunningServerUpdateOutcome,
}
@@ -706,28 +709,34 @@ fn prompt_to_stop_old_servers_before_update(
) -> Result<bool, String> {
if !io::stdin().is_terminal() {
return Err(
- "one or more herdr targets must restart for this update; run `herdr update` from an interactive terminal, or stop those targets and run `herdr update` again"
+ "one or more Herdr sessions must restart for this update. Stop the old server to use the new version, then run `herdr update` again from an interactive terminal."
.to_string(),
);
}
eprintln!(
- "these running herdr targets must restart to use v{}:",
+ "This update changes Herdr's client/server protocol.\n\nRunning sessions that must restart to use v{}:",
release.version
);
for plan in plans {
eprintln!(
- " {}: v{} protocol {}",
+ " {}: server v{} protocol {}",
plan.label(),
version_label(plan.server.version.as_deref()),
protocol_label(plan.server.protocol)
);
}
- eprintln!("herdr can leave them running, or stop them after installing the update.");
- eprintln!("stopping them will exit their pane processes.");
+ eprintln!(
+ " update: v{} protocol {}",
+ release.version,
+ protocol_label(release.target_protocol)
+ );
+ eprintln!();
+ eprintln!("If you choose no, these sessions keep using the old server until you stop them.");
+ eprintln!("Stop the old server after installing? Stopping exits pane processes.");
loop {
- eprint!("stop these old targets after updating? [y/N] ");
+ eprint!("stop after installing? [y/N] ");
io::stderr()
.flush()
.map_err(|e| format!("failed to flush prompt: {e}"))?;
@@ -1322,8 +1331,11 @@ fn apply_running_session_update_decisions(
let stop_command = decision.plan.stop_command();
outcomes.push(RunningSessionUpdateOutcome {
session_label: decision.plan.label().to_string(),
+ target_noun: decision.plan.target_noun(),
stop_command,
attach_command: decision.plan.attach_command(),
+ server_version: decision.plan.server.version.clone(),
+ server_protocol: decision.plan.server.protocol,
outcome,
});
}
@@ -1333,7 +1345,7 @@ fn apply_running_session_update_decisions(
fn print_running_session_update_outcomes(
outcomes: &[RunningSessionUpdateOutcome],
- release: &ReleaseInfo,
+ _release: &ReleaseInfo,
) {
if outcomes.is_empty() {
eprintln!("run herdr again.");
@@ -1356,17 +1368,20 @@ fn print_running_session_update_outcomes(
}
}
RunningServerUpdateOutcome::RestartDeferred => {
- if outcome.attach_command.is_some() {
- eprintln!(
- "session {} will use v{} after it restarts.",
- outcome.session_label, release.version
- );
- } else {
- eprintln!(
- "server {} will use v{} after it restarts.",
- outcome.session_label, release.version
- );
- }
+ eprintln!(
+ "{} {} is still running v{} protocol {}.",
+ outcome.target_noun,
+ outcome.session_label,
+ version_label(outcome.server_version.as_deref()),
+ protocol_label(outcome.server_protocol)
+ );
+ eprintln!(
+ "{}",
+ crate::session::restart_after_update_guidance(
+ &outcome.stop_command,
+ outcome.attach_command.as_deref()
+ )
+ );
}
RunningServerUpdateOutcome::Stopped
| RunningServerUpdateOutcome::FailedHandoffOldServerStopped
@@ -1384,17 +1399,20 @@ fn print_running_session_update_outcomes(
}
}
RunningServerUpdateOutcome::FailedHandoffOldServerKept => {
- if outcome.attach_command.is_some() {
- eprintln!(
- "session {} is still running with your panes; stop it later with `{}` to use v{}.",
- outcome.session_label, outcome.stop_command, release.version
- );
- } else {
- eprintln!(
- "server {} is still running with your panes; stop it later with `{}` to use v{}.",
- outcome.session_label, outcome.stop_command, release.version
- );
- }
+ eprintln!(
+ "{} {} is still running v{} protocol {}.",
+ outcome.target_noun,
+ outcome.session_label,
+ version_label(outcome.server_version.as_deref()),
+ protocol_label(outcome.server_protocol)
+ );
+ eprintln!(
+ "{}",
+ crate::session::restart_after_update_guidance(
+ &outcome.stop_command,
+ outcome.attach_command.as_deref()
+ )
+ );
}
RunningServerUpdateOutcome::FailedHandoffUnknown => {
if let Some(command) = &outcome.attach_command {
@@ -1427,6 +1445,21 @@ pub(crate) fn update_install_command() -> &'static str {
}
}
+pub(crate) fn update_install_instruction(install_command: &str) -> String {
+ match install_command {
+ HERDR_UPDATE_COMMAND => {
+ "detach, run `herdr update`, then follow its restart guidance".to_string()
+ }
+ HOMEBREW_UPDATE_COMMAND => {
+ "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready".to_string()
+ }
+ NIX_UPDATE_COMMAND => {
+ "detach, update through Nix, then restart this Herdr session when ready".to_string()
+ }
+ command => format!("detach, run `{command}`, then restart this Herdr session when ready"),
+ }
+}
+
fn is_homebrew_managed_install() -> bool {
let Ok(current_exe) = env::current_exe() else {
return false;
@@ -2255,6 +2288,18 @@ mod tests {
}
#[test]
+ fn update_install_instruction_distinguishes_install_from_restart() {
+ assert_eq!(
+ update_install_instruction(HERDR_UPDATE_COMMAND),
+ "detach, run `herdr update`, then follow its restart guidance"
+ );
+ assert_eq!(
+ update_install_instruction(HOMEBREW_UPDATE_COMMAND),
+ "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready"
+ );
+ }
+
+ #[test]
fn fake_release_notes_default_to_real_large_changelog_section() {
let _guard = env_lock().lock().unwrap();
std::env::remove_var(FAKE_UPDATE_NOTES_VERSION_ENV);