From 282b387a0671af4546c2aab8a544dcd7e50b9a07 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:40:53 +0800 Subject: [PATCH] fix(ui): fold the names toasts did not compose either MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same rule, same reason, the surface next door. A branch name, a machine label, a settings source path, an agent's display name, and the path out of a terminal hyperlink — which is whatever the program writing to your terminal chose to emit — all went into a notification raw. An error message is deliberately left alone. In a toast the error *is* the content, not a fragment inside a sentence of ours, and git and ssh write genuinely multi-line errors whose second line is the useful one. A dialog embeds one mid-question, so there it is still folded; the guard now carries a different key list per surface and says why. --- CHANGELOG.md | 6 +++- src/terminal/view.rs | 7 +++-- src/ui/app.rs | 47 ++++++++++++++++++++++++++----- src/ui/file_tree.rs | 4 +-- src/ui/mod.rs | 57 +++++++++++++++++++++++++------------- src/ui/remote_workspace.rs | 5 +++- src/ui/settings.rs | 9 ++++-- 7 files changed, 99 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 594d42ee..571d79cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,7 +160,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 could append text of its own to the question authorising the action. Every name a dialog did not compose is now folded onto one line, the rule the file tree, the SFTP list and the sidebar already followed when drawing the same - names. + names. Toasts fold the same names — a branch, a machine, or the path out of a + terminal hyperlink, which is whatever the program writing to your terminal + chose to emit. An error message in a toast is left alone, because there the + message is the content rather than a fragment of a sentence of ours, and git + and ssh write real multi-line errors. - **A tab created and closed again before the window has reconciled no longer leaves its shell running.** This was listed here as a known issue: with a window open, `tab new` immediately followed by `tab close` stranded a live diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 3d0e171d..f72f2602 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -3074,7 +3074,7 @@ impl TerminalView { window.push_notification( crate::ui::i18n::t_fmt( crate::ui::i18n::L10nKey::SftpImagePasteUploadFailed, - &[("host", host), ("error", reason)], + &[("host", &one_line(host)), ("error", reason)], ), cx, ); @@ -3100,7 +3100,10 @@ impl TerminalView { window.push_notification( crate::ui::i18n::t_fmt( crate::ui::i18n::L10nKey::LinkFileOpenFailed, - &[("path", path.as_str()), ("error", reason.as_str())], + &[ + ("path", &one_line(path.as_str())), + ("error", reason.as_str()), + ], ), cx, ); diff --git a/src/ui/app.rs b/src/ui/app.rs index ed6ca492..1d869afa 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -863,7 +863,10 @@ fn close_prompt(ends_the_tab: bool, reason: &CloseReason) -> (String, String) { ), CloseReason::UnsavedEdits(name) => ( t(L10nKey::CloseUnsavedEditsTitle).to_string(), - t_fmt(L10nKey::CloseUnsavedEditsBody, &[("name", name)]), + t_fmt( + L10nKey::CloseUnsavedEditsBody, + &[("name", &crate::terminal::view::one_line(name))], + ), ), CloseReason::Busy(busy) => { let title = match ends_the_tab { @@ -4327,7 +4330,10 @@ impl Tty7App { move |h| crate::core::worktree::remove(h, &wt, force), move |_this, result, window, cx| match result { Ok(()) => window.push_notification( - t_fmt(L10nKey::AppWorktreeRemoved, &[("branch", &branch)]), + t_fmt( + L10nKey::AppWorktreeRemoved, + &[("branch", &crate::terminal::view::one_line(&branch))], + ), cx, ), Err(e) => window.push_notification( @@ -4584,27 +4590,54 @@ impl Tty7App { }; let name = agent.display_name(); if agent.fork_label().is_none() { - window.push_notification(t_fmt(L10nKey::AppForkNoCommand, &[("name", name)]), cx); + window.push_notification( + t_fmt( + L10nKey::AppForkNoCommand, + &[("name", &crate::terminal::view::one_line(name))], + ), + cx, + ); return None; } if remote.is_some() { - window.push_notification(t_fmt(L10nKey::AppForkLocalOnly, &[("name", name)]), cx); + window.push_notification( + t_fmt( + L10nKey::AppForkLocalOnly, + &[("name", &crate::terminal::view::one_line(name))], + ), + cx, + ); return None; } let session = session.unwrap_or_default(); let Some(id) = session.session_id.as_deref() else { - window.push_notification(t_fmt(L10nKey::AppForkNoSessionId, &[("name", name)]), cx); + window.push_notification( + t_fmt( + L10nKey::AppForkNoSessionId, + &[("name", &crate::terminal::view::one_line(name))], + ), + cx, + ); return None; }; let Some(cmd) = agent.fork_command(id, session.launch_argv.as_deref()) else { window.push_notification( - t_fmt(L10nKey::AppForkSessionIdNotToken, &[("name", name)]), + t_fmt( + L10nKey::AppForkSessionIdNotToken, + &[("name", &crate::terminal::view::one_line(name))], + ), cx, ); return None; }; if session.status == AgentStatus::Working { - window.push_notification(t_fmt(L10nKey::AppForkMidTurn, &[("name", name)]), cx); + window.push_notification( + t_fmt( + L10nKey::AppForkMidTurn, + &[("name", &crate::terminal::view::one_line(name))], + ), + cx, + ); } Some(cmd) } diff --git a/src/ui/file_tree.rs b/src/ui/file_tree.rs index 185afa2b..80358d30 100644 --- a/src/ui/file_tree.rs +++ b/src/ui/file_tree.rs @@ -1031,7 +1031,7 @@ impl Tty7App { t_fmt( L10nKey::LinkFileOpenFailed, &[ - ("path", &path.display().to_string()), + ("path", &one_line(&path.display().to_string())), ("error", &crate::ui::host_ops::explain_io(&e)), ], ), @@ -1042,7 +1042,7 @@ impl Tty7App { window.push_notification( t_fmt( L10nKey::LinkDirOutsideTree, - &[("path", &path.display().to_string())], + &[("path", &one_line(&path.display().to_string()))], ), cx, ); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f20a4769..00c2af73 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -106,7 +106,7 @@ mod tests { assert!(keep.is_cancel(), "the cancel is marked, not guessed"); } - /// Every name a confirmation dialog did not compose is folded onto one + /// Every name a dialog or a toast did not compose is folded onto one /// line before it goes in. /// /// `terminal::view::one_line` says the rule out loud — "anything that @@ -126,13 +126,28 @@ mod tests { /// `{name}`, `{path}`, `{branch}` and the rest are always someone else's /// bytes, while `{verb}` and `{n}` are ours. #[test] - fn a_dialog_folds_every_name_it_did_not_compose() { + fn a_dialog_or_toast_folds_every_name_it_did_not_compose() { /// Substitution keys whose value is never text this app wrote. - const THEIRS: [&str; 9] = [ - "name", "path", "branch", "machine", "host", "file", "subject", "author", "error", + /// + /// A dialog's `{error}` is checked too. A toast's is not: there the + /// message *is* the content rather than a fragment inside a sentence + /// of ours, and git and ssh write genuinely multi-line errors whose + /// second line is the useful one. A dialog embeds it mid-question. + const NAMES: [&str; 8] = [ + "name", "path", "branch", "machine", "host", "file", "subject", "author", + ]; + let surfaces: [(&str, &[&str]); 2] = [ + ( + ".prompt(", + &[ + "name", "path", "branch", "machine", "host", "file", "subject", "author", + "error", + ], + ), + ("push_notification(", &NAMES), ]; - fn walk(dir: &std::path::Path, found: &mut Vec) { + fn walk(dir: &std::path::Path, surfaces: &[(&str, &[&str]); 2], found: &mut Vec) { let mut entries: Vec<_> = std::fs::read_dir(dir) .expect("the ui sources are readable") .filter_map(Result::ok) @@ -141,22 +156,24 @@ mod tests { entries.sort(); for path in entries { if path.is_dir() { - walk(&path, found); + walk(&path, surfaces, found); continue; } if path.extension().is_none_or(|e| e != "rs") { continue; } let src = std::fs::read_to_string(&path).expect("a source file reads"); - for (n, region) in prompt_regions(&src) { - for key in THEIRS { - for value in substitutions(®ion, key) { - if !value.contains("one_line(") { - found.push(format!( - "{}:{} — {{{key}}} is {value}", - path.display(), - n - )); + for (call, keys) in *surfaces { + for (n, region) in call_regions(&src, call) { + for key in keys.iter().copied() { + for value in substitutions(®ion, key) { + if !value.contains("one_line(") { + found.push(format!( + "{}:{} — {{{key}}} is {value}", + path.display(), + n + )); + } } } } @@ -164,13 +181,13 @@ mod tests { } } - /// Each `prompt(` call in `src`, as (1-based line, argument text). - fn prompt_regions(src: &str) -> Vec<(usize, String)> { + /// Each `call` site in `src`, as (1-based line, argument text). + fn call_regions(src: &str, call: &str) -> Vec<(usize, String)> { let mut out = Vec::new(); let bytes = src.as_bytes(); let mut at = 0; - while let Some(hit) = src[at..].find(".prompt(") { - let open = at + hit + ".prompt(".len(); + while let Some(hit) = src[at..].find(call) { + let open = at + hit + call.len(); let mut depth = 1usize; let mut end = open; while end < bytes.len() && depth > 0 { @@ -216,7 +233,7 @@ mod tests { let ui = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); assert!(ui.is_dir(), "the sources moved: {ui:?}"); let mut found = Vec::new(); - walk(&ui, &mut found); + walk(&ui, &surfaces, &mut found); assert!( found.is_empty(), "these dialogs interpolate a name nobody folded, so the name gets \ diff --git a/src/ui/remote_workspace.rs b/src/ui/remote_workspace.rs index d7c7c934..06e3622f 100644 --- a/src/ui/remote_workspace.rs +++ b/src/ui/remote_workspace.rs @@ -353,7 +353,10 @@ impl Tty7App { _ => { let machine = self.remote_machine_label(cx); window.push_notification( - t_fmt(L10nKey::RemoteNoConnectionDetails, &[("machine", &machine)]), + t_fmt( + L10nKey::RemoteNoConnectionDetails, + &[("machine", &one_line(&machine))], + ), cx, ); false diff --git a/src/ui/settings.rs b/src/ui/settings.rs index dbad4662..ab8ad8f0 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -4077,7 +4077,7 @@ impl Tty7App { window.push_notification( Notification::error(t_fmt( L10nKey::SettingsImportUnreadable, - &[("path", &source)], + &[("path", &crate::terminal::view::one_line(&source))], )) .id1::(NOTIFICATION), cx, @@ -4086,8 +4086,11 @@ impl Tty7App { } if report.profiles.is_empty() { window.push_notification( - Notification::warning(t_fmt(L10nKey::SettingsImportNoHosts, &[("path", &source)])) - .id1::(NOTIFICATION), + Notification::warning(t_fmt( + L10nKey::SettingsImportNoHosts, + &[("path", &crate::terminal::view::one_line(&source))], + )) + .id1::(NOTIFICATION), cx, ); return;