diff --git a/src/ui/app.rs b/src/ui/app.rs index eae9ffc9..86683e26 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -488,12 +488,27 @@ impl Tty7App { app } + /// Put a version-mismatched local server to the user, with no third way out. + /// + /// Both handshakes compare their version for equality and hang up on + /// anything else — the pane protocol in `daemon::spawn::ensure_running`, the + /// control dialect in `host::server`'s hello. So a server whose number + /// disagrees cannot be talked round, and carrying on beside it is not a + /// degraded mode but a broken one: panes still spawn while every + /// machine-tree call is refused, which is how a window comes to open with no + /// tabs and save none of the ones you make. This used to be offered as "Keep + /// Shells", and taking it was indistinguishable from the bug. + /// + /// Restart or quit, then. Quitting is the half that destroys nothing: the + /// server and every shell under it keep running, which is what makes it a + /// real answer for someone who would rather go install the matching build + /// than lose a session mid-flight. fn prompt_daemon_version_mismatch(window: &mut Window, cx: &mut Context) { let Some(mismatch) = crate::daemon::spawn::take_mismatched_daemon() else { return; }; let ours = crate::daemon::protocol::PROTOCOL_VERSION; - let detail = match mismatch { + let detail = match &mismatch { DaemonMismatch::Protocol(Some(v)) => t_fmt( L10nKey::AppRestartServerMismatchDetail, &[ @@ -520,18 +535,31 @@ impl Tty7App { ], ), }; + // Quit first, like every other destructive prompt here. NSAlert and + // TaskDialog both give the first button Return, and this one arrives + // unasked at launch — the moment a stray Return is most likely — so the + // key that lands by reflex has to be the one that destroys nothing. + // Restarting is a click or an arrow key away, and it is the same shape + // as `restart_daemon` below: safe first, act on the second. let answer = window.prompt( PromptLevel::Warning, t(L10nKey::AppRestartServerTitle), Some(&detail), - &[t(L10nKey::AppKeepShells), t(L10nKey::AppRestart)], + &[t(L10nKey::CmdQuitTty7), t(L10nKey::RestartServer)], cx, ); - cx.spawn(async move |this, cx| { - if !matches!(answer.await, Ok(1)) { - return; + cx.spawn(async move |this, cx| match answer.await { + Ok(1) => { + let _ = this.update_in(cx, |this, _window, cx| this.restart_daemon_confirmed(cx)); } - let _ = this.update_in(cx, |this, _window, cx| this.restart_daemon_confirmed(cx)); + Ok(_) => { + let _ = cx.update(|cx| cx.quit()); + } + // Dismissed without an answer: the window went away before the + // question was settled. Arm it again so the next window asks, rather + // than letting the state this prompt exists to prevent slip through + // the gap. + Err(_) => crate::daemon::spawn::note_daemon_mismatch(mismatch), }) .detach(); } diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 72295f10..6b645fc0 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -994,7 +994,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SshPromptSubmit => "Submit", L10nKey::HostOpsError => "{context}: {error}", L10nKey::TreeWindowOpenedEmpty => { - "This window's server never handed over its tabs, so it opened empty. Nothing was thrown away — the tabs come back once the server answers, and \"Restart Server\" in the command palette is what to reach for if it doesn't." + "This window's server never handed over its tabs, so it opened empty. Nothing was lost — they come back when it answers. If it doesn't, run \"Restart Server\" from the command palette." } L10nKey::CmdGroupTabsPanes => "Tabs & Panes", L10nKey::CmdGroupWorkspaces => "Workspaces", @@ -1088,18 +1088,17 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::CmdRecent => "Recent", L10nKey::AppRestartServerTitle => "Restart Server?", L10nKey::AppRestartServerMismatchDetail => { - "The server holding your shells is from another build (v{build}, protocol {protocol} — this app speaks {ours}). You can keep using it and your shells stay, but features whose wire format changed may misbehave until it's restarted. Restarting starts a clean server: tabs reopen with fresh shells and anything running in them is terminated." + "The server holding your shells is build v{build}, protocol {protocol}; this app speaks {ours}. They can't talk, so your tabs are out of reach.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells, and anything running now is killed." } L10nKey::AppRestartServerDialectDetail => { - "The server holding your shells is from an older build (v{build}): it speaks control dialect v{dialect}, this app speaks v{ours}. Your shells keep running, but it cannot hand this app your tabs, so windows open empty until it's restarted. Restarting starts a clean server: tabs reopen with fresh shells and anything running in them is terminated." + "The server holding your shells is build v{build}: control dialect v{dialect}, where this app speaks v{ours}. It can't hand over your tabs, so every window opens empty.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells, and anything running now is killed." } L10nKey::AppRestartServerDialectNewerDetail => { - "The server holding your shells is from a newer build (v{build}): it speaks control dialect v{dialect}, this app speaks v{ours}. Your shells keep running, but it cannot hand this app your tabs, so windows open empty. Updating this app is the real fix; restarting the server replaces it with one this app can talk to, but tabs reopen with fresh shells and anything running in them is terminated." + "The server holding your shells is build v{build}: control dialect v{dialect}, where this app speaks v{ours}. It can't hand over your tabs, so every window opens empty.\n\nQuit and install the newer build: the real fix, and your shells survive it.\nRestart: tabs come back with fresh shells, and anything running now is killed." } L10nKey::AppRestartServerOldDetail => { - "The server holding your shells is from an older version of the app. You can keep using it and your shells stay, but newer features may misbehave until it's restarted. Restarting starts a clean server: tabs reopen with fresh shells and anything running in them is terminated." + "The server holding your shells predates the version handshake, so this app can't tell what it speaks.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells, and anything running now is killed." } - L10nKey::AppKeepShells => "Keep Shells", L10nKey::AppRestart => "Restart", L10nKey::AppRestartServerNotSsh => { "tty7 can only restart the server on machines it reaches over SSH. {label} is served from this computer — stop its workspace instead." diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index a2dda072..42e4fe05 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1027,7 +1027,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SshPromptSubmit => "送信", L10nKey::HostOpsError => "{context}: {error}", L10nKey::TreeWindowOpenedEmpty => { - "このウィンドウのサーバーがタブを渡さなかったため、空のまま開きました。失われたものはありません。サーバーが応答すればタブは戻ります。戻らない場合はコマンドパレットの「サーバーを再起動」を使ってください" + "このウィンドウのサーバーがタブを渡さなかったため、空のまま開きました。失われたものはなく、応答すれば戻ります。戻らない場合はコマンドパレットの「サーバーを再起動」を実行してください" } L10nKey::CmdGroupTabsPanes => "タブとペイン", L10nKey::CmdGroupWorkspaces => "ワークスペース", @@ -1121,18 +1121,17 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::CmdRecent => "最近", L10nKey::AppRestartServerTitle => "サーバーを再起動しますか?", L10nKey::AppRestartServerMismatchDetail => { - "シェルを保持中のサーバーは別のビルドです(v{build}、プロトコル {protocol} — このアプリは {ours} を使用)。そのまま使ってもシェルは残せますが、プロトコルが変わった機能は再起動まで正しく動かない可能性があります。再起動すると新しいサーバーが起動します。タブは新しいシェルで開きます。実行中のものはすべて終了します" + "サーバーは v{build}、プロトコル {protocol}。このアプリは {ours} です。噛み合わないため、タブを取り出せません。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectDetail => { - "シェルを保持中のサーバーは古いビルドです(v{build})。制御方言 v{dialect} を使用し、このアプリは v{ours} を使用しています。シェルはそのまま動き続けますが、タブをこのアプリに渡せないため、再起動するまでウィンドウは空のまま開きます。再起動すると新しいサーバーが起動します。タブは新しいシェルで開きます。実行中のものはすべて終了します" + "サーバーは v{build}:制御方言 v{dialect}、このアプリは v{ours} です。タブを渡せないため、ウィンドウはどれも空のまま開きます。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectNewerDetail => { - "シェルを保持中のサーバーは新しいビルドです(v{build})。制御方言 v{dialect} を使用し、このアプリは v{ours} を使用しています。シェルはそのまま動き続けますが、タブをこのアプリに渡せないため、ウィンドウは空のまま開きます。根本的な解決はこのアプリを更新することです。サーバーを再起動すればこのアプリと話せるものに入れ替わりますが、タブは新しいシェルで開き、実行中のものはすべて終了します" + "サーバーは v{build}:制御方言 v{dialect}、このアプリは v{ours} です。タブを渡せないため、ウィンドウはどれも空のまま開きます。\n\n終了して新しいビルドを入れる:根本的な解決で、シェルはそのまま残ります。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerOldDetail => { - "シェルを保持中のサーバーは、アプリの古いバージョンのものです。そのまま使ってもシェルは残せますが、新しい機能は再起動まで正しく動かない可能性があります。再起動すると新しいサーバーが起動します。タブは新しいシェルで開きます。実行中のものはすべて終了します" + "サーバーはバージョン照合より前のもので、何を話すか分かりません。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } - L10nKey::AppKeepShells => "シェルを保持", L10nKey::AppRestart => "再起動", L10nKey::AppRestartServerNotSsh => { "tty7 は SSH で到達できるマシン上のサーバーしか再起動できません。{label} はこのコンピュータで実行されています。代わりにそのワークスペースを止めてください" diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index b74e6e8a..9d3b825e 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -915,7 +915,6 @@ pub enum L10nKey { AppRestartServerDialectDetail, AppRestartServerDialectNewerDetail, AppRestartServerOldDetail, - AppKeepShells, AppRestart, AppRestartServerNotSsh, AppRestartServerBody, @@ -1932,7 +1931,6 @@ mod tests { L10nKey::AppRestartServerDialectDetail, L10nKey::AppRestartServerDialectNewerDetail, L10nKey::AppRestartServerOldDetail, - L10nKey::AppKeepShells, L10nKey::AppRestart, L10nKey::AppRestartServerNotSsh, L10nKey::AppRestartServerBody, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 864a8b22..7568d59c 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -942,7 +942,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SshPromptSubmit => "提交", L10nKey::HostOpsError => "{context}:{error}", L10nKey::TreeWindowOpenedEmpty => { - "这个窗口的服务器没有交出标签页,所以窗口是空的。什么都没有丢——服务器一旦响应,标签页就会回来;如果一直不回来,请在命令面板里执行「重启服务器」。" + "这个窗口的服务器没有交出标签页,所以窗口是空的。什么都没丢,它一响应就会回来。如果一直不回来,在命令面板里执行「重启服务器」。" } L10nKey::CmdGroupTabsPanes => "标签页与窗格", L10nKey::CmdGroupWorkspaces => "工作区", @@ -1036,18 +1036,17 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::CmdRecent => "最近使用", L10nKey::AppRestartServerTitle => "重启服务器?", L10nKey::AppRestartServerMismatchDetail => { - "正在运行你 shell 的服务器来自另一个构建(v{build},协议 {protocol};此应用使用 {ours})。你可以继续使用,shell 也会保留,但协议格式已变更的功能可能会表现异常,直到重启服务器。重启会启动一个干净的服务器:标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。" + "服务器是 v{build},协议 {protocol};此应用使用 {ours}。两者无法对话,标签页取不出来。\n\n退出:什么都不变,服务器和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerDialectDetail => { - "正在运行你 shell 的服务器来自较旧的构建(v{build}):它使用 control 方言 v{dialect},此应用使用 v{ours}。shell 会继续运行,但它无法把标签页交给此应用,在重启之前窗口都会是空的。重启会启动一个干净的服务器:标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。" + "服务器是 v{build}:control 方言 v{dialect},而此应用使用 v{ours}。它交不出标签页,所以每个窗口都开成空的。\n\n退出:什么都不变,服务器和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerDialectNewerDetail => { - "正在运行你 shell 的服务器来自更新的构建(v{build}):它使用 control 方言 v{dialect},此应用使用 v{ours}。shell 会继续运行,但它无法把标签页交给此应用,所以窗口会是空的。真正的解法是升级此应用;重启服务器也能换成此应用能对话的版本,但标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。" + "服务器是 v{build}:control 方言 v{dialect},而此应用使用 v{ours}。它交不出标签页,所以每个窗口都开成空的。\n\n退出并装上更新的构建:真正的解法,shell 全都还在。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerOldDetail => { - "正在运行你 shell 的服务器来自应用的旧版本。你可以继续使用,shell 也会保留,但新功能可能会表现异常,直到重启服务器。重启会启动一个干净的服务器:标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。" + "服务器早于版本握手,此应用无从得知它说的是什么。\n\n退出:什么都不变,服务器和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } - L10nKey::AppKeepShells => "保留 Shell", L10nKey::AppRestart => "重启", L10nKey::AppRestartServerNotSsh => { "tty7 只能重启通过 SSH 连接的机器上的服务器。{label} 由本机提供服务——请改为停止其工作区。"