fix(tray): say what quit-and-stop takes that does not come back

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.
This commit is contained in:
l0ng-ai
2026-08-23 13:36:49 +08:00
parent 4440d78e99
commit 5d5fa77d0f
6 changed files with 65 additions and 1 deletions
+17 -1
View File
@@ -1816,10 +1816,26 @@ impl Tty7App {
fn quit_stop_sessions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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),
+44
View File
@@ -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,
+1
View File
@@ -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…",
+1
View File
@@ -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 => "ワークスペースを切り替える…",
+1
View File
@@ -99,6 +99,7 @@ l10n_keys! {
CloseUnsavedEditsTitle,
CloseWindowUnsavedEditsTitle,
QuitUnsavedEditsTitle,
QuitStopServerUnsaved,
RelaunchUnsavedEditsTitle,
HomeReopenClosedTab,
HomeSwitchWorkspace,
+1
View File
@@ -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 => "切换工作区…",