From 6fa353f8ca98661a2f436e7aae3710edc5679739 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:43:21 +0800 Subject: [PATCH] fix(ui): one confirmation idiom, and copy that names what it is looking at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The SSH close confirmation was the app's only bespoke in-app dialog: no scrim, no Escape, no click-outside, no focus — the two buttons were the only way out, and an on_key_down there could never have fired because nothing inside the card is focusable. It now asks through the same platform prompt as the other eleven confirmations, which gets Escape and dismissal from the OS. - The switcher's tab column reported "No workspace or machine matches." That is the other column's noun. - ja left "bind" and "to" in English inside a form whose other labels are Japanese; zh had already translated both. - The bind and target host fields in an SSH forward rule were pinned at 104px, which does not hold an EC2 internal hostname. Same floor, but they now take a share of the row's slack instead of handing all of it to the free-text description beside them. - docs/features.md still described ⌃⇥ as "next tab"; since the two-column switcher landed it holds a panel open and commits on release. Also drops two more names for actions the app calls Zoom Pane and Clear Scrollback. --- docs/features.md | 7 ++--- docs/features.zh-CN.md | 7 ++--- src/ui/app.rs | 33 +++++++++++++++++++---- src/ui/forwards.rs | 61 ------------------------------------------ src/ui/i18n/en.rs | 1 + src/ui/i18n/ja.rs | 5 ++-- src/ui/i18n/mod.rs | 2 ++ src/ui/i18n/zh.rs | 1 + src/ui/settings.rs | 20 +++++++++++--- src/ui/switcher.rs | 2 +- 10 files changed, 61 insertions(+), 78 deletions(-) diff --git a/docs/features.md b/docs/features.md index d8672561..dec624b6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -95,12 +95,13 @@ Keys are shown in macOS notation — on Windows and Linux, read a | | | |---|---| | ⌘ T · ⌘ W · ⌘ ⇧ T | new tab · close tab · reopen closed tab | -| ⌘ 1⌘ 9 · ⌃ ⇥ · ⌃ ⇧ ⇥ | jump to tab 1–9 · next tab · previous tab | +| ⌘ 1⌘ 9 | jump to tab 1–9 | +| ⌃ ⇥ · ⌃ ⇧ ⇥ | hold to walk the switcher forwards · backwards; it commits when you let go | | ⌘ D · ⌘ ⇧ D | split right · split down | | ⌘ ] · ⌘ [ | next pane · previous pane | | ⌘ ⌥ ←→↑↓ | focus the pane in that direction | -| ⌘ ⏎ · ⌘ ⇧ ⏎ | toggle fullscreen · maximize / restore the pane | -| ⌘ K | clear the screen and scrollback | +| ⌘ ⏎ · ⌘ ⇧ ⏎ | toggle fullscreen · zoom pane | +| ⌘ K | clear scrollback | | ⌘ P | command palette | | ⌘ F | search the scrollback | | ⌃ R | fuzzy-search shell history | diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md index 982524a0..ec93d185 100644 --- a/docs/features.zh-CN.md +++ b/docs/features.zh-CN.md @@ -92,12 +92,13 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包 | | | |---|---| | ⌘ T · ⌘ W · ⌘ ⇧ T | 新建标签页 · 关闭标签页 · 恢复关闭的标签页 | -| ⌘ 1⌘ 9 · ⌃ ⇥ · ⌃ ⇧ ⇥ | 跳到第 1–9 个标签页 · 下一个 · 上一个标签页 | +| ⌘ 1⌘ 9 | 跳到第 1–9 个标签页 | +| ⌃ ⇥ · ⌃ ⇧ ⇥ | 按住不放在切换面板里向后 · 向前走,松手即切换 | | ⌘ D · ⌘ ⇧ D | 向右分屏 · 向下分屏 | | ⌘ ] · ⌘ [ | 下一个窗格 · 上一个窗格 | | ⌘ ⌥ ←→↑↓ | 按方向切换焦点窗格 | -| ⌘ ⏎ · ⌘ ⇧ ⏎ | 切换全屏 · 最大化 / 还原窗格 | -| ⌘ K | 清屏并清空 scrollback | +| ⌘ ⏎ · ⌘ ⇧ ⏎ | 切换全屏 · 缩放窗格 | +| ⌘ K | 清除 scrollback | | ⌘ P | 命令面板 | | ⌘ F | 搜索 scrollback | | ⌃ R | 模糊搜索 shell 历史 | diff --git a/src/ui/app.rs b/src/ui/app.rs index bf5c4880..0c38bd5a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -2480,7 +2480,7 @@ impl Tty7App { fn close_pane(&mut self, window: &mut Window, cx: &mut Context) { if self.ssh_close_confirm.is_none() && self.focused_pane_is_warn_ssh(window, cx) { self.ssh_close_confirm = Some(SshCloseKind::Pane); - cx.notify(); + self.ask_ssh_close(window, cx); return; } self.ssh_close_confirm = None; @@ -2732,7 +2732,7 @@ impl Tty7App { let already_confirming = self.ssh_close_confirm == Some(SshCloseKind::Tab(index)); if !already_confirming && self.tab_has_warn_ssh(index, cx) { self.ssh_close_confirm = Some(SshCloseKind::Tab(index)); - cx.notify(); + self.ask_ssh_close(window, cx); return; } self.ssh_close_confirm = None; @@ -4449,6 +4449,32 @@ impl Tty7App { .unwrap_or(false) } + /// The app asks every other question of this class through the platform's + /// own dialog. This one used to be a bespoke in-app card with no scrim, no + /// Escape and no click-outside — the two buttons were the only way out. + fn ask_ssh_close(&mut self, window: &mut Window, cx: &mut Context) { + let answer = window.prompt( + PromptLevel::Warning, + t(crate::ui::i18n::L10nKey::CloseSshConnectionTitle), + Some(t(crate::ui::i18n::L10nKey::CloseSshConnectionBody)), + &[ + t(crate::ui::i18n::L10nKey::Keep), + t(crate::ui::i18n::L10nKey::Close), + ], + cx, + ); + cx.spawn_in(window, async move |this, cx| { + let close = matches!(answer.await, Ok(1)); + let _ = this.update_in(cx, |this, window, cx| match close { + true => this.confirm_ssh_close(window, cx), + // Cancelled, or the window went away with the question open: + // either way the connection stays up. + false => this.cancel_ssh_close(cx), + }); + }) + .detach(); + } + pub(crate) fn confirm_ssh_close(&mut self, window: &mut Window, cx: &mut Context) { match self.ssh_close_confirm { Some(SshCloseKind::Tab(i)) => self.close_tab(i, window, cx), @@ -5204,9 +5230,6 @@ impl Render for Tty7App { .when_some(self.render_remote_input_notice(cx), |this, el| { this.child(el) }) - .when_some(self.render_ssh_close_confirm_overlay(cx), |this, el| { - this.child(el) - }) .when_some(self.render_worktree_prompt_overlay(cx), |this, el| { this.child(el) }); diff --git a/src/ui/forwards.rs b/src/ui/forwards.rs index 45ec7faa..3deb1d72 100644 --- a/src/ui/forwards.rs +++ b/src/ui/forwards.rs @@ -74,67 +74,6 @@ impl Tty7App { ) } - pub(crate) fn render_ssh_close_confirm_overlay( - &self, - cx: &mut Context, - ) -> Option { - self.ssh_close_confirm?; - let theme = cx.theme(); - let card = v_flex() - .w(px(360.)) - .gap_3() - .p_4() - .bg(theme.popover) - .border_1() - .border_color(theme.border) - .rounded_lg() - .shadow_lg() - .occlude() - .child( - div() - .font_weight(FontWeight::SEMIBOLD) - .child(t(crate::ui::i18n::L10nKey::CloseSshConnectionTitle)), - ) - .child( - div() - .text_sm() - .text_color(theme.muted_foreground) - .child(t(crate::ui::i18n::L10nKey::CloseSshConnectionBody)), - ) - .child( - h_flex() - .justify_end() - .gap_2() - .child( - Button::new("ssh-close-cancel") - .label(t(crate::ui::i18n::L10nKey::Keep)) - .small() - .on_click( - cx.listener(|this, _, _window, cx| this.cancel_ssh_close(cx)), - ), - ) - .child( - Button::new("ssh-close-confirm") - .label(t(crate::ui::i18n::L10nKey::Close)) - .primary() - .small() - .on_click(cx.listener(|this, _, window, cx| { - this.confirm_ssh_close(window, cx) - })), - ), - ); - Some( - div() - .absolute() - .inset_0() - .flex() - .items_center() - .justify_center() - .child(card) - .into_any_element(), - ) - } - pub(crate) fn forwards_section( &self, pane_id: Option, diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index f8767753..b4d9438e 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -981,6 +981,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SwitcherRename => "Rename…", L10nKey::SwitcherPickAWorkspace => "Pick a workspace to see its tabs", L10nKey::SwitcherNoTabs => "No tabs in this workspace", + L10nKey::SwitcherNoTabMatch => "No tab matches.", L10nKey::SwitcherTabsAfterOpening => "Open this workspace to see its tabs", L10nKey::SwitcherTabCount => "{n} tabs", L10nKey::SwitcherTabCountOne => "1 tab", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 2641042d..91139b56 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -776,8 +776,8 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::ForwardLocal => "ローカル", L10nKey::ForwardRemote => "リモート", L10nKey::ForwardDynamic => "ダイナミック", - L10nKey::ForwardBindLabel => "bind", - L10nKey::ForwardToLabel => "to", + L10nKey::ForwardBindLabel => "バインド", + L10nKey::ForwardToLabel => "転送先", L10nKey::ForwardSocksLabel => "SOCKS", L10nKey::ForwardAdd => "追加", L10nKey::FileTreePlaceholderFileName => "ファイル名", @@ -1012,6 +1012,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SwitcherRename => "名前を変更…", L10nKey::SwitcherPickAWorkspace => "ワークスペースを選ぶとタブが表示されます", L10nKey::SwitcherNoTabs => "このワークスペースにタブはありません", + L10nKey::SwitcherNoTabMatch => "一致するタブがありません。", L10nKey::SwitcherTabsAfterOpening => "このワークスペースを開くとタブが表示されます", L10nKey::SwitcherTabCount => "{n} 個のタブ", L10nKey::SwitcherTabCountOne => "1 個のタブ", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index add96591..0212b0c3 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -806,6 +806,7 @@ pub enum L10nKey { SwitcherRename, SwitcherPickAWorkspace, SwitcherNoTabs, + SwitcherNoTabMatch, SwitcherTabsAfterOpening, SwitcherTabCount, SwitcherTabCountOne, @@ -1822,6 +1823,7 @@ mod tests { L10nKey::SwitcherRename, L10nKey::SwitcherPickAWorkspace, L10nKey::SwitcherNoTabs, + L10nKey::SwitcherNoTabMatch, L10nKey::SwitcherTabsAfterOpening, L10nKey::SwitcherTabCount, L10nKey::SwitcherTabCountOne, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index cd02070f..c8aa3dd1 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -930,6 +930,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SwitcherRename => "重命名…", L10nKey::SwitcherPickAWorkspace => "选一个工作区查看它的标签页", L10nKey::SwitcherNoTabs => "这个工作区没有标签页", + L10nKey::SwitcherNoTabMatch => "没有匹配的标签页。", L10nKey::SwitcherTabsAfterOpening => "打开这个工作区后才能看到它的标签页", L10nKey::SwitcherTabCount => "{n} 个标签页", L10nKey::SwitcherTabCountOne => "1 个标签页", diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 3a893306..581a7686 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -2906,7 +2906,16 @@ impl Tty7App { h_flex() .gap_1() .items_center() - .child(div().w(px(104.)).child(Input::new(host).xsmall())) + // Was pinned at 104px, which does not hold + // `ip-10-0-3-217.eu-west-1.compute.internal`. Same floor, but + // the field now takes a share of the row's slack instead of + // handing all of it to the free-text description beside it. + .child( + div() + .flex_1() + .min_w(px(104.)) + .child(Input::new(host).xsmall()), + ) .child(div().text_xs().text_color(muted).child(":")) .child(div().w(px(58.)).child(Input::new(port).xsmall())) }; @@ -2937,10 +2946,15 @@ impl Tty7App { } }, )) - .child(endpoint(&row.bind_host, &row.bind_port)) - .child(div().text_xs().text_color(muted).child("→")) .child( div() + .flex_1() + .child(endpoint(&row.bind_host, &row.bind_port)), + ) + .child(div().flex_shrink_0().text_xs().text_color(muted).child("→")) + .child( + div() + .flex_1() .opacity(if needs_target { 1.0 } else { 0.35 }) .child(endpoint(&row.target_host, &row.target_port)), ) diff --git a/src/ui/switcher.rs b/src/ui/switcher.rs index 8658ae13..2a29558a 100644 --- a/src/ui/switcher.rs +++ b/src/ui/switcher.rs @@ -1921,7 +1921,7 @@ impl Tty7App { .unwrap_or_default(); let hits = visible_tabs(row, &query); if hits.is_empty() { - return note(t(L10nKey::SwitcherNoMatch).to_string()); + return note(t(L10nKey::SwitcherNoTabMatch).to_string()); } let sf = rungs(cx);