diff --git a/apps/swift-ios/App/NativeFeatureClient.swift b/apps/swift-ios/App/NativeFeatureClient.swift index 5793e527de364..10257bcc63a97 100644 --- a/apps/swift-ios/App/NativeFeatureClient.swift +++ b/apps/swift-ios/App/NativeFeatureClient.swift @@ -6710,6 +6710,7 @@ final class NativeFeatureClient: FeatureClient, FeatureDeviceManaging, mapped.machineIcon = serverConfigsByEnvironmentID[environment.id]?.settings?.environmentIcon ?? environment.descriptor?.platform.machine mapped.newProjectsRoot = serverConfigsByEnvironmentID[environment.id]?.newProjectsRoot + mapped.addProjectBaseDirectory = serverConfigsByEnvironmentID[environment.id]?.settings?.addProjectBaseDirectory mapped.supportsScratch = serverConfigsByEnvironmentID[environment.id]?.scratchWorkspaceRoot != nil mapped.canCustomizeIcon = serverConfigsByEnvironmentID[environment.id]?.environment?.capabilities.environmentIcon ?? environment.descriptor?.capabilities.environmentIcon diff --git a/apps/swift-ios/Core/ServerConfigModels.swift b/apps/swift-ios/Core/ServerConfigModels.swift index 6eb5f1af31ae9..ab284f3154648 100644 --- a/apps/swift-ios/Core/ServerConfigModels.swift +++ b/apps/swift-ios/Core/ServerConfigModels.swift @@ -221,6 +221,7 @@ public struct ServerSettingsSnapshot: Codable, Equatable, Sendable { public var environmentIcon: String? = nil public var sourceControlWritingStyle: JSONValue? = nil public var worktreesDirectory: String? = nil + public var addProjectBaseDirectory: String? = nil public var previousWorktreesDirectories: [String]? = nil public var removeAgentCreditsOnMerge: Bool? = nil public var sourceControlHosts: [String: [String: JSONValue]]? = nil @@ -310,6 +311,7 @@ public struct ServerSettingsSnapshot: Codable, Equatable, Sendable { case environmentIcon case sourceControlWritingStyle case worktreesDirectory, previousWorktreesDirectories, removeAgentCreditsOnMerge, sourceControlHosts + case addProjectBaseDirectory case worktreeSubmodules, storageCleanup, worktreeCleanup case defaultAutoPull, responseStreamingMode, projectSettingsOverrides, projectSettingsFolded, providerInstances case favorites, providerModelPreferences @@ -335,6 +337,7 @@ public struct ServerSettingsSnapshot: Codable, Equatable, Sendable { environmentIcon = try container.decodeIfPresent(String.self, forKey: .environmentIcon) sourceControlWritingStyle = try container.decodeIfPresent(JSONValue.self, forKey: .sourceControlWritingStyle) worktreesDirectory = try container.decodeIfPresent(String.self, forKey: .worktreesDirectory) + addProjectBaseDirectory = try container.decodeIfPresent(String.self, forKey: .addProjectBaseDirectory) previousWorktreesDirectories = try container.decodeIfPresent([String].self, forKey: .previousWorktreesDirectories) removeAgentCreditsOnMerge = try container.decodeIfPresent(Bool.self, forKey: .removeAgentCreditsOnMerge) sourceControlHosts = try container.decodeIfPresent([String: [String: JSONValue]].self, forKey: .sourceControlHosts) diff --git a/apps/swift-ios/Features/Shared/FeatureModels.swift b/apps/swift-ios/Features/Shared/FeatureModels.swift index 3b0d31b9022e7..b37f37f47e564 100644 --- a/apps/swift-ios/Features/Shared/FeatureModels.swift +++ b/apps/swift-ios/Features/Shared/FeatureModels.swift @@ -51,6 +51,7 @@ public struct FeatureEnvironment: Identifiable, Sendable, Equatable, Hashable, C public var machineIcon: String? = nil public var supportsScratch: Bool? = nil public var newProjectsRoot: String? = nil + public var addProjectBaseDirectory: String? = nil public var canCustomizeIcon: Bool? = nil public var supportsWorktreesDirectory: Bool? = nil public var supportsServerBrowser: Bool? = nil @@ -97,7 +98,7 @@ public struct FeatureEnvironment: Identifiable, Sendable, Equatable, Hashable, C case connectionState case connectionDetail case machineIcon - case supportsScratch, newProjectsRoot + case supportsScratch, newProjectsRoot, addProjectBaseDirectory case canCustomizeIcon, supportsWorktreesDirectory, supportsServerBrowser, permissions } @@ -116,6 +117,7 @@ public struct FeatureEnvironment: Identifiable, Sendable, Equatable, Hashable, C connectionDetail = try container.decodeIfPresent(String.self, forKey: .connectionDetail) machineIcon = try container.decodeIfPresent(String.self, forKey: .machineIcon) newProjectsRoot = try container.decodeIfPresent(String.self, forKey: .newProjectsRoot) + addProjectBaseDirectory = try container.decodeIfPresent(String.self, forKey: .addProjectBaseDirectory) supportsScratch = try container.decodeIfPresent(Bool.self, forKey: .supportsScratch) canCustomizeIcon = try container.decodeIfPresent(Bool.self, forKey: .canCustomizeIcon) supportsWorktreesDirectory = try container.decodeIfPresent(Bool.self, forKey: .supportsWorktreesDirectory) diff --git a/apps/swift-ios/Features/Workspace/AddProjectView.swift b/apps/swift-ios/Features/Workspace/AddProjectView.swift index 69ab5d090c2fb..4c696d5b76d5a 100644 --- a/apps/swift-ios/Features/Workspace/AddProjectView.swift +++ b/apps/swift-ios/Features/Workspace/AddProjectView.swift @@ -49,6 +49,8 @@ public struct AddProjectView: View { @State private var isBrowsing = false @State private var browseError: String? @State private var browseRequestID: UUID? + /// The base directory seeded for an environment, kept until the user browses. + @State private var seededBaseDirectory: (environmentID: String, path: String)? @State private var discovery: SourceControlDiscoveryResult? @State private var isDiscovering = false @@ -154,6 +156,9 @@ public struct AddProjectView: View { updateSuggestedDestination() errorMessage = nil } + .onChange(of: addProjectBaseDirectory) { + reseedLateBaseDirectory() + } .task(id: selectedEnvironmentID) { guard selectedEnvironmentID != nil else { return } resetEnvironmentState() @@ -631,7 +636,9 @@ public struct AddProjectView: View { private func resetEnvironmentState() { if mode == .newProject && selectedEnvironment?.newProjectsRoot == nil { mode = .folder } publishesToGitHub = false - browsePath = "~/" + let baseDirectory = addProjectBaseDirectory + seededBaseDirectory = selectedEnvironmentID.map { ($0, baseDirectory) } + browsePath = baseDirectory browseResult = nil browseError = nil browseRequestID = nil @@ -644,13 +651,45 @@ public struct AddProjectView: View { pendingManagedClone = nil cloneRequestID = nil didEditDestination = false - localPath = "~/" + localPath = baseDirectory destinationPath = repositoryInput.isEmpty - ? "~/" - : ProjectCreationPath.appending(repositoryName, to: "~/") + ? baseDirectory + : ProjectCreationPath.appending(repositoryName, to: baseDirectory) errorMessage = nil } + /// A connected environment can publish its shell before its config, so the + /// setting may arrive after seeding. Follow it only while the paths are untouched. + private func reseedLateBaseDirectory() { + let baseDirectory = addProjectBaseDirectory + guard selectedEnvironment != nil, + let seeded = seededBaseDirectory, + seeded.environmentID == selectedEnvironmentID, + seeded.path != baseDirectory, + localPath == seeded.path, + !didEditDestination, + !isSubmitting, + cloneRequestID == nil, + pendingCloneRegistration == nil, + pendingManagedClone == nil else { + return + } + seededBaseDirectory = (seeded.environmentID, baseDirectory) + browsePath = baseDirectory + localPath = baseDirectory + destinationPath = repositoryInput.isEmpty + ? baseDirectory + : ProjectCreationPath.appending(repositoryName, to: baseDirectory) + Task { await loadDirectory(baseDirectory, updateSelection: false) } + } + + /// Matches web: a blank per-environment setting falls back to the home directory. + private var addProjectBaseDirectory: String { + let configured = selectedEnvironment?.addProjectBaseDirectory? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return configured.isEmpty ? "~/" : ProjectCreationPath.directoryBrowsePath(configured) + } + private func loadDiscovery() async { guard let environmentID = selectedEnvironmentID, let projectClient else { @@ -695,6 +734,7 @@ public struct AddProjectView: View { } let requestedPath = path.trimmingCharacters(in: .whitespacesAndNewlines) guard !requestedPath.isEmpty else { return } + if updateSelection { seededBaseDirectory = nil } let requestID = UUID() browseRequestID = requestID isBrowsing = true @@ -859,7 +899,11 @@ public struct AddProjectView: View { errorMessage = nil isSubmitting = true - defer { isSubmitting = false } + defer { + isSubmitting = false + // The base directory may have arrived while this lookup blocked reseeding. + reseedLateBaseDirectory() + } do { let result = try await projectClient.lookupProjectRepository( environmentID: environment.id,