From 5d5fa77d0fedc025d9f2a81d77e4867f7e12068b Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:36:49 +0800 Subject: [PATCH] fix(tray): say what quit-and-stop takes that does not come back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last way out of the app that had not learned about unwritten buffers. ⌘Q, the window close and the update relaunch each ask; the tray's Quit and Stop asked its own question — every shell ends — and never mentioned them. Worse than silence, its body reassures: "your tabs and layout reopen with fresh shells next launch". They do. A buffer nobody has written down does not, so a reader who accepts on the strength of that sentence loses something the sentence implied was safe. It is the delete prompt's mistake in another place: precise about what survives, quiet about what does not. Named in the body rather than raised as a second dialog. They are already being asked one question about what they are about to lose, and that answer should account for all of it. --- src/ui/app.rs | 18 +++++++++++++++++- src/ui/code_editor.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ src/ui/i18n/en.rs | 1 + src/ui/i18n/ja.rs | 1 + src/ui/i18n/mod.rs | 1 + src/ui/i18n/zh.rs | 1 + 6 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 3d1f07a4..a64ddf08 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1816,10 +1816,26 @@ impl Tty7App { fn quit_stop_sessions(&mut self, window: &mut Window, cx: &mut Context) { cx.activate(true); window.activate_window(); + // The body promises the tabs and layout come back. A buffer nobody has + // written down does not, so when there is one it is named here rather + // than raised as a second dialog — the reader is already being asked + // one question about what they are about to lose, and the answer to it + // should account for all of it. + let body = match Self::quit_loses_unwritten_work(cx) { + None => t(crate::ui::i18n::L10nKey::QuitStopServerBody).to_string(), + Some((_, _, name)) => format!( + "{} {}", + t(crate::ui::i18n::L10nKey::QuitStopServerBody), + t_fmt( + crate::ui::i18n::L10nKey::QuitStopServerUnsaved, + &[("name", &name)] + ) + ), + }; let answer = window.prompt( PromptLevel::Warning, t(crate::ui::i18n::L10nKey::QuitStopServerTitle), - Some(t(crate::ui::i18n::L10nKey::QuitStopServerBody)), + Some(&body), &crate::ui::confirm_answers( t(crate::ui::i18n::L10nKey::QuitAndStop), t(crate::ui::i18n::L10nKey::Cancel), diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index bed9e5f7..a97d374e 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -2031,6 +2031,50 @@ mod unsaved_close_gpui_tests { let _ = (first, vcx); } + /// Quit-and-stop names the buffer it is about to take. + /// + /// That prompt already asks a question about loss — it says every shell + /// ends — and reassures that "your tabs and layout reopen with fresh + /// shells next launch". Unwritten text does not reopen, so a reader who + /// accepts on the strength of that sentence loses something the sentence + /// implied was safe. Named in the body rather than raised as a second + /// dialog: they are already being asked once, and that answer should + /// account for all of it. + #[gpui::test] + fn quit_and_stop_names_an_unwritten_buffer_in_its_body(cx: &mut TestAppContext) { + use crate::ui::i18n::{L10nKey, t, t_fmt}; + + cx.update(crate::ui::windows::WindowRegistry::init); + let (app, mut vcx, _streams) = harness_with_tabs(cx, 2); + app.update_in(&mut vcx, |app, window, cx| { + let (ws, weak) = (app.workspace, cx.weak_entity()); + crate::ui::windows::WindowRegistry::register(cx, ws, window.window_handle(), weak); + }); + + // Nothing unwritten: the body is the plain one, unchanged. + cx.update(|cx| { + assert!(crate::ui::app::Tty7App::quit_loses_unwritten_work(cx).is_none()); + }); + + app.update_in(&mut vcx, |app, window, cx| { + app.editor_seed_dirty_file_for_test("/w/repo/notes.md", window, cx); + }); + + let (_, _, name) = cx + .update(crate::ui::app::Tty7App::quit_loses_unwritten_work) + .expect("quit-and-stop would take it"); + assert_eq!(name, "notes.md"); + + // The sentence the body gains says the changes do not come back — + // which is the half the plain body does not cover. + let added = t_fmt(L10nKey::QuitStopServerUnsaved, &[("name", &name)]); + assert!(added.contains("notes.md"), "it names the file: {added}"); + assert!( + !t(L10nKey::QuitStopServerBody).contains("notes.md"), + "and the plain body never did" + ); + } + /// Building the buffer the way `editor_install_file` does. fn new_buffer( text: &str, diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 766f22ba..cbcd7813 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -21,6 +21,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::CloseUnsavedEditsTitle => "Close this tab?", L10nKey::CloseWindowUnsavedEditsTitle => "Close this window?", L10nKey::QuitUnsavedEditsTitle => "Quit tty7?", + L10nKey::QuitStopServerUnsaved => "{name} has unsaved changes, and those do not come back.", L10nKey::RelaunchUnsavedEditsTitle => "Relaunch to update?", L10nKey::HomeReopenClosedTab => "Reopen Closed Tab", L10nKey::HomeSwitchWorkspace => "Switch Workspace…", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index d3e9d6cd..a9dd877f 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -21,6 +21,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::CloseUnsavedEditsTitle => "このタブを閉じますか?", L10nKey::CloseWindowUnsavedEditsTitle => "このウィンドウを閉じますか?", L10nKey::QuitUnsavedEditsTitle => "tty7 を終了しますか?", + L10nKey::QuitStopServerUnsaved => "{name} に未保存の変更があります。これは復元されません。", L10nKey::RelaunchUnsavedEditsTitle => "更新のため再起動しますか?", L10nKey::HomeReopenClosedTab => "閉じたタブをもう一度開く", L10nKey::HomeSwitchWorkspace => "ワークスペースを切り替える…", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 117ed4bd..f07dad3f 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -99,6 +99,7 @@ l10n_keys! { CloseUnsavedEditsTitle, CloseWindowUnsavedEditsTitle, QuitUnsavedEditsTitle, + QuitStopServerUnsaved, RelaunchUnsavedEditsTitle, HomeReopenClosedTab, HomeSwitchWorkspace, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 3704bc11..4ab7ab35 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -21,6 +21,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::CloseUnsavedEditsTitle => "关闭这个标签页?", L10nKey::CloseWindowUnsavedEditsTitle => "要关闭此窗口吗?", L10nKey::QuitUnsavedEditsTitle => "要退出 tty7 吗?", + L10nKey::QuitStopServerUnsaved => "{name} 有未保存的更改,这些不会恢复。", L10nKey::RelaunchUnsavedEditsTitle => "要重新启动以更新吗?", L10nKey::HomeReopenClosedTab => "重新打开已关闭的标签页", L10nKey::HomeSwitchWorkspace => "切换工作区…",