From a4a2ad5b31e22efccdf12b34c2f3421c054cc928 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Thu, 9 May 2024 23:29:59 +0000 Subject: [PATCH 1/3] Merged PR 10740369: QuickStartPlayground - Stage for GitHub Related work items: #46195449, #48702845, #48702968, #49293419, #49717358, #49721021, #49721031, #49915277, #49917614 --- .editorconfig | 6 + common/Contracts/IQuickstartSetupService.cs | 13 + common/DevHome.Common.csproj | 2 +- common/Models/ExperimentalFeature.cs | 159 ++--- common/Services/AppInstallManagerService.cs | 18 +- common/Services/IStringResource.cs | 2 + common/Services/QuickstartSetupService.cs | 51 ++ common/Services/StringResource.cs | 27 + .../QuickstartPlayground/FeedbackSubmitted.cs | 24 + .../GenerateButtonClicked.cs | 22 + .../ProjectGenerationErrorInfo.cs | 26 + .../CoreWidgetProvider.csproj | 2 +- .../HyperVExtension/HyperVExtension.csproj | 2 +- .../Strings/en-us/Resources.resw | 12 +- src/App.xaml.cs | 1 + src/Models/ExtensionWrapper.cs | 1 + src/NavConfig.jsonc | 21 + src/Services/PageService.cs | 3 +- .../Assets/Setup_QuickstartPlayground.png | Bin 0 -> 13638 bytes .../DevHome.SetupFlow.csproj | 11 + .../Extensions/ServiceExtensions.cs | 13 + .../SetupFlow/DevHome.SetupFlow/Models/Doc.cs | 48 ++ .../Models/QuickStartProjectProvider.cs | 84 +++ .../DevHome.SetupFlow/NativeMethods.txt | 4 +- .../Selectors/SetupFlowViewSelector.cs | 21 +- .../Services/IQuickStartProjectService.cs | 13 + .../Services/QuickStartProjectService.cs | 53 ++ .../Services/StringResourceKey.cs | 3 + .../Strings/en-us/Resources.resw | 108 ++++ .../Styles/QuickstartStyles.xaml | 13 + .../DeveloperQuickstartTaskGroup.cs | 35 + .../Utilities/EmbeddingsCalc.cs | 87 +++ .../ViewModels/MainPageViewModel.cs | 59 +- .../QuickstartPlaygroundViewModel.cs | 604 ++++++++++++++++++ .../ViewModels/SearchViewModel.cs | 224 +++---- .../DevHome.SetupFlow/Views/MainPageView.xaml | 19 + .../Views/QuickstartPlaygroundView.xaml | 379 +++++++++++ .../Views/QuickstartPlaygroundView.xaml.cs | 229 +++++++ .../Views/SetupFlowPage.xaml | 5 + 39 files changed, 2200 insertions(+), 204 deletions(-) create mode 100644 common/Contracts/IQuickstartSetupService.cs create mode 100644 common/Services/QuickstartSetupService.cs create mode 100644 common/TelemetryEvents/SetupFlow/QuickstartPlayground/FeedbackSubmitted.cs create mode 100644 common/TelemetryEvents/SetupFlow/QuickstartPlayground/GenerateButtonClicked.cs create mode 100644 common/TelemetryEvents/SetupFlow/QuickstartPlayground/ProjectGenerationErrorInfo.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Assets/Setup_QuickstartPlayground.png create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/Doc.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/QuickStartProjectProvider.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Services/IQuickStartProjectService.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Services/QuickStartProjectService.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Styles/QuickstartStyles.xaml create mode 100644 tools/SetupFlow/DevHome.SetupFlow/TaskGroups/DeveloperQuickstartTaskGroup.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs diff --git a/.editorconfig b/.editorconfig index 2b1a719c0..ecfa25120 100644 --- a/.editorconfig +++ b/.editorconfig @@ -206,3 +206,9 @@ dotnet_style_prefer_simplified_interpolation = true:suggestion # Spelling spelling_exclusion_path = .\exclusion.dic + +# Diagnostic configuration + +# CS8305: Type is for evaluation purposes only and is subject to change or removal in future updates. +dotnet_diagnostic.CS8305.severity = suggestion + diff --git a/common/Contracts/IQuickstartSetupService.cs b/common/Contracts/IQuickstartSetupService.cs new file mode 100644 index 000000000..9b4ccecaf --- /dev/null +++ b/common/Contracts/IQuickstartSetupService.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace DevHome.Common.Contracts; + +public interface IQuickstartSetupService +{ + public bool IsDevHomeAzureExtensionInstalled(); + + public Task InstallDevHomeAzureExtensionAsync(); +} diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index aa10f8a43..c22fee997 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -42,7 +42,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/common/Models/ExperimentalFeature.cs b/common/Models/ExperimentalFeature.cs index bc07cfc84..635fddd5d 100644 --- a/common/Models/ExperimentalFeature.cs +++ b/common/Models/ExperimentalFeature.cs @@ -1,75 +1,88 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using DevHome.Common.Contracts; -using DevHome.Common.Services; -using DevHome.Common.TelemetryEvents; -using DevHome.Telemetry; - -namespace DevHome.Common.Models; - -public partial class ExperimentalFeature : ObservableObject -{ - private readonly bool _isEnabledByDefault; - - [ObservableProperty] - private bool _isEnabled; - - public string Id { get; init; } - - public bool IsVisible { get; init; } - - public static ILocalSettingsService? LocalSettingsService { get; set; } - - public ExperimentalFeature(string id, bool enabledByDefault, bool visible = true) - { - Id = id; - _isEnabledByDefault = enabledByDefault; - IsVisible = visible; - - IsEnabled = CalculateEnabled(); - } - - public bool CalculateEnabled() - { - if (LocalSettingsService!.HasSettingAsync($"ExperimentalFeature_{Id}").Result) - { - return LocalSettingsService.ReadSettingAsync($"ExperimentalFeature_{Id}").Result; - } - - return _isEnabledByDefault; - } - - public string Name - { - get - { - var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources"); - return stringResource.GetLocalized(Id + "_Name"); - } - } - - public string Description - { - get - { - var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources"); - return stringResource.GetLocalized(Id + "_Description"); - } - } - - [RelayCommand] - public async Task OnToggledAsync() - { - IsEnabled = !IsEnabled; - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Contracts; +using DevHome.Common.Services; +using DevHome.Common.TelemetryEvents; +using DevHome.Telemetry; + +namespace DevHome.Common.Models; + +public partial class ExperimentalFeature : ObservableObject +{ + private readonly bool _isEnabledByDefault; + + [ObservableProperty] + private bool _isEnabled; + + public string Id { get; init; } + + public bool IsVisible { get; init; } + + public static ILocalSettingsService? LocalSettingsService { get; set; } + + public static IQuickstartSetupService? QuickstartSetupService { get; set; } + + public ExperimentalFeature(string id, bool enabledByDefault, bool visible = true) + { + Id = id; + _isEnabledByDefault = enabledByDefault; + IsVisible = visible; + + IsEnabled = CalculateEnabled(); + } + + public bool CalculateEnabled() + { + if (LocalSettingsService!.HasSettingAsync($"ExperimentalFeature_{Id}").Result) + { + return LocalSettingsService.ReadSettingAsync($"ExperimentalFeature_{Id}").Result; + } + + return _isEnabledByDefault; + } + + public string Name + { + get + { + var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources"); + return stringResource.GetLocalized(Id + "_Name"); + } + } + + public string Description + { + get + { + var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources"); + return stringResource.GetLocalized(Id + "_Description"); + } + } + + [RelayCommand] + public async Task OnToggledAsync() + { + IsEnabled = !IsEnabled; + await LocalSettingsService!.SaveSettingAsync($"ExperimentalFeature_{Id}", IsEnabled); - await LocalSettingsService!.SaveSettingAsync($"IsSeeker", true); - - TelemetryFactory.Get().Log("ExperimentalFeature_Toggled_Event", LogLevel.Critical, new ExperimentalFeatureEvent(Id, IsEnabled)); - } -} + await LocalSettingsService!.SaveSettingAsync($"IsSeeker", true); + + TelemetryFactory.Get().Log("ExperimentalFeature_Toggled_Event", LogLevel.Critical, new ExperimentalFeatureEvent(Id, IsEnabled)); + + // To simplify setup for the Quickstart experimental feature, install the associated Dev Home Azure Extension if it's not already present + // when that feature is enabled. Those operations will only occur on Canary and Stable builds of Dev Home. + if (string.Equals(Id, "QuickstartPlayground", StringComparison.Ordinal) && IsEnabled) + { + if (!QuickstartSetupService!.IsDevHomeAzureExtensionInstalled()) + { + await QuickstartSetupService!.InstallDevHomeAzureExtensionAsync(); + } + } + } +} diff --git a/common/Services/AppInstallManagerService.cs b/common/Services/AppInstallManagerService.cs index 4e8e017ef..419217ebc 100644 --- a/common/Services/AppInstallManagerService.cs +++ b/common/Services/AppInstallManagerService.cs @@ -123,7 +123,14 @@ await Task.Run(() => installItem.Completed += (sender, args) => { - tcs.SetResult(true); + if (!tcs.TrySetResult(true)) + { + _log.Information("WidgetHostingService", $"{packageId} In Completed handler, RanToCompleted already set."); + } + else + { + _log.Information("WidgetHostingService", $"{packageId} In Completed handler, RanToCompleted set."); + } }; installItem.StatusChanged += (sender, args) => @@ -135,7 +142,14 @@ await Task.Run(() => } else if (installItem.GetCurrentStatus().InstallState == AppInstallState.Completed) { - tcs.SetResult(true); + if (!tcs.TrySetResult(true)) + { + _log.Information("WidgetHostingService", $"{packageId} In StatusChanged handler, RanToCompleted already set."); + } + else + { + _log.Information("WidgetHostingService", $"{packageId} In StatusChanged handler, RanToCompleted set."); + } } }; return tcs.Task; diff --git a/common/Services/IStringResource.cs b/common/Services/IStringResource.cs index e97b52cc1..9d0709f4c 100644 --- a/common/Services/IStringResource.cs +++ b/common/Services/IStringResource.cs @@ -6,4 +6,6 @@ namespace DevHome.Common.Services; public interface IStringResource { public string GetLocalized(string key, params object[] args); + + public string GetResourceFromPackage(string resource, string packageFullName); } diff --git a/common/Services/QuickstartSetupService.cs b/common/Services/QuickstartSetupService.cs new file mode 100644 index 000000000..2325955e4 --- /dev/null +++ b/common/Services/QuickstartSetupService.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using DevHome.Common.Contracts; +using DevHome.Common.Services; +using Serilog; + +namespace DevHome.Services; + +public class QuickstartSetupService(IAppInstallManagerService appInstallManagerService, IPackageDeploymentService packageDeploymentService) : IQuickstartSetupService +{ +#if CANARY_BUILD + private const string AzureExtensionStorePackageId = "9NBVFRMSFXHW"; + private const string AzureExtensionPackageFamilyName = "Microsoft.Windows.DevHomeAzureExtension.Canary_8wekyb3d8bbwe"; +#elif STABLE_BUILD + private const string AzureExtensionStorePackageId = "9MV8F79FGXTR"; + private const string AzureExtensionPackageFamilyName = "Microsoft.Windows.DevHomeAzureExtension_8wekyb3d8bbwe"; +#else + private const string AzureExtensionStorePackageId = ""; + private const string AzureExtensionPackageFamilyName = ""; +#endif + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickstartSetupService)); + + private readonly IAppInstallManagerService _appInstallManagerService = appInstallManagerService; + private readonly IPackageDeploymentService _packageDeploymentService = packageDeploymentService; + + public bool IsDevHomeAzureExtensionInstalled() + { +#if CANARY_BUILD || STABLE_BUILD + var packages = _packageDeploymentService.FindPackagesForCurrentUser(AzureExtensionPackageFamilyName); + return packages.Any(); +#endif + return true; + } + + public async Task InstallDevHomeAzureExtensionAsync() + { + try + { + _log.Information("Installing DevHomeAzureExtension"); + await _appInstallManagerService.TryInstallPackageAsync(AzureExtensionStorePackageId); + } + catch (Exception ex) + { + _log.Error(ex, "Installing DevHomeAzureExtension failed"); + } + } +} diff --git a/common/Services/StringResource.cs b/common/Services/StringResource.cs index 1be13d023..068f7b5f7 100644 --- a/common/Services/StringResource.cs +++ b/common/Services/StringResource.cs @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Globalization; using Microsoft.Windows.ApplicationModel.Resources; +using Windows.Win32; +using Windows.Win32.Foundation; namespace DevHome.Common.Services; public class StringResource : IStringResource { + private const int MaxBufferLength = 1024; + private readonly ResourceLoader _resourceLoader; /// @@ -52,4 +57,26 @@ public string GetLocalized(string key, params object[] args) return string.IsNullOrEmpty(value) ? key : value; } + + /// + /// Gets the string of a ms-resource for a given package. + /// + /// the ms-resource:// path to a resource in an app package's pri file. + /// the package containing the resource. + /// The retrieved string represented by the resource key. + public unsafe string GetResourceFromPackage(string resource, string packageFullName) + { + var indirectPathToResource = "@{" + packageFullName + "?" + resource + "}"; + Span outputBuffer = new char[MaxBufferLength]; + + fixed (char* outBufferPointer = outputBuffer) + { + fixed (char* resourcePathPointer = indirectPathToResource) + { + var res = PInvoke.SHLoadIndirectString(resourcePathPointer, new PWSTR(outBufferPointer), (uint)outputBuffer.Length); + res.ThrowOnFailure(); + return new string(outputBuffer.TrimEnd('\0')); + } + } + } } diff --git a/common/TelemetryEvents/SetupFlow/QuickstartPlayground/FeedbackSubmitted.cs b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/FeedbackSubmitted.cs new file mode 100644 index 000000000..00fa55dbd --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/FeedbackSubmitted.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground; + +[EventData] +public class FeedbackSubmitted(bool isPositive, string message) : EventBase +{ + public bool IsPositive { get; } = isPositive; + + public string Message { get; } = message; + + public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } +} diff --git a/common/TelemetryEvents/SetupFlow/QuickstartPlayground/GenerateButtonClicked.cs b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/GenerateButtonClicked.cs new file mode 100644 index 000000000..d8d27a707 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/GenerateButtonClicked.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground; + +[EventData] +public class GenerateButtonClicked(string prompt) : EventBase +{ + public string Prompt { get; } = prompt; + + public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } +} diff --git a/common/TelemetryEvents/SetupFlow/QuickstartPlayground/ProjectGenerationErrorInfo.cs b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/ProjectGenerationErrorInfo.cs new file mode 100644 index 000000000..1790475e0 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/QuickstartPlayground/ProjectGenerationErrorInfo.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground; + +[EventData] +public class ProjectGenerationErrorInfo(string errorMessage, Exception extendedError, string diagnosticText) : EventBase +{ + public string ErrorMessage { get; } = errorMessage; + + public string ExtendedError { get; } = extendedError.ToString(); + + public string DiagnosticText { get; } = diagnosticText; + + public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServicePerformance; + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } +} diff --git a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj index 603330e69..7eb8890a6 100644 --- a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj +++ b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj @@ -22,7 +22,7 @@ all - + diff --git a/extensions/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj b/extensions/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj index e8974a6f6..d6ec3808d 100644 --- a/extensions/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj +++ b/extensions/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj @@ -40,7 +40,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index 9b6d6f880..2a2852575 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -547,10 +547,18 @@ Quiet background processes - Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. + Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. Quiet background processes allows you to free up resources while developing - Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. + Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. + + + Quickstart Playground + Locked="{Quickstart Playground}" Title text for the Quickstart Playground feature. + + + Get started in a new devcontainer-based project from a natural language prompt. Enabling this feature installs the Dev Home Azure Extension from the Store. + Text within a display card that explains what users can do with the Quickstart Playground feature. Users can choose to toggle this feature on or off. diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 809f96586..bcaf2fa4a 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -127,6 +127,7 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); // Core Services diff --git a/src/Models/ExtensionWrapper.cs b/src/Models/ExtensionWrapper.cs index 7f5743aff..1cf86570e 100644 --- a/src/Models/ExtensionWrapper.cs +++ b/src/Models/ExtensionWrapper.cs @@ -26,6 +26,7 @@ public class ExtensionWrapper : IExtensionWrapper [typeof(ISettingsProvider)] = ProviderType.Settings, [typeof(IFeaturedApplicationsProvider)] = ProviderType.FeaturedApplications, [typeof(IComputeSystemProvider)] = ProviderType.ComputeSystem, + [typeof(IQuickStartProjectProvider)] = ProviderType.QuickStartProject, }; private IExtension? _extensionObject; diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index 7a8dc0319..e3524698e 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -65,6 +65,27 @@ "visible": true } ] + }, + { + "identity": "QuickstartPlayground", + "enabledByDefault": false, + "buildTypeOverrides": [ + { + "buildType": "dev", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "canary", + "enabledByDefault": false, + "visible": false + }, + { + "buildType": "release", + "enabledByDefault": false, + "visible": false + } + ] } ] } diff --git a/src/Services/PageService.cs b/src/Services/PageService.cs index 0b11e2b5a..00c704e4b 100644 --- a/src/Services/PageService.cs +++ b/src/Services/PageService.cs @@ -26,7 +26,7 @@ public class PageService : IPageService private readonly Dictionary _pages = new(); - public PageService(ILocalSettingsService localSettingsService, IExperimentationService experimentationService) + public PageService(ILocalSettingsService localSettingsService, IExperimentationService experimentationService, IQuickstartSetupService quickstartSetupService) { // Configure top-level pages from registered tools var assemblies = AppDomain.CurrentDomain.GetAssemblies(); @@ -50,6 +50,7 @@ where assembly.GetName().Name == tool.Assembly // Configure Experimental Feature pages ExperimentalFeature.LocalSettingsService = localSettingsService; + ExperimentalFeature.QuickstartSetupService = quickstartSetupService; foreach (var experimentalFeature in App.NavConfig.ExperimentFeatures ?? Array.Empty()) { var enabledByDefault = experimentalFeature.EnabledByDefault; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/Setup_QuickstartPlayground.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/Setup_QuickstartPlayground.png new file mode 100644 index 0000000000000000000000000000000000000000..5207081d7852c81d476c5e355e44d7364d8c6325 GIT binary patch literal 13638 zcmZvDV{j!*u=a^hPHbzlyRoy$#;`e^?OCIv=Duxr={?2sB$?v$ zz}Uo;h5!pmg&{32E@W<)D=kHFh&96cF+8ScGtbI4+CU}2l~OXFVv$)(r6j0doMM(C z0V(5VTzttoUpvQ^p7P{Z8y!fuT1PitS44BlJ7slUZRNfFy7aolJFG9&+#6EQs&%~K z{iy57`)!9uD6lRVYORG2?SDY)7KO5CBZJW?}GUbN*R+4 zjdV$aOCB-N5W;slKl^kr*k0*8lgY^k127{>%8Hx_d;R}C;hL@meB3~eV90G{+6f%ovKVo zf(Bw}I$v1Spnmr9W<0?=kN#VjJmUX#3m-s|Ga|4ehU8Ln!)ikZoRi@uFhD=l0Q!Cu z=VH&a{W68*4HNL|f&H}K3ba>2NrvkO;vnz4Xa5&GaDabJ7w4B7PKrwUy9EF}4NN{~ z!R6-SzFz#_rKNR9wlF!h`SD*s!wUZ9Ci%VHQ1!232a;xC+VlTSP`EA^p?M=4fE)H1 zxdvxVe$$s(`KsbfL)H_ZQjPtqwpR3^(ha8Ejvs`Qet{PvK4sm1!v#J!5jB+5CSRpFe_er zyU<_KphP_ATha&oirJIQW>Azo%KLFopuvvDZr3kvlzB+tG@1AP!HFsfowiknqo<`b;zo_iWLzgx;F-(SM5iyt4r*dGntxd*+df@7<^ zH%I<%NFboG+MGLAuY3P&;NFnl*L#2SzI)1lG1#X@u`zc4*m{YOMoaftYXpp;6`SJ) z_^oM~zhPp|1_I#HV2Lq5OFf(4he&cdu%-(d((>+g6!ims`t__sCo(p0JL7tyO8*?- zH~&UpKP<*bNg!YDXI{WQ`)qU;3s0TlCO<>;F?-TPe(n9j*)?nd5o9LrN?fuS?cr5| zg57C`FN#wF0WBUHK+Fgu-I=947sIHkgCfGv{sRa{k8WeZIM0G``*wX1{ce^9n0zO7N?(V0@25T}Zc>wc^!;pjFH}AuTI|tsK7j&;##*geSYW%w9lWM zN8p31F1SIsFTTJ%I1q5-<>AQFtApL?GrFr%Equ>7K~)SrzHexr4K4~F2_MX!OASav z03cu*AWRiNliKex}90txYI{Y;e20`KpD;m|vp!H8cQ=e)ekwO%df4d>_!C>@DC;N+m1py0K zN0;SYZMu1cei3Y(9}V9+x~X69JZmEFZebxFe&Rv3+Ea>JYzf@zJ*x2GwuqV zLgCCBMjmnM!gWu9`AtP2Y@~4M|4x9F#itgW<^_$487L14+w`N)k9`DSu;x_Ppf*h+{V#fn&+2h3j!T@vk|w zNmoeYY$p#fvb>w#aPV^0j<$W!vx7H*SE^aD`IaCWW8&Pjf8T!Y7kD#46uasbF?Icy z0mN~fVJ4Z)&tx&I-rd%FwA~T14Ff8<#P^@>P7cK7_tB)IQjHvZLt~RDq^Vfr{y9&`kVO=LH*?h{>;Srv)t{w z9JAs+0fc~_-#WO8C?ojU}<9lgQ0f|V$n`R+`pIdyS+czPE1UnOd`5O86 z{+f`oqI+u!KO+W!_x+TI#TNqw6XAB`Q5%h4<438k7r=J=im?_`2CH}Hwe+m3+jmSK zNRvsUN>nSlWRzxhFM=(;oU#3P&PN$WyUU_~ZYqh7NJ?xtkc$^U=GVg_T;CWzyI;b@ z>x!)<$8#t^+_%7Gw)fB>Q*)Ggo`TAa_kz|K|Ap>HI^JA(q``;;#W z$7&tn$V8FWJ*;or9(QPsj; zwMVoUc`VcCcI+E0Pt53IyF&M?<-MK*@S&?S*JB4ouemH*|3qMQ44El)C`T&9;QK7D z`+|n_0B3i)bjuwc^uGQx5)W|5OFEl3ax}d@Et@xVji9lS;mx5F?%^N$velUY>z^&r zT$`LtOs-^)t?4cZ?p;A^NM1pH5gHt>$mr5i{DJ9Tisl}_!N@q9Oyn;aIIFxR6DU)F zs&#ONSUTUKV^ATwdOcTYg@-n9wN^&HS*mLes^KAmov;M=-mAo|z`Dv79C_N%@DH_OhwPlG|oSNw<{Hp&D}hJMx&R#XwFT!!Bo zEyP3&r@O2)dF%NPo!C!1vBk79_9xv#F=Uv2G#_{0O?pVwuIXDIk=e*-N z6`VWnT?>Iv!*i*(EYG#Sd#7%T5@s&N;MLk5$t%d<^fT<4`)xy^tAfn*x<_UGcXKGa zxdYpFaI}AM3&|0!9oE3ji7?FppDv2tfDx2A8^+~(#+B1+|4A6K?S|S}9C~;dNDQQT zfA0Y}*C_P00bQ)DzO7G~GGU}W*e2)UPjNkKZ5R2W`{5}FF-wkk|@==Q*^+8ip z*MSlk3HA5uJ$0A6Qq4eA;Utc+7P@sl3qJ@EAlxsnE!>2_jk-XOo8^wa4@cbZ&|H`TZ7O6r_`=%3={5G7MILZ%L2|wRWXFT({F@AuN>~ig4|>V z3oBA^(RA%`lBi zdCqjV1V!`t_PEOa)g993C&UtV!qL&b!p=dnh9V{i+Y*pRAi!b#T#`WXQ>j2y#k1bU zn*eUH$tU;hL?u*1(86C>r~p&Z>@p>iEffGnCM3kF90kXXC2>UzqvdGY%xxY%`S*lH z&dArCckS~I^2}^1*Cm8e{=L1`X;g{k&-yia1;=gofLeN;BvO|S#t;a85Kw*=+bZs;(j4<9S|d&U zvPIPKmevc3>q8;ZPFwCRgD569PJz;r>!<$~AT(gqAI0zGURtNP^;*Nw@wMYA-tL$p zzpF5k))b;ERP5M1yl{W+te7Y}GpVmdR(`z!|B`%Qpf+?El7ZofCY2X^jG&ETFoJIV zca2Kq~Asyuc`LZj~4&m3gyftF?v zu3GaDov16)a1y*Kbm8BF8xfCG%;Yhm&|mxI8E`tvk`4d(I;9s*idzFpfMHajP87~_ z(_}R81-U;wP6qqJ#%GZA)Uu`~i$_=^)+x(UIt{W;VpWF%+jfgtwXoWi3~8+%;X zr(wym6ea1WfW%LBNmqBTg&MCQ#VIaYqwbVXM+-ngYL-y@{Uhq0of2wFguL#0A1P7k z*qxVb9fYZ3`42k?P5qZNG~)cc+3L>|8X&a-+|;OX!%f8!bJ{6fWdT!PK0y?bRI;fa zR_w4BSS{;PTTIVv#~dPys=>#mu@`at`@_x+;#>qYY2$^8NiI~yK)z^35d2!9VH>|( zf}L{kmK8W{FV2Z5wKY~Q+YF~}ZnXH;qK-S;Se+w^gmp`FWerk?@WnIrqY<69%>86N z`Qj(fUya!FebBFX+$qTskMg1LrR#3JnPq_a+;U5$oaOfDRE5L63R=HFvMJ}+Hx9WN#D+YiEd0N;{l90=6W7i^?(F{{ z1yltT<4Povnc&H0bGyex3q2yMcP@yYI9LOwut+J#Qk82Jdb!H+8+H|NA)0Q-4?vce zJ#dyMhlcGhFTw)mNS%#Gx-g--Qn&RtA|r#FmgcG=PP}xLE|FyJQa(!#sddIF;?@Vo zcafK3O9k<`%OnHp-UMd2!DPo^HH6E-KT#M!)FdQR1T?%jZG(fOzywl8gr+-EZwoLr zX9P3E*~m^usczd(38T+V=N@AqDesK)LVpFdYQXS%Po^seHEoZm7S%i zLz-B987J*L5R*FPtTJ?pd?L{G4-t8+7Hi!je{sso?8L)eA1nkgIf`V&&0pMC3OtLD zthbb<$Z@Pwi$N8dCUMF{P7f9{K5J@9Q*7L2`&`b*j1-VgGSG3F(g3V1tg%d;xvEKa zmPJDb)!)7-Ub_UaHg#x6S9Cjy4Xy|(8M#UJkVcYI5Hjd9kPV;&X#RH0wh5FJ!!U>v z@a}@4AA40IblUeg{q;Gna@|49cT2Jzx_;%o_ssWpMtar)F@nGqss{F0{E7N6~> zj`?o-^9%%oC$tt5f6|laAx4M%*=R*}M{TLmzN+tR#MuFn8ediyUvQ7lBa4<_RWsmN zkL#S%Wt>s+z1}gp^$IO*Smgv(rQ)$RY0|xILR0mh`2_=ziDdgjPMPCzO*^&ii*Cd>u)!u2wLHDI z_kr>Ux^6hcFsC4aGtOjKp!6+{gIXY%$qTV+J-A4y{Ur-kMrm+U?}Mr6NqcIlsxx1~ zsNf`j;((*)2esT>$}3qx(Fs2Vrx!0Sv>r=*9vXCSURQ=j> z5O=vw%6L`xvkSb`mRIj%_sdy}F(2(=>Y=9%!Gdrj=b)%O%CJ2=Se-2Xr#&g0rT)ec zG;D;k_?*~iO5CY(ayViJK^{>OO_eUN=r~^AjR`lEIw2eX9uOi{&aMO5zzzK8ktovyL9-(5!cXEFR z3Fb9L*bJ0uj0vNMIX7%*@!Uq*AE5zD#W85dfOUf<^Y4_#MesDH!B~jgxGAZsJd}^O zPmG%~I>W{k&H?;%hqriGwC%Q{H0Wz)hl}mCPWCwEkd|f8kZ`Nd>w%XEi$W@GQLX-U zj}g6wb@x?gg4GOMoUw38`zT<|V5dQ33D~kfKd9JKbaTFsiNG`8Appn)l7{&~J5(o% zI(3a(_&e%axncRD1qdyM7HoWGToP4Db#MA8R;nKNA2t5*ui$tN%*7o~Tg_7W>x`ZJp3q`0U3xg)x&bYU(YJlud>n4O!h-!s1BC zPRA^i-ln;$@q*N2KT1kRn(0t4IG6MF@!m8RIJT0IX6h0PgBq=SE@QbdT4S07uHKzu z;a5!;t;WrG@cOdZFZ;7eIB*4a&VOO?L#ysQ;Q#41Q)AeCm*aEvLYih$sWQ?xsV37i zHnTdmax`Pk%3HY3P;ku-n9P-@Dc+lb6=ws@q*J0+PoE8zpBGch>c!(|P`vtSaAd2eUry9OuQtsW`&P zjgDH&y!2qyTCKDH`A`|U-|Q{t-XflxaW&PtbhRk+kWR1St~M^)wcdQc)aUF&u0YO{ zCnA3uGx-Qvq5(B5z7-Pa0v}B!oSr_?tjMQ4bDC@F&*(t6Q$8Q8jV^zZ_8Wl$_~&^z zx>eB~q?8Vi9jbeSLo682?=58=FRpG`J)2^@zGsZtA`W7{FT@~Eq}ZVzdup1z{x#2K zgYX6WTF0YFoEj`AB6ym8z|dh}(I+k5VLkbe1H(et1`u8JD$bQ2G39w3)JDg@p>F3s zBz-5&Nk;he_rJW;BBBM_kko*<^LQ0M02^)P`qTVd?UM|gJPGcA4BtNQj1dLRB00g0 z-!ZFfQEYovOUp5ObtmcSV&m~i5W~EU$|9j>F`dt5*4GUIW>XG{Y>y|obl(<`2!{g3 zcZ60dP|Cah0O4z>yNiVHUy8~F$I9?)I+^T-Cz|i$uvx8piu~^rMx-;zNIoI*uVj38 zZ4mf8MA4n76+LJOsMK4EykLz;-|fJjQr!(1 zmgz?g#NR^VErmu;MrDUMB+||&n7rtjnEBTM%oZJlqq%~JfnUVr2`v%fE1Nco9 zN6R7iuB`P6W)H|up7;pNn8;_{)qm#X5vi$^JG*s;rUr8jc&Bz-XqD(wNcmlhWe;e{6$JiU!R8UZQ|TImd2Xgu3d@nFOK*w^^Cz$Gr0ewkAMv zsXdHU4wV zW2bc8wSN?zcthN?C*j==<+nYqALJjIA7trB_=OLtVO;e$vNjzo*%ypwZjZ{W^lq+f zn^k7GKj$}A>bp9M6+P}|7}LkfU2Cr8`6T7f+E+yB19ywwZay>p<$MqRSXVx|x2YYieCCW!Ii^l&^7TPsl|H*ig z)l0Y0Vn_6mdO7APFy4a}2=RKB!+%1^=n(#DAkYJwFO|j6-Pi%rQqw#Y*34Gf47 zOzhDGh%4VyL1wf(0dIR_y154?vl9zmk8SqB8uZgsw6~9vC2E9WBF3A1!~JUjn95WU z%5=3v;A)5&?r>$1%gY=I(o9$4Kzi(hN9k-&NiE~rp!n+L!Tk9<$mSuSc7&nCoJHna zb9X#3cxr+CT*mht)ba7baQ7&hgQ7M$dyI*g4ckA98q4Q}oMZZHR<4K4$iLtDN+vps zVx-_)y#{V zGbYTkGmS7PwdZZ|B<06`H8s>%6~&x184&z+Ia&86NHiUP+HS%*ve9qu-&ynieRHH{ zSmXOk7#;@EKo)9|miFSk0eQ6UxOwuW3s=sKB;=HFB4zDJLvi=Irn1}aGqwSWkjLYo z($UqyHRt`uHE+|bchn!~Y=)eUMn!z(YO(jbBc+Ecr1wod&mUEtSAf90s{Wlw8mA^n zOUQom^XI_QpVV2{rh7ZWDDBITgI42=3L2(b zU4+VWO^N0f`@J&>sarLlc|=V8O`nc)&r|oq*~@y-G`?rQO4X`%*hgVa`JsivD9^aQ zd%<~JlU|JN-Qjx3Be8!Ro9YKVG^KC3-0O9Jv|nxMeqF?V%%KT-`NmhROJ&UoxN^&Z zX9pVpBKq6;+`?CM+Tx_By`N}R$Mn~4#yBCOvVC)Xx;Z1!8VcNQ@vDqXs9r;)*R^Vd zncV3+6lIW|j^D!kF;P}E5|Vi4prn?hU$6(8{x$OpUCPBF%{_NKgsI+OgH{TAkp9-i zqoX{7FQkOs{i9uDfH&S$uc3_pCH&1Q)xjbC7Au@7GEfzSQE-|o%M4IBKq zP7Gw?+cK=S0GEoVkfu7fg45{nNMZ@EKISYs-mBdhFV0wSjq8eO*7(gt;m92<RbZ?&Z|0p?MK@X()FDf`mKoP_ahWG|0K#9hd_0@B}rIKx@X`k90@^MvSOX zV(9*6;*T3H*&U5yT=}tR{SS}NN|pdo44w0U2Ua)|*91e24$#76B%S9?hq(BjBFr(uQhEOZ?TA|gmLZ#FLp1=5 zUi?hAX6{k+MVa#2Wx@{3NW#*DM~yZq!V;{=9kgi)D&&$gWk;#PZHME;Vmj8hVDUl0 z#OYzY*MA;}f3ttrGIIlzS^R6^R$J#LKN&&|TgR`W8lB0L%+ zzYa99pGu!r`&kZ)j5pq=rR%rejMz`QE9e7n!Qt z%@>l*0;q@pX7Lds*S5BXQ^?xh2#sE#Pm8S|4M`~-I;`$w=aCuxi13g%^_PbFp|1GB zML`EpbdPwfv#d5&9fw^4(c?lZL;lqd8b=X)Av<(L$w{e&BH_x_C-tZ%3yNV-p$l4Y zbVL+dH#e!HS6hg-ml!^?+r93D^L*wa@q(}NeyPGto)2-b5JrQ9yw<6xuCD*{63y3r zav4=Fk8kxF1Ko3`jse6@PK-<+U!melxL2Cu`#472FH|DRm?}A(qxaMPqr;vYmEu_U zq*T)K@8UPtT6tJW*9Wxfqb`{du;U+R$HU>=BII%Q_i>Fg+F2{-Hf=LhBm~G8t80Hy zsPA1HG^F1i8$(-#bl*yEmujOE&pK6X}8pf%0UpD3NqEu6X zz)x)igGU;~Y;LR~25Y!}X^o3&LKh!g*|u+Bgx$8#i9e3WSDyFOZi?VO4}C89&n(c^)MJ7K&CLG;T+QTZW!kRw zqx(tPkVC69j<%iT`YYP0%AojOPZoP$U!4Cwp6xtgK?i=+sWL{@^dieBQ7@m7u_(Z_ z{$?)7kanlSSzHkKq&1Mox2%Q2q~)EiH^WaN9bdKtJ!Gt^u9(2f3FbagFhFSB7;14s z>5>O4V5t&n#i`4<QcqGQm+}Tl~d3PPJuC*~DAMgZ@ z{`~Fx$xZBY!peInz__{APYR3_!-G}@?KhKn?1Q#mw*p&SZ14uL%!Rhv#M{#=^OEMfJP(!^w z{#%1nca7u#qK3rot5t}5Sm2@d%qsmZZZSqi6)QblxL7+49u^!lc2XJ%Ya31CJx;9c zO|+Z%b!NeL)D2CPpQ?s**u;E90eWgu@3P|u;G#mX4pU#oya5&98aD!IYc|uB!T93- zSTNAeAXr>-MDQBl#}(A?n|z2bV!+G*`^XvEKcN4&N93t&4Al3jaA1Hw#zl;7fGthI zFYNw1n`^F-K&5=mB#VK!nd`RivMBB@nc3CL!F9Jmam6ynTJ<4Nqofnd;&*O6*VQ*| zIo2;&o^0kS4Y8LUnCZ&N1^b@1M_UWUOHJa(gv`a1Q^C>eX{Bb3P+&=xIC1CNB2%>) z^*r)cvJ!RAOVC#(X-%NiZo5fGXT!Owp~|> zW^h4c!TmKo&>9bu*0SV8>D~+Eeg0q}N-N@0`kVCeY?&*Y=X;d02SRY%?|IuK|CH;* z$i`Kr^=3%}}2JKDlLAKOODy3x8X&;!=Os777Znoo?)J4$(Y%-Pg&Aq1 zv;TLlD|X{W)PoL{tVd~7jt#FZy3&ZO4lyPNeAyHT7DG!6^Pm`4I@Nc9oR5P+-Av)7aP<9EroQr=^FINaIRjfac;b zQk(GmWl>aTlKT3uAjCTBj>~Q6G3Qf4tFTu>p?-9EdtO38ybcO|7j7|aQ_m&`>Pi2*1!A9 zkaw7DsVo4V;oz~}Mutd$92eBgzQQ?+s1HdH9YStxZM)NJm(`p4wT!FwoQpGu>BT&B z$?dVMc=DD{i34 z+Ig)5tg7*LdQ`lvZKM87<75w|emOz?`j4WKfP(#yGCuQ1eb`Q~G})X{N)#~Je2ure z{qf4fRCdnG)wq}KJ3GZYclr7S2O_c^Kji!OPB1Q07_1WBlRX4dgoen2s{_a{oS9W z&18fKY?Fb7(w~RdmIV_O39gq%JDs^RtF?5*?o}PqxHE&h{7qnajgjPV{A~sF*O^@H z+jPfQqHPKdoe`B#UDD8;c0`M2ahXGW8+)c!v&IHek?L&&o!~yUXOalEf-`d?hp3r67oRcRG=?O$x;>9Z>t)}C+BdvvSsiawTL4KEr z?u&YI#p!UULD6(CtCfInn+qwqE-6g3JFTjxDFIgqe~(&ZbwvU5A6&z*WDgB{Hf!e; zDe)8g(d7lN6QYZ5abZR~Qt$M_f4Iussw4s8hjNLU2N1?I$QaVQ2$tz@*W{7EkocWD zdf7Upc+1q`bE3oD%a#4&UH&@srCDEVPK`ap>CQ{ZPJh$h1{WR+6@y&scM@7SA< z(uT)R1A{c>&to$^YKyu`%m&5QC+joKp z$TAZNqJ}^aSxMQi6S;&{cg5Y%62~F~j{S01ULi}a{m+zu(FV$NSlU(L)5R$f`xjDN zs<~O7BJcsK zr-kFvJXRq2UbuX?hf%P9bjxj}D9BG14obo@Var+yfcYfIMU2iG&R627b9EJ<%6?C( z1?QMG?w^&;gC19|TBDnw?oMAm(_kiSio%?bK}T0fKIaM(DkOHpNlootbk*6KWe(RX z55LB|Q3?o-r|q%MlTyfMrYx-SgNY%{iZYIyU8lU7d@|>tl<+#kNP>*-u}~j6GAEe& zjJsPpui#CM%$gk72s|)tpZX|@mTw3hulTiph-pGjW7b+p)%5%D7HvG2E8atiI6ab~ z;9@SRf5I^J9tyzKrF+sQ+)EVpHkk)>@wfD%d7o@Z9_)0AAaeyZuu4$U8+Y z%&w9;n9M}yX1T6y#|`D(r}}MtCY`l&b2ht38-tMQWSN`zw4C=BPAocwRlgRs zi;2bz2ORTR`Gs=pT$8|Q*CQG$sd1i(g{mQeNg zWv=CkOvX`=?|~)(yZeNBRIoH<$(&m)k`Sf3Ftx=GN;Y9z_<2f3M8O)~T*a>m?+i@> zO6Isf*lDd%5i1Es_p$^cMI4Wjr+@N|rB1k%IjR8lB;@iPk2U%o@AB&FkqPnU>#E8E zVL8(4wJ0m2A^6d;VAcgMB(5Zn{nG=-&t3L$Q@ogMtnXaK0}z-F#b}b}O+Y8}l2c(6 za*NOwUQ}ze0WQ* zSFc}lU#(x;2bsnuw~SXfUF7SCF_ZUPhoaQac}GT!Rfv#jLqW_ zuaJDIA(@V3;#?A)yL?V-fbjg?*degvAF16GQQw&X&xu=I@ShdO(Ll1On^)Xpo0YtQ zqL;&*4-!{5rCrm95|h|sbK}*uwtGD?XN~4n$w_Za$?!SktV4&hjq+lX2;QmtZ*h}y zJwqHD(lf9k$0rXQ)v}w?>B?{GESq%`DOiKQp-unQo6I~~k)w9V?5Ok(K&`Xs2Dglr9A#J3`x_U`9JR0gRVsHeu;s$7Bv+d+l1!OrkNiCf)}P z_B%cMmYze`K3OMYYr>Vjso{U$ Qod5s{5n16XLH&UL0l{2B1ONa4 literal 0 HcmV?d00001 diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index f73407727..836e98326 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -32,6 +32,9 @@ Always + + MSBuild:Compile + MSBuild:Compile @@ -41,6 +44,9 @@ MSBuild:Compile + + MSBuild:Compile + $(DefaultXamlRuntime) Designer @@ -73,6 +79,7 @@ + @@ -82,6 +89,7 @@ + @@ -225,6 +233,9 @@ Always + + Always + Always diff --git a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs index d51f38047..478231e1e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs @@ -33,6 +33,7 @@ public static IServiceCollection AddSetupFlow(this IServiceCollection services, services.AddRepoConfig(); services.AddReview(); services.AddSummary(); + services.AddQuickstart(); services.AddSummaryInformation(); services.AddCreateEnvironment(); @@ -197,6 +198,18 @@ private static IServiceCollection AddSetupTarget(this IServiceCollection service return services; } + private static IServiceCollection AddQuickstart(this IServiceCollection services) + { + // View models + services.AddTransient(); + + // Services + services.AddTransient(); + services.AddSingleton(); + + return services; + } + private static IServiceCollection AddCreateEnvironment(this IServiceCollection services) { // Task groups diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Doc.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Doc.cs new file mode 100644 index 000000000..4bf6a18bb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Doc.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevHome.SetupFlow.Models; + +public class Doc +{ + public string Name + { + get; set; + } + + public string Codespaces + { + get; set; + } + + public string Prompt + { + get; set; + } + + public string Code + { + get; set; + } + + public string Readme + { + get; set; + } + + public IReadOnlyList Embedding + { + get; set; + } + + public string Language + { + get; set; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/QuickStartProjectProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/QuickStartProjectProvider.cs new file mode 100644 index 000000000..553ed8b50 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/QuickStartProjectProvider.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using DevHome.SetupFlow.Services; +using DevHome.Telemetry; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Storage; + +namespace DevHome.SetupFlow.Models; + +/// +/// Wrapper class for the IQuickStartProjectProvider interface that can be used throughout the application. +/// Note: Additional methods added to this class should be wrapped in try/catch blocks to ensure that +/// exceptions don't bubble up to the caller as the methods are cross proc COM calls. +/// +public sealed class QuickStartProjectProvider +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickStartProjectProvider)); + + private readonly string _errorString; + + private readonly IQuickStartProjectProvider _quickStartProjectProvider; + + public string PackageFullName { get; } + + public string DisplayName { get; } + + public Uri TermsOfServiceUri { get; } + + public Uri PrivacyPolicyUri { get; } + + public string[] SamplePrompts { get; } + + public QuickStartProjectProvider( + IQuickStartProjectProvider quickStartProjectProvider, + ISetupFlowStringResource setupFlowStringResource, + string packageFullName) + { + _quickStartProjectProvider = quickStartProjectProvider; + PackageFullName = packageFullName; + DisplayName = quickStartProjectProvider.DisplayName; + TermsOfServiceUri = quickStartProjectProvider.TermsOfServiceUri; + PrivacyPolicyUri = quickStartProjectProvider.PrivacyPolicyUri; + _errorString = setupFlowStringResource.GetLocalized("QuickStartProjectUnexpectedError", DisplayName); + SamplePrompts = quickStartProjectProvider.SamplePrompts; + } + + public QuickStartProjectAdaptiveCardResult CreateAdaptiveCardSessionForExtensionInitialization() + { + try + { + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundExtensionInitialization"); + return _quickStartProjectProvider.CreateAdaptiveCardSessionForExtensionInitialization(); + } + catch (Exception ex) + { + _log.Error(ex, $"CreateAdaptiveCardSessionForExtensionInitialization for: {this} failed due to exception"); + TelemetryFactory.Get().LogException("CreateAdaptiveCardSessionForExtensionInitialization", ex); + return new QuickStartProjectAdaptiveCardResult(ex, ex.Message, ex.Message); + } + } + + public IQuickStartProjectGenerationOperation CreateProjectGenerationOperation(string prompt, StorageFolder outputFolder) + { + try + { + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundProjectGenerationOperation"); + return _quickStartProjectProvider.CreateProjectGenerationOperation(prompt, outputFolder); + } + catch (Exception ex) + { + TelemetryFactory.Get().LogException("QuickstartPlaygroundProjectGenerationOperation", ex); + _log.Error(ex, $"CreateProjectGenerationOperation for: {this} failed due to exception"); + return null; + } + } + + public override string ToString() + { + return $"QuickStartProject provider: {DisplayName}"; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/NativeMethods.txt b/tools/SetupFlow/DevHome.SetupFlow/NativeMethods.txt index d2af28834..6c60ca30e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/NativeMethods.txt +++ b/tools/SetupFlow/DevHome.SetupFlow/NativeMethods.txt @@ -10,4 +10,6 @@ LocalFree FormatMessage S_OK IsApiSetImplemented -FILE_ACCESS_RIGHTS \ No newline at end of file +FILE_ACCESS_RIGHTS +ShellExecuteEx +SHOW_WINDOW_CMD \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs index 9aadd5a35..5feb42ec5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs @@ -19,8 +19,8 @@ public class SetupFlowViewSelector : DataTemplateSelector public DataTemplate MainPageTemplate { get; set; } public DataTemplate RepoConfigTemplate { get; set; } - - public DataTemplate SetupTargetTemplate { get; set; } + + public DataTemplate SetupTargetTemplate { get; set; } public DataTemplate AppManagementTemplate { get; set; } @@ -30,12 +30,14 @@ public class SetupFlowViewSelector : DataTemplateSelector public DataTemplate SummaryTemplate { get; set; } - public DataTemplate ConfigurationFileTemplate { get; set; } - - public DataTemplate SelectEnvironmentsProviderTemplate { get; set; } - + public DataTemplate ConfigurationFileTemplate { get; set; } + + public DataTemplate SelectEnvironmentsProviderTemplate { get; set; } + public DataTemplate EnvironmentCreationOptionsTemplate { get; set; } + public DataTemplate QuickstartPlaygroundTemplate { get; set; } + protected override DataTemplate SelectTemplateCore(object item) { return ResolveDataTemplate(item, () => base.SelectTemplateCore(item)); @@ -62,10 +64,11 @@ private DataTemplate ResolveDataTemplate(object item, Func default ReviewViewModel => ReviewTemplate, LoadingViewModel => LoadingTemplate, SummaryViewModel => SummaryTemplate, - ConfigurationFileViewModel => ConfigurationFileTemplate, - SetupTargetViewModel => SetupTargetTemplate, - SelectEnvironmentProviderViewModel => SelectEnvironmentsProviderTemplate, + ConfigurationFileViewModel => ConfigurationFileTemplate, + SetupTargetViewModel => SetupTargetTemplate, + SelectEnvironmentProviderViewModel => SelectEnvironmentsProviderTemplate, EnvironmentCreationOptionsViewModel => EnvironmentCreationOptionsTemplate, + QuickstartPlaygroundViewModel => QuickstartPlaygroundTemplate, _ => defaultDataTemplate(), }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/IQuickStartProjectService.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/IQuickStartProjectService.cs new file mode 100644 index 000000000..025fcc1aa --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/IQuickStartProjectService.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using DevHome.SetupFlow.Models; + +namespace DevHome.SetupFlow.Services; + +public interface IQuickStartProjectService +{ + public Task> GetQuickStartProjectProvidersAsync(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/QuickStartProjectService.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/QuickStartProjectService.cs new file mode 100644 index 000000000..8a6c18db6 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/QuickStartProjectService.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DevHome.Common.Services; +using DevHome.SetupFlow.Models; +using Microsoft.Windows.DevHome.SDK; +using Serilog; + +namespace DevHome.SetupFlow.Services; + +public sealed class QuickStartProjectService : IQuickStartProjectService +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickStartProjectService)); + + private readonly IExtensionService _extensionService; + private readonly ISetupFlowStringResource _setupFlowStringResource; + + public QuickStartProjectService(IExtensionService extensionService, ISetupFlowStringResource setupFlowStringResource) + { + _extensionService = extensionService; + _setupFlowStringResource = setupFlowStringResource; + } + + public async Task> GetQuickStartProjectProvidersAsync() + { + var quickStartProjectProvidersFromAllExtensions = new List(); + var extensions = await _extensionService.GetInstalledExtensionsAsync(ProviderType.QuickStartProject); + foreach (var extension in extensions) + { + try + { + var quickStartProjectProviders = (await extension.GetListOfProvidersAsync()).ToList(); + foreach (var quickStartProjectProvider in quickStartProjectProviders) + { + if (quickStartProjectProvider != null) + { + quickStartProjectProvidersFromAllExtensions.Add(new(quickStartProjectProvider, _setupFlowStringResource, extension.PackageFullName)); + } + } + } + catch (Exception ex) + { + _log.Error(ex, $"Failed to get {nameof(IQuickStartProjectProvider)} provider from '{extension.PackageFullName}'"); + } + } + + return quickStartProjectProvidersFromAllExtensions; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 499965248..2eedd6984 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -272,6 +272,9 @@ public static class StringResourceKey public static readonly string SetupTargetConfigurationUnitProgressErrorWithMsg = nameof(SetupTargetConfigurationUnitProgressErrorWithMsg); public static readonly string SetupTargetConfigurationUnitUnknown = nameof(SetupTargetConfigurationUnitUnknown); + // Quickstart Playground + public static readonly string QuickstartPlaygroundLaunchButton = nameof(QuickstartPlaygroundLaunchButton); + // Create Environment flow public static readonly string SelectEnvironmentPageTitle = nameof(SelectEnvironmentPageTitle); public static readonly string ConfigureEnvironmentPageTitle = nameof(ConfigureEnvironmentPageTitle); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 9c763b4c9..3343c6b9e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -173,6 +173,10 @@ Close Label for a close button + + Close + Label for a close button + The field '{0}' in the configuration file is invalid {Locked="{0}"} An error in reading a configuration file. {0} is a placeholder replaced by the field name from the file. @@ -541,6 +545,14 @@ Clone repositories, install applications, and generate a WinGet Configuration file to apply to an environment. Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine + + [Experimental] Use natural language and AI to set up a new devcontainer-based programming environment + Body text description for a card than when clicked takes the user to a flow for creating a devcontainer environment for their project idea + + + Quickstart Playground + Header for a card than when clicked takes the user to a flow for creating a devcontainer environment for their project idea + No apps to install Text shown if no applications were selected to install @@ -1871,4 +1883,100 @@ View log Content for a button to view logs. + + An unexpected error occurred while attempting to perform the operation on the {0} provider. Please see the logs for more information. + Locked={"{0}"} Text for when there was an error performing a quick start project operation. . {0} is the name of the quick start project provider. + + + Quickstart Playground + Locked="{Quickstart Playground}" Title for the QuickstartPlayground page in the SetupFlow + + + Extension Provider + Label for the ComboBox with the list of available QuickstartProject extensions + + + Progress Output + Header for the optional progress output section when generating a Quickstart project + + + Generate + Label for the generate button on the QuickstartPlayground page + + + Generate project + Tooltip for the generate button on the QuickstartPlayground page + + + Open in Editor + Drop down button to launch QuickstartPlayground project + + + Open in {0} + Locked={"{0}"} Button to launch QuickstartPlayground project using {0} + + + Save + Button to save QuickstartPlayground project in another folder + + + What kind of project would you like to create? Try a sample prompt or write one of your own. + Label for prompt asking user what type of project to create + + + Generation Output: + Label for section showing generated folder + + + Like + Tooltip for like button on QuickstartPlayground page + + + Dislike + Tooltip for dislike button on QuickstartPlayground page + + + Provide additional feedback + Header above feedback section on QuickstartPlayground page + + + Submit feedback + Submit feedback button on QuickstartPlayground page + + + Your feedback has been submitted successfully! + Label for when feedback has been submitted on QuickstartPlayground page + + + What did you like? + Placeholder for like feedback prompt on QuickstartPlayground page + + + What did you dislike? + Placeholder for dislike feedback prompt on QuickstartPlayground page + + + Type a new generation command + Placeholder for generation prompt on QuickstartPlayground page once user has selected a provider + + + Quickstart Playground uses AI, check for mistakes. + AI warning label on QuickstartPlayground page + + + AI Terms + AI terms hyperlink text on QuickstartPlayground page + + + AI Privacy + AI privacy hyperlink text on QuickstartPlayground page + + + No Quickstart extension providers found. Please install one before continuing. + Message to indicate to the user that Dev Home could not find any Quickstart providers on the system + + + Select an extension provider before continuing. + Guidance text to instruct user to select a Quickstart provider before continuing + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/QuickstartStyles.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/QuickstartStyles.xaml new file mode 100644 index 000000000..e1db2d29d --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/QuickstartStyles.xaml @@ -0,0 +1,13 @@ + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/DeveloperQuickstartTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/DeveloperQuickstartTaskGroup.cs new file mode 100644 index 000000000..edc5dde65 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/DeveloperQuickstartTaskGroup.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.ViewModels; + +namespace DevHome.SetupFlow.TaskGroups; + +public class DeveloperQuickstartTaskGroup : ISetupTaskGroup +{ + private readonly QuickstartPlaygroundViewModel _viewModel; + + public DeveloperQuickstartTaskGroup(QuickstartPlaygroundViewModel quickstartPlaygroundViewModel) + { + _viewModel = quickstartPlaygroundViewModel; + } + + public IEnumerable SetupTasks => throw new NotImplementedException(); + + public IEnumerable DSCTasks => throw new NotImplementedException(); + + public ReviewTabViewModelBase GetReviewTabViewModel() + { + // Developer quickstart does not have a review tab + return null; + } + + public SetupPageViewModelBase GetSetupPageViewModel() => _viewModel; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs b/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs new file mode 100644 index 000000000..2fa0aecb0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using DevHome.SetupFlow.Models; + +namespace DevHome.SetupFlow.Utilities; + +public class EmbeddingsCalc +{ + private static double DotProduct(IReadOnlyList a, IReadOnlyList b) + { + return Enumerable.Range(0, a.Count).Sum(i => a[i] * b[i]); + } + + private static double CalcCosineSimilarity(IReadOnlyList a, IReadOnlyList b) + { + try + { + var dotProduct = DotProduct(a, b); + var magnitudeA = Math.Sqrt(DotProduct(a, a)); + var magnitudeB = Math.Sqrt(DotProduct(b, b)); + return dotProduct / (magnitudeA * magnitudeB); + } + catch (Exception) + { + return 0; + } + } + + public static List<(double cosineSimilarity, Doc doc)> SortByLanguageThenCosine(List<(double, Doc)> docs, string recommendedLanguage) + { + // Convert the recommendedLanguage to lowercase for case-insensitive comparison + recommendedLanguage = recommendedLanguage.ToLower(CultureInfo.InvariantCulture); + + // Clone the list of docs to avoid modifying the original list + List<(double cosineSimilarity, Doc doc)> similarDocList = docs.ToList(); + + // Sort doc list to rank highest any projects with the same language as recommended + similarDocList.Sort((a, b) => + { + // Sort by recommended language (case-insensitive) first + var aHasRecommendedLang = a.doc.Language != null ? a.doc.Language.Equals(recommendedLanguage, StringComparison.OrdinalIgnoreCase) : false; + var bHasRecommendedLang = b.doc.Language != null ? b.doc.Language.Equals(recommendedLanguage, StringComparison.OrdinalIgnoreCase) : false; + + if (aHasRecommendedLang && !bHasRecommendedLang) + { + return -1; + } + else if (!aHasRecommendedLang && bHasRecommendedLang) + { + return 1; + } + + // If recommended languages are the same or both are different from the recommended language, + // then sort by cosine similarity in descending order + return b.cosineSimilarity.CompareTo(a.cosineSimilarity); + }); + + return similarDocList; + } + + public static List<(double cosineSimilarity, Doc doc)> GetCosineSimilarityDocs(IReadOnlyList questionEmbedding, IReadOnlyList docs) + { + // For each doc in docs, calculate the cosine similarity between the question embedding and the doc embedding + // Sort the docs by the cosine similarity value + var cosineSimilarityDocs = new List<(double cosineSimilarity, Doc doc)>(); + + for (var i = 0; i < docs.Count; i++) + { + var doc = docs[i] ?? throw new ArgumentOutOfRangeException($"Document {i} is not expected to not be null"); + var embedding = doc.Embedding ?? throw new InvalidOperationException($"Document {i} does not have a valid embedding"); + var cosineSimilarity = CalcCosineSimilarity(questionEmbedding, embedding); + cosineSimilarityDocs.Add((cosineSimilarity, doc)); + } + + cosineSimilarityDocs.Sort((a, b) => b.cosineSimilarity.CompareTo(a.cosineSimilarity)); + + return cosineSimilarityDocs; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs index c28d4d6a1..915400e78 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; +using DevHome.Common.Models; using DevHome.Common.Services; using DevHome.Common.TelemetryEvents; using DevHome.Common.TelemetryEvents.SetupFlow; @@ -28,10 +30,12 @@ namespace DevHome.SetupFlow.ViewModels; /// combinations of steps to perform. For example, only Configuration file, /// or a full flow with Dev Volume, Clone Repos, and App Management. /// -public partial class MainPageViewModel : SetupPageViewModelBase +public partial class MainPageViewModel : SetupPageViewModelBase, IDisposable { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(MainPageViewModel)); + private const string QuickstartPlaygroundFlowFeatureName = "QuickstartPlayground"; + private readonly IHost _host; private readonly IWindowsPackageManager _wpm; private readonly IDesiredStateConfiguration _dsc; @@ -51,6 +55,11 @@ public partial class MainPageViewModel : SetupPageViewModelBase [ObservableProperty] private bool _showAppInstallerUpdateNotification; + [ObservableProperty] + private bool _enableQuickstartPlayground; + + private bool _disposedValue; + public string MainPageEnvironmentSetupGroupName => StringResource.GetLocalized(StringResourceKey.MainPageEnvironmentSetupGroup); public string MainPageQuickStepsGroupName => StringResource.GetLocalized(StringResourceKey.MainPageQuickConfigurationGroup); @@ -82,6 +91,47 @@ public MainPageViewModel( ShowDevDriveItem = DevDriveUtil.IsDevDriveFeatureEnabled; BannerViewModel = bannerViewModel; + + // If the feature is turned on, it doesn't show up in the configuration section (toggling it off and on again fixes it) + // It's because this is constructed after ExperimentalFeaturesViewModel, so the handler isn't added yet. + _host.GetService().ExperimentalFeatures.FirstOrDefault(f => string.Equals(f.Id, QuickstartPlaygroundFlowFeatureName, StringComparison.Ordinal))!.PropertyChanged += ExperimentalFeaturesViewModel_PropertyChanged; + + // Hack around this by setting the property explicitly based on the state of the feature. + EnableQuickstartPlayground = _host.GetService().ExperimentalFeatures.FirstOrDefault(f => string.Equals(f.Id, QuickstartPlaygroundFlowFeatureName, StringComparison.Ordinal))!.IsEnabled; + } + + // Create a PropertyChanged handler that we will add to the ExperimentalFeaturesViewModel + // to update the EnableQuickstartPlayground property when the feature is enabled/disabled. + private void ExperimentalFeaturesViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ExperimentalFeature.IsEnabled)) + { + EnableQuickstartPlayground = _host.GetService().ExperimentalFeatures.FirstOrDefault(f => string.Equals(f.Id, QuickstartPlaygroundFlowFeatureName, StringComparison.Ordinal))!.IsEnabled; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + var experimentationService = _host.GetService(); + if (experimentationService != null) + { + experimentationService.ExperimentalFeatures.FirstOrDefault(f => string.Equals(f.Id, QuickstartPlaygroundFlowFeatureName, StringComparison.Ordinal))!.PropertyChanged -= ExperimentalFeaturesViewModel_PropertyChanged; + } + } + + _disposedValue = true; + } + } + + // Disconnect event handler when the view model is disposed. + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } public async Task StartConfigurationFileAsync(StorageFile file) @@ -188,6 +238,13 @@ private void StartRepoConfig(string flowTitle) _host.GetService()); } + [RelayCommand] + private void StartQuickstart(string flowTitle) + { + _log.Information("Starting flow for developer quickstart playground"); + StartSetupFlowForTaskGroups(flowTitle, "DeveloperQuickstartPlayground", _host.GetService()); + } + /// /// Starts the create environment flow. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs new file mode 100644 index 000000000..46344c4c4 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/QuickstartPlaygroundViewModel.cs @@ -0,0 +1,604 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Contracts; +using DevHome.Common.Extensions; +using DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Services; +using DevHome.Telemetry; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Storage; +using Windows.Storage.Pickers; +using WinUIEx; + +namespace DevHome.SetupFlow.ViewModels; + +#nullable enable + +public partial class QuickstartPlaygroundViewModel : SetupPageViewModelBase +{ + public class FolderComboBoxItem + { + public string? DisplayFolderOutput + { + get; set; + } + + public string? FullFolderOutput + { + get; set; + } + } + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickstartPlaygroundViewModel)); + + private readonly IQuickStartProjectService _quickStartProjectService; + + private readonly ILocalSettingsService _localSettingsService; + + private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + private readonly ObservableCollection _dataSource = new(); + + private IQuickStartProjectGenerationOperation _quickStartProjectGenerationOperation = null!; + + private QuickStartProjectResult _quickStartProject = null!; + + [ObservableProperty] + private bool _showExamplePrompts = false; + + [ObservableProperty] + private bool _showPrivacyAndTermsLink = false; + + [ObservableProperty] + private bool _enableQuickstartProjectCombobox = true; + + [ObservableProperty] + private bool _isQuickstartProjectComboboxExpanded = false; + + [ObservableProperty] + private IQuickStartProjectHost[] _quickStartProjectHosts = []; + + [ObservableProperty] + private bool _isLaunchButtonVisible = true; + + [ObservableProperty] + private bool _isLaunchDropDownVisible = false; + + [ObservableProperty] + private string _launchButtonText = string.Empty; + + public ObservableCollection DataSource => _dataSource; + + public ObservableCollection QuickstartProviders { get; private set; } = []; + + [ObservableProperty] + private string _samplePromptOne = string.Empty; + + [ObservableProperty] + private string _samplePromptTwo = string.Empty; + + [ObservableProperty] + private string _samplePromptThree = string.Empty; + + [ObservableProperty] + private Uri? _privacyUri = default; + + [ObservableProperty] + private Uri? _termsUri = default; + + [ObservableProperty] + private string _outputFolderRoot = Path.Combine(Path.GetTempPath(), "DevHomeQuickstart"); + + private string _outputFolderForCurrentPrompt = string.Empty; + + [ObservableProperty] + private QuickStartProjectProvider? _activeQuickstartSelection; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(GenerateCodespaceCommand))] + private string? _customPrompt; + + [ObservableProperty] + private string? _promptTextBoxPlaceholder; + + [ObservableProperty] + private bool _isFileViewVisible = false; + + [ObservableProperty] + private bool _isProgressOutputVisible = false; + + [ObservableProperty] + private bool _isPromptTextBoxReadOnly = false; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(GenerateCodespaceCommand))] + private bool _areDependenciesPresent = false; + + [ObservableProperty] + private string? _progressMessage; + + [ObservableProperty] + private int _progressValue; + + [ObservableProperty] + private IExtensionAdaptiveCardSession2? _progressAdaptiveCardSession = null; + + // The four properties below are used to track the visibility state of the five controls + // that make up the positive and negative feedback flyouts. These states are set in the "submit" + // and "close" handlers. It turns out that the states can be completely represented by two + // booleans, so each flyout has its own group that the View will data-bind to. + [ObservableProperty] + private bool _negativesGroupOne = true; + + [ObservableProperty] + private bool _negativesGroupTwo = false; + + [ObservableProperty] + private bool _positivesGroupOne = true; + + [ObservableProperty] + private bool _positivesGroupTwo = false; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(LaunchProjectHostCommand), nameof(SaveProjectCommand))] + private bool _enableProjectButtons = false; + + public QuickstartPlaygroundViewModel( + ISetupFlowStringResource stringResource, + IQuickStartProjectService quickStartProjectService, + ILocalSettingsService localSettingsService, + SetupFlowOrchestrator orchestrator) + : base(stringResource, orchestrator) + { + IsStepPage = false; + + _quickStartProjectService = quickStartProjectService; + _localSettingsService = localSettingsService; + + // Placeholder launch text while button is disabled. + LaunchButtonText = StringResource.GetLocalized(StringResourceKey.QuickstartPlaygroundLaunchButton, string.Empty); + } + + [RelayCommand(CanExecute = nameof(EnableProjectButtons))] + public Task SaveProject() + { + return Task.Run(async () => + { + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundSaveProjectClicked"); + + // TODO: Replace with WindowSaveFileDialog + var folderPicker = new FolderPicker(); + var hWnd = Application.Current.GetService().GetWindowHandle(); + WinRT.Interop.InitializeWithWindow.Initialize(folderPicker, hWnd); + folderPicker.FileTypeFilter.Add("*"); + + var location = await folderPicker.PickSingleFolderAsync(); + if (!string.IsNullOrWhiteSpace(location?.Path)) + { + CopyDirectory(_outputFolderForCurrentPrompt, location.Path); + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundSaveProjectCompleted"); + } + }); + } + + private static void CopyDirectory(string sourceDir, string destinationDir) + { + var dir = new DirectoryInfo(sourceDir); + if (dir.Exists) + { + // Cache directories before we start copying + var dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (var file in dir.GetFiles()) + { + var targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // Copy subdirectories + foreach (var subDir in dirs) + { + var newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir); + } + } + } + + public async Task GetOutputFolder() + { + // Ensure we're starting from a clean state + DeleteDirectoryContents(OutputFolderRoot); + + var outputFolderForCurrentPrompt = Path.Combine(OutputFolderRoot, DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)); + + if (!Directory.Exists(outputFolderForCurrentPrompt)) + { + Directory.CreateDirectory(outputFolderForCurrentPrompt); + } + else + { + throw new IOException("Directory already exists."); + } + + return await StorageFolder.GetFolderFromPathAsync(outputFolderForCurrentPrompt); + } + + private void DeleteDirectoryContents(string folder) + { + // This is best-effort, so we won't block on failing to delete a prior project + try + { + var files = Directory.GetFiles(folder); + foreach (var file in files) + { + File.Delete(file); + } + + // Delete all subdirectories and their contents + var subDirs = Directory.GetDirectories(folder); + foreach (var dir in subDirs) + { + Directory.Delete(dir, true); + } + } + catch (Exception ex) + { + TelemetryFactory.Get().LogException("QuickstartPlaygroundDeleteDirectoryContents", ex); + } + } + + public void SetUpFileView() + { + var explorerItems = CreateExplorerItem(_outputFolderForCurrentPrompt); + + if (explorerItems != null) + { + DataSource.Clear(); + DataSource.Add(explorerItems); + } + else + { + throw new ArgumentNullException("Error creating explorer items."); + } + + IsFileViewVisible = true; + } + + public static ExplorerItem? CreateExplorerItem(string path) + { + ExplorerItem? item = null; + try + { + item = new ExplorerItem + { + Name = Path.GetFileName(path), + Type = File.GetAttributes(path).HasFlag(System.IO.FileAttributes.Directory) ? ExplorerItem.ExplorerItemType.Folder : ExplorerItem.ExplorerItemType.File, + FullPath = path, + }; + + if (item.Type == ExplorerItem.ExplorerItemType.Folder) + { + foreach (var subFolderPathString in Directory.GetDirectories(path)) + { + var subDirectory = CreateExplorerItem(subFolderPathString); + if (subDirectory != null) + { + item.Children.Add(subDirectory); + } + else + { + throw new ArgumentNullException($"Failed to add an ExplorerItem for {subFolderPathString}, CreateExplorerItem generated a null item"); + } + } + + foreach (var subPath in Directory.GetFiles(path)) + { + item.Children.Add(new ExplorerItem + { + Name = Path.GetFileName(subPath), + Type = ExplorerItem.ExplorerItemType.File, + FullPath = subPath, + }); + } + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + + return item; + } + + [RelayCommand] + public async Task PopulateQuickstartProvidersComboBox() + { + var providers = await _quickStartProjectService.GetQuickStartProjectProvidersAsync(); + foreach (var provider in providers) + { + ArgumentNullException.ThrowIfNull(provider); + QuickstartProviders.Add(provider); + } + + // If there are no providers, update the placeholder text and return + if (QuickstartProviders.Count == 0) + { + _log.Information("No installed Quickstart providers detected"); + PromptTextBoxPlaceholder = StringResource.GetLocalized("QuickstartPlaygroundNoProviderInstalled"); + return; + } + + // Check to see if there's already a previous provider selected. If so, we will automatically set it. + // This will raise the selection changed event and trigger the OnQuickstartSelectionChanged method. + if (_localSettingsService.HasSettingAsync("QuickstartPlaygroundSelectedProvider").Result) + { + // Check to see if that DisplayName is in the list of providers + var defaultProviderDisplayName = _localSettingsService.ReadSettingAsync("QuickstartPlaygroundSelectedProvider").Result; + var selectedProvider = QuickstartProviders.FirstOrDefault(p => p.DisplayName == defaultProviderDisplayName); + if (selectedProvider != null) + { + _log.Information("Automatically using previously-selected Quickstart extension provider"); + ActiveQuickstartSelection = selectedProvider; + PromptTextBoxPlaceholder = StringResource.GetLocalized("QuickstartPlaygroundGenerationPromptPlaceholder"); + } + else + { + _log.Information("Previously-selected provider not found in provider list (maybe it was uninstalled or the extension was turned off)"); + ConfigureForProviderSelection(); + } + } + else + { + _log.Information("No prior provider selection found."); + ConfigureForProviderSelection(); + } + } + + private void ConfigureForProviderSelection() + { + _log.Information("Asking user to select a new provider"); + IsQuickstartProjectComboboxExpanded = true; + PromptTextBoxPlaceholder = StringResource.GetLocalized("QuickstartPlaygroundSelectProvider"); + } + + public void UpdateProgress(QuickStartProjectProgress progress) + { + ProgressMessage = progress.DisplayStatus; + ProgressValue = (int)(progress.Progress * 100); + var adaptiveCardSession = _quickStartProjectGenerationOperation.AdaptiveCardSession; + if (adaptiveCardSession != ProgressAdaptiveCardSession) + { + ProgressAdaptiveCardSession = adaptiveCardSession; + IsProgressOutputVisible = adaptiveCardSession != null; + } + } + + [RelayCommand(CanExecute = nameof(CanGenerateCodespace))] + public async Task GenerateCodespace() + { + try + { + var userPrompt = CustomPrompt; + + // TODO: Replace this (and throughout) with proper diagnostics / logging + ArgumentNullException.ThrowIfNullOrEmpty(userPrompt); + ArgumentNullException.ThrowIfNull(ActiveQuickstartSelection); + + TelemetryFactory.Get().Log("QuickstartPlaygroundGenerateButtonClicked", LogLevel.Critical, new GenerateButtonClicked(userPrompt)); + + // Ensure file view isn't visible (in the case where the user has previously run a Generate command + IsFileViewVisible = false; + + // Temporarily turn off the provider combobox and ensure user cannot edit the prompt for the moment + EnableQuickstartProjectCombobox = false; + IsPromptTextBoxReadOnly = true; + + IProgress progress = new Progress(UpdateProgress); + + var outputFolder = await GetOutputFolder(); + _outputFolderForCurrentPrompt = outputFolder.Path; + + _quickStartProjectGenerationOperation = ActiveQuickstartSelection.CreateProjectGenerationOperation(userPrompt, outputFolder); + _quickStartProject = await _quickStartProjectGenerationOperation.GenerateAsync().AsTask(progress); + _quickStartProjectGenerationOperation = null!; + if (_quickStartProject.Result.Status == ProviderOperationStatus.Success) + { + SetUpFileView(); + SetupLaunchButton(); + EnableProjectButtons = true; + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundGenerateSuccceded"); + } + else + { + // TODO handle error scenario + TelemetryFactory.Get().Log("QuickstartPlaygroundGenerateFailed", LogLevel.Critical, new ProjectGenerationErrorInfo(_quickStartProject.Result.DisplayMessage, _quickStartProject.Result.ExtendedError, _quickStartProject.Result.DiagnosticText)); + } + } + finally + { + // Re-enable the provider combobox and prompt textbox + EnableQuickstartProjectCombobox = true; + IsPromptTextBoxReadOnly = false; + } + } + + private void SetupLaunchButton() + { + QuickStartProjectHosts = _quickStartProject.ProjectHosts; + + IsLaunchButtonVisible = QuickStartProjectHosts.Length == 1; + IsLaunchDropDownVisible = QuickStartProjectHosts.Length > 1; + + if (IsLaunchButtonVisible) + { + LaunchButtonText = StringResource.GetLocalized(StringResourceKey.QuickstartPlaygroundLaunchButton, QuickStartProjectHosts[0].DisplayName); + } + } + + [RelayCommand(CanExecute = nameof(EnableProjectButtons))] + private async Task LaunchProjectHost(IQuickStartProjectHost? projectHost = null) + { + TelemetryFactory.Get().LogCritical("QuickstartPlaygroundLaunchProjectClicked"); + var projectHostToLaunch = projectHost ?? QuickStartProjectHosts[0]; + await Task.Run(projectHostToLaunch.Launch); + } + + public void ProvideFeedback(bool isPositive, string feedback) + { + TelemetryFactory.Get().Log("QuickstartPlaygroundFeedbackSubmitted", LogLevel.Critical, new FeedbackSubmitted(isPositive, feedback)); + _quickStartProject?.FeedbackHandler?.ProvideFeedback(isPositive, feedback); + } + + public bool CanGenerateCodespace() + { + return !string.IsNullOrEmpty(CustomPrompt) && ActiveQuickstartSelection != null; + } + + [RelayCommand] + public void CopyExamplePrompt(string selectedPrompt) + { + CustomPrompt = selectedPrompt; + } + + [RelayCommand] + public void OnQuickstartSelectionChanged() + { + if (ActiveQuickstartSelection != null) + { + var prompts = ActiveQuickstartSelection.SamplePrompts; + + // TODO: this needs to be more robust to potentially handle variable numbers of + // prompts supplied by an extension. Right now, we're going to assume that we are + // only dealing with three prompts and that extensions conform to this. + if (prompts.Length == 3) + { + SamplePromptOne = prompts[0]; + SamplePromptTwo = prompts[1]; + SamplePromptThree = prompts[2]; + } + else + { + _log.Error($"{ActiveQuickstartSelection.DisplayName} did not provide the expected number of sample prompts. Expected 3, but got {prompts.Length}"); + } + + TermsUri = ActiveQuickstartSelection.TermsOfServiceUri; + PrivacyUri = ActiveQuickstartSelection.PrivacyPolicyUri; + + // Reset state + ShowExamplePrompts = true; + CustomPrompt = string.Empty; + IsPromptTextBoxReadOnly = false; + ShowPrivacyAndTermsLink = true; + _outputFolderForCurrentPrompt = string.Empty; + DataSource.Clear(); + IsFileViewVisible = false; + IsLaunchDropDownVisible = false; + IsLaunchButtonVisible = true; + EnableProjectButtons = false; + IsQuickstartProjectComboboxExpanded = false; + PromptTextBoxPlaceholder = StringResource.GetLocalized("QuickstartPlaygroundGenerationPromptPlaceholder"); + + // Update our setting to indicate the user preference + _localSettingsService.SaveSettingAsync("QuickstartPlaygroundSelectedProvider", ActiveQuickstartSelection.DisplayName); + + _log.Information("Completed setup work for extension selection"); + } + } +} + +public class ExplorerItem : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + public enum ExplorerItemType + { + Folder, + File, + } + + public string? Name + { + get; set; + } + + public ExplorerItemType Type + { + get; set; + } + + public string? FullPath + { + get; set; + } + + private ObservableCollection? children; + + public ObservableCollection Children + { + get + { + children ??= new ObservableCollection(); + return children; + } + set => children = value; + } + + private bool isExpanded; + + public bool IsExpanded + { + get => isExpanded; + set + { + if (isExpanded != value) + { + isExpanded = value; + NotifyPropertyChanged(nameof(IsExpanded)); + } + } + } + + private void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} + +public class ExplorerItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate? FolderTemplate + { + get; set; + } + + public DataTemplate? FileTemplate + { + get; set; + } + + protected override DataTemplate? SelectTemplateCore(object item) + { + var explorerItem = (ExplorerItem)item; + return explorerItem.Type == ExplorerItem.ExplorerItemType.Folder ? FolderTemplate : FileTemplate; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs index c44802cf5..6932f20bb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs @@ -1,108 +1,108 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using DevHome.Common.Services; -using DevHome.Common.TelemetryEvents.SetupFlow; -using DevHome.SetupFlow.Exceptions; -using DevHome.SetupFlow.Services; +using DevHome.Common.TelemetryEvents.SetupFlow; +using DevHome.SetupFlow.Exceptions; +using DevHome.SetupFlow.Services; using DevHome.Telemetry; using Serilog; -namespace DevHome.SetupFlow.ViewModels; - -public partial class SearchViewModel : ObservableObject -{ - public enum SearchResultStatus - { - // Search was successful - Ok, - - // Search was performed on a null, empty or whitespace string - EmptySearchQuery, - - // Search canceled - Canceled, - - // Search aborted because catalog is not connected yet - CatalogNotConnect, - - // Exception thrown during search - ExceptionThrown, - } - - private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SearchViewModel)); - private readonly IWindowsPackageManager _wpm; - private readonly ISetupFlowStringResource _stringResource; +namespace DevHome.SetupFlow.ViewModels; + +public partial class SearchViewModel : ObservableObject +{ + public enum SearchResultStatus + { + // Search was successful + Ok, + + // Search was performed on a null, empty or whitespace string + EmptySearchQuery, + + // Search canceled + Canceled, + + // Search aborted because catalog is not connected yet + CatalogNotConnect, + + // Exception thrown during search + ExceptionThrown, + } + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SearchViewModel)); + private readonly IWindowsPackageManager _wpm; + private readonly ISetupFlowStringResource _stringResource; private readonly PackageProvider _packageProvider; - private readonly IScreenReaderService _screenReaderService; - private const int SearchResultLimit = 20; - - /// - /// Search query text - /// - [ObservableProperty] - private string _searchText; - - /// - /// List of search results - /// - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SearchCountText))] - [NotifyPropertyChangedFor(nameof(NoSearchResultsText))] - private List _resultPackages = new(); - - /// - /// Gets the localized string for - /// - public string SearchCountText => ResultPackages.Count == 1 ? _stringResource.GetLocalized(StringResourceKey.ResultCountSingular, ResultPackages.Count, SearchText) : _stringResource.GetLocalized(StringResourceKey.ResultCountPlural, ResultPackages.Count, SearchText); - - /// - /// Gets the localized string for - /// - public string NoSearchResultsText => _stringResource.GetLocalized(StringResourceKey.NoSearchResultsFoundTitle, SearchText); - - public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, PackageProvider packageProvider, IScreenReaderService screenReaderService) - { - _wpm = wpm; - _stringResource = stringResource; + private readonly IScreenReaderService _screenReaderService; + private const int SearchResultLimit = 20; + + /// + /// Search query text + /// + [ObservableProperty] + private string _searchText; + + /// + /// List of search results + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SearchCountText))] + [NotifyPropertyChangedFor(nameof(NoSearchResultsText))] + private List _resultPackages = new(); + + /// + /// Gets the localized string for + /// + public string SearchCountText => ResultPackages.Count == 1 ? _stringResource.GetLocalized(StringResourceKey.ResultCountSingular, ResultPackages.Count, SearchText) : _stringResource.GetLocalized(StringResourceKey.ResultCountPlural, ResultPackages.Count, SearchText); + + /// + /// Gets the localized string for + /// + public string NoSearchResultsText => _stringResource.GetLocalized(StringResourceKey.NoSearchResultsFoundTitle, SearchText); + + public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, PackageProvider packageProvider, IScreenReaderService screenReaderService) + { + _wpm = wpm; + _stringResource = stringResource; _packageProvider = packageProvider; - _screenReaderService = screenReaderService; - } - - /// - /// Search for packages in all remote and local catalogs - /// - /// Text search query - /// Cancellation token - /// Search status and result - public async Task<(SearchResultStatus, List)> SearchAsync(string text, CancellationToken cancellationToken) - { - // Skip search if text is empty - if (string.IsNullOrWhiteSpace(text)) - { - return (SearchResultStatus.EmptySearchQuery, null); - } - - try - { - // Run the search on a separate (non-UI) thread to prevent lagging the UI. + _screenReaderService = screenReaderService; + } + + /// + /// Search for packages in all remote and local catalogs + /// + /// Text search query + /// Cancellation token + /// Search status and result + public async Task<(SearchResultStatus, List)> SearchAsync(string text, CancellationToken cancellationToken) + { + // Skip search if text is empty + if (string.IsNullOrWhiteSpace(text)) + { + return (SearchResultStatus.EmptySearchQuery, null); + } + + try + { + // Run the search on a separate (non-UI) thread to prevent lagging the UI. _log.Information($"Running package search for query [{text}]"); - var matches = await Task.Run(async () => await _wpm.SearchAsync(text, SearchResultLimit), cancellationToken); - - // Don't update the UI if the operation was canceled - if (cancellationToken.IsCancellationRequested) - { - return (SearchResultStatus.Canceled, null); - } - - // Update the UI only if the operation was successful - SearchText = text; + var matches = await Task.Run(async () => await _wpm.SearchAsync(text, SearchResultLimit), cancellationToken); + + // Don't update the UI if the operation was canceled + if (cancellationToken.IsCancellationRequested) + { + return (SearchResultStatus.Canceled, null); + } + + // Update the UI only if the operation was successful + SearchText = text; ResultPackages = await Task.Run(() => matches.Select(m => _packageProvider.CreateOrGet(m)).ToList()); // Announce the results. @@ -116,21 +116,21 @@ public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stri TelemetryFactory.Get().Log("Search_SearchingForApplication_NotFound_Event", LogLevel.Critical, new SearchEvent()); _screenReaderService.Announce(NoSearchResultsText); } - - return (SearchResultStatus.Ok, ResultPackages); + + return (SearchResultStatus.Ok, ResultPackages); } catch (WindowsPackageManagerRecoveryException) { return (SearchResultStatus.CatalogNotConnect, null); - } - catch (OperationCanceledException) - { - return (SearchResultStatus.Canceled, null); - } - catch (Exception e) - { - _log.Error(e, $"Search error."); - return (SearchResultStatus.ExceptionThrown, null); - } - } -} + } + catch (OperationCanceledException) + { + return (SearchResultStatus.Canceled, null); + } + catch (Exception e) + { + _log.Error(e, $"Search error."); + return (SearchResultStatus.ExceptionThrown, null); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml index eed3b8dcf..b3c858cab 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml @@ -17,6 +17,7 @@ + 32 @@ -161,6 +162,24 @@ + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml new file mode 100644 index 000000000..052a9f703 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs new file mode 100644 index 000000000..cf96e5a0f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.Common.Views; +using DevHome.SetupFlow.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; +using Serilog; + +namespace DevHome.SetupFlow.Views; + +#nullable enable +public sealed partial class QuickstartPlaygroundView : UserControl +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickstartPlaygroundView)); + + private ContentDialog? _adaptiveCardContentDialog; + + public QuickstartPlaygroundViewModel ViewModel + { + get; set; + } + + public QuickstartPlaygroundView() + { + ViewModel = Application.Current.GetService(); + ViewModel.PropertyChanged += ViewModel_PropertyChanged; + this.InitializeComponent(); + PromptCharacterCount.Text = $"0 / {CustomPrompt.MaxLength}"; + } + + private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (sender != null) + { + if (e.PropertyName == "IsLaunchDropDownVisible") + { + DropDownButtonFlyout.Items.Clear(); + foreach (var item in ViewModel.QuickStartProjectHosts) + { + DropDownButtonFlyout.Items.Add( + new MenuFlyoutItem() + { + Text = item.DisplayName, + Command = ViewModel.LaunchProjectHostCommand, + CommandParameter = item, + }); + } + } + else if (e.PropertyName == "ProgressAdaptiveCardSession") + { + _ = ShowProgressAdaptiveCard(); + } + } + } + + private void FolderHierarchy_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args) + { + if (args.InvokedItem is ExplorerItem item) + { + if (item.Type == ExplorerItem.ExplorerItemType.Folder) + { + GeneratedFileContent.Text = string.Empty; + } + else + { + // TODO: should theoretically do this more efficiently instead of re-reading the file + if (item.FullPath != null) + { + GeneratedFileContent.Text = File.ReadAllText(item.FullPath); + } + } + } + } + + private async void ExtensionProviderComboBox_Loading(FrameworkElement sender, object args) + { + // TODO: For a nicer UX, we should enable the user to pick a default provider so that they don't have to select it every time. + await ViewModel.PopulateQuickstartProvidersComboBox(); + } + + private async void ExtensionProviderComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + ViewModel.OnQuickstartSelectionChanged(); + await ShowExtensionInitializationUI(); + } + + private void NegativeFeedbackConfirmation_Click(object sender, RoutedEventArgs e) + { + ViewModel.NegativesGroupOne = false; + ViewModel.NegativesGroupTwo = true; + + ViewModel.ProvideFeedback(false, negativeFeedbackTextBox.Text); + negativeFeedbackTextBox.Text = string.Empty; + } + + private void PositiveFeedbackConfirmation_Click(object sender, RoutedEventArgs e) + { + ViewModel.ProvideFeedback(true, positiveFeedbackTextBox.Text); + positiveFeedbackTextBox.Text = string.Empty; + + ViewModel.PositivesGroupOne = false; + ViewModel.PositivesGroupTwo = true; + } + + private void NegativeFeedbackFlyout_Closed(object sender, object e) + { + ViewModel.NegativesGroupOne = true; + ViewModel.NegativesGroupTwo = false; + } + + private void PositiveFeedbackFlyout_Closed(object sender, object e) + { + ViewModel.PositivesGroupOne = true; + ViewModel.PositivesGroupTwo = false; + } + + private void NegCloseFlyout_Click(object sender, RoutedEventArgs e) + { + negativeFeedbackFlyout.Hide(); + } + + private void PosCloseFlyout_Click(object sender, RoutedEventArgs e) + { + positiveFeedbackFlyout.Hide(); + } + + public void OnAdaptiveCardSessionStopped(IExtensionAdaptiveCardSession2 cardSession, ExtensionAdaptiveCardSessionStoppedEventArgs data) + { + cardSession.Stopped -= OnAdaptiveCardSessionStopped; + if (_adaptiveCardContentDialog is not null) + { + DispatcherQueue.TryEnqueue(() => + { + _adaptiveCardContentDialog?.Hide(); + }); + } + } + + private async Task ShowAdaptiveCardOnContentDialog(QuickStartProjectAdaptiveCardResult adaptiveCardSessionResult) + { + if (adaptiveCardSessionResult == null) + { + // No adaptive card to show (i.e. no dependencies or AI initialization). + return; + } + + if (adaptiveCardSessionResult.Result.Status == ProviderOperationStatus.Failure) + { + _log.Error($"Failed to show adaptive card. {adaptiveCardSessionResult.Result.DisplayMessage} - {adaptiveCardSessionResult.Result.DiagnosticText}"); + return; + } + + var adapativeCardController = adaptiveCardSessionResult.AdaptiveCardSession; + var extensionAdaptiveCardPanel = new ExtensionAdaptiveCardPanel(); + var renderingService = Application.Current.GetService(); + var renderer = await renderingService.GetRendererAsync(); + + extensionAdaptiveCardPanel.Bind(adapativeCardController, renderer); + extensionAdaptiveCardPanel.RequestedTheme = ActualTheme; + + adapativeCardController.Stopped += OnAdaptiveCardSessionStopped; + + _adaptiveCardContentDialog = new ContentDialog + { + XamlRoot = this.XamlRoot, + Content = extensionAdaptiveCardPanel, + }; + + await _adaptiveCardContentDialog.ShowAsync(); + + adapativeCardController.Dispose(); + _adaptiveCardContentDialog = null; + } + + public async Task ShowExtensionInitializationUI() + { + ArgumentNullException.ThrowIfNull(ViewModel.ActiveQuickstartSelection); + var adaptiveCardSessionResult = ViewModel.ActiveQuickstartSelection.CreateAdaptiveCardSessionForExtensionInitialization(); + await ShowAdaptiveCardOnContentDialog(adaptiveCardSessionResult); + } + + public async Task ShowProgressAdaptiveCard() + { + var progressAdaptiveCardSession = ViewModel.ProgressAdaptiveCardSession; + if (progressAdaptiveCardSession == null) + { + return; + } + + var extensionAdaptiveCardPanel = new ExtensionAdaptiveCardPanel(); + var renderingService = Application.Current.GetService(); + var renderer = await renderingService.GetRendererAsync(); + + extensionAdaptiveCardPanel.Bind(progressAdaptiveCardSession, renderer); + extensionAdaptiveCardPanel.RequestedTheme = ActualTheme; + + extensionAdaptiveCardPanel.UiUpdate += (object? sender, FrameworkElement e) => + { + DispatcherQueue.TryEnqueue(() => + { + ProgressOutputScrollViewer.ScrollToVerticalOffset(ProgressOutputScrollViewer.ScrollableHeight); + }); + }; + + ProgressOutputScrollViewer.Content = extensionAdaptiveCardPanel; + } + + private void CustomPrompt_TextChanged(object sender, TextChangedEventArgs e) + { + PromptCharacterCount.Text = $"{CustomPrompt.Text.Length} / {CustomPrompt.MaxLength}"; + } + + private void CustomPrompt_GotFocus(object sender, RoutedEventArgs e) + { + var promptTextBox = sender as TextBox; + if (promptTextBox != null) + { + promptTextBox.SelectionStart = promptTextBox.Text.Length; + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml index c76facd36..23fd93454 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml @@ -84,6 +84,11 @@ + + + + + From 85bac9766a4e259c3de9335d9b1c603cb3a9d501 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Thu, 9 May 2024 16:59:17 -0700 Subject: [PATCH 2/3] Removing unncessary changes --- .../Strings/en-us/Resources.resw | 4 +- .../Utilities/EmbeddingsCalc.cs | 87 ------------------- 2 files changed, 2 insertions(+), 89 deletions(-) delete mode 100644 tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index 2a2852575..812608237 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -547,11 +547,11 @@ Quiet background processes - Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. + Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. Quiet background processes allows you to free up resources while developing - Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. + Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. Quickstart Playground diff --git a/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs b/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs deleted file mode 100644 index 2fa0aecb0..000000000 --- a/tools/SetupFlow/DevHome.SetupFlow/Utilities/EmbeddingsCalc.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Runtime.Versioning; -using System.Text; -using System.Threading.Tasks; -using DevHome.SetupFlow.Models; - -namespace DevHome.SetupFlow.Utilities; - -public class EmbeddingsCalc -{ - private static double DotProduct(IReadOnlyList a, IReadOnlyList b) - { - return Enumerable.Range(0, a.Count).Sum(i => a[i] * b[i]); - } - - private static double CalcCosineSimilarity(IReadOnlyList a, IReadOnlyList b) - { - try - { - var dotProduct = DotProduct(a, b); - var magnitudeA = Math.Sqrt(DotProduct(a, a)); - var magnitudeB = Math.Sqrt(DotProduct(b, b)); - return dotProduct / (magnitudeA * magnitudeB); - } - catch (Exception) - { - return 0; - } - } - - public static List<(double cosineSimilarity, Doc doc)> SortByLanguageThenCosine(List<(double, Doc)> docs, string recommendedLanguage) - { - // Convert the recommendedLanguage to lowercase for case-insensitive comparison - recommendedLanguage = recommendedLanguage.ToLower(CultureInfo.InvariantCulture); - - // Clone the list of docs to avoid modifying the original list - List<(double cosineSimilarity, Doc doc)> similarDocList = docs.ToList(); - - // Sort doc list to rank highest any projects with the same language as recommended - similarDocList.Sort((a, b) => - { - // Sort by recommended language (case-insensitive) first - var aHasRecommendedLang = a.doc.Language != null ? a.doc.Language.Equals(recommendedLanguage, StringComparison.OrdinalIgnoreCase) : false; - var bHasRecommendedLang = b.doc.Language != null ? b.doc.Language.Equals(recommendedLanguage, StringComparison.OrdinalIgnoreCase) : false; - - if (aHasRecommendedLang && !bHasRecommendedLang) - { - return -1; - } - else if (!aHasRecommendedLang && bHasRecommendedLang) - { - return 1; - } - - // If recommended languages are the same or both are different from the recommended language, - // then sort by cosine similarity in descending order - return b.cosineSimilarity.CompareTo(a.cosineSimilarity); - }); - - return similarDocList; - } - - public static List<(double cosineSimilarity, Doc doc)> GetCosineSimilarityDocs(IReadOnlyList questionEmbedding, IReadOnlyList docs) - { - // For each doc in docs, calculate the cosine similarity between the question embedding and the doc embedding - // Sort the docs by the cosine similarity value - var cosineSimilarityDocs = new List<(double cosineSimilarity, Doc doc)>(); - - for (var i = 0; i < docs.Count; i++) - { - var doc = docs[i] ?? throw new ArgumentOutOfRangeException($"Document {i} is not expected to not be null"); - var embedding = doc.Embedding ?? throw new InvalidOperationException($"Document {i} does not have a valid embedding"); - var cosineSimilarity = CalcCosineSimilarity(questionEmbedding, embedding); - cosineSimilarityDocs.Add((cosineSimilarity, doc)); - } - - cosineSimilarityDocs.Sort((a, b) => b.cosineSimilarity.CompareTo(a.cosineSimilarity)); - - return cosineSimilarityDocs; - } -} From f7cd75187c41b6319dddfb3f84b4fb6028eedea0 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Thu, 9 May 2024 18:07:02 -0700 Subject: [PATCH 3/3] Make experiment visible --- src/NavConfig.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index e3524698e..312364998 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -78,12 +78,12 @@ { "buildType": "canary", "enabledByDefault": false, - "visible": false + "visible": true }, { "buildType": "release", "enabledByDefault": false, - "visible": false + "visible": true } ] }