Skip to content

Commit b3c2eaa

Browse files
committed
rework agent expand bubbling
1 parent 5a2ffc9 commit b3c2eaa

10 files changed

+134
-65
lines changed

App/App.xaml.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
using Microsoft.UI.Xaml;
2121
using Microsoft.Win32;
2222
using Microsoft.Windows.AppLifecycle;
23+
using Microsoft.Windows.AppNotifications;
2324
using Serilog;
2425
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
25-
using Microsoft.Windows.AppNotifications;
2626

2727
namespace Coder.Desktop.App;
2828

@@ -196,6 +196,7 @@ public void OnActivated(object? sender, AppActivationArguments args)
196196
_logger.LogWarning("URI activation with null data");
197197
return;
198198
}
199+
199200
HandleURIActivation(protoArgs.Uri);
200201
break;
201202

App/Program.cs

-2
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,9 @@ private static void Main(string[] args)
6363
notificationManager.NotificationInvoked += app.HandleNotification;
6464
notificationManager.Register();
6565
if (activationArgs.Kind != ExtendedActivationKind.Launch)
66-
{
6766
// this means we were activated without having already launched, so handle
6867
// the activation as well.
6968
app.OnActivated(null, activationArgs);
70-
}
7169
});
7270
}
7371
catch (Exception e)

App/Services/UserNotifier.cs

-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ public Task ShowErrorNotification(string title, string message)
2626
return Task.CompletedTask;
2727
}
2828
}
29-

App/Utils/TitleBarIcon.cs

+7-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
using System;
21
using Microsoft.UI;
32
using Microsoft.UI.Windowing;
43
using Microsoft.UI.Xaml;
5-
using Microsoft.UI.Xaml.Controls.Primitives;
64
using WinRT.Interop;
75

8-
namespace Coder.Desktop.App.Utils
6+
namespace Coder.Desktop.App.Utils;
7+
8+
public static class TitleBarIcon
99
{
10-
public static class TitleBarIcon
10+
public static void SetTitlebarIcon(Window window)
1111
{
12-
public static void SetTitlebarIcon(Window window)
13-
{
14-
var hwnd = WindowNative.GetWindowHandle(window);
15-
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
16-
AppWindow.GetFromWindowId(windowId).SetIcon("coder.ico");
17-
}
12+
var hwnd = WindowNative.GetWindowHandle(window);
13+
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
14+
AppWindow.GetFromWindowId(windowId).SetIcon("coder.ico");
1815
}
1916
}

App/ViewModels/AgentAppViewModel.cs

+22-12
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ namespace Coder.Desktop.App.ViewModels;
1818

1919
public interface IAgentAppViewModelFactory
2020
{
21-
public AgentAppViewModel Create(Uuid id, string name, string appUri, Uri? iconUrl);
21+
public AgentAppViewModel Create(Uuid id, string name, Uri appUri, Uri? iconUrl);
2222
}
2323

2424
public class AgentAppViewModelFactory(ILogger<AgentAppViewModel> childLogger, ICredentialManager credentialManager)
2525
: IAgentAppViewModelFactory
2626
{
27-
public AgentAppViewModel Create(Uuid id, string name, string appUri, Uri? iconUrl)
27+
public AgentAppViewModel Create(Uuid id, string name, Uri appUri, Uri? iconUrl)
2828
{
2929
return new AgentAppViewModel(childLogger, credentialManager)
3030
{
@@ -45,11 +45,13 @@ public partial class AgentAppViewModel : ObservableObject, IModelUpdateable<Agen
4545

4646
public required Uuid Id { get; init; }
4747

48-
[ObservableProperty] public required partial string Name { get; set; }
48+
[ObservableProperty]
49+
[NotifyPropertyChangedFor(nameof(Details))]
50+
public required partial string Name { get; set; }
4951

5052
[ObservableProperty]
5153
[NotifyPropertyChangedFor(nameof(Details))]
52-
public required partial string AppUri { get; set; }
54+
public required partial Uri AppUri { get; set; }
5355

5456
[ObservableProperty] public partial Uri? IconUrl { get; set; }
5557

@@ -138,14 +140,22 @@ private void OpenApp(object parameter)
138140
{
139141
try
140142
{
141-
var uriString = AppUri;
142-
var cred = _credentialManager.GetCachedCredentials();
143-
if (cred.State is CredentialState.Valid && cred.ApiToken is not null)
144-
uriString = uriString.Replace(SessionTokenUriVar, cred.ApiToken);
145-
if (uriString.Contains(SessionTokenUriVar))
146-
throw new Exception($"URI contains {SessionTokenUriVar} variable but could not be replaced");
147-
148-
var uri = new Uri(uriString);
143+
var uri = AppUri;
144+
145+
// http and https URLs should already be filtered out by
146+
// AgentViewModel, but as a second line of defence don't do session
147+
// token var replacement on those URLs.
148+
if (uri.Scheme is not "http" and not "https")
149+
{
150+
var cred = _credentialManager.GetCachedCredentials();
151+
if (cred.State is CredentialState.Valid && cred.ApiToken is not null)
152+
uri = new Uri(uri.ToString().Replace(SessionTokenUriVar, cred.ApiToken));
153+
}
154+
155+
if (uri.ToString().Contains(SessionTokenUriVar))
156+
throw new Exception(
157+
$"URI contains {SessionTokenUriVar} variable but could not be replaced (http and https URLs cannot contain {SessionTokenUriVar})");
158+
149159
_ = Launcher.LaunchUriAsync(uri);
150160
}
151161
catch (Exception e)

App/ViewModels/AgentViewModel.cs

+72-17
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace Coder.Desktop.App.ViewModels;
2222

2323
public interface IAgentViewModelFactory
2424
{
25-
public AgentViewModel Create(Uuid id, string hostname, string hostnameSuffix,
25+
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix,
2626
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName);
2727
}
2828

@@ -32,12 +32,12 @@ public class AgentViewModelFactory(
3232
ICredentialManager credentialManager,
3333
IAgentAppViewModelFactory agentAppViewModelFactory) : IAgentViewModelFactory
3434
{
35-
public AgentViewModel Create(Uuid id, string hostname, string hostnameSuffix,
35+
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix,
3636
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName)
3737
{
38-
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory)
38+
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory,
39+
expanderHost, id)
3940
{
40-
Id = id,
4141
Hostname = hostname,
4242
HostnameSuffix = hostnameSuffix,
4343
ConnectionStatus = connectionStatus,
@@ -74,12 +74,14 @@ public partial class AgentViewModel : ObservableObject, IModelUpdateable<AgentVi
7474
private readonly DispatcherQueue _dispatcherQueue =
7575
DispatcherQueue.GetForCurrentThread();
7676

77+
private readonly IAgentExpanderHost _expanderHost;
78+
7779
// This isn't an ObservableProperty because the property itself never
7880
// changes. We add an event listener for the collection changing in the
7981
// constructor.
8082
public readonly ObservableCollection<AgentAppViewModel> Apps = [];
8183

82-
public required Uuid Id { get; init; }
84+
public readonly Uuid Id;
8385

8486
[ObservableProperty]
8587
[NotifyPropertyChangedFor(nameof(FullHostname))]
@@ -160,12 +162,28 @@ public string DashboardUrl
160162
}
161163

162164
public AgentViewModel(ILogger<AgentViewModel> logger, ICoderApiClientFactory coderApiClientFactory,
163-
ICredentialManager credentialManager, IAgentAppViewModelFactory agentAppViewModelFactory)
165+
ICredentialManager credentialManager, IAgentAppViewModelFactory agentAppViewModelFactory,
166+
IAgentExpanderHost expanderHost, Uuid id)
164167
{
165168
_logger = logger;
166169
_coderApiClientFactory = coderApiClientFactory;
167170
_credentialManager = credentialManager;
168171
_agentAppViewModelFactory = agentAppViewModelFactory;
172+
_expanderHost = expanderHost;
173+
174+
Id = id;
175+
176+
PropertyChanged += (_, args) =>
177+
{
178+
if (args.PropertyName == nameof(IsExpanded))
179+
{
180+
_expanderHost.HandleAgentExpanded(Id, IsExpanded);
181+
182+
// Every time the drawer is expanded, re-fetch all apps.
183+
if (IsExpanded && !FetchingApps)
184+
FetchApps();
185+
}
186+
};
169187

170188
// Since the property value itself never changes, we add event
171189
// listeners for the underlying collection changing instead.
@@ -202,18 +220,15 @@ public bool TryApplyChanges(AgentViewModel model)
202220
[RelayCommand]
203221
private void ToggleExpanded()
204222
{
205-
// TODO: this should bubble to every other agent in the list so only
206-
// one can be active at a time.
207223
SetExpanded(!IsExpanded);
208224
}
209225

210226
public void SetExpanded(bool expanded)
211227
{
228+
if (IsExpanded == expanded) return;
229+
// This will bubble up to the TrayWindowViewModel because of the
230+
// PropertyChanged handler.
212231
IsExpanded = expanded;
213-
214-
// Every time the drawer is expanded, re-fetch all apps.
215-
if (expanded && !FetchingApps)
216-
FetchApps();
217232
}
218233

219234
partial void OnConnectionStatusChanged(AgentConnectionStatus oldValue, AgentConnectionStatus newValue)
@@ -226,7 +241,26 @@ private void FetchApps()
226241
if (FetchingApps) return;
227242
FetchingApps = true;
228243

229-
var client = _coderApiClientFactory.Create(_credentialManager);
244+
// If the workspace is off, then there's no agent and there's no apps.
245+
if (ConnectionStatus == AgentConnectionStatus.Gray)
246+
{
247+
FetchingApps = false;
248+
Apps.Clear();
249+
return;
250+
}
251+
252+
// API client creation could fail, which would leave FetchingApps true.
253+
ICoderApiClient client;
254+
try
255+
{
256+
client = _coderApiClientFactory.Create(_credentialManager);
257+
}
258+
catch
259+
{
260+
FetchingApps = false;
261+
throw;
262+
}
263+
230264
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
231265
client.GetWorkspaceAgent(Id.ToString(), cts.Token).ContinueWith(t =>
232266
{
@@ -265,18 +299,24 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
265299
continue;
266300
}
267301

268-
if (string.IsNullOrEmpty(app.Url))
302+
if (!Uri.TryCreate(app.Url, UriKind.Absolute, out var appUri))
269303
{
270-
_logger.LogWarning("App URI '{Url}' for '{DisplayName}' is empty, app will not appear in list", app.Url,
304+
_logger.LogWarning("Could not parse app URI '{Url}' for '{DisplayName}', app will not appear in list",
305+
app.Url,
271306
app.DisplayName);
272307
continue;
273308
}
274309

310+
// HTTP or HTTPS external apps are usually things like
311+
// wikis/documentation, which clutters up the app.
312+
if (appUri.Scheme is "http" or "https")
313+
continue;
314+
275315
// Icon parse failures are not fatal, we will just use the fallback
276316
// icon.
277317
_ = Uri.TryCreate(DashboardBaseUrl, app.Icon, out var iconUrl);
278318

279-
apps.Add(_agentAppViewModelFactory.Create(uuid, app.DisplayName, app.Url, iconUrl));
319+
apps.Add(_agentAppViewModelFactory.Create(uuid, app.DisplayName, appUri, iconUrl));
280320
}
281321

282322
foreach (var displayApp in workspaceAgent.DisplayApps)
@@ -296,7 +336,22 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
296336
scheme = "vscode-insiders";
297337
}
298338

299-
var appUri = $"{scheme}://vscode-remote/ssh-remote+{FullHostname}/{workspaceAgent.ExpandedDirectory}";
339+
Uri appUri;
340+
try
341+
{
342+
appUri = new UriBuilder
343+
{
344+
Scheme = scheme,
345+
Host = "vscode-remote",
346+
Path = $"/ssh-remote+{FullHostname}/{workspaceAgent.ExpandedDirectory}",
347+
}.Uri;
348+
}
349+
catch (Exception e)
350+
{
351+
_logger.LogWarning("Could not craft app URI for display app {displayApp}, app will not appear in list",
352+
displayApp);
353+
continue;
354+
}
300355

301356
// Icon parse failures are not fatal, we will just use the fallback
302357
// icon.

0 commit comments

Comments
 (0)