Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -86038,6 +86038,74 @@
}
}
},
"fileExplorer.status.sshHomeFailed": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unable to resolve SSH home: %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "SSH のホームを解決できません: %@"
}
}
}
},
"fileExplorer.status.sshResolvingHome": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Resolving remote home..."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "リモートのホームを解決中..."
}
}
}
},
"fileExplorer.status.sshUnavailable": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "SSH files unavailable"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "SSH ファイルを利用できません"
}
}
}
},
"fileExplorer.status.sshUnavailableWithDetail": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "SSH files unavailable: %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "SSH ファイルを利用できません: %@"
}
}
}
},
"fileExplorer.search.empty": {
"extractionState": "manual",
"localizations": {
Expand Down
136 changes: 78 additions & 58 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,15 @@ private func installFileDropOverlayWhenReady(

@MainActor
private final class SelectedWorkspaceDirectoryObserver: ObservableObject {
private struct Snapshot: Equatable {
let workspaceId: UUID?
let currentDirectory: String?
let remoteConfiguration: WorkspaceRemoteConfiguration?
let remoteConnectionState: WorkspaceRemoteConnectionState?
let remoteConnectionDetail: String?
let remoteDaemonStatus: WorkspaceRemoteDaemonStatus?
}

@Published private(set) var directoryChangeGeneration: UInt64 = 0
private weak var tabManager: TabManager?
private var cancellable: AnyCancellable?
Expand All @@ -992,18 +1001,47 @@ private final class SelectedWorkspaceDirectoryObserver: ObservableObject {
return tabManager.tabs.first(where: { $0.id == tabId })
}
.removeDuplicates(by: { $0?.id == $1?.id })
.map { workspace -> AnyPublisher<(UUID?, String?), Never> in
.map { workspace -> AnyPublisher<Snapshot, Never> in
guard let workspace else {
return Just<(UUID?, String?)>((nil, nil)).eraseToAnyPublisher()
return Just(
Snapshot(
workspaceId: nil,
currentDirectory: nil,
remoteConfiguration: nil,
remoteConnectionState: nil,
remoteConnectionDetail: nil,
remoteDaemonStatus: nil
)
)
.eraseToAnyPublisher()
}
return workspace.$currentDirectory
.map { (Optional(workspace.id), Optional($0)) }
.combineLatest(
workspace.$remoteConfiguration,
workspace.$remoteConnectionState,
workspace.$remoteConnectionDetail
)
.combineLatest(workspace.$remoteDaemonStatus)
.map { values, remoteDaemonStatus in
let (
currentDirectory,
remoteConfiguration,
remoteConnectionState,
remoteConnectionDetail
) = values
Snapshot(
workspaceId: workspace.id,
currentDirectory: currentDirectory,
remoteConfiguration: remoteConfiguration,
remoteConnectionState: remoteConnectionState,
remoteConnectionDetail: remoteConnectionDetail,
remoteDaemonStatus: remoteDaemonStatus
)
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
.eraseToAnyPublisher()
}
.switchToLatest()
.removeDuplicates { previous, next in
previous.0 == next.0 && previous.1 == next.1
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.directoryChangeGeneration &+= 1
Expand Down Expand Up @@ -2437,70 +2475,52 @@ struct ContentView: View {
// No selection means we have no local cwd to scope by; clear so the
// sessions panel doesn't keep filtering by a stale previous tab.
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
return
}

let dir = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
guard !dir.isEmpty else {
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
fileExplorerStore.applyWorkspaceRoot(.none)
return
}

fileExplorerStore.showHiddenFiles = true
if !tab.isRemoteWorkspace {
sessionIndexStore.setCurrentDirectoryIfChanged(dir)
} else {
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
}

if tab.isRemoteWorkspace {
let config = tab.remoteConfiguration
let remotePath = tab.remoteDaemonStatus.remotePath
let isReady = tab.remoteDaemonStatus.state == .ready
let homePath = remotePath.flatMap { path -> String? in
let components = dir.split(separator: "/")
if components.count >= 2, components[0] == "home" {
return "/home/\(components[1])"
}
if dir.hasPrefix("/root") {
return "/root"
}
return nil
} ?? ""
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
guard let config = tab.remoteConfiguration, config.transport == .ssh else {
fileExplorerStore.applyWorkspaceRoot(.none)
return
}

#if DEBUG
cmuxDebugLog("fileExplorer.sync remote dir=\(dir) ready=\(isReady) dest=\(config?.destination ?? "nil")")
cmuxDebugLog(
"fileExplorer.sync remote state=\(tab.remoteConnectionState.rawValue) " +
"dest=\(config.destination) target=\(config.displayTarget)"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
#endif

if let existingProvider = fileExplorerStore.provider as? SSHFileExplorerProvider,
existingProvider.destination == config?.destination {
existingProvider.updateAvailability(isReady, homePath: isReady ? homePath : nil)
if isReady {
// Only reload if the path actually changed
let pathChanged = fileExplorerStore.rootPath != dir
fileExplorerStore.setRootPath(dir)
if pathChanged {
fileExplorerStore.hydrateExpandedNodes()
}
}
} else if let config {
let provider = SSHFileExplorerProvider(
destination: config.destination,
port: config.port,
identityFile: config.identityFile,
sshOptions: config.sshOptions,
homePath: homePath,
isAvailable: isReady
fileExplorerStore.applyWorkspaceRoot(
.remoteSSH(
workspaceId: tab.id,
connection: SSHFileExplorerConnection(
destination: config.destination,
port: config.port,
identityFile: config.identityFile,
sshOptions: config.sshOptions
),
displayTarget: config.displayTarget,
isAvailable: tab.remoteConnectionState == .connected,
unavailableDetail: tab.remoteConnectionDetail ?? tab.remoteDaemonStatus.detail
)
fileExplorerStore.setProvider(provider)
fileExplorerStore.setRootPath(dir)
}
} else {
if !(fileExplorerStore.provider is LocalFileExplorerProvider) {
fileExplorerStore.setProvider(LocalFileExplorerProvider())
}
fileExplorerStore.setRootPath(dir)
)
return
}

let dir = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
guard !dir.isEmpty else {
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
fileExplorerStore.applyWorkspaceRoot(.none)
return
}

sessionIndexStore.setCurrentDirectoryIfChanged(dir)
fileExplorerStore.applyWorkspaceRoot(.local(path: dir))
}

private var focusedDirectory: String? {
Expand Down
Loading
Loading