feat(ssh): 'Forget password' entry in the profile ⋯ menu

Deletes the keychain-stored password for the profile's endpoint
(user@host:port); the profile is untouched and the next connect re-prompts.
No-op when nothing is stored. Surfaces a window notification. Credentials are
endpoint-keyed, so this matches only when the profile pins an explicit user.
This commit is contained in:
l0ng-ai
2026-07-14 11:22:21 +08:00
parent c1ab77b5f4
commit cfacbcc13e
+41 -1
View File
@@ -20,7 +20,9 @@ use gpui_component::select::{SearchableVec, Select, SelectState};
use gpui_component::sidebar::{Sidebar, SidebarCollapsible, SidebarMenu, SidebarMenuItem};
use gpui_component::slider::{Slider, SliderState};
use gpui_component::switch::Switch;
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex};
use gpui_component::{
ActiveTheme as _, Icon, IconName, Sizable as _, WindowExt as _, h_flex, v_flex,
};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
@@ -1520,6 +1522,18 @@ impl Tty7App {
let _ = app.update(cx, |this, cx| this.duplicate_profile(id, window, cx));
}
}))
.item(PopupMenuItem::new("Forget password").on_click({
let app = app.clone();
move |_, window, cx| {
if let Some(msg) = app
.update(cx, |this, cx| this.forget_profile_password(id, cx))
.ok()
.flatten()
{
window.push_notification(msg, cx);
}
}
}))
.separator();
// Destructive, last, in danger red and set apart by the separator above.
@@ -1870,6 +1884,32 @@ impl Tty7App {
}
}
/// Remove any keychain-stored password for this profile's endpoint
/// (`user@host:port`). The profile itself is untouched — the next connect will
/// prompt again. A no-op if nothing was stored. Returns a status line for the
/// caller to surface as a notification. Credentials are keyed by endpoint, not
/// profile, so this only matches when the profile pins an explicit user.
pub(crate) fn forget_profile_password(
&mut self,
id: Uuid,
cx: &mut Context<Self>,
) -> Option<String> {
use crate::core::keychain::{CredentialStore, OsCredentialStore};
let (user, host, port) = cx
.global::<Config>()
.ssh_profiles
.iter()
.find(|p| p.id == id)
.map(|p| (p.user.clone(), p.host.clone(), p.port))?;
let endpoint = format!("{user}@{host}:{port}");
Some(
match OsCredentialStore.delete_password(&user, &host, port) {
Ok(()) => format!("Forgot saved password for {endpoint}"),
Err(e) => format!("Couldn't forget password for {endpoint}: {e}"),
},
)
}
/// The inline edit form: four core fields + collapsible jump / forwards /
/// advanced, rendered below the profile list for the selected profile.
fn render_ssh_profile_form(&self, cx: &mut Context<Self>) -> AnyElement {