diff --git a/App/App.csproj b/App/App.csproj
index 4d049fd..982612f 100644
--- a/App/App.csproj
+++ b/App/App.csproj
@@ -56,6 +56,7 @@
 
     <ItemGroup>
         <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+        <PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
         <PackageReference Include="DependencyPropertyGenerator" Version="1.5.0">
             <PrivateAssets>all</PrivateAssets>
             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/App/App.xaml.cs b/App/App.xaml.cs
index c6f22b4..2c7e87e 100644
--- a/App/App.xaml.cs
+++ b/App/App.xaml.cs
@@ -1,24 +1,26 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using Windows.ApplicationModel.Activation;
 using Coder.Desktop.App.Models;
 using Coder.Desktop.App.Services;
 using Coder.Desktop.App.ViewModels;
 using Coder.Desktop.App.Views;
 using Coder.Desktop.App.Views.Pages;
+using Coder.Desktop.CoderSdk.Agent;
 using Coder.Desktop.Vpn;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
 using Microsoft.UI.Xaml;
 using Microsoft.Win32;
 using Microsoft.Windows.AppLifecycle;
-using Windows.ApplicationModel.Activation;
-using Microsoft.Extensions.Logging;
 using Serilog;
-using System.Collections.Generic;
+using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
 
 namespace Coder.Desktop.App;
 
@@ -60,6 +62,8 @@ public App()
             loggerConfig.ReadFrom.Configuration(builder.Configuration);
         });
 
+        services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
+
         services.AddSingleton<ICredentialManager, CredentialManager>();
         services.AddSingleton<IRpcController, RpcController>();
 
@@ -76,6 +80,8 @@ public App()
         // FileSyncListMainPage is created by FileSyncListWindow.
         services.AddTransient<FileSyncListWindow>();
 
+        // DirectoryPickerWindow views and view models are created by FileSyncListViewModel.
+
         // TrayWindow views and view models
         services.AddTransient<TrayWindowLoadingPage>();
         services.AddTransient<TrayWindowDisconnectedViewModel>();
@@ -89,7 +95,7 @@ public App()
         services.AddTransient<TrayWindow>();
 
         _services = services.BuildServiceProvider();
-        _logger = (ILogger<App>)(_services.GetService(typeof(ILogger<App>))!);
+        _logger = (ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
 
         InitializeComponent();
     }
@@ -107,7 +113,7 @@ public async Task ExitApplication()
         Environment.Exit(0);
     }
 
-    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+    protected override void OnLaunched(LaunchActivatedEventArgs args)
     {
         _logger.LogInformation("new instance launched");
         // Start connecting to the manager in the background.
diff --git a/App/Converters/DependencyObjectSelector.cs b/App/Converters/DependencyObjectSelector.cs
index 8c1570f..a31c33b 100644
--- a/App/Converters/DependencyObjectSelector.cs
+++ b/App/Converters/DependencyObjectSelector.cs
@@ -186,3 +186,7 @@ private static void SelectedKeyPropertyChanged(DependencyObject obj, DependencyP
 public sealed class StringToBrushSelectorItem : DependencyObjectSelectorItem<string, Brush>;
 
 public sealed class StringToBrushSelector : DependencyObjectSelector<string, Brush>;
+
+public sealed class StringToStringSelectorItem : DependencyObjectSelectorItem<string, string>;
+
+public sealed class StringToStringSelector : DependencyObjectSelector<string, string>;
diff --git a/App/Program.cs b/App/Program.cs
index 2ad863d..1a54b2b 100644
--- a/App/Program.cs
+++ b/App/Program.cs
@@ -27,7 +27,7 @@ private static void Main(string[] args)
         try
         {
             ComWrappersSupport.InitializeComWrappers();
-            AppInstance mainInstance = GetMainInstance();
+            var mainInstance = GetMainInstance();
             if (!mainInstance.IsCurrent)
             {
                 var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
diff --git a/App/Services/CredentialManager.cs b/App/Services/CredentialManager.cs
index 41a8dc7..a2f6567 100644
--- a/App/Services/CredentialManager.cs
+++ b/App/Services/CredentialManager.cs
@@ -7,6 +7,7 @@
 using System.Threading.Tasks;
 using Coder.Desktop.App.Models;
 using Coder.Desktop.CoderSdk;
+using Coder.Desktop.CoderSdk.Coder;
 using Coder.Desktop.Vpn.Utilities;
 
 namespace Coder.Desktop.App.Services;
diff --git a/App/Services/MutagenController.cs b/App/Services/MutagenController.cs
index 3a68962..5b85b2c 100644
--- a/App/Services/MutagenController.cs
+++ b/App/Services/MutagenController.cs
@@ -12,6 +12,7 @@
 using Coder.Desktop.MutagenSdk.Proto.Service.Prompting;
 using Coder.Desktop.MutagenSdk.Proto.Service.Synchronization;
 using Coder.Desktop.MutagenSdk.Proto.Synchronization;
+using Coder.Desktop.MutagenSdk.Proto.Synchronization.Core.Ignore;
 using Coder.Desktop.MutagenSdk.Proto.Url;
 using Coder.Desktop.Vpn.Utilities;
 using Grpc.Core;
@@ -85,7 +86,9 @@ public interface ISyncSessionController : IAsyncDisposable
     /// </summary>
     Task<SyncSessionControllerStateModel> RefreshState(CancellationToken ct = default);
 
-    Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string> progressCallback, CancellationToken ct = default);
+    Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string> progressCallback,
+        CancellationToken ct = default);
+
     Task<SyncSessionModel> PauseSyncSession(string identifier, CancellationToken ct = default);
     Task<SyncSessionModel> ResumeSyncSession(string identifier, CancellationToken ct = default);
     Task TerminateSyncSession(string identifier, CancellationToken ct = default);
@@ -200,7 +203,8 @@ public async Task<SyncSessionControllerStateModel> RefreshState(CancellationToke
         return state;
     }
 
-    public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string>? progressCallback = null, CancellationToken ct = default)
+    public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req,
+        Action<string>? progressCallback = null, CancellationToken ct = default)
     {
         using var _ = await _lock.LockAsync(ct);
         var client = await EnsureDaemon(ct);
@@ -216,8 +220,11 @@ public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest r
             {
                 Alpha = req.Alpha.MutagenUrl,
                 Beta = req.Beta.MutagenUrl,
-                // TODO: probably should set these at some point
-                Configuration = new Configuration(),
+                // TODO: probably should add a configuration page for these at some point
+                Configuration = new Configuration
+                {
+                    IgnoreVCSMode = IgnoreVCSMode.Ignore,
+                },
                 ConfigurationAlpha = new Configuration(),
                 ConfigurationBeta = new Configuration(),
             },
diff --git a/App/ViewModels/DirectoryPickerViewModel.cs b/App/ViewModels/DirectoryPickerViewModel.cs
new file mode 100644
index 0000000..131934f
--- /dev/null
+++ b/App/ViewModels/DirectoryPickerViewModel.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Coder.Desktop.CoderSdk.Agent;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Coder.Desktop.App.ViewModels;
+
+public class DirectoryPickerBreadcrumb
+{
+    // HACK: you cannot access the parent context when inside an ItemsRepeater.
+    public required DirectoryPickerViewModel ViewModel;
+
+    public required string Name { get; init; }
+
+    public required IReadOnlyList<string> AbsolutePathSegments { get; init; }
+
+    // HACK: we need to know which one is first so we don't prepend an arrow
+    // icon. You can't get the index of the current ItemsRepeater item in XAML.
+    public required bool IsFirst { get; init; }
+}
+
+public enum DirectoryPickerItemKind
+{
+    ParentDirectory, // aka. ".."
+    Directory,
+    File, // includes everything else
+}
+
+public class DirectoryPickerItem
+{
+    // HACK: you cannot access the parent context when inside an ItemsRepeater.
+    public required DirectoryPickerViewModel ViewModel;
+
+    public required DirectoryPickerItemKind Kind { get; init; }
+    public required string Name { get; init; }
+    public required IReadOnlyList<string> AbsolutePathSegments { get; init; }
+
+    public bool Selectable => Kind is DirectoryPickerItemKind.ParentDirectory or DirectoryPickerItemKind.Directory;
+}
+
+public partial class DirectoryPickerViewModel : ObservableObject
+{
+    // PathSelected will be called ONCE when the user either cancels or selects
+    // a directory. If the user cancelled, the path will be null.
+    public event EventHandler<string?>? PathSelected;
+
+    private const int RequestTimeoutMilliseconds = 15_000;
+
+    private readonly IAgentApiClient _client;
+
+    private Window? _window;
+    private DispatcherQueue? _dispatcherQueue;
+
+    public readonly string AgentFqdn;
+
+    // The initial loading screen is differentiated from subsequent loading
+    // screens because:
+    // 1. We don't want to show a broken state while the page is loading.
+    // 2. An error dialog allows the user to get to a broken state with no
+    //    breadcrumbs, no items, etc. with no chance to reload.
+    [ObservableProperty]
+    [NotifyPropertyChangedFor(nameof(ShowLoadingScreen))]
+    [NotifyPropertyChangedFor(nameof(ShowListScreen))]
+    public partial bool InitialLoading { get; set; } = true;
+
+    [ObservableProperty]
+    [NotifyPropertyChangedFor(nameof(ShowLoadingScreen))]
+    [NotifyPropertyChangedFor(nameof(ShowErrorScreen))]
+    [NotifyPropertyChangedFor(nameof(ShowListScreen))]
+    public partial string? InitialLoadError { get; set; } = null;
+
+    [ObservableProperty] public partial bool NavigatingLoading { get; set; } = false;
+
+    [ObservableProperty]
+    [NotifyPropertyChangedFor(nameof(IsSelectable))]
+    public partial string CurrentDirectory { get; set; } = "";
+
+    [ObservableProperty] public partial IReadOnlyList<DirectoryPickerBreadcrumb> Breadcrumbs { get; set; } = [];
+
+    [ObservableProperty] public partial IReadOnlyList<DirectoryPickerItem> Items { get; set; } = [];
+
+    public bool ShowLoadingScreen => InitialLoadError == null && InitialLoading;
+    public bool ShowErrorScreen => InitialLoadError != null;
+    public bool ShowListScreen => InitialLoadError == null && !InitialLoading;
+
+    // The "root" directory on Windows isn't a real thing, but in our model
+    // it's a drive listing. We don't allow users to select the fake drive
+    // listing directory.
+    //
+    // On Linux, this will never be empty since the highest you can go is "/".
+    public bool IsSelectable => CurrentDirectory != "";
+
+    public DirectoryPickerViewModel(IAgentApiClientFactory clientFactory, string agentFqdn)
+    {
+        _client = clientFactory.Create(agentFqdn);
+        AgentFqdn = agentFqdn;
+    }
+
+    public void Initialize(Window window, DispatcherQueue dispatcherQueue)
+    {
+        _window = window;
+        _dispatcherQueue = dispatcherQueue;
+        if (!_dispatcherQueue.HasThreadAccess)
+            throw new InvalidOperationException("Initialize must be called from the UI thread");
+
+        InitialLoading = true;
+        InitialLoadError = null;
+        // Initial load is in the home directory.
+        _ = BackgroundLoad(ListDirectoryRelativity.Home, []).ContinueWith(ContinueInitialLoad);
+    }
+
+    [RelayCommand]
+    private void RetryLoad()
+    {
+        InitialLoading = true;
+        InitialLoadError = null;
+        // Subsequent loads after the initial failure are always in the root
+        // directory in case there's a permanent issue preventing listing the
+        // home directory.
+        _ = BackgroundLoad(ListDirectoryRelativity.Root, []).ContinueWith(ContinueInitialLoad);
+    }
+
+    private async Task<ListDirectoryResponse> BackgroundLoad(ListDirectoryRelativity relativity, List<string> path)
+    {
+        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
+        return await _client.ListDirectory(new ListDirectoryRequest
+        {
+            Path = path,
+            Relativity = relativity,
+        }, cts.Token);
+    }
+
+    private void ContinueInitialLoad(Task<ListDirectoryResponse> task)
+    {
+        // Ensure we're on the UI thread.
+        if (_dispatcherQueue == null) return;
+        if (!_dispatcherQueue.HasThreadAccess)
+        {
+            _dispatcherQueue.TryEnqueue(() => ContinueInitialLoad(task));
+            return;
+        }
+
+        if (task.IsCompletedSuccessfully)
+        {
+            ProcessResponse(task.Result);
+            return;
+        }
+
+        InitialLoadError = "Could not list home directory in workspace: ";
+        if (task.IsCanceled) InitialLoadError += new TaskCanceledException();
+        else if (task.IsFaulted) InitialLoadError += task.Exception;
+        else InitialLoadError += "no successful result or error";
+        InitialLoading = false;
+    }
+
+    [RelayCommand]
+    public async Task ListPath(IReadOnlyList<string> path)
+    {
+        if (_window is null || NavigatingLoading) return;
+        NavigatingLoading = true;
+
+        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(RequestTimeoutMilliseconds));
+        try
+        {
+            var res = await _client.ListDirectory(new ListDirectoryRequest
+            {
+                Path = path.ToList(),
+                Relativity = ListDirectoryRelativity.Root,
+            }, cts.Token);
+            ProcessResponse(res);
+        }
+        catch (Exception e)
+        {
+            // Subsequent listing errors are just shown as dialog boxes.
+            var dialog = new ContentDialog
+            {
+                Title = "Failed to list remote directory",
+                Content = $"{e}",
+                CloseButtonText = "Ok",
+                XamlRoot = _window.Content.XamlRoot,
+            };
+            _ = await dialog.ShowAsync();
+        }
+        finally
+        {
+            NavigatingLoading = false;
+        }
+    }
+
+    [RelayCommand]
+    public void Cancel()
+    {
+        PathSelected?.Invoke(this, null);
+        _window?.Close();
+    }
+
+    [RelayCommand]
+    public void Select()
+    {
+        if (CurrentDirectory == "") return;
+        PathSelected?.Invoke(this, CurrentDirectory);
+        _window?.Close();
+    }
+
+    private void ProcessResponse(ListDirectoryResponse res)
+    {
+        InitialLoading = false;
+        InitialLoadError = null;
+        NavigatingLoading = false;
+
+        var breadcrumbs = new List<DirectoryPickerBreadcrumb>(res.AbsolutePath.Count + 1)
+        {
+            new()
+            {
+                Name = "🖥️",
+                AbsolutePathSegments = [],
+                IsFirst = true,
+                ViewModel = this,
+            },
+        };
+        for (var i = 0; i < res.AbsolutePath.Count; i++)
+            breadcrumbs.Add(new DirectoryPickerBreadcrumb
+            {
+                Name = res.AbsolutePath[i],
+                AbsolutePathSegments = res.AbsolutePath[..(i + 1)],
+                IsFirst = false,
+                ViewModel = this,
+            });
+
+        var items = new List<DirectoryPickerItem>(res.Contents.Count + 1);
+        if (res.AbsolutePath.Count != 0)
+            items.Add(new DirectoryPickerItem
+            {
+                Kind = DirectoryPickerItemKind.ParentDirectory,
+                Name = "..",
+                AbsolutePathSegments = res.AbsolutePath[..^1],
+                ViewModel = this,
+            });
+
+        foreach (var item in res.Contents)
+        {
+            if (item.Name.StartsWith(".")) continue;
+            items.Add(new DirectoryPickerItem
+            {
+                Kind = item.IsDir ? DirectoryPickerItemKind.Directory : DirectoryPickerItemKind.File,
+                Name = item.Name,
+                AbsolutePathSegments = res.AbsolutePath.Append(item.Name).ToList(),
+                ViewModel = this,
+            });
+        }
+
+        CurrentDirectory = res.AbsolutePathString;
+        Breadcrumbs = breadcrumbs;
+        Items = items;
+    }
+}
diff --git a/App/ViewModels/FileSyncListViewModel.cs b/App/ViewModels/FileSyncListViewModel.cs
index d01338c..9235141 100644
--- a/App/ViewModels/FileSyncListViewModel.cs
+++ b/App/ViewModels/FileSyncListViewModel.cs
@@ -6,6 +6,8 @@
 using Windows.Storage.Pickers;
 using Coder.Desktop.App.Models;
 using Coder.Desktop.App.Services;
+using Coder.Desktop.App.Views;
+using Coder.Desktop.CoderSdk.Agent;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.UI.Dispatching;
@@ -19,10 +21,12 @@ public partial class FileSyncListViewModel : ObservableObject
 {
     private Window? _window;
     private DispatcherQueue? _dispatcherQueue;
+    private DirectoryPickerWindow? _remotePickerWindow;
 
     private readonly ISyncSessionController _syncSessionController;
     private readonly IRpcController _rpcController;
     private readonly ICredentialManager _credentialManager;
+    private readonly IAgentApiClientFactory _agentApiClientFactory;
 
     [ObservableProperty]
     [NotifyPropertyChangedFor(nameof(ShowUnavailable))]
@@ -46,7 +50,7 @@ public partial class FileSyncListViewModel : ObservableObject
 
     [ObservableProperty] public partial bool OperationInProgress { get; set; } = false;
 
-    [ObservableProperty] public partial List<SyncSessionViewModel> Sessions { get; set; } = [];
+    [ObservableProperty] public partial IReadOnlyList<SyncSessionViewModel> Sessions { get; set; } = [];
 
     [ObservableProperty] public partial bool CreatingNewSession { get; set; } = false;
 
@@ -58,17 +62,30 @@ public partial class FileSyncListViewModel : ObservableObject
     [NotifyPropertyChangedFor(nameof(NewSessionCreateEnabled))]
     public partial bool NewSessionLocalPathDialogOpen { get; set; } = false;
 
+    [ObservableProperty]
+    [NotifyPropertyChangedFor(nameof(NewSessionRemoteHostEnabled))]
+    public partial IReadOnlyList<string> AvailableHosts { get; set; } = [];
+
     [ObservableProperty]
     [NotifyPropertyChangedFor(nameof(NewSessionCreateEnabled))]
-    public partial string NewSessionRemoteHost { get; set; } = "";
+    [NotifyPropertyChangedFor(nameof(NewSessionRemotePathDialogEnabled))]
+    public partial string? NewSessionRemoteHost { get; set; } = null;
 
     [ObservableProperty]
     [NotifyPropertyChangedFor(nameof(NewSessionCreateEnabled))]
     public partial string NewSessionRemotePath { get; set; } = "";
-    // TODO: NewSessionRemotePathDialogOpen for remote path
 
     [ObservableProperty]
-    public partial string NewSessionStatus { get; set; } = "";
+    [NotifyPropertyChangedFor(nameof(NewSessionCreateEnabled))]
+    [NotifyPropertyChangedFor(nameof(NewSessionRemotePathDialogEnabled))]
+    public partial bool NewSessionRemotePathDialogOpen { get; set; } = false;
+
+    public bool NewSessionRemoteHostEnabled => AvailableHosts.Count > 0;
+
+    public bool NewSessionRemotePathDialogEnabled =>
+        !string.IsNullOrWhiteSpace(NewSessionRemoteHost) && !NewSessionRemotePathDialogOpen;
+
+    [ObservableProperty] public partial string NewSessionStatus { get; set; } = "";
 
     public bool NewSessionCreateEnabled
     {
@@ -78,6 +95,7 @@ public bool NewSessionCreateEnabled
             if (NewSessionLocalPathDialogOpen) return false;
             if (string.IsNullOrWhiteSpace(NewSessionRemoteHost)) return false;
             if (string.IsNullOrWhiteSpace(NewSessionRemotePath)) return false;
+            if (NewSessionRemotePathDialogOpen) return false;
             return true;
         }
     }
@@ -89,11 +107,12 @@ public bool NewSessionCreateEnabled
     public bool ShowSessions => !Loading && UnavailableMessage == null && Error == null;
 
     public FileSyncListViewModel(ISyncSessionController syncSessionController, IRpcController rpcController,
-        ICredentialManager credentialManager)
+        ICredentialManager credentialManager, IAgentApiClientFactory agentApiClientFactory)
     {
         _syncSessionController = syncSessionController;
         _rpcController = rpcController;
         _credentialManager = credentialManager;
+        _agentApiClientFactory = agentApiClientFactory;
     }
 
     public void Initialize(Window window, DispatcherQueue dispatcherQueue)
@@ -106,6 +125,14 @@ public void Initialize(Window window, DispatcherQueue dispatcherQueue)
         _rpcController.StateChanged += RpcControllerStateChanged;
         _credentialManager.CredentialsChanged += CredentialManagerCredentialsChanged;
         _syncSessionController.StateChanged += SyncSessionStateChanged;
+        _window.Closed += (_, _) =>
+        {
+            _remotePickerWindow?.Close();
+
+            _rpcController.StateChanged -= RpcControllerStateChanged;
+            _credentialManager.CredentialsChanged -= CredentialManagerCredentialsChanged;
+            _syncSessionController.StateChanged -= SyncSessionStateChanged;
+        };
 
         var rpcModel = _rpcController.GetState();
         var credentialModel = _credentialManager.GetCachedCredentials();
@@ -174,8 +201,13 @@ private void MaybeSetUnavailableMessage(RpcModel rpcModel, CredentialModel crede
         else
         {
             UnavailableMessage = null;
+            // Reload if we transitioned from unavailable to available.
             if (oldMessage != null) ReloadSessions();
         }
+
+        // When transitioning from available to unavailable:
+        if (oldMessage == null && UnavailableMessage != null)
+            ClearNewForm();
     }
 
     private void UpdateSyncSessionState(SyncSessionControllerStateModel syncSessionState)
@@ -191,6 +223,7 @@ private void ClearNewForm()
         NewSessionRemoteHost = "";
         NewSessionRemotePath = "";
         NewSessionStatus = "";
+        _remotePickerWindow?.Close();
     }
 
     [RelayCommand]
@@ -227,21 +260,50 @@ private void HandleRefresh(Task<SyncSessionControllerStateModel> t)
         Loading = false;
     }
 
+    // Overriding AvailableHosts seems to make the ComboBox clear its value, so
+    // we only do this while the create form is not open.
+    // Must be called in UI thread.
+    private void SetAvailableHostsFromRpcModel(RpcModel rpcModel)
+    {
+        var hosts = new List<string>(rpcModel.Agents.Count);
+        // Agents will only contain started agents.
+        foreach (var agent in rpcModel.Agents)
+        {
+            var fqdn = agent.Fqdn
+                .Select(a => a.Trim('.'))
+                .Where(a => !string.IsNullOrWhiteSpace(a))
+                .Aggregate((a, b) => a.Count(c => c == '.') < b.Count(c => c == '.') ? a : b);
+            if (string.IsNullOrWhiteSpace(fqdn))
+                continue;
+            hosts.Add(fqdn);
+        }
+
+        NewSessionRemoteHost = null;
+        AvailableHosts = hosts;
+    }
+
     [RelayCommand]
     private void StartCreatingNewSession()
     {
         ClearNewForm();
+        // Ensure we have a fresh hosts list before we open the form. We don't
+        // bind directly to the list on RPC state updates as updating the list
+        // while in use seems to break it.
+        SetAvailableHostsFromRpcModel(_rpcController.GetState());
         CreatingNewSession = true;
     }
 
-    public async Task OpenLocalPathSelectDialog(Window window)
+    [RelayCommand]
+    public async Task OpenLocalPathSelectDialog()
     {
+        if (_window is null) return;
+
         var picker = new FolderPicker
         {
             SuggestedStartLocation = PickerLocationId.ComputerFolder,
         };
 
-        var hwnd = WindowNative.GetWindowHandle(window);
+        var hwnd = WindowNative.GetWindowHandle(_window);
         InitializeWithWindow.Initialize(picker, hwnd);
 
         NewSessionLocalPathDialogOpen = true;
@@ -261,6 +323,40 @@ public async Task OpenLocalPathSelectDialog(Window window)
         }
     }
 
+    [RelayCommand]
+    public void OpenRemotePathSelectDialog()
+    {
+        if (string.IsNullOrWhiteSpace(NewSessionRemoteHost))
+            return;
+        if (_remotePickerWindow is not null)
+        {
+            _remotePickerWindow.Activate();
+            return;
+        }
+
+        NewSessionRemotePathDialogOpen = true;
+        var pickerViewModel = new DirectoryPickerViewModel(_agentApiClientFactory, NewSessionRemoteHost);
+        pickerViewModel.PathSelected += OnRemotePathSelected;
+
+        _remotePickerWindow = new DirectoryPickerWindow(pickerViewModel);
+        _remotePickerWindow.SetParent(_window);
+        _remotePickerWindow.Closed += (_, _) =>
+        {
+            _remotePickerWindow = null;
+            NewSessionRemotePathDialogOpen = false;
+        };
+        _remotePickerWindow.Activate();
+    }
+
+    private void OnRemotePathSelected(object? sender, string? path)
+    {
+        if (sender is not DirectoryPickerViewModel pickerViewModel) return;
+        pickerViewModel.PathSelected -= OnRemotePathSelected;
+
+        if (path == null) return;
+        NewSessionRemotePath = path;
+    }
+
     [RelayCommand]
     private void CancelNewSession()
     {
@@ -300,7 +396,7 @@ await _syncSessionController.CreateSyncSession(new CreateSyncSessionRequest
                 Beta = new CreateSyncSessionRequest.Endpoint
                 {
                     Protocol = CreateSyncSessionRequest.Endpoint.ProtocolKind.Ssh,
-                    Host = NewSessionRemoteHost,
+                    Host = NewSessionRemoteHost!,
                     Path = NewSessionRemotePath,
                 },
             }, OnCreateSessionProgress, cts.Token);
diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs
index 532bfe4..f845521 100644
--- a/App/ViewModels/TrayWindowViewModel.cs
+++ b/App/ViewModels/TrayWindowViewModel.cs
@@ -178,6 +178,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
             {
                 // We just assume that it's a single-agent workspace.
                 Hostname = workspace.Name,
+                // TODO: this needs to get the suffix from the server
                 HostnameSuffix = ".coder",
                 ConnectionStatus = AgentConnectionStatus.Gray,
                 DashboardUrl = WorkspaceUri(coderUri, workspace.Name),
diff --git a/App/Views/DirectoryPickerWindow.xaml b/App/Views/DirectoryPickerWindow.xaml
new file mode 100644
index 0000000..8a107cb
--- /dev/null
+++ b/App/Views/DirectoryPickerWindow.xaml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<winuiex:WindowEx
+    x:Class="Coder.Desktop.App.Views.DirectoryPickerWindow"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:winuiex="using:WinUIEx"
+    mc:Ignorable="d"
+    Title="Directory Picker"
+    Width="400" Height="600"
+    MinWidth="400" MinHeight="600">
+
+    <Window.SystemBackdrop>
+        <DesktopAcrylicBackdrop />
+    </Window.SystemBackdrop>
+
+    <Frame x:Name="RootFrame" />
+</winuiex:WindowEx>
diff --git a/App/Views/DirectoryPickerWindow.xaml.cs b/App/Views/DirectoryPickerWindow.xaml.cs
new file mode 100644
index 0000000..6ed5f43
--- /dev/null
+++ b/App/Views/DirectoryPickerWindow.xaml.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Runtime.InteropServices;
+using Windows.Graphics;
+using Coder.Desktop.App.ViewModels;
+using Coder.Desktop.App.Views.Pages;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using WinRT.Interop;
+using WinUIEx;
+
+namespace Coder.Desktop.App.Views;
+
+public sealed partial class DirectoryPickerWindow : WindowEx
+{
+    public DirectoryPickerWindow(DirectoryPickerViewModel viewModel)
+    {
+        InitializeComponent();
+        SystemBackdrop = new DesktopAcrylicBackdrop();
+
+        viewModel.Initialize(this, DispatcherQueue);
+        RootFrame.Content = new DirectoryPickerMainPage(viewModel);
+
+        // This will be moved to the center of the parent window in SetParent.
+        this.CenterOnScreen();
+    }
+
+    public void SetParent(Window parentWindow)
+    {
+        // Move the window to the center of the parent window.
+        var scale = DisplayScale.WindowScale(parentWindow);
+        var windowPos = new PointInt32(
+            parentWindow.AppWindow.Position.X + parentWindow.AppWindow.Size.Width / 2 - AppWindow.Size.Width / 2,
+            parentWindow.AppWindow.Position.Y + parentWindow.AppWindow.Size.Height / 2 - AppWindow.Size.Height / 2
+        );
+
+        // Ensure we stay within the display.
+        var workArea = DisplayArea.GetFromPoint(parentWindow.AppWindow.Position, DisplayAreaFallback.Primary).WorkArea;
+        if (windowPos.X + AppWindow.Size.Width > workArea.X + workArea.Width) // right edge
+            windowPos.X = workArea.X + workArea.Width - AppWindow.Size.Width;
+        if (windowPos.Y + AppWindow.Size.Height > workArea.Y + workArea.Height) // bottom edge
+            windowPos.Y = workArea.Y + workArea.Height - AppWindow.Size.Height;
+        if (windowPos.X < workArea.X) // left edge
+            windowPos.X = workArea.X;
+        if (windowPos.Y < workArea.Y) // top edge
+            windowPos.Y = workArea.Y;
+
+        AppWindow.Move(windowPos);
+
+        var parentHandle = WindowNative.GetWindowHandle(parentWindow);
+        var thisHandle = WindowNative.GetWindowHandle(this);
+
+        // Set the parent window in win API.
+        NativeApi.SetWindowParent(thisHandle, parentHandle);
+
+        // Override the presenter, which allows us to enable modal-like
+        // behavior for this window:
+        // - Disables the parent window
+        // - Any activations of the parent window will play a bell sound and
+        //   focus the modal window
+        //
+        // This behavior is very similar to the native file/directory picker on
+        // Windows.
+        var presenter = OverlappedPresenter.CreateForDialog();
+        presenter.IsModal = true;
+        AppWindow.SetPresenter(presenter);
+        AppWindow.Show();
+
+        // Cascade close events.
+        parentWindow.Closed += OnParentWindowClosed;
+        Closed += (_, _) =>
+        {
+            parentWindow.Closed -= OnParentWindowClosed;
+            parentWindow.Activate();
+        };
+    }
+
+    private void OnParentWindowClosed(object? sender, WindowEventArgs e)
+    {
+        Close();
+    }
+
+    private static class NativeApi
+    {
+        [DllImport("user32.dll")]
+        private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
+
+        public static void SetWindowParent(IntPtr window, IntPtr parent)
+        {
+            SetWindowLongPtr(window, -8, parent);
+        }
+    }
+}
diff --git a/App/Views/FileSyncListWindow.xaml.cs b/App/Views/FileSyncListWindow.xaml.cs
index 8a409d7..428363b 100644
--- a/App/Views/FileSyncListWindow.xaml.cs
+++ b/App/Views/FileSyncListWindow.xaml.cs
@@ -16,7 +16,7 @@ public FileSyncListWindow(FileSyncListViewModel viewModel)
         SystemBackdrop = new DesktopAcrylicBackdrop();
 
         ViewModel.Initialize(this, DispatcherQueue);
-        RootFrame.Content = new FileSyncListMainPage(ViewModel, this);
+        RootFrame.Content = new FileSyncListMainPage(ViewModel);
 
         this.CenterOnScreen();
     }
diff --git a/App/Views/Pages/DirectoryPickerMainPage.xaml b/App/Views/Pages/DirectoryPickerMainPage.xaml
new file mode 100644
index 0000000..dd08c46
--- /dev/null
+++ b/App/Views/Pages/DirectoryPickerMainPage.xaml
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<Page
+    x:Class="Coder.Desktop.App.Views.Pages.DirectoryPickerMainPage"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:converters="using:Coder.Desktop.App.Converters"
+    xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
+    xmlns:viewmodels="using:Coder.Desktop.App.ViewModels"
+    mc:Ignorable="d"
+    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
+
+    <Grid>
+        <Grid
+            Visibility="{x:Bind ViewModel.ShowLoadingScreen, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
+            Padding="60,60"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center">
+
+            <ProgressRing
+                Width="32"
+                Height="32"
+                Margin="0,30"
+                HorizontalAlignment="Center" />
+
+            <TextBlock HorizontalAlignment="Center" Text="Loading home directory..." />
+        </Grid>
+
+        <Grid
+            Visibility="{x:Bind ViewModel.ShowErrorScreen, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
+            Padding="20">
+
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+
+            <ScrollView Grid.Row="0">
+                <TextBlock
+                    Margin="0,0,0,20"
+                    Foreground="Red"
+                    TextWrapping="Wrap"
+                    Text="{x:Bind ViewModel.InitialLoadError, Mode=OneWay}" />
+            </ScrollView>
+
+            <Button Grid.Row="1" Command="{x:Bind ViewModel.RetryLoadCommand, Mode=OneWay}">
+                <TextBlock Text="Reload" />
+            </Button>
+        </Grid>
+
+        <Grid
+            Visibility="{x:Bind ViewModel.ShowListScreen, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
+            Padding="20">
+
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="*" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+
+            <Grid Grid.Row="0">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="*" />
+                    <ColumnDefinition Width="Auto" />
+                </Grid.ColumnDefinitions>
+
+                <TextBlock
+                    Grid.Column="0"
+                    Text="{x:Bind ViewModel.AgentFqdn}"
+                    Style="{StaticResource SubtitleTextBlockStyle}"
+                    TextTrimming="CharacterEllipsis"
+                    IsTextTrimmedChanged="TooltipText_IsTextTrimmedChanged"
+                    Margin="0,0,0,10" />
+                <ProgressRing
+                    Grid.Column="1"
+                    IsActive="{x:Bind ViewModel.NavigatingLoading, Mode=OneWay}"
+                    Width="24"
+                    Height="24"
+                    Margin="10,0"
+                    HorizontalAlignment="Right" />
+            </Grid>
+
+            <ItemsRepeater
+                Grid.Row="1"
+                Margin="-4,0,0,15"
+                ItemsSource="{x:Bind ViewModel.Breadcrumbs, Mode=OneWay}">
+
+                <ItemsRepeater.Layout>
+                    <toolkit:WrapLayout Orientation="Horizontal" />
+                </ItemsRepeater.Layout>
+
+                <ItemsRepeater.ItemTemplate>
+                    <DataTemplate x:DataType="viewmodels:DirectoryPickerBreadcrumb">
+                        <StackPanel Orientation="Horizontal">
+                            <!-- Add a chevron before each item except the "root" item -->
+                            <FontIcon
+                                Glyph="&#xE974;"
+                                FontSize="14"
+                                Visibility="{x:Bind IsFirst, Converter={StaticResource InverseBoolToVisibilityConverter}}" />
+                            <HyperlinkButton
+                                Content="{x:Bind Name}"
+                                Command="{x:Bind ViewModel.ListPathCommand}"
+                                CommandParameter="{x:Bind AbsolutePathSegments}"
+                                Padding="2,-1,2,0" />
+                        </StackPanel>
+                    </DataTemplate>
+                </ItemsRepeater.ItemTemplate>
+            </ItemsRepeater>
+
+            <ScrollView Grid.Row="2" Margin="-12,0,-12,15">
+                <ItemsRepeater ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}">
+                    <ItemsRepeater.Layout>
+                        <StackLayout Orientation="Vertical" />
+                    </ItemsRepeater.Layout>
+
+                    <ItemsRepeater.ItemTemplate>
+                        <DataTemplate x:DataType="viewmodels:DirectoryPickerItem">
+                            <HyperlinkButton
+                                IsEnabled="{x:Bind Selectable}"
+                                Command="{x:Bind ViewModel.ListPathCommand}"
+                                CommandParameter="{x:Bind AbsolutePathSegments}"
+                                HorizontalAlignment="Stretch"
+                                HorizontalContentAlignment="Left">
+
+                                <Grid>
+                                    <Grid.Resources>
+                                        <converters:StringToStringSelector x:Key="Icon"
+                                                                           SelectedKey="{x:Bind Path=Kind}">
+                                            <converters:StringToStringSelectorItem Value="&#xE8A5;" />
+                                            <!-- Document -->
+                                            <converters:StringToStringSelectorItem Key="ParentDirectory"
+                                                Value="&#xE72B;" /> <!-- Back -->
+                                            <converters:StringToStringSelectorItem Key="Directory" Value="&#xE8B7;" />
+                                            <!-- Folder -->
+                                            <converters:StringToStringSelectorItem Key="File" Value="&#xE8A5;" />
+                                            <!-- Document -->
+                                        </converters:StringToStringSelector>
+                                    </Grid.Resources>
+
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="Auto" />
+                                        <ColumnDefinition Width="*" />
+                                    </Grid.ColumnDefinitions>
+
+                                    <!-- The accent-colored icon actually looks nice here, so we don't override it -->
+                                    <FontIcon
+                                        Grid.Column="0"
+                                        Glyph="{Binding Source={StaticResource Icon}, Path=SelectedObject}"
+                                        Margin="0,0,10,0" FontSize="16" />
+                                    <TextBlock
+                                        Grid.Column="1"
+                                        Text="{x:Bind Name}"
+                                        Foreground="{ThemeResource DefaultTextForegroundThemeBrush}"
+                                        TextTrimming="CharacterEllipsis"
+                                        IsTextTrimmedChanged="TooltipText_IsTextTrimmedChanged" />
+                                </Grid>
+                            </HyperlinkButton>
+                        </DataTemplate>
+                    </ItemsRepeater.ItemTemplate>
+                </ItemsRepeater>
+            </ScrollView>
+
+            <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
+                <Button
+                    Content="Cancel"
+                    Command="{x:Bind ViewModel.CancelCommand}"
+                    Margin="0,0,10,0" />
+                <Button
+                    IsEnabled="{x:Bind ViewModel.IsSelectable, Mode=OneWay}"
+                    Content="Use This Directory"
+                    Command="{x:Bind ViewModel.SelectCommand}"
+                    Style="{StaticResource AccentButtonStyle}" />
+            </StackPanel>
+        </Grid>
+    </Grid>
+</Page>
diff --git a/App/Views/Pages/DirectoryPickerMainPage.xaml.cs b/App/Views/Pages/DirectoryPickerMainPage.xaml.cs
new file mode 100644
index 0000000..4e26200
--- /dev/null
+++ b/App/Views/Pages/DirectoryPickerMainPage.xaml.cs
@@ -0,0 +1,27 @@
+using Coder.Desktop.App.ViewModels;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Coder.Desktop.App.Views.Pages;
+
+public sealed partial class DirectoryPickerMainPage : Page
+{
+    public readonly DirectoryPickerViewModel ViewModel;
+
+    public DirectoryPickerMainPage(DirectoryPickerViewModel viewModel)
+    {
+        ViewModel = viewModel;
+        InitializeComponent();
+    }
+
+    private void TooltipText_IsTextTrimmedChanged(TextBlock sender, IsTextTrimmedChangedEventArgs e)
+    {
+        ToolTipService.SetToolTip(sender, null);
+        if (!sender.IsTextTrimmed) return;
+
+        var toolTip = new ToolTip
+        {
+            Content = sender.Text,
+        };
+        ToolTipService.SetToolTip(sender, toolTip);
+    }
+}
diff --git a/App/Views/Pages/FileSyncListMainPage.xaml b/App/Views/Pages/FileSyncListMainPage.xaml
index 5a96898..cb9f2bb 100644
--- a/App/Views/Pages/FileSyncListMainPage.xaml
+++ b/App/Views/Pages/FileSyncListMainPage.xaml
@@ -38,21 +38,27 @@
             <TextBlock HorizontalAlignment="Center" Text="Loading sync sessions..." />
         </Grid>
 
-        <StackPanel
+        <Grid
             Visibility="{x:Bind ViewModel.ShowError, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
-            Orientation="Vertical"
             Padding="20">
 
-            <TextBlock
-                Margin="0,0,0,20"
-                Foreground="Red"
-                TextWrapping="Wrap"
-                Text="{x:Bind ViewModel.Error, Mode=OneWay}" />
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+
+            <ScrollView Grid.Row="0">
+                <TextBlock
+                    Margin="0,0,0,20"
+                    Foreground="Red"
+                    TextWrapping="Wrap"
+                    Text="{x:Bind ViewModel.Error, Mode=OneWay}" />
+            </ScrollView>
 
-            <Button Command="{x:Bind ViewModel.ReloadSessionsCommand, Mode=OneWay}">
+            <Button Grid.Row="1" Command="{x:Bind ViewModel.ReloadSessionsCommand, Mode=OneWay}">
                 <TextBlock Text="Reload" />
             </Button>
-        </StackPanel>
+        </Grid>
 
         <!-- This grid lets us fix the header and only scroll the content. -->
         <Grid
@@ -80,7 +86,7 @@
                             <Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
                         </Style>
                         <Style TargetType="Border">
-                            <Setter Property="Padding" Value="40,0,0,0" />
+                            <Setter Property="Padding" Value="30,0,0,0" />
                         </Style>
                     </Grid.Resources>
 
@@ -132,7 +138,7 @@
                                     <!-- These are (mostly) from the header Grid and should be copied here -->
                                     <Grid.Resources>
                                         <Style TargetType="Border">
-                                            <Setter Property="Padding" Value="40,0,0,0" />
+                                            <Setter Property="Padding" Value="30,0,0,0" />
                                         </Style>
                                     </Grid.Resources>
                                     <Grid.ColumnDefinitions>
@@ -266,7 +272,7 @@
                         <!-- These are (mostly) from the header Grid and should be copied here -->
                         <Grid.Resources>
                             <Style TargetType="Border">
-                                <Setter Property="Padding" Value="40,0,0,0" />
+                                <Setter Property="Padding" Value="30,0,0,0" />
                             </Style>
                         </Grid.Resources>
                         <Grid.ColumnDefinitions>
@@ -317,7 +323,7 @@
                                 <Button
                                     Grid.Column="1"
                                     IsEnabled="{x:Bind ViewModel.NewSessionLocalPathDialogOpen, Converter={StaticResource InverseBoolConverter}, Mode=OneWay}"
-                                    Command="{x:Bind OpenLocalPathSelectDialogCommand}"
+                                    Command="{x:Bind ViewModel.OpenLocalPathSelectDialogCommand}"
                                     VerticalAlignment="Stretch">
 
                                     <FontIcon Glyph="&#xE838;" FontSize="13" />
@@ -325,23 +331,36 @@
                             </Grid>
                         </Border>
                         <Border Grid.Column="2">
-                            <!-- TODO: use a combo box for workspace agents -->
-                            <!--
                             <ComboBox
-                                ItemsSource="{x:Bind WorkspaceAgents}"
+                                IsEnabled="{x:Bind ViewModel.NewSessionRemoteHostEnabled, Mode=OneWay}"
+                                ItemsSource="{x:Bind ViewModel.AvailableHosts, Mode=OneWay}"
+                                SelectedItem="{x:Bind ViewModel.NewSessionRemoteHost, Mode=TwoWay}"
+                                ToolTipService.ToolTip="{x:Bind ViewModel.NewSessionRemoteHost, Mode=OneWay}"
                                 VerticalAlignment="Stretch"
                                 HorizontalAlignment="Stretch" />
-                            -->
-                            <TextBox
-                                VerticalAlignment="Stretch"
-                                HorizontalAlignment="Stretch"
-                                Text="{x:Bind ViewModel.NewSessionRemoteHost, Mode=TwoWay}" />
                         </Border>
                         <Border Grid.Column="3">
-                            <TextBox
-                                VerticalAlignment="Stretch"
-                                HorizontalAlignment="Stretch"
-                                Text="{x:Bind ViewModel.NewSessionRemotePath, Mode=TwoWay}" />
+                            <Grid>
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="*" />
+                                    <ColumnDefinition Width="Auto" />
+                                </Grid.ColumnDefinitions>
+
+                                <TextBox
+                                    Grid.Column="0"
+                                    Margin="0,0,5,0"
+                                    VerticalAlignment="Stretch"
+                                    Text="{x:Bind ViewModel.NewSessionRemotePath, Mode=TwoWay}" />
+
+                                <Button
+                                    Grid.Column="1"
+                                    IsEnabled="{x:Bind ViewModel.NewSessionRemotePathDialogEnabled, Mode=OneWay}"
+                                    Command="{x:Bind ViewModel.OpenRemotePathSelectDialogCommand}"
+                                    VerticalAlignment="Stretch">
+
+                                    <FontIcon Glyph="&#xE838;" FontSize="13" />
+                                </Button>
+                            </Grid>
                         </Border>
                         <Border Grid.Column="4">
                             <TextBlock
diff --git a/App/Views/Pages/FileSyncListMainPage.xaml.cs b/App/Views/Pages/FileSyncListMainPage.xaml.cs
index c54c29e..a677522 100644
--- a/App/Views/Pages/FileSyncListMainPage.xaml.cs
+++ b/App/Views/Pages/FileSyncListMainPage.xaml.cs
@@ -1,7 +1,4 @@
-using System.Threading.Tasks;
 using Coder.Desktop.App.ViewModels;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.UI.Xaml;
 using Microsoft.UI.Xaml.Controls;
 
 namespace Coder.Desktop.App.Views.Pages;
@@ -10,12 +7,9 @@ public sealed partial class FileSyncListMainPage : Page
 {
     public FileSyncListViewModel ViewModel;
 
-    private readonly Window _window;
-
-    public FileSyncListMainPage(FileSyncListViewModel viewModel, Window window)
+    public FileSyncListMainPage(FileSyncListViewModel viewModel)
     {
         ViewModel = viewModel; // already initialized
-        _window = window;
         InitializeComponent();
     }
 
@@ -31,10 +25,4 @@ private void TooltipText_IsTextTrimmedChanged(TextBlock sender, IsTextTrimmedCha
         };
         ToolTipService.SetToolTip(sender, toolTip);
     }
-
-    [RelayCommand]
-    public async Task OpenLocalPathSelectDialog()
-    {
-        await ViewModel.OpenLocalPathSelectDialog(_window);
-    }
 }
diff --git a/App/packages.lock.json b/App/packages.lock.json
index 5561686..1541d01 100644
--- a/App/packages.lock.json
+++ b/App/packages.lock.json
@@ -8,6 +8,16 @@
         "resolved": "8.4.0",
         "contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
       },
+      "CommunityToolkit.WinUI.Controls.Primitives": {
+        "type": "Direct",
+        "requested": "[8.2.250402, )",
+        "resolved": "8.2.250402",
+        "contentHash": "Wx3t1zADrzBWDar45uRl+lmSxDO5Vx7tTMFm/mNgl3fs5xSQ1ySPdGqD10EFov3rkKc5fbpHGW5xj8t62Yisvg==",
+        "dependencies": {
+          "CommunityToolkit.WinUI.Extensions": "8.2.250402",
+          "Microsoft.WindowsAppSDK": "1.6.250108002"
+        }
+      },
       "DependencyPropertyGenerator": {
         "type": "Direct",
         "requested": "[1.5.0, )",
@@ -127,6 +137,20 @@
           "Microsoft.WindowsAppSDK": "1.6.240829007"
         }
       },
+      "CommunityToolkit.Common": {
+        "type": "Transitive",
+        "resolved": "8.2.1",
+        "contentHash": "LWuhy8cQKJ/MYcy3XafJ916U3gPH/YDvYoNGWyQWN11aiEKCZszzPOTJAOvBjP9yG8vHmIcCyPUt4L82OK47Iw=="
+      },
+      "CommunityToolkit.WinUI.Extensions": {
+        "type": "Transitive",
+        "resolved": "8.2.250402",
+        "contentHash": "rAOYzNX6kdUeeE1ejGd6Q8B+xmyZvOrWFUbqCgOtP8OQsOL66en9ZQTtzxAlaaFC4qleLvnKcn8FJFBezujOlw==",
+        "dependencies": {
+          "CommunityToolkit.Common": "8.2.1",
+          "Microsoft.WindowsAppSDK": "1.6.250108002"
+        }
+      },
       "Google.Protobuf": {
         "type": "Transitive",
         "resolved": "3.29.3",
diff --git a/CoderSdk/Agent/AgentApiClient.cs b/CoderSdk/Agent/AgentApiClient.cs
new file mode 100644
index 0000000..27eaea3
--- /dev/null
+++ b/CoderSdk/Agent/AgentApiClient.cs
@@ -0,0 +1,61 @@
+using System.Text.Json.Serialization;
+
+namespace Coder.Desktop.CoderSdk.Agent;
+
+public interface IAgentApiClientFactory
+{
+    public IAgentApiClient Create(string hostname);
+}
+
+public class AgentApiClientFactory : IAgentApiClientFactory
+{
+    public IAgentApiClient Create(string hostname)
+    {
+        return new AgentApiClient(hostname);
+    }
+}
+
+public partial interface IAgentApiClient
+{
+}
+
+[JsonSerializable(typeof(ListDirectoryRequest))]
+[JsonSerializable(typeof(ListDirectoryResponse))]
+[JsonSerializable(typeof(Response))]
+public partial class AgentApiJsonContext : JsonSerializerContext;
+
+public partial class AgentApiClient : IAgentApiClient
+{
+    private const int AgentApiPort = 4;
+
+    private readonly JsonHttpClient _httpClient;
+
+    public AgentApiClient(string hostname) : this(new UriBuilder
+    {
+        Scheme = "http",
+        Host = hostname,
+        Port = AgentApiPort,
+        Path = "/",
+    }.Uri)
+    {
+    }
+
+    public AgentApiClient(Uri baseUrl)
+    {
+        if (baseUrl.PathAndQuery != "/")
+            throw new ArgumentException($"Base URL '{baseUrl}' must not contain a path", nameof(baseUrl));
+        _httpClient = new JsonHttpClient(baseUrl, AgentApiJsonContext.Default);
+    }
+
+    private async Task<TResponse> SendRequestNoBodyAsync<TResponse>(HttpMethod method, string path,
+        CancellationToken ct = default)
+    {
+        return await SendRequestAsync<object, TResponse>(method, path, null, ct);
+    }
+
+    private Task<TResponse> SendRequestAsync<TRequest, TResponse>(HttpMethod method, string path,
+        TRequest? payload, CancellationToken ct = default)
+    {
+        return _httpClient.SendRequestAsync<TRequest, TResponse>(method, path, payload, ct);
+    }
+}
diff --git a/CoderSdk/Agent/ListDirectory.cs b/CoderSdk/Agent/ListDirectory.cs
new file mode 100644
index 0000000..72e4a15
--- /dev/null
+++ b/CoderSdk/Agent/ListDirectory.cs
@@ -0,0 +1,54 @@
+namespace Coder.Desktop.CoderSdk.Agent;
+
+public partial interface IAgentApiClient
+{
+    public Task<ListDirectoryResponse> ListDirectory(ListDirectoryRequest req, CancellationToken ct = default);
+}
+
+public enum ListDirectoryRelativity
+{
+    // Root means `/` on Linux, and lists drive letters on Windows.
+    Root,
+
+    // Home means the user's home directory, usually `/home/xyz` or
+    // `C:\Users\xyz`.
+    Home,
+}
+
+public class ListDirectoryRequest
+{
+    // Path segments like ["home", "coder", "repo"] or even just []
+    public List<string> Path { get; set; } = [];
+
+    // Where the path originates, either in the home directory or on the root
+    // of the system
+    public ListDirectoryRelativity Relativity { get; set; } = ListDirectoryRelativity.Root;
+}
+
+public class ListDirectoryItem
+{
+    public required string Name { get; init; }
+    public required string AbsolutePathString { get; init; }
+    public required bool IsDir { get; init; }
+}
+
+public class ListDirectoryResponse
+{
+    // The resolved absolute path (always from root) for future requests.
+    // E.g. if you did a request like `home: ["repo"]`,
+    // this would return ["home", "coder", "repo"] and "/home/coder/repo"
+    public required List<string> AbsolutePath { get; init; }
+
+    // e.g. "C:\\Users\\coder\\repo" or "/home/coder/repo"
+    public required string AbsolutePathString { get; init; }
+    public required List<ListDirectoryItem> Contents { get; init; }
+}
+
+public partial class AgentApiClient
+{
+    public Task<ListDirectoryResponse> ListDirectory(ListDirectoryRequest req, CancellationToken ct = default)
+    {
+        return SendRequestAsync<ListDirectoryRequest, ListDirectoryResponse>(HttpMethod.Post, "/api/v0/list-directory",
+            req, ct);
+    }
+}
diff --git a/CoderSdk/Coder/CoderApiClient.cs b/CoderSdk/Coder/CoderApiClient.cs
new file mode 100644
index 0000000..79c5c2f
--- /dev/null
+++ b/CoderSdk/Coder/CoderApiClient.cs
@@ -0,0 +1,71 @@
+using System.Text.Json.Serialization;
+
+namespace Coder.Desktop.CoderSdk.Coder;
+
+public interface ICoderApiClientFactory
+{
+    public ICoderApiClient Create(string baseUrl);
+}
+
+public class CoderApiClientFactory : ICoderApiClientFactory
+{
+    public ICoderApiClient Create(string baseUrl)
+    {
+        return new CoderApiClient(baseUrl);
+    }
+}
+
+public partial interface ICoderApiClient
+{
+    public void SetSessionToken(string token);
+}
+
+[JsonSerializable(typeof(BuildInfo))]
+[JsonSerializable(typeof(Response))]
+[JsonSerializable(typeof(User))]
+[JsonSerializable(typeof(ValidationError))]
+public partial class CoderApiJsonContext : JsonSerializerContext;
+
+/// <summary>
+///     Provides a limited selection of API methods for a Coder instance.
+/// </summary>
+public partial class CoderApiClient : ICoderApiClient
+{
+    private const string SessionTokenHeader = "Coder-Session-Token";
+
+    private readonly JsonHttpClient _httpClient;
+
+    public CoderApiClient(string baseUrl) : this(new Uri(baseUrl, UriKind.Absolute))
+    {
+    }
+
+    public CoderApiClient(Uri baseUrl)
+    {
+        if (baseUrl.PathAndQuery != "/")
+            throw new ArgumentException($"Base URL '{baseUrl}' must not contain a path", nameof(baseUrl));
+        _httpClient = new JsonHttpClient(baseUrl, CoderApiJsonContext.Default);
+    }
+
+    public CoderApiClient(string baseUrl, string token) : this(baseUrl)
+    {
+        SetSessionToken(token);
+    }
+
+    public void SetSessionToken(string token)
+    {
+        _httpClient.RemoveHeader(SessionTokenHeader);
+        _httpClient.SetHeader(SessionTokenHeader, token);
+    }
+
+    private async Task<TResponse> SendRequestNoBodyAsync<TResponse>(HttpMethod method, string path,
+        CancellationToken ct = default)
+    {
+        return await SendRequestAsync<object, TResponse>(method, path, null, ct);
+    }
+
+    private Task<TResponse> SendRequestAsync<TRequest, TResponse>(HttpMethod method, string path,
+        TRequest? payload, CancellationToken ct = default)
+    {
+        return _httpClient.SendRequestAsync<TRequest, TResponse>(method, path, payload, ct);
+    }
+}
diff --git a/CoderSdk/Deployment.cs b/CoderSdk/Coder/Deployment.cs
similarity index 91%
rename from CoderSdk/Deployment.cs
rename to CoderSdk/Coder/Deployment.cs
index e95e039..978d79d 100644
--- a/CoderSdk/Deployment.cs
+++ b/CoderSdk/Coder/Deployment.cs
@@ -1,4 +1,4 @@
-namespace Coder.Desktop.CoderSdk;
+namespace Coder.Desktop.CoderSdk.Coder;
 
 public partial interface ICoderApiClient
 {
diff --git a/CoderSdk/Users.cs b/CoderSdk/Coder/Users.cs
similarity index 91%
rename from CoderSdk/Users.cs
rename to CoderSdk/Coder/Users.cs
index fd81b32..6d1914b 100644
--- a/CoderSdk/Users.cs
+++ b/CoderSdk/Coder/Users.cs
@@ -1,4 +1,4 @@
-namespace Coder.Desktop.CoderSdk;
+namespace Coder.Desktop.CoderSdk.Coder;
 
 public partial interface ICoderApiClient
 {
diff --git a/CoderSdk/CoderApiClient.cs b/CoderSdk/CoderApiClient.cs
deleted file mode 100644
index df2d923..0000000
--- a/CoderSdk/CoderApiClient.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Coder.Desktop.CoderSdk;
-
-public interface ICoderApiClientFactory
-{
-    public ICoderApiClient Create(string baseUrl);
-}
-
-public class CoderApiClientFactory : ICoderApiClientFactory
-{
-    public ICoderApiClient Create(string baseUrl)
-    {
-        return new CoderApiClient(baseUrl);
-    }
-}
-
-public partial interface ICoderApiClient
-{
-    public void SetSessionToken(string token);
-}
-
-/// <summary>
-///     Changes names from PascalCase to snake_case.
-/// </summary>
-internal class SnakeCaseNamingPolicy : JsonNamingPolicy
-{
-    public override string ConvertName(string name)
-    {
-        return string.Concat(
-            name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + char.ToLower(x) : char.ToLower(x).ToString())
-        );
-    }
-}
-
-[JsonSerializable(typeof(BuildInfo))]
-[JsonSerializable(typeof(Response))]
-[JsonSerializable(typeof(User))]
-[JsonSerializable(typeof(ValidationError))]
-public partial class CoderSdkJsonContext : JsonSerializerContext;
-
-/// <summary>
-///     Provides a limited selection of API methods for a Coder instance.
-/// </summary>
-public partial class CoderApiClient : ICoderApiClient
-{
-    public static readonly JsonSerializerOptions JsonOptions = new()
-    {
-        TypeInfoResolver = CoderSdkJsonContext.Default,
-        PropertyNameCaseInsensitive = true,
-        PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
-        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
-    };
-
-    // TODO: allow adding headers
-    private readonly HttpClient _httpClient = new();
-
-    public CoderApiClient(string baseUrl) : this(new Uri(baseUrl, UriKind.Absolute))
-    {
-    }
-
-    public CoderApiClient(Uri baseUrl)
-    {
-        if (baseUrl.PathAndQuery != "/")
-            throw new ArgumentException($"Base URL '{baseUrl}' must not contain a path", nameof(baseUrl));
-        _httpClient.BaseAddress = baseUrl;
-    }
-
-    public CoderApiClient(string baseUrl, string token) : this(baseUrl)
-    {
-        SetSessionToken(token);
-    }
-
-    public void SetSessionToken(string token)
-    {
-        _httpClient.DefaultRequestHeaders.Remove("Coder-Session-Token");
-        _httpClient.DefaultRequestHeaders.Add("Coder-Session-Token", token);
-    }
-
-    private async Task<TResponse> SendRequestNoBodyAsync<TResponse>(HttpMethod method, string path,
-        CancellationToken ct = default)
-    {
-        return await SendRequestAsync<object, TResponse>(method, path, null, ct);
-    }
-
-    private async Task<TResponse> SendRequestAsync<TRequest, TResponse>(HttpMethod method, string path,
-        TRequest? payload, CancellationToken ct = default)
-    {
-        try
-        {
-            var request = new HttpRequestMessage(method, path);
-
-            if (payload is not null)
-            {
-                var json = JsonSerializer.Serialize(payload, typeof(TRequest), JsonOptions);
-                request.Content = new StringContent(json, Encoding.UTF8, "application/json");
-            }
-
-            var res = await _httpClient.SendAsync(request, ct);
-            if (!res.IsSuccessStatusCode)
-                throw await CoderApiHttpException.FromResponse(res, ct);
-
-            var content = await res.Content.ReadAsStringAsync(ct);
-            var data = JsonSerializer.Deserialize<TResponse>(content, JsonOptions);
-            if (data is null) throw new JsonException("Deserialized response is null");
-            return data;
-        }
-        catch (CoderApiHttpException)
-        {
-            throw;
-        }
-        catch (Exception e)
-        {
-            throw new Exception($"Coder API Request failed: {method} {path}", e);
-        }
-    }
-}
diff --git a/CoderSdk/Errors.cs b/CoderSdk/Errors.cs
index 4d79a59..a7c56c0 100644
--- a/CoderSdk/Errors.cs
+++ b/CoderSdk/Errors.cs
@@ -1,5 +1,6 @@
 using System.Net;
 using System.Text.Json;
+using System.Text.Json.Serialization;
 
 namespace Coder.Desktop.CoderSdk;
 
@@ -16,8 +17,20 @@ public class Response
     public List<ValidationError> Validations { get; set; } = [];
 }
 
+[JsonSerializable(typeof(Response))]
+[JsonSerializable(typeof(ValidationError))]
+public partial class ErrorJsonContext : JsonSerializerContext;
+
 public class CoderApiHttpException : Exception
 {
+    private static readonly JsonSerializerOptions JsonOptions = new()
+    {
+        TypeInfoResolver = ErrorJsonContext.Default,
+        PropertyNameCaseInsensitive = true,
+        PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
+        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+    };
+
     private static readonly Dictionary<HttpStatusCode, string> Helpers = new()
     {
         { HttpStatusCode.Unauthorized, "Try signing in again" },
@@ -45,7 +58,7 @@ public static async Task<CoderApiHttpException> FromResponse(HttpResponseMessage
         Response? responseObject;
         try
         {
-            responseObject = JsonSerializer.Deserialize<Response>(content, CoderApiClient.JsonOptions);
+            responseObject = JsonSerializer.Deserialize<Response>(content, JsonOptions);
         }
         catch (JsonException)
         {
diff --git a/CoderSdk/JsonHttpClient.cs b/CoderSdk/JsonHttpClient.cs
new file mode 100644
index 0000000..362391e
--- /dev/null
+++ b/CoderSdk/JsonHttpClient.cs
@@ -0,0 +1,82 @@
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Coder.Desktop.CoderSdk;
+
+/// <summary>
+///     Changes names from PascalCase to snake_case.
+/// </summary>
+internal class SnakeCaseNamingPolicy : JsonNamingPolicy
+{
+    public override string ConvertName(string name)
+    {
+        return string.Concat(
+            name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + char.ToLower(x) : char.ToLower(x).ToString())
+        );
+    }
+}
+
+internal class JsonHttpClient
+{
+    private readonly JsonSerializerOptions _jsonOptions;
+
+    // TODO: allow users to add headers
+    private readonly HttpClient _httpClient = new();
+
+    public JsonHttpClient(Uri baseUri, IJsonTypeInfoResolver typeResolver)
+    {
+        _jsonOptions = new JsonSerializerOptions
+        {
+            TypeInfoResolver = typeResolver,
+            PropertyNameCaseInsensitive = true,
+            PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
+            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+        };
+        _jsonOptions.Converters.Add(new JsonStringEnumConverter(new SnakeCaseNamingPolicy(), false));
+        _httpClient.BaseAddress = baseUri;
+    }
+
+    public void RemoveHeader(string key)
+    {
+        _httpClient.DefaultRequestHeaders.Remove(key);
+    }
+
+    public void SetHeader(string key, string value)
+    {
+        _httpClient.DefaultRequestHeaders.Add(key, value);
+    }
+
+    public async Task<TResponse> SendRequestAsync<TRequest, TResponse>(HttpMethod method, string path,
+        TRequest? payload, CancellationToken ct = default)
+    {
+        try
+        {
+            var request = new HttpRequestMessage(method, path);
+
+            if (payload is not null)
+            {
+                var json = JsonSerializer.Serialize(payload, typeof(TRequest), _jsonOptions);
+                request.Content = new StringContent(json, Encoding.UTF8, "application/json");
+            }
+
+            var res = await _httpClient.SendAsync(request, ct);
+            if (!res.IsSuccessStatusCode)
+                throw await CoderApiHttpException.FromResponse(res, ct);
+
+            var content = await res.Content.ReadAsStringAsync(ct);
+            var data = JsonSerializer.Deserialize<TResponse>(content, _jsonOptions);
+            if (data is null) throw new JsonException("Deserialized response is null");
+            return data;
+        }
+        catch (CoderApiHttpException)
+        {
+            throw;
+        }
+        catch (Exception e)
+        {
+            throw new Exception($"API Request failed: {method} {path}", e);
+        }
+    }
+}
diff --git a/Installer/Program.cs b/Installer/Program.cs
index 1894a2d..10a09a7 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -2,9 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text;
 using CommandLine;
-using Microsoft.Extensions.Configuration;
 using WixSharp;
 using WixSharp.Bootstrapper;
 using WixSharp.CommonTasks;
@@ -389,8 +387,8 @@ private static int BuildBundle(BootstrapperOptions opts)
                 [
                     new ExePackagePayload
                     {
-                        SourceFile = opts.WindowsAppSdkPath
-                    }
+                        SourceFile = opts.WindowsAppSdkPath,
+                    },
                 ],
             },
             new MsiPackage(opts.MsiPath)
diff --git a/Tests.App/Services/CredentialManagerTest.cs b/Tests.App/Services/CredentialManagerTest.cs
index 2fa4699..9d00cf2 100644
--- a/Tests.App/Services/CredentialManagerTest.cs
+++ b/Tests.App/Services/CredentialManagerTest.cs
@@ -1,7 +1,7 @@
 using System.Diagnostics;
 using Coder.Desktop.App.Models;
 using Coder.Desktop.App.Services;
-using Coder.Desktop.CoderSdk;
+using Coder.Desktop.CoderSdk.Coder;
 using Moq;
 
 namespace Coder.Desktop.Tests.App.Services;
diff --git a/Tests.App/Services/MutagenControllerTest.cs b/Tests.App/Services/MutagenControllerTest.cs
index c834009..2c97515 100644
--- a/Tests.App/Services/MutagenControllerTest.cs
+++ b/Tests.App/Services/MutagenControllerTest.cs
@@ -113,6 +113,7 @@ public async Task Ok(CancellationToken ct)
         await AssertDaemonStopped(dataDirectory, ct);
 
         var progressMessages = new List<string>();
+
         void OnProgress(string message)
         {
             TestContext.Out.WriteLine("Create session progress: " + message);
diff --git a/Vpn.Service/Downloader.cs b/Vpn.Service/Downloader.cs
index c7b94c6..6a3108b 100644
--- a/Vpn.Service/Downloader.cs
+++ b/Vpn.Service/Downloader.cs
@@ -297,15 +297,10 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
                 // remove the key first, before checking the exception, to ensure
                 // we still clean up.
                 _downloads.TryRemove(destinationPath, out _);
-                if (tsk.Exception == null)
-                {
-                    return;
-                }
+                if (tsk.Exception == null) return;
 
                 if (tsk.Exception.InnerException != null)
-                {
                     ExceptionDispatchInfo.Capture(tsk.Exception.InnerException).Throw();
-                }
 
                 // not sure if this is hittable, but just in case:
                 throw tsk.Exception;
@@ -328,7 +323,7 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
     }
 
     /// <summary>
-    /// TaskOrCancellation waits for either the task to complete, or the given token to be canceled.
+    ///     TaskOrCancellation waits for either the task to complete, or the given token to be canceled.
     /// </summary>
     internal static async Task TaskOrCancellation(Task task, CancellationToken cancellationToken)
     {
@@ -454,7 +449,6 @@ private async Task Start(CancellationToken ct = default)
             TotalBytes = (ulong)res.Content.Headers.ContentLength;
 
         await Download(res, ct);
-        return;
     }
 
     private async Task Download(HttpResponseMessage res, CancellationToken ct)
@@ -472,6 +466,7 @@ private async Task Download(HttpResponseMessage res, CancellationToken ct)
                 _logger.LogError(e, "Failed to create temporary file '{TempDestinationPath}'", TempDestinationPath);
                 throw;
             }
+
             await using (tempFile)
             {
                 var stream = await res.Content.ReadAsStreamAsync(ct);
diff --git a/Vpn.Service/Manager.cs b/Vpn.Service/Manager.cs
index 1eca8bf..fc014c0 100644
--- a/Vpn.Service/Manager.cs
+++ b/Vpn.Service/Manager.cs
@@ -1,5 +1,5 @@
 using System.Runtime.InteropServices;
-using Coder.Desktop.CoderSdk;
+using Coder.Desktop.CoderSdk.Coder;
 using Coder.Desktop.Vpn.Proto;
 using Coder.Desktop.Vpn.Utilities;
 using Microsoft.Extensions.Logging;