fix(terminal): use default transport for SFTP companion sessions
This commit is contained in:
1 parent
3c06c23435
commit
7f66b3a444
10 files changed
+160
-103
No files matched your search
@@ -47438,6 +47438,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open current directory with %@ — right-click for more options" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aktuelles Verzeichnis mit %@ öffnen — Rechtsklick für weitere Optionen"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ouvrir le répertoire actuel via %@ — clic droit pour plus d’options"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "現在のディレクトリを%@で開く — 右クリックでその他の操作を表示"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@로 현재 디렉터리 열기 — 추가 옵션을 보려면 마우스 오른쪽 버튼 클릭"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@打开当前目录—右键查看更多操作"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@開啟目前目錄—右鍵檢視更多操作"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open current directory with SFTP — right-click for more options" : {
|
||||
"comment" : "A contextual help for the open button in the status bar.",
|
||||
"extractionState" : "manual",
|
||||
@@ -47481,47 +47522,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open current directory with SSH — right-click for more options" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aktuelles Verzeichnis mit SSH öffnen — Rechtsklick für weitere Optionen"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ouvrir le répertoire actuel via SSH — clic droit pour plus d’options"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "現在のディレクトリをSSHで開く — 右クリックでその他の操作を表示"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "SSH로 현재 디렉터리 열기 — 추가 옵션을 보려면 마우스 오른쪽 버튼 클릭"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用SSH打开当前目录—右键查看更多操作"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用SSH開啟目前目錄—右鍵檢視更多操作"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open login page" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -47645,6 +47645,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open with %@" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mit %@ öffnen"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ouvrir via %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@で開く"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@로 열기"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@打开"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@開啟"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open with SFTP" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -47686,47 +47727,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open with SSH" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mit SSH öffnen"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ouvrir via SSH"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "SSHで開く"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "SSH로 열기"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用SSH打开"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用SSH開啟"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenSSH commands are unavailable for routes using an HTTP or SOCKS5 proxy." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -143,8 +143,8 @@ extension AppStrings {
|
||||
public static var hostsYouConnectToAppearHere: LocalizedStringResource {
|
||||
"Hosts you connect to appear here."
|
||||
}
|
||||
public static var openWithSsh: LocalizedStringResource {
|
||||
"Open with SSH"
|
||||
public static func openWithTransport(_ name: String) -> LocalizedStringResource {
|
||||
"Open with \(name)"
|
||||
}
|
||||
public static var sshIsDisabledForThisHost: LocalizedStringResource {
|
||||
"SSH is disabled for this Host."
|
||||
|
||||
@@ -306,8 +306,8 @@ extension AppStrings {
|
||||
"Open Telnet Connection?"
|
||||
}
|
||||
|
||||
public static var openDirectoryWithSSHHelp: LocalizedStringResource {
|
||||
"Open current directory with SSH — right-click for more options"
|
||||
public static func openDirectoryWithTransportHelp(_ name: String) -> LocalizedStringResource {
|
||||
"Open current directory with \(name) — right-click for more options"
|
||||
}
|
||||
|
||||
public static var previousWorkspace: LocalizedStringResource {
|
||||
|
||||
@@ -241,10 +241,12 @@ final class MobileWorkspace: Identifiable {
|
||||
path: RemotePath.normalized(workingDirectory)
|
||||
)
|
||||
case .sftp:
|
||||
guard !sftpChannel.path.isEmpty else { return nil }
|
||||
guard !sftpChannel.path.isEmpty, let host = connectionHost(descriptor.hostID),
|
||||
host.sshEnabled, let transport = host.preferredTerminalProtocol
|
||||
else { return nil }
|
||||
return MobileWorkspaceDirectoryLaunch(
|
||||
hostID: descriptor.hostID,
|
||||
kind: .ssh,
|
||||
kind: .terminal(transport),
|
||||
path: RemotePath.normalized(sftpChannel.path)
|
||||
)
|
||||
case .telnet:
|
||||
|
||||
@@ -765,7 +765,12 @@ struct MobileWorkspaceView: View {
|
||||
if let launch = workspace.sameDirectoryLaunch {
|
||||
switch launch.kind {
|
||||
case .ssh, .mosh, .eternalTerminal:
|
||||
Button(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: "terminal") {
|
||||
Button(
|
||||
AppStrings.Connections.openWithTransport(
|
||||
(launch.kind.terminalProtocol ?? .ssh).displayName
|
||||
).swiftUIKey,
|
||||
systemImage: launch.kind.symbol
|
||||
) {
|
||||
openVerifiedDirectory(launch)
|
||||
}
|
||||
case .telnet:
|
||||
|
||||
@@ -345,13 +345,18 @@ struct MobileWorkspaceTests {
|
||||
|
||||
@Suite @MainActor
|
||||
struct MobileWorkspaceConnectionRecoveryTests {
|
||||
@Test func sameDirectorySFTPStartupUsesTheOverrideAndOffersSSHOnlyWhileConnected() async throws {
|
||||
@Test(arguments: [HostTerminalProtocol.ssh, .mosh, .eternalTerminal])
|
||||
func sameDirectorySFTPUsesTheDefaultTransportOnlyWhileConnected(_ transport: HostTerminalProtocol) async throws {
|
||||
let connections = MobileSFTPConnectionBox(clients: [MobileTestSFTPClient()])
|
||||
let app = try MobileAppModel(
|
||||
inMemory: true,
|
||||
workspaceConnectionServices: connections.services
|
||||
)
|
||||
let host = try configureConnectionFixture(in: app)
|
||||
var host = try configureConnectionFixture(in: app)
|
||||
host.terminalTransports.moshEnabled = true
|
||||
host.terminalTransports.eternalTerminalEnabled = true
|
||||
host.terminalTransports.defaultTransport = transport
|
||||
app.library.upsert(host)
|
||||
let id = try #require(
|
||||
app.testWorkspaceSession.open(hostID: host.id, kind: .sftp, initialPath: "/srv/releases/"))
|
||||
let workspace = try #require(app.workspaces.first(where: { $0.id == id }))
|
||||
@@ -362,7 +367,8 @@ struct MobileWorkspaceConnectionRecoveryTests {
|
||||
#expect(workspace.sftpChannel.path == "/srv/releases")
|
||||
#expect(
|
||||
workspace.sameDirectoryLaunch
|
||||
== MobileWorkspaceDirectoryLaunch(hostID: host.id, kind: .ssh, path: "/srv/releases")
|
||||
== MobileWorkspaceDirectoryLaunch(
|
||||
hostID: host.id, kind: .terminal(transport), path: "/srv/releases")
|
||||
)
|
||||
#expect(app.testWorkspaceSession.renameWorkspace(id, to: " Release Files "))
|
||||
#expect(workspace.descriptor.title == "Release Files")
|
||||
|
||||
@@ -505,7 +505,8 @@ struct SFTPBrowserView: View {
|
||||
}
|
||||
|
||||
private func actionsMenu(layout: Layout) -> some View {
|
||||
Menu {
|
||||
let transport = app.hostLibrary.host(sftpChannel.hostID)?.preferredTerminalProtocol ?? .ssh
|
||||
return Menu {
|
||||
Button(AppStrings.SFTP.newFolder.swiftUIKey, systemImage: MenuSymbol.newFolder) {
|
||||
newFolderPresented = true
|
||||
}
|
||||
@@ -513,7 +514,10 @@ struct SFTPBrowserView: View {
|
||||
Button(AppStrings.Common.paste.swiftUIKey, systemImage: MenuSymbol.paste) { pasteRemoteItems() }
|
||||
.disabled(sftpChannel.status != .connected || !canPasteRemoteItems)
|
||||
Divider()
|
||||
Menu(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: MenuSymbol.ssh) {
|
||||
Menu(
|
||||
AppStrings.Connections.openWithTransport(transport.displayName).swiftUIKey,
|
||||
systemImage: transport.symbol
|
||||
) {
|
||||
Button(AppStrings.Workspace.inCurrentWorkspace.swiftUIKey, systemImage: MenuSymbol.currentWorkspace) {
|
||||
window.openCompanionSession(from: sftpChannel.id, placement: .currentWorkspace)
|
||||
}
|
||||
|
||||
@@ -301,20 +301,25 @@ struct StatusBarView: View {
|
||||
}
|
||||
|
||||
private func sftpDirectoryButton(_ path: String, displayed: String) -> some View {
|
||||
Button {
|
||||
let transport = host?.preferredTerminalProtocol ?? .ssh
|
||||
return Button {
|
||||
app.onboarding.recordUse(of: .statusBarPath)
|
||||
openInSSH(path, placement: .currentWorkspace)
|
||||
} label: {
|
||||
StatusBarPathLabel(
|
||||
symbol: "apple.terminal", text: displayed, destinationSymbol: "arrow.up.right"
|
||||
symbol: transport.symbol, text: displayed, destinationSymbol: "arrow.up.right"
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(Text(AppStrings.Workspace.openDirectoryWithSSHHelp))
|
||||
.accessibilityLabel(Text(AppStrings.Connections.openWithSsh))
|
||||
.disabled(host?.sshEnabled != true)
|
||||
.help(Text(AppStrings.Workspace.openDirectoryWithTransportHelp(transport.displayName)))
|
||||
.accessibilityLabel(Text(AppStrings.Connections.openWithTransport(transport.displayName)))
|
||||
.onDrag { TerminalPathDrag.provider(for: path) }
|
||||
.contextMenu {
|
||||
Menu(AppStrings.Connections.openWithSsh.swiftUIKey, systemImage: MenuSymbol.ssh) {
|
||||
Menu(
|
||||
AppStrings.Connections.openWithTransport(transport.displayName).swiftUIKey,
|
||||
systemImage: transport.symbol
|
||||
) {
|
||||
Button(AppStrings.Workspace.inCurrentWorkspace.swiftUIKey, systemImage: MenuSymbol.currentWorkspace) {
|
||||
app.onboarding.recordUse(of: .statusBarPath)
|
||||
openInSSH(path, placement: .currentWorkspace)
|
||||
@@ -355,8 +360,8 @@ struct StatusBarView: View {
|
||||
}
|
||||
|
||||
private func openInSSH(_ path: String, placement: SessionOpenPlacement) {
|
||||
guard let host else { return }
|
||||
window.openSSHSession(host: host, initialPath: path, placement: placement)
|
||||
guard let host, host.sshEnabled, let transport = host.preferredTerminalProtocol else { return }
|
||||
window.openSSHSession(host: host, transport: transport, initialPath: path, placement: placement)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -860,7 +860,8 @@ final class WindowModel {
|
||||
}
|
||||
|
||||
func canOpenCompanionSession(from sessionID: UUID) -> Bool {
|
||||
companionSessionSource(sessionID) != nil
|
||||
guard let source = companionSessionSource(sessionID) else { return false }
|
||||
return source.pane.sshSession != nil || source.host.sshEnabled
|
||||
}
|
||||
|
||||
/// Resolve the owning pane again at dispatch, not the pane that happened to
|
||||
@@ -892,7 +893,8 @@ final class WindowModel {
|
||||
if source.pane.sshSession != nil {
|
||||
return openSFTPChannel(host: source.host, initialPath: source.path, placement: placement)
|
||||
}
|
||||
return openSSHSession(host: source.host, initialPath: source.path, placement: placement)
|
||||
guard source.host.sshEnabled, let transport = source.host.preferredTerminalProtocol else { return nil }
|
||||
return openSSHSession(host: source.host, transport: transport, initialPath: source.path, placement: placement)
|
||||
}
|
||||
|
||||
private func companionSessionSource(_ sessionID: UUID) -> (
|
||||
|
||||
@@ -1454,6 +1454,39 @@ struct WindowStateTests {
|
||||
#expect(sftpWorkspace.focusedPane.sshSession?.hostID == host.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
func sftpCompanionUsesTheHostDefaultWithoutChangingExplicitSSH() throws {
|
||||
for transport in [HostTerminalProtocol.ssh, .mosh, .eternalTerminal] {
|
||||
var host = Host(name: "Shell", host: "shell.example")
|
||||
host.terminalTransports.moshEnabled = true
|
||||
host.terminalTransports.eternalTerminalEnabled = true
|
||||
host.terminalTransports.defaultTransport = transport
|
||||
let channel = SFTPChannelModel.demo(hostID: host.id, path: "/srv/app")
|
||||
let workspace = Workspace(pane: Pane(.sftpChannel(channel)))
|
||||
let window = WindowModel(workspaces: [workspace])
|
||||
let library = HostLibraryModel(hosts: [host])
|
||||
window.hostStore = library
|
||||
|
||||
#expect(window.canOpenCompanionSession(from: channel.id))
|
||||
let companion = try #require(
|
||||
window.openCompanionSession(from: channel.id, placement: .newWorkspace))
|
||||
#expect(companion.focusedPane.sshSession?.transport == transport)
|
||||
#expect(window.openSSHSession(host: host).focusedPane.sshSession?.transport == .ssh)
|
||||
|
||||
window.select(workspace.id)
|
||||
let split = try #require(
|
||||
window.openCompanionSession(from: channel.id, placement: .currentWorkspace))
|
||||
#expect(split === workspace)
|
||||
#expect(split.focusedPane.sshSession?.transport == transport)
|
||||
|
||||
host.sshEnabled = false
|
||||
host.telnetEnabled = true
|
||||
#expect(library.saveHost(host))
|
||||
#expect(!window.canOpenCompanionSession(from: channel.id))
|
||||
#expect(window.openCompanionSession(from: channel.id, placement: .newWorkspace) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func sftpWithoutAnOverrideKeepsTheHostsConfiguredInitialPath() {
|
||||
var host = Host(name: "Files", host: "files.example")
|
||||
|
||||
Reference in new issue
Block a user