From cfacbcc13ef8a12dcf15ba995e6a0baca485d96c Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:22:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(ssh):=20'Forget=20password'=20entry=20in?= =?UTF-8?q?=20the=20profile=20=E2=8B=AF=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ui/settings.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 96467cf9..e1b97b51 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -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, + ) -> Option { + use crate::core::keychain::{CredentialStore, OsCredentialStore}; + let (user, host, port) = cx + .global::() + .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) -> AnyElement {