diff --git a/3rd/Directory.Packages.props b/3rd/Directory.Packages.props
new file mode 100644
index 000000000..ef3a8262f
--- /dev/null
+++ b/3rd/Directory.Packages.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3877ca214..e61b0ee8c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,6 +1,5 @@
- true
10.0.9
10.5.2
10.0.9
diff --git a/Everywhere.slnx b/Everywhere.slnx
index 2e9d7c400..99af81eec 100644
--- a/Everywhere.slnx
+++ b/Everywhere.slnx
@@ -16,6 +16,7 @@
+
@@ -57,6 +58,8 @@
+
+
@@ -71,6 +74,7 @@
+
diff --git a/patches/Directory.Packages.props b/patches/Directory.Packages.props
new file mode 100644
index 000000000..70bc5acdf
--- /dev/null
+++ b/patches/Directory.Packages.props
@@ -0,0 +1,6 @@
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/src/Build.Pure.DI.MS.targets b/src/Build.Pure.DI.MS.targets
new file mode 100644
index 000000000..268b77596
--- /dev/null
+++ b/src/Build.Pure.DI.MS.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
new file mode 100644
index 000000000..d4f3c6e95
--- /dev/null
+++ b/src/Directory.Packages.props
@@ -0,0 +1,10 @@
+
+
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs b/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs
new file mode 100644
index 000000000..a2bef42cd
--- /dev/null
+++ b/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs
@@ -0,0 +1,41 @@
+using Everywhere.Database;
+using Microsoft.Extensions.Logging;
+using Pure.DI;
+using Pure.DI.MS;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.Cloud.DependencyInjection;
+
+public partial class CloudComposition : ServiceProviderFactory
+{
+ // ReSharper disable once UnusedMember.Local
+ private static void SetupCloudServices() =>
+ DI.Setup()
+ // Pure.DI.MS hooks and framework fallbacks.
+ .Hint(Hint.OnCannotResolve, "On")
+ .Hint(Hint.OnCannotResolvePartial, "Off")
+ .Hint(Hint.OnNewRoot, "On")
+ .Hint(Hint.OnNewRootPartial, "Off")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.AspNetCore.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Maui.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*")
+
+ // Logging facade instances are created by Pure.DI; ILoggerFactory stays in MS DI.
+ .Bind>().As(Singleton).To>()
+
+ // Cloud service implementations.
+ .Bind().Bind().As(Singleton).To()
+ .Bind().Bind().As(Singleton).To()
+ .Bind().Bind().As(Singleton).To()
+
+ // Cloud roots exported to the final MS provider.
+ .Root(kind: RootKinds.Exported)
+ .Root(kind: RootKinds.Exported)
+ .Root(kind: RootKinds.Exported)
+ .Root(kind: RootKinds.Exported)
+ .Root(kind: RootKinds.Exported)
+ .Root(kind: RootKinds.Exported);
+}
diff --git a/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs b/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs
new file mode 100644
index 000000000..9d9c097a4
--- /dev/null
+++ b/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs
@@ -0,0 +1,53 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using Everywhere.Common;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Everywhere.Cloud.DependencyInjection;
+
+public static class CloudServiceCollection
+{
+ public static IServiceCollection Configure(IServiceCollection services)
+ {
+ // Cloud HTTP user-agent handler.
+ services.AddTransient();
+
+ // Cloud API named HTTP client.
+ services
+ .AddHttpClient(
+ nameof(ICloudClient),
+ client => client.Timeout = TimeSpan.FromSeconds(30))
+ .ConfigurePrimaryHttpMessageHandler(sp => CreateHttpClientHandler(sp.GetRequiredService()))
+ .AddHttpMessageHandler(sp => sp.GetRequiredService().CreateAuthenticationHandler())
+ .AddHttpMessageHandler();
+
+ return services;
+ }
+
+ public static IServiceCollection ConfigureAliases(IServiceCollection services)
+ {
+ // Cloud startup initializer aliases.
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp => sp.GetRequiredService());
+ return services;
+ }
+
+ private static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy) =>
+ new()
+ {
+ Proxy = proxy,
+ UseProxy = true,
+ AllowAutoRedirect = true,
+ };
+
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ private sealed class CloudUserAgentHandler : DelegatingHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ request.Headers.Remove("User-Agent");
+ request.Headers.Add("User-Agent", $"Everywhere/{App.Version}");
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/src/Everywhere.Cloud/Everywhere.Cloud.csproj b/src/Everywhere.Cloud/Everywhere.Cloud.csproj
index 004f70425..b5ac60855 100644
--- a/src/Everywhere.Cloud/Everywhere.Cloud.csproj
+++ b/src/Everywhere.Cloud/Everywhere.Cloud.csproj
@@ -3,15 +3,16 @@
true
Everywhere.Cloud.I18N
+ $(NoWarn);CS0436
-
-
-
-
+
+
+
+
@@ -19,6 +20,14 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
+
diff --git a/src/Everywhere.Core/App.axaml.cs b/src/Everywhere.Core/App.axaml.cs
index 81fde6ee3..c1082f4c0 100644
--- a/src/Everywhere.Core/App.axaml.cs
+++ b/src/Everywhere.Core/App.axaml.cs
@@ -63,7 +63,9 @@ public override void Initialize()
#if DEBUG
if (Design.IsDesignMode)
{
- ServiceLocator.Build(x => x.AddAvaloniaBasicServices());
+ var services = new ServiceCollection();
+ services.AddAvaloniaBasicServices();
+ ServiceLocator.SetProvider(services.BuildServiceProvider());
return;
}
diff --git a/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs b/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs
index 1b1a9f70f..81df7fbb3 100644
--- a/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs
+++ b/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
namespace Everywhere.Chat.Plugins.Mcp;
@@ -36,94 +37,94 @@ public static IServiceCollection AddManagedMcp(this IServiceCollection services)
return services;
}
-
- ///
- /// A delegating handler that buffers the request content to compute and set the
- /// Content-Length header. This is useful for servers that do not support
- /// chunked transfer encoding.
- ///
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- private sealed class ContentLengthBufferingHandler : DelegatingHandler
+}
+
+///
+/// A delegating handler that buffers the request content to compute and set the
+/// Content-Length header. This is useful for servers that do not support
+/// chunked transfer encoding.
+///
+[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+public sealed class ContentLengthBufferingHandler : DelegatingHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ if (request.Content is not null)
{
- if (request.Content is not null)
- {
- // By calling LoadIntoBufferAsync, we force the content to be buffered in memory.
- // This allows the HttpContent instance to calculate its length, which then gets
- // automatically set as the Content-Length header when the request is sent.
- // This effectively disables chunked transfer encoding.
- await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false);
- request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
- }
-
- return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ // By calling LoadIntoBufferAsync, we force the content to be buffered in memory.
+ // This allows the HttpContent instance to calculate its length, which then gets
+ // automatically set as the Content-Length header when the request is sent.
+ // This effectively disables chunked transfer encoding.
+ await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false);
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
- }
- ///
- /// A delegating handler that intercepts non-404 4xx responses from MCP servers
- /// and converts them to 404 if the response body indicates a session expired error.
- /// This allows the SDK's standard SetSessionExpired path to handle it.
- ///
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- private sealed class McpSessionExpiryHandler : DelegatingHandler
+ return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+}
+
+///
+/// A delegating handler that intercepts non-404 4xx responses from MCP servers
+/// and converts them to 404 if the response body indicates a session expired error.
+/// This allows the SDK's standard SetSessionExpired path to handle it.
+///
+[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+public sealed class McpSessionExpiryHandler : DelegatingHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
-
- // Only intercept non-404 4xx responses.
- if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500)
- {
- // Buffer the response content so we can read it and still return it if no match.
- var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- if (!ContainsSessionExpiredKeyword(body)) return response;
+ var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
- var newContent = new StringContent(body);
+ // Only intercept non-404 4xx responses.
+ if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500)
+ {
+ // Buffer the response content so we can read it and still return it if no match.
+ var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ if (!ContainsSessionExpiredKeyword(body)) return response;
- // Preserve original content headers (such as Content-Type/charset).
- // Skip Content-Length because it is computed from the replacement content.
- foreach (var header in response.Content.Headers)
- {
- if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
- {
- newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
- }
- }
+ var newContent = new StringContent(body);
- var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound)
- {
- RequestMessage = response.RequestMessage,
- ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)",
- Version = response.Version,
- Content = newContent,
- };
-
- // Copy response headers.
- foreach (var header in response.Headers)
+ // Preserve original content headers (such as Content-Type/charset).
+ // Skip Content-Length because it is computed from the replacement content.
+ foreach (var header in response.Content.Headers)
+ {
+ if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
- newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
+ }
- // Preserve trailing headers as well.
- foreach (var header in response.TrailingHeaders)
- {
- newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value);
- }
+ var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound)
+ {
+ RequestMessage = response.RequestMessage,
+ ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)",
+ Version = response.Version,
+ Content = newContent,
+ };
+
+ // Copy response headers.
+ foreach (var header in response.Headers)
+ {
+ newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
- response.Dispose();
- return newResponse;
+ // Preserve trailing headers as well.
+ foreach (var header in response.TrailingHeaders)
+ {
+ newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value);
}
- return response;
+ response.Dispose();
+ return newResponse;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool ContainsSessionExpiredKeyword(string body) =>
- body.Contains("session", StringComparison.OrdinalIgnoreCase) &&
- (body.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
- body.Contains("expires", StringComparison.OrdinalIgnoreCase) ||
- body.Contains("not found", StringComparison.OrdinalIgnoreCase));
+ return response;
}
-}
\ No newline at end of file
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool ContainsSessionExpiredKeyword(string body) =>
+ body.Contains("session", StringComparison.OrdinalIgnoreCase) &&
+ (body.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
+ body.Contains("expires", StringComparison.OrdinalIgnoreCase) ||
+ body.Contains("not found", StringComparison.OrdinalIgnoreCase));
+}
diff --git a/src/Everywhere.Core/Common/ServiceLocator.cs b/src/Everywhere.Core/Common/ServiceLocator.cs
index f748e432a..9b24ee222 100644
--- a/src/Everywhere.Core/Common/ServiceLocator.cs
+++ b/src/Everywhere.Core/Common/ServiceLocator.cs
@@ -6,23 +6,22 @@ public static class ServiceLocator
{
private static IServiceProvider? _serviceProvider;
- public static void Build(Action configureServices)
+ public static void SetProvider(IServiceProvider serviceProvider)
{
if (_serviceProvider != null) throw new InvalidOperationException($"{nameof(ServiceLocator)} is already built.");
- var serviceCollection = new ServiceCollection();
- configureServices(serviceCollection);
- _serviceProvider = serviceCollection.BuildServiceProvider();
+ _serviceProvider = serviceProvider;
}
public static object Resolve(Type type, object? key = null)
{
if (_serviceProvider == null) throw new InvalidOperationException($"{nameof(ServiceLocator)} is not built.");
+ if (key != null) throw new NotSupportedException("Keyed service resolution is not supported by the source-generated provider.");
if (key == null) return _serviceProvider.GetRequiredService(type);
- return _serviceProvider.GetRequiredKeyedService(type, key);
+ throw new InvalidOperationException("Unreachable service resolution branch.");
}
public static T Resolve(object? key = null) where T : class
{
return (T)Resolve(typeof(T), key);
}
-}
\ No newline at end of file
+}
diff --git a/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs b/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs
new file mode 100644
index 000000000..8f5ca7363
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs
@@ -0,0 +1,164 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using Everywhere.AI.Prompts.Database;
+using Everywhere.Chat;
+using Everywhere.Chat.Plugins;
+using Everywhere.Chat.Plugins.BuiltIn;
+using Everywhere.Chat.Plugins.Mcp;
+using Everywhere.Common;
+using Everywhere.Configuration;
+using Everywhere.Configuration.Engine;
+using Everywhere.Database;
+using Everywhere.Initialization;
+using Everywhere.Interop;
+using Everywhere.Skills;
+using Everywhere.Statistics;
+using Everywhere.Statistics.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Serilog;
+using Serilog.Extensions.Logging;
+
+namespace Everywhere.DependencyInjection;
+
+public static class ApplicationServiceCollection
+{
+ public static IServiceCollection Configure(IServiceCollection services) =>
+ services
+ .ConfigureLogging()
+ .ConfigureHttpClients()
+ .ConfigureEntityFramework();
+
+ // Pure.DI.MS exports roots by their own contract. These aliases preserve
+ // MS DI enumerable behavior for services also consumed through aggregate
+ // contracts such as IAsyncInitializer and BuiltInChatPlugin.
+ public static IServiceCollection ConfigureCoreAliases(IServiceCollection services)
+ {
+ // Startup initializer aliases.
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+
+ // Built-in chat plugin aliases.
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+
+ return services;
+ }
+
+ // Platform initializers and plugins are registered after the platform
+ // composition exports its concrete roots.
+ public static IServiceCollection ConfigurePlatformAliases(IServiceCollection services) where TPlatformPlugin : BuiltInChatPlugin
+ {
+ // Platform startup initializer aliases.
+ services.AddTransient(x => x.GetRequiredService());
+ services.AddTransient(x => x.GetRequiredService());
+
+ // Platform chat plugin alias.
+ services.AddTransient(x => x.GetRequiredService());
+ return services;
+ }
+
+ private static IServiceCollection ConfigureLogging(this IServiceCollection services) =>
+ services.AddLogging(builder => builder
+#if DEBUG
+ .SetMinimumLevel(LogLevel.Debug)
+#endif
+ .AddSerilog(dispose: true)
+ .AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning));
+
+ private static IServiceCollection ConfigureHttpClients(this IServiceCollection services)
+ {
+ // Delegating handlers used by named clients.
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ // Default app HTTP client.
+ services
+ .AddHttpClient(
+ Options.DefaultName,
+ client => client.Timeout = TimeSpan.FromSeconds(10))
+ .ConfigurePrimaryHttpMessageHandler(x => CreateHttpClientHandler(x.GetRequiredService()))
+ .AddHttpMessageHandler();
+
+ // MCP transport HTTP client.
+ services
+ .AddHttpClient(
+ McpServiceExtension.McpClientName,
+ client => client.Timeout = TimeSpan.FromSeconds(30))
+ .ConfigurePrimaryHttpMessageHandler(x => CreateHttpClientHandler(x.GetRequiredService()))
+ .AddHttpMessageHandler()
+ .AddHttpMessageHandler();
+
+ return services;
+ }
+
+ private static IServiceCollection ConfigureEntityFramework(this IServiceCollection services) =>
+ services
+ // Chat database context factory.
+ .AddDbContextFactory((_, options) =>
+ {
+ var dbPath = RuntimeConstants.GetDatabasePath("chat.db");
+ options.UseSqlite($"Data Source={dbPath}");
+ })
+
+ // Prompt database context factory.
+ .AddDbContextFactory((_, options) =>
+ {
+ var dbPath = RuntimeConstants.GetDatabasePath("prompt.db");
+ options.UseSqlite($"Data Source={dbPath}");
+ })
+
+ // Statistics database context factory.
+ .AddDbContextFactory((_, options) =>
+ {
+ var dbPath = RuntimeConstants.GetDatabasePath("statistics.db");
+ options.UseSqlite($"Data Source={dbPath}");
+ });
+
+ private static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy) =>
+ new()
+ {
+ Proxy = proxy,
+ UseProxy = true,
+ AllowAutoRedirect = true,
+ };
+
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ private sealed class DefaultUserAgentHandler : DelegatingHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ request.Headers.Remove("User-Agent");
+ request.Headers.Add("User-Agent", $"Chrome/142.0.0.0 Safari/537.36 Everywhere/{App.Version}");
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ private sealed class CloudUserAgentHandler : DelegatingHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ request.Headers.Remove("User-Agent");
+ request.Headers.Add("User-Agent", $"Everywhere/{App.Version}");
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs b/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs
new file mode 100644
index 000000000..64f888329
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs
@@ -0,0 +1,21 @@
+using Avalonia.Controls.ApplicationLifetimes;
+using Everywhere.Views;
+using ShadUI;
+using ZLinq;
+
+namespace Everywhere.DependencyInjection;
+
+public static class ApplicationServiceProviderFactories
+{
+ public static DialogManager CreateDialogManager() => TryGetReactiveHost()?.DialogHost.Manager ?? new DialogManager();
+
+ public static ToastHost CreateToastHost() => TryGetReactiveHost()?.ToastHost ?? new ToastHost();
+
+ private static IReactiveHost? TryGetReactiveHost()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime) return null;
+ return lifetime.Windows.AsValueEnumerable().FirstOrDefault(w => w.IsActive) as IReactiveHost ??
+ lifetime.MainWindow as IReactiveHost ??
+ lifetime.Windows.AsValueEnumerable().OfType().FirstOrDefault();
+ }
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs b/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs
new file mode 100644
index 000000000..119190b3f
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs
@@ -0,0 +1,50 @@
+using Everywhere.Views;
+using Everywhere.Views.Pages;
+using Pure.DI;
+using ShadUI;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.DependencyInjection;
+
+public partial class CoreComposition
+{
+ // ReSharper disable once UnusedMember.Local
+ private static void SetupAvaloniaServices() =>
+ DI.Setup()
+ // UI host services that require Avalonia runtime state.
+ .Bind().To(_ => ApplicationServiceProviderFactories.CreateDialogManager())
+ .Bind().To(_ => ApplicationServiceProviderFactories.CreateToastHost())
+ .Bind().As(Singleton).To()
+
+ // Chat window shell and animation target.
+ .Bind().As(Singleton).To()
+ .Bind().Bind().As(Singleton).To()
+
+ // Main navigation pages.
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().To()
+ .Bind().To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().Bind(Tag.Unique).To()
+
+ // Secondary views and app shell.
+ .Bind().To()
+ .Bind().To()
+ .Bind().To()
+ .Bind().To()
+ .Bind().As(Singleton).To()
+ .Bind().As(Singleton).To()
+
+ // Visual effects.
+ .Bind().As(Singleton).To();
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs b/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs
new file mode 100644
index 000000000..27fb4790a
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs
@@ -0,0 +1,19 @@
+using Everywhere.Chat.Plugins;
+using Everywhere.Chat.Plugins.BuiltIn;
+using Pure.DI;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.DependencyInjection;
+
+public partial class CoreComposition
+{
+ // ReSharper disable once UnusedMember.Local
+ private static void SetupChatPluginServices() =>
+ DI.Setup()
+ // Core built-in chat plugins.
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To()
+ .Bind().Bind(Tag.Unique).As(Singleton).To();
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/ChatServices.cs b/src/Everywhere.Core/DependencyInjection/ChatServices.cs
new file mode 100644
index 000000000..9de8936eb
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/ChatServices.cs
@@ -0,0 +1,31 @@
+using Everywhere.AI;
+using Everywhere.Chat;
+using Everywhere.Common;
+using Everywhere.Skills;
+using Everywhere.Web;
+using Pure.DI;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.DependencyInjection;
+
+public partial class CoreComposition
+{
+ // ReSharper disable once UnusedMember.Local
+ private static void SetupChatServices() =>
+ DI.Setup()
+ // Kernel mixins and skill services.
+ .Bind().As(Singleton).To()
+ .Bind().As(Singleton).To()
+ .Bind().Bind().Bind().As(Singleton).To()
+
+ // Chat runtime services.
+ .Bind().As(Singleton).To()
+ .Bind().As(Singleton).To()
+
+ // Chat-adjacent host helpers.
+ .Bind().As(Singleton).To()
+ .Bind().As(Singleton).To()
+
+ // Chat context state.
+ .Bind().Bind().As(Singleton).To();
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/CoreComposition.cs b/src/Everywhere.Core/DependencyInjection/CoreComposition.cs
new file mode 100644
index 000000000..52fee21b6
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/CoreComposition.cs
@@ -0,0 +1,159 @@
+using System.Net;
+using Everywhere.AI;
+using Everywhere.AI.Prompts;
+using Everywhere.AI.Prompts.Database;
+using Everywhere.Chat;
+using Everywhere.Chat.Plugins.BuiltIn;
+using Everywhere.Common;
+using Everywhere.Common.Notification;
+using Everywhere.Configuration;
+using Everywhere.Configuration.Engine;
+using Everywhere.Database;
+using Everywhere.Initialization;
+using Everywhere.Interop;
+using Everywhere.Skills;
+using Everywhere.Statistics;
+using Everywhere.Storage;
+using Everywhere.StrategyEngine;
+using Everywhere.Views;
+using Everywhere.Views.Pages;
+using Everywhere.Web;
+using Microsoft.Extensions.Logging;
+using Pure.DI;
+using Pure.DI.MS;
+using ShadUI;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.DependencyInjection;
+
+#pragma warning disable CA1416
+
+// Project DI convention:
+// - Bind application services in the business-focused partial setup files.
+// - Root exports a service to the final Microsoft IServiceProvider; Pure.DI.MS
+// registers by contract type, tag, and lifetime, so roots stay anonymous unless
+// code directly calls a generated composition member.
+// - OnNewRoot is required by Pure.DI.MS to add roots to IServiceCollection.
+// - OnCannotResolve is reserved for framework services owned by MS DI.
+public partial class CoreComposition : ServiceProviderFactory
+{
+ private const RootKinds ExportedRoot = RootKinds.Default | RootKinds.Exported;
+
+ // ReSharper disable once UnusedMember.Local
+ private static void SetupCoreServices() =>
+ DI.Setup()
+ // Pure.DI.MS hooks and framework fallbacks.
+ .Hint(Hint.OnCannotResolve, "On")
+ .Hint(Hint.OnCannotResolvePartial, "Off")
+ .Hint(Hint.OnNewRoot, "On")
+ .Hint(Hint.OnNewRootPartial, "Off")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*")
+ .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*")
+
+ // Logging facade instances are created by Pure.DI; ILoggerFactory stays in MS DI.
+ .Bind>().As(Singleton).To>()
+
+ // Root service provider and settings roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+#if WINDOWS
+ .Root(kind: ExportedRoot)
+#endif
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Network and runtime roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Storage, prompt, notification, and statistics roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Avalonia host, shell, and effect roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Navigation page roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Secondary view roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Chat, skill, and browser interaction roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Interop and strategy engine roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Built-in chat plugin concrete roots.
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+ .Root(kind: ExportedRoot)
+
+ // Pure.DI aggregate consumed through the final MS provider.
+ .Root>(kind: ExportedRoot);
+}
\ No newline at end of file
diff --git a/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs b/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs
new file mode 100644
index 000000000..dd09b6955
--- /dev/null
+++ b/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs
@@ -0,0 +1,31 @@
+using Everywhere.Chat.Plugins;
+using Everywhere.Cloud;
+using Everywhere.Common;
+using Everywhere.Interop;
+using Microsoft.Extensions.DependencyInjection;
+using Pure.DI;
+using static Pure.DI.Lifetime;
+
+namespace Everywhere.DependencyInjection;
+
+public partial class CoreComposition
+{
+ // Core services sometimes depend on platform or cloud services that are
+ // registered by later compositions. This file is the intentional boundary
+ // where Core asks the final MS provider for those external roots.
+ // ReSharper disable once UnusedMember.Local
+ private void SetupExternalBoundaryServices() =>
+ DI.Setup()
+ // Final MS provider bridge.
+ .Bind().To(_ => ServiceProvider)
+
+ // Cloud and platform services consumed by Core.
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService())
+ .Bind