mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
fix(ssh): make the Override button on a changed host key actually override
`host_key_changed_decision` returns `accept: false` for anything but "yes", which is byte-for-byte what Abort sends — and the button had no disabled state and closed the sheet unconditionally. So clicking Override with an empty field rejected the key and dismissed the prompt, indistinguishable from having aborted, with nothing said. Enter on the input had the same trap. Override is now dead until the word is there, which is what the line above the field has been claiming all along, and Enter on a half-typed answer leaves the sheet up instead of quietly deciding. `changed_confirmed` is the single predicate behind both, so the button and the decision cannot disagree about what "yes" means. `host_key_changed_decision`'s `false` branch stays as defence in depth. Both input subscriptions also notify on `Change`, or the enabled flag would go stale between keystrokes, and a hint appears once the field holds something that is not "yes". Abort is untouched: still primary, still last.
This commit is contained in:
@@ -873,6 +873,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
"You already know this host by a {previous_algorithm} key. This is a new \
|
||||
{algorithm} key, not a replacement for that one."
|
||||
}
|
||||
L10nKey::SshPromptTypeYesToOverride => "Type \"yes\" to enable Override.",
|
||||
L10nKey::EditorCantOpen => "Could not open {path}: {e}",
|
||||
L10nKey::EditorCantRead => "Could not read {path}: {e}",
|
||||
L10nKey::EditorNotUtf8 => "\"{path}\" is not valid UTF-8",
|
||||
|
||||
@@ -914,6 +914,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SshPromptHostKeyNewAlgorithm => {
|
||||
"このホストはすでに {previous_algorithm} キーで登録されています。これはそれを置き換えるものではなく、新しい {algorithm} キーです"
|
||||
}
|
||||
L10nKey::SshPromptTypeYesToOverride => "「yes」を入力すると「上書き」が有効になります",
|
||||
L10nKey::EditorCantOpen => "{path} を開けません: {e}",
|
||||
L10nKey::EditorCantRead => "{path} を読み取れません: {e}",
|
||||
L10nKey::EditorNotUtf8 => "「{path}」は有効な UTF-8 ではありません",
|
||||
|
||||
@@ -670,6 +670,7 @@ l10n_keys! {
|
||||
SshPromptNewKey,
|
||||
SshPromptOldKey,
|
||||
SshPromptHostKeyNewAlgorithm,
|
||||
SshPromptTypeYesToOverride,
|
||||
EditorCantOpen,
|
||||
EditorCantRead,
|
||||
EditorNotUtf8,
|
||||
|
||||
@@ -835,6 +835,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SshPromptHostKeyNewAlgorithm => {
|
||||
"你已经通过一把 {previous_algorithm} 密钥认识这台主机。这是一把新的 {algorithm} 密钥,并不是用来替换那一把的。"
|
||||
}
|
||||
L10nKey::SshPromptTypeYesToOverride => "输入 yes 才能启用“覆盖”。",
|
||||
L10nKey::EditorCantOpen => "无法打开 {path}:{e}",
|
||||
L10nKey::EditorCantRead => "无法读取 {path}:{e}",
|
||||
L10nKey::EditorNotUtf8 => "“{path}”不是有效的 UTF-8",
|
||||
|
||||
+79
-12
@@ -5,7 +5,7 @@ use gpui::{
|
||||
use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::checkbox::Checkbox;
|
||||
use gpui_component::input::{Input, InputEvent, InputState};
|
||||
use gpui_component::{ActiveTheme as _, Sizable as _, h_flex, v_flex};
|
||||
use gpui_component::{ActiveTheme as _, Disableable as _, Sizable as _, h_flex, v_flex};
|
||||
|
||||
use crate::core::keychain::{CredentialStore as _, OsCredentialStore};
|
||||
use crate::daemon::protocol::{AuthPromptKind, AuthResponse, SshPhase};
|
||||
@@ -317,10 +317,14 @@ impl Tty7App {
|
||||
subs.push(cx.subscribe_in(
|
||||
input,
|
||||
window,
|
||||
|this, _input, ev: &InputEvent, window, cx| {
|
||||
if matches!(ev, InputEvent::PressEnter { .. }) {
|
||||
this.submit_ssh_prompt(window, cx);
|
||||
}
|
||||
|this, _input, ev: &InputEvent, window, cx| match ev {
|
||||
InputEvent::PressEnter { .. } => this.submit_ssh_prompt(window, cx),
|
||||
// The changed-host sheet enables Override off the
|
||||
// typed text, so the flag has to be recomputed
|
||||
// between keystrokes rather than at whatever
|
||||
// repaint happened to come along.
|
||||
InputEvent::Change => cx.notify(),
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -363,10 +367,12 @@ impl Tty7App {
|
||||
subs.push(cx.subscribe_in(
|
||||
input,
|
||||
window,
|
||||
|this, _input, ev: &InputEvent, window, cx| {
|
||||
if matches!(ev, InputEvent::PressEnter { .. }) {
|
||||
this.submit_ssh_prompt(window, cx);
|
||||
}
|
||||
|this, _input, ev: &InputEvent, window, cx| match ev {
|
||||
InputEvent::PressEnter { .. } => this.submit_ssh_prompt(window, cx),
|
||||
// Same reason as the pane-routed path above: Override's
|
||||
// enabled state is read off the input on every repaint.
|
||||
InputEvent::Change => cx.notify(),
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -398,6 +404,18 @@ impl Tty7App {
|
||||
.map(|i| i.read(cx).value().to_string())
|
||||
.collect();
|
||||
|
||||
// A changed host key is the one prompt where submitting the wrong thing
|
||||
// is indistinguishable from aborting: the decision it sends for
|
||||
// anything but "yes" is byte-for-byte what Abort sends, and the sheet
|
||||
// closed either way. So Enter on a half-typed answer looked like the
|
||||
// app had swallowed the connection. Leave the sheet up instead; the
|
||||
// rejection stays available on Abort, where the user meant it.
|
||||
if let PromptModel::HostKeyChanged { .. } = &model {
|
||||
if !changed_confirmed(values.first().map(String::as_str).unwrap_or_default()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let (response, write) = match &model {
|
||||
PromptModel::Password {
|
||||
user,
|
||||
@@ -787,8 +805,20 @@ impl Tty7App {
|
||||
old_fingerprint,
|
||||
port,
|
||||
host,
|
||||
} => card
|
||||
.child(div().text_xs().text_color(danger).child(crate::ui::i18n::t(
|
||||
} => {
|
||||
// Override without "yes" typed used to send the *rejection* —
|
||||
// the same bytes Abort sends — and close the sheet, so the
|
||||
// button read as a way through and behaved as a way out. It is
|
||||
// dead until the word is there, which is what the line above
|
||||
// the field has been claiming all along.
|
||||
let typed = self
|
||||
.ssh_prompt
|
||||
.inputs
|
||||
.first()
|
||||
.map(|i| i.read(cx).value().to_string())
|
||||
.unwrap_or_default();
|
||||
let can_override = changed_confirmed(&typed);
|
||||
card.child(div().text_xs().text_color(danger).child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::SshPromptHostKeyChangedBody,
|
||||
)))
|
||||
.child(div().text_xs().child(format!("{host}:{port} {algorithm}")))
|
||||
@@ -815,6 +845,18 @@ impl Tty7App {
|
||||
crate::ui::i18n::L10nKey::HostKeyOverrideMessage,
|
||||
)))
|
||||
.child(self.render_ssh_input(0))
|
||||
// Only once they have typed something: an empty field is not a
|
||||
// mistake to be corrected, it is where everyone starts.
|
||||
.when(!typed.trim().is_empty() && !can_override, |c| {
|
||||
c.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::SshPromptTypeYesToOverride,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
@@ -826,6 +868,7 @@ impl Tty7App {
|
||||
Button::new("ssh-hkc-override")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Override))
|
||||
.small()
|
||||
.disabled(!can_override)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.submit_ssh_prompt(window, cx)
|
||||
})),
|
||||
@@ -839,7 +882,8 @@ impl Tty7App {
|
||||
this.cancel_ssh_prompt(window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
card.into_any_element()
|
||||
@@ -1046,6 +1090,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `changed_confirmed` is also what enables the Override button, so the
|
||||
/// button and the decision can never disagree about what "yes" means — the
|
||||
/// bug was a button that offered a way through and sent the rejection.
|
||||
#[test]
|
||||
fn override_is_enabled_by_exactly_what_accepts() {
|
||||
for typed in ["", " ", "y", "no", "yesss"] {
|
||||
assert!(
|
||||
!changed_confirmed(typed),
|
||||
"{typed:?} must leave Override disabled"
|
||||
);
|
||||
assert_eq!(
|
||||
host_key_changed_decision(typed),
|
||||
AuthResponse::HostKeyDecision {
|
||||
accept: false,
|
||||
remember: false
|
||||
}
|
||||
);
|
||||
}
|
||||
for typed in ["yes", "YES", " yes "] {
|
||||
assert!(changed_confirmed(typed), "{typed:?} must enable Override");
|
||||
}
|
||||
}
|
||||
|
||||
/// The host is known, just not by this algorithm — the mild confirmation,
|
||||
/// carrying the algorithm it *is* known by, and never the danger sheet.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user