From f3c3976d80258c76cfe22bbf5c4a838f50536509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=A3=9E?= <1835698775@qq.com> Date: Fri, 18 Sep 2026 20:05:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(terminal):=20=E4=B8=B4=E6=97=B6=20SSH=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=94=AF=E6=8C=81=E4=BF=9D=E5=AD=98=E4=B8=BA?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=EF=BC=8C=E5=B9=B6=E8=A1=A5=E9=BD=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=BE=A7=E8=BE=B9=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复:需要运行时输入凭据的 SSH 连接(临时连接)此前完全没有文件侧边栏——侧边栏只在构造时算一次可用面板,那时还拿不到 SshSessionManager。现在可用面板集合允许重算(TerminalToolDockState::update_available_panels),凭据就绪后按需补建文件管理器 / 服务器监控面板,同一修复也覆盖「已保存但每次输入密码」的连接。\n- 新增「保存为连接」入口:终端工具栏按钮(仅临时连接展示)与首页快速连接结果项,事件经 TerminalSidebarEvent -> TerminalPaneEvent -> TerminalWorkspaceEvent 上抛到主程序。\n- 保存流程:从运行时 ssh_config 合并用户名 / 密码生成待保存连接、清除每次输入标记,名称由「SSH xxx (temporary)」改为 user@host:port;SshFormWindow 预填后保存,密码随连接写入凭据库,全程不落日志。\n- i18n:新增 TerminalSidebar.save_as_connection、Home.save_as_connection、Home.save_as_connection_done。\n- 测试:新增运行时凭据合并 3 个用例、工具面板集合增删 1 个用例。 --- .../terminal_view/locales/terminal_view.yml | 4 + crates/terminal_view/src/ftp_form_window.rs | 136 ++++--- crates/terminal_view/src/lib.rs | 4 +- crates/terminal_view/src/sidebar/mod.rs | 350 ++++++++++++++++-- .../src/sidebar/settings_panel.rs | 8 +- crates/terminal_view/src/ssh_form_window.rs | 20 +- crates/terminal_view/src/transfer_notice.rs | 7 +- crates/terminal_view/src/view.rs | 5 + .../src/view/background_tasks.rs | 4 +- .../terminal_view/src/view/initialization.rs | 4 + .../terminal_view/src/view/sidebar_events.rs | 181 +++++++++ .../terminal_view/src/view/terminal_events.rs | 2 + crates/terminal_view/src/view/tests/core.rs | 8 +- .../src/view/workspace_support.rs | 2 + crates/terminal_view/src/workspace/actions.rs | 3 + crates/terminal_view/src/workspace/view.rs | 2 + main/locales/main.yml | 8 + main/src/home/home_connection_quick_open.rs | 31 +- main/src/home/home_tabs.rs | 3 + main/src/home_tab/connection_forms.rs | 34 ++ main/src/main.rs | 2 +- 21 files changed, 670 insertions(+), 148 deletions(-) diff --git a/crates/terminal_view/locales/terminal_view.yml b/crates/terminal_view/locales/terminal_view.yml index 0862f141e..89f4c285f 100644 --- a/crates/terminal_view/locales/terminal_view.yml +++ b/crates/terminal_view/locales/terminal_view.yml @@ -1254,6 +1254,10 @@ TerminalSidebar: en: Server Monitor zh-CN: 服务器监控 zh-HK: 伺服器監控 + save_as_connection: + en: Save as connection + zh-CN: 保存为连接 + zh-HK: 儲存為連線 LocalTerminalClose: title: diff --git a/crates/terminal_view/src/ftp_form_window.rs b/crates/terminal_view/src/ftp_form_window.rs index 2c36f3102..bb88931bd 100644 --- a/crates/terminal_view/src/ftp_form_window.rs +++ b/crates/terminal_view/src/ftp_form_window.rs @@ -8,11 +8,6 @@ use connection_form::team::{ refresh_teams_tooltip, resolve_team_assignment, selected_team_id, team_label, team_management_enabled, }; -use one_core::cloud_sync::TeamOption; -use one_core::connection_notifier::{ConnectionDataEvent, get_notifier}; -use one_core::gpui_tokio::Tokio; -use one_core::storage::traits::Repository; -use one_core::storage::{FtpParams, StoredConnection, Workspace}; use ftp::FtpClient; use gpui::prelude::FluentBuilder; use gpui::{ @@ -30,6 +25,11 @@ use gpui_component::{ v_flex, }; use one_assets::IconName; +use one_core::cloud_sync::TeamOption; +use one_core::connection_notifier::{ConnectionDataEvent, get_notifier}; +use one_core::gpui_tokio::Tokio; +use one_core::storage::traits::Repository; +use one_core::storage::{FtpParams, StoredConnection, Workspace}; use rust_i18n::t; pub struct FtpFormWindowConfig { @@ -121,11 +121,7 @@ pub struct FtpFormWindow { } impl FtpFormWindow { - pub fn new( - config: FtpFormWindowConfig, - window: &mut Window, - cx: &mut Context, - ) -> Self { + pub fn new(config: FtpFormWindowConfig, window: &mut Window, cx: &mut Context) -> Self { let is_editing = config.editing_connection.is_some(); let editing_id = config.editing_connection.as_ref().and_then(|c| c.id); let editing_cloud_id = config @@ -568,16 +564,10 @@ impl Render for FtpFormWindow { v_flex() .gap_2() .child( - self.render_form_row( - &t!("Ftp.name"), - Input::new(&self.name_input), - ), + self.render_form_row(&t!("Ftp.name"), Input::new(&self.name_input)), ) .child( - self.render_form_row( - &t!("Ftp.host"), - Input::new(&self.host_input), - ), + self.render_form_row(&t!("Ftp.host"), Input::new(&self.host_input)), ) .child( self.render_form_row( @@ -599,57 +589,57 @@ impl Render for FtpFormWindow { self.credential_picker.clone(), )) .when(credential_is_manual, |form| { - form.child( - self.render_form_row( - &t!("Ftp.username"), - Input::new(&self.username_input), - ), - ) - .child( - self.render_form_row( - &t!("Ftp.password"), - Input::new(&self.password_input), - ), - ) + form.child(self.render_form_row( + &t!("Ftp.username"), + Input::new(&self.username_input), + )) + .child(self.render_form_row( + &t!("Ftp.password"), + Input::new(&self.password_input), + )) }) - .child(self.render_form_row( - &t!("Ftp.passive_mode"), - h_flex() - .gap_2() - .child( - Checkbox::new("passive-mode") - .checked(self.passive_mode) - .on_click(cx.listener(|this, _, _, cx| { - this.passive_mode = !this.passive_mode; - cx.notify(); - })), - ) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child(t!("Ftp.passive_mode_desc").to_string()), - ), - )) - .child(self.render_form_row( - &t!("Ftp.use_tls"), - h_flex() - .gap_2() - .child( - Checkbox::new("use-tls") - .checked(self.use_tls) - .on_click(cx.listener(|this, _, _, cx| { - this.use_tls = !this.use_tls; - cx.notify(); - })), - ) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child(t!("Ftp.use_tls_desc").to_string()), - ), - )) + .child( + self.render_form_row( + &t!("Ftp.passive_mode"), + h_flex() + .gap_2() + .child( + Checkbox::new("passive-mode") + .checked(self.passive_mode) + .on_click(cx.listener(|this, _, _, cx| { + this.passive_mode = !this.passive_mode; + cx.notify(); + })), + ) + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(t!("Ftp.passive_mode_desc").to_string()), + ), + ), + ) + .child( + self.render_form_row( + &t!("Ftp.use_tls"), + h_flex() + .gap_2() + .child( + Checkbox::new("use-tls") + .checked(self.use_tls) + .on_click(cx.listener(|this, _, _, cx| { + this.use_tls = !this.use_tls; + cx.notify(); + })), + ) + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(t!("Ftp.use_tls_desc").to_string()), + ), + ), + ) .child(self.render_form_row( &t!("Ftp.workspace"), Select::new(&self.workspace_select).w_full(), @@ -705,12 +695,10 @@ impl Render for FtpFormWindow { ), ) }) - .child( - self.render_form_row( - &t!("Ftp.remark"), - Textarea::new(&self.remark_input), - ), - ), + .child(self.render_form_row( + &t!("Ftp.remark"), + Textarea::new(&self.remark_input), + )), ), ) // 测试结果 diff --git a/crates/terminal_view/src/lib.rs b/crates/terminal_view/src/lib.rs index 5d493a4a7..15b8517cb 100644 --- a/crates/terminal_view/src/lib.rs +++ b/crates/terminal_view/src/lib.rs @@ -4,10 +4,10 @@ pub mod addon; pub mod broadcast_input; mod broadcast_registry; pub mod cd_completion; +pub mod ftp_form_window; pub mod highlight_presets; pub mod history_prompt; mod host_key_dialog; -pub mod ftp_form_window; pub mod keys; pub mod public_mcp; pub mod public_mcp_remote_ops; @@ -26,6 +26,7 @@ pub mod view; pub mod workspace; pub use addon::{AddonManager, HoveredLink, SearchAddon, TerminalAddon, WebLinksAddon}; +pub use ftp_form_window::{FtpFormWindow, FtpFormWindowConfig}; pub use one_core::layout::{ SIDEBAR_DEFAULT_WIDTH, SIDEBAR_MAX_WIDTH, SIDEBAR_MIN_WIDTH, TOOLBAR_WIDTH, }; @@ -36,7 +37,6 @@ pub use settings::{ pub use sidebar::{SettingsPanel, SidebarPanel, TerminalSidebar, TerminalSidebarEvent}; pub use ssh_form_window::{SshFormPostSaveAction, SshFormWindow, SshFormWindowConfig}; pub use telnet_form_window::{TelnetFormWindow, TelnetFormWindowConfig}; -pub use ftp_form_window::{FtpFormWindow, FtpFormWindowConfig}; pub use terminal::terminal::{ ConnectionState, SshTerminalConfig, Terminal, TerminalConnectionKind, TerminalModelEvent, }; diff --git a/crates/terminal_view/src/sidebar/mod.rs b/crates/terminal_view/src/sidebar/mod.rs index d5a9733e4..949be1d3f 100644 --- a/crates/terminal_view/src/sidebar/mod.rs +++ b/crates/terminal_view/src/sidebar/mod.rs @@ -374,6 +374,32 @@ fn terminal_toolbar_icon_button( .tooltip(panel.title()) } +fn terminal_toolbar_save_as_connection_button( + sidebar: Entity, + colors: &TerminalColors, + item_size: Size, + cx: &App, +) -> IconButton { + let style = ButtonCustomVariant::new(cx) + .foreground(colors.foreground) + .hover(colors.muted) + .active(colors.muted); + + IconButton::new( + SharedString::from("terminal-toolbar-save-as-connection"), + IconName::Save, + ) + .hit_size(item_size) + .glyph_size(OneIconSize::Small) + .custom(style) + .tooltip(t!("TerminalSidebar.save_as_connection").to_string()) + .on_click(move |_, _window, cx| { + sidebar.update(cx, |sidebar, cx| { + sidebar.request_save_as_connection(cx); + }); + }) +} + fn terminal_sidebar_available_panels( has_file_explorer: bool, has_file_manager: bool, @@ -438,6 +464,33 @@ impl TerminalToolDockState { Self { panels, states } } + /// 重算可用面板集合,保留已有面板的展开状态与位置。 + /// + /// 需要运行时输入凭据的 SSH 连接(临时连接等)在构造侧边栏时还没有 + /// `SshSessionManager`,文件管理器与服务器监控面板只能在凭据就绪后补建, + /// 因此可用面板集合必须允许在生命周期内变化。 + pub fn update_available_panels( + &mut self, + panels: impl IntoIterator, + ) -> bool { + let mut seen = HashSet::new(); + let next = panels + .into_iter() + .filter(|panel| seen.insert(*panel)) + .collect::>(); + if next == self.panels { + return false; + } + self.states.retain(|panel, _| next.contains(panel)); + for panel in &next { + self.states + .entry(*panel) + .or_insert_with(ToolPanelState::default); + } + self.panels = next; + true + } + pub fn toolbar_visible(&self) -> bool { true } @@ -613,10 +666,32 @@ pub enum TerminalSidebarEvent { OpenSftp(StoredConnection), /// 在终端中 cd 到指定路径 CdToTerminal(String), + /// 把当前临时连接保存为正式连接 + SaveAsConnection, /// 请求将终端当前工作目录同步到文件管理器 SyncWorkingDir, } +/// 需要等凭据就绪才能补建的 SSH 工具面板所需的构造输入。 +/// +/// 临时连接这类“运行时输入凭据”的 SSH 会话在构造侧边栏时还没有 +/// `SshSessionManager`,文件管理器与服务器监控面板只能在凭据提交后创建。 +#[derive(Clone)] +struct PendingSshToolPanels { + connection_id: Option, + auto_show_server_monitor: bool, + /// 服务器监控依赖 SSH 会话配置,非 SSH 会话不创建 + ssh_session: bool, + /// 文件管理器需要完整连接信息(含 host/端口/用户名) + stored_connection: Option, +} + +impl PendingSshToolPanels { + fn is_empty(&self) -> bool { + self.stored_connection.is_none() && !self.ssh_session + } +} + /// 终端侧边栏组件 pub struct TerminalSidebar { /// 终端工具面板 dock 状态 @@ -639,6 +714,10 @@ pub struct TerminalSidebar { server_monitor_panel: Option>, /// 路径与终端同步开关(默认开启) sync_path_enabled: bool, + /// 当前终端对应的连接;临时连接的 `id` 为空,可用于“保存为连接” + stored_connection: Option, + /// 尚未创建的 SSH 工具面板(凭据就绪后补建) + pending_ssh_tool_panels: Option, /// 焦点句柄 focus_handle: FocusHandle, /// 终端主题配色(用于侧边栏工具栏) @@ -647,12 +726,181 @@ pub struct TerminalSidebar { _subs: Vec, } +fn subscribe_file_manager_panel( + cx: &mut Context, + panel: &Entity, +) -> Subscription { + cx.subscribe(panel, |this, _, event: &FileManagerPanelEvent, cx| { + this.handle_file_manager_event(event, cx); + }) +} + +fn subscribe_server_monitor_panel( + cx: &mut Context, + panel: &Entity, +) -> Subscription { + cx.subscribe(panel, |this, _, event: &ServerMonitorPanelEvent, cx| { + this.handle_server_monitor_event(event, cx); + }) +} + impl TerminalSidebar { pub(crate) fn refresh_quick_commands(&mut self, cx: &mut Context) { self.quick_command_panel .update(cx, |panel, cx| panel.load_commands(cx)); } + fn handle_file_manager_event(&mut self, event: &FileManagerPanelEvent, cx: &mut Context) { + match event { + FileManagerPanelEvent::Close => { + self.close_tool(SidebarPanel::FileManager, cx); + } + FileManagerPanelEvent::MoveTo(placement) => { + self.move_tool(SidebarPanel::FileManager, *placement, cx); + } + FileManagerPanelEvent::OpenSftp(connection) => { + cx.emit(TerminalSidebarEvent::OpenSftp(connection.clone())); + } + FileManagerPanelEvent::CdToTerminal(path) => { + cx.emit(TerminalSidebarEvent::CdToTerminal(path.clone())); + } + FileManagerPanelEvent::SyncWorkingDir => { + cx.emit(TerminalSidebarEvent::SyncWorkingDir); + } + FileManagerPanelEvent::ToggleFollowTerminalCwd => { + let enabled = !self.sync_path_enabled; + self.set_sync_path_enabled(enabled, cx); + if let Some(fm_panel) = &self.file_manager_panel { + fm_panel.update(cx, |panel, cx| { + panel.set_follow_terminal_cwd(enabled, cx); + }); + } + cx.emit(TerminalSidebarEvent::SyncPathChanged(enabled)); + } + } + } + + fn handle_server_monitor_event( + &mut self, + event: &ServerMonitorPanelEvent, + cx: &mut Context, + ) { + match event { + ServerMonitorPanelEvent::Close => { + self.close_tool(SidebarPanel::ServerMonitor, cx); + } + } + } + + /// 当前终端是否为需要保存的临时连接(无数据库 ID)。 + pub(crate) fn save_as_connection_available(&self) -> bool { + self.stored_connection + .as_ref() + .is_some_and(|connection| connection.id.is_none()) + } + + /// 临时连接的连接信息;已是正式连接时返回 `None`。 + pub(crate) fn temporary_connection(&self) -> Option { + self.stored_connection + .as_ref() + .filter(|connection| connection.id.is_none()) + .cloned() + } + + /// 请求把当前临时连接保存为正式连接。 + pub(crate) fn request_save_as_connection(&self, cx: &mut Context) { + if !self.save_as_connection_available() { + return; + } + cx.emit(TerminalSidebarEvent::SaveAsConnection); + } + + /// 文件管理器 / 服务器监控面板是否都已可用(或本来就不适用)。 + pub(crate) fn ssh_tool_panels_ready(&self) -> bool { + let Some(pending) = self.pending_ssh_tool_panels.as_ref() else { + return true; + }; + let file_manager_ready = + pending.stored_connection.is_none() || self.file_manager_panel.is_some(); + let server_monitor_ready = !pending.ssh_session || self.server_monitor_panel.is_some(); + file_manager_ready && server_monitor_ready + } + + /// 凭据就绪后补建 SSH 工具面板。 + /// + /// 只补建缺失的面板,重复调用是幂等的;新面板会重新计算工具栏可用项。 + pub(crate) fn install_ssh_tool_panels( + &mut self, + session_manager: Arc, + window: &mut Window, + cx: &mut Context, + ) { + let Some(pending) = self.pending_ssh_tool_panels.clone() else { + return; + }; + let colors = self.colors.clone(); + let mut installed = false; + + let file_manager_connection = pending + .stored_connection + .clone() + .filter(|_| self.file_manager_panel.is_none()); + if let Some(connection) = file_manager_connection { + let panel = cx.new(|cx| { + FileManagerPanel::new( + connection, + session_manager.clone(), + colors.clone(), + window, + cx, + ) + }); + let sync_path_enabled = self.sync_path_enabled; + panel.update(cx, |panel, cx| { + panel.set_follow_terminal_cwd(sync_path_enabled, cx); + }); + self._subs.push(subscribe_file_manager_panel(cx, &panel)); + self.file_manager_panel = Some(panel); + installed = true; + } + + if self.server_monitor_panel.is_none() && pending.ssh_session { + let panel = cx.new(|cx| { + ServerMonitorPanel::new( + pending.connection_id, + session_manager, + pending.auto_show_server_monitor, + colors, + cx, + ) + }); + self._subs.push(subscribe_server_monitor_panel(cx, &panel)); + self.server_monitor_panel = Some(panel); + installed = true; + } + + if installed { + self.refresh_available_panels(cx); + } + } + + /// 按当前实际存在的面板刷新工具栏可用项。 + fn refresh_available_panels(&mut self, cx: &mut Context) -> bool { + let changed = self + .tool_dock + .update_available_panels(terminal_sidebar_available_panels( + self.file_explorer_panel.is_some(), + self.file_manager_panel.is_some(), + self.server_monitor_panel.is_some(), + self.history_command_panel.is_some(), + self.broadcast_input_panel.is_some(), + )); + if changed { + cx.notify(); + } + changed + } + pub(crate) fn new( connection_id: Option, connection_kind: TerminalConnectionKind, @@ -671,6 +919,8 @@ impl TerminalSidebar { ) -> Self { let colors = initial_theme.colors(); let has_file_manager = stored_connection.is_some(); + // 保留一份连接信息:临时连接(`id` 为空)据此支持“保存为连接”。 + let sidebar_connection = stored_connection.clone(); let broadcast_supported = ssh_config.is_some(); if broadcast_supported { init_broadcast_input_registry(cx); @@ -680,6 +930,14 @@ impl TerminalSidebar { .map(|config| config.ssh_config.username.clone()) .or_else(|| std::env::var("USER").ok()); let auto_show_server_monitor = ServerMonitorPanel::load_monitor_enabled(connection_id); + // 临时连接等需要运行时输入凭据的 SSH 会话在这里还拿不到 + // `SshSessionManager`,文件管理器 / 服务器监控面板要等凭据提交后补建。 + let pending_ssh_tool_panels = PendingSshToolPanels { + connection_id, + auto_show_server_monitor, + ssh_session: ssh_config.is_some(), + stored_connection: stored_connection.clone(), + }; let settings_panel = cx.new(|cx| { SettingsPanel::new( initial_theme, @@ -945,38 +1203,7 @@ impl TerminalSidebar { // 订阅文件管理器面板事件 if let Some(ref fm_panel) = file_manager_panel { - let fm_sub = - cx.subscribe( - fm_panel, - |this, _, event: &FileManagerPanelEvent, cx| match event { - FileManagerPanelEvent::Close => { - this.close_tool(SidebarPanel::FileManager, cx); - } - FileManagerPanelEvent::MoveTo(placement) => { - this.move_tool(SidebarPanel::FileManager, *placement, cx); - } - FileManagerPanelEvent::OpenSftp(connection) => { - cx.emit(TerminalSidebarEvent::OpenSftp(connection.clone())); - } - FileManagerPanelEvent::CdToTerminal(path) => { - cx.emit(TerminalSidebarEvent::CdToTerminal(path.clone())); - } - FileManagerPanelEvent::SyncWorkingDir => { - cx.emit(TerminalSidebarEvent::SyncWorkingDir); - } - FileManagerPanelEvent::ToggleFollowTerminalCwd => { - let enabled = !this.sync_path_enabled; - this.set_sync_path_enabled(enabled, cx); - if let Some(fm_panel) = &this.file_manager_panel { - fm_panel.update(cx, |panel, cx| { - panel.set_follow_terminal_cwd(enabled, cx); - }); - } - cx.emit(TerminalSidebarEvent::SyncPathChanged(enabled)); - } - }, - ); - subs.push(fm_sub); + subs.push(subscribe_file_manager_panel(cx, fm_panel)); } if let Some(ref explorer) = file_explorer_panel { @@ -1002,15 +1229,7 @@ impl TerminalSidebar { } if let Some(ref monitor_panel) = server_monitor_panel { - let monitor_sub = cx.subscribe( - monitor_panel, - |this, _, event: &ServerMonitorPanelEvent, cx| match event { - ServerMonitorPanelEvent::Close => { - this.close_tool(SidebarPanel::ServerMonitor, cx); - } - }, - ); - subs.push(monitor_sub); + subs.push(subscribe_server_monitor_panel(cx, monitor_panel)); } let available_panels = terminal_sidebar_available_panels( @@ -1031,6 +1250,9 @@ impl TerminalSidebar { file_explorer_panel, server_monitor_panel, sync_path_enabled, + stored_connection: sidebar_connection, + pending_ssh_tool_panels: (!pending_ssh_tool_panels.is_empty()) + .then_some(pending_ssh_tool_panels), focus_handle: cx.focus_handle(), colors, _subs: subs, @@ -1137,6 +1359,7 @@ impl TerminalSidebar { open: self.tool_dock.is_tool_open(panel), }) .collect(), + save_as_connection: self.save_as_connection_available(), } } @@ -1616,6 +1839,8 @@ impl TerminalSidebar { struct TerminalToolbarSnapshot { colors: TerminalColors, buttons: Vec, + /// 当前是临时连接(无数据库 ID)时展示“保存为连接” + save_as_connection: bool, } #[derive(Clone, Copy)] @@ -1663,6 +1888,14 @@ impl Render for TerminalSidebarToolbar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let snapshot = self.sidebar.read(cx).toolbar_snapshot(); let item_size = Size::Size(one_ui::theme_geometry().layout.global_rail_item); + let save_button = snapshot.save_as_connection.then(|| { + terminal_toolbar_save_as_connection_button( + self.sidebar.clone(), + &snapshot.colors, + item_size, + cx, + ) + }); v_flex() .flex_shrink_0() @@ -1680,6 +1913,9 @@ impl Render for TerminalSidebarToolbar { .map(|button| self.render_button(button, item_size, &snapshot.colors, cx)), ), ) + .when_some(save_button, |this, button| { + this.child(v_flex().w_full().items_center().pb_2().child(button)) + }) } } @@ -1777,6 +2013,38 @@ mod tests { } } + #[test] + fn available_panels_can_grow_and_shrink_after_construction() { + let mut dock = TerminalToolDockState::new([SidebarPanel::Settings, SidebarPanel::AiChat]); + assert!(!dock.panels.contains(&SidebarPanel::FileManager)); + + // 临时连接提交凭据后补建文件管理器面板 + assert!(dock.update_available_panels([ + SidebarPanel::Settings, + SidebarPanel::AiChat, + SidebarPanel::FileManager, + ])); + assert_eq!( + vec![ + SidebarPanel::Settings, + SidebarPanel::AiChat, + SidebarPanel::FileManager + ], + dock.panels + ); + + // 重复调用等价,不重复添加面板 + assert!(!dock.update_available_panels([ + SidebarPanel::Settings, + SidebarPanel::AiChat, + SidebarPanel::FileManager, + ])); + + // 收缩时移除不再可用的面板 + assert!(dock.update_available_panels([SidebarPanel::Settings])); + assert_eq!(vec![SidebarPanel::Settings], dock.panels); + } + #[test] fn local_terminal_ai_context_uses_the_live_session_as_current_target() { let live_terminal = diff --git a/crates/terminal_view/src/sidebar/settings_panel.rs b/crates/terminal_view/src/sidebar/settings_panel.rs index dc79b1ba7..5ca8f92cc 100644 --- a/crates/terminal_view/src/sidebar/settings_panel.rs +++ b/crates/terminal_view/src/sidebar/settings_panel.rs @@ -1841,12 +1841,14 @@ mod tests { // 内置精选列表已经含有 Consolas,不应重复出现。 assert_eq!( 1, - values.iter().filter(|font| font.as_str() == "Consolas").count() + values + .iter() + .filter(|font| font.as_str() == "Consolas") + .count() ); // 系统字体是已安装状态,标签里不应带「(未安装)」。 assert!(fonts.iter().any(|font| { - font.value.as_ref() == "Sarasa Mono SC" - && font.label.as_ref() == "Sarasa Mono SC" + font.value.as_ref() == "Sarasa Mono SC" && font.label.as_ref() == "Sarasa Mono SC" })); } diff --git a/crates/terminal_view/src/ssh_form_window.rs b/crates/terminal_view/src/ssh_form_window.rs index 70a723fc9..5618b5f96 100644 --- a/crates/terminal_view/src/ssh_form_window.rs +++ b/crates/terminal_view/src/ssh_form_window.rs @@ -797,9 +797,8 @@ impl SshFormWindow { cx.new(|cx| InputState::new(window, cx).placeholder(t!("SSH.ftp_host_placeholder"))); let ftp_port_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("SSH.ftp_port_placeholder"))); - let ftp_username_input = cx.new(|cx| { - InputState::new(window, cx).placeholder(t!("SSH.ftp_username_placeholder")) - }); + let ftp_username_input = cx + .new(|cx| InputState::new(window, cx).placeholder(t!("SSH.ftp_username_placeholder"))); let ftp_password_input = cx.new(|cx| { InputState::new(window, cx) .placeholder(t!("SSH.ftp_password_placeholder")) @@ -976,11 +975,9 @@ impl SshFormWindow { ); }); if let Some(ref ftp) = remote_file.ftp { - ftp_host_input - .update(cx, |s, cx| s.set_value(&ftp.host, window, cx)); - ftp_port_input.update(cx, |s, cx| { - s.set_value(&ftp.port.to_string(), window, cx) - }); + ftp_host_input.update(cx, |s, cx| s.set_value(&ftp.host, window, cx)); + ftp_port_input + .update(cx, |s, cx| s.set_value(&ftp.port.to_string(), window, cx)); ftp_username_input .update(cx, |s, cx| s.set_value(&ftp.username, window, cx)); ftp_password_input @@ -3569,7 +3566,7 @@ mod tests { fn sample_params() -> SshParams { SshParams { - remote_file: None, + remote_file: None, sftp_account: None, sftp_default_directory: None, disabled_jump_server: None, @@ -4766,10 +4763,7 @@ mod tests { let built = form .read_with(cx, |form, cx| form.build_ssh_params(cx)) .expect("FTP 连接应能构建参数"); - assert_eq!( - built.remote_file_protocol(), - StoredRemoteFileProtocol::Ftp - ); + assert_eq!(built.remote_file_protocol(), StoredRemoteFileProtocol::Ftp); let ftp = built.ftp_params().expect("ftp params preserved"); assert_eq!(ftp.host, "ftp.example.com"); assert_eq!(ftp.port, 2121); diff --git a/crates/terminal_view/src/transfer_notice.rs b/crates/terminal_view/src/transfer_notice.rs index 047dcba9c..984c22c53 100644 --- a/crates/terminal_view/src/transfer_notice.rs +++ b/crates/terminal_view/src/transfer_notice.rs @@ -62,11 +62,8 @@ pub(crate) fn transfer_finish_notification( file_name: Option<&str>, error: Option<&str>, ) -> Notification { - let mut message = transfer_finish_message( - &action.label(), - file_name, - &finish_state_label(error), - ); + let mut message = + transfer_finish_message(&action.label(), file_name, &finish_state_label(error)); append_failure_reason(&mut message, error); match error { diff --git a/crates/terminal_view/src/view.rs b/crates/terminal_view/src/view.rs index fe6a66ed7..4466ff350 100644 --- a/crates/terminal_view/src/view.rs +++ b/crates/terminal_view/src/view.rs @@ -216,6 +216,11 @@ pub struct TerminalView { blink_manager: Entity, /// 侧边栏 sidebar: Entity, + /// SSH 工具面板(文件管理器 / 服务器监控)是否还没建好。 + /// + /// 需要运行时输入凭据的 SSH 连接在构造时拿不到 `SshSessionManager`, + /// 面板要等凭据提交后再补建,否则这类连接完全没有文件侧边栏。 + ssh_tool_panels_pending: bool, /// 本地工作区文件编辑器(仅本地终端) workspace_editor: Option>, /// 终端底部命令输入栏 diff --git a/crates/terminal_view/src/view/background_tasks.rs b/crates/terminal_view/src/view/background_tasks.rs index d4367994a..ae41230a1 100644 --- a/crates/terminal_view/src/view/background_tasks.rs +++ b/crates/terminal_view/src/view/background_tasks.rs @@ -236,9 +236,7 @@ fn zmodem_progress_values( mod tests { use super::{format_zmodem_bytes, zmodem_finish_notification, zmodem_progress_values}; use std::collections::HashMap; - use terminal::zmodem::{ - ZmodemTransferDirection, ZmodemTransferId, ZmodemTransferOutcome, - }; + use terminal::zmodem::{ZmodemTransferDirection, ZmodemTransferId, ZmodemTransferOutcome}; #[test] fn formats_zmodem_byte_counts() { diff --git a/crates/terminal_view/src/view/initialization.rs b/crates/terminal_view/src/view/initialization.rs index bdd04141f..16df23635 100644 --- a/crates/terminal_view/src/view/initialization.rs +++ b/crates/terminal_view/src/view/initialization.rs @@ -127,6 +127,9 @@ impl TerminalView { ) }); let sidebar_toolbar = cx.new(|_| TerminalSidebarToolbar::new(sidebar.clone())); + // 临时连接等需要运行时凭据的 SSH 会话此时还没有 SSH 工具面板, + // 等凭据提交后再补建(见 `TerminalView::ensure_ssh_tool_panels`)。 + let ssh_tool_panels_pending = !sidebar.read(cx).ssh_tool_panels_ready(); let sidebar_tool_panels = SidebarPanel::all() .iter() .copied() @@ -232,6 +235,7 @@ impl TerminalView { }, blink_manager, sidebar, + ssh_tool_panels_pending, workspace_editor, command_bar, sidebar_toolbar, diff --git a/crates/terminal_view/src/view/sidebar_events.rs b/crates/terminal_view/src/view/sidebar_events.rs index a68c0688a..f25c39818 100644 --- a/crates/terminal_view/src/view/sidebar_events.rs +++ b/crates/terminal_view/src/view/sidebar_events.rs @@ -1,8 +1,75 @@ use super::*; +use one_core::storage::models::SshAuthMethod; +use ssh::SshAuth; +use terminal::terminal::SshTerminalConfig; const MAX_PENDING_TERMINAL_SEARCH_RUNS: usize = 64; const MAX_PENDING_TERMINAL_SEARCH_REQUESTS: usize = 256; +/// “保存为连接”时用于补全临时连接的目标信息与运行时凭据。 +#[derive(Clone, Debug, PartialEq, Eq)] +struct RuntimeSshTarget { + host: String, + port: u16, + username: String, + password: Option, +} + +/// 从终端的运行时会话配置中取出目标信息与运行时输入的凭据。 +/// +/// 临时连接在提交凭据后会把运行时配置写回 `Terminal::ssh_config`, +/// 这里的密码只应存在于内存中,用于预填保存表单并写入凭据库。 +fn runtime_ssh_target(ssh_config: Option<&SshTerminalConfig>) -> Option { + let runtime = ssh_config?; + let password = match &runtime.ssh_config.auth { + SshAuth::Password(password) => Some(password.clone()).filter(|value| !value.is_empty()), + _ => None, + }; + Some(RuntimeSshTarget { + host: runtime.ssh_config.host.clone(), + port: runtime.ssh_config.port, + username: runtime.ssh_config.username.clone(), + password, + }) +} + +/// 把运行时目标信息与凭据合并进临时连接,得到可直接交给保存表单的连接。 +/// +/// 临时连接默认每次连接都要求输入用户名 / 密码,这里已经有运行时值, +/// 因此清空对应的 `prompt_*` 标记,让保存表单默认勾选“保存用户名 / 密码”。 +fn temporary_connection_with_runtime_target( + connection: &StoredConnection, + target: &RuntimeSshTarget, +) -> StoredConnection { + let Ok(mut params) = connection.to_ssh_params() else { + return connection.clone(); + }; + + if !target.host.is_empty() { + params.host = target.host.clone(); + params.port = target.port; + } + if !target.username.is_empty() { + params.username = target.username.clone(); + params.prompt_username = None; + } + if let Some(password) = target.password.as_deref() { + params.auth_method = SshAuthMethod::Password { + password: password.to_string(), + }; + params.prompt_password = None; + } + + // 名称交给 `new_ssh` 生成,避免带上临时连接的 “(temporary)” 后缀。 + let mut merged = StoredConnection::new_ssh(String::new(), params, connection.workspace_id); + merged.remark = connection.remark.clone(); + merged.sync_enabled = connection.sync_enabled; + merged.team_id = connection.team_id.clone(); + merged.owner_id = connection.owner_id.clone(); + merged.preferred_open_mode = connection.preferred_open_mode; + merged +} + struct TerminalSearchCompletion { generation: u64, pattern: String, @@ -89,6 +156,37 @@ impl TerminalView { self.apply_theme(&theme, window, cx); } + /// 构造“保存为连接”所需的连接信息。 + /// + /// 返回 `None` 表示当前终端不是临时连接(已有数据库记录)。 + fn temporary_connection_for_save(&self, cx: &Context) -> Option { + let connection = self.sidebar.read(cx).temporary_connection()?; + let ssh_config = self.terminal.read(cx).ssh_config().cloned(); + Some(match runtime_ssh_target(ssh_config.as_ref()) { + Some(target) => temporary_connection_with_runtime_target(&connection, &target), + None => connection, + }) + } + + /// SSH 凭据就绪后补建文件管理器 / 服务器监控面板。 + /// + /// 需要运行时输入凭据的 SSH 连接在构造时拿不到 `SshSessionManager`, + /// 这两个面板只能等凭据提交后再创建。 + pub(super) fn ensure_ssh_tool_panels(&mut self, window: &mut Window, cx: &mut Context) { + if !self.ssh_tool_panels_pending { + return; + } + let Some(session_manager) = self.terminal.read(cx).ssh_session_manager().cloned() else { + return; + }; + self.sidebar.update(cx, |sidebar, cx| { + sidebar.install_ssh_tool_panels(session_manager, window, cx); + }); + if self.sidebar.read(cx).ssh_tool_panels_ready() { + self.ssh_tool_panels_pending = false; + } + } + /// 处理侧边栏事件 pub(super) fn handle_sidebar_event( &mut self, @@ -235,6 +333,11 @@ impl TerminalView { TerminalSidebarEvent::OpenSftp(connection) => { cx.emit(TerminalPaneEvent::OpenSftp(connection.clone())); } + TerminalSidebarEvent::SaveAsConnection => { + if let Some(connection) = self.temporary_connection_for_save(cx) { + cx.emit(TerminalPaneEvent::SaveAsConnection(connection)); + } + } TerminalSidebarEvent::CdToTerminal(path) => { // 向终端发送 cd 命令并回车 let cmd = format!("cd {}\n", shell_escape(path)); @@ -416,3 +519,81 @@ impl TerminalView { self.enqueue_terminal_search(TerminalSearchDirection::Backward, cx); } } + +#[cfg(test)] +mod tests { + use super::{RuntimeSshTarget, temporary_connection_with_runtime_target}; + use one_core::storage::StoredConnection; + use one_core::storage::models::{SshAuthMethod, SshParams}; + + /// 与主页快速连接生成的临时连接保持一致:默认每次连接都输入用户名 / 密码。 + fn temporary_ssh_connection() -> StoredConnection { + let params: SshParams = serde_json::from_value(serde_json::json!({ + "host": "example.com", + "port": 22, + "username": "", + "auth_method": { "Password": { "password": "" } }, + "prompt_username": true, + "prompt_password": true, + })) + .expect("临时连接的参数 JSON 应可解析"); + StoredConnection::new_ssh("SSH example.com (temporary)".to_string(), params, None) + } + + fn runtime_target() -> RuntimeSshTarget { + RuntimeSshTarget { + host: "10.0.0.5".to_string(), + port: 2222, + username: "alice".to_string(), + password: Some("s3cret".to_string()), + } + } + + #[test] + fn runtime_credentials_replace_temporary_prompts() { + let merged = temporary_connection_with_runtime_target( + &temporary_ssh_connection(), + &runtime_target(), + ); + let params = merged.to_ssh_params().expect("合并后的参数应可解析"); + + assert_eq!("10.0.0.5", params.host); + assert_eq!(2222, params.port); + assert_eq!("alice", params.username); + match ¶ms.auth_method { + SshAuthMethod::Password { password } => assert_eq!("s3cret", password), + other => panic!("运行时凭据应合并为密码认证,实际为 {other:?}"), + } + // 运行时已有值,保存表单应默认勾选保存用户名 / 密码。 + assert!(!params.prompts_for_username()); + assert!(!params.prompts_for_password()); + // 新连接没有数据库 ID,名称也不带临时标记。 + assert_eq!(None, merged.id); + assert_eq!("alice@10.0.0.5:2222", merged.name); + } + + #[test] + fn missing_runtime_password_keeps_password_prompt() { + let target = RuntimeSshTarget { + password: None, + ..runtime_target() + }; + let merged = temporary_connection_with_runtime_target(&temporary_ssh_connection(), &target); + let params = merged.to_ssh_params().expect("合并后的参数应可解析"); + + assert_eq!("alice", params.username); + assert!(!params.prompts_for_username()); + // 没有拿到运行时密码,仍保留每次输入密码的行为。 + assert!(params.prompts_for_password()); + } + + #[test] + fn unparsable_params_are_left_untouched() { + let mut connection = temporary_ssh_connection(); + connection.params = "{not json".to_string(); + + let merged = temporary_connection_with_runtime_target(&connection, &runtime_target()); + + assert_eq!("{not json", merged.params); + } +} diff --git a/crates/terminal_view/src/view/terminal_events.rs b/crates/terminal_view/src/view/terminal_events.rs index 461fca541..f46c5e085 100644 --- a/crates/terminal_view/src/view/terminal_events.rs +++ b/crates/terminal_view/src/view/terminal_events.rs @@ -41,6 +41,7 @@ impl TerminalView { TerminalModelEvent::Wakeup => { self.sync_recording_ticker(cx); self.sync_credential_capture(cx); + self.ensure_ssh_tool_panels(window, cx); self.sync_zmodem_background_task(None, cx); self.focus_terminal_after_connect_if_ready(window, cx); self.refresh_history_prompt_matches(cx); @@ -58,6 +59,7 @@ impl TerminalView { } TerminalModelEvent::SshCredentialChanged => { self.sync_credential_capture(cx); + self.ensure_ssh_tool_panels(window, cx); self.focus_terminal_after_connect_if_ready(window, cx); cx.notify(); } diff --git a/crates/terminal_view/src/view/tests/core.rs b/crates/terminal_view/src/view/tests/core.rs index c92c70bcc..849571876 100644 --- a/crates/terminal_view/src/view/tests/core.rs +++ b/crates/terminal_view/src/view/tests/core.rs @@ -57,7 +57,7 @@ fn tab_duplicate_requires_a_live_local_ssh_or_serial_terminal() { let ssh = StoredConnection::new_ssh( "ssh".to_string(), SshParams { - remote_file: None, + remote_file: None, sftp_default_directory: None, disabled_jump_server: None, sftp_account: None, @@ -190,7 +190,7 @@ fn duplicate_source_for_ssh_terminal_prefers_current_working_dir() { let ssh = StoredConnection::new_ssh( "ssh".to_string(), SshParams { - remote_file: None, + remote_file: None, sftp_default_directory: None, disabled_jump_server: None, sftp_account: None, @@ -248,7 +248,7 @@ fn ssh_reconnect_resolves_latest_saved_connection_by_id() { let mut original = StoredConnection::new_ssh( "ssh".to_string(), SshParams { - remote_file: None, + remote_file: None, sftp_default_directory: None, disabled_jump_server: None, sftp_account: None, @@ -284,7 +284,7 @@ fn ssh_reconnect_resolves_latest_saved_connection_by_id() { original.id = Some(42); let mut latest = original.clone(); latest.params = serde_json::to_string(&SshParams { - remote_file: None, + remote_file: None, sftp_default_directory: None, disabled_jump_server: None, sftp_account: None, diff --git a/crates/terminal_view/src/view/workspace_support.rs b/crates/terminal_view/src/view/workspace_support.rs index 90386d156..f6401504a 100644 --- a/crates/terminal_view/src/view/workspace_support.rs +++ b/crates/terminal_view/src/view/workspace_support.rs @@ -4,6 +4,8 @@ use super::*; pub(crate) enum TerminalPaneEvent { Focused, OpenSftp(StoredConnection), + /// 把临时连接保存为正式连接(由上层写入数据库) + SaveAsConnection(StoredConnection), } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/terminal_view/src/workspace/actions.rs b/crates/terminal_view/src/workspace/actions.rs index b1c983e60..7ee4c7c57 100644 --- a/crates/terminal_view/src/workspace/actions.rs +++ b/crates/terminal_view/src/workspace/actions.rs @@ -51,6 +51,9 @@ impl TerminalWorkspace { TerminalPaneEvent::OpenSftp(connection) => { cx.emit(TerminalWorkspaceEvent::OpenSftp(connection.clone())); } + TerminalPaneEvent::SaveAsConnection(connection) => { + cx.emit(TerminalWorkspaceEvent::SaveAsConnection(connection.clone())); + } }, ); let content_subscription = cx.subscribe_in( diff --git a/crates/terminal_view/src/workspace/view.rs b/crates/terminal_view/src/workspace/view.rs index 5e197101b..a151a73a5 100644 --- a/crates/terminal_view/src/workspace/view.rs +++ b/crates/terminal_view/src/workspace/view.rs @@ -29,6 +29,8 @@ pub struct TerminalWorkspace { #[derive(Clone, Debug)] pub enum TerminalWorkspaceEvent { OpenSftp(StoredConnection), + /// 把临时连接保存为正式连接 + SaveAsConnection(StoredConnection), } impl TerminalWorkspace { diff --git a/main/locales/main.yml b/main/locales/main.yml index 9498a8a3f..8ded8e41c 100644 --- a/main/locales/main.yml +++ b/main/locales/main.yml @@ -126,6 +126,14 @@ Home: en: "Connections" zh-CN: "连接" zh-HK: "連線" + save_as_connection: + en: "Save as connection" + zh-CN: "保存为连接" + zh-HK: "儲存為連線" + save_as_connection_done: + en: "Saved “%{name}” as a connection" + zh-CN: "已将「%{name}」保存为连接" + zh-HK: "已將「%{name}」儲存為連線" toggle_sidebar: en: "Collapse or expand application sidebar" zh-CN: "折叠或展开应用侧栏" diff --git a/main/src/home/home_connection_quick_open.rs b/main/src/home/home_connection_quick_open.rs index 588cd63bb..5f95c4fcc 100644 --- a/main/src/home/home_connection_quick_open.rs +++ b/main/src/home/home_connection_quick_open.rs @@ -1,14 +1,18 @@ use crate::connection_visuals::{ConnectionVisualSize, stored_connection_icon_with_catalog}; use crate::home_tab::{HomePage, connection_matches_query}; use db::ipc::IpcDriverRegistry; +use gpui::prelude::FluentBuilder; use gpui::{ App, Context, Entity, FontWeight, ParentElement, SharedString, Styled, Task, Window, div, px, }; use gpui_component::{ - ActiveTheme, IndexPath, WindowExt, h_flex, + ActiveTheme, Icon, IndexPath, Sizable, WindowExt, h_flex, list::{ListDelegate, ListItem, ListState}, }; +use one_assets::IconName; use one_core::storage::{SshAuthMethod, SshParams, StoredConnection}; +use one_ui::{IconButton, IconButtonRole, IconSize}; +use rust_i18n::t; pub(crate) struct ConnectionQuickOpenDelegate { parent: Entity, @@ -244,6 +248,9 @@ impl ListDelegate for ConnectionQuickOpenDelegate { ) }; let connection_for_open = connection.clone(); + let save_parent = parent.clone(); + // 临时连接(无数据库 ID)提供“保存为连接”入口。 + let temporary_connection = connection.id.is_none().then(|| connection.clone()); Some( ListItem::new(ix) @@ -283,7 +290,27 @@ impl ListDelegate for ConnectionQuickOpenDelegate { cx, ), )), - ), + ) + .when_some(temporary_connection, |this, connection| { + this.child( + IconButton::new( + SharedString::from("quick-open-save-connection"), + Icon::new(IconName::Save).mono().with_size(IconSize::Small), + ) + .role(IconButtonRole::Compact) + .tooltip(t!("Home.save_as_connection")) + .on_click(move |_, window, cx| { + cx.stop_propagation(); + let connection = connection.clone(); + save_parent.update(cx, |this, cx| { + this.show_save_temporary_connection_form( + connection, window, cx, + ); + }); + window.close_dialog(cx); + }), + ) + }), ), ) } diff --git a/main/src/home/home_tabs.rs b/main/src/home/home_tabs.rs index f4f1c3902..afd678b9b 100644 --- a/main/src/home/home_tabs.rs +++ b/main/src/home/home_tabs.rs @@ -740,6 +740,9 @@ impl HomePage { TerminalWorkspaceEvent::OpenSftp(connection) => { this.open_sftp_view(connection.clone(), window, cx); } + TerminalWorkspaceEvent::SaveAsConnection(connection) => { + this.show_save_temporary_connection_form(connection.clone(), window, cx); + } }, ); self._subscriptions.push(subscription); diff --git a/main/src/home_tab/connection_forms.rs b/main/src/home_tab/connection_forms.rs index fbcc5ce3c..0406c7426 100644 --- a/main/src/home_tab/connection_forms.rs +++ b/main/src/home_tab/connection_forms.rs @@ -205,6 +205,40 @@ impl HomePage { ); } + /// 把临时连接保存为正式连接。 + /// + /// 连接信息已由终端侧补全运行时用户名 / 密码,保存时会一并写入凭据库。 + pub(crate) fn show_save_temporary_connection_form( + &mut self, + connection: StoredConnection, + window: &mut Window, + cx: &mut Context, + ) { + if !self.is_master_key_ready_for_new_connection() { + return; + } + + let config = SshFormWindowConfig { + editing_connection: None, + initial_connection: Some(connection), + on_saved: Some(Arc::new(|saved, _action, window, cx| { + window.push_notification( + t!("Home.save_as_connection_done", name = saved.name.clone()).to_string(), + cx, + ); + })), + workspaces: self.workspaces.clone(), + teams: get_cached_team_options(cx), + }; + + open_popup_window( + PopupWindowOptions::new(t!("Home.save_as_connection").to_string()).size(820.0, 750.0), + move |window, cx| cx.new(|cx| SshFormWindow::new(config, window, cx)), + Some(window), + cx, + ); + } + pub(crate) fn show_ssh_form(&mut self, _window: &mut Window, cx: &mut Context) { if self.editing_connection_id.is_none() && !self.is_master_key_ready_for_new_connection() { return; diff --git a/main/src/main.rs b/main/src/main.rs index 5dc3c7555..627426765 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -25,8 +25,8 @@ mod home_tab; mod license; mod local_terminal_profiles; mod navigation_applications; -pub mod new_connection; mod navop_app; +pub mod new_connection; mod persistent_connection_sidebar; mod personal_sync_conflicts; mod personal_sync_runtime;