From e44b90540f65521317b6660d29ded78cf5acc416 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:49:20 +0800 Subject: [PATCH 01/13] feat(ssh): add pane-scoped local port forwarding --- nebula_app/src/gpui_shell/terminal/view.rs | 2 + .../src/gpui_shell/terminal/view/runtime.rs | 1 + .../src/gpui_shell/terminal/view/startup.rs | 1 + .../src/gpui_shell/workspace/remote_files.rs | 2 +- .../src/gpui_shell/workspace/sidebar.rs | 2 + .../src/gpui_shell/workspace/ssh_dialog.rs | 200 +++++++++++++++++- .../src/gpui_shell/workspace/top_tabs.rs | 6 +- nebula_app/src/ssh_session.rs | 2 + nebula_app/src/ssh_session/forward.rs | 102 +++++++++ 9 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 nebula_app/src/ssh_session/forward.rs diff --git a/nebula_app/src/gpui_shell/terminal/view.rs b/nebula_app/src/gpui_shell/terminal/view.rs index a3a488db..21a4f3cb 100644 --- a/nebula_app/src/gpui_shell/terminal/view.rs +++ b/nebula_app/src/gpui_shell/terminal/view.rs @@ -336,6 +336,8 @@ pub struct TerminalView { path_drop: path_drop::PathDropState, /// SSH 直连目的地(`user@host[:port]`);本地会话为 None。 pub ssh_destination: Option, + /// 本 pane 拥有的本地端口转发;pane 销毁即停止监听。 + pub(crate) port_forwards: Vec, ssh_label: Option, /// 创建本地 PTY 时冻结的受控环境,供独立 `pane.exec` child 复用。 pub(crate) exec_context: Option, diff --git a/nebula_app/src/gpui_shell/terminal/view/runtime.rs b/nebula_app/src/gpui_shell/terminal/view/runtime.rs index bf8b1583..b9b37e50 100644 --- a/nebula_app/src/gpui_shell/terminal/view/runtime.rs +++ b/nebula_app/src/gpui_shell/terminal/view/runtime.rs @@ -111,6 +111,7 @@ impl TerminalView { ); self.ssh_connect_last_step = std::time::Instant::now(); if matches!(stage, crate::ssh_session::SshStage::Failed(_)) { + self.port_forwards.clear(); self.pending_runtime_submit = None; self.pending_shell_command = None; self.command_running = false; diff --git a/nebula_app/src/gpui_shell/terminal/view/startup.rs b/nebula_app/src/gpui_shell/terminal/view/startup.rs index 19364aaf..54fa9983 100644 --- a/nebula_app/src/gpui_shell/terminal/view/startup.rs +++ b/nebula_app/src/gpui_shell/terminal/view/startup.rs @@ -260,6 +260,7 @@ impl TerminalView { image_paste: image_paste::ImagePasteState::default(), path_drop: path_drop::PathDropState::default(), ssh_destination, + port_forwards: Vec::new(), ssh_label: None, exec_context, ssh_stage: None, diff --git a/nebula_app/src/gpui_shell/workspace/remote_files.rs b/nebula_app/src/gpui_shell/workspace/remote_files.rs index eb07878e..0eccf448 100644 --- a/nebula_app/src/gpui_shell/workspace/remote_files.rs +++ b/nebula_app/src/gpui_shell/workspace/remote_files.rs @@ -909,7 +909,7 @@ fn format_transfer_bytes(bytes: u64) -> String { /// /// 返回 `None` 表示网络 runtime 起不来或任务被丢弃——调用方据此报"连接不 /// 可用",而不是把它和"远端答了个错误"混为一谈。 -async fn remote_call(work: F) -> Option +pub(super) async fn remote_call(work: F) -> Option where T: Send + 'static, F: FnOnce() -> Fut + Send + 'static, diff --git a/nebula_app/src/gpui_shell/workspace/sidebar.rs b/nebula_app/src/gpui_shell/workspace/sidebar.rs index 6ee9b7ae..aae89074 100644 --- a/nebula_app/src/gpui_shell/workspace/sidebar.rs +++ b/nebula_app/src/gpui_shell/workspace/sidebar.rs @@ -865,6 +865,7 @@ impl NebulaWorkspace { let settings_active_bg = cx.theme().sidebar_accent; let settings_active_fg = cx.theme().sidebar_accent_foreground; let sidebar_visible = !self.sidebar_collapsed && !self.reader_focus_active(cx); + let port_forward_button = self.render_port_forward_button(cx); h_flex() .size_full() .items_center() @@ -929,6 +930,7 @@ impl NebulaWorkspace { this.toggle_command_manager(window, cx); })), ) + .when_some(port_forward_button, |controls, button| controls.child(button)) .child(self.render_right_sidebar_button(settings_active, cx)), ) .into_any_element() diff --git a/nebula_app/src/gpui_shell/workspace/ssh_dialog.rs b/nebula_app/src/gpui_shell/workspace/ssh_dialog.rs index fa89fd15..0f26d085 100644 --- a/nebula_app/src/gpui_shell/workspace/ssh_dialog.rs +++ b/nebula_app/src/gpui_shell/workspace/ssh_dialog.rs @@ -2,11 +2,14 @@ use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; -use gpui::{App, AppContext as _, ParentElement as _, Styled as _, Window, div}; +use gpui::{App, AppContext as _, Context, Entity, ParentElement as _, Styled as _, Window, div}; use crate::gpui_shell::prelude::*; +use crate::gpui_shell::terminal::view::TerminalView; use crate::ssh_prompt::{Prompt, PromptKind, PromptResponse}; +use super::{NebulaWorkspace, workspace_ui_language}; + pub(super) fn show(request: Arc, window: &mut Window, cx: &mut App) { if !request.is_pending() { return; @@ -177,3 +180,198 @@ mod tests { } } } + +impl NebulaWorkspace { + fn active_ready_ssh_view(&self, cx: &App) -> Option> { + if self.settings_open { + return None; + } + let view = self.tabs.get(self.active)?.focused_view()?.clone(); + view.read(cx).ready_ssh_destination()?; + Some(view) + } + + pub(super) fn render_port_forward_button(&self, cx: &mut Context) -> Option