mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(ui): fold the names toasts did not compose either
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.
This commit is contained in:
+5
-1
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
+40
-7
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
+37
-20
@@ -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<String>) {
|
||||
fn walk(dir: &std::path::Path, surfaces: &[(&str, &[&str]); 2], found: &mut Vec<String>) {
|
||||
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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-3
@@ -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::<Self>(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::<Self>(NOTIFICATION),
|
||||
Notification::warning(t_fmt(
|
||||
L10nKey::SettingsImportNoHosts,
|
||||
&[("path", &crate::terminal::view::one_line(&source))],
|
||||
))
|
||||
.id1::<Self>(NOTIFICATION),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user