diff --git a/.github/workflows/build-ts.yml b/.github/workflows/build-ts.yml index a164baa8a..972e900a2 100644 --- a/.github/workflows/build-ts.yml +++ b/.github/workflows/build-ts.yml @@ -124,7 +124,7 @@ jobs: shell: bash run: | git fetch --no-tags origin "${{ github.base_ref }}" - npm run code-circular -- --ratchet --base "origin/${{ github.base_ref }}" + npm run code-circular -- --ratchet --base "origin/${{ github.base_ref }}" --exceptions-file tools/scripts/code/circular-baseline-exception.json # Test-debt gate (PRs only): zero tolerance for focused tests # (.only/fit/fdescribe) and no newly skipped tests (.skip/xit/xdescribe) # in changed files. A small, fixable problem -> a hard gate, not a ratchet. diff --git a/dotnet/autoShell.Tests/ActionDispatcherIntegrationTests.cs b/dotnet/autoShell.Tests/ActionDispatcherIntegrationTests.cs index 361c3f850..1fe1fc039 100644 --- a/dotnet/autoShell.Tests/ActionDispatcherIntegrationTests.cs +++ b/dotnet/autoShell.Tests/ActionDispatcherIntegrationTests.cs @@ -26,6 +26,7 @@ public class ActionDispatcherIntegrationTests private readonly Mock _windowMock = new(); private readonly Mock _networkMock = new(); private readonly Mock _virtualDesktopMock = new(); + private readonly Mock _serviceControlMock = new(); private readonly Mock _loggerMock = new(); private readonly ActionDispatcher _dispatcher; @@ -43,7 +44,8 @@ public ActionDispatcherIntegrationTests() _displayMock.Object, _windowMock.Object, _networkMock.Object, - _virtualDesktopMock.Object); + _virtualDesktopMock.Object, + _serviceControlMock.Object); } /// diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index 1701a3bc9..f53ba0dde 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -29,6 +29,7 @@ public HandlerRegistrationTests() var windowMock = new Mock(); var networkMock = new Mock(); var virtualDesktopMock = new Mock(); + var serviceControlMock = new Mock(); var loggerMock = new Mock(); _handlers = @@ -50,6 +51,7 @@ public HandlerRegistrationTests() new PrivacySettingsHandler(registryMock.Object), new SystemSettingsHandler(registryMock.Object, processMock.Object), new SystemActionHandler(processMock.Object, debuggerMock.Object), + new ServiceActionHandler(serviceControlMock.Object), ]; } diff --git a/dotnet/autoShell.Tests/ServiceActionHandlerTests.cs b/dotnet/autoShell.Tests/ServiceActionHandlerTests.cs new file mode 100644 index 000000000..5114687f8 --- /dev/null +++ b/dotnet/autoShell.Tests/ServiceActionHandlerTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using autoShell.Handlers; +using autoShell.Services; +using Moq; + +namespace autoShell.Tests; + +public class ServiceActionHandlerTests +{ + private readonly Mock _serviceMock = new(); + private readonly ServiceActionHandler _handler; + + public ServiceActionHandlerTests() + { + _handler = new ServiceActionHandler(_serviceMock.Object); + } + + /// + /// Verifies that RestartService with matchBy "name" locates the service by name. + /// + [Fact] + public void RestartService_MatchByName_CallsServiceByName() + { + _serviceMock + .Setup(s => s.RestartService("Spooler", false, false)) + .Returns(ServiceControlResult.Ok("Print Spooler")); + + var json = JsonDocument.Parse("""{"service":"Spooler","matchBy":"name"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + _serviceMock.Verify(s => s.RestartService("Spooler", false, false), Times.Once); + } + + /// + /// Verifies that RestartService with matchBy "description" locates the service by description. + /// + [Fact] + public void RestartService_MatchByDescription_CallsServiceByDescription() + { + _serviceMock + .Setup(s => s.RestartService("windows update", true, false)) + .Returns(ServiceControlResult.Ok("Windows Update")); + + var json = JsonDocument.Parse("""{"service":"windows update","matchBy":"description"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + _serviceMock.Verify(s => s.RestartService("windows update", true, false), Times.Once); + } + + /// + /// Verifies that RestartService without matchBy defaults to matching by name. + /// + [Fact] + public void RestartService_NoMatchBy_DefaultsToName() + { + _serviceMock + .Setup(s => s.RestartService("Audiosrv", false, false)) + .Returns(ServiceControlResult.Ok("Windows Audio")); + + var json = JsonDocument.Parse("""{"service":"Audiosrv"}""").RootElement; + _handler.Handle("RestartService", json); + + _serviceMock.Verify(s => s.RestartService("Audiosrv", false, false), Times.Once); + } + + /// + /// Verifies that the resolved service display name is included in the success message. + /// + [Fact] + public void RestartService_Success_ReturnsDisplayNameInMessage() + { + _serviceMock + .Setup(s => s.RestartService(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ServiceControlResult.Ok("Print Spooler")); + + var json = JsonDocument.Parse("""{"service":"Spooler"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + Assert.Contains("Print Spooler", result.Message); + } + + /// + /// Verifies that a failure from the service is surfaced as a failed result with its error message. + /// + [Fact] + public void RestartService_ServiceFails_ReturnsFailureWithError() + { + _serviceMock + .Setup(s => s.RestartService(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ServiceControlResult.Fail("No Windows service found with the name or display name 'bogus'.")); + + var json = JsonDocument.Parse("""{"service":"bogus"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.False(result.Success); + Assert.Contains("bogus", result.Message); + } + + /// + /// Verifies that an empty service identifier returns a failure without calling the service. + /// + [Fact] + public void RestartService_EmptyService_ReturnsFailure() + { + var json = JsonDocument.Parse("""{"service":""}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.False(result.Success); + _serviceMock.Verify(s => s.RestartService(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Verifies that a fuzzy match returns a success result carrying a confirmation payload + /// (rather than restarting anything) so the agent can confirm the target with the user. + /// + [Fact] + public void RestartService_FuzzyMatch_ReturnsConfirmationData() + { + _serviceMock + .Setup(s => s.RestartService("spool", false, false)) + .Returns(ServiceControlResult.Confirm("Spooler", "Print Spooler")); + + var json = JsonDocument.Parse("""{"service":"spool"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + Assert.NotNull(result.Data); + JsonElement data = result.Data.Value; + Assert.True(data.GetProperty("needsConfirmation").GetBoolean()); + Assert.Equal("Spooler", data.GetProperty("resolvedServiceName").GetString()); + Assert.Equal("Print Spooler", data.GetProperty("resolvedDisplayName").GetString()); + Assert.Equal("restart", data.GetProperty("operation").GetString()); + } + + /// + /// Verifies that when the service reports elevation is required, the handler returns a success + /// result carrying an elevation-confirmation payload (rather than restarting anything). + /// + [Fact] + public void RestartService_NeedsElevation_ReturnsElevationData() + { + _serviceMock + .Setup(s => s.RestartService("Spooler", false, false)) + .Returns(ServiceControlResult.Elevate("Spooler", "Print Spooler")); + + var json = JsonDocument.Parse("""{"service":"Spooler"}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + Assert.NotNull(result.Data); + JsonElement data = result.Data.Value; + Assert.True(data.GetProperty("needsElevation").GetBoolean()); + Assert.Equal("Spooler", data.GetProperty("resolvedServiceName").GetString()); + Assert.Equal("Print Spooler", data.GetProperty("resolvedDisplayName").GetString()); + Assert.Equal("restart", data.GetProperty("operation").GetString()); + } + + /// + /// Verifies that once the user has consented, the elevate flag is forwarded to the service. + /// + [Fact] + public void RestartService_ElevateTrue_PassesElevateToService() + { + _serviceMock + .Setup(s => s.RestartService("Spooler", false, true)) + .Returns(ServiceControlResult.Ok("Print Spooler")); + + var json = JsonDocument.Parse("""{"service":"Spooler","matchBy":"name","elevate":true}""").RootElement; + var result = _handler.Handle("RestartService", json); + + Assert.True(result.Success); + _serviceMock.Verify(s => s.RestartService("Spooler", false, true), Times.Once); + } +} diff --git a/dotnet/autoShell.Tests/ServiceMatcherTests.cs b/dotnet/autoShell.Tests/ServiceMatcherTests.cs new file mode 100644 index 000000000..65ac18db4 --- /dev/null +++ b/dotnet/autoShell.Tests/ServiceMatcherTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; + +namespace autoShell.Tests; + +public class ServiceMatcherTests +{ + private static readonly List Services = new() + { + new ServiceInfo( + "Spooler", + "Print Spooler", + "This service spools print jobs and handles interaction with the printer."), + new ServiceInfo( + "wuauserv", + "Windows Update", + "Enables the detection, download, and installation of updates for Windows and other programs."), + new ServiceInfo( + "Audiosrv", + "Windows Audio", + "Manages audio for Windows-based programs."), + new ServiceInfo( + "Dnscache", + "DNS Client", + "The DNS Client service caches Domain Name System (DNS) names."), + }; + + /// + /// Verifies that an empty query yields no match. + /// + [Fact] + public void Match_EmptyQuery_ReturnsNone() + { + var match = ServiceMatcher.Match(Services, "", byDescription: false); + + Assert.Equal(ServiceMatchKind.None, match.Kind); + } + + /// + /// Verifies that an empty candidate list yields no match. + /// + [Fact] + public void Match_NoServices_ReturnsNone() + { + var match = ServiceMatcher.Match(new List(), "spooler", byDescription: false); + + Assert.Equal(ServiceMatchKind.None, match.Kind); + } + + /// + /// Verifies that a case-insensitive service-name equality is an exact match. + /// + [Fact] + public void Match_ExactServiceName_CaseInsensitive_ReturnsExact() + { + var match = ServiceMatcher.Match(Services, "SPOOLER", byDescription: false); + + Assert.Equal(ServiceMatchKind.Exact, match.Kind); + Assert.Equal("Spooler", match.ServiceName); + Assert.Equal("Print Spooler", match.DisplayName); + } + + /// + /// Verifies that a display-name equality (ignoring whitespace/case) is an exact match. + /// + [Fact] + public void Match_ExactDisplayName_ReturnsExact() + { + var match = ServiceMatcher.Match(Services, " windows update ", byDescription: false); + + Assert.Equal(ServiceMatchKind.Exact, match.Kind); + Assert.Equal("wuauserv", match.ServiceName); + } + + /// + /// Verifies that a partial display-name query is offered as a fuzzy match. + /// + [Fact] + public void Match_SubstringOfDisplayName_ReturnsFuzzy() + { + var match = ServiceMatcher.Match(Services, "print", byDescription: false); + + Assert.Equal(ServiceMatchKind.Fuzzy, match.Kind); + Assert.Equal("Spooler", match.ServiceName); + Assert.Equal("Print Spooler", match.DisplayName); + } + + /// + /// Verifies that a typo close to a display name is offered as a fuzzy match. + /// + [Fact] + public void Match_Typo_ReturnsFuzzy() + { + var match = ServiceMatcher.Match(Services, "widnows update", byDescription: false); + + Assert.Equal(ServiceMatchKind.Fuzzy, match.Kind); + Assert.Equal("wuauserv", match.ServiceName); + } + + /// + /// Verifies that an unrelated query yields no match. + /// + [Fact] + public void Match_Unrelated_ReturnsNone() + { + var match = ServiceMatcher.Match(Services, "xyzzy nonexistent", byDescription: false); + + Assert.Equal(ServiceMatchKind.None, match.Kind); + } + + /// + /// Verifies that a phrase contained in a service's description is offered as a fuzzy match. + /// + [Fact] + public void Match_ByDescription_PhraseContained_ReturnsFuzzy() + { + var match = ServiceMatcher.Match(Services, "spools print jobs", byDescription: true); + + Assert.Equal(ServiceMatchKind.Fuzzy, match.Kind); + Assert.Equal("Spooler", match.ServiceName); + } + + /// + /// Verifies that overlapping description keywords produce a fuzzy match even when not contiguous. + /// + [Fact] + public void Match_ByDescription_TokenCoverage_ReturnsFuzzy() + { + var match = ServiceMatcher.Match( + Services, + "detection download installation updates", + byDescription: true); + + Assert.Equal(ServiceMatchKind.Fuzzy, match.Kind); + Assert.Equal("wuauserv", match.ServiceName); + } + + /// + /// Verifies that a description query with no meaningful overlap yields no match. + /// + [Fact] + public void Match_ByDescription_NoMatch_ReturnsNone() + { + var match = ServiceMatcher.Match(Services, "quantum flux capacitor", byDescription: true); + + Assert.Equal(ServiceMatchKind.None, match.Kind); + } + + /// + /// Verifies that an exact display-name match is still preferred in description mode. + /// + [Fact] + public void Match_ByDescription_ExactDisplayName_ReturnsExact() + { + var match = ServiceMatcher.Match(Services, "DNS Client", byDescription: true); + + Assert.Equal(ServiceMatchKind.Exact, match.Kind); + Assert.Equal("Dnscache", match.ServiceName); + } + + /// + /// Verifies that the service name is used as the display name when no display name is set. + /// + [Fact] + public void Match_MissingDisplayName_FallsBackToServiceName() + { + var services = new List + { + new("CustomSvc", "", "A custom background worker service."), + }; + + var match = ServiceMatcher.Match(services, "CustomSvc", byDescription: false); + + Assert.Equal(ServiceMatchKind.Exact, match.Kind); + Assert.Equal("CustomSvc", match.DisplayName); + } +} diff --git a/dotnet/autoShell/ActionDispatcher.cs b/dotnet/autoShell/ActionDispatcher.cs index a51e48220..a05f398b0 100644 --- a/dotnet/autoShell/ActionDispatcher.cs +++ b/dotnet/autoShell/ActionDispatcher.cs @@ -47,7 +47,8 @@ public static ActionDispatcher Create(ILogger logger) new WindowsDisplayService(logger), new WindowsWindowService(logger), new WindowsNetworkService(logger), - new WindowsVirtualDesktopService(logger) + new WindowsVirtualDesktopService(logger), + new WindowsServiceControlService(logger) ); } @@ -67,7 +68,8 @@ internal static ActionDispatcher Create( IDisplayService display, IWindowService window, INetworkService network, - IVirtualDesktopService virtualDesktop) + IVirtualDesktopService virtualDesktop, + IServiceControlService serviceControl) { var dispatcher = new ActionDispatcher(logger); @@ -88,7 +90,8 @@ internal static ActionDispatcher Create( new FileExplorerSettingsHandler(registry), new PrivacySettingsHandler(registry), new SystemSettingsHandler(registry, process), - new SystemActionHandler(process, debugger) + new SystemActionHandler(process, debugger), + new ServiceActionHandler(serviceControl) ); var validator = new SchemaValidator(logger); diff --git a/dotnet/autoShell/Handlers/ServiceActionHandler.cs b/dotnet/autoShell/Handlers/ServiceActionHandler.cs new file mode 100644 index 000000000..f7f7f85b7 --- /dev/null +++ b/dotnet/autoShell/Handlers/ServiceActionHandler.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using autoShell.Handlers.Generated; +using autoShell.Services; + +namespace autoShell.Handlers; + +/// +/// Handles Windows service control commands: RestartService. +/// +internal class ServiceActionHandler : ActionHandlerBase +{ + private readonly IServiceControlService _services; + + public ServiceActionHandler(IServiceControlService services) + { + _services = services; + AddAction("RestartService", HandleRestartService); + } + + private ActionResult HandleRestartService(RestartServiceParams p) + { + if (string.IsNullOrWhiteSpace(p.Service)) + { + return ActionResult.Fail("A service name or description is required."); + } + + bool matchByDescription = string.Equals(p.MatchBy, "description", StringComparison.OrdinalIgnoreCase); + bool elevate = p.Elevate ?? false; + ServiceControlResult result = _services.RestartService(p.Service, matchByDescription, elevate); + + // A fuzzy match asks the caller (the TS agent) to confirm the resolved service with + // the user before acting. Report success with a confirmation payload rather than + // restarting anything yet. + if (result.NeedsConfirmation) + { + return ActionResult.Ok( + $"Found a close match: '{result.ResolvedDisplayName}'. Awaiting confirmation before restarting.", + BuildConfirmationData("needsConfirmation", result.ResolvedServiceName, result.ResolvedDisplayName)); + } + + // The restart needs administrator rights the host lacks. Ask the TS agent to confirm the + // user is willing to run it elevated before doing anything. + if (result.NeedsElevation) + { + return ActionResult.Ok( + $"Restarting '{result.ResolvedDisplayName}' requires administrator privileges. Awaiting confirmation.", + BuildConfirmationData("needsElevation", result.ResolvedServiceName, result.ResolvedDisplayName)); + } + + return result.Success + ? ActionResult.Ok($"Restarted service '{result.ServiceDisplayName}'") + : ActionResult.Fail(result.Error); + } + + private static JsonElement BuildConfirmationData(string flag, string resolvedServiceName, string resolvedDisplayName) + { + var payload = new Dictionary + { + [flag] = true, + ["resolvedServiceName"] = resolvedServiceName, + ["resolvedDisplayName"] = resolvedDisplayName, + ["operation"] = "restart", + }; + using var doc = JsonSerializer.SerializeToDocument(payload); + return doc.RootElement.Clone(); + } +} diff --git a/dotnet/autoShell/Services/IServiceControlService.cs b/dotnet/autoShell/Services/IServiceControlService.cs new file mode 100644 index 000000000..d682a1fd2 --- /dev/null +++ b/dotnet/autoShell/Services/IServiceControlService.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts Windows service control operations (start/stop/restart) for testability. +/// +internal interface IServiceControlService +{ + /// + /// Restarts a Windows service, stopping it (and any running dependents) and starting it again. + /// + /// + /// The service name or display name when is false; + /// otherwise a phrase to search for within service descriptions. + /// + /// + /// When true, locates the service by searching its description text; otherwise matches by + /// service name or display name. + /// + /// + /// Whether the user has consented to running the restart with administrator privileges. When the + /// host is not elevated and this is false, the result has + /// set and nothing is restarted; the caller + /// should obtain consent and retry with this set to true. + /// + /// + /// A describing the outcome. When the query resolves only to a + /// fuzzy (approximate) match, the result has + /// set and the service is not restarted; the caller should confirm with the user and retry + /// using the resolved exact service name. + /// + ServiceControlResult RestartService(string identifier, bool matchByDescription, bool elevate); +} + +/// +/// Result of a service control operation. +/// +internal sealed record ServiceControlResult +{ + /// Whether the operation succeeded. + public bool Success { get; init; } + + /// The display name of the affected service, when the operation succeeded. + public string ServiceDisplayName { get; init; } + + /// A human-readable error message, when the operation failed. + public string Error { get; init; } + + /// + /// When true, the query resolved to a fuzzy (non-exact) match that the caller + /// should confirm with the user before acting. No change has been made yet. + /// + public bool NeedsConfirmation { get; init; } + + /// The exact service name of the resolved match, for use in a confirmed or elevated retry. + public string ResolvedServiceName { get; init; } + + /// The display name of the resolved match, for presenting to the user. + public string ResolvedDisplayName { get; init; } + + /// + /// When true, the restart requires administrator privileges the host does not have. The + /// caller should confirm with the user and retry with elevation. No change has been made yet. + /// + public bool NeedsElevation { get; init; } + + /// Creates a successful result for the given service display name. + public static ServiceControlResult Ok(string serviceDisplayName) => + new() { Success = true, ServiceDisplayName = serviceDisplayName }; + + /// Creates a failure result with an error message. + public static ServiceControlResult Fail(string error) => + new() { Success = false, Error = error }; + + /// + /// Creates a result indicating a fuzzy match was found and user confirmation is required + /// before the operation is performed. + /// + public static ServiceControlResult Confirm(string resolvedServiceName, string resolvedDisplayName) => + new() + { + Success = true, + NeedsConfirmation = true, + ResolvedServiceName = resolvedServiceName, + ResolvedDisplayName = resolvedDisplayName, + }; + + /// + /// Creates a result indicating the restart needs administrator privileges the host lacks, so the + /// caller should obtain user consent and retry with elevation. + /// + public static ServiceControlResult Elevate(string resolvedServiceName, string resolvedDisplayName) => + new() + { + Success = true, + NeedsElevation = true, + ResolvedServiceName = resolvedServiceName, + ResolvedDisplayName = resolvedDisplayName, + }; +} diff --git a/dotnet/autoShell/Services/ServiceMatcher.cs b/dotnet/autoShell/Services/ServiceMatcher.cs new file mode 100644 index 000000000..ddcd65e3a --- /dev/null +++ b/dotnet/autoShell/Services/ServiceMatcher.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace autoShell.Services; + +/// +/// The quality of a service match: an exact name/display-name equality, a fuzzy +/// (approximate) match that should be confirmed with the user, or no match. +/// +internal enum ServiceMatchKind +{ + None, + Exact, + Fuzzy, +} + +/// +/// Minimal, platform-independent description of a Windows service used for matching. +/// +internal readonly record struct ServiceInfo(string ServiceName, string DisplayName, string Description); + +/// +/// The result of resolving a query against the installed services. +/// +internal readonly record struct ServiceMatch(ServiceMatchKind Kind, string ServiceName, string DisplayName) +{ + /// A sentinel representing "no match". + public static ServiceMatch None { get; } = new(ServiceMatchKind.None, null, null); +} + +/// +/// Resolves a free-text query to an installed Windows service, distinguishing exact +/// matches from fuzzy (approximate) ones. Pure logic with no OS dependencies so it can +/// be unit-tested directly. +/// +internal static class ServiceMatcher +{ + /// Minimum similarity for a name/display-name fuzzy match to be offered. + private const double NameThreshold = 0.5; + + /// Minimum coverage for a description fuzzy match to be offered. + private const double DescriptionThreshold = 0.5; + + private static readonly char[] TokenSeparators = + { ' ', '\t', '\r', '\n', '-', '_', '.', ',', ';', ':', '(', ')', '/', '\\', '"', '\'' }; + + /// + /// Finds the best match for among . + /// An exact (case/whitespace-insensitive) match on the service name or display name is + /// preferred; otherwise the highest-scoring candidate above the fuzzy threshold is returned. + /// + /// The candidate services. + /// The user-provided service name, display name, or description phrase. + /// When true, scores candidates by their description text. + public static ServiceMatch Match(IReadOnlyList services, string query, bool byDescription) + { + if (services == null || services.Count == 0) + { + return ServiceMatch.None; + } + + string normQuery = Normalize(query); + if (normQuery.Length == 0) + { + return ServiceMatch.None; + } + + // 1. Exact match on service name or display name always wins and needs no confirmation. + foreach (var s in services) + { + if (Normalize(s.ServiceName) == normQuery || Normalize(s.DisplayName) == normQuery) + { + return new ServiceMatch(ServiceMatchKind.Exact, s.ServiceName, DisplayOf(s)); + } + } + + // 2. Otherwise pick the single highest-scoring candidate above the threshold. + ServiceInfo best = default; + double bestScore = 0.0; + bool found = false; + + foreach (var s in services) + { + double score = byDescription ? ScoreByDescription(normQuery, s) : ScoreByName(normQuery, s); + if (score > bestScore) + { + bestScore = score; + best = s; + found = true; + } + } + + double threshold = byDescription ? DescriptionThreshold : NameThreshold; + return found && bestScore >= threshold + ? new ServiceMatch(ServiceMatchKind.Fuzzy, best.ServiceName, DisplayOf(best)) + : ServiceMatch.None; + } + + private static string DisplayOf(ServiceInfo s) => + string.IsNullOrWhiteSpace(s.DisplayName) ? s.ServiceName : s.DisplayName; + + private static double ScoreByName(string normQuery, ServiceInfo s) => + Math.Max( + SimilarityScore(normQuery, Normalize(s.DisplayName)), + SimilarityScore(normQuery, Normalize(s.ServiceName))); + + private static double ScoreByDescription(string normQuery, ServiceInfo s) + { + // A name/display hit still counts when searching by description; otherwise fall + // back to how much of the query phrase is covered by the description text. + double nameScore = ScoreByName(normQuery, s); + double descScore = DescriptionCoverage(normQuery, Normalize(s.Description)); + return Math.Max(nameScore, descScore); + } + + /// + /// General-purpose 0..1 similarity for short strings, combining substring containment, + /// token (Jaccard) overlap, and an edit-distance ratio. + /// + private static double SimilarityScore(string query, string target) + { + if (query.Length == 0 || target.Length == 0) + { + return 0.0; + } + if (query == target) + { + return 1.0; + } + + double score = LevenshteinRatio(query, target); + + if (target.Contains(query) || query.Contains(target)) + { + int shorter = Math.Min(query.Length, target.Length); + int longer = Math.Max(query.Length, target.Length); + double containment = 0.7 + (0.3 * ((double)shorter / longer)); + score = Math.Max(score, containment); + } + + return Math.Max(score, TokenOverlap(query, target)); + } + + /// + /// Fraction of the query phrase covered by a (typically longer) description: a full + /// substring hit scores highest, otherwise the ratio of query tokens present. + /// + private static double DescriptionCoverage(string query, string description) + { + if (query.Length == 0 || description.Length == 0) + { + return 0.0; + } + if (description.Contains(query)) + { + return 0.9; + } + + var queryTokens = Tokenize(query); + if (queryTokens.Count == 0) + { + return 0.0; + } + + var descTokens = new HashSet(Tokenize(description)); + int matched = queryTokens.Count(t => descTokens.Contains(t)); + return (double)matched / queryTokens.Count; + } + + private static double TokenOverlap(string a, string b) + { + var setA = new HashSet(Tokenize(a)); + var setB = new HashSet(Tokenize(b)); + if (setA.Count == 0 || setB.Count == 0) + { + return 0.0; + } + + int intersection = setA.Count(t => setB.Contains(t)); + int union = setA.Count + setB.Count - intersection; + return union == 0 ? 0.0 : (double)intersection / union; + } + + private static List Tokenize(string s) => + s.Split(TokenSeparators, StringSplitOptions.RemoveEmptyEntries).ToList(); + + private static double LevenshteinRatio(string a, string b) + { + int max = Math.Max(a.Length, b.Length); + return max == 0 ? 0.0 : 1.0 - ((double)Levenshtein(a, b) / max); + } + + private static int Levenshtein(string a, string b) + { + int n = a.Length; + int m = b.Length; + if (n == 0) + { + return m; + } + if (m == 0) + { + return n; + } + + var prev = new int[m + 1]; + var curr = new int[m + 1]; + for (int j = 0; j <= m; j++) + { + prev[j] = j; + } + + for (int i = 1; i <= n; i++) + { + curr[0] = i; + for (int j = 1; j <= m; j++) + { + int cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + + (prev, curr) = (curr, prev); + } + + return prev[m]; + } + + /// Lower-cases, trims, and collapses internal whitespace. + private static string Normalize(string s) + { + if (string.IsNullOrWhiteSpace(s)) + { + return string.Empty; + } + + var parts = s.Trim().ToLowerInvariant().Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + return string.Join(" ", parts); + } +} diff --git a/dotnet/autoShell/Services/WindowsServiceControlService.cs b/dotnet/autoShell/Services/WindowsServiceControlService.cs new file mode 100644 index 000000000..33ae93ee4 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsServiceControlService.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Management; +using System.Security.Principal; +using System.ServiceProcess; +using autoShell.Logging; + +namespace autoShell.Services; + +/// +/// Concrete implementation of . Services are enumerated and +/// matched (including by description, via WMI Win32_Service) without elevation; the restart +/// itself uses when the host is already elevated, otherwise an +/// elevated PowerShell helper that prompts the user for consent via UAC. +/// +internal sealed class WindowsServiceControlService : IServiceControlService +{ + /// Maximum time to wait for a service to reach a target state. + private static readonly TimeSpan StatusTimeout = TimeSpan.FromSeconds(20); + + /// Maximum time to wait for the elevated restart helper to finish. + private const int ElevatedRestartTimeoutMs = 28000; + + private readonly ILogger _logger; + + public WindowsServiceControlService(ILogger logger) + { + _logger = logger; + } + + /// + public ServiceControlResult RestartService(string identifier, bool matchByDescription, bool elevate) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + return ServiceControlResult.Fail("A service name or description is required."); + } + + ServiceMatch match; + try + { + IReadOnlyList candidates = matchByDescription + ? EnumerateServicesWithDescriptions() + : EnumerateServices(); + match = ServiceMatcher.Match(candidates, identifier, matchByDescription); + } + catch (Exception ex) + { + _logger.Error(ex); + return ServiceControlResult.Fail($"Failed to look up service '{identifier}': {ex.Message}"); + } + + if (match.Kind == ServiceMatchKind.None) + { + string how = matchByDescription ? "a description matching" : "the name or display name"; + return ServiceControlResult.Fail($"No Windows service found with {how} '{identifier}'."); + } + + // A fuzzy (non-exact) match is only a best guess. Defer to the caller to confirm + // the resolved service with the user before actually restarting anything. + if (match.Kind == ServiceMatchKind.Fuzzy) + { + return ServiceControlResult.Confirm(match.ServiceName, match.DisplayName); + } + + return PerformRestart(match.ServiceName, match.DisplayName, elevate); + } + + /// + /// Restarts the resolved service. Controlling a service requires administrator rights, so when + /// the host is not elevated the caller must first obtain the user's consent (); + /// only then is the restart delegated to an elevated PowerShell helper (which prompts via UAC). + /// + private ServiceControlResult PerformRestart(string serviceName, string displayName, bool elevate) + { + if (IsElevated()) + { + return RestartInProcess(serviceName, displayName); + } + + // Not elevated: only run the elevated helper once the user has agreed to it; otherwise ask + // the caller to obtain consent first. + return elevate + ? RestartElevated(serviceName, displayName) + : ServiceControlResult.Elevate(serviceName, displayName); + } + + /// + /// Restarts the service in-process via (requires the host to + /// already be elevated). + /// + private ServiceControlResult RestartInProcess(string serviceName, string displayName) + { + try + { + using var controller = new ServiceController(serviceName); + string resolvedDisplayName = string.IsNullOrWhiteSpace(controller.DisplayName) + ? displayName + : controller.DisplayName; + Restart(controller); + return ServiceControlResult.Ok(resolvedDisplayName); + } + catch (Exception ex) when (IsAccessDenied(ex)) + { + _logger.Error(ex); + return ServiceControlResult.Fail( + $"Access was denied restarting '{displayName}'. The service's security settings prevent it from being controlled."); + } + catch (Exception ex) + { + _logger.Error(ex); + return ServiceControlResult.Fail($"Failed to restart service '{serviceName}': {ex.Message}"); + } + } + + /// + /// Restarts the service by launching an elevated PowerShell process. The OS shows a UAC consent + /// prompt; declining it (or a failure inside the helper) yields a failure result. + /// + private ServiceControlResult RestartElevated(string serviceName, string displayName) + { + // Single-quote-escape the service name for safe embedding in the PowerShell command. + string psName = serviceName.Replace("'", "''"); + var startInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = + "-NoProfile -ExecutionPolicy Bypass -Command " + + $"\"try {{ Restart-Service -Name '{psName}' -Force -ErrorAction Stop }} catch {{ exit 1 }}\"", + UseShellExecute = true, + Verb = "runas", + WindowStyle = ProcessWindowStyle.Hidden, + }; + + try + { + using Process proc = Process.Start(startInfo); + if (proc == null) + { + return ServiceControlResult.Fail($"Failed to launch an elevated restart for '{displayName}'."); + } + + if (!proc.WaitForExit(ElevatedRestartTimeoutMs)) + { + return ServiceControlResult.Fail( + $"Timed out waiting for '{displayName}' to restart with elevation."); + } + + return proc.ExitCode == 0 + ? ServiceControlResult.Ok(displayName) + : ServiceControlResult.Fail( + $"The elevated restart of '{displayName}' did not complete successfully."); + } + catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) // ERROR_CANCELLED + { + return ServiceControlResult.Fail( + $"Restarting '{displayName}' needs administrator approval, which was declined."); + } + catch (Exception ex) + { + _logger.Error(ex); + return ServiceControlResult.Fail($"Failed to restart '{displayName}' with elevation: {ex.Message}"); + } + } + + /// Returns whether the current process is running with administrator rights. + private static bool IsElevated() + { + try + { + using var identity = WindowsIdentity.GetCurrent(); + return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); + } + catch + { + return false; + } + } + + /// Returns whether an exception (or any inner exception) is a Win32 access-denied error. + private static bool IsAccessDenied(Exception ex) + { + for (Exception current = ex; current != null; current = current.InnerException) + { + if (current is Win32Exception win32 && win32.NativeErrorCode == 5) // ERROR_ACCESS_DENIED + { + return true; + } + } + + return false; + } + + /// + /// Stops the service (and any running dependents) and starts it again, waiting for each + /// state transition to complete. + /// + private static void Restart(ServiceController controller) + { + // Dependent services must be stopped before the target service can stop. + // Remember which ones we stopped so we can restore them after restarting. + var dependentNamesToRestart = new List(); + foreach (var dependent in controller.DependentServices) + { + if (dependent.Status != ServiceControllerStatus.Stopped) + { + dependentNamesToRestart.Add(dependent.ServiceName); + dependent.Stop(); + dependent.WaitForStatus(ServiceControllerStatus.Stopped, StatusTimeout); + } + } + + controller.Refresh(); + if (controller.Status != ServiceControllerStatus.Stopped) + { + if (!controller.CanStop) + { + throw new InvalidOperationException( + $"Service '{controller.ServiceName}' cannot be stopped in its current state ({controller.Status})."); + } + + controller.Stop(); + controller.WaitForStatus(ServiceControllerStatus.Stopped, StatusTimeout); + } + + controller.Start(); + controller.WaitForStatus(ServiceControllerStatus.Running, StatusTimeout); + + foreach (var name in dependentNamesToRestart) + { + using var dependent = new ServiceController(name); + dependent.Refresh(); + if (dependent.Status != ServiceControllerStatus.Running) + { + dependent.Start(); + dependent.WaitForStatus(ServiceControllerStatus.Running, StatusTimeout); + } + } + } + + /// + /// Enumerates all installed services (name and display name only) for matching. + /// + private static List EnumerateServices() + { + var services = ServiceController.GetServices(); + try + { + var list = new List(services.Length); + foreach (var sc in services) + { + list.Add(new ServiceInfo(sc.ServiceName, sc.DisplayName, null)); + } + + return list; + } + finally + { + foreach (var sc in services) + { + sc.Dispose(); + } + } + } + + /// + /// Enumerates all installed services including their descriptions (via WMI) for matching. + /// + private static List EnumerateServicesWithDescriptions() + { + var list = new List(); + using var searcher = new ManagementObjectSearcher( + "SELECT Name, DisplayName, Description FROM Win32_Service"); + using var results = searcher.Get(); + foreach (ManagementBaseObject service in results) + { + using (service) + { + list.Add(new ServiceInfo( + service["Name"] as string, + service["DisplayName"] as string, + service["Description"] as string)); + } + } + + return list; + } +} diff --git a/dotnet/autoShell/autoShell.csproj b/dotnet/autoShell/autoShell.csproj index c4a157d11..c2a6f3b0f 100644 --- a/dotnet/autoShell/autoShell.csproj +++ b/dotnet/autoShell/autoShell.csproj @@ -30,6 +30,7 @@ + diff --git a/ts/.gitignore b/ts/.gitignore index 4e80c68de..aed177c28 100644 --- a/ts/.gitignore +++ b/ts/.gitignore @@ -48,6 +48,14 @@ tools/scripts/code/debt-report/ # Holds local endpoints, deployment names, and (in Phase 1) secrets. # Never commit. config.local.yaml +config.local.yaml.bak + +# Per-developer npm registry config — points pnpm at the internal +# "typeagent-feed" Azure Artifacts feed. Provisioned via `npm run getNPMRC` +# from Azure Key Vault; never committed (internal feed URL, and external +# contributors use their own registry). Root-anchored so packages/*/.npmrc +# stay tracked. +/.npmrc # Per-developer npm registry config — points pnpm at the internal # "typeagent-feed" Azure Artifacts feed. Provisioned via `npm run getNPMRC` diff --git a/ts/.vscode/extensions.json b/ts/.vscode/extensions.json index 458b66e37..3e6d07f35 100644 --- a/ts/.vscode/extensions.json +++ b/ts/.vscode/extensions.json @@ -4,6 +4,7 @@ "Orta.vscode-jest", "ms-playwright.playwright", "aisystems.agent-coda", - "typeagent.vscode-shell" + "typeagent.vscode-shell", + "typeagent.vscode-chat" ] } diff --git a/ts/.vscode/settings.json b/ts/.vscode/settings.json index 5060fc917..45fa995ab 100644 --- a/ts/.vscode/settings.json +++ b/ts/.vscode/settings.json @@ -53,11 +53,26 @@ }, "files.watcherExclude": { "**/node_modules/**": true, - "**/.git/objects/**": true, - "**/dist/**": true + "**/.git/**": true, + "**/.pnpm/**": true, + "**/dist/**": true, + "**/build/**": true, + "**/out/**": true, + "**/coverage/**": true, + "**/*.tsbuildinfo": true }, "search.exclude": { "**/node_modules": true, - "**/dist": true - } + "**/.pnpm": true, + "**/dist": true, + "**/build": true, + "**/out": true, + "**/coverage": true, + "**/*.tsbuildinfo": true, + "**/pnpm-lock.yaml": true + }, + "search.followSymlinks": false, + "search.useIgnoreFiles": true, + "search.useParentIgnoreFiles": true, + "typeagent.serverStartCommand": "pnpm --filter agent-server start" } \ No newline at end of file diff --git a/ts/.vscode/tasks.json b/ts/.vscode/tasks.json index 5e3ec64e6..f980da175 100644 --- a/ts/.vscode/tasks.json +++ b/ts/.vscode/tasks.json @@ -55,14 +55,25 @@ "label": "TypeAgent: Install VS Code Shell extension (local)", "detail": "Package and install the vscode-shell extension from packages/vscode-shell" }, + { + "type": "shell", + "command": "pnpm run deploy:local", + "options": { + "cwd": "${workspaceFolder}/packages/vscode-chat" + }, + "problemMatcher": [], + "label": "TypeAgent: Install Chat extension (local)", + "detail": "Package and install the vscode-chat extension from packages/vscode-chat" + }, { "type": "shell", "label": "TypeAgent: Install local extensions", - "detail": "Install both the Coda and VS Code Shell extensions from this workspace", + "detail": "Install the Coda, VS Code Shell, and Chat extensions from this workspace", "dependsOrder": "sequence", "dependsOn": [ "TypeAgent: Install Coda extension (local)", - "TypeAgent: Install VS Code Shell extension (local)" + "TypeAgent: Install VS Code Shell extension (local)", + "TypeAgent: Install Chat extension (local)" ], "problemMatcher": [] }, diff --git a/ts/package.json b/ts/package.json index 2b00d846c..864902862 100644 --- a/ts/package.json +++ b/ts/package.json @@ -71,6 +71,7 @@ "shell:start:package": "pnpm -C packages/shell run start:package", "shell:test": "pnpm -C packages/shell run shell:test", "start:agent-server": "pnpm -C packages/agentServer/server run start", + "start:agent-server:dev": "pnpm -C packages/agentServer/server run start:dev", "start:agent-server:tunnel": "pnpm -C packages/agentServer/server run start:tunnel", "start:mcp": "pnpm -C packages/commandExecutor run start", "stop:agent-server": "pnpm -C packages/agentServer/server run stop", @@ -123,7 +124,9 @@ "exifreader", "keytar", "koffi", + "node-pty", "onnxruntime-node", + "protobufjs", "puppeteer", "sharp" ], diff --git a/ts/packages/agentServer/client/src/agentServerClient.ts b/ts/packages/agentServer/client/src/agentServerClient.ts index e345082b0..5beadf58b 100644 --- a/ts/packages/agentServer/client/src/agentServerClient.ts +++ b/ts/packages/agentServer/client/src/agentServerClient.ts @@ -28,6 +28,7 @@ import { ConversationInfo, JoinConversationResult, RenameConversationOptions, + SpeechToken, getDispatcherChannelName, getClientIOChannelName, } from "@typeagent/agent-server-protocol"; @@ -119,6 +120,12 @@ export type AgentServerConnection = { ): Promise; deleteConversation(conversationId: string): Promise; shutdown(): Promise; + /** + * Request a short-lived Azure Speech authorization token from the server + * (which owns the `speech:` config). Resolves to `undefined` when speech + * isn't configured, so callers can hide/disable the mic affordance. + */ + getSpeechToken(): Promise; /** * Reopen the underlying transport and rebind the control rpc onto it, * reusing this connection object instead of building a new one. Returns @@ -316,6 +323,10 @@ export function createAgentServerConnection( await rpc.invoke("shutdown"); }, + async getSpeechToken(): Promise { + return rpc.invoke("getSpeechToken"); + }, + async reconnect(): Promise { if (closed || reopenTransport === undefined) { return false; diff --git a/ts/packages/agentServer/client/src/index.ts b/ts/packages/agentServer/client/src/index.ts index ccafe2a98..2b1f46f1e 100644 --- a/ts/packages/agentServer/client/src/index.ts +++ b/ts/packages/agentServer/client/src/index.ts @@ -25,6 +25,7 @@ export type { ConversationInfo, JoinConversationResult, DispatcherConnectOptions, + SpeechToken, } from "@typeagent/agent-server-protocol"; export { AGENT_SERVER_DEFAULT_PORT, diff --git a/ts/packages/agentServer/client/test/conversation-stubConnection.ts b/ts/packages/agentServer/client/test/conversation-stubConnection.ts index 782320b0c..e377976c4 100644 --- a/ts/packages/agentServer/client/test/conversation-stubConnection.ts +++ b/ts/packages/agentServer/client/test/conversation-stubConnection.ts @@ -202,6 +202,9 @@ export function makeStubConnection( }, async shutdown() {}, + async getSpeechToken() { + return undefined; + }, async reconnect() { return true; }, diff --git a/ts/packages/agentServer/protocol/src/index.ts b/ts/packages/agentServer/protocol/src/index.ts index e7c978707..a94c6e73a 100644 --- a/ts/packages/agentServer/protocol/src/index.ts +++ b/ts/packages/agentServer/protocol/src/index.ts @@ -20,6 +20,7 @@ export { RenameConversationOptions, UserIdentity, DefaultUserIdentity, + SpeechToken, getDispatcherChannelName, getClientIOChannelName, registerClientType, diff --git a/ts/packages/agentServer/protocol/src/protocol.ts b/ts/packages/agentServer/protocol/src/protocol.ts index 70cbf3cf9..3967f2e07 100644 --- a/ts/packages/agentServer/protocol/src/protocol.ts +++ b/ts/packages/agentServer/protocol/src/protocol.ts @@ -77,6 +77,21 @@ export type UserIdentity = { initial: string; // Single uppercase character for avatars }; +/** + * Short-lived Azure Speech authorization token, vended by the server (which + * owns the `speech:` config) to clients that render a microphone affordance + * (e.g. the VS Code shell webview). Clients build a + * `SpeechConfig.fromAuthorizationToken("aad##", region)` from + * these fields. The token expires after ~10 minutes; `expire` is the ms-since- + * epoch at which callers should request a fresh one. + */ +export type SpeechToken = { + token: string; + expire: number; // ms since epoch + region: string; + endpoint: string; +}; + export type AgentServerInvokeFunctions = { joinConversation: ( options?: DispatcherConnectOptions, @@ -95,6 +110,13 @@ export type AgentServerInvokeFunctions = { deleteConversation: (conversationId: string) => Promise; shutdown: () => Promise; getUserIdentity: () => Promise; + /** + * Vend a short-lived Azure Speech authorization token from the server's + * `speech:` config. Returns `undefined` when speech is not configured or + * a token can't be acquired, so clients can gracefully hide/disable the + * mic affordance. + */ + getSpeechToken: () => Promise; }; /** diff --git a/ts/packages/agentServer/server/package.json b/ts/packages/agentServer/server/package.json index fd89716d6..4f9c67e86 100644 --- a/ts/packages/agentServer/server/package.json +++ b/ts/packages/agentServer/server/package.json @@ -31,6 +31,7 @@ "prettier": "prettier --check . --ignore-path ../../../.prettierignore", "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", "start": "node --disable-warning=DEP0190 dist/server.js", + "start:dev": "node --disable-warning=DEP0190 dist/server.js --dev", "start:tunnel": "node --disable-warning=DEP0190 dist/startWithTunnel.js", "stop": "node dist/stop.js", "test": "npm run test:local", diff --git a/ts/packages/agentServer/server/src/connectionHandler.ts b/ts/packages/agentServer/server/src/connectionHandler.ts index 9034b726a..2ee19f4c6 100644 --- a/ts/packages/agentServer/server/src/connectionHandler.ts +++ b/ts/packages/agentServer/server/src/connectionHandler.ts @@ -20,6 +20,7 @@ import type { Dispatcher } from "agent-dispatcher"; import type { PortRegistrar } from "agent-dispatcher"; import type { ConversationManager } from "./conversationManager.js"; import { resolveTunnelUrlForDiscovery } from "./tunnelResolver.js"; +import { getSpeechToken } from "./speechToken.js"; /** * Per-connection handler signature expected by transports (the WebSocket @@ -218,6 +219,7 @@ export function createAgentServerConnectionHandler( await shutdown(); }, getUserIdentity: async () => getUserIdentity(), + getSpeechToken: async () => getSpeechToken(), }; // Clean up all conversations on disconnect diff --git a/ts/packages/agentServer/server/src/server.ts b/ts/packages/agentServer/server/src/server.ts index 0d230b541..7584713dd 100644 --- a/ts/packages/agentServer/server/src/server.ts +++ b/ts/packages/agentServer/server/src/server.ts @@ -150,6 +150,17 @@ async function main() { const configName = configIdx !== -1 ? process.argv[configIdx + 1] : undefined; + // `--dev` (or TYPEAGENT_DEV=1) starts every conversation with developer + // mode enabled — captures translation debug data and shows dev-only UI + // affordances (per-message delete) without needing `@config dev on`. + const developerMode = + process.argv.includes("--dev") || + process.env.TYPEAGENT_DEV === "1" || + process.env.TYPEAGENT_DEV === "true"; + if (developerMode) { + debugStartup("developer mode enabled at startup (--dev)"); + } + debugStartup("creating conversation manager (will lockInstanceDir)"); // Single PortRegistrar shared across every conversation in this // process. Lets external clients (browser extension, VS Code, CLI) @@ -174,6 +185,7 @@ async function main() { storageProvider: getFsStorageProvider(), metrics: true, dblogging: true, + developerMode, traceId, indexingServiceRegistry: await getIndexingServiceRegistry( instanceDir, diff --git a/ts/packages/agentServer/server/src/speechToken.ts b/ts/packages/agentServer/server/src/speechToken.ts new file mode 100644 index 000000000..571322630 --- /dev/null +++ b/ts/packages/agentServer/server/src/speechToken.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Server-side Azure Speech authorization-token vending. + * + * The agent-server owns the `speech:` configuration (loaded into the + * `SPEECH_SDK_KEY` / `SPEECH_SDK_REGION` / `SPEECH_SDK_ENDPOINT` env vars by + * `@typeagent/config`). Thin clients that render a microphone affordance but + * have no config access (e.g. the VS Code shell webview) request a token over + * the agent-server RPC and use it with the browser Speech SDK. + * + * Auth mirrors the Electron shell's `AzureSpeech`: + * - `identity` (default): acquire an Azure AD token for the Cognitive + * Services scope via `DefaultAzureCredential`; clients pass it as + * `aad##`. + * - a subscription key: exchange it for a short-lived token at the region's + * STS `issuetoken` endpoint. + */ + +import { DefaultAzureCredential } from "@azure/identity"; +import type { SpeechToken } from "@typeagent/agent-server-protocol"; +import registerDebug from "debug"; + +const debug = registerDebug("agent-server:speech"); +const debugError = registerDebug("agent-server:speech:error"); + +const IdentityApiKey = "identity"; +const CogServicesScope = "https://cognitiveservices.azure.com/.default"; + +// Process-wide cache. Tokens live ~10 minutes; refresh a minute early. +let cachedToken: SpeechToken | undefined; + +/** + * Return a valid Azure Speech authorization token, or `undefined` when speech + * is not configured (no region) or a token can't be acquired. Results are + * cached until shortly before expiry. + */ +export async function getSpeechToken(): Promise { + const region = process.env["SPEECH_SDK_REGION"]; + if (!region) { + // Speech not configured — clients hide the mic affordance. + return undefined; + } + if (cachedToken !== undefined && cachedToken.expire > Date.now()) { + return cachedToken; + } + + const key = process.env["SPEECH_SDK_KEY"] ?? IdentityApiKey; + // Only identity/AAD tokens use the `aad##` prefix; key-issued STS + // tokens must be passed verbatim, so keep endpoint empty for key-based auth. + // Identity (AAD) tokens must include an endpoint so clients can format + // `aad##` for the Speech SDK. + const endpoint = + key.toLowerCase() === IdentityApiKey + ? (process.env["SPEECH_SDK_ENDPOINT"] ?? "") + : ""; + if (key.toLowerCase() === IdentityApiKey && !endpoint) { + debugError( + "identity-based speech tokens require SPEECH_SDK_ENDPOINT to be set", + ); + return undefined; + } + try { + let token: string; + if (key.toLowerCase() === IdentityApiKey) { + if (!endpoint) { + debugError( + "identity-based speech token acquisition requires SPEECH_SDK_ENDPOINT", + ); + return undefined; + } + const result = await new DefaultAzureCredential().getToken( + CogServicesScope, + ); + if (!result?.token) { + debugError( + "identity-based speech token acquisition returned no token", + ); + return undefined; + } + token = result.token; + } else { + const response = await fetch( + `https://${region}.api.cognitive.microsoft.com/sts/v1.0/issuetoken`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Ocp-Apim-Subscription-Key": key, + }, + }, + ); + if (!response.ok) { + debugError( + `key-based speech token request failed: ${response.status} ${response.statusText}`, + ); + return undefined; + } + token = await response.text(); + } + + cachedToken = { + token, + // Token is valid for 10 minutes; expire our cache at 9 so callers + // always receive a token with headroom. + expire: Date.now() + 9 * 60 * 1000, + region, + endpoint, + }; + debug("issued speech token (region=%s)", region); + return cachedToken; + } catch (e) { + debugError("error acquiring speech token", e); + return undefined; + } +} diff --git a/ts/packages/agents/browser/src/extension/serviceWorker/dispatcherConnection.ts b/ts/packages/agents/browser/src/extension/serviceWorker/dispatcherConnection.ts index 5bafefb0b..e07d9d879 100644 --- a/ts/packages/agents/browser/src/extension/serviceWorker/dispatcherConnection.ts +++ b/ts/packages/agents/browser/src/extension/serviceWorker/dispatcherConnection.ts @@ -569,6 +569,10 @@ function makeConnectionAdapter(): AgentServerConnection { return rpc.invoke("deleteConversation", id) as Promise; }, shutdown: () => notSupported("shutdown"), + getSpeechToken: () => { + const { rpc } = requireFresh(); + return rpc.invoke("getSpeechToken"); + }, // In-place rebind reconnect isn't driven through this adapter; the // service worker reconnects via its own doConnect path. reconnect: async () => false, diff --git a/ts/packages/agents/desktop/src/actionHandler.ts b/ts/packages/agents/desktop/src/actionHandler.ts index 0b0700fb7..c29bccdac 100644 --- a/ts/packages/agents/desktop/src/actionHandler.ts +++ b/ts/packages/agents/desktop/src/actionHandler.ts @@ -91,6 +91,12 @@ async function runAction( action.schemaName, // Pass schema name for disambiguation ); if (result.success) { + if (isServiceConfirmationRequest(result.data)) { + return offerServiceConfirmation(context, action, result.data); + } + if (isServiceElevationRequest(result.data)) { + return offerServiceElevation(context, action, result.data); + } const displayText = formatResultDisplay( action.actionName, result.message, @@ -101,6 +107,145 @@ async function runAction( return { error: result.message }; } +// Shape of the confirmation payload emitted by autoShell when a service action +// resolves to a fuzzy (non-exact) match and needs the user to confirm the target. +type ServiceConfirmationData = { + needsConfirmation: true; + resolvedServiceName: string; + resolvedDisplayName: string; + operation?: string; +}; + +function isServiceConfirmationRequest( + data: unknown, +): data is ServiceConfirmationData { + if (typeof data !== "object" || data === null) { + return false; + } + const d = data as Record; + return ( + d.needsConfirmation === true && + typeof d.resolvedServiceName === "string" && + typeof d.resolvedDisplayName === "string" + ); +} + +// When autoShell only fuzzily matches a service, ask the user to confirm the resolved +// service before acting. On confirmation, re-run the original action targeting the +// resolved service by its exact name — which matches exactly, so autoShell performs the +// operation without prompting again. +function offerServiceConfirmation( + context: ActionContext, + action: AppAction, + data: ServiceConfirmationData, +): ActionResult { + const state = context.sessionContext.agentContext; + const operation = data.operation ?? "restart"; + const requested = action.parameters?.service; + const query = typeof requested === "string" ? requested : ""; + return createYesNoChoiceResult( + state.choiceManager, + `I couldn't find a service exactly matching "${query}". ` + + `The closest match is "${data.resolvedDisplayName}" (${data.resolvedServiceName}). ` + + `Should I ${operation} it?`, + async (confirmed: boolean) => { + if (!confirmed) { + return createActionResultFromTextDisplay( + "Cancelled — no service was changed.", + ); + } + const ctx = state.pendingChoiceContext ?? context; + const confirmedAction: AppAction = { + ...action, + parameters: { + ...action.parameters, + service: data.resolvedServiceName, + matchBy: "name", + }, + }; + try { + return await runAction(confirmedAction, ctx); + } catch (e) { + if (e instanceof AutoShellMissingError) { + return offerAutoShellBuild(ctx, e, confirmedAction); + } + return createActionResultFromError( + e instanceof Error ? e.message : String(e), + ); + } + }, + ); +} + +// Shape of the payload emitted by autoShell when a service restart requires administrator +// privileges that the (non-elevated) helper doesn't have. +type ServiceElevationData = { + needsElevation: true; + resolvedServiceName: string; + resolvedDisplayName: string; + operation?: string; +}; + +function isServiceElevationRequest( + data: unknown, +): data is ServiceElevationData { + if (typeof data !== "object" || data === null) { + return false; + } + const d = data as Record; + return ( + d.needsElevation === true && + typeof d.resolvedServiceName === "string" && + typeof d.resolvedDisplayName === "string" + ); +} + +// Controlling a Windows service requires administrator rights, and autoShell runs unelevated. +// Ask the user for consent before running the restart elevated. On confirmation, re-run the +// action with `elevate: true`; autoShell then performs the restart via an elevated helper, which +// triggers a Windows User Account Control (UAC) prompt. +function offerServiceElevation( + context: ActionContext, + action: AppAction, + data: ServiceElevationData, +): ActionResult { + const state = context.sessionContext.agentContext; + const operation = data.operation ?? "restart"; + return createYesNoChoiceResult( + state.choiceManager, + `Restarting the "${data.resolvedDisplayName}" (${data.resolvedServiceName}) service ` + + `requires administrator privileges. I'll run the ${operation} with elevation, which ` + + `shows a Windows User Account Control (UAC) prompt. Do you want to continue?`, + async (confirmed: boolean) => { + if (!confirmed) { + return createActionResultFromTextDisplay( + `Cancelled — no changes were made to the "${data.resolvedDisplayName}" service.`, + ); + } + const ctx = state.pendingChoiceContext ?? context; + const elevatedAction: AppAction = { + ...action, + parameters: { + ...action.parameters, + service: data.resolvedServiceName, + matchBy: "name", + elevate: true, + }, + }; + try { + return await runAction(elevatedAction, ctx); + } catch (e) { + if (e instanceof AutoShellMissingError) { + return offerAutoShellBuild(ctx, e, elevatedAction); + } + return createActionResultFromError( + e instanceof Error ? e.message : String(e), + ); + } + }, + ); +} + function offerAutoShellBuild( context: ActionContext, error: AutoShellMissingError, diff --git a/ts/packages/agents/desktop/src/actionsSchema.ts b/ts/packages/agents/desktop/src/actionsSchema.ts index 589be0596..059bc8bb4 100644 --- a/ts/packages/agents/desktop/src/actionsSchema.ts +++ b/ts/packages/agents/desktop/src/actionsSchema.ts @@ -28,6 +28,7 @@ export type DesktopActions = | PreviousDesktopAction | ToggleNotificationsAction | DebugAutoShellAction + | RestartServiceAction | SetTextSizeAction | SetScreenResolutionAction // Common settings actions @@ -254,6 +255,35 @@ export type DebugAutoShellAction = { parameters: {}; }; +// Restarts a Windows service (stops it and starts it again). The target can be +// identified either by its service name / display name, or by a phrase found in +// the service's description. +// +// Example: +// User: restart the print spooler service +// Agent: { actionName: "RestartService", parameters: { service: "Print Spooler", matchBy: "name" } } +// +// Example: +// User: restart the service that manages windows updates +// Agent: { actionName: "RestartService", parameters: { service: "windows update", matchBy: "description" } } +export type RestartServiceAction = { + actionName: "RestartService"; + parameters: { + // The Windows service name or display name (e.g. "Spooler" or "Print + // Spooler") when matchBy is "name"; or a phrase to search for within + // service descriptions when matchBy is "description". + service: string; + // How to locate the service. "name" (the default) matches the service + // name or display name; "description" searches each service's + // description text for the provided phrase. + matchBy?: "name" | "description"; + // Internal use only: set to true by the agent after the user has agreed to + // run the restart with administrator privileges. Never set this from the + // user's initial request — leave it unset so the agent asks for consent first. + elevate?: boolean; + }; +}; + // Changes the text size that appears throughout Windows and your apps export type SetTextSizeAction = { actionName: "SetTextSize"; diff --git a/ts/packages/agents/desktop/src/desktopSchema.agr b/ts/packages/agents/desktop/src/desktopSchema.agr index 662b9bcf2..453e1a0ff 100644 --- a/ts/packages/agents/desktop/src/desktopSchema.agr +++ b/ts/packages/agents/desktop/src/desktopSchema.agr @@ -14,7 +14,8 @@ import { DesktopActions } from "./actionsSchema.ts"; | | | - | ; + | + | ; // ===== Known Programs (for completions and priority matching) ===== // These are matched with higher priority than wildcards @@ -146,3 +147,13 @@ import { DesktopActions } from "./actionsSchema.ts"; | make (the)? screen dimmer -> { actionName: "AdjustScreenBrightness", parameters: { brightnessLevel: "decrease" } } | dim (the)? screen -> { actionName: "AdjustScreenBrightness", parameters: { brightnessLevel: "decrease" } } | brighten (the)? screen -> { actionName: "AdjustScreenBrightness", parameters: { brightnessLevel: "increase" } }; + +// ===== Windows Service Control ===== +// "restart the service" matches by service name / display name. +// "restart the service that " matches by a phrase in the service description. + = + restart (the)? $(service:wildcard) service -> { actionName: "RestartService", parameters: { service: service, matchBy: "name" } } + | restart (the)? $(service:wildcard) windows service -> { actionName: "RestartService", parameters: { service: service, matchBy: "name" } } + | restart service $(service:wildcard) -> { actionName: "RestartService", parameters: { service: service, matchBy: "name" } } + | bounce (the)? $(service:wildcard) service -> { actionName: "RestartService", parameters: { service: service, matchBy: "name" } } + | restart (the)? service (with description|described as|whose description is) $(service:wildcard) -> { actionName: "RestartService", parameters: { service: service, matchBy: "description" } }; diff --git a/ts/packages/agents/github-cli/src/github-cliActionHandler.ts b/ts/packages/agents/github-cli/src/github-cliActionHandler.ts index 7825a278d..c5604d5f2 100644 --- a/ts/packages/agents/github-cli/src/github-cliActionHandler.ts +++ b/ts/packages/agents/github-cli/src/github-cliActionHandler.ts @@ -352,8 +352,58 @@ async function runGh(args: string[], timeoutMs = 30_000): Promise { return stdout.trim(); } +// Sentinel values that mean "no assignee". `gh issue list --assignee ` +// treats as a literal GitHub login, so "--assignee none" fails with +// "Could not find an assignee with the login 'none'". The supported way to +// list unassigned items is the search qualifier `--search "no:assignee"`. +const UNASSIGNED_SENTINELS = new Set([ + "none", + "unassigned", + "nobody", + "noone", + "no one", + "no-one", +]); + +function isUnassignedAssignee(assignee: string): boolean { + return UNASSIGNED_SENTINELS.has( + assignee.trim().toLowerCase().replace(/^@/, ""), + ); +} + +// Build args for `gh issue list` / `gh pr list`. `state`, `label`, and a +// concrete `assignee` map to the matching flags. "Unassigned" has no flag +// equivalent — `gh ... list --assignee none` treats "none" as a literal +// login and fails with "Could not find an assignee with the login 'none'" — +// so it maps to the `no:assignee` search qualifier instead. +// +// gh honors `--search` alongside `--state` and `--label`, so a request like +// "open unassigned issues labeled X" composes to +// `--state open --label X --search no:assignee` and filters on all three. +function buildListArgs( + kind: "issue" | "pr", + p: Record, + jsonFields: string, +): string[] { + const args = [kind, "list"]; + if (p.repo) args.push("--repo", String(p.repo)); + if (p.state) args.push("--state", String(p.state)); + if (p.label) args.push("--label", String(p.label)); + + const assignee = p.assignee ? String(p.assignee) : ""; + if (isUnassignedAssignee(assignee)) { + args.push("--search", "no:assignee"); + } else if (assignee) { + args.push("--assignee", assignee); + } + + if (p.limit) args.push("--limit", String(p.limit)); + args.push("--json", jsonFields); + return args; +} + // Build gh CLI args from an action name and parameters. -function buildArgs( +export function buildArgs( action: TypeAgentAction, ): string[] | undefined { const p = action.parameters as Record; @@ -462,16 +512,12 @@ function buildArgs( if (p.repo) args.push("--repo", String(p.repo)); return args; } - case "issueList": { - const args = ["issue", "list"]; - if (p.repo) args.push("--repo", String(p.repo)); - if (p.state) args.push("--state", String(p.state)); - if (p.label) args.push("--label", String(p.label)); - if (p.assignee) args.push("--assignee", String(p.assignee)); - if (p.limit) args.push("--limit", String(p.limit)); - args.push("--json", "number,title,state,url,createdAt,labels"); - return args; - } + case "issueList": + return buildListArgs( + "issue", + p, + "number,title,state,url,createdAt,labels", + ); case "issueView": { const args = ["issue", "view"]; if (p.number) args.push(String(p.number)); @@ -513,19 +559,12 @@ function buildArgs( if (p.mergeMethod) args.push(`--${String(p.mergeMethod)}`); return args; } - case "prList": { - const args = ["pr", "list"]; - if (p.repo) args.push("--repo", String(p.repo)); - if (p.state) args.push("--state", String(p.state)); - if (p.label) args.push("--label", String(p.label)); - if (p.assignee) args.push("--assignee", String(p.assignee)); - if (p.limit) args.push("--limit", String(p.limit)); - args.push( - "--json", + case "prList": + return buildListArgs( + "pr", + p, "number,title,state,url,createdAt,headRefName,isDraft", ); - return args; - } case "prView": { const args = ["pr", "view"]; if (p.number) args.push(String(p.number)); diff --git a/ts/packages/agents/github-cli/src/github-cliSchema.agr b/ts/packages/agents/github-cli/src/github-cliSchema.agr index 272bccae3..ab8592661 100644 --- a/ts/packages/agents/github-cli/src/github-cliSchema.agr +++ b/ts/packages/agents/github-cli/src/github-cliSchema.agr @@ -63,7 +63,22 @@ } }; - = show open issues in $(repo:wildcard) -> { + = show open unassigned issues in $(repo:wildcard) -> { + actionName: "issueList", + parameters: { + repo, + state: "open", + assignee: "none" + } +} + | list unassigned issues in $(repo:wildcard) -> { + actionName: "issueList", + parameters: { + repo, + assignee: "none" + } +} + | show open issues in $(repo:wildcard) -> { actionName: "issueList", parameters: { repo, diff --git a/ts/packages/agents/github-cli/src/github-cliSchema.ts b/ts/packages/agents/github-cli/src/github-cliSchema.ts index 6b6e0d9ab..690ba9f5b 100644 --- a/ts/packages/agents/github-cli/src/github-cliSchema.ts +++ b/ts/packages/agents/github-cli/src/github-cliSchema.ts @@ -213,6 +213,8 @@ export type IssueListAction = { label?: string; + // Filter by assignee: a GitHub login (e.g. "octocat"), "@me" for the + // current user, or "none" to list only unassigned issues. assignee?: string; limit?: number; @@ -293,6 +295,8 @@ export type PrListAction = { label?: string; + // Filter by assignee: a GitHub login (e.g. "octocat"), "@me" for the + // current user, or "none" to list only unassigned pull requests. assignee?: string; limit?: number; diff --git a/ts/packages/agents/github-cli/test/githubCliBuildArgs.spec.ts b/ts/packages/agents/github-cli/test/githubCliBuildArgs.spec.ts new file mode 100644 index 000000000..e3bd570b0 --- /dev/null +++ b/ts/packages/agents/github-cli/test/githubCliBuildArgs.spec.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Tests for gh CLI argument construction — specifically the "unassigned" + * filter. `gh issue list --assignee ` treats as a literal GitHub + * login, so "--assignee none" fails with "Could not find an assignee with + * the login 'none'". Unassigned items must instead be filtered with the + * search qualifier `--search "no:assignee"`. + */ + +import { buildArgs } from "../src/github-cliActionHandler.js"; +import type { TypeAgentAction } from "@typeagent/agent-sdk"; +import type { GithubCliActions } from "../src/github-cliSchema.js"; + +function action( + actionName: string, + parameters: Record, +): TypeAgentAction { + return { + schemaName: "github-cli", + actionName, + parameters, + } as unknown as TypeAgentAction; +} + +describe("buildArgs — issueList assignee handling", () => { + test("maps assignee 'none' to the no:assignee search qualifier", () => { + const args = buildArgs( + action("issueList", { + repo: "microsoft/TypeAgent", + state: "open", + assignee: "none", + }), + )!; + expect(args.join(" ")).toContain("--search no:assignee"); + // Must NOT pass the invalid literal login. + expect(args).not.toContain("--assignee"); + // State filtering still applies alongside the search qualifier. + expect(args.join(" ")).toContain("--state open"); + }); + + test.each(["none", "@none", "unassigned", "nobody", "NONE", " none "])( + "treats %j as unassigned", + (value) => { + const args = buildArgs( + action("issueList", { repo: "o/r", assignee: value }), + )!; + expect(args.join(" ")).toContain("--search no:assignee"); + expect(args).not.toContain("--assignee"); + }, + ); + + test("composes state + label filters alongside the unassigned search", () => { + const args = buildArgs( + action("issueList", { + repo: "o/r", + state: "open", + label: "bug", + assignee: "none", + }), + )!; + // gh honors --state and --label together with --search, so all three + // filters must be present (this is the label+unassigned combination). + const joined = args.join(" "); + expect(joined).toContain("--state open"); + expect(joined).toContain("--label bug"); + expect(joined).toContain("--search no:assignee"); + expect(args).not.toContain("--assignee"); + }); + + test("passes a real login through as --assignee", () => { + const args = buildArgs( + action("issueList", { repo: "o/r", assignee: "octocat" }), + )!; + expect(args).toContain("--assignee"); + expect(args).toContain("octocat"); + expect(args.join(" ")).not.toContain("no:assignee"); + }); + + test("keeps @me as an --assignee filter", () => { + const args = buildArgs( + action("issueList", { repo: "o/r", assignee: "@me" }), + )!; + expect(args).toContain("--assignee"); + expect(args).toContain("@me"); + expect(args.join(" ")).not.toContain("no:assignee"); + }); + + test("omits assignee filtering entirely when unset", () => { + const args = buildArgs( + action("issueList", { repo: "o/r", state: "open" }), + )!; + expect(args).not.toContain("--assignee"); + expect(args.join(" ")).not.toContain("no:assignee"); + }); +}); + +describe("buildArgs — prList assignee handling", () => { + test("maps assignee 'none' to the no:assignee search qualifier", () => { + const args = buildArgs( + action("prList", { + repo: "microsoft/TypeAgent", + state: "open", + assignee: "none", + }), + )!; + expect(args.join(" ")).toContain("--search no:assignee"); + expect(args).not.toContain("--assignee"); + }); + + test("passes a real login through as --assignee", () => { + const args = buildArgs( + action("prList", { repo: "o/r", assignee: "octocat" }), + )!; + expect(args).toContain("--assignee"); + expect(args).toContain("octocat"); + expect(args.join(" ")).not.toContain("no:assignee"); + }); + + test("composes state + label filters alongside the unassigned search", () => { + const args = buildArgs( + action("prList", { + repo: "o/r", + state: "open", + label: "bug", + assignee: "none", + }), + )!; + const joined = args.join(" "); + expect(joined).toContain("--state open"); + expect(joined).toContain("--label bug"); + expect(joined).toContain("--search no:assignee"); + expect(args).not.toContain("--assignee"); + }); +}); diff --git a/ts/packages/chat-ui/src/chatPanel.ts b/ts/packages/chat-ui/src/chatPanel.ts index 48ae6f24c..77384202c 100644 --- a/ts/packages/chat-ui/src/chatPanel.ts +++ b/ts/packages/chat-ui/src/chatPanel.ts @@ -27,6 +27,7 @@ import { FeedbackUIVariant, FeedbackWidget, } from "./feedbackWidget.js"; +import { createDeleteControl } from "./deleteControl.js"; import { ChatContextMenu } from "./contextMenu.js"; import { renderConnectionStatus, @@ -66,6 +67,7 @@ import type { SpeechState, TtsProvider, } from "./providers.js"; +import { createWebSpeechProvider } from "./webSpeechProvider.js"; import { openSettingsPopup, openHelpPopup } from "./popups.js"; import { TemplateEditor, type TemplateEditServices } from "./templateEditor.js"; import type { TemplateEditConfig } from "@typeagent/dispatcher-types"; @@ -175,6 +177,66 @@ export type HistoryEntry = } | { kind: "system"; text: string }; +/** + * Compute a human-friendly relative-time label for the history separator + * shown between replayed session history and new live messages (e.g. + * "a few minutes ago", "yesterday"). Uses the newest timestamp among the + * given entries; falls back to "earlier" when none carry a timestamp. + * + * Entries carry a numeric epoch-ms `timestamp` (the dispatcher display-log + * shape), which is distinct from `HistoryEntry.timestamp` (an ISO string). + */ +export function formatHistorySeparatorLabel( + entries: ReadonlyArray<{ timestamp?: number }>, +): string { + let newestTimestamp: number | undefined; + for (const entry of entries) { + if (typeof entry.timestamp !== "number") continue; + if ( + newestTimestamp === undefined || + entry.timestamp > newestTimestamp + ) { + newestTimestamp = entry.timestamp; + } + } + + if (newestTimestamp === undefined) { + return "earlier"; + } + + const diffMs = Math.max(0, Date.now() - newestTimestamp); + const diffMinutes = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMinutes < 2) { + return "a moment ago"; + } + if (diffMinutes < 10) { + return "a few minutes ago"; + } + if (diffMinutes < 60) { + return `${diffMinutes} minutes ago`; + } + if (diffHours < 2) { + return "an hour ago"; + } + if (diffHours < 6) { + return "a few hours ago"; + } + if (diffHours < 24) { + return `${diffHours} hours ago`; + } + if (diffDays < 2) { + return "yesterday"; + } + if (diffDays < 7) { + return `${diffDays} days ago`; + } + + return new Date(newestTimestamp).toLocaleDateString(); +} + function formatDuration(ms: number): string { if (ms < 1) return `${ms.toFixed(2)}ms`; if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; @@ -391,9 +453,10 @@ export interface ChatPanelOptions { feedbackUIVariant?: FeedbackUIVariant; /** - * Optional speech-to-text provider. When supplied, ChatPanel renders a - * microphone button (reflecting the provider's state) and a "listening" - * banner, and inserts recognized text into the input. + * Optional speech-to-text provider. The mic button renders by default + * using the browser's Web Speech API; supplying a provider overrides that + * default (e.g. the Electron shell's Azure/Whisper recognizer), driving + * the mic-button state, the "listening" banner, and recognized-text input. */ speechProvider?: SpeechInputProvider; /** @@ -402,9 +465,11 @@ export interface ChatPanelOptions { */ ttsProvider?: TtsProvider; /** - * Optional image-capture provider. When supplied, ChatPanel renders an - * attach-file button (if pickFile present) and a camera button (if - * openCamera present) that feed the next message's attachments. + * Optional image-capture provider. The attach-file button renders by + * default using a web-native hidden file picker; supplying `pickFile` + * overrides that with a host-native picker (e.g. the Electron dialog). + * When `openCamera` is present, an in-app camera button is also rendered. + * Both feed the next message's attachments. */ imageCaptureProvider?: ImageCaptureProvider; /** @@ -418,14 +483,16 @@ export interface ChatPanelOptions { */ helpPanel?: HelpPanelContent; /** - * Optional soft-hide ("trash") hook for the feedback widget. When - * supplied, the feedback footer shows a trash button that toggles the - * hidden state of the user or agent message for the given request. + * Optional developer-mode hook to delete a message. When supplied and + * developer mode is enabled (see `ChatPanel.setDeveloperMode`), each + * message bubble shows a split "delete" button: the primary action is a + * soft (recoverable) delete; the caret offers a permanent (hard) delete. + * `permanent` is `true` for the hard-delete choice. */ - onFeedbackHidden?: ( + onDeleteMessage?: ( requestId: RequestId, target: "user" | "agent", - hidden: boolean, + permanent: boolean, ) => void; } @@ -635,15 +702,19 @@ export class ChatPanel { private imageCaptureProvider?: ImageCaptureProvider; private settingsPanel?: SettingsPanelSchema; private helpPanel?: HelpPanelContent; - public onFeedbackHidden?: ( + public onDeleteMessage?: ( requestId: RequestId, target: "user" | "agent", - hidden: boolean, + permanent: boolean, ) => void; + private developerMode = false; // Input-bar affordances created only when the matching provider exists. private micButton?: HTMLButtonElement; private attachButton?: HTMLButtonElement; private cameraButton?: HTMLButtonElement; + // Hidden backing the web-native default attach button + // (used when the host doesn't supply an imageCaptureProvider.pickFile). + private fileInput?: HTMLInputElement; private voiceBanner?: HTMLDivElement; private lightboxOverlay?: HTMLDivElement; private lightboxKeyHandler?: (ev: KeyboardEvent) => void; @@ -667,7 +738,15 @@ export class ChatPanel { this.imageCaptureProvider = options.imageCaptureProvider; this.settingsPanel = options.settingsPanel; this.helpPanel = options.helpPanel; - this.onFeedbackHidden = options.onFeedbackHidden; + this.onDeleteMessage = options.onDeleteMessage; + + // Web-native default: when the host doesn't inject a speech provider, + // fall back to the browser's Web Speech API so the mic button works + // out of the box. Resolves to undefined where the API is unavailable, + // in which case the mic affordance is simply not rendered. + if (!this.speechProvider) { + this.speechProvider = createWebSpeechProvider(); + } // Build DOM structure const wrapper = document.createElement("div"); @@ -985,22 +1064,37 @@ export class ChatPanel { /** * Create the mic / attach / camera buttons and wire the speech - * provider's state callbacks. Only the affordances whose providers are - * present get rendered, so hosts that omit a provider see no change. + * provider's state callbacks. The attach-file and mic buttons render by + * default (web-native file picker + Web Speech API); the camera button + * renders only when the host supplies `imageCaptureProvider.openCamera`. */ private setupProviderAffordances() { - // Attach-file + camera buttons (image capture provider). - if (this.imageCaptureProvider?.pickFile) { - this.attachButton = document.createElement("button"); - this.attachButton.className = - "chat-input-button chat-attach-button"; - this.attachButton.title = "Attach image"; - this.attachButton.innerHTML = ``; - this.attachButton.addEventListener("click", () => - this.handleAttachFile(), - ); - this.inputArea.insertBefore(this.attachButton, this.sendButton); - } + // Attach-file button — always rendered. Uses the host's + // imageCaptureProvider.pickFile when supplied, otherwise a built-in + // web-native hidden (see handleAttachFile). + this.fileInput = document.createElement("input"); + this.fileInput.type = "file"; + this.fileInput.accept = "image/*"; + this.fileInput.multiple = true; + this.fileInput.style.display = "none"; + this.fileInput.addEventListener("change", () => { + this.handleFileDrop(this.fileInput?.files); + // Reset so re-selecting the same file still fires "change". + if (this.fileInput) this.fileInput.value = ""; + }); + this.inputArea.appendChild(this.fileInput); + + this.attachButton = document.createElement("button"); + this.attachButton.className = "chat-input-button chat-attach-button"; + this.attachButton.title = "Attach image"; + this.attachButton.innerHTML = ``; + this.attachButton.addEventListener("click", () => + this.handleAttachFile(), + ); + this.inputArea.insertBefore(this.attachButton, this.sendButton); + + // Camera button — only when the host supplies an in-app capture + // (there is no reliable web-native default for this yet). if (this.imageCaptureProvider?.openCamera) { this.cameraButton = document.createElement("button"); this.cameraButton.className = @@ -1089,13 +1183,20 @@ export class ChatPanel { } private async handleAttachFile() { - const urls = await this.imageCaptureProvider?.pickFile(); - if (urls && urls.length > 0) { - for (const url of urls) { - this.pendingAttachments.push(url); - this.showAttachmentPreview(url); + // Prefer a host-supplied native picker (e.g. the Electron dialog). + if (this.imageCaptureProvider?.pickFile) { + const urls = await this.imageCaptureProvider.pickFile(); + if (urls && urls.length > 0) { + for (const url of urls) { + this.pendingAttachments.push(url); + this.showAttachmentPreview(url); + } } + return; } + // Web-native default: open the hidden file input; the "change" + // handler reads the selection via handleFileDrop. + this.fileInput?.click(); } private async handleCameraCapture() { @@ -1607,6 +1708,69 @@ export class ChatPanel { return this.activeRequestId; } + /** + * Toggle the developer-mode per-message delete affordance. When enabled, + * every message bubble that carries a requestId shows a split "delete" + * button offering soft (recoverable) or permanent delete. No-op unless the + * host wired `onDeleteMessage`. + */ + public setDeveloperMode(enabled: boolean): void { + if (this.developerMode === enabled) { + return; + } + this.developerMode = enabled; + this.rootElement.classList.toggle("chat-developer-mode", enabled); + if (this.onDeleteMessage === undefined) { + return; + } + if (enabled) { + const containers = this.messageDiv.querySelectorAll( + ".chat-message-container-user[data-request-id]," + + ".chat-message-container-agent[data-request-id]", + ); + containers.forEach((c) => { + const target = c.classList.contains( + "chat-message-container-user", + ) + ? "user" + : "agent"; + this.attachDeleteControl(c, c.dataset.requestId!, target); + }); + } else { + this.messageDiv + .querySelectorAll(".chat-delete-control") + .forEach((el) => el.remove()); + } + } + + /** + * Attach the dev-mode delete split button to a message container, reading + * the requestId + target from it. No-op when developer mode is off, the + * host didn't wire `onDeleteMessage`, or a control is already present. + */ + private attachDeleteControl( + containerEl: HTMLElement, + requestId: string, + target: "user" | "agent", + ): void { + if (!this.developerMode || this.onDeleteMessage === undefined) { + return; + } + if (containerEl.querySelector(":scope > .chat-delete-control")) { + return; + } + const control = createDeleteControl((permanent) => { + containerEl.classList.add("chat-message-trashed"); + try { + this.onDeleteMessage?.({ requestId }, target, permanent); + } catch (e) { + containerEl.classList.remove("chat-message-trashed"); + console.error("onDeleteMessage callback failed", e); + } + }); + containerEl.appendChild(control); + } + /** * Display a user message bubble. * @@ -1856,6 +2020,9 @@ export class ChatPanel { const id = container.dataset.requestId!; this.userMessageById.set(id, container); + if (this.developerMode) { + this.attachDeleteControl(container, id, "user"); + } if (!isRemote) { if (!this.suppressFirstMessageTracking) { this.requestStartByRequestId.set(id, Date.now()); @@ -2131,6 +2298,10 @@ export class ChatPanel { anchor, ); this.threadContainers.set(threadId, container); + container.div.dataset.requestId = threadId; + if (this.developerMode) { + this.attachDeleteControl(container.div, threadId, "agent"); + } const requestContainers = this.requestAgentContainers.get(threadId) ?? []; requestContainers.push(container); @@ -3360,7 +3531,12 @@ export class ChatPanel { const finish = (value: unknown) => { setKeyHandler(undefined); clearButtons(); - actionContainer.remove(); + // Remove the whole confirmation bubble, not just the inner + // template editor. Removing only `actionContainer` left an + // empty agent container ("dispatcher" bubble) behind after + // Accept/Cancel/Replace. The action's own result renders in + // its own bubble when it runs. + container.remove(); resolve(value); }; @@ -4076,12 +4252,6 @@ export class ChatPanel { } }, }; - // Only expose the trash affordance when the host supplied a hide hook. - if (this.onFeedbackHidden) { - controller.setHidden = async (hidden, target) => { - this.onFeedbackHidden!(requestId, target ?? "agent", hidden); - }; - } container.attachFeedbackController(controller, this._feedbackUIVariant); const existing = this.feedbackByRequestId.get(threadId); if (existing) { diff --git a/ts/packages/chat-ui/src/deleteControl.ts b/ts/packages/chat-ui/src/deleteControl.ts new file mode 100644 index 000000000..74e0079c0 --- /dev/null +++ b/ts/packages/chat-ui/src/deleteControl.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { iconChevronDown, iconTrash } from "./icons.js"; + +/** + * Create a developer-mode "delete message" split button. + * + * The primary button performs a soft delete (a recoverable "move to trash"); + * the caret opens a small menu that lets the user choose between the same soft + * delete and a permanent (hard) delete. `onDelete(permanent)` is invoked with + * the chosen mode. + */ +export function createDeleteControl( + onDelete: (permanent: boolean) => void, +): HTMLElement { + const root = document.createElement("div"); + root.className = "chat-message-actions chat-delete-control"; + + const primary = makeButton( + "chat-action-button chat-delete-primary", + "Move to trash (recoverable)", + iconTrash(), + (ev) => { + ev.stopPropagation(); + onDelete(false); + }, + ); + + const caret = makeButton( + "chat-action-button chat-delete-caret", + "Delete options", + iconChevronDown(), + (ev) => { + ev.stopPropagation(); + openDeleteMenu(caret, onDelete); + }, + ); + + root.appendChild(primary); + root.appendChild(caret); + return root; +} + +function makeButton( + className: string, + label: string, + glyph: HTMLElement, + onClick: (ev: MouseEvent) => void, +): HTMLButtonElement { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = className; + btn.title = label; + btn.setAttribute("aria-label", label); + btn.appendChild(glyph); + btn.addEventListener("click", onClick); + return btn; +} + +function openDeleteMenu( + anchor: HTMLElement, + onDelete: (permanent: boolean) => void, +): void { + document.querySelector(".chat-delete-menu")?.remove(); + + const menu = document.createElement("div"); + menu.className = "chat-feedback-menu chat-delete-menu"; + + const dismiss = (ev: MouseEvent) => { + const target = ev.target as Node; + if (!menu.contains(target) && !anchor.contains(target)) { + menu.remove(); + document.removeEventListener("mousedown", dismiss, true); + } + }; + + const entries: { label: string; permanent: boolean }[] = [ + { label: "🗑 Move to trash (recoverable)", permanent: false }, + { label: "⨯ Delete permanently", permanent: true }, + ]; + for (const e of entries) { + const item = document.createElement("button"); + item.type = "button"; + item.className = "chat-feedback-menu-item"; + item.textContent = e.label; + item.addEventListener("click", () => { + menu.remove(); + document.removeEventListener("mousedown", dismiss, true); + onDelete(e.permanent); + }); + menu.appendChild(item); + } + + positionMenu(menu, anchor); + setTimeout(() => document.addEventListener("mousedown", dismiss, true), 0); +} + +function positionMenu(el: HTMLElement, anchor: HTMLElement): void { + el.style.position = "fixed"; + el.style.zIndex = "9999"; + el.style.visibility = "hidden"; + document.body.appendChild(el); + const rect = anchor.getBoundingClientRect(); + const w = el.offsetWidth; + const h = el.offsetHeight; + const vw = window.innerWidth; + const vh = window.innerHeight; + let top = rect.bottom + 4; + let left = rect.left; + if (top + h > vh - 8) top = Math.max(8, rect.top - h - 4); + if (left + w > vw - 8) left = Math.max(8, vw - w - 8); + if (left < 8) left = 8; + el.style.top = `${top}px`; + el.style.left = `${left}px`; + el.style.visibility = ""; +} diff --git a/ts/packages/chat-ui/src/feedbackWidget.ts b/ts/packages/chat-ui/src/feedbackWidget.ts index a8117cef8..b75a6af2b 100644 --- a/ts/packages/chat-ui/src/feedbackWidget.ts +++ b/ts/packages/chat-ui/src/feedbackWidget.ts @@ -19,7 +19,6 @@ import { iconMore, iconThumbsDown, iconThumbsUp, - iconTrash, } from "./icons.js"; export type FeedbackUIVariant = "footer-always"; @@ -34,7 +33,6 @@ export type FeedbackController = { comment?: string, includeContext?: boolean, ): Promise; - setHidden?(hidden: boolean, target?: "user" | "agent"): Promise; }; type FeedbackHost = { @@ -140,35 +138,9 @@ export class FeedbackWidget { root.appendChild(moreBtn); } - // Soft-hide ("trash") affordance — only when the host supplied a - // setHidden hook. Optimistically toggles the trashed state on the - // bubble container, then notifies the host; reverts on failure. - if (withExtras && this.controller.setHidden) { - const trashBtn = makeIconButton( - "trash", - "Move to trash", - iconTrash(), - () => void this.onTrash(), - ); - root.appendChild(trashBtn); - } - return { root, thumbsUp, thumbsDown }; } - private async onTrash(): Promise { - const setHidden = this.controller.setHidden; - if (!setHidden) return; - const container = this.host.container; - container.classList.add("chat-message-trashed"); - try { - await setHidden(true, "agent"); - } catch (e) { - container.classList.remove("chat-message-trashed"); - console.error("setHidden callback failed", e); - } - } - private async onThumb( rating: "up" | "down", anchor?: HTMLElement, diff --git a/ts/packages/chat-ui/src/icons.ts b/ts/packages/chat-ui/src/icons.ts index 362c6f091..9d93fccec 100644 --- a/ts/packages/chat-ui/src/icons.ts +++ b/ts/packages/chat-ui/src/icons.ts @@ -60,6 +60,12 @@ export function iconMore() { ); } +export function iconChevronDown() { + return fromSvg( + ``, + ); +} + export function iconX() { return fromSvg( ``, diff --git a/ts/packages/chat-ui/src/index.ts b/ts/packages/chat-ui/src/index.ts index e274b5bd8..49fb3b67d 100644 --- a/ts/packages/chat-ui/src/index.ts +++ b/ts/packages/chat-ui/src/index.ts @@ -25,6 +25,7 @@ export { DEFAULT_AVATAR_MAP, NotifyExplainedData, HistoryEntry, + formatHistorySeparatorLabel, } from "./chatPanel.js"; export { diff --git a/ts/packages/chat-ui/src/webSpeechProvider.ts b/ts/packages/chat-ui/src/webSpeechProvider.ts new file mode 100644 index 000000000..1b95a7d6b --- /dev/null +++ b/ts/packages/chat-ui/src/webSpeechProvider.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Zero-dependency, web-native speech-to-text provider built on the browser's + * Web Speech API (`SpeechRecognition` / `webkitSpeechRecognition`). + * + * ChatPanel uses this as the default `SpeechInputProvider` when the host does + * not supply its own (e.g. the Electron shell injects an Azure/Whisper-backed + * provider). It keeps the mic button working out of the box in plain browser + * hosts without pulling any platform-specific SDK into chat-ui. + * + * NOTE: The Web Speech API is not available (or not functional) in every + * environment — notably some Electron/VS Code webviews ship the constructor + * but cannot reach a recognition backend. `createWebSpeechProvider()` returns + * `undefined` when the constructor is missing so the mic button is simply not + * rendered; recognition errors at runtime resolve the state back to idle. + */ + +import type { SpeechInputProvider, SpeechState } from "./providers.js"; + +// Minimal structural typings for the Web Speech API, which is not part of the +// standard TypeScript DOM lib. +interface WebSpeechAlternative { + transcript: string; +} +interface WebSpeechResult { + isFinal: boolean; + 0: WebSpeechAlternative; +} +interface WebSpeechResultList { + length: number; + [index: number]: WebSpeechResult; +} +interface WebSpeechRecognitionEvent { + resultIndex: number; + results: WebSpeechResultList; +} +interface WebSpeechRecognition { + lang: string; + continuous: boolean; + interimResults: boolean; + start(): void; + stop(): void; + abort(): void; + onresult: ((event: WebSpeechRecognitionEvent) => void) | null; + onerror: ((event: unknown) => void) | null; + onend: (() => void) | null; +} +interface WebSpeechRecognitionCtor { + new (): WebSpeechRecognition; +} + +function getRecognitionCtor(): WebSpeechRecognitionCtor | undefined { + if (typeof window === "undefined") return undefined; + const w = window as unknown as { + SpeechRecognition?: WebSpeechRecognitionCtor; + webkitSpeechRecognition?: WebSpeechRecognitionCtor; + }; + return w.SpeechRecognition ?? w.webkitSpeechRecognition; +} + +class WebSpeechProvider implements SpeechInputProvider { + private readonly recognition: WebSpeechRecognition; + private state: SpeechState = "idle"; + private resultCb?: (text: string, final: boolean) => void; + private stateCb?: (state: SpeechState) => void; + private continuous = false; + + constructor(ctor: WebSpeechRecognitionCtor) { + this.recognition = new ctor(); + this.recognition.lang = "en-US"; + this.recognition.interimResults = true; + this.recognition.continuous = false; + + this.recognition.onresult = (event) => { + let interim = ""; + let final = ""; + for (let i = event.resultIndex; i < event.results.length; i++) { + const res = event.results[i]; + const transcript = res[0]?.transcript ?? ""; + if (res.isFinal) { + final += transcript; + } else { + interim += transcript; + } + } + if (interim) this.resultCb?.(interim, false); + if (final) this.resultCb?.(final, true); + }; + this.recognition.onerror = () => { + // Recognition backend unreachable / permission denied / no speech. + // Reset to idle so the mic button doesn't get stuck "listening". + this.continuous = false; + this.setState("idle"); + }; + this.recognition.onend = () => { + if (this.continuous && this.state !== "idle") { + // Continuous mode: the API stops after each utterance; restart + // until the caller explicitly stops. + try { + this.recognition.start(); + } catch { + this.setState("idle"); + } + } else { + this.setState("idle"); + } + }; + } + + public getState(): SpeechState { + return this.state; + } + + private setState(state: SpeechState): void { + if (this.state === state) return; + this.state = state; + this.stateCb?.(state); + } + + public onResult(cb: (text: string, final: boolean) => void): void { + this.resultCb = cb; + } + + public onStateChange(cb: (state: SpeechState) => void): void { + this.stateCb = cb; + } + + public start(): void { + if (this.state === "listening") { + this.stop(); + return; + } + try { + this.recognition.continuous = false; + this.recognition.start(); + this.setState("listening"); + } catch { + // start() throws if called while already running — treat as idle. + this.setState("idle"); + } + } + + public stop(): void { + this.continuous = false; + try { + this.recognition.stop(); + } catch { + // ignore — nothing was running + } + this.setState("idle"); + } + + public setContinuous(on: boolean): void { + this.continuous = on; + if (on) { + try { + this.recognition.continuous = true; + this.recognition.start(); + this.setState("always-on"); + } catch { + this.setState("idle"); + } + } else { + this.stop(); + } + } +} + +/** + * Create a Web Speech API-backed {@link SpeechInputProvider}, or `undefined` + * when the browser doesn't expose the API (so callers can skip rendering the + * mic affordance). + */ +export function createWebSpeechProvider(): SpeechInputProvider | undefined { + const ctor = getRecognitionCtor(); + if (!ctor) return undefined; + try { + return new WebSpeechProvider(ctor); + } catch { + return undefined; + } +} diff --git a/ts/packages/chat-ui/styles/chat.css b/ts/packages/chat-ui/styles/chat.css index 5a8f536ee..686e34ace 100644 --- a/ts/packages/chat-ui/styles/chat.css +++ b/ts/packages/chat-ui/styles/chat.css @@ -1829,6 +1829,54 @@ span.ansi-bright-white-fg { background: #f3f5f7; } +/* ========================================================================= + Developer-mode per-message delete split button. Shown (on hover) only when + the host enables developer mode via ChatPanel.setDeveloperMode(). + ========================================================================= */ +.chat-delete-control { + gap: 0; + opacity: 0; +} +.chat-message-container-agent:hover .chat-delete-control, +.chat-message-container-user:hover .chat-delete-control { + opacity: 0.85; +} +.chat-delete-control:hover { + opacity: 1; +} +/* User bubbles are right-aligned; mirror the agent 30px inset on the right. */ +.chat-message-container-user .chat-delete-control { + margin: 2px 30px 6px 0; + justify-content: flex-end; +} +/* Join the primary + caret into one split control. */ +.chat-delete-primary { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.chat-delete-caret { + width: 16px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.chat-delete-caret i svg { + width: 12px; + height: 12px; +} +.chat-delete-primary:hover, +.chat-delete-caret:hover { + color: #c83e3e; + background: rgba(200, 62, 62, 0.1); +} +/* Hide the control once its message is trashed/collapsing. */ +.chat-message-container-agent.chat-message-trashed > .chat-delete-control, +.chat-message-container-user.chat-message-trashed > .chat-delete-control { + display: none; +} +.chat-delete-menu { + min-width: 200px; +} + /* Popover card */ .chat-feedback-popover { background: #fff; diff --git a/ts/packages/chat-ui/test/chatPanel.spec.ts b/ts/packages/chat-ui/test/chatPanel.spec.ts index 054a2c379..a5376af5a 100644 --- a/ts/packages/chat-ui/test/chatPanel.spec.ts +++ b/ts/packages/chat-ui/test/chatPanel.spec.ts @@ -8,22 +8,12 @@ import { iconStop, iconJumpQueue, iconX } from "../src/icons.js"; // chat-ui is DOM-rendering; these tests run under jsdom (see jest.config.cjs) // and assert the DOM produced by the status-rail / roadrunner affordances. -type HiddenHook = ( - requestId: { requestId: string }, - target: "user" | "agent", - hidden: boolean, -) => void; - -function makePanel(opts?: { - onCancel?: (requestId: string) => void; - onFeedbackHidden?: HiddenHook; -}) { +function makePanel(opts?: { onCancel?: (requestId: string) => void }) { const root = document.createElement("div"); document.body.appendChild(root); const panel = new ChatPanel(root, { platformAdapter: { handleLinkClick() {} }, onCancel: opts?.onCancel, - onFeedbackHidden: opts?.onFeedbackHidden, }); return { root, panel }; } @@ -119,10 +109,8 @@ describe("user status rail — queue state", () => { }); it("no rail is rendered on an idle user bubble", () => { - const onFeedbackHidden = jest.fn(); - // Even with a hide hook wired (used for agent-bubble trash), the - // user bubble shows no rail until there's a queue state. - const { root, panel } = makePanel({ onFeedbackHidden }); + // An idle user bubble shows no rail until there's a queue state. + const { root, panel } = makePanel(); panel.addUserMessage("hello", "req-1"); expect(userRail(root, "req-1")).toBeNull(); }); diff --git a/ts/packages/defaultAgentProvider/test/data/translate-mcpfs-e2e.json b/ts/packages/defaultAgentProvider/test/data/translate-mcpfs-e2e.json index 46e6d559d..113262d79 100644 --- a/ts/packages/defaultAgentProvider/test/data/translate-mcpfs-e2e.json +++ b/ts/packages/defaultAgentProvider/test/data/translate-mcpfs-e2e.json @@ -47,7 +47,7 @@ } }, { - "request": "can you rename that file to /data/world.txt.", + "request": "can you move that file to /data/world.txt.", "expected": { "anyof": [ { diff --git a/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts b/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts index 5fc5f5859..888987530 100644 --- a/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts +++ b/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts @@ -19,6 +19,7 @@ import { createPromptLogger, PromptLoggerOptions, } from "telemetry"; +import { DevTrace } from "./devTrace.js"; import { AgentCache } from "agent-cache"; import { randomUUID } from "crypto"; import { @@ -232,6 +233,11 @@ export type CommandHandlerContext = { readonly copilotImport?: CopilotImporter | undefined; // Per activation configs developerMode?: boolean; + // When true, each translated request is confirmed via the client + // (clientIO.proposeAction) before it runs. Independent of developerMode + // so recording conversation data does not force an interactive prompt. + // Enabled with `@config dev on --confirm`; reset on restart. + confirmActions?: boolean; explanationAsynchronousMode: boolean; dblogging: boolean; clientIO: ClientIO; @@ -275,6 +281,7 @@ export type CommandHandlerContext = { metricsManager?: RequestMetricsManager | undefined; commandProfiler?: Profiler | undefined; promptLogger?: PromptLogger | undefined; + devTrace: DevTrace; instanceDirLock: (() => Promise) | undefined; @@ -420,6 +427,11 @@ export type DispatcherOptions = DeepPartialUndefined & { constructionProvider?: ConstructionProvider; explanationAsynchronousMode?: boolean; // default to true + // When true, developer mode starts enabled for the session (translation + // debug capture + dev-only UI affordances like per-message delete). + // Default false; can still be toggled at runtime via `@config dev`. + developerMode?: boolean; + // Use for tests so that embedding can be cached without 'persistDir' embeddingCacheDir?: string | undefined; // default to 'cache' under 'persistDir' if specified @@ -986,6 +998,21 @@ export async function initializeCommandHandlerContext( options?.agentInitOptions, ); const constructionProvider = options?.constructionProvider; + const promptLogger = createPromptLogger(getCosmosFactories()); + // Developer-mode capture: mirror every complete translation prompt into + // the dev trace so a translation can be inspected/reconstructed later. + const devTrace = new DevTrace(() => ({ + enabled: context.developerMode === true, + sessionDirPath: context.session.sessionDirPath, + requestId: context.currentRequestId + ? requestIdToString(context.currentRequestId) + : undefined, + })); + const originalLogModelRequest = promptLogger.logModelRequest; + promptLogger.logModelRequest = (requestContent: unknown) => { + originalLogModelRequest(requestContent); + devTrace.recordPrompt(requestContent); + }; const context: CommandHandlerContext = { agents, portRegistrar, @@ -1001,6 +1028,8 @@ export async function initializeCommandHandlerContext( storageProvider, explanationAsynchronousMode, dblogging: options?.dblogging ?? true, + developerMode: options?.developerMode ?? false, + confirmActions: false, clientIO, getConversationList: options?.getConversationList, copilotImport: options?.copilotImport, @@ -1032,7 +1061,8 @@ export async function initializeCommandHandlerContext( displayLog: await DisplayLog.load(persistDir), logger, metricsManager: metrics ? new RequestMetricsManager() : undefined, - promptLogger: createPromptLogger(getCosmosFactories()), + promptLogger, + devTrace, batchMode: false, pendingChoiceRoutes: new Map(), instanceDirLock, diff --git a/ts/packages/dispatcher/dispatcher/src/context/devTrace.ts b/ts/packages/dispatcher/dispatcher/src/context/devTrace.ts new file mode 100644 index 000000000..087a6a533 --- /dev/null +++ b/ts/packages/dispatcher/dispatcher/src/context/devTrace.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import fs from "node:fs"; +import path from "node:path"; +import registerDebug from "debug"; + +const debugDevTrace = registerDebug("typeagent:devTrace"); + +export interface DevTraceState { + // True when developer mode is on; when false, capture is a no-op. + enabled: boolean; + // Session directory to write captures under; undefined for ephemeral sessions. + sessionDirPath: string | undefined; + // Stringified request id used to name capture files. + requestId: string | undefined; +} + +/** + * Records developer-mode debugging captures for translation requests. + * + * When developer mode is on, each translation writes a self-contained JSON + * file under `/dev-captures/` containing the request, the history + * context, the resolved actions and the complete translation prompt(s) sent to + * the model — enough to inspect (and later reconstruct) why a request + * translated the way it did. A no-op when developer mode is off or the session + * is ephemeral (no session directory). + */ +export class DevTrace { + // Complete prompt(s) sent to the model during the current translation. + // Multiple entries accumulate across the initial translation, schema switch + // and selected-action passes for a single request. + private prompts: unknown[] = []; + + constructor(private readonly getState: () => DevTraceState) {} + + public get enabled(): boolean { + return this.getState().enabled; + } + + /** + * Start capturing prompts for a new translation request. Clears any prompts + * left over from a previous request. + */ + public beginTranslation(): void { + this.prompts = []; + } + + /** + * Record a complete model request (the fully expanded translation prompt). + * Called for every model completion during a translation. A no-op when + * developer mode is off. + */ + public recordPrompt(content: unknown): void { + if (!this.getState().enabled) { + return; + } + this.prompts.push(content); + } + + /** + * Persist a translation capture to `/dev-captures/`. The prompts + * recorded since `beginTranslation` are attached under the `prompts` key. A + * no-op when developer mode is off or there is no session directory. + */ + public async writeTranslationCapture( + record: Record, + ): Promise { + const { enabled, sessionDirPath, requestId } = this.getState(); + const prompts = this.prompts; + this.prompts = []; + if (!enabled || sessionDirPath === undefined) { + return; + } + try { + const dir = path.join(sessionDirPath, "dev-captures"); + await fs.promises.mkdir(dir, { recursive: true }); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const safeRequestId = (requestId ?? "unknown").replace( + /[^a-zA-Z0-9._-]/g, + "_", + ); + const file = path.join( + dir, + `translate-${timestamp}-${safeRequestId}.json`, + ); + const data = { + timestamp: new Date().toISOString(), + requestId, + ...record, + prompts, + }; + await fs.promises.writeFile( + file, + JSON.stringify(data, null, 2), + "utf8", + ); + debugDevTrace(`Wrote translation capture: ${file}`); + } catch (e) { + debugDevTrace(`Failed to write translation capture: ${e}`); + } + } +} diff --git a/ts/packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts b/ts/packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts index c7af85120..dd7786a63 100644 --- a/ts/packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts +++ b/ts/packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts @@ -43,6 +43,7 @@ import { } from "@typeagent/agent-sdk/helpers/command"; import { displayResult, + displaySuccess, displayWarn, } from "@typeagent/agent-sdk/helpers/display"; import { alwaysEnabledAgents } from "../../appAgentManager.js"; @@ -2949,6 +2950,54 @@ function getCollisionCommandHandlers(): CommandHandlerTable { }; } +/** + * `@config dev on [--confirm]` — turn on developer mode. + * + * Developer mode records conversation + translation data (see DevTrace) and + * enables dev-only UI affordances (per-message delete). The optional + * `--confirm` flag additionally turns on per-request action confirmation + * (confirmTranslation -> clientIO.proposeAction), which is otherwise off so + * that recording data does not force an interactive Run/Cancel/Edit prompt. + */ +class DevModeOnCommandHandler implements CommandHandler { + public readonly description = + "Turn on development mode (records conversation + translation data)"; + public readonly parameters = { + flags: { + confirm: { + description: + "Also confirm each translated action via the client before running it", + char: "c", + type: "boolean", + default: false, + }, + }, + } as const; + public async run( + context: ActionContext, + params: ParsedCommandParams, + ) { + const systemContext = context.sessionContext.agentContext; + const confirm = params.flags.confirm === true; + systemContext.developerMode = true; + systemContext.confirmActions = confirm; + // Notify connected clients so dev-mode UI affordances (e.g. the + // per-message delete button) can toggle live. + systemContext.clientIO.notify( + undefined, + "developerMode", + { enabled: true }, + "dispatcher", + ); + displaySuccess( + confirm + ? "development mode is enabled (action confirmation on)." + : "development mode is enabled.", + context, + ); + } +} + export function getConfigCommandHandlers(): CommandHandlerTable { return { description: "Configuration commands", @@ -2992,12 +3041,34 @@ export function getConfigCommandHandlers(): CommandHandlerTable { explainer: configExplainerCommandHandlers, execution: configExecutionCommandHandlers, modelProvider: new ConfigModelProviderCommandHandler(), - dev: getToggleHandlerTable( - "development mode", - async (context, enable) => { - context.sessionContext.agentContext.developerMode = enable; + dev: { + description: "Toggle development mode", + defaultSubCommand: "on", + commands: { + on: new DevModeOnCommandHandler(), + off: { + description: "Turn off development mode", + run: async ( + context: ActionContext, + ) => { + const systemContext = + context.sessionContext.agentContext; + systemContext.developerMode = false; + systemContext.confirmActions = false; + systemContext.clientIO.notify( + undefined, + "developerMode", + { enabled: false }, + "dispatcher", + ); + displaySuccess( + "development mode is disabled.", + context, + ); + }, + }, }, - ), + }, log: { description: "Toggle logging", commands: { diff --git a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts index c725fbc51..6151aa5c0 100644 --- a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts +++ b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts @@ -6,7 +6,6 @@ import { DynamicDisplay, TemplateSchema, } from "@typeagent/agent-sdk"; -import { displayError } from "@typeagent/agent-sdk/helpers/display"; import type { ActionInfo, AgentSchemaInfo, @@ -31,6 +30,7 @@ import { import { getDispatcherStatus, processCommand } from "./command/command.js"; import { getCommandCompletion } from "./command/completion.js"; import { getActionContext } from "./execute/actionContext.js"; +import { emitActionResult } from "./execute/actionHandlers.js"; import { closeCommandHandlerContext, CommandHandlerContext, @@ -372,6 +372,9 @@ export function createDispatcherFromContext( async getQueueSnapshot() { return context.requestQueue.getSnapshot(); }, + async getDeveloperMode() { + return context.developerMode === true; + }, async interrupt(command, attachments, options, clientRequestId) { let entry; try { @@ -658,14 +661,21 @@ export function createDispatcherFromContext( actionContext, ); if (result) { - if (result.error !== undefined) { - displayError(result.error, actionContext); - } else if (result.displayContent !== undefined) { - actionContext.actionIO.appendDisplay( - result.displayContent, - "block", - ); - } + // Run the same post-processing as the action + // pipeline so a choice callback that returns a new + // ActionResult renders fully — including a chained + // `pendingChoice` (yes/no card). Without this, a + // follow-up choice's message would show but its + // interactive card would never appear. + emitActionResult( + result, + actionContext, + context, + pending.requestId, + pending.agentName, + pending.actionIndex ?? 0, + pending.agentName, + ); } } finally { closeActionContext(); diff --git a/ts/packages/dispatcher/dispatcher/src/translation/confirmTranslation.ts b/ts/packages/dispatcher/dispatcher/src/translation/confirmTranslation.ts index fc8dffedd..17aad04ed 100644 --- a/ts/packages/dispatcher/dispatcher/src/translation/confirmTranslation.ts +++ b/ts/packages/dispatcher/dispatcher/src/translation/confirmTranslation.ts @@ -56,9 +56,12 @@ export async function confirmTranslation( }> { const actions = requestAction.actions; const systemContext = context.sessionContext.agentContext; - if (!systemContext.developerMode || systemContext.batchMode) { - // Non-developer mode: skip inline display of translation result. - // Action data is still accessible via the clickable label above the bubble. + if (!systemContext.confirmActions || systemContext.batchMode) { + // Confirmation not requested: skip inline display of translation + // result. Action data is still accessible via the clickable label + // above the bubble. Gated on `confirmActions` (see `@config dev on + // --confirm`) rather than `developerMode` so recording conversation + // data does not force an interactive Run/Cancel/Edit prompt. return { requestAction }; } const preface = diff --git a/ts/packages/dispatcher/dispatcher/src/translation/interpretRequest.ts b/ts/packages/dispatcher/dispatcher/src/translation/interpretRequest.ts index a7cb32c8c..3d995827a 100644 --- a/ts/packages/dispatcher/dispatcher/src/translation/interpretRequest.ts +++ b/ts/packages/dispatcher/dispatcher/src/translation/interpretRequest.ts @@ -234,6 +234,9 @@ export async function interpretRequest( const systemContext = context.sessionContext.agentContext; const activeSchemaNames = systemContext.agents.getActiveSchemas(); + // Developer-mode capture: start a fresh prompt buffer for this request. + systemContext.devTrace.beginTranslation(); + const tokenUsage: ai.CompletionUsageStats = { completion_tokens: 0, prompt_tokens: 0, @@ -294,6 +297,24 @@ export async function interpretRequest( }); } + // Developer-mode capture: persist the history + complete translation + // prompt(s) for this request so it can be inspected/reconstructed later. + // No-op unless developer mode is on and the session is persisted. + await systemContext.devTrace.writeTranslationCapture({ + request, + developerMode: systemContext.developerMode === true, + translationType: translateResult.type, + elapsedMs: translateResult.elapsedMs, + schemaNames: [...activeSchemaNames], + config: translateResult.config, + history, + attachmentCount: attachments?.length ?? 0, + actions: translateResult.requestAction.actions, + replacedAction, + allMatches: translateResult.allMatches, + tokenUsage, + }); + // Record this completed user turn into the contextSelector signal *after* // resolution, so it never contributes to its own context vector // (history-only, §10). Runs once per user turn at this ungated ingress. diff --git a/ts/packages/dispatcher/dispatcher/test/chainedChoice.spec.ts b/ts/packages/dispatcher/dispatcher/test/chainedChoice.spec.ts new file mode 100644 index 000000000..fb61d02ec --- /dev/null +++ b/ts/packages/dispatcher/dispatcher/test/chainedChoice.spec.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Regression test for chained (nested) choice cards. + * + * When a choice callback returns a new ActionResult that itself carries a + * `pendingChoice` (e.g. the desktop agent's "confirm the fuzzy service match" + * yes/no, whose Yes leads to a "run elevated?" yes/no), the follow-up card must + * actually render. Before the fix, `respondToChoice` only forwarded a callback + * result's error/displayContent and dropped the chained `pendingChoice`, so the + * second card's message appeared but its yes/no buttons never did. + * + * Strategy: a test agent exposes a command that returns a yes/no choice whose + * callback returns a second yes/no choice. A capturing ClientIO records every + * `requestChoice` call. After answering the first choice, a second choice card + * must have been requested. + */ + +import { AppAgent, AppAgentManifest } from "@typeagent/agent-sdk"; +import { + ChoiceManager, + createActionResultFromTextDisplay, + createYesNoChoiceResult, +} from "@typeagent/agent-sdk/helpers/action"; +import { getCommandInterface } from "@typeagent/agent-sdk/helpers/command"; +import { AppAgentProvider } from "../src/agentProvider/agentProvider.js"; +import { createDispatcher } from "../src/dispatcher.js"; +import { awaitCommand } from "@typeagent/dispatcher-types"; +import type { ClientIO, Dispatcher } from "@typeagent/dispatcher-types"; + +const choiceManager = new ChoiceManager(); + +const config: AppAgentManifest = { + emojiChar: "🔗", + description: "Chained-choice test agent", +}; + +const handlers = { + description: "Chained choice command table", + commands: { + chain: { + description: + "Returns a yes/no choice whose Yes leads to a second yes/no choice", + run: async () => + createYesNoChoiceResult(choiceManager, "first?", async () => + createYesNoChoiceResult( + choiceManager, + "second?", + async () => createActionResultFromTextDisplay("done"), + ), + ), + }, + }, +} as const; + +const agent: AppAgent = { + ...getCommandInterface(handlers), + handleChoice: async (choiceId, response, context) => + choiceManager.handleChoice(choiceId, response, context), +}; + +const agentProvider: AppAgentProvider = { + getAppAgentNames: () => ["choicechain"], + getAppAgentManifest: async (name) => { + if (name !== "choicechain") throw new Error(`Unknown agent: ${name}`); + return config; + }, + loadAppAgent: async (name) => { + if (name !== "choicechain") throw new Error(`Unknown agent: ${name}`); + return agent; + }, + unloadAppAgent: async () => {}, +}; + +type CapturedChoice = { choiceId: string; message: string }; + +function makeClientIO(captured: CapturedChoice[]): ClientIO { + return { + clear: () => {}, + exit: () => process.exit(0), + shutdown: () => process.exit(0), + setUserRequest: () => {}, + setDisplayInfo: () => {}, + setDisplay: () => {}, + appendDisplay: () => {}, + appendDiagnosticData: () => {}, + setDynamicDisplay: () => {}, + question: async (_r, _m, _c, defaultId) => defaultId ?? 0, + proposeAction: async () => undefined, + notify: () => {}, + openLocalView: async () => {}, + closeLocalView: async () => {}, + requestChoice: (_requestId, choiceId, _type, message) => { + captured.push({ choiceId, message }); + }, + requestInteraction: () => {}, + interactionResolved: () => {}, + interactionCancelled: () => {}, + takeAction: (_requestId, action) => { + throw new Error(`Action ${action} not supported`); + }, + }; +} + +describe("Chained choice cards", () => { + let dispatcher: Dispatcher; + const captured: CapturedChoice[] = []; + + beforeAll(async () => { + dispatcher = await createDispatcher("test-chained-choice", { + agents: { actions: false, schemas: false }, + translation: { enabled: false }, + explainer: { enabled: false }, + cache: { enabled: false }, + appAgentProviders: [agentProvider], + collectCommandResult: true, + clientIO: makeClientIO(captured), + }); + }); + + afterAll(async () => { + if (dispatcher) { + await dispatcher.close(); + } + }); + + it("renders a second choice card when a choice callback returns a new pendingChoice", async () => { + await awaitCommand(dispatcher, "@choicechain chain"); + + // The command presents the first yes/no card. + expect(captured).toHaveLength(1); + expect(captured[0].message).toBe("first?"); + + // Simulate the user clicking "Yes" on the first card. Its callback + // returns a second yes/no choice, whose card must now be requested. + await dispatcher.respondToChoice(captured[0].choiceId, true); + + expect(captured).toHaveLength(2); + expect(captured[1].message).toBe("second?"); + expect(captured[1].choiceId).not.toBe(captured[0].choiceId); + }, 10_000); +}); diff --git a/ts/packages/dispatcher/dispatcher/test/confirmTranslation.spec.ts b/ts/packages/dispatcher/dispatcher/test/confirmTranslation.spec.ts new file mode 100644 index 000000000..605380cdc --- /dev/null +++ b/ts/packages/dispatcher/dispatcher/test/confirmTranslation.spec.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect, jest } from "@jest/globals"; +import type { ActionContext } from "@typeagent/agent-sdk"; +import type { RequestAction } from "agent-cache"; +import { confirmTranslation } from "../src/translation/confirmTranslation.js"; +import type { CommandHandlerContext } from "../src/context/commandHandlerContext.js"; + +// --------------------------------------------------------------------------- +// confirmTranslation gates the interactive Run/Cancel/Edit action +// confirmation (clientIO.proposeAction) on `confirmActions`, NOT on +// `developerMode`. This guards the normal (no `--confirm`) path: recording +// conversation data with `@config dev on` must NOT force a confirmation +// prompt, and batch mode must never prompt. Only `@config dev on --confirm` +// (which sets confirmActions) triggers the prompt. +// --------------------------------------------------------------------------- + +type Overrides = { + confirmActions?: boolean; + batchMode?: boolean; +}; + +function makeContext(overrides: Overrides) { + const proposeAction = jest.fn(async () => undefined); + const agentContext = { + confirmActions: overrides.confirmActions ?? false, + batchMode: overrides.batchMode ?? false, + currentRequestId: { requestId: "req-1" }, + agents: { getActiveSchemas: () => [] as string[] }, + clientIO: { proposeAction }, + }; + const context = { + sessionContext: { agentContext }, + } as unknown as ActionContext; + return { context, proposeAction }; +} + +const requestAction = { + request: "test", + actions: [], +} as unknown as RequestAction; + +describe("confirmTranslation gate", () => { + it("does not confirm when confirmActions is off (normal @config dev on / no dev mode)", async () => { + const { context, proposeAction } = makeContext({ + confirmActions: false, + }); + const result = await confirmTranslation( + 0, + "user", + requestAction, + context, + ); + expect(result).toEqual({ requestAction }); + expect(proposeAction).not.toHaveBeenCalled(); + }); + + it("does not confirm in batch mode even when confirmActions is on", async () => { + const { context, proposeAction } = makeContext({ + confirmActions: true, + batchMode: true, + }); + const result = await confirmTranslation( + 0, + "user", + requestAction, + context, + ); + expect(result).toEqual({ requestAction }); + expect(proposeAction).not.toHaveBeenCalled(); + }); + + it("confirms via clientIO.proposeAction when confirmActions is on (--confirm)", async () => { + const { context, proposeAction } = makeContext({ + confirmActions: true, + batchMode: false, + }); + const result = await confirmTranslation( + 0, + "user", + requestAction, + context, + ); + // proposeAction returns undefined (no replacement) -> original kept. + expect(result).toEqual({ requestAction }); + expect(proposeAction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ts/packages/dispatcher/rpc/src/dispatcherClient.ts b/ts/packages/dispatcher/rpc/src/dispatcherClient.ts index d05b2ce7b..a3af9c171 100644 --- a/ts/packages/dispatcher/rpc/src/dispatcherClient.ts +++ b/ts/packages/dispatcher/rpc/src/dispatcherClient.ts @@ -135,6 +135,9 @@ export function createDispatcherRpcClient( async getQueueSnapshot() { return rpc.invoke("getQueueSnapshot"); }, + async getDeveloperMode() { + return rpc.invoke("getDeveloperMode"); + }, async getDynamicDisplay(...args) { return rpc.invoke("getDynamicDisplay", ...args); }, diff --git a/ts/packages/dispatcher/rpc/src/dispatcherServer.ts b/ts/packages/dispatcher/rpc/src/dispatcherServer.ts index 29fc05aff..ca9921196 100644 --- a/ts/packages/dispatcher/rpc/src/dispatcherServer.ts +++ b/ts/packages/dispatcher/rpc/src/dispatcherServer.ts @@ -52,6 +52,9 @@ export function createDispatcherRpcServer( getQueueSnapshot: async () => { return dispatcher.getQueueSnapshot(); }, + getDeveloperMode: async () => { + return dispatcher.getDeveloperMode(); + }, getDynamicDisplay: async (...args) => { return dispatcher.getDynamicDisplay(...args); }, diff --git a/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts b/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts index a7f253688..c3fb3411e 100644 --- a/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts +++ b/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts @@ -65,6 +65,8 @@ export type DispatcherInvokeFunctions = { getQueueSnapshot(): Promise; + getDeveloperMode(): Promise; + getDynamicDisplay( appAgentName: string, type: DisplayType, diff --git a/ts/packages/dispatcher/rpc/test/dispatcherRpc.spec.ts b/ts/packages/dispatcher/rpc/test/dispatcherRpc.spec.ts index 50e666ec0..0c84d75c6 100644 --- a/ts/packages/dispatcher/rpc/test/dispatcherRpc.spec.ts +++ b/ts/packages/dispatcher/rpc/test/dispatcherRpc.spec.ts @@ -62,6 +62,7 @@ function makeStubDispatcher(overrides: Partial = {}): Dispatcher & { submitCommand: notImplemented("submitCommand") as any, interrupt: notImplemented("interrupt") as any, getQueueSnapshot: notImplemented("getQueueSnapshot") as any, + getDeveloperMode: notImplemented("getDeveloperMode") as any, getDynamicDisplay: notImplemented("getDynamicDisplay") as any, getTemplateSchema: notImplemented("getTemplateSchema") as any, getTemplateCompletion: notImplemented("getTemplateCompletion") as any, diff --git a/ts/packages/dispatcher/types/src/dispatcher.ts b/ts/packages/dispatcher/types/src/dispatcher.ts index b9a1ad060..93d04e1b4 100644 --- a/ts/packages/dispatcher/types/src/dispatcher.ts +++ b/ts/packages/dispatcher/types/src/dispatcher.ts @@ -229,6 +229,14 @@ export interface Dispatcher { */ getQueueSnapshot(): Promise; + /** + * Whether developer mode is currently on for this session. Clients call + * this after connecting so dev-only UI (e.g. the per-message delete + * button) reflects a server started with `--dev` without waiting for a + * `@config dev` toggle. + */ + getDeveloperMode(): Promise; + /** * Cancel the currently-running request (if any) and enqueue `text` at the * head of the queue so it runs next. The cancel-then-prepend pair is diff --git a/ts/packages/shell/src/renderer/src/chatPanelBridge.ts b/ts/packages/shell/src/renderer/src/chatPanelBridge.ts index c4e77e075..fa875208b 100644 --- a/ts/packages/shell/src/renderer/src/chatPanelBridge.ts +++ b/ts/packages/shell/src/renderer/src/chatPanelBridge.ts @@ -21,6 +21,7 @@ import { HistoryEntry, SettingsPanelSchema, HelpPanelContent, + formatHistorySeparatorLabel, type TemplateEditServices, type ConnectionStatus, } from "chat-ui"; @@ -220,55 +221,6 @@ function toHistoryEntries(entries: any[]): HistoryEntry[] { return out; } -function formatHistorySeparatorLabel(entries: any[]): string { - let newestTimestamp: number | undefined; - for (const entry of entries) { - if (typeof entry.timestamp !== "number") continue; - if ( - newestTimestamp === undefined || - entry.timestamp > newestTimestamp - ) { - newestTimestamp = entry.timestamp; - } - } - - if (newestTimestamp === undefined) { - return "earlier"; - } - - const diffMs = Math.max(0, Date.now() - newestTimestamp); - const diffMinutes = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMinutes < 2) { - return "a moment ago"; - } - if (diffMinutes < 10) { - return "a few minutes ago"; - } - if (diffMinutes < 60) { - return `${diffMinutes} minutes ago`; - } - if (diffHours < 2) { - return "an hour ago"; - } - if (diffHours < 6) { - return "a few hours ago"; - } - if (diffHours < 24) { - return `${diffHours} hours ago`; - } - if (diffDays < 2) { - return "yesterday"; - } - if (diffDays < 7) { - return `${diffDays} days ago`; - } - - return new Date(newestTimestamp).toLocaleDateString(); -} - export type ChatPanelClient = { client: Client; chatPanel: ChatPanel; @@ -285,6 +237,17 @@ export function createChatPanelClient( agents: Map, ): ChatPanelClient { let dispatcher: Dispatcher | undefined; + // Requests the user submitted before the dispatcher finished + // initializing. Buffered here (rather than dropped with a "not ready" + // notice) and flushed by dispatcherInitialized() once the dispatcher is + // live — after the main process has enqueued the startup `@greeting`, so + // the greeting stays at the head of the queue and these follow in the + // order they were typed. + const pendingSends: Array<{ + text: string; + attachments: string[] | undefined; + requestId: string; + }> = []; let settings: ShellUserSettings = defaultUserSettings; const notifications: NotificationEntry[] = []; @@ -639,6 +602,49 @@ export function createChatPanelClient( ], }; + // Submit a user command to the dispatcher and finalize its bubble on + // completion. Extracted from `onSend` so requests buffered before the + // dispatcher was ready can be replayed through the identical path. + function submitUserCommand( + text: string, + attachments: string[] | undefined, + requestId: string, + ): void { + void (async () => { + try { + const result = await awaitCommand( + dispatcher!, + text, + attachments, + undefined, + requestId, + ); + const mapped = mapResult(result); + // Dedupe with queueRequestCancelled to avoid + // double-stamping the affordance. + const cancelled = + mapped?.cancelled === true && + claimCancelledRender(requestId); + chatPanel.completeRequest( + requestId, + mapped ? { ...mapped, cancelled } : undefined, + ); + clearQueueChip(requestId); + } catch (e: any) { + // Drop rejection-from-cancel stragglers (affordance + // already painted by queueRequestCancelled). + if (!isCancelledRequest(requestId)) { + chatPanel.addSystemMessage( + `Error: ${e?.message ?? String(e)}`, + ); + } + clearQueueChip(requestId); + } finally { + chatPanel.setIdle(); + } + })(); + } + // --- ChatPanel ------------------------------------------------------- const chatPanel = new ChatPanel(rootElement, { platformAdapter: { @@ -648,43 +654,14 @@ export function createChatPanelClient( }, onSend: (text, attachments, requestId) => { if (dispatcher === undefined) { - chatPanel.addSystemMessage("Dispatcher not ready."); - chatPanel.setIdle(); + // Dispatcher still initializing — queue the request instead + // of dropping it. dispatcherInitialized() flushes these once + // the dispatcher is live (after the startup `@greeting`). The + // bubble stays in its processing state so it reads as pending. + pendingSends.push({ text, attachments, requestId }); return; } - void (async () => { - try { - const result = await awaitCommand( - dispatcher!, - text, - attachments, - undefined, - requestId, - ); - const mapped = mapResult(result); - // Dedupe with queueRequestCancelled to avoid - // double-stamping the affordance. - const cancelled = - mapped?.cancelled === true && - claimCancelledRender(requestId); - chatPanel.completeRequest( - requestId, - mapped ? { ...mapped, cancelled } : undefined, - ); - clearQueueChip(requestId); - } catch (e: any) { - // Drop rejection-from-cancel stragglers (affordance - // already painted by queueRequestCancelled). - if (!isCancelledRequest(requestId)) { - chatPanel.addSystemMessage( - `Error: ${e?.message ?? String(e)}`, - ); - } - clearQueueChip(requestId); - } finally { - chatPanel.setIdle(); - } - })(); + submitUserCommand(text, attachments, requestId); }, onCancel: (requestId) => { dispatcher?.cancelCommandByClientId(requestId); @@ -698,8 +675,11 @@ export function createChatPanelClient( ctx, ); }, - onFeedbackHidden: (requestId, target, hidden) => { - void dispatcher?.recordUserHide(requestId, hidden, target); + onDeleteMessage: (requestId, target, permanent) => { + // Developer-mode per-message delete. Reuse the feedback "hide" + // RPC: permanent=true is a non-recoverable hard delete; + // permanent=false is a recoverable soft delete (trash). + void dispatcher?.recordUserHide(requestId, true, target, permanent); }, speechProvider, ttsProvider, @@ -911,6 +891,10 @@ export function createChatPanelClient( case "explained": chatPanel.notifyExplained(ridStr(requestId)!, data); break; + case "developerMode": + // Toggle dev-only UI affordances (per-message delete). + chatPanel.setDeveloperMode(data?.enabled === true); + break; case "randomCommandSelected": break; case "grammarRule": @@ -1311,6 +1295,15 @@ export function createChatPanelClient( cutoffSeq: number | undefined, ): void { dispatcher = d; + // Seed dev-mode state so the per-message delete button reflects a + // server started with `--dev` on connect, without waiting for a + // `@config dev` toggle. Best-effort (older servers lack the RPC). + if (typeof d.getDeveloperMode === "function") { + void d + .getDeveloperMode() + .then((enabled) => chatPanel.setDeveloperMode(enabled)) + .catch(() => {}); + } // Seed mirror sync; defer DOM reconcile until replay finishes. if (initialQueueSnapshot) { const prev = queueMirror.snapshot; @@ -1318,6 +1311,20 @@ export function createChatPanelClient( const snapshot = initialQueueSnapshot; afterReplay(() => reconcileQueueChips(prev, snapshot)); } + // Flush any requests the user typed before the dispatcher was + // ready. Deferred behind replay (same gate as the queue-chip + // reconcile) so it runs after history settles and after the main + // process has enqueued the startup `@greeting` — keeping the + // greeting at the head of the dispatcher queue, followed by these + // in the order they were typed. + if (pendingSends.length > 0) { + const buffered = pendingSends.splice(0); + afterReplay(() => { + for (const { text, attachments, requestId } of buffered) { + submitUserCommand(text, attachments, requestId); + } + }); + } void replayDisplayHistory(cutoffSeq); }, updateRegisterAgents(updatedAgents: [string, string][]): void { diff --git a/ts/packages/vscode-shell/package.json b/ts/packages/vscode-shell/package.json index cd17b5fc7..ab9b9191e 100644 --- a/ts/packages/vscode-shell/package.json +++ b/ts/packages/vscode-shell/package.json @@ -216,6 +216,7 @@ "dompurify": "^3.4.11", "isomorphic-ws": "^5.0.0", "markdown-it": "^14.2.0", + "microsoft-cognitiveservices-speech-sdk": "1.43.1", "ws": "^8.21.0" }, "devDependencies": { diff --git a/ts/packages/vscode-shell/src/agentServerBridge.ts b/ts/packages/vscode-shell/src/agentServerBridge.ts index 93d00929a..fad354f0c 100644 --- a/ts/packages/vscode-shell/src/agentServerBridge.ts +++ b/ts/packages/vscode-shell/src/agentServerBridge.ts @@ -6,6 +6,7 @@ import * as os from "os"; import { connectAgentServer, type AgentServerConnection, + type SpeechToken, } from "@typeagent/agent-server-client"; import { findOrCreateNamedConversation, @@ -123,6 +124,16 @@ export class AgentServerBridge { /** In-flight session-join promise — serializes joinSpecificSession calls. */ private joinInFlight: Promise | undefined; private session: SessionDispatcher | undefined; + // Commands the user submitted before a session/dispatcher was ready. + // Buffered here (rather than dropped with a "no active session" notice) + // and flushed by flushPendingSends() once a session is established, in + // the order they were typed. Mirrors the Electron shell's pre-init send + // queue in chatPanelBridge.ts. + private pendingSends: Array<{ + command: string; + requestId?: string; + attachments?: string[]; + }> = []; private webviews: Set = new Set(); /** * Per-webview buffer for live broadcasts that arrive while the webview @@ -467,6 +478,11 @@ export class AgentServerBridge { this.lastReplayedSessionId = this.session.sessionId; await this.replayHistory(this.session); } + + // Flush any commands the user typed before the session was + // ready. Deferred until after history replay so they submit on + // a settled transcript, then run in the order they were typed. + void this.flushPendingSends(); } catch (e: any) { const msg = e?.message ?? String(e); // Suppress per-attempt error toasts in the chat area; the @@ -1046,6 +1062,50 @@ export class AgentServerBridge { loading: false, }); } + // Push current developer-mode state so dev-only UI (the per-message + // delete button) reflects a server started with `--dev` on connect + // and session switches, without waiting for a `@config dev` toggle. + await this.pushDeveloperMode(); + + // TODO(reconnect-mid-confirmation): re-render pending interactions on + // rejoin. When `@config dev on --confirm` is active, a request can be + // blocked on a `proposeAction`/`question` (SharedDispatcher deferred + // promise, 10-min timeout). If the webview reconnects or switches to a + // session while that prompt is outstanding, the interaction is NOT + // replayed here, so the user sees a "working" request with no way to + // answer it until the server times out. The agent-server already + // tracks these (DisplayLog.logPendingInteraction, included in the + // join/JoinSessionResult) — surface them (e.g. a getPendingInteractions + // RPC or a field on the join result) and re-broadcast each as a + // `requestInteraction` after the history replay so the confirmation UI + // reappears and `respondToInteraction` can complete the request. + } + + /** + * Query the current developer-mode flag from the dispatcher and forward + * it to the webview(s) so dev-only affordances toggle to match. Targets a + * single webview when provided (re-attach hydration), else broadcasts. + * Best-effort: silently skipped when the dispatcher predates + * `getDeveloperMode`. + */ + private async pushDeveloperMode(webview?: vscode.Webview): Promise { + const dispatcher = this.session?.dispatcher; + if (!dispatcher || typeof dispatcher.getDeveloperMode !== "function") { + return; + } + let enabled: boolean; + try { + enabled = await dispatcher.getDeveloperMode(); + } catch (e) { + console.warn("[agentServerBridge] getDeveloperMode failed:", e); + return; + } + const message = { type: "developerMode" as const, enabled }; + if (webview) { + this.postToWebview(webview, message); + } else { + this.broadcastToWebviews(message); + } } private async replayHistoryInner( @@ -1155,6 +1215,9 @@ export class AgentServerBridge { e, ); } + // Seed dev-mode state so the per-message delete button + // reflects a `--dev` server on webview re-attach. + await this.pushDeveloperMode(webview); } catch (e) { console.warn( "[agentServerBridge] hydrateWebview replay failed:", @@ -1191,7 +1254,144 @@ export class AgentServerBridge { ): Promise { switch (msg.type) { case "sendCommand": - await this.sendCommand(msg.command, msg.requestId); + await this.sendCommand( + msg.command, + msg.requestId, + msg.attachments, + ); + break; + case "interactionResponse": + // Reply to a server-driven interactive prompt (dev-mode + // action confirmation / agent question). Resolves the + // dispatcher's pending proposeAction/question so the request + // can finish instead of blocking to the 10-min timeout. + try { + await this.session?.dispatcher.respondToInteraction( + msg.response, + ); + } catch (e) { + console.warn( + "[agentServerBridge] respondToInteraction failed:", + e, + ); + } + break; + case "choiceResponse": + // Reply to an agent's non-blocking choice card + // (createYesNoChoiceResult / createMultiChoiceResult / + // createPickRememberChoiceResult). Resolves the dispatcher's + // pending choice route so the agent's handleChoice callback + // runs. Without this the card's buttons would do nothing. + try { + await this.session?.dispatcher.respondToChoice( + msg.choiceId, + msg.response, + ); + } catch (e) { + console.warn( + "[agentServerBridge] respondToChoice failed:", + e, + ); + } + break; + case "recordUserFeedback": + // Persist a user's thumbs up/down rating. Mirrors the + // deleteMessage -> recordUserHide path (same RequestId shape); + // the dispatcher broadcasts the result back via + // ClientIO.onUserFeedback so the bubble updates. + try { + await this.session?.dispatcher.recordUserFeedback( + { requestId: msg.requestId }, + msg.rating, + msg.category, + msg.comment, + msg.includeContext, + ); + } catch (e) { + console.warn( + "[agentServerBridge] recordUserFeedback failed:", + e, + ); + } + break; + case "bridgeRpcRequest": { + // Service call routed from the webview to the dispatcher: + // template-editor lookups (proposeAction edit flow) or + // getDynamicDisplay (live display refresh). Correlated by + // `id`; always answered so the webview promise settles. + const dispatcher = this.session?.dispatcher; + try { + if (dispatcher === undefined) { + throw new Error("No active session"); + } + let result: unknown; + if (msg.method === "getTemplateSchema") { + const [templateAgentName, templateName, data] = + msg.args as Parameters< + typeof dispatcher.getTemplateSchema + >; + result = await dispatcher.getTemplateSchema( + templateAgentName, + templateName, + data, + ); + } else if (msg.method === "getTemplateCompletion") { + const [ + templateAgentName, + templateName, + data, + propertyName, + ] = msg.args as Parameters< + typeof dispatcher.getTemplateCompletion + >; + result = await dispatcher.getTemplateCompletion( + templateAgentName, + templateName, + data, + propertyName, + ); + } else { + // getDynamicDisplay(appAgentName, type, displayId) + const [appAgentName, type, id] = msg.args as Parameters< + typeof dispatcher.getDynamicDisplay + >; + result = await dispatcher.getDynamicDisplay( + appAgentName, + type, + id, + ); + } + this.postToWebview(webview, { + type: "bridgeRpcResponse", + id: msg.id, + result, + }); + } catch (e) { + this.postToWebview(webview, { + type: "bridgeRpcResponse", + id: msg.id, + error: e instanceof Error ? e.message : String(e), + }); + } + break; + } + case "deleteMessage": + // Developer-mode per-message delete. Reuse the feedback + // "hide" RPC: permanent=true is a non-recoverable hard delete; + // permanent=false is a recoverable soft delete (trash). + try { + await this.session?.dispatcher.recordUserHide( + { requestId: msg.requestId }, + true, + msg.target, + msg.permanent, + ); + } catch (e) { + console.warn( + "[agentServerBridge] deleteMessage failed:", + e, + ); + } break; case "cancelCommand": // Forward to the dispatcher so an in-flight request can be @@ -1251,6 +1451,27 @@ export class AgentServerBridge { void vscode.env.openExternal(vscode.Uri.parse(msg.href)); } break; + case "getSpeechToken": { + // Relay a speech-token request to the agent server (which owns + // the `speech:` config) so the webview can run Azure Speech + // recognition. Always answer so the webview promise settles; + // `token` is undefined when speech isn't configured. + let speechToken: SpeechToken | undefined; + try { + speechToken = await this.rawConnection?.getSpeechToken(); + } catch (e) { + console.warn( + "[agentServerBridge] getSpeechToken failed:", + e, + ); + } + this.postToWebview(webview, { + type: "speechTokenResponse", + id: msg.id, + token: speechToken, + }); + break; + } case "connect": // The webview posts `connect` once it has wired up its // message listener — this is also our cue that it's ready @@ -1610,6 +1831,7 @@ export class AgentServerBridge { private async sendCommand( command: string, requestId?: string, + attachments?: string[], ): Promise { // Once the user actually engages with an ephemeral panel session, // promote it to a persistent named session so it survives panel @@ -1623,17 +1845,12 @@ export class AgentServerBridge { // (reading 'dispatcher')" and the webview's stop button stays // stuck on screen. if (!this.session) { - this.broadcastToWebviews({ - type: "commandComplete", - requestId: requestId ?? "", - result: null, - }); - this.broadcastToWebviews({ - type: "error", - message: - "No active session — reconnect or pick a conversation.", - requestId: requestId ?? "", - }); + // Session/dispatcher not ready yet (initial connect, reconnect, + // or a mid-send session swap). Queue the command instead of + // dropping it; flushPendingSends() replays it once a session is + // established. The webview bubble stays in its processing state + // so it reads as pending rather than erroring out. + this.pendingSends.push({ command, requestId, attachments }); return; } @@ -1645,7 +1862,7 @@ export class AgentServerBridge { const result = await awaitCommand( this.session.dispatcher, command, - undefined, + attachments, options, requestId, ); @@ -1694,6 +1911,22 @@ export class AgentServerBridge { } } + /** + * Replay commands buffered while no session was ready (see + * `pendingSends`). Drained in one shot and submitted sequentially so + * they run in the order the user typed them. Fired from connectImpl() + * once a session is established and history has replayed. + */ + private async flushPendingSends(): Promise { + if (this.pendingSends.length === 0) { + return; + } + const buffered = this.pendingSends.splice(0); + for (const { command, requestId, attachments } of buffered) { + await this.sendCommand(command, requestId, attachments); + } + } + private broadcastMetricsToPeers( requestId: string | undefined, result: any, diff --git a/ts/packages/vscode-shell/src/bridge/clientIO.ts b/ts/packages/vscode-shell/src/bridge/clientIO.ts index e9c291d0a..807bc3939 100644 --- a/ts/packages/vscode-shell/src/bridge/clientIO.ts +++ b/ts/packages/vscode-shell/src/bridge/clientIO.ts @@ -183,7 +183,25 @@ export function createBridgeClientIO(ctx: BridgeClientIOContext): ClientIO { }); }, appendDiagnosticData: () => {}, - setDynamicDisplay: () => {}, + // Live-updating display (agent set ActionResult.dynamicDisplayId). + // Forward to the webview, which registers a refresh timer via chat-ui's + // ChatPanel.setDynamicDisplay and polls back for fresh content through + // the `getDynamicDisplay` bridge RPC. Previously a no-op, so dynamic + // displays (e.g. the player "now playing" status) never refreshed. + setDynamicDisplay: ( + _requestId: RequestId, + source: string, + _actionIndex: number, + displayId: string, + nextRefreshMs: number, + ) => { + ctx.broadcast({ + type: "setDynamicDisplay", + source, + displayId, + nextRefreshMs, + }); + }, notify: ( notificationId: string | RequestId | undefined, event: string, @@ -191,6 +209,15 @@ export function createBridgeClientIO(ctx: BridgeClientIOContext): ClientIO { source: string, seq?: number, ) => { + // Developer-mode toggle: forward as a dedicated message so the + // webview can flip dev-only UI affordances (per-message delete). + if (event === "developerMode") { + ctx.broadcast({ + type: "developerMode", + enabled: data?.enabled === true, + }); + return; + } const clientId = clientIdOf(notificationId); // For commandComplete, also attach the canonical server UUID // (when known) so the webview's cancellation dedupe can mark @@ -222,10 +249,50 @@ export function createBridgeClientIO(ctx: BridgeClientIOContext): ClientIO { aliasRequestId, }); }, - requestChoice: () => {}, - requestInteraction: (_interaction: PendingInteractionRequest) => {}, - interactionResolved: () => {}, - interactionCancelled: () => {}, + // Non-blocking choice card (yes/no buttons, multi-select, or a + // single-select pick + "remember" checkbox). The dispatcher already + // rendered the prompt text as the action's displayContent + // (appendDisplay above); we forward the choice so the webview can add + // the interactive buttons to that same agent bubble and reply with a + // `choiceResponse`. Previously a no-op, which is why yes/no cards + // (e.g. github-cli install) never showed their buttons here. + requestChoice: ( + requestId: RequestId, + choiceId: string, + type: "yesNo" | "multiChoice" | "pickRemember", + message: string, + choices: string[], + source: string, + checkboxLabel?: string, + ) => { + ctx.broadcast({ + type: "requestChoice", + choiceId, + choiceType: type, + message, + choices, + source, + checkboxLabel, + requestId: clientIdOf(requestId), + }); + }, + // Forward server-driven interactive prompts (dev-mode action + // confirmation via `@config dev on --confirm`, or agent questions) to + // the webview, which renders them and replies with an + // `interactionResponse` (handled in agentServerBridge -> + // dispatcher.respondToInteraction). Without this the request blocks on + // the server until the 10-min proposeAction timeout. + requestInteraction: (interaction: PendingInteractionRequest) => { + ctx.broadcast({ type: "requestInteraction", interaction }); + }, + // Another connected client answered, or the server cancelled/timed + // out the interaction — tell the webview to tear down its prompt. + interactionResolved: (interactionId: string) => { + ctx.broadcast({ type: "interactionResolved", interactionId }); + }, + interactionCancelled: (interactionId: string) => { + ctx.broadcast({ type: "interactionCancelled", interactionId }); + }, takeAction: (requestId, action, data) => { if (action === "vscode-shell-action") { ctx.handleShellAction(requestId, data).catch((e: any) => { @@ -245,6 +312,16 @@ export function createBridgeClientIO(ctx: BridgeClientIOContext): ClientIO { }, shutdown: () => {}, + // User feedback (thumbs up/down) recorded by any client is fanned out + // here so every connected client's bubble stays in sync. Forward to the + // webview, which applies it via chat-ui's ChatPanel.applyFeedback. The + // entry is keyed by the client request id the originating client + // submitted (see recordUserFeedback in agentServerBridge), which + // matches the webview's bubble threadId. + onUserFeedback: (entry) => { + ctx.broadcast({ type: "userFeedback", entry }); + }, + // Queue lifecycle push events. Forwarded straight to the webview // so the chat-ui can mirror state and dedupe the cancellation // affordance between `commandComplete` (local awaitCommand path) diff --git a/ts/packages/vscode-shell/src/bridge/messages.ts b/ts/packages/vscode-shell/src/bridge/messages.ts index 54b9acbd5..02e3e9fee 100644 --- a/ts/packages/vscode-shell/src/bridge/messages.ts +++ b/ts/packages/vscode-shell/src/bridge/messages.ts @@ -3,9 +3,14 @@ import type { IAgentMessage, + PendingInteractionRequest, + PendingInteractionResponse, QueuedRequest, QueueCancelReason, QueueSnapshot, + UserFeedbackEntry, + UserFeedbackRating, + UserFeedbackCategory, } from "@typeagent/dispatcher-types"; import type { CompletionDirection, @@ -14,6 +19,7 @@ import type { } from "@typeagent/agent-sdk"; import type { CompletionState } from "agent-dispatcher/helpers/completion"; import type { ConnectionActionId } from "chat-ui"; +import type { SpeechToken } from "@typeagent/agent-server-protocol"; /** * Messages from extension host → webview @@ -188,19 +194,102 @@ export type BridgeToWebviewMessage = }>; currentSessionId?: string; } + | { type: "developerMode"; enabled: boolean } + | { + // Non-blocking choice card from an agent action + // (createYesNoChoiceResult / createMultiChoiceResult / + // createPickRememberChoiceResult -> ClientIO.requestChoice). The + // prompt text is already shown as the action's `displayContent` + // (rendered via appendDisplay), so the webview renders ONLY the + // buttons and anchors them to that request's agent bubble via + // `requestId`. It replies with a `choiceResponse`. Without this the + // card's buttons never appear and the user can't answer the prompt. + type: "requestChoice"; + choiceId: string; + // Renamed from ClientIO's `type` param to avoid colliding with this + // message's own `type` discriminant. + choiceType: "yesNo" | "multiChoice" | "pickRemember"; + message: string; + choices: string[]; + source: string; + checkboxLabel?: string; + requestId?: string; + } + | { + // Live-updating ("dynamic") display from an agent action that set + // ActionResult.dynamicDisplayId (e.g. the player agent's "now + // playing" status). The webview registers a refresh timer via + // chat-ui's ChatPanel.setDynamicDisplay, which polls back for fresh + // content through the `getDynamicDisplay` bridge RPC. Without this + // the initial content renders once but never refreshes. + type: "setDynamicDisplay"; + source: string; + displayId: string; + nextRefreshMs: number; + } + | { + // Broadcast of a user feedback rating (thumbs up/down) recorded by + // any connected client, fanned out via ClientIO.onUserFeedback so + // every client's bubble stays in sync. The webview applies it via + // chat-ui's ChatPanel.applyFeedback. + type: "userFeedback"; + entry: UserFeedbackEntry; + } + | { + // Server-driven interactive prompt: dev-mode action confirmation + // (`@config dev on --confirm`) or an agent question. The webview + // renders it (proposeActionEdit / choice prompt) and replies with + // an `interactionResponse` message. + type: "requestInteraction"; + interaction: PendingInteractionRequest; + } + // Another client answered / the server cancelled the interaction; the + // webview should tear down its in-progress prompt (identified by id). + | { type: "interactionResolved"; interactionId: string } + | { type: "interactionCancelled"; interactionId: string } + | { + // Response to a webview-issued `bridgeRpcRequest` (template editor + // schema / completion lookups routed through the host to the + // dispatcher). Correlated by `id`. + type: "bridgeRpcResponse"; + id: number; + result?: unknown; + error?: string; + } + // Response to a webview-issued `getSpeechToken` request. Correlated by + // `id`; `token` is undefined when speech isn't configured / unavailable. + | { type: "speechTokenResponse"; id: number; token?: SpeechToken } | { type: "sessionError"; message: string }; /** * Messages from webview → extension host */ export type BridgeFromWebviewMessage = - | { type: "sendCommand"; command: string; requestId?: string } + | { + type: "sendCommand"; + command: string; + requestId?: string; + // Base64 data URLs of image attachments picked/dropped/pasted in + // the chat input (from chat-ui's attach button). + attachments?: string[]; + } | { type: "cancelCommand"; requestId: string } + // Developer-mode per-message delete. `permanent` chooses hard delete + // (non-recoverable) vs soft delete (recoverable "move to trash"). + | { + type: "deleteMessage"; + requestId: string; + target: "user" | "agent"; + permanent: boolean; + } // Promote a queued request so it runs next ("jump the queue"). | { type: "promoteCommand"; requestId: string } // Double-Esc gesture: cancel every queued + running entry on the session. | { type: "cancelAllQueuedAndRunning" } | { type: "openExternal"; href: string } + // Request a fresh Azure Speech authorization token from the server + // (relayed via the bridge). Correlated by `id`. + | { type: "getSpeechToken"; id: number } | { type: "connect" } | { type: "disconnect" } | { type: "getStatus" } @@ -221,4 +310,48 @@ export type BridgeFromWebviewMessage = | { type: "pcHide" } | { type: "pcDispose" } | { type: "demoCommand"; action: "continue" | "cancel" } - | { type: "demoLineCancelled"; requestId: string }; + | { type: "demoLineCancelled"; requestId: string } + // Reply to a `requestInteraction` prompt. Forwarded to the dispatcher + // via respondToInteraction. + | { type: "interactionResponse"; response: PendingInteractionResponse } + // Reply to a `requestChoice` card. Forwarded to the dispatcher via + // respondToChoice, which resolves the pending choice route so the + // agent's handleChoice callback runs. `response` is boolean (yesNo), + // number[] of selected indices (multiChoice), or { selected, remember } + // (pickRemember). + | { + type: "choiceResponse"; + choiceId: string; + response: + | boolean + | number[] + | { selected: number; remember: boolean }; + } + | { + // Submit a user feedback rating (thumbs up/down + optional category / + // comment) for an agent message. Forwarded to the dispatcher via + // recordUserFeedback, which persists it and broadcasts a + // `userFeedback` back to all clients. `requestId` is the bubble's + // client request id (RequestId.requestId), mirroring the + // deleteMessage -> recordUserHide path. + type: "recordUserFeedback"; + requestId: string; + rating: UserFeedbackRating; + category?: UserFeedbackCategory; + comment?: string; + includeContext?: boolean; + } + | { + // Service call routed through the host to the dispatcher: template- + // editor lookups (getTemplateSchema / getTemplateCompletion) for the + // proposeAction edit flow, or getDynamicDisplay for refreshing a + // live display. Correlated by `id`; answered with a + // `bridgeRpcResponse`. + type: "bridgeRpcRequest"; + id: number; + method: + | "getTemplateSchema" + | "getTemplateCompletion" + | "getDynamicDisplay"; + args: unknown[]; + }; diff --git a/ts/packages/vscode-shell/src/webview/azureSpeechProvider.ts b/ts/packages/vscode-shell/src/webview/azureSpeechProvider.ts new file mode 100644 index 000000000..df84d81b5 --- /dev/null +++ b/ts/packages/vscode-shell/src/webview/azureSpeechProvider.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * VS Code webview implementation of chat-ui's `SpeechInputProvider`, backed by + * Azure Speech. The browser Web Speech API is not functional inside VS Code + * webviews, so the shell injects this provider to override chat-ui's default. + * + * The `speech:` config lives on the agent server; this provider obtains a + * short-lived authorization token via a host round-trip (`requestToken`) and + * runs single-shot recognition with the browser Speech SDK. It mirrors the + * Electron shell's `recognizeOnce` flow (see shell/renderer/src/speech.ts). + */ + +import * as speechSDK from "microsoft-cognitiveservices-speech-sdk"; +import type { SpeechInputProvider, SpeechState } from "chat-ui"; +import type { SpeechToken } from "@typeagent/agent-server-protocol"; + +export class VsCodeAzureSpeechProvider implements SpeechInputProvider { + private state: SpeechState = "idle"; + private resultCb?: (text: string, final: boolean) => void; + private stateCb?: (state: SpeechState) => void; + private recognizer?: speechSDK.SpeechRecognizer; + // Client-side token cache; the server also caches, but this avoids a + // host round-trip on every mic click within the token's lifetime. + private cachedToken?: SpeechToken; + + /** + * @param requestToken Fetches a fresh Azure Speech token from the host + * (relayed to the agent server). Resolves to undefined when speech is + * not configured. + */ + constructor( + private readonly requestToken: () => Promise, + ) {} + + public getState(): SpeechState { + return this.state; + } + + private setState(state: SpeechState): void { + if (this.state === state) return; + this.state = state; + this.stateCb?.(state); + } + + public onResult(cb: (text: string, final: boolean) => void): void { + this.resultCb = cb; + } + + public onStateChange(cb: (state: SpeechState) => void): void { + this.stateCb = cb; + } + + public start(): void { + if (this.state === "listening") { + // Toggle off an in-progress recognition. + this.stop(); + return; + } + void this.startSingleShot(); + } + + private async getToken(): Promise { + if (this.cachedToken && this.cachedToken.expire > Date.now()) { + return this.cachedToken; + } + const token = await this.requestToken(); + this.cachedToken = token; + return token; + } + + private async startSingleShot(): Promise { + let token: SpeechToken | undefined; + try { + token = await this.getToken(); + } catch { + token = undefined; + } + if (!token) { + // Speech not configured / token unavailable — reflect a disabled + // mic so the user gets feedback rather than a silent no-op. + this.setState("disabled"); + return; + } + + let reco: speechSDK.SpeechRecognizer; + try { + // Identity (AAD) tokens are passed as `aad##`; + // key-issued tokens (no endpoint) are passed verbatim. + const authToken = token.endpoint + ? `aad#${token.endpoint}#${token.token}` + : token.token; + const speechConfig = speechSDK.SpeechConfig.fromAuthorizationToken( + authToken, + token.region, + ); + speechConfig.speechRecognitionLanguage = "en-US"; + const audioConfig = + speechSDK.AudioConfig.fromDefaultMicrophoneInput(); + reco = new speechSDK.SpeechRecognizer(speechConfig, audioConfig); + } catch { + this.setState("idle"); + return; + } + + this.recognizer = reco; + this.setState("listening"); + + reco.recognizing = (_s, e) => { + this.resultCb?.(e.result.text, false); + }; + reco.recognizeOnceAsync( + (result) => { + if ( + result.reason === speechSDK.ResultReason.RecognizedSpeech && + result.text + ) { + this.resultCb?.(result.text, true); + } + this.cleanup(); + }, + () => { + // Recognition error (no mic permission, network, etc.). + this.cleanup(); + }, + ); + } + + public stop(): void { + this.cleanup(); + } + + public setContinuous(_on: boolean, _waitForWakeWord?: boolean): void { + // Continuous / wake-word modes are not supported in the webview yet; + // the mic button drives single-shot recognition only. + } + + private cleanup(): void { + const reco = this.recognizer; + this.recognizer = undefined; + if (reco) { + try { + reco.close(); + } catch { + // ignore — recognizer may already be torn down + } + } + this.setState("idle"); + } +} diff --git a/ts/packages/vscode-shell/src/webview/main.ts b/ts/packages/vscode-shell/src/webview/main.ts index 078cf2f1b..c54e4babb 100644 --- a/ts/packages/vscode-shell/src/webview/main.ts +++ b/ts/packages/vscode-shell/src/webview/main.ts @@ -11,13 +11,22 @@ import { ChatPanel, ConversationBar, HistoryEntry, + formatHistorySeparatorLabel, type ConnectionStatus, } from "chat-ui"; +import type { TemplateEditServices, DynamicDisplayResult } from "chat-ui"; +import { VsCodeAzureSpeechProvider } from "./azureSpeechProvider.js"; +import type { SpeechToken } from "@typeagent/agent-server-protocol"; import chatPanelStyles from "chat-ui/styles"; import completionUiStyles from "@typeagent/completion-ui/styles.css"; import vscodeThemeStyles from "./vscode-theme.css"; import { QueueStateMirror } from "@typeagent/dispatcher-types"; -import type { QueuedRequest, QueueSnapshot } from "@typeagent/dispatcher-types"; +import type { + PendingInteractionRequest, + PendingInteractionResponse, + QueuedRequest, + QueueSnapshot, +} from "@typeagent/dispatcher-types"; // Inject the chat-ui base styles first, then the completion-ui dropdown // styles, then the VS Code theme overlay so it can override defaults via @@ -95,6 +104,30 @@ const conversationBar = new ConversationBar(conversationBarRootEl, { window.addEventListener("beforeunload", () => conversationBar.dispose()); +// Azure Speech token round-trip: the mic provider asks the extension host +// (which relays to the agent server that owns the `speech:` config) for a +// short-lived token. Correlated by `id`; see "speechTokenResponse" below. +let nextSpeechTokenId = 1; +const pendingSpeechToken = new Map< + number, + (token: SpeechToken | undefined) => void +>(); +function requestSpeechToken( + timeoutMs = 15_000, +): Promise { + const id = nextSpeechTokenId++; + return new Promise((resolve) => { + const timer = setTimeout(() => { + if (pendingSpeechToken.delete(id)) resolve(undefined); + }, timeoutMs); + pendingSpeechToken.set(id, (token) => { + clearTimeout(timer); + resolve(token); + }); + vscode.postMessage({ type: "getSpeechToken", id }); + }); +} + const chatPanel = new ChatPanel(rootEl, { platformAdapter: { // Open links via the extension host — webviews can't call window.open @@ -103,12 +136,53 @@ const chatPanel = new ChatPanel(rootEl, { vscode.postMessage({ type: "openExternal", href }); }, }, - onSend: (text: string, _attachments, requestId: string) => { - vscode.postMessage({ type: "sendCommand", command: text, requestId }); + // The browser Web Speech API doesn't work inside VS Code webviews, so + // override chat-ui's default with an Azure-backed provider fed by a + // server-vended token. + speechProvider: new VsCodeAzureSpeechProvider(requestSpeechToken), + onSend: (text: string, attachments, requestId: string) => { + vscode.postMessage({ + type: "sendCommand", + command: text, + requestId, + attachments, + }); }, onCancel: (requestId: string) => { vscode.postMessage({ type: "cancelCommand", requestId }); }, + onDeleteMessage: (requestId, target, permanent) => { + vscode.postMessage({ + type: "deleteMessage", + requestId: requestId.requestId, + target, + permanent, + }); + }, + // Refresh callback for live-updating ("dynamic") displays. chat-ui's + // setDynamicDisplay schedules the timer; each tick calls this to fetch + // fresh content, routed through the host to dispatcher.getDynamicDisplay. + // "html" is the render format (same choice as the visualStudio and + // browser webview hosts). + getDynamicDisplay: (source: string, displayId: string) => + bridgeRpc("getDynamicDisplay", [ + source, + "html", + displayId, + ]) as Promise, + // User rated an agent message (thumbs up/down). Forward to the host, + // which calls dispatcher.recordUserFeedback; the resulting broadcast + // (userFeedback) updates the bubble via applyFeedback. + onFeedback: (requestId, rating, category, comment, includeContext) => { + vscode.postMessage({ + type: "recordUserFeedback", + requestId: requestId.requestId, + rating, + category, + comment, + includeContext, + }); + }, }); // `onDemoAction` is exposed as a settable public property on ChatPanel @@ -123,6 +197,171 @@ chatPanel.onDemoAction = (action: "continue" | "cancel") => { // answers with `pcState` (handled in the message switch below). chatPanel.attachCompletion((msg) => vscode.postMessage(msg)); +// ─── Server-driven interactions (dev-mode action confirmation / questions) ─── +// The dispatcher (via the agent-server) can block a request on an interactive +// prompt: `@config dev on --confirm` confirms each translated action before it +// runs, and agents can ask questions. The host forwards these as +// `requestInteraction`; we render them with chat-ui and reply via +// `interactionResponse`. Without this the request blocks on the server until +// the 10-minute proposeAction timeout. In-flight prompts are tracked by +// interactionId so `interactionResolved` / `interactionCancelled` (another +// client answered, or a server timeout) can tear the local UI down. +const activeInteractions = new Map(); + +// Template-editor services (schema refresh + per-field completion) for the +// proposeAction edit flow. chat-ui is framework-free, so these are injected; +// each call is routed through the host to the dispatcher and correlated by id. +let nextBridgeRpcId = 1; +const pendingBridgeRpc = new Map< + number, + { resolve: (v: unknown) => void; reject: (e: unknown) => void } +>(); +function bridgeRpc( + method: "getTemplateSchema" | "getTemplateCompletion" | "getDynamicDisplay", + args: unknown[], + timeoutMs = 30_000, +): Promise { + const id = nextBridgeRpcId++; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (pendingBridgeRpc.delete(id)) { + reject(new Error(`bridgeRpc '${method}' timed out`)); + } + }, timeoutMs); + pendingBridgeRpc.set(id, { + resolve: (v) => { + clearTimeout(timer); + resolve(v); + }, + reject: (e) => { + clearTimeout(timer); + reject(e); + }, + }); + vscode.postMessage({ type: "bridgeRpcRequest", id, method, args }); + }); +} +const templateServices: TemplateEditServices = { + getTemplateSchema: (templateAgentName, templateName, data) => + bridgeRpc("getTemplateSchema", [ + templateAgentName, + templateName, + data, + ]) as ReturnType, + getTemplateCompletion: ( + templateAgentName, + templateName, + data, + propertyName, + ) => + bridgeRpc("getTemplateCompletion", [ + templateAgentName, + templateName, + data, + propertyName, + ]) as ReturnType, +}; + +// Render a server-driven interaction and reply with the user's response. +// Mirrors the Electron shell (chatPanelBridge.ts requestInteraction). +function handleRequestInteraction( + interaction: PendingInteractionRequest, +): void { + const ac = new AbortController(); + activeInteractions.set(interaction.interactionId, ac); + void (async () => { + let response: PendingInteractionResponse; + try { + if (interaction.type === "question") { + const value = await chatPanel.addChoicePrompt( + interaction.message, + interaction.choices.map((label, index) => ({ + label, + value: index, + })), + { defaultValue: interaction.defaultId, signal: ac.signal }, + ); + response = { + interactionId: interaction.interactionId, + type: "question", + value, + }; + } else { + const value = await chatPanel.proposeActionEdit( + interaction.actionTemplates, + interaction.source, + templateServices, + ); + response = { + interactionId: interaction.interactionId, + type: "proposeAction", + value, + }; + } + } catch (e) { + // Aborted (resolved/cancelled by another client or server timeout) + // — nothing to send. + activeInteractions.delete(interaction.interactionId); + if (!ac.signal.aborted) { + console.error("[requestInteraction] failed", e); + } + return; + } + if (ac.signal.aborted) { + activeInteractions.delete(interaction.interactionId); + return; + } + activeInteractions.delete(interaction.interactionId); + if (ac.signal.aborted) return; + vscode.postMessage({ type: "interactionResponse", response }); + })().catch((e) => console.error("[requestInteraction] failed", e)); +} + +// Render a non-blocking choice card (yes/no, multi-select, or pick+remember) +// and reply with the user's response. Mirrors the Electron shell +// (chatPanelBridge.ts requestChoice). The prompt text is already rendered as +// the action's displayContent, so `showMessage:false` suppresses the card's +// duplicate copy and `requestId` anchors the buttons onto that agent bubble so +// the message and the buttons read as one card. +function handleRequestChoice(msg: { + choiceId: string; + choiceType: "yesNo" | "multiChoice" | "pickRemember"; + message: string; + choices: string[]; + checkboxLabel?: string; + requestId?: string; +}): void { + void (async () => { + const opts = { showMessage: false, requestId: msg.requestId }; + let response: + | boolean + | number[] + | { selected: number; remember: boolean }; + if (msg.choiceType === "yesNo") { + response = await chatPanel.askYesNo(msg.message, undefined, opts); + } else if (msg.choiceType === "pickRemember") { + response = await chatPanel.addPickRememberPrompt( + msg.message, + msg.choices, + msg.checkboxLabel ?? "Remember this for next time", + opts, + ); + } else { + const index = await chatPanel.addChoicePrompt( + msg.message, + msg.choices.map((label, i) => ({ label, value: i })), + opts, + ); + response = [index]; + } + vscode.postMessage({ + type: "choiceResponse", + choiceId: msg.choiceId, + response, + }); + })().catch((e) => console.error("[requestChoice] failed", e)); +} + // Mirror of dispatcher's queue lifecycle (requestQueued / requestStarted // / requestCancelled / queueStateChanged) so we can dedupe "⚠ Cancelled" // across paths and support double-Esc cancel-all without round-tripping. @@ -524,6 +763,9 @@ window.addEventListener("message", (event) => { case "userInfo": chatPanel.setUserInfo(msg.name); break; + case "developerMode": + chatPanel.setDeveloperMode(msg.enabled); + break; case "sessionChanged": currentSessionId = msg.sessionId; isSwitching = false; @@ -712,6 +954,15 @@ window.addEventListener("message", (event) => { // races or is lost. const replayEntries = toChatPanelHistory(msg.entries); void chatPanel.replayHistoryStreaming(replayEntries).then(() => { + // Divider between replayed history and live messages, + // matching the Electron shell. The bridge only sends + // historyReplay when prior history exists, but guard + // defensively. + if (msg.entries.length > 0) { + chatPanel.addHistorySeparator( + formatHistorySeparatorLabel(msg.entries), + ); + } chatPanel.setHistoryLoading(false); chatPanel.setEnabled(isConnected); }); @@ -871,6 +1122,58 @@ window.addEventListener("message", (event) => { reconcileQueueChips(result.previous, msg.snapshot); break; } + case "requestInteraction": + handleRequestInteraction(msg.interaction); + break; + case "requestChoice": + handleRequestChoice(msg); + break; + case "setDynamicDisplay": + // Register/refresh a live-updating display. chat-ui owns the + // refresh timer and calls back via the getDynamicDisplay option + // wired at construction. + chatPanel.setDynamicDisplay( + msg.source, + msg.displayId, + msg.nextRefreshMs, + ); + break; + case "userFeedback": + // A rating was recorded (by us or a peer) — mirror it onto the + // matching bubble. + chatPanel.applyFeedback(msg.entry); + break; + case "interactionResolved": + case "interactionCancelled": { + // Another client answered, or the server cancelled/timed out the + // interaction — abort our local prompt so it stops waiting. + const ac = activeInteractions.get(msg.interactionId); + if (ac) { + activeInteractions.delete(msg.interactionId); + ac.abort(); + } + break; + } + case "bridgeRpcResponse": { + const pending = pendingBridgeRpc.get(msg.id); + if (pending) { + pendingBridgeRpc.delete(msg.id); + if (msg.error !== undefined) { + pending.reject(new Error(msg.error)); + } else { + pending.resolve(msg.result); + } + } + break; + } + case "speechTokenResponse": { + const resolve = pendingSpeechToken.get(msg.id); + if (typeof resolve === "function") { + pendingSpeechToken.delete(msg.id); + resolve(msg.token); + } + break; + } } }); diff --git a/ts/packages/vscode-shell/src/webview/vscode-theme.css b/ts/packages/vscode-shell/src/webview/vscode-theme.css index 063bcb6db..3de7f81fa 100644 --- a/ts/packages/vscode-shell/src/webview/vscode-theme.css +++ b/ts/packages/vscode-shell/src/webview/vscode-theme.css @@ -175,7 +175,10 @@ color: transparent !important; } .agent-icon { - background-color: var(--vscode-badge-background, #2a2a40) !important; + background-color: var( + --vscode-badge-background, + var(--vscode-editorWidget-background, #3c3c3c) + ) !important; color: var(--vscode-badge-foreground, var(--vscode-foreground)) !important; } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index bbe816361..094a1c6ea 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -6225,6 +6225,9 @@ importers: markdown-it: specifier: ^14.2.0 version: 14.2.0 + microsoft-cognitiveservices-speech-sdk: + specifier: 1.43.1 + version: 1.43.1 ws: specifier: ^8.21.0 version: 8.21.0 @@ -6371,46 +6374,46 @@ packages: resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.162': - resolution: {integrity: sha512-hafbfEtDeYko1rYCgIBAQbYnFXzd/hHf3IcoaD8mmlCQOAQhKA8gT/RPdLuuzQHdOjEgqDGTNOU+IjwL+msYcw==} + resolution: {integrity: sha1-UZ0hvQIcY+n7qsSOIbo6uU1Lw7s=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.162.tgz} cpu: [arm64] os: [darwin] '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.162': - resolution: {integrity: sha512-BNg2Mh/4zc2Jsgpq7Mj8+UH8iJ9xzHS/0CmusBMik0H2Bn7Hra5R/f+csAfZOzSflQl4CNQdGOB2bir7d2n8Ww==} + resolution: {integrity: sha1-Di4TOiXULnuKmWMbA779X2NeNMU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.162.tgz} cpu: [x64] os: [darwin] '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.162': - resolution: {integrity: sha512-Jr4cyfqzb5V2+p/1PynIfGROOI0JNtV3vBMAgckAdtDzpIFk4mV2T8936tsZzgZr04y40YH1HlQpnJDr92I+Fw==} + resolution: {integrity: sha1-CqED3S+fXiQ9l6HMhSYpoWhse14=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.162.tgz} cpu: [arm64] os: [linux] libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.162': - resolution: {integrity: sha512-zCXYSimaXWQKZASfDJkoKXQr//toYDGIi16wKDh02Rqcr4mqFi9f5SBw/UCyimkGyYkNx3e+bmC+o/tFrLSTWw==} + resolution: {integrity: sha1-Tlduj+RMdbVhy1nGkU0mX9Vvj/g=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.162.tgz} cpu: [arm64] os: [linux] libc: [glibc] '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.162': - resolution: {integrity: sha512-gW9Gpk7W3w3zGFHBDyY8uer/PE6T0pB+emN1aafZAomfseIH1ixJ6ya5Fw2cIS/K0/4oR2pvu4AprlbRBtr45Q==} + resolution: {integrity: sha1-HFdhO+npa0m+rUVY0FEZAsJGmPY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.162.tgz} cpu: [x64] os: [linux] libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.162': - resolution: {integrity: sha512-FO2+zDuSTsZ/5MqxIwExLxG0c2auA5wO6iICwZdUOWtboC6yU2AEgp4mzF0jbe1UOP0J27uODFpvz7uRpWipkg==} + resolution: {integrity: sha1-5hP03u3nOsIF+bKqgQfsjnDe2KY=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.162.tgz} cpu: [x64] os: [linux] libc: [glibc] '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.162': - resolution: {integrity: sha512-TZQifFDBdhzt1u6wbbpQ2AZcRkhNVYx1iZVfydAbs3L7ZMK264LjRPytBK4n4S413Is1XyLHbGMvEUNA3N+Tng==} + resolution: {integrity: sha1-mt5LQqODj/hQfpxFVJcgxbPY0Sw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.162.tgz} cpu: [arm64] os: [win32] '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.162': - resolution: {integrity: sha512-8ZYDgNxkGp47xwpcEZ6/qUXd4BByA8hTnNf4fJD6+P1wWi7Ofxc/d5jwXSYwajYvBvbbktQDthmGzjtoK3lXUw==} + resolution: {integrity: sha1-fIVcoQnYv9VFem7QejfO6c1/l78=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.162.tgz} cpu: [x64] os: [win32] @@ -7124,7 +7127,7 @@ packages: resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + resolution: {integrity: sha1-u1BFecHK6SPmV2pPXaQ9Jfl729k=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@colors/colors/-/colors-1.5.0.tgz} engines: {node: '>=0.1.90'} '@cspotcode/source-map-support@0.8.1': @@ -7237,27 +7240,27 @@ packages: engines: {node: '>=16.4'} '@electron/windows-sign@1.2.2': - resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + resolution: {integrity: sha1-jOqtUtXB6xhwL0gQPV87x8M4+p0=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@electron/windows-sign/-/windows-sign-1.2.2.tgz} engines: {node: '>=14.14'} hasBin: true '@emnapi/core@1.11.0': - resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + resolution: {integrity: sha1-imVQQtu7ENAmZnDJkDw0pwAccFs=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/core/-/core-1.11.0.tgz} '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + resolution: {integrity: sha1-ueEGTzprFjHiQeY460jXNr/TcqY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/core/-/core-1.11.1.tgz} '@emnapi/runtime@1.11.0': - resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + resolution: {integrity: sha1-zhazZ0/3Jmu/UPlmi96KBPMBTU4=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/runtime/-/runtime-1.11.0.tgz} '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + resolution: {integrity: sha1-WPHz1dgamxL3k6tojJY3GQECfCQ=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/runtime/-/runtime-1.11.1.tgz} '@emnapi/runtime@1.4.0': - resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + resolution: {integrity: sha1-j1Cb8QWaVVHI/oKaHE6R2zX9++4=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/runtime/-/runtime-1.4.0.tgz} '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + resolution: {integrity: sha1-TJO+z1v6OxPRu9zAau44MhrYE5o=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz} '@esbuild-plugins/node-globals-polyfill@0.2.3': resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} @@ -7265,607 +7268,607 @@ packages: esbuild: '*' '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + resolution: {integrity: sha1-xxhKMmUz/N8bjuBzPiHHE7l1V18=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz} engines: {node: '>=12'} cpu: [ppc64] os: [aix] '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + resolution: {integrity: sha1-gPy+NhMOWLdnBRHoiLjoiiWe12w=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [aix] '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + resolution: {integrity: sha1-grdPkqp41yC3FBYpOfskjJCt31M=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [aix] '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + resolution: {integrity: sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [aix] '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + resolution: {integrity: sha1-Cdm0NXeA2p6jp9+4M6Hx/0ObQFI=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [android] '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + resolution: {integrity: sha1-iqSWX40KeYLcIXNL9mATI6Ztp1I=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [android] '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + resolution: {integrity: sha1-94y4oxIfwgWlMoWtskly2zhdGF0=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [android] '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + resolution: {integrity: sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [android] '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + resolution: {integrity: sha1-mwQ4T7dxkm36bXrQQyTssqubLig=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [android] '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + resolution: {integrity: sha1-MAcSEB9/UPHSYnoWLm4JsQm2dno=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm] os: [android] '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + resolution: {integrity: sha1-WT4QoUULv8rGyzIfYfRoRTusIJ0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm] os: [android] '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + resolution: {integrity: sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm] os: [android] '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + resolution: {integrity: sha1-KZGOwtt1TO3LbBsE3ozWVHr2Rh4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [android] '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + resolution: {integrity: sha1-h9+ycWEgK9yVjvSLthsJx1j67hY=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [android] '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + resolution: {integrity: sha1-RTFD0HMyYDPS0iyvnkjeS64nSwc=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [android] '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + resolution: {integrity: sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + resolution: {integrity: sha1-5JW1OWYOUWkPOSivUKdvsKbM/yo=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [darwin] '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + resolution: {integrity: sha1-eRl4mOwf90XSHAceHHzDyALwwf0=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [darwin] '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + resolution: {integrity: sha1-byMAD7m0C34Et9BgbAaTvQYy8yI=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [darwin] '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + resolution: {integrity: sha1-EDSyZFf8iGNo/mG70J9lP2r6jlQ=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + resolution: {integrity: sha1-wTg4+lc3KDmr3dyR1xVCzuouHiI=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [darwin] '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + resolution: {integrity: sha1-FGQAqFYhM/RcTS6tzzfd0JcYB54=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [darwin] '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + resolution: {integrity: sha1-Jzk90YuxJjxmOXnF8VduAMLQJL4=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [darwin] '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + resolution: {integrity: sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + resolution: {integrity: sha1-ZGuYmqIL+J/Qcd1dv61po1QuVQ4=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + resolution: {integrity: sha1-HF+bpyBuFY/SskxZ+i0si7R8oP4=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + resolution: {integrity: sha1-IuRjj6UC0cACcHcyTJdkDjrfOmI=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + resolution: {integrity: sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + resolution: {integrity: sha1-qmFc/ICvlU00WJBuOMoiwYz1wmE=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [freebsd] '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + resolution: {integrity: sha1-6mMfSja+qsS5J5+g/MbKKerusrM=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [freebsd] '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + resolution: {integrity: sha1-kiS45P6pJM4hlOPvw+muv4IhktY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [freebsd] '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + resolution: {integrity: sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + resolution: {integrity: sha1-cKxvoU9ct+H3+Ie8/7aArQmSK1s=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [linux] '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + resolution: {integrity: sha1-4QZrzlg5TxsRQd7shVel8KIvWXc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [linux] '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + resolution: {integrity: sha1-T10cJ1J9gXs1aEriFBnlfCvaCWY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [linux] '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + resolution: {integrity: sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + resolution: {integrity: sha1-/G/RGorKVsH284lPK+oEefj2Jrk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [linux] '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + resolution: {integrity: sha1-RSzWayCTLQi9xTqLYcDjC69DSLk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm] os: [linux] '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + resolution: {integrity: sha1-uenQcMjBwESc8Ssg6sN9cKRZWSE=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm] os: [linux] '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + resolution: {integrity: sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + resolution: {integrity: sha1-MnH1Oz+T49CT1RjRZJ1taNNG7eI=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [linux] '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + resolution: {integrity: sha1-sk+KzEW89UGSx/LzvhtT5lUer+A=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz} engines: {node: '>=18'} cpu: [ia32] os: [linux] '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + resolution: {integrity: sha1-P4D7aWqpYFGpQEfzXIWwiyHDb54=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz} engines: {node: '>=18'} cpu: [ia32] os: [linux] '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + resolution: {integrity: sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz} engines: {node: '>=18'} cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + resolution: {integrity: sha1-7WLgQjjFcCauqDHFoTC3PA+fJt8=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz} engines: {node: '>=12'} cpu: [loong64] os: [linux] '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + resolution: {integrity: sha1-+c//p/yDIlcfvEyLMmjK8VvYGtA=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz} engines: {node: '>=18'} cpu: [loong64] os: [linux] '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + resolution: {integrity: sha1-m+HywoIQsT67QVYiG7o1b+FnUgU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz} engines: {node: '>=18'} cpu: [loong64] os: [linux] '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + resolution: {integrity: sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz} engines: {node: '>=18'} cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + resolution: {integrity: sha1-55uOtIvzsQb63sGsgkD7l7TmTL4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz} engines: {node: '>=12'} cpu: [mips64el] os: [linux] '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + resolution: {integrity: sha1-V1oUvXRkT/q4ka3H1+YNJ1KW8s0=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz} engines: {node: '>=18'} cpu: [mips64el] os: [linux] '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + resolution: {integrity: sha1-SrXuZ6Pfy8tej9eIPa5uc1sRY7g=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz} engines: {node: '>=18'} cpu: [mips64el] os: [linux] '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + resolution: {integrity: sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz} engines: {node: '>=18'} cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + resolution: {integrity: sha1-XyIDhgoUO5kZ04PvdXNSH7FUw+Q=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz} engines: {node: '>=12'} cpu: [ppc64] os: [linux] '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + resolution: {integrity: sha1-dbmccKlfvV93OddpK+/mBgFZGGk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [linux] '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + resolution: {integrity: sha1-2seMaJ9kmUWcQyHlwVAywSMH5+o=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [linux] '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + resolution: {integrity: sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + resolution: {integrity: sha1-B7yv2ZMi1a9i9hjLnmqbf0u4Jdw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz} engines: {node: '>=12'} cpu: [riscv64] os: [linux] '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + resolution: {integrity: sha1-LjJZRAMhpE553fdTXDJQV9qHXNY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz} engines: {node: '>=18'} cpu: [riscv64] os: [linux] '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + resolution: {integrity: sha1-BQ99OzVcOpgwjpNbxNYyXakbACc=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz} engines: {node: '>=18'} cpu: [riscv64] os: [linux] '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + resolution: {integrity: sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz} engines: {node: '>=18'} cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + resolution: {integrity: sha1-t8z2hnUdaj5EuGJ6uryL4+9i2N4=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz} engines: {node: '>=12'} cpu: [s390x] os: [linux] '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + resolution: {integrity: sha1-F2dsq7/lko2lsqDW311YzQjbJmM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz} engines: {node: '>=18'} cpu: [s390x] os: [linux] '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + resolution: {integrity: sha1-1h9xXOYdQ/5YRK0Nj0Y/iMvk/vY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz} engines: {node: '>=18'} cpu: [s390x] os: [linux] '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + resolution: {integrity: sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz} engines: {node: '>=18'} cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + resolution: {integrity: sha1-bY8Mdo4HDmQwmvgAS7lOaKsrs7A=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [linux] '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + resolution: {integrity: sha1-BYN3VoXKggZtBMNQfwlSTTzXowY=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [linux] '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + resolution: {integrity: sha1-yo4apHj8ggkle/Osj3nE3CmC8yo=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [linux] '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + resolution: {integrity: sha1-bww84MtkxTS3DExF7LLBbTTjXf0=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [linux] '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + resolution: {integrity: sha1-8ExAScsuJS/paxb+2Q9wdGsT9KQ=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + resolution: {integrity: sha1-FlDywblI3us++Ujy/DBhRyPAlpA=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + resolution: {integrity: sha1-i813B3oNzjN4tXT+2ybSolO3PTY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + resolution: {integrity: sha1-u+Qw9g03jsuI3sshnGAmZzh6YEc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [netbsd] '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + resolution: {integrity: sha1-d9oNCg2CbXySHuo9QCklSLJYoHY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [netbsd] '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + resolution: {integrity: sha1-ZXcqs0LEszGb8HBaIRBQqsG24yA=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [netbsd] '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + resolution: {integrity: sha1-5/sqAemcgwyU5mI82f77TI+1g0c=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [netbsd] '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + resolution: {integrity: sha1-Ypb1hnrt7yioGyKrIAnHhqlS3M0=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + resolution: {integrity: sha1-N+18+mZUnXlVhS/ON9DD3k5xXqE=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + resolution: {integrity: sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + resolution: {integrity: sha1-mdHPKTcnlWDSEEgh9czOIgyyr3A=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [openbsd] '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + resolution: {integrity: sha1-+NIzAzYOJ7Fs8GWyO7/0PBQUJnk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [openbsd] '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + resolution: {integrity: sha1-Ab89OFhV71DLM9t8S1L5V8NM0Xk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [openbsd] '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + resolution: {integrity: sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [openbsd] '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + resolution: {integrity: sha1-SeC3aHRKOSS+DX/ZfdbOmykj2I0=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + resolution: {integrity: sha1-bB+Us0CGWZqr2k6sj2OClLmHdBA=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + resolution: {integrity: sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + resolution: {integrity: sha1-CHQVEsENUpVmurqDe0/gUsjzSHs=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [sunos] '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + resolution: {integrity: sha1-pu19Z3jWflKMgfsWWyP0kRubE9Y=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [sunos] '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + resolution: {integrity: sha1-Sw3ReuCmlB0tD9NakGOSUXBxqQ0=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [sunos] '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + resolution: {integrity: sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + resolution: {integrity: sha1-Z1tzhTmEESQHNQFhRKsumaYPx10=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [win32] '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + resolution: {integrity: sha1-msFMN44bZTrxfQjn0840yu9YcyM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz} engines: {node: '>=18'} cpu: [arm64] os: [win32] '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + resolution: {integrity: sha1-NBk6tVZdb/aMqSisBL51ECzLLnc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz} engines: {node: '>=18'} cpu: [arm64] os: [win32] '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + resolution: {integrity: sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + resolution: {integrity: sha1-G/w86YqmypoJaeTSr3IUTFnBGTs=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [win32] '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + resolution: {integrity: sha1-kYlC3LuzXMFPyjmvuRteaj0Scmc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz} engines: {node: '>=18'} cpu: [ia32] os: [win32] '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + resolution: {integrity: sha1-62fw5EglFdjBiU7eYxwyek2p/E0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz} engines: {node: '>=18'} cpu: [ia32] os: [win32] '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + resolution: {integrity: sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz} engines: {node: '>=18'} cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + resolution: {integrity: sha1-rK01HVgtFXuxRVNdsqb/U91RS1w=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [win32] '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + resolution: {integrity: sha1-m9rYF2vngRrRSNH4dyNZBB9GxsU=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz} engines: {node: '>=18'} cpu: [x64] os: [win32] '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + resolution: {integrity: sha1-j+MLMIi4m0hzw6bMh1l645IMCos=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz} engines: {node: '>=18'} cpu: [x64] os: [win32] '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + resolution: {integrity: sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -8045,267 +8048,267 @@ packages: engines: {node: '>=18'} '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + resolution: {integrity: sha1-71taB4YoBfHoFFo3fIum6YgTygg=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + resolution: {integrity: sha1-bgcy3K3hJrZnCveqFwYLkmg16oY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + resolution: {integrity: sha1-4D00Uc2eZk+qcpSMxwpAPqQGPWE=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + resolution: {integrity: sha1-Gbwd1uum1alig0mLnJ9AEYDunHs=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + resolution: {integrity: sha1-RHxQJnAMAamTx4BOuK9fbphowH8=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz} cpu: [arm64] os: [darwin] '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + resolution: {integrity: sha1-KJTAy4fUInbDiJlC6OLbUXpJLEM=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz} cpu: [arm64] os: [darwin] '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + resolution: {integrity: sha1-4EVvj3xiP52/vcdzg8qnIoHYYGI=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz} cpu: [x64] os: [darwin] '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + resolution: {integrity: sha1-5jaB9FOalK+c0XJG7YiBc0OG+Mw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz} cpu: [x64] os: [darwin] '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + resolution: {integrity: sha1-l5scZsmpH3/yiTVW7yZ/kOvlFwQ=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz} cpu: [arm64] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + resolution: {integrity: sha1-sbKIs2hks7zlRa2R+m2tzxpK0xg=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz} cpu: [arm64] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + resolution: {integrity: sha1-mfki1OFSFuwgXctokbchv9KIQZc=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz} cpu: [arm] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + resolution: {integrity: sha1-uSYN0evm+eO9vL3KydKsEl81hS0=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz} cpu: [arm] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + resolution: {integrity: sha1-S4Ps8qgpBXIis4hIx7Ai57TQeqc=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz} cpu: [ppc64] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + resolution: {integrity: sha1-iAtGeACeWiCArxkjMrALCq+KSN4=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz} cpu: [riscv64] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + resolution: {integrity: sha1-+KXrHzdKCC9ys/ReL7JbgRiopc4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz} cpu: [s390x] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + resolution: {integrity: sha1-dPNDyOEPrYIbOPdc7TBIiTncWew=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz} cpu: [s390x] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + resolution: {integrity: sha1-1MRhnN0Vd3SQbhV3DuEZkxx+9eA=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz} cpu: [x64] os: [linux] libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + resolution: {integrity: sha1-30GD6L2EEPfWG2aFmjXt6rClMc4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz} cpu: [x64] os: [linux] libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + resolution: {integrity: sha1-Fmd42g9I3Sve0fowM87mtYjw1dU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz} cpu: [arm64] os: [linux] libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + resolution: {integrity: sha1-yNa0ghHfZxN1QQB+6NG3sfjKjgY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz} cpu: [arm64] os: [linux] libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + resolution: {integrity: sha1-k3lOTXcgsHf8rT4CmC8vHCRnUf8=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz} cpu: [x64] os: [linux] libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + resolution: {integrity: sha1-vhHHW+5bCAy+4xoVOod5RI+Rn3U=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz} cpu: [x64] os: [linux] libc: [musl] '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + resolution: {integrity: sha1-7bBpfnqCecn8gppg/DVkTEg5uyI=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] libc: [glibc] '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + resolution: {integrity: sha1-eqd2TvnAAfFeYQVG1C/OVpEXkMw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] libc: [glibc] '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + resolution: {integrity: sha1-QiwaNS57WDKEJXfcUWArzVtvXv8=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] libc: [glibc] '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + resolution: {integrity: sha1-X7DDaV3RJSLTnD/3pryBZGF4Cg0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + resolution: {integrity: sha1-nCE6gVIKIMr2aXjz1MB0Vv8uCBM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + resolution: {integrity: sha1-zdKBgndOrb4E9iZ1oWqrvMuDP2A=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] libc: [glibc] '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + resolution: {integrity: sha1-9cB3kmtI6X5KBNAE368XWXIFlmc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] libc: [glibc] '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + resolution: {integrity: sha1-k+rGAbnzKbsnkX4OGQmMci1jDfc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] libc: [glibc] '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + resolution: {integrity: sha1-2Abgr9ca5ndcyH8NqPLQOnwiCcs=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] libc: [glibc] '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + resolution: {integrity: sha1-VavHzXVP/KUAK2wrcZq9/IRoGag=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + resolution: {integrity: sha1-JSl1uRWJT7MVr13uoXRlHiCNPWs=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + resolution: {integrity: sha1-1lFe6XG7YvcwAaSCm52GWhG3cIY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + resolution: {integrity: sha1-P0YJrF2O+Ox9re6AtWCWGmD9T0g=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + resolution: {integrity: sha1-2Xl4rsfFIS+ZlxTy9bc2RX4S7p8=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] libc: [musl] '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + resolution: {integrity: sha1-b0TzKDBp2TW7XKWBMVNXLz5vYaE=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + resolution: {integrity: sha1-LxWAOqYm+MWd18nQu8dm8atSz6A=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + resolution: {integrity: sha1-Nwbp46w1/d/ByH+U6Enxt1MHzgo=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + resolution: {integrity: sha1-GgyDmkDFNR6YhWKMhfLl39ArUqk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + resolution: {integrity: sha1-C3EWZZmwSeAy8IX7kmPgL05HiN4=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + resolution: {integrity: sha1-VvAJYv8MTg65PTSgR9KfqZXj40I=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + resolution: {integrity: sha1-qB/7AOaSZ80KHWJurtuKhDCysvg=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -8901,76 +8904,76 @@ packages: engines: {node: '>=14.0.0'} '@napi-rs/canvas-android-arm64@0.1.72': - resolution: {integrity: sha512-OW99TDJEdfOhpJWQ7SXFsQi1BXd6UFuWM8AoQvJ0SQMHWY/iwuopmb1UqGV6Df9aM/SWxvCWBN/onjeCM8KVKQ==} + resolution: {integrity: sha1-vByo4D7SymTYAwuQ+R+gmF6+llo=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.72.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [android] '@napi-rs/canvas-darwin-arm64@0.1.72': - resolution: {integrity: sha512-gB8Pn/4GdS+B6P4HYuNqPGx8iQJ16Go1D6e5hIxfUbA/efupVGZ7e3OMGWGCUgF0vgbEPEF31sPzhcad4mdR5g==} + resolution: {integrity: sha1-3Ett1Bq6ObuF27J57nM5DA9YWHY=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.72.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] '@napi-rs/canvas-darwin-x64@0.1.72': - resolution: {integrity: sha512-x1zKtWVSnf+yLETHdSDAFJ1w6bctS/V2NP0wskTTBKkC+c/AmI2Dl+ZMIW11gF6rilBibrIzBeXJKPzV0GMWGA==} + resolution: {integrity: sha1-UviBC7zuWmg9sxzlEfcUlp/KVEk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.72.tgz} engines: {node: '>= 10'} cpu: [x64] os: [darwin] '@napi-rs/canvas-linux-arm-gnueabihf@0.1.72': - resolution: {integrity: sha512-Ef6HMF+TBS+lqBNpcUj2D17ODJrbgevXaVPtr2nQFCao5IvoEhVMdmVwWk5YiI+GcgbAkg5AF3LiU47RoSY5yg==} + resolution: {integrity: sha1-REpnSdEIVoU/ZmTWY7MAMcGvg7c=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.72.tgz} engines: {node: '>= 10'} cpu: [arm] os: [linux] '@napi-rs/canvas-linux-arm64-gnu@0.1.72': - resolution: {integrity: sha512-i1tWu+Li1Z6G4t+ckT38JwuB/cAAREV6H8VD3dip2yTYU+qnLz6kG4i+whm+SEQb1e4vk3xA1lKnjYx3jlOy8g==} + resolution: {integrity: sha1-dqP4Pyr7RKq7vdfx8nGrji3N60c=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.72.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.72': - resolution: {integrity: sha512-Mu+2hHZAT9SdrjiRtCxMD/Unac8vqVxF/p+Tvjb5sN1NZkLGu+l7WIfrug8aeX150OwrYgAvsR4mhrm0BZvLxg==} + resolution: {integrity: sha1-g8HYY9DLbHtldZ7e3tTsMZjudfA=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.72.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.72': - resolution: {integrity: sha512-xBPG/ImL58I4Ep6VM+sCrpwl8rE/8e7Dt9U7zzggNvYHrWD13vIF3q5L2/N9VxdBMh1pee6dBC/VcaXLYccZNQ==} + resolution: {integrity: sha1-Bc2vO7Q4NFgaSCR3wB4h/XaAuVQ=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.72.tgz} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.72': - resolution: {integrity: sha512-jkC8L+QovHpzQrw+Jm1IUqxgLV5QB1hJ1cR8iYzxNRd0TOF7YfxLaIGxvd/ReRi9r48JT6PL7z2IGT7TqK8T4w==} + resolution: {integrity: sha1-7tgu9Mct71636zaGN40vfWwUKug=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.72.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.72': - resolution: {integrity: sha512-PwPdPmHgJYnTMUr8Gff80eRVdpGjwrxueIqw+7v4aeFxbQjmQ+paa2xaGedFtkvdS2Dn5z8a0mVlrlbSfec+1Q==} + resolution: {integrity: sha1-vba03KCLqUXvYVsnkRwkGnrF4Xs=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.72.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.72': - resolution: {integrity: sha512-hZhXJZZ/2ZjkAoOtyGUs3Mx6jA4o9ESbc5bk+NKYO6thZRvRNA7rqvT9WF9pZK0xcRK5EyWRymv8fCzqmSVEzg==} + resolution: {integrity: sha1-KZ/MiJzqhS2XK19rk6faFE3Zrqg=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.72.tgz} engines: {node: '>= 10'} cpu: [x64] os: [win32] '@napi-rs/canvas@0.1.72': - resolution: {integrity: sha512-ypTJ/DXzsJbTU3o7qXFlWmZGgEbh42JWQl7v5/i+DJz/HURELcSnq9ler9e1ukqma70JzmCQcIseiE/Xs6sczw==} + resolution: {integrity: sha1-owBt/7OVDEZcMVgZhuiAo2uIRUs=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas/-/canvas-0.1.72.tgz} engines: {node: '>= 10'} '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + resolution: {integrity: sha1-7TOAbQ+b6Y3HbQw9T9hy/acBtdU=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -9072,128 +9075,128 @@ packages: engines: {node: '>=14'} '@oxc-parser/binding-android-arm-eabi@0.137.0': - resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + resolution: {integrity: sha1-RBwU6z6tP/xu7JPYl/2z8aHx0po=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] '@oxc-parser/binding-android-arm64@0.137.0': - resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + resolution: {integrity: sha1-u4PSHTWG3ETXYDZ6RhA6MtnSMCY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] '@oxc-parser/binding-darwin-arm64@0.137.0': - resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + resolution: {integrity: sha1-spcPFkc2QbN7rgkpv7JotbREL7I=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] '@oxc-parser/binding-darwin-x64@0.137.0': - resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + resolution: {integrity: sha1-+/JeQPMx3LtuXKyT+75TxVfmHys=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] '@oxc-parser/binding-freebsd-x64@0.137.0': - resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + resolution: {integrity: sha1-RqPpnBnYWitYzUiXJ5kmhaanG10=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': - resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + resolution: {integrity: sha1-29njspdVO18mFiJIoUMYdP4ETvE=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': - resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + resolution: {integrity: sha1-xAIxMelPCPZaetOgjEGuohwMfos=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm64-gnu@0.137.0': - resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + resolution: {integrity: sha1-XzCWcAa+Hh5VTv/DPCBYItl0src=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.137.0': - resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + resolution: {integrity: sha1-LmUvd/ykkSaiTfxK7Ow/tHDTf34=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': - resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + resolution: {integrity: sha1-UnlPALuxjjZfFvOU/lSWqkbA+JA=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': - resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + resolution: {integrity: sha1-/0NM5LzDCgHS2Bu1SR3tzs9N/SE=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.137.0': - resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + resolution: {integrity: sha1-LZ1N5k7GCnWQ/EDBLNNjERsfDB4=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.137.0': - resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + resolution: {integrity: sha1-9eY5e7wH982X7zMu8qABGmCdLgM=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.137.0': - resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + resolution: {integrity: sha1-BYtliRhlSb2ZnEJFzasOJjlnbPQ=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.137.0': - resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + resolution: {integrity: sha1-bLFc3crP7sCtxrPDyesBfo3gJX0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.137.0': - resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + resolution: {integrity: sha1-05EeuZJZVSoB2NLH40yqaxvLrYk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] '@oxc-parser/binding-wasm32-wasi@0.137.0': - resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + resolution: {integrity: sha1-ON/pxgjQrRmwEEV4Oi8i7PDn/II=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] '@oxc-parser/binding-win32-arm64-msvc@0.137.0': - resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + resolution: {integrity: sha1-3HNMmPN7Ez65qgonQ8fnsk4SH9I=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] '@oxc-parser/binding-win32-ia32-msvc@0.137.0': - resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + resolution: {integrity: sha1-kUjC23v/rKnwmx3LWPAdEa6xaEE=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] '@oxc-parser/binding-win32-x64-msvc@0.137.0': - resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + resolution: {integrity: sha1-NICQDG/KToQwF/bemR/mr1e3NYw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -9202,105 +9205,105 @@ packages: resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} '@oxc-resolver/binding-android-arm-eabi@11.21.3': - resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + resolution: {integrity: sha1-98pC2UYU7sl+ILmvqnFz79YBnuk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz} cpu: [arm] os: [android] '@oxc-resolver/binding-android-arm64@11.21.3': - resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + resolution: {integrity: sha1-9yNoLFm03+FD7cttNk1aA94+lJ4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz} cpu: [arm64] os: [android] '@oxc-resolver/binding-darwin-arm64@11.21.3': - resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} + resolution: {integrity: sha1-58TzdByHkRxAxCmiCgW3gTacq6o=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz} cpu: [arm64] os: [darwin] '@oxc-resolver/binding-darwin-x64@11.21.3': - resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} + resolution: {integrity: sha1-jSNFzS2NTc+lBCLibzsAja6Rv8E=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz} cpu: [x64] os: [darwin] '@oxc-resolver/binding-freebsd-x64@11.21.3': - resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} + resolution: {integrity: sha1-t1VMOreQOpWq1hC6XRY5lvS3EJU=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz} cpu: [x64] os: [freebsd] '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': - resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} + resolution: {integrity: sha1-KnF9b3m911pczch1vNCp/uWF8cM=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz} cpu: [arm] os: [linux] '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': - resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + resolution: {integrity: sha1-LP1SNN4pE8KAXkK1+uWqk1TZm6k=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz} cpu: [arm] os: [linux] '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': - resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} + resolution: {integrity: sha1-MhcvvRtm191tygX82Fzuew4yBgo=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz} cpu: [arm64] os: [linux] libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.21.3': - resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} + resolution: {integrity: sha1-rDHkiUy7DAC9pb4eUmZBBzkjv9w=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz} cpu: [arm64] os: [linux] libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': - resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + resolution: {integrity: sha1-qK5+zBElkYEk0YdJ79caTgTgqFY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz} cpu: [ppc64] os: [linux] libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': - resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + resolution: {integrity: sha1-0kztmzIfZg8ahZ+ZEt6rP4f9MRU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz} cpu: [riscv64] os: [linux] libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': - resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} + resolution: {integrity: sha1-sgzWttTGHqYnGJvJOaxfRppabIk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz} cpu: [riscv64] os: [linux] libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': - resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} + resolution: {integrity: sha1-0/SuyCtMuhh+UOIRxg+Va8YRb38=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz} cpu: [s390x] os: [linux] libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.21.3': - resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} + resolution: {integrity: sha1-rdO3YpPvHolZ4AvGtiradm4YSFw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz} cpu: [x64] os: [linux] libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.21.3': - resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} + resolution: {integrity: sha1-S0x5vSaIoPmgLAoLZxAV/y98Zfw=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz} cpu: [x64] os: [linux] libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.21.3': - resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + resolution: {integrity: sha1-PcM6FZqu830td9EN8EWt76tYUt8=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz} cpu: [arm64] os: [openharmony] '@oxc-resolver/binding-wasm32-wasi@11.21.3': - resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} + resolution: {integrity: sha1-a8ib4FPijVU9sQCzUFxrDSUZsOw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz} engines: {node: '>=14.0.0'} cpu: [wasm32] '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': - resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} + resolution: {integrity: sha1-Y0XucnpZn+wGLEikO5Rct4huh+o=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz} cpu: [arm64] os: [win32] '@oxc-resolver/binding-win32-x64-msvc@11.21.3': - resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} + resolution: {integrity: sha1-0Nox/ORta4VnL6HnUVQA+vzkXho=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz} cpu: [x64] os: [win32] @@ -9342,7 +9345,7 @@ packages: engines: {node: '>=20.0.0'} '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + resolution: {integrity: sha1-p36nQvqyV3UUVDTrHSMoz1ATrDM=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@pkgjs/parseargs/-/parseargs-0.11.0.tgz} engines: {node: '>=14'} '@playwright/test@1.57.0': @@ -9427,140 +9430,140 @@ packages: optional: true '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + resolution: {integrity: sha1-pnQsdMfZ1tYE74pI+ZMmtOzaPYI=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz} cpu: [arm] os: [android] '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + resolution: {integrity: sha1-lyR74JjeTfDBGXEIn9Lt+ApdqM8=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz} cpu: [arm64] os: [android] '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + resolution: {integrity: sha1-Z0hSzxTPEbgFbgsaL06HK1I1ds8=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz} cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + resolution: {integrity: sha1-Nt/X7QqvTZ2J2e+YOvcmMkVbAkY=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz} cpu: [x64] os: [darwin] '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + resolution: {integrity: sha1-L4fCB0tCICYP21KpmWJG7fxjPCI=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz} cpu: [arm64] os: [freebsd] '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + resolution: {integrity: sha1-m1omUio4qV3AZhbRk51NmnaTeAM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz} cpu: [x64] os: [freebsd] '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + resolution: {integrity: sha1-hqpIWThahzQjW15ApI5S13B1jDo=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz} cpu: [arm] os: [linux] libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + resolution: {integrity: sha1-y+cOVubs6NrIPrdztiT8nlpGCXY=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz} cpu: [arm] os: [linux] libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + resolution: {integrity: sha1-0UmSouZTvDJj0oS8ZXm3ookOHEU=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz} cpu: [arm64] os: [linux] libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + resolution: {integrity: sha1-L90d3ENOqQrqoIUdIER4m00H9to=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz} cpu: [arm64] os: [linux] libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + resolution: {integrity: sha1-ihgeb4n5afIWZqdDzUEUFsgAmec=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz} cpu: [loong64] os: [linux] libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + resolution: {integrity: sha1-kEElryurw5X4Bh2qJ7WvH04/L3g=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz} cpu: [loong64] os: [linux] libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + resolution: {integrity: sha1-pXlwrGhkyaNEdBGmWCJL3PlIviI=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz} cpu: [ppc64] os: [linux] libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + resolution: {integrity: sha1-u4TeWyaHBWekJnZm4IiR6Au1amM=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz} cpu: [ppc64] os: [linux] libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + resolution: {integrity: sha1-ctANLH+zdc41ZOdZ2zPxejW/+rk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz} cpu: [riscv64] os: [linux] libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + resolution: {integrity: sha1-TBZu9Y5xj5JFvTGHM4S6FaXBqIM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz} cpu: [riscv64] os: [linux] libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + resolution: {integrity: sha1-u1AlzemmHbR4wspyFYCK07znOgk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz} cpu: [s390x] os: [linux] libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + resolution: {integrity: sha1-m2ax+c2VxmJMeI8CHHViaf/tFVI=} cpu: [x64] os: [linux] libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + resolution: {integrity: sha1-sAfKJV3HFmAX1X19JFGWPwvSP9k=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz} cpu: [x64] os: [linux] libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + resolution: {integrity: sha1-6LNXstGqLI12qY9fDYieq+k/Tvk=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz} cpu: [x64] os: [openbsd] '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + resolution: {integrity: sha1-lsLj9KrNPZIZgTKYMf+N3kkiBNw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz} cpu: [arm64] os: [openharmony] '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + resolution: {integrity: sha1-LYZRSdcG2Tjfi0uPEX5pp3ZG1YE=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz} cpu: [arm64] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + resolution: {integrity: sha1-q+FZO+D6kjJemXHI2kKcXgW5LDY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz} cpu: [ia32] os: [win32] '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + resolution: {integrity: sha1-xK8+lRjJpc1LHBY9yB0K1Ngufqs=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz} cpu: [x64] os: [win32] '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + resolution: {integrity: sha1-RYSoqHspGIpMH+mHqfz3AeJW2Gw=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz} cpu: [x64] os: [win32] @@ -9903,7 +9906,7 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + resolution: {integrity: sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@tybys/wasm-util/-/wasm-util-0.10.3.tgz} '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -10302,7 +10305,7 @@ packages: resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} '@types/node@25.9.3': - resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + resolution: {integrity: sha1-Ed/noz5o+lxWDwqnbMVZViHvJrk=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-25.9.3.tgz} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -10311,7 +10314,7 @@ packages: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} '@types/plist@3.0.5': - resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + resolution: {integrity: sha1-mgxJwPmIbIyGlqeQTdcD9ihANuA=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/plist/-/plist-3.0.5.tgz} '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -10395,7 +10398,7 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + resolution: {integrity: sha1-usywepcLkXB986PoumiWxX6tLRE=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/trusted-types/-/trusted-types-2.0.7.tgz} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -10404,7 +10407,7 @@ packages: resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/verror@1.10.9': - resolution: {integrity: sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==} + resolution: {integrity: sha1-Qgwyrbmi3VCz20yPllAeBaDnKUE=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/verror/-/verror-1.10.9.tgz} '@types/vscode@1.100.0': resolution: {integrity: sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==} @@ -10413,7 +10416,7 @@ packages: resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} '@types/webrtc@0.0.37': - resolution: {integrity: sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==} + resolution: {integrity: sha1-aTZj3F3oxshUBvbPVmHMwehOTGg=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/webrtc/-/webrtc-0.0.37.tgz} '@types/webvtt-parser@2.2.0': resolution: {integrity: sha512-T8n08m3VWAOCVDHWPOtEehnLcVSyQaUmyjIvGCERWRPMyVooUjqWaDsvKD3bcwQSU8TLBW19YTSRx9UxBNfckA==} @@ -10440,7 +10443,7 @@ packages: resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + resolution: {integrity: sha1-6bKAi08QlQSgPNqVglmHb2EBeZk=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/yauzl/-/yauzl-2.10.3.tgz} '@typescript-eslint/eslint-plugin@8.62.1': resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} @@ -10525,47 +10528,47 @@ packages: engines: {node: '>=16'} '@vscode/vsce-sign-alpine-arm64@2.0.2': - resolution: {integrity: sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==} + resolution: {integrity: sha1-SszEheVapv8EsZW0f3IurVfapY4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz} cpu: [arm64] os: [alpine] '@vscode/vsce-sign-alpine-x64@2.0.2': - resolution: {integrity: sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==} + resolution: {integrity: sha1-Skt7UFtMwPWFljlIl8SaC84OVAw=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz} cpu: [x64] os: [alpine] '@vscode/vsce-sign-darwin-arm64@2.0.2': - resolution: {integrity: sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==} + resolution: {integrity: sha1-EKpp/rf4Gj3GjCQgOMoD6v8ZwS4=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz} cpu: [arm64] os: [darwin] '@vscode/vsce-sign-darwin-x64@2.0.2': - resolution: {integrity: sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==} + resolution: {integrity: sha1-MxVSjz6hAHpkizMgv/NqM6ngeqU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz} cpu: [x64] os: [darwin] '@vscode/vsce-sign-linux-arm64@2.0.2': - resolution: {integrity: sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==} + resolution: {integrity: sha1-zlxc/JnjRUtPt3BAWBK0a9bcqHA=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz} cpu: [arm64] os: [linux] '@vscode/vsce-sign-linux-arm@2.0.2': - resolution: {integrity: sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==} + resolution: {integrity: sha1-QUL9qD5xMLMa7diqgeTapjNDI8I=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz} cpu: [arm] os: [linux] '@vscode/vsce-sign-linux-x64@2.0.2': - resolution: {integrity: sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==} + resolution: {integrity: sha1-WauT8yLvs89JFm1OLoEnicMRdCg=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz} cpu: [x64] os: [linux] '@vscode/vsce-sign-win32-arm64@2.0.2': - resolution: {integrity: sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==} + resolution: {integrity: sha1-0JVwShSwQEwLb2lumInppRsxqGw=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz} cpu: [arm64] os: [win32] '@vscode/vsce-sign-win32-x64@2.0.2': - resolution: {integrity: sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==} + resolution: {integrity: sha1-KU6nK0T+3WlNSfXO9MVb84dtwlc=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz} cpu: [x64] os: [win32] @@ -10746,11 +10749,11 @@ packages: optional: true '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + resolution: {integrity: sha1-ANHdlAshjf8uSTCdQQ2LshIVkiU=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@xmldom/xmldom/-/xmldom-0.8.13.tgz} engines: {node: '>=10.0.0'} '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + resolution: {integrity: sha1-oK1aJv6KqZYxCHBybhcEl392ne4=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@xmldom/xmldom/-/xmldom-0.9.10.tgz} engines: {node: '>=14.6'} '@xtuc/ieee754@1.2.0': @@ -10819,15 +10822,15 @@ packages: hasBin: true agent-base@5.1.1: - resolution: {integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==} + resolution: {integrity: sha1-6Ps/JClZ20TWO+Zl23qOc5U3oyw=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/agent-base/-/agent-base-5.1.1.tgz} engines: {node: '>= 6.0.0'} agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + resolution: {integrity: sha1-Sf/1hXfP7j83F2/qtMIuAPhtf3c=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/agent-base/-/agent-base-6.0.2.tgz} engines: {node: '>= 6.0.0'} agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + resolution: {integrity: sha1-KUNeuCG8QZRjOluJ5bxHA7r8JaE=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/agent-base/-/agent-base-7.1.3.tgz} engines: {node: '>= 14'} agentkeepalive@4.6.0: @@ -11022,7 +11025,7 @@ packages: resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + resolution: {integrity: sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assert-plus/-/assert-plus-1.0.0.tgz} engines: {node: '>=0.8'} assertion-error@2.0.1: @@ -11038,7 +11041,7 @@ packages: engines: {node: '>=4'} astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + resolution: {integrity: sha1-SDFDxWeu7UeFdZwIZXhtx319LjE=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/astral-regex/-/astral-regex-2.0.0.tgz} engines: {node: '>=8'} async-exit-hook@2.0.1: @@ -11126,10 +11129,10 @@ packages: engines: {node: 20 || >=22} bare-events@2.5.4: - resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + resolution: {integrity: sha1-FhQ9Q14e2er9GrhfEribM1ekF0U=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bare-events/-/bare-events-2.5.4.tgz} bare-fs@4.1.5: - resolution: {integrity: sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==} + resolution: {integrity: sha1-HQbAduaMyL+XAQ0pr546w4CM3Pc=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bare-fs/-/bare-fs-4.1.5.tgz} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -11142,7 +11145,7 @@ packages: engines: {bare: '>=1.14.0'} bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + resolution: {integrity: sha1-tZ0YEwulKmr5J22z6WouPT6lIXg=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bare-path/-/bare-path-3.0.0.tgz} bare-stream@2.6.5: resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} @@ -11156,7 +11159,7 @@ packages: optional: true base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: {integrity: sha1-GxtEAWClv3rUC2UPCVljSBkDkwo=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/base64-js/-/base64-js-1.5.1.tgz} baseline-browser-mapping@2.10.37: resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==} @@ -11175,7 +11178,7 @@ packages: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} bent@7.3.12: - resolution: {integrity: sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==} + resolution: {integrity: sha1-4KJ3XUQl52dMZLeLJCr09J2msDU=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bent/-/bent-7.3.12.tgz} better-sqlite3@12.6.2: resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} @@ -11293,7 +11296,7 @@ packages: resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + resolution: {integrity: sha1-umLnwTEzBTWCGXFghRqPZI6Z7tA=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/buffer/-/buffer-5.7.1.tgz} buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -11322,7 +11325,7 @@ packages: engines: {node: '>= 0.8'} bytesish@0.4.4: - resolution: {integrity: sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==} + resolution: {integrity: sha1-87U1oPEVN0dCeu4nJWdIz/kjR+Y=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bytesish/-/bytesish-0.4.4.tgz} bytestreamjs@2.0.1: resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} @@ -11388,7 +11391,7 @@ packages: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + resolution: {integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/caseless/-/caseless-0.12.0.tgz} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -11529,7 +11532,7 @@ packages: engines: {node: 10.* || >= 12.*} cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + resolution: {integrity: sha1-w54ovwXtzeW+O5iZKiLe7Vork8c=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cli-truncate/-/cli-truncate-2.1.0.tgz} engines: {node: '>=8'} cli-truncate@4.0.0: @@ -11755,7 +11758,7 @@ packages: hasBin: true core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/core-util-is/-/core-util-is-1.0.2.tgz} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -11802,7 +11805,7 @@ packages: engines: {node: '>= 14'} crc@3.8.0: - resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + resolution: {integrity: sha1-rWAmnCyFb4wpnixMwN5FVpFAVsY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/crc/-/crc-3.8.0.tgz} create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} @@ -12094,7 +12097,7 @@ packages: optional: true debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + resolution: {integrity: sha1-clgLfpFF+zm2Z2+cXl+xALk0F5o=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-3.2.7.tgz} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -12346,7 +12349,7 @@ packages: resolution: {integrity: sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==} dmg-license@1.0.11: - resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + resolution: {integrity: sha1-ezvDdF0bUr51BrTugMth325M15o=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dmg-license/-/dmg-license-1.0.11.tgz} engines: {node: '>=8'} os: [darwin] hasBin: true @@ -12503,7 +12506,7 @@ packages: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + resolution: {integrity: sha1-VldK/deR9UqOmyeFwFgqLSYhD6k=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/encoding/-/encoding-0.1.13.tgz} end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -12555,7 +12558,7 @@ packages: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} errno@0.1.8: - resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + resolution: {integrity: sha1-i7Ppx9Rjvkl2/4iPdrSAnrwugR8=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/errno/-/errno-0.1.8.tgz} hasBin: true error-ex@1.3.2: @@ -12812,7 +12815,7 @@ packages: hasBin: true extsprintf@1.4.1: - resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + resolution: {integrity: sha1-jRcsBkhn8jXAyEpZaAbSeb9LzAc=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/extsprintf/-/extsprintf-1.4.1.tgz} engines: {'0': node >=0.6.0} fast-deep-equal@3.1.3: @@ -13042,12 +13045,12 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + resolution: {integrity: sha1-ilJveLj99GI7cJ4Ll1xSwkwC/Ro=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.2.tgz} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + resolution: {integrity: sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.3.tgz} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] @@ -13191,7 +13194,7 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + resolution: {integrity: sha1-rnzTG9NYO5PFoWQ3oa/ifMM6GrY=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/global-agent/-/global-agent-3.0.0.tgz} engines: {node: '>=10.0'} globals@17.7.0: @@ -13252,7 +13255,7 @@ packages: engines: {node: '>=10.19.0'} graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + resolution: {integrity: sha1-QYPk6L8Iu24Fu7L30uDI9xLKQOM=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/graceful-fs/-/graceful-fs-4.2.11.tgz} graphlib@2.1.8: resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} @@ -13479,11 +13482,11 @@ packages: engines: {node: '>=10.19.0'} https-proxy-agent@4.0.0: - resolution: {integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==} + resolution: {integrity: sha1-cCtx+1UgoTKmbeH2dUHZ5iFU2Cs=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz} engines: {node: '>= 6.0.0'} https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + resolution: {integrity: sha1-xZ7yJKBP6LdU89sAY6Jeow0ABdY=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz} engines: {node: '>= 6'} https-proxy-agent@7.0.6: @@ -13506,7 +13509,7 @@ packages: engines: {node: '>=10.18'} iconv-corefoundation@1.1.7: - resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + resolution: {integrity: sha1-MQZearLJJyFUyLCCEVHiyI8bACo=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz} engines: {node: ^8.11.2 || >=10} os: [darwin] @@ -13542,7 +13545,7 @@ packages: engines: {node: '>= 4'} image-size@0.5.5: - resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + resolution: {integrity: sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/image-size/-/image-size-0.5.5.tgz} engines: {node: '>=0.10.0'} hasBin: true @@ -13855,11 +13858,11 @@ packages: engines: {node: '>= 0.4'} is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + resolution: {integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-stream/-/is-stream-1.1.0.tgz} engines: {node: '>=0.10.0'} is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + resolution: {integrity: sha1-+sHj1TuXrVqdCunO8jifWBClwHc=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-stream/-/is-stream-2.0.1.tgz} engines: {node: '>=8'} is-string@1.1.1: @@ -14307,7 +14310,7 @@ packages: engines: {node: '>= 0.6'} keytar@7.9.0: - resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} + resolution: {integrity: sha1-TGIlcI9RtQy/d8Wq6BchlkwpGMs=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/keytar/-/keytar-7.9.0.tgz} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -14612,7 +14615,7 @@ packages: resolution: {integrity: sha512-EJYTDWMrOS1kddK1mTsRkrx2Ngh2nYsg54SRMWVVWGVEGbHH4tod8tqqU9hIRPgGQVboSjFubDn9cboSitbM3Q==} make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + resolution: {integrity: sha1-XwMQ4YuL6JjMBwCSlaMK5B6R5vU=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/make-dir/-/make-dir-2.1.0.tgz} engines: {node: '>=6'} make-dir@4.0.0: @@ -14866,7 +14869,7 @@ packages: engines: {node: '>=8.6'} microsoft-cognitiveservices-speech-sdk@1.43.1: - resolution: {integrity: sha512-xO/rlhNSodzCNBtlA3edXDO0+8Q2tMG96nNsjE0JiyAR4Ul/qsZtM4iggTSi+Zax3JtYzrH7W+7349vdpJNaTA==} + resolution: {integrity: sha1-QWVkDgBMTUE9HrSiVeTeRrxad4s=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.43.1.tgz} mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -14885,7 +14888,7 @@ packages: engines: {node: '>=18'} mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + resolution: {integrity: sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime/-/mime-1.6.0.tgz} engines: {node: '>=4'} hasBin: true @@ -15123,7 +15126,7 @@ packages: engines: {node: '>=22.12.0'} node-addon-api@1.7.2: - resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + resolution: {integrity: sha1-PfMLlXILU8JOWZSLSVMrZiRE9U0=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/node-addon-api/-/node-addon-api-1.7.2.tgz} node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} @@ -15629,7 +15632,7 @@ packages: hasBin: true plist@3.1.0: - resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + resolution: {integrity: sha1-eXpRapPmL1veVeC5zJyWf4YIk8k=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/plist/-/plist-3.1.0.tgz} engines: {node: '>=10.4.0'} pluralize@2.0.0: @@ -16580,7 +16583,7 @@ packages: engines: {node: '>=14.16'} slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + resolution: {integrity: sha1-Md3BCTCht+C2ewjJbC9Jt3p4l4c=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/slice-ansi/-/slice-ansi-3.0.0.tgz} engines: {node: '>=8'} slice-ansi@4.0.0: @@ -16596,7 +16599,7 @@ packages: engines: {node: '>=18'} smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + resolution: {integrity: sha1-bh1x+k8YwF99D/IW3RakgdDo2a4=, tarball: https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/smart-buffer/-/smart-buffer-4.2.0.tgz} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} smol-toml@1.7.0: @@ -16636,7 +16639,7 @@ packages: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + resolution: {integrity: sha1-dHIq8y6WFOnCh6jQu95IteLxomM=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map/-/source-map-0.6.1.tgz} engines: {node: '>=0.10.0'} source-map@0.7.4: @@ -17312,7 +17315,7 @@ packages: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + resolution: {integrity: sha1-gjFem7xvKyWIiFis0f/4RBA1t38=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uglify-js/-/uglify-js-3.19.3.tgz} engines: {node: '>=0.8.0'} hasBin: true @@ -17346,7 +17349,7 @@ packages: resolution: {integrity: sha512-cRaY9PagdEZoRmcwzk3tUV3SVGrVQkR6bcSilav/A0vXsfpW4Lvd0BvgRMwTEDTLLGN+QdyBTG+nnvTgJhdt6w==} undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + resolution: {integrity: sha1-YSdbSF1/1OnSacfPBOwoc8nMD5E=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-7.24.6.tgz} undici@6.27.0: resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} @@ -17455,12 +17458,12 @@ packages: hasBin: true uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + resolution: {integrity: sha1-gNW1ztJxu5r2xEXyGhoExgbO++I=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uuid/-/uuid-8.3.2.tgz} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + resolution: {integrity: sha1-4YjUyIU8xyIiA5LEJM1jfzIpPzA=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uuid/-/uuid-9.0.1.tgz} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true @@ -17487,7 +17490,7 @@ packages: engines: {node: '>= 0.8'} verror@1.10.1: - resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + resolution: {integrity: sha1-S/Ce7M9FY7EJ7Us9RYOAyXKwzes=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/verror/-/verror-1.10.1.tgz} engines: {node: '>=0.6.0'} vfile-message@4.0.2: @@ -17856,20 +17859,8 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@7.5.11: - resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + resolution: {integrity: sha1-lGDa8YEruBpCPFuerHRpQahjEPo=, tarball: https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ws/-/ws-7.5.11.tgz} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -17929,7 +17920,7 @@ packages: engines: {node: '>=4.0'} xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + resolution: {integrity: sha1-nc3OSe6mbY0QtCyulKecPI0MLsU=, tarball: https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xmlbuilder/-/xmlbuilder-15.1.1.tgz} engines: {node: '>=8.0'} xmlchars@2.2.0: @@ -29417,7 +29408,7 @@ snapshots: bent: 7.3.12 https-proxy-agent: 4.0.0 uuid: 9.0.1 - ws: 7.5.10 + ws: 7.5.11 transitivePeerDependencies: - bufferutil - supports-color @@ -32999,8 +32990,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@7.5.10: {} - ws@7.5.11: {} ws@8.21.0: {} diff --git a/ts/pnpm-workspace.yaml b/ts/pnpm-workspace.yaml index 5e33d498a..79421c695 100644 --- a/ts/pnpm-workspace.yaml +++ b/ts/pnpm-workspace.yaml @@ -40,5 +40,8 @@ onlyBuiltDependencies: - exifreader - keytar - koffi + - node-pty + - onnxruntime-node + - protobufjs - puppeteer - sharp diff --git a/ts/tools/scripts/code/circular-baseline-exception.json b/ts/tools/scripts/code/circular-baseline-exception.json new file mode 100644 index 000000000..ad3ac3599 --- /dev/null +++ b/ts/tools/scripts/code/circular-baseline-exception.json @@ -0,0 +1,7 @@ +{ + "exceptions": [ + { + "key": "packages/dispatcher/dispatcher/src/execute/actionHandlers.ts > packages/dispatcher/dispatcher/src/context/memory.ts > packages/dispatcher/dispatcher/src/internal.ts > packages/dispatcher/dispatcher/src/dispatcher.ts" + } + ] +} diff --git a/ts/tools/scripts/code/complexity-baseline-exception.json b/ts/tools/scripts/code/complexity-baseline-exception.json index 55012549e..ba1eaff51 100644 --- a/ts/tools/scripts/code/complexity-baseline-exception.json +++ b/ts/tools/scripts/code/complexity-baseline-exception.json @@ -17,7 +17,7 @@ "exceptions": [ { "file": "packages/agents/github-cli/src/github-cliActionHandler.ts", - "line": 356, + "line": 406, "name": "Function 'buildArgs'", "cyclomatic": 179, "cognitive": 232, @@ -125,7 +125,7 @@ }, { "file": "packages/vscode-shell/src/webview/main.ts", - "line": 487, + "line": 699, "name": "Arrow function", "cyclomatic": 81, "cognitive": 87, @@ -197,7 +197,7 @@ }, { "file": "packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts", - "line": 921, + "line": 933, "name": "Async function 'initializeCommandHandlerContext'", "cyclomatic": 63, "cognitive": 33, @@ -548,7 +548,7 @@ }, { "file": "packages/agents/github-cli/src/github-cliActionHandler.ts", - "line": 1244, + "line": 1283, "name": "Async function 'executeAction'", "cyclomatic": 43, "cognitive": 66, @@ -593,7 +593,7 @@ }, { "file": "packages/vscode-shell/src/agentServerBridge.ts", - "line": 1188, + "line": 1251, "name": "Async method 'handleWebviewMessage'", "cyclomatic": 43, "cognitive": 17, @@ -602,7 +602,7 @@ }, { "file": "packages/shell/src/renderer/src/chatPanelBridge.ts", - "line": 120, + "line": 121, "name": "Function 'toHistoryEntries'", "cyclomatic": 42, "cognitive": 44, @@ -620,7 +620,7 @@ }, { "file": "packages/vscode-shell/src/webview/main.ts", - "line": 345, + "line": 557, "name": "Function 'toChatPanelHistory'", "cyclomatic": 42, "cognitive": 33, @@ -629,7 +629,7 @@ }, { "file": "packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts", - "line": 1656, + "line": 1686, "name": "Async function 'changeContextConfig'", "cyclomatic": 42, "cognitive": 29, @@ -1061,7 +1061,7 @@ }, { "file": "packages/agents/github-cli/src/github-cliActionHandler.ts", - "line": 842, + "line": 881, "name": "Function 'formatStatusOutput'", "cyclomatic": 33, "cognitive": 84, @@ -1493,7 +1493,7 @@ }, { "file": "packages/chat-ui/src/chatPanel.ts", - "line": 205, + "line": 267, "name": "Function 'highlightJson'", "cyclomatic": 28, "cognitive": 34, @@ -1952,7 +1952,7 @@ }, { "file": "packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts", - "line": 220, + "line": 221, "name": "Function 'buildAgentStatusHtml'", "cyclomatic": 24, "cognitive": 42, @@ -2033,7 +2033,7 @@ }, { "file": "packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts", - "line": 311, + "line": 312, "name": "Async function 'showAgentStatus'", "cyclomatic": 23, "cognitive": 41, @@ -2069,7 +2069,7 @@ }, { "file": "packages/dispatcher/dispatcher/src/context/system/handlers/configCommandHandlers.ts", - "line": 1872, + "line": 1873, "name": "Async function 'listModelsForProvider'", "cyclomatic": 23, "cognitive": 38, diff --git a/ts/tools/scripts/getNPMRC.mjs b/ts/tools/scripts/getNPMRC.mjs index 1e0742b9d..5ba1e4a90 100644 --- a/ts/tools/scripts/getNPMRC.mjs +++ b/ts/tools/scripts/getNPMRC.mjs @@ -50,6 +50,13 @@ let paramVault = undefined; let paramSecret = undefined; let paramCommit = true; let paramAuthOnly = false; +// Feed auth mechanism (default "azureauth"): +// azureauth — tokenHelper using the azureauth CLI; an Entra ID token for +// Azure DevOps, cached & silently refreshed via the OS broker +// (WAM). No PAT stored, no rotation, CAE-aware. +// pat — mint a rotating Azure DevOps PAT; Basic auth in ~/.npmrc. +// token-helper — az-backed tokenHelper (CAE-fragile; needs `az login`). +let paramAuthMode = process.env.TYPEAGENT_NPM_AUTH ?? "azureauth"; // --- identity (mirrors getKeys.mjs) -------------------------------------- @@ -108,6 +115,16 @@ async function getAzureCredential() { return defaultCred; } +// Cache the credential so we don't re-run the auth chain (or prompt twice) when +// both the Key Vault pull and the PAT mint need a token in the same run. +let cachedCredential; +async function getCredential() { + if (!cachedCredential) { + cachedCredential = await getAzureCredential(); + } + return cachedCredential; +} + // --- npmrc helpers ------------------------------------------------------- function parseRegistry(npmrcContent) { @@ -180,6 +197,173 @@ function upsertTokenHelper(nerfDart, helperPath) { fs.writeFileSync(userNpmrcPath, lines.join("\n") + "\n"); } +// Parse the Azure DevOps org and feed name from an npm registry URL. Supports +// project-scoped and org-scoped feeds on both the modern and legacy hosts: +// https://pkgs.dev.azure.com/{org}/{project}/_packaging/{feed}/npm/registry/ +// https://pkgs.dev.azure.com/{org}/_packaging/{feed}/npm/registry/ +// https://{org}.pkgs.visualstudio.com/_packaging/{feed}/npm/registry/ +function parseAdoRegistry(registry) { + const u = new URL(registry); + const feed = u.pathname.match(/\/_packaging\/([^/]+)\//)?.[1]; + let org; + const legacy = u.hostname.match(/^([^.]+)\.pkgs\.visualstudio\.com$/i); + if (legacy) { + org = legacy[1]; + } else if (/^pkgs\.dev\.azure\.com$/i.test(u.hostname)) { + org = u.pathname.split("/").filter(Boolean)[0]; + } + if (!org || !feed) { + throw new Error( + `Could not parse Azure DevOps org/feed from registry: ${registry}`, + ); + } + return { org, feed }; +} + +// Mint an Azure DevOps Personal Access Token scoped to Packaging (read) via the +// PATs REST API, authenticating with an AAD bearer token from `credential`. +// Returns { token, validTo, authorizationId }. Never logs the token value. +async function createFeedPat(credential, org, feed) { + const adoResource = + process.env.TYPEAGENT_ADO_RESOURCE ?? config.adoResource; + const at = await credential.getToken(`${adoResource}/.default`); + if (!at?.token) { + throw new Error("Could not acquire an Azure DevOps access token."); + } + const days = Number(process.env.TYPEAGENT_PAT_DAYS ?? config.patDays ?? 90); + const validTo = new Date( + Date.now() + days * 24 * 60 * 60 * 1000, + ).toISOString(); + const url = `https://vssps.dev.azure.com/${encodeURIComponent( + org, + )}/_apis/tokens/pats?api-version=7.1-preview.1`; + const res = await fetch(url, { + method: "POST", + headers: { + authorization: `Bearer ${at.token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ + displayName: `${feed} npm (getNPMRC ${new Date() + .toISOString() + .slice(0, 10)})`, + scope: "vso.packaging", + validTo, + allOrgs: false, + }), + }); + const body = await res.text(); + if (res.status === 401) { + throw new Error( + "Azure DevOps returned 401 while creating the PAT (token revoked or insufficient claims).", + ); + } + if (!res.ok) { + throw new Error( + `PAT creation failed: HTTP ${res.status} ${res.statusText}. ${body.slice(0, 300)}`, + ); + } + let data; + try { + data = JSON.parse(body); + } catch { + throw new Error( + `PAT API returned a non-JSON response (wrong tenant?): ${body.slice(0, 200)}`, + ); + } + const token = data?.patToken?.token; + // Azure DevOps returns patTokenError: "none" on SUCCESS — only a value other + // than "none" is an actual error (e.g. a policy denial). + const patErr = data?.patTokenError; + if (patErr && patErr !== "none") { + throw new Error( + `PAT creation was denied (patTokenError=${patErr}). Your organization may restrict PAT creation — fall back to --auth token-helper.`, + ); + } + if (!token) { + throw new Error( + `PAT API returned no token and no error (unexpected response): ${body.slice(0, 200)}`, + ); + } + return { + token, + validTo: data.patToken?.validTo ?? validTo, + authorizationId: data.patToken?.authorizationId, + }; +} + +// Best-effort: revoke previously getNPMRC-created PATs for this feed (matched by +// display-name prefix) except the one we just created, so rotation and any +// earlier buggy runs don't leave orphaned tokens behind. Never throws. +async function revokeOldFeedPats(credential, org, feed, keepAuthorizationId) { + const adoResource = + process.env.TYPEAGENT_ADO_RESOURCE ?? config.adoResource; + const prefix = `${feed} npm (getNPMRC`; + const base = `https://vssps.dev.azure.com/${encodeURIComponent( + org, + )}/_apis/tokens/pats?api-version=7.1-preview.1`; + try { + const at = await credential.getToken(`${adoResource}/.default`); + if (!at?.token) return 0; + const auth = { authorization: `Bearer ${at.token}` }; + const res = await fetch(base, { headers: auth }); + if (!res.ok) return 0; + const data = await res.json(); + const pats = Array.isArray(data?.patTokens) ? data.patTokens : []; + let revoked = 0; + for (const p of pats) { + if ( + p?.authorizationId && + p.authorizationId !== keepAuthorizationId && + typeof p.displayName === "string" && + p.displayName.startsWith(prefix) + ) { + const del = await fetch( + `${base}&authorizationId=${encodeURIComponent( + p.authorizationId, + )}`, + { method: "DELETE", headers: auth }, + ).catch(() => undefined); + if (del?.ok) revoked++; + } + } + return revoked; + } catch { + return 0; // best-effort + } +} + +// Write PAT Basic-auth lines for the feed nerf-dart into ~/.npmrc, replacing any +// prior auth entries (tokenHelper / _authToken / username / _password / email) +// for the same registry. `_password` holds the base64-encoded PAT, per Azure +// DevOps' npm convention. +function upsertFeedBasicAuth(nerfDart, org, pat) { + const b64 = Buffer.from(pat, "utf8").toString("base64"); + const entries = [ + `${nerfDart}:username=${org}`, + `${nerfDart}:_password=${b64}`, + `${nerfDart}:email=npm-requires-email-not-used@example.com`, + ]; + let lines = fs.existsSync(userNpmrcPath) + ? fs.readFileSync(userNpmrcPath, "utf8").split(/\r?\n/) + : []; + const esc = nerfDart.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const stale = new RegExp( + `^\\s*${esc}:(?:_authToken|_auth|tokenHelper|username|_password|email)\\s*=`, + ); + lines = lines.filter((l) => !stale.test(l)); + while (lines.length && lines[lines.length - 1].trim() === "") lines.pop(); + lines.push(...entries); + fs.writeFileSync(userNpmrcPath, lines.join("\n") + "\n"); + if (process.platform !== "win32") { + try { + fs.chmodSync(userNpmrcPath, 0o600); + } catch { + // best-effort hardening; ignore where unsupported + } + } +} + // Quick, non-fatal check that the Azure CLI is available and signed in — the // feed tokenHelper depends on it at pnpm-install time. async function hasAzLogin() { @@ -195,6 +379,74 @@ async function hasAzLogin() { } } +// True if the azureauth CLI (Microsoft's MSAL/WAM auth helper) is available. +async function hasAzureAuth() { + try { + const { execFileSync } = await import("node:child_process"); + execFileSync("azureauth", ["--version"], { + stdio: "ignore", + shell: process.platform === "win32", + }); + return true; + } catch { + return false; + } +} + +// Write a tokenHelper that prints an Azure DevOps Entra token from azureauth. +// azureauth caches and silently refreshes via the OS broker (WAM on Windows) and +// answers CAE claims challenges, so no PAT is stored and there is no periodic +// rotation. Returns the helper's absolute path (forward slashes). +function writeAzureAuthTokenHelper() { + fs.mkdirSync(typeagentDir, { recursive: true }); + let helperPath; + if (process.platform === "win32") { + helperPath = path.join(typeagentDir, "npmrc-azureauth-helper.cmd"); + fs.writeFileSync( + helperPath, + `@echo off\r\nazureauth ado token --output token --mode broker --prompt-hint "typeagent-feed npm (pnpm)"\r\n`, + ); + } else { + helperPath = path.join(typeagentDir, "npmrc-azureauth-helper.sh"); + fs.writeFileSync( + helperPath, + `#!/bin/sh\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\nexec azureauth ado token --output token --prompt-hint "typeagent-feed npm (pnpm)"\n`, + ); + fs.chmodSync(helperPath, 0o755); + } + return helperPath.replace(/\\/g, "/"); +} + +// Run azureauth once (default broker→web mode) to sign in / warm the token cache +// so the broker-only helper resolves silently during `pnpm install`. Returns +// true on success. Never logs the token. +async function primeAzureAuth() { + try { + const { execFileSync } = await import("node:child_process"); + // NB: no spaces in the prompt-hint — with shell:true on Windows, + // execFileSync does not quote args, so a spaced value would be split. + execFileSync( + "azureauth", + [ + "ado", + "token", + "--output", + "token", + "--prompt-hint", + "typeagent-feed-npm-setup", + ], + { + stdio: ["inherit", "ignore", "inherit"], + shell: process.platform === "win32", + timeout: 15 * 60 * 1000, + }, + ); + return true; + } catch { + return false; + } +} + // --- commands ------------------------------------------------------------ async function pull() { @@ -217,7 +469,7 @@ async function pull() { console.log( `Pulling ${chalk.cyanBright(secret)} from ${chalk.cyanBright(vault)} key vault...`, ); - const credential = await getAzureCredential(); + const credential = await getCredential(); const client = new SecretClient( `https://${vault}.vault.azure.net`, credential, @@ -258,8 +510,9 @@ async function pull() { } } - // Feed auth: install a pnpm tokenHelper that mints a fresh Azure DevOps - // token on demand (auto-refreshing) instead of a static, short-lived token. + // Feed auth (default 'azureauth'): install a tokenHelper backed by the + // azureauth CLI (Entra token, broker-cached, no PAT). 'pat' mints a rotating + // PAT (Basic auth); 'token-helper' uses an az-backed tokenHelper. const registry = parseRegistry(npmrcContent); if (!registry) { console.warn( @@ -269,9 +522,7 @@ async function pull() { } if (!/^https:\/\//i.test(registry)) { console.error( - chalk.red( - `Registry must be https:// for tokenHelper auth: ${registry}`, - ), + chalk.red(`Registry must be https:// for feed auth: ${registry}`), ); process.exitCode = 1; return; @@ -279,25 +530,146 @@ async function pull() { const nerfDart = registryToNerfDart(registry); if (!paramCommit) { console.log( - `[dry-run] Would install a feed tokenHelper for ${chalk.cyanBright(registry)}.`, + `[dry-run] Would install feed auth (${paramAuthMode}) for ${chalk.cyanBright(registry)}.`, ); return; } - const helperPath = writeTokenHelperScript(); - upsertTokenHelper(nerfDart, helperPath); + + if (paramAuthMode === "azureauth") { + if (!(await hasAzureAuth())) { + console.error( + chalk.red( + "azureauth CLI not found. Install it (https://aka.ms/azureauth), or use\n" + + "another mode: --auth pat or --auth token-helper.", + ), + ); + process.exitCode = 1; + return; + } + const helperPath = writeAzureAuthTokenHelper(); + upsertTokenHelper(nerfDart, helperPath); + console.log( + chalk.green( + `Feed azureauth tokenHelper installed in ${chalk.cyanBright(userNpmrcPath)} (no PAT; Entra token auto-refreshed via the OS broker).`, + ), + ); + if (await primeAzureAuth()) { + console.log( + chalk.green( + "Verified: azureauth returned an Azure DevOps token.", + ), + ); + } else { + console.warn( + chalk.yellowBright( + "Could not get a token non-interactively just now — the first `pnpm install`\n" + + "may prompt once via the OS broker, then cache silently.", + ), + ); + } + // Feed auth no longer uses a PAT: revoke any leftover getNPMRC-created + // PATs (best-effort). Their ~/.npmrc lines were already replaced above. + try { + const { org, feed } = parseAdoRegistry(registry); + const revoked = await revokeOldFeedPats( + await getCredential(), + org, + feed, + null, + ); + if (revoked > 0) { + console.log( + chalk.gray( + `Revoked ${revoked} leftover getNPMRC PAT(s) for this feed.`, + ), + ); + } + } catch { + // best-effort cleanup; ignore + } + return; + } + + if (paramAuthMode === "token-helper") { + const helperPath = writeTokenHelperScript(); + upsertTokenHelper(nerfDart, helperPath); + console.log( + chalk.green( + `Feed tokenHelper installed in ${chalk.cyanBright(userNpmrcPath)} (auto-refreshes via az).`, + ), + ); + if (!(await hasAzLogin())) { + console.warn( + chalk.yellowBright( + "\nWARNING: `az` is not signed in (or not installed). Run `az login` so the\n" + + "feed tokenHelper can mint tokens — otherwise `pnpm install` will 401.", + ), + ); + } + return; + } + + if (paramAuthMode !== "pat") { + console.error( + chalk.red( + `Unknown --auth mode '${paramAuthMode}'. Use 'azureauth', 'pat', or 'token-helper'.`, + ), + ); + process.exitCode = 1; + return; + } + + // PAT mode (default): mint a rotating Azure DevOps Personal Access Token and + // write Basic auth. Unlike an AAD access token, a PAT is not revoked + // mid-life by Continuous Access Evaluation, so `pnpm install` keeps working + // until the PAT expires — rotate by re-running this script. + const { org, feed } = parseAdoRegistry(registry); + let pat; + try { + const credential = await getCredential(); + pat = await createFeedPat(credential, org, feed); + } catch (e) { + console.error( + chalk.red(`Failed to mint feed PAT: ${e?.message ?? String(e)}`), + ); + console.log( + chalk.yellow( + "\nHints:\n" + + " • If your session was revoked (CAE) or you are not signed in, run `az login` and retry.\n" + + " • If your organization blocks PAT creation, use the helper instead:\n" + + " npm run getNPMRC -- --auth-only --auth token-helper", + ), + ); + process.exitCode = 1; + return; + } + upsertFeedBasicAuth(nerfDart, org, pat.token); + const validUntil = pat.validTo + ? new Date(pat.validTo).toLocaleString() + : "the configured lifetime"; console.log( chalk.green( - `Feed tokenHelper installed in ${chalk.cyanBright(userNpmrcPath)} (auto-refreshes via az).`, + `Feed PAT installed in ${chalk.cyanBright(userNpmrcPath)} (valid until ${validUntil}).`, ), ); - if (!(await hasAzLogin())) { - console.warn( - chalk.yellowBright( - "\nWARNING: `az` is not signed in (or not installed). Run `az login` so the\n" + - "feed tokenHelper can mint tokens — otherwise `pnpm install` will 401.", + const revoked = await revokeOldFeedPats( + await getCredential(), + org, + feed, + pat.authorizationId, + ); + if (revoked > 0) { + console.log( + chalk.gray( + `Revoked ${revoked} older getNPMRC PAT(s) for this feed.`, ), ); } + console.log( + chalk.gray( + "Re-run `npm run getNPMRC` before it expires to rotate the PAT.", + ), + ); } async function push() { @@ -369,14 +741,22 @@ ${chalk.bold("Commands:")} help Show this help ${chalk.bold("Options:")} - --auth-only Skip the Key Vault download; just (re)install the feed tokenHelper + --auth Feed auth: 'azureauth' (default), 'pat', or 'token-helper' + --auth-only Skip the Key Vault download; just (re)install feed auth --vault Key Vault name (default: ${config.vault}) --secret Secret name (default: ${config.secret}) --commit Write changes (default) --dry-run Preview without writing -Feed auth uses a pnpm tokenHelper backed by the Azure CLI — run 'az login' once -and tokens refresh automatically. +Feed auth modes: + azureauth (default) — tokenHelper using the azureauth CLI. Acquires an Entra ID + token for Azure DevOps, cached and silently refreshed via the OS broker + (WAM). No PAT stored, no rotation, CAE handled. Requires azureauth + (https://aka.ms/azureauth). + pat — mint a rotating Azure DevOps PAT (Packaging read), Basic auth in + ~/.npmrc. Org policy may cap its lifetime; re-run to rotate. + token-helper — az-backed tokenHelper; auto-refreshes but is CAE-fragile and + needs an interactive 'az login' when the session is revoked. `); } @@ -402,6 +782,15 @@ const commands = ["push", "pull", "help"]; } continue; } + if (arg === "--auth") { + paramAuthMode = process.argv[++i]; + if (paramAuthMode === undefined) { + throw new Error( + "Missing value for --auth (pat | token-helper)", + ); + } + continue; + } if (arg === "--auth-only") { paramAuthOnly = true; continue;