diff --git a/LanMountainDesktop.AirAppDevServer/AirAppDevServer.cs b/LanMountainDesktop.AirAppDevServer/AirAppDevServer.cs
new file mode 100644
index 0000000..743cda3
--- /dev/null
+++ b/LanMountainDesktop.AirAppDevServer/AirAppDevServer.cs
@@ -0,0 +1,230 @@
+using System.Diagnostics;
+using Microsoft.Build.Locator;
+using Microsoft.Build.Execution;
+
+namespace LanMountainDesktop.AirAppDevServer;
+
+///
+/// AirApp 开发服务器
+/// 提供文件监视、自动编译、热重载功能
+///
+public sealed class AirAppDevServer
+{
+ private readonly string _projectPath;
+ private readonly int _port;
+ private readonly bool _verbose;
+ private FileSystemWatcher? _watcher;
+ private DateTime _lastBuildTime = DateTime.MinValue;
+ private readonly object _buildLock = new();
+ private bool _isBuilding;
+
+ public AirAppDevServer(string projectPath, int port, bool verbose)
+ {
+ _projectPath = Path.GetFullPath(projectPath);
+ _port = port;
+ _verbose = verbose;
+ }
+
+ public Task StartAsync()
+ {
+ // 初始构建
+ Console.WriteLine("🔨 初始构建中...");
+ if (!BuildProject())
+ {
+ Console.WriteLine("❌ 初始构建失败");
+ return Task.CompletedTask;
+ }
+ Console.WriteLine("✅ 初始构建成功");
+ Console.WriteLine();
+
+ // 启动文件监视
+ StartFileWatcher();
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync()
+ {
+ _watcher?.Dispose();
+ return Task.CompletedTask;
+ }
+
+ private void StartFileWatcher()
+ {
+ _watcher = new FileSystemWatcher(_projectPath)
+ {
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
+ Filter = "*.*",
+ IncludeSubdirectories = true,
+ EnableRaisingEvents = true
+ };
+
+ _watcher.Changed += OnFileChanged;
+ _watcher.Created += OnFileChanged;
+ _watcher.Deleted += OnFileChanged;
+ _watcher.Renamed += OnFileRenamed;
+
+ Console.WriteLine("👁️ 文件监视已启动,等待更改...");
+ Console.WriteLine();
+ }
+
+ private void OnFileChanged(object sender, FileSystemEventArgs e)
+ {
+ // 忽略 bin、obj、.vs 等目录
+ if (e.FullPath.Contains("\\bin\\") ||
+ e.FullPath.Contains("\\obj\\") ||
+ e.FullPath.Contains("\\.vs\\") ||
+ e.FullPath.Contains("\\.git\\"))
+ {
+ return;
+ }
+
+ // 只处理源代码文件
+ var ext = Path.GetExtension(e.FullPath).ToLowerInvariant();
+ if (ext != ".cs" && ext != ".axaml" && ext != ".json" && ext != ".csproj")
+ {
+ return;
+ }
+
+ // 防止重复触发(文件保存时可能触发多次)
+ var now = DateTime.Now;
+ if ((now - _lastBuildTime).TotalMilliseconds < 500)
+ {
+ return;
+ }
+
+ LogVerbose($"📝 检测到文件更改: {Path.GetFileName(e.FullPath)}");
+ TriggerRebuild();
+ }
+
+ private void OnFileRenamed(object sender, RenamedEventArgs e)
+ {
+ LogVerbose($"📝 检测到文件重命名: {Path.GetFileName(e.OldFullPath)} -> {Path.GetFileName(e.FullPath)}");
+ TriggerRebuild();
+ }
+
+ private void TriggerRebuild()
+ {
+ lock (_buildLock)
+ {
+ if (_isBuilding)
+ {
+ LogVerbose("⏳ 构建进行中,跳过此次触发");
+ return;
+ }
+
+ _isBuilding = true;
+ }
+
+ Task.Run(() =>
+ {
+ try
+ {
+ // 短暂延迟,让文件写入完成
+ Thread.Sleep(300);
+
+ Console.WriteLine("🔄 重新构建中...");
+ var success = BuildProject();
+
+ _lastBuildTime = DateTime.Now;
+
+ if (success)
+ {
+ Console.WriteLine($"✅ 重新构建成功 [{DateTime.Now:HH:mm:ss}]");
+ Console.WriteLine("♻️ 热重载已生效");
+ }
+ else
+ {
+ Console.WriteLine($"❌ 重新构建失败 [{DateTime.Now:HH:mm:ss}]");
+ }
+ Console.WriteLine();
+ }
+ finally
+ {
+ lock (_buildLock)
+ {
+ _isBuilding = false;
+ }
+ }
+ });
+ }
+
+ private bool BuildProject()
+ {
+ try
+ {
+ // 查找项目文件
+ var projectFile = FindProjectFile();
+ if (projectFile == null)
+ {
+ Console.WriteLine("❌ 未找到项目文件 (.csproj)");
+ return false;
+ }
+
+ LogVerbose($"📄 项目文件: {Path.GetFileName(projectFile)}");
+
+ // 使用 dotnet build
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"build \"{projectFile}\" -c Debug --nologo",
+ WorkingDirectory = Path.GetDirectoryName(projectFile),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo);
+ if (process == null)
+ {
+ Console.WriteLine("❌ 无法启动 dotnet build");
+ return false;
+ }
+
+ var output = process.StandardOutput.ReadToEnd();
+ var error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ if (_verbose)
+ {
+ if (!string.IsNullOrWhiteSpace(output))
+ {
+ Console.WriteLine(output);
+ }
+ }
+
+ if (process.ExitCode != 0)
+ {
+ Console.WriteLine("❌ 构建错误:");
+ Console.WriteLine(error);
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 构建异常: {ex.Message}");
+ if (_verbose)
+ {
+ Console.WriteLine(ex.StackTrace);
+ }
+ return false;
+ }
+ }
+
+ private string? FindProjectFile()
+ {
+ var files = Directory.GetFiles(_projectPath, "*.csproj", SearchOption.TopDirectoryOnly);
+ return files.Length > 0 ? files[0] : null;
+ }
+
+ private void LogVerbose(string message)
+ {
+ if (_verbose)
+ {
+ Console.WriteLine($"[VERBOSE] {message}");
+ }
+ }
+}
diff --git a/LanMountainDesktop.AirAppDevServer/AirAppPackager.cs b/LanMountainDesktop.AirAppDevServer/AirAppPackager.cs
new file mode 100644
index 0000000..5bc44af
--- /dev/null
+++ b/LanMountainDesktop.AirAppDevServer/AirAppPackager.cs
@@ -0,0 +1,119 @@
+using System.Diagnostics;
+using System.IO.Compression;
+
+namespace LanMountainDesktop.AirAppDevServer;
+
+///
+/// AirApp 打包工具
+/// 将 AirApp 项目打包为 .laapp 文件
+///
+public sealed class AirAppPackager
+{
+ private readonly string _projectPath;
+
+ public AirAppPackager(string projectPath)
+ {
+ _projectPath = Path.GetFullPath(projectPath);
+ }
+
+ public async Task PackageAsync(string? outputPath)
+ {
+ Console.WriteLine("🔨 构建项目...");
+ if (!await BuildProjectAsync())
+ {
+ throw new InvalidOperationException("构建失败");
+ }
+
+ var binPath = Path.Combine(_projectPath, "bin", "Release", "net10.0");
+ if (!Directory.Exists(binPath))
+ {
+ binPath = Path.Combine(_projectPath, "bin", "Debug", "net10.0");
+ if (!Directory.Exists(binPath))
+ {
+ throw new InvalidOperationException("未找到构建输出");
+ }
+ }
+
+ Console.WriteLine($"📁 输出目录: {binPath}");
+
+ // 确定输出文件名
+ var projectName = Path.GetFileNameWithoutExtension(
+ Directory.GetFiles(_projectPath, "*.csproj").FirstOrDefault() ?? "AirApp");
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ outputPath = Path.Combine(binPath, $"{projectName}.laapp");
+ }
+ else
+ {
+ outputPath = Path.GetFullPath(outputPath);
+ if (Directory.Exists(outputPath))
+ {
+ outputPath = Path.Combine(outputPath, $"{projectName}.laapp");
+ }
+ }
+
+ // 删除旧的包
+ if (File.Exists(outputPath))
+ {
+ File.Delete(outputPath);
+ }
+
+ Console.WriteLine($"📦 打包到: {outputPath}");
+
+ // 创建 ZIP 包
+ using (var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create))
+ {
+ var filesToPackage = Directory.GetFiles(binPath, "*.*", SearchOption.AllDirectories)
+ .Where(f => !f.Contains(".pdb") && !f.EndsWith(".laapp"))
+ .ToList();
+
+ Console.WriteLine($"📄 打包 {filesToPackage.Count} 个文件...");
+
+ foreach (var file in filesToPackage)
+ {
+ var relativePath = Path.GetRelativePath(binPath, file);
+ archive.CreateEntryFromFile(file, relativePath);
+ }
+ }
+
+ Console.WriteLine($"✅ 包大小: {new FileInfo(outputPath).Length / 1024} KB");
+
+ return outputPath;
+ }
+
+ private async Task BuildProjectAsync()
+ {
+ var projectFile = Directory.GetFiles(_projectPath, "*.csproj").FirstOrDefault();
+ if (projectFile == null)
+ {
+ Console.WriteLine("❌ 未找到项目文件");
+ return false;
+ }
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"build \"{projectFile}\" -c Release --nologo",
+ WorkingDirectory = _projectPath,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo);
+ if (process == null) return false;
+
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ var error = await process.StandardError.ReadToEndAsync();
+ Console.WriteLine($"❌ 构建错误:\n{error}");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/LanMountainDesktop.AirAppDevServer/AirAppPreviewer.cs b/LanMountainDesktop.AirAppDevServer/AirAppPreviewer.cs
new file mode 100644
index 0000000..dfeb706
--- /dev/null
+++ b/LanMountainDesktop.AirAppDevServer/AirAppPreviewer.cs
@@ -0,0 +1,129 @@
+using System.Diagnostics;
+using System.Text.Json;
+
+namespace LanMountainDesktop.AirAppDevServer;
+
+///
+/// AirApp 预览工具
+/// 在独立窗口中预览组件或窗口,无需安装到宿主
+///
+public sealed class AirAppPreviewer
+{
+ private readonly string _projectPath;
+
+ public AirAppPreviewer(string projectPath)
+ {
+ _projectPath = Path.GetFullPath(projectPath);
+ }
+
+ public async Task PreviewComponentAsync(string componentId)
+ {
+ Console.WriteLine($"🎨 预览组件: {componentId}");
+ await LaunchPreviewAsync("component", componentId);
+ }
+
+ public async Task PreviewWindowAsync(string windowId)
+ {
+ Console.WriteLine($"🪟 预览窗口: {windowId}");
+ await LaunchPreviewAsync("window", windowId);
+ }
+
+ public async Task PreviewAllAsync()
+ {
+ Console.WriteLine("📋 加载 AirApp 清单...");
+
+ var manifest = await LoadManifestAsync();
+ if (manifest == null)
+ {
+ Console.WriteLine("❌ 未找到 airapp.json");
+ return;
+ }
+
+ Console.WriteLine($"✅ AirApp: {manifest.Name}");
+ Console.WriteLine();
+
+ // 显示可用的组件和窗口
+ if (manifest.Components?.Count > 0)
+ {
+ Console.WriteLine("📦 可用组件:");
+ foreach (var comp in manifest.Components)
+ {
+ Console.WriteLine($" - {comp.Id}: {comp.Name}");
+ }
+ Console.WriteLine();
+ }
+
+ if (manifest.Windows?.Count > 0)
+ {
+ Console.WriteLine("🪟 可用窗口:");
+ foreach (var win in manifest.Windows)
+ {
+ Console.WriteLine($" - {win.Id}: {win.Name}");
+ }
+ Console.WriteLine();
+ }
+
+ Console.WriteLine("使用以下命令预览:");
+ Console.WriteLine(" airapp-dev preview --component ");
+ Console.WriteLine(" airapp-dev preview --window ");
+ }
+
+ private async Task LaunchPreviewAsync(string type, string id)
+ {
+ // 确保项目已构建
+ var binPath = Path.Combine(_projectPath, "bin", "Debug", "net10.0");
+ if (!Directory.Exists(binPath))
+ {
+ Console.WriteLine("❌ 未找到构建输出,请先运行: dotnet build");
+ return;
+ }
+
+ Console.WriteLine($"📁 输出路径: {binPath}");
+ Console.WriteLine("🚀 启动预览窗口...");
+ Console.WriteLine();
+ Console.WriteLine("💡 提示: 关闭预览窗口以退出");
+ Console.WriteLine();
+
+ // TODO: 这里需要启动一个预览宿主应用
+ // 预览宿主会加载 AirApp 并显示指定的组件或窗口
+ Console.WriteLine("⚠️ 预览功能需要配合 LanMountainDesktop 宿主运行");
+ Console.WriteLine(" 暂时请使用: dotnet run --project LanMountainDesktop.csproj -- --debug-airapp ");
+
+ await Task.CompletedTask;
+ }
+
+ private async Task LoadManifestAsync()
+ {
+ var manifestPath = Path.Combine(_projectPath, "airapp.json");
+ if (!File.Exists(manifestPath))
+ {
+ return null;
+ }
+
+ var json = await File.ReadAllTextAsync(manifestPath);
+ return JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ }
+
+ private sealed class ManifestModel
+ {
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public List? Components { get; set; }
+ public List? Windows { get; set; }
+ }
+
+ private sealed class ComponentModel
+ {
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ }
+
+ private sealed class WindowModel
+ {
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ }
+}
diff --git a/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj b/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj
new file mode 100644
index 0000000..768461a
--- /dev/null
+++ b/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj
@@ -0,0 +1,20 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ preview
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop.AirAppDevServer/Program.cs b/LanMountainDesktop.AirAppDevServer/Program.cs
new file mode 100644
index 0000000..c301e0d
--- /dev/null
+++ b/LanMountainDesktop.AirAppDevServer/Program.cs
@@ -0,0 +1,149 @@
+using System.CommandLine;
+using System.Diagnostics;
+
+namespace LanMountainDesktop.AirAppDevServer;
+
+///
+/// AirApp 开发服务器主程序
+/// 提供热重载、实时预览等开发功能
+///
+class Program
+{
+ static async Task Main(string[] args)
+ {
+ var rootCommand = new RootCommand("LanMountainDesktop AirApp 开发服务器");
+
+ // 开发模式命令
+ var devCommand = new Command("dev", "启动开发服务器(支持热重载)");
+ var projectPathOption = new Option(
+ aliases: new[] { "--project", "-p" },
+ description: "AirApp 项目路径",
+ getDefaultValue: () => Directory.GetCurrentDirectory());
+ var portOption = new Option(
+ aliases: new[] { "--port" },
+ description: "开发服务器端口",
+ getDefaultValue: () => 5000);
+ var verboseOption = new Option(
+ aliases: new[] { "--verbose", "-v" },
+ description: "显示详细日志");
+
+ devCommand.AddOption(projectPathOption);
+ devCommand.AddOption(portOption);
+ devCommand.AddOption(verboseOption);
+
+ devCommand.SetHandler(async (projectPath, port, verbose) =>
+ {
+ await RunDevServerAsync(projectPath, port, verbose);
+ }, projectPathOption, portOption, verboseOption);
+
+ // 预览命令
+ var previewCommand = new Command("preview", "预览 AirApp(无需安装到宿主)");
+ var componentOption = new Option(
+ aliases: new[] { "--component", "-c" },
+ description: "要预览的组件 ID");
+ var windowOption = new Option(
+ aliases: new[] { "--window", "-w" },
+ description: "要预览的窗口 ID");
+
+ previewCommand.AddOption(projectPathOption);
+ previewCommand.AddOption(componentOption);
+ previewCommand.AddOption(windowOption);
+
+ previewCommand.SetHandler(async (projectPath, component, window) =>
+ {
+ await RunPreviewAsync(projectPath, component, window);
+ }, projectPathOption, componentOption, windowOption);
+
+ // 打包命令
+ var packageCommand = new Command("package", "打包 AirApp 为 .laapp 文件");
+ var outputOption = new Option(
+ aliases: new[] { "--output", "-o" },
+ description: "输出路径");
+
+ packageCommand.AddOption(projectPathOption);
+ packageCommand.AddOption(outputOption);
+
+ packageCommand.SetHandler(async (projectPath, output) =>
+ {
+ await PackageAirAppAsync(projectPath, output);
+ }, projectPathOption, outputOption);
+
+ rootCommand.AddCommand(devCommand);
+ rootCommand.AddCommand(previewCommand);
+ rootCommand.AddCommand(packageCommand);
+
+ return await rootCommand.InvokeAsync(args);
+ }
+
+ static async Task RunDevServerAsync(string projectPath, int port, bool verbose)
+ {
+ Console.WriteLine("🚀 启动 AirApp 开发服务器...");
+ Console.WriteLine($"📁 项目路径: {projectPath}");
+ Console.WriteLine($"🔌 端口: {port}");
+ Console.WriteLine();
+
+ var server = new AirAppDevServer(projectPath, port, verbose);
+ await server.StartAsync();
+
+ Console.WriteLine();
+ Console.WriteLine("✅ 开发服务器已启动");
+ Console.WriteLine($"🌐 预览地址: http://localhost:{port}");
+ Console.WriteLine();
+ Console.WriteLine("按 Ctrl+C 停止服务器...");
+ Console.WriteLine();
+
+ // 等待取消信号
+ var cts = new CancellationTokenSource();
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ e.Cancel = true;
+ cts.Cancel();
+ };
+
+ try
+ {
+ await Task.Delay(Timeout.Infinite, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ Console.WriteLine();
+ Console.WriteLine("🛑 正在停止服务器...");
+ }
+
+ await server.StopAsync();
+ Console.WriteLine("✅ 服务器已停止");
+ }
+
+ static async Task RunPreviewAsync(string projectPath, string? component, string? window)
+ {
+ Console.WriteLine("👁️ 启动 AirApp 预览...");
+ Console.WriteLine($"📁 项目路径: {projectPath}");
+
+ var previewer = new AirAppPreviewer(projectPath);
+
+ if (!string.IsNullOrEmpty(component))
+ {
+ await previewer.PreviewComponentAsync(component);
+ }
+ else if (!string.IsNullOrEmpty(window))
+ {
+ await previewer.PreviewWindowAsync(window);
+ }
+ else
+ {
+ await previewer.PreviewAllAsync();
+ }
+ }
+
+ static async Task PackageAirAppAsync(string projectPath, string? output)
+ {
+ Console.WriteLine("📦 打包 AirApp...");
+ Console.WriteLine($"📁 项目路径: {projectPath}");
+
+ var packager = new AirAppPackager(projectPath);
+ var outputPath = await packager.PackageAsync(output);
+
+ Console.WriteLine();
+ Console.WriteLine($"✅ 打包完成: {outputPath}");
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppAppearanceSnapshot.cs b/LanMountainDesktop.AirAppSdk/AirAppAppearanceSnapshot.cs
new file mode 100644
index 0000000..5522802
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppAppearanceSnapshot.cs
@@ -0,0 +1,49 @@
+using Avalonia.Media;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Snapshot of the current appearance settings.
+///
+public sealed class AirAppAppearanceSnapshot
+{
+ ///
+ /// Gets whether dark mode is enabled.
+ ///
+ public bool IsDarkMode { get; init; }
+
+ ///
+ /// Gets the primary accent color.
+ ///
+ public Color AccentColor { get; init; }
+
+ ///
+ /// Gets the glass effect opacity (0.0 - 1.0).
+ ///
+ public double GlassOpacity { get; init; }
+
+ ///
+ /// Gets the corner radius preset.
+ ///
+ public AirAppCornerRadiusPreset CornerRadiusPreset { get; init; }
+
+ ///
+ /// Gets the background color.
+ ///
+ public Color BackgroundColor { get; init; }
+
+ ///
+ /// Gets the foreground (text) color.
+ ///
+ public Color ForegroundColor { get; init; }
+
+ ///
+ /// Gets the border color.
+ ///
+ public Color BorderColor { get; init; }
+
+ ///
+ /// Gets additional custom properties.
+ ///
+ public IReadOnlyDictionary? CustomProperties { get; init; }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppBase.cs b/LanMountainDesktop.AirAppSdk/AirAppBase.cs
new file mode 100644
index 0000000..482db30
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppBase.cs
@@ -0,0 +1,119 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Base class for AirApp implementations.
+/// Inherit from this class and apply the [AirAppEntrance] attribute.
+///
+public abstract class AirAppBase : IAirApp
+{
+ ///
+ /// Gets the runtime context after the AirApp has started.
+ /// Available after OnStartedAsync is called.
+ ///
+ protected IAirAppRuntimeContext? RuntimeContext { get; private set; }
+
+ ///
+ /// Initialize the AirApp and register services.
+ /// Override this method to register your components, windows, and services.
+ ///
+ /// Host builder context
+ /// Service collection
+ public virtual void Initialize(HostBuilderContext context, IServiceCollection services)
+ {
+ // Default implementation: do nothing
+ // Derived classes can override to register services
+ }
+
+ ///
+ /// Called after the host application has started.
+ /// Override this for runtime initialization.
+ ///
+ /// AirApp runtime context
+ public virtual Task OnStartedAsync(IAirAppRuntimeContext context)
+ {
+ RuntimeContext = context;
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Called when the host application is stopping.
+ /// Override this for cleanup logic.
+ ///
+ public virtual Task OnStoppingAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Register a desktop component widget.
+ ///
+ /// Widget implementation type
+ /// Unique component identifier
+ /// Display name
+ /// Optional configuration
+ protected void RegisterComponent(
+ string id,
+ string name,
+ Action? configure = null)
+ where TWidget : class, IAirAppWidget
+ {
+ if (RuntimeContext == null)
+ {
+ throw new InvalidOperationException(
+ "RegisterComponent can only be called after OnStartedAsync. " +
+ "Use IServiceCollection extension methods in Initialize() instead.");
+ }
+
+ var options = new AirAppComponentOptions
+ {
+ Id = id,
+ Name = name,
+ WidgetType = typeof(TWidget)
+ };
+
+ configure?.Invoke(options);
+
+ // Delegate to runtime context
+ RuntimeContext.RegisterComponent(options);
+ }
+
+ ///
+ /// Register a window.
+ ///
+ /// Window implementation type
+ /// Unique window identifier
+ /// Display name
+ protected void RegisterWindow(string id, string name)
+ where TWindow : class, IAirAppWindow
+ {
+ if (RuntimeContext == null)
+ {
+ throw new InvalidOperationException(
+ "RegisterWindow can only be called after OnStartedAsync.");
+ }
+
+ RuntimeContext.RegisterWindow(id, name, typeof(TWindow));
+ }
+
+ ///
+ /// Register a service in the DI container.
+ ///
+ /// Service interface
+ /// Implementation type
+ protected void RegisterService()
+ where TService : class
+ where TImplementation : class, TService
+ {
+ if (RuntimeContext == null)
+ {
+ throw new InvalidOperationException(
+ "RegisterService can only be called after OnStartedAsync. " +
+ "Use IServiceCollection in Initialize() instead.");
+ }
+
+ RuntimeContext.RegisterService();
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppComponentOptions.cs b/LanMountainDesktop.AirAppSdk/AirAppComponentOptions.cs
new file mode 100644
index 0000000..8fd3560
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppComponentOptions.cs
@@ -0,0 +1,61 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Options for registering an AirApp desktop component.
+///
+public sealed class AirAppComponentOptions
+{
+ ///
+ /// Gets or sets the unique component identifier.
+ ///
+ public required string Id { get; set; }
+
+ ///
+ /// Gets or sets the display name.
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Gets or sets the widget implementation type.
+ /// Must implement IAirAppWidget.
+ ///
+ public required Type WidgetType { get; set; }
+
+ ///
+ /// Gets or sets the optional description.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the default width in grid cells.
+ /// Default is 2.
+ ///
+ public int DefaultWidth { get; set; } = 2;
+
+ ///
+ /// Gets or sets the default height in grid cells.
+ /// Default is 2.
+ ///
+ public int DefaultHeight { get; set; } = 2;
+
+ ///
+ /// Gets or sets the resize mode.
+ ///
+ public AirAppComponentResizeMode ResizeMode { get; set; } = AirAppComponentResizeMode.Both;
+
+ ///
+ /// Gets or sets whether this component can be added multiple times.
+ /// Default is true.
+ ///
+ public bool AllowMultipleInstances { get; set; } = true;
+
+ ///
+ /// Gets or sets the category for grouping in the component library.
+ ///
+ public string? Category { get; set; }
+
+ ///
+ /// Gets or sets the icon identifier.
+ ///
+ public string? IconKey { get; set; }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppComponentResizeMode.cs b/LanMountainDesktop.AirAppSdk/AirAppComponentResizeMode.cs
new file mode 100644
index 0000000..c8feec4
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppComponentResizeMode.cs
@@ -0,0 +1,27 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Resize mode for AirApp desktop components.
+///
+public enum AirAppComponentResizeMode
+{
+ ///
+ /// Cannot be resized.
+ ///
+ None = 0,
+
+ ///
+ /// Can be resized horizontally only.
+ ///
+ Horizontal = 1,
+
+ ///
+ /// Can be resized vertically only.
+ ///
+ Vertical = 2,
+
+ ///
+ /// Can be resized in both directions.
+ ///
+ Both = 3
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppCornerRadiusPreset.cs b/LanMountainDesktop.AirAppSdk/AirAppCornerRadiusPreset.cs
new file mode 100644
index 0000000..828ae49
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppCornerRadiusPreset.cs
@@ -0,0 +1,32 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Corner radius presets.
+///
+public enum AirAppCornerRadiusPreset
+{
+ ///
+ /// No rounded corners.
+ ///
+ None = 0,
+
+ ///
+ /// Small corner radius (4px).
+ ///
+ Small = 1,
+
+ ///
+ /// Medium corner radius (8px).
+ ///
+ Medium = 2,
+
+ ///
+ /// Large corner radius (12px).
+ ///
+ Large = 3,
+
+ ///
+ /// Extra large corner radius (16px).
+ ///
+ ExtraLarge = 4
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppEntranceAttribute.cs b/LanMountainDesktop.AirAppSdk/AirAppEntranceAttribute.cs
new file mode 100644
index 0000000..05fea07
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppEntranceAttribute.cs
@@ -0,0 +1,10 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Marks a class as the entry point for an AirApp.
+/// The marked class must inherit from AirAppBase or implement IAirApp.
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+public sealed class AirAppEntranceAttribute : Attribute
+{
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppManifest.cs b/LanMountainDesktop.AirAppSdk/AirAppManifest.cs
new file mode 100644
index 0000000..662fa25
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppManifest.cs
@@ -0,0 +1,188 @@
+using System.Text.Json;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// AirApp manifest (airapp.json).
+///
+public sealed record AirAppManifest(
+ string Id,
+ string Name,
+ string EntranceAssembly,
+ string? Description = null,
+ string? Author = null,
+ string? Version = null,
+ string? ApiVersion = null,
+ AirAppRuntimeConfiguration? Runtime = null,
+ IReadOnlyList? Components = null,
+ IReadOnlyList? Windows = null,
+ IReadOnlyList? Permissions = null,
+ IReadOnlyList? SharedContracts = null)
+{
+ private static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true
+ };
+
+ ///
+ /// Load manifest from file.
+ ///
+ public static AirAppManifest Load(string manifestPath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
+
+ using var stream = File.OpenRead(manifestPath);
+ return Load(stream, manifestPath);
+ }
+
+ ///
+ /// Load manifest from stream.
+ ///
+ public static AirAppManifest Load(Stream stream, string sourceName)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
+
+ var manifest = JsonSerializer.Deserialize(stream, SerializerOptions);
+ if (manifest is null)
+ {
+ throw new InvalidOperationException($"Failed to deserialize AirApp manifest '{sourceName}'.");
+ }
+
+ return manifest.NormalizeAndValidate(sourceName);
+ }
+
+ ///
+ /// Resolve entrance assembly path.
+ ///
+ public string ResolveEntranceAssemblyPath(string manifestPath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
+
+ if (Path.IsPathRooted(EntranceAssembly))
+ {
+ return Path.GetFullPath(EntranceAssembly);
+ }
+
+ var manifestDirectory = Path.GetDirectoryName(Path.GetFullPath(manifestPath))
+ ?? throw new InvalidOperationException($"Failed to determine directory of '{manifestPath}'.");
+
+ return Path.GetFullPath(Path.Combine(manifestDirectory, EntranceAssembly));
+ }
+
+ ///
+ /// Get runtime mode.
+ ///
+ public AirAppRuntimeMode RuntimeMode =>
+ AirAppRuntimeModes.TryParse(Runtime?.Mode, out var mode) ? mode : AirAppRuntimeMode.InProcess;
+
+ private AirAppManifest NormalizeAndValidate(string manifestPath)
+ {
+ var normalizedRuntime = (Runtime ?? new AirAppRuntimeConfiguration()).NormalizeAndValidate(manifestPath);
+
+ var normalized = this with
+ {
+ Id = RequireValue(Id, nameof(Id), manifestPath),
+ Name = RequireValue(Name, nameof(Name), manifestPath),
+ EntranceAssembly = RequireValue(EntranceAssembly, nameof(EntranceAssembly), manifestPath),
+ Description = NormalizeOptionalValue(Description),
+ Author = NormalizeOptionalValue(Author),
+ Version = NormalizeOptionalValue(Version),
+ ApiVersion = NormalizeOptionalValue(ApiVersion) ?? AirAppSdkInfo.ApiVersion,
+ Runtime = normalizedRuntime,
+ Components = Components ?? Array.Empty(),
+ Windows = Windows ?? Array.Empty(),
+ Permissions = Permissions ?? Array.Empty(),
+ SharedContracts = SharedContracts ?? Array.Empty()
+ };
+
+ // Validate API version
+ if (!System.Version.TryParse(normalized.ApiVersion, out var requestedVersion))
+ {
+ throw new InvalidOperationException(
+ $"AirApp manifest '{manifestPath}' declares invalid API version '{normalized.ApiVersion}'.");
+ }
+
+ if (!System.Version.TryParse(AirAppSdkInfo.ApiVersion, out var currentVersion))
+ {
+ throw new InvalidOperationException($"AirApp SDK API version '{AirAppSdkInfo.ApiVersion}' is invalid.");
+ }
+
+ if (requestedVersion.Major != currentVersion.Major)
+ {
+ throw new InvalidOperationException(
+ $"AirApp '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
+ $"but the host provides '{AirAppSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
+ $"This host only supports v{currentVersion.Major}.x AirApps and rejects v{requestedVersion.Major}.x packages. " +
+ $"Migrate the AirApp manifest and code to API {AirAppSdkInfo.ApiVersion}, then rebuild and republish.");
+ }
+
+ return normalized;
+ }
+
+ private static string RequireValue(string? value, string propertyName, string manifestPath)
+ {
+ var normalized = NormalizeOptionalValue(value);
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ throw new InvalidOperationException(
+ $"AirApp manifest '{manifestPath}' is missing required property '{propertyName}'.");
+ }
+
+ return normalized;
+ }
+
+ private static string? NormalizeOptionalValue(string? value)
+ {
+ return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+ }
+}
+
+///
+/// Component declaration in manifest.
+///
+public sealed record AirAppComponentManifest(
+ string Id,
+ string Name,
+ int DefaultWidth = 2,
+ int DefaultHeight = 2,
+ string? Description = null,
+ string? Category = null,
+ string? IconKey = null);
+
+///
+/// Window declaration in manifest.
+///
+public sealed record AirAppWindowManifest(
+ string Id,
+ string Name,
+ double DefaultWidth = 800,
+ double DefaultHeight = 600,
+ string? Description = null);
+
+///
+/// Shared contract reference.
+///
+public sealed record AirAppSharedContractReference(
+ string Id,
+ string Version);
+
+///
+/// Runtime configuration.
+///
+public sealed record AirAppRuntimeConfiguration
+{
+ public string? Mode { get; init; }
+ public IReadOnlyList? Capabilities { get; init; }
+
+ internal AirAppRuntimeConfiguration NormalizeAndValidate(string manifestPath)
+ {
+ return this with
+ {
+ Mode = string.IsNullOrWhiteSpace(Mode) ? "in-process" : Mode.Trim().ToLowerInvariant(),
+ Capabilities = Capabilities ?? Array.Empty()
+ };
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppRuntimeMode.cs b/LanMountainDesktop.AirAppSdk/AirAppRuntimeMode.cs
new file mode 100644
index 0000000..dfba50f
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppRuntimeMode.cs
@@ -0,0 +1,53 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Runtime mode for AirApps.
+///
+public enum AirAppRuntimeMode
+{
+ ///
+ /// Run in the host process (best performance, shared memory).
+ ///
+ InProcess = 0,
+
+ ///
+ /// Run in an isolated background process (safer, separate memory).
+ ///
+ IsolatedBackground = 1,
+
+ ///
+ /// Run in an isolated window process (full isolation).
+ ///
+ IsolatedWindow = 2
+}
+
+///
+/// Helper for parsing runtime modes.
+///
+public static class AirAppRuntimeModes
+{
+ public static bool TryParse(string? mode, out AirAppRuntimeMode result)
+ {
+ result = AirAppRuntimeMode.InProcess;
+
+ if (string.IsNullOrWhiteSpace(mode))
+ {
+ return false;
+ }
+
+ var normalized = mode.Trim().ToLowerInvariant();
+ return normalized switch
+ {
+ "in-process" => SetResult(AirAppRuntimeMode.InProcess, out result),
+ "isolated-background" => SetResult(AirAppRuntimeMode.IsolatedBackground, out result),
+ "isolated-window" => SetResult(AirAppRuntimeMode.IsolatedWindow, out result),
+ _ => false
+ };
+ }
+
+ private static bool SetResult(AirAppRuntimeMode mode, out AirAppRuntimeMode result)
+ {
+ result = mode;
+ return true;
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppSdkInfo.cs b/LanMountainDesktop.AirAppSdk/AirAppSdkInfo.cs
new file mode 100644
index 0000000..cbbacdb
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppSdkInfo.cs
@@ -0,0 +1,33 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// AirApp SDK information.
+///
+public static class AirAppSdkInfo
+{
+ ///
+ /// Current SDK version.
+ ///
+ public const string SdkVersion = "6.0.0";
+
+ ///
+ /// Current API version.
+ /// AirApps must target this major version to be compatible.
+ ///
+ public const string ApiVersion = "6.0.0";
+
+ ///
+ /// Gets the SDK display name.
+ ///
+ public static string DisplayName => "LanMountainDesktop AirApp SDK";
+
+ ///
+ /// Gets the default manifest file name.
+ ///
+ public const string ManifestFileName = "airapp.json";
+
+ ///
+ /// Gets the package file extension.
+ ///
+ public const string PackageExtension = ".laapp";
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppServiceCollectionExtensions.cs b/LanMountainDesktop.AirAppSdk/AirAppServiceCollectionExtensions.cs
new file mode 100644
index 0000000..827d7b9
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppServiceCollectionExtensions.cs
@@ -0,0 +1,158 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Extension methods for registering AirApp services.
+///
+public static class AirAppServiceCollectionExtensions
+{
+ ///
+ /// Register a desktop component.
+ ///
+ public static IServiceCollection AddAirAppComponent(
+ this IServiceCollection services,
+ string id,
+ string name,
+ Action? configure = null)
+ where TWidget : class, IAirAppWidget
+ {
+ var options = new AirAppComponentOptions
+ {
+ Id = id,
+ Name = name,
+ WidgetType = typeof(TWidget)
+ };
+
+ configure?.Invoke(options);
+
+ // Register the widget as transient (new instance per placement)
+ services.AddTransient();
+
+ // Register the component options (will be picked up by the host)
+ services.AddSingleton(options);
+
+ return services;
+ }
+
+ ///
+ /// Register a window.
+ ///
+ public static IServiceCollection AddAirAppWindow(
+ this IServiceCollection services,
+ string id,
+ string name)
+ where TWindow : class, IAirAppWindow
+ {
+ // Register the window as transient (new instance per open)
+ services.AddTransient();
+
+ // TODO: Register window metadata
+ return services;
+ }
+
+ ///
+ /// Register a settings section (declarative).
+ ///
+ public static IServiceCollection AddAirAppSettings(
+ this IServiceCollection services,
+ string id,
+ string name,
+ Action? configure = null)
+ {
+ var builder = new AirAppSettingsSectionBuilder(id, name);
+ configure?.Invoke(builder);
+
+ // Register the settings section
+ services.AddSingleton(builder.Build());
+
+ return services;
+ }
+}
+
+///
+/// Builder for settings sections.
+///
+public sealed class AirAppSettingsSectionBuilder
+{
+ private readonly string _id;
+ private readonly string _name;
+ private readonly List _options = new();
+
+ internal AirAppSettingsSectionBuilder(string id, string name)
+ {
+ _id = id;
+ _name = name;
+ }
+
+ public AirAppSettingsSectionBuilder AddToggle(string key, string label, bool defaultValue = false)
+ {
+ _options.Add(new AirAppSettingOption
+ {
+ Key = key,
+ Label = label,
+ Type = "toggle",
+ DefaultValue = defaultValue
+ });
+ return this;
+ }
+
+ public AirAppSettingsSectionBuilder AddText(string key, string label, string? defaultValue = null)
+ {
+ _options.Add(new AirAppSettingOption
+ {
+ Key = key,
+ Label = label,
+ Type = "text",
+ DefaultValue = defaultValue
+ });
+ return this;
+ }
+
+ public AirAppSettingsSectionBuilder AddNumber(string key, string label, double defaultValue = 0, double? minimum = null, double? maximum = null)
+ {
+ _options.Add(new AirAppSettingOption
+ {
+ Key = key,
+ Label = label,
+ Type = "number",
+ DefaultValue = defaultValue,
+ Minimum = minimum,
+ Maximum = maximum
+ });
+ return this;
+ }
+
+ internal AirAppSettingsSection Build()
+ {
+ return new AirAppSettingsSection
+ {
+ Id = _id,
+ Name = _name,
+ Options = _options
+ };
+ }
+}
+
+///
+/// Settings section metadata.
+///
+public sealed class AirAppSettingsSection
+{
+ public required string Id { get; init; }
+ public required string Name { get; init; }
+ public required List Options { get; init; }
+}
+
+///
+/// Individual setting option.
+///
+public sealed class AirAppSettingOption
+{
+ public required string Key { get; init; }
+ public required string Label { get; init; }
+ public required string Type { get; init; }
+ public object? DefaultValue { get; init; }
+ public double? Minimum { get; init; }
+ public double? Maximum { get; init; }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppWidgetBase.cs b/LanMountainDesktop.AirAppSdk/AirAppWidgetBase.cs
new file mode 100644
index 0000000..da7a000
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppWidgetBase.cs
@@ -0,0 +1,80 @@
+using Avalonia.Controls;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Base class for AirApp desktop component widgets.
+/// Inherit from this to create custom desktop components.
+///
+public abstract class AirAppWidgetBase : UserControl, IAirAppWidget
+{
+ private IAirAppComponentContext? _context;
+
+ ///
+ /// Gets or sets the component context.
+ ///
+ public IAirAppComponentContext Context
+ {
+ get => _context ?? throw new InvalidOperationException("Context has not been set yet.");
+ set
+ {
+ _context = value;
+ OnContextSet();
+ }
+ }
+
+ ///
+ /// Called when the context is first set.
+ /// Override this to initialize based on context.
+ ///
+ protected virtual void OnContextSet()
+ {
+ }
+
+ ///
+ /// Called when the widget is attached to the desktop.
+ ///
+ public void OnAttached()
+ {
+ OnAttachedCore();
+ }
+
+ ///
+ /// Called when the widget is detached from the desktop.
+ ///
+ public void OnDetached()
+ {
+ OnDetachedCore();
+ }
+
+ ///
+ /// Called when the appearance has changed.
+ ///
+ /// New appearance snapshot
+ public void OnAppearanceChanged(AirAppAppearanceSnapshot snapshot)
+ {
+ OnAppearanceChangedCore(snapshot);
+ }
+
+ ///
+ /// Override this to handle widget attachment.
+ ///
+ protected virtual void OnAttachedCore()
+ {
+ }
+
+ ///
+ /// Override this to handle widget detachment.
+ ///
+ protected virtual void OnDetachedCore()
+ {
+ }
+
+ ///
+ /// Override this to handle appearance changes.
+ ///
+ /// New appearance snapshot
+ protected virtual void OnAppearanceChangedCore(AirAppAppearanceSnapshot snapshot)
+ {
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs b/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs
new file mode 100644
index 0000000..a5ec528
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs
@@ -0,0 +1,96 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Base class for AirApp windows.
+///
+public abstract class AirAppWindowBase : Window, IAirAppWindow
+{
+ ///
+ /// Gets the window descriptor.
+ /// Override this to customize window configuration.
+ ///
+ public virtual AirAppWindowDescriptor Descriptor => new()
+ {
+ Width = 800,
+ Height = 600,
+ MinWidth = 400,
+ MinHeight = 300,
+ ChromeMode = AirAppWindowChromeMode.Standard,
+ CanResize = true,
+ ShowInTaskbar = true,
+ ShowAsDialog = false
+ };
+
+ ///
+ /// Initializes a new instance of AirAppWindowBase.
+ ///
+ protected AirAppWindowBase()
+ {
+ ApplyDescriptor(Descriptor);
+ }
+
+ ///
+ /// Called before the window is opened.
+ ///
+ public virtual Task OnWindowOpeningAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Called after the window has been opened.
+ ///
+ public virtual void OnWindowOpened()
+ {
+ }
+
+ ///
+ /// Called when the window is closing.
+ ///
+ public virtual void OnWindowClosing(WindowClosingEventArgs e)
+ {
+ }
+
+ ///
+ /// Called after the window has been closed.
+ ///
+ public virtual void OnWindowClosed()
+ {
+ }
+
+ ///
+ /// Apply the window descriptor configuration.
+ ///
+ private void ApplyDescriptor(AirAppWindowDescriptor descriptor)
+ {
+ Width = descriptor.Width;
+ Height = descriptor.Height;
+ MinWidth = descriptor.MinWidth;
+ MinHeight = descriptor.MinHeight;
+ CanResize = descriptor.CanResize;
+ ShowInTaskbar = descriptor.ShowInTaskbar;
+ ShowAsDialog = descriptor.ShowAsDialog;
+
+ // Apply chrome mode
+ switch (descriptor.ChromeMode)
+ {
+ case AirAppWindowChromeMode.Standard:
+ SystemDecorations = SystemDecorations.Full;
+ break;
+ case AirAppWindowChromeMode.Borderless:
+ SystemDecorations = SystemDecorations.BorderOnly;
+ break;
+ case AirAppWindowChromeMode.FullScreen:
+ SystemDecorations = SystemDecorations.None;
+ WindowState = WindowState.FullScreen;
+ break;
+ case AirAppWindowChromeMode.Tool:
+ SystemDecorations = SystemDecorations.Full;
+ ShowInTaskbar = false;
+ break;
+ }
+ }
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppWindowChromeMode.cs b/LanMountainDesktop.AirAppSdk/AirAppWindowChromeMode.cs
new file mode 100644
index 0000000..505dceb
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppWindowChromeMode.cs
@@ -0,0 +1,32 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Window chrome mode for AirApp windows.
+///
+public enum AirAppWindowChromeMode
+{
+ ///
+ /// Standard window with title bar and borders.
+ ///
+ Standard = 0,
+
+ ///
+ /// Borderless window with custom chrome.
+ ///
+ Borderless = 1,
+
+ ///
+ /// Full-screen window with no decorations.
+ ///
+ FullScreen = 2,
+
+ ///
+ /// Tool window (no taskbar icon, small title bar).
+ ///
+ Tool = 3,
+
+ ///
+ /// Background-only (no UI, reserved for future use).
+ ///
+ BackgroundOnly = 4
+}
diff --git a/LanMountainDesktop.AirAppSdk/AirAppWindowDescriptor.cs b/LanMountainDesktop.AirAppSdk/AirAppWindowDescriptor.cs
new file mode 100644
index 0000000..433970c
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/AirAppWindowDescriptor.cs
@@ -0,0 +1,52 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Window configuration descriptor.
+///
+public sealed class AirAppWindowDescriptor
+{
+ ///
+ /// Gets or sets the window title.
+ ///
+ public string Title { get; set; } = "AirApp Window";
+
+ ///
+ /// Gets or sets the initial width.
+ ///
+ public double Width { get; set; } = 800;
+
+ ///
+ /// Gets or sets the initial height.
+ ///
+ public double Height { get; set; } = 600;
+
+ ///
+ /// Gets or sets the minimum width.
+ ///
+ public double MinWidth { get; set; } = 400;
+
+ ///
+ /// Gets or sets the minimum height.
+ ///
+ public double MinHeight { get; set; } = 300;
+
+ ///
+ /// Gets or sets the chrome mode.
+ ///
+ public AirAppWindowChromeMode ChromeMode { get; set; } = AirAppWindowChromeMode.Standard;
+
+ ///
+ /// Gets or sets whether the window can be resized.
+ ///
+ public bool CanResize { get; set; } = true;
+
+ ///
+ /// Gets or sets whether the window shows in the taskbar.
+ ///
+ public bool ShowInTaskbar { get; set; } = true;
+
+ ///
+ /// Gets or sets whether the window is modal.
+ ///
+ public bool ShowAsDialog { get; set; } = false;
+}
diff --git a/LanMountainDesktop.AirAppSdk/IAirApp.cs b/LanMountainDesktop.AirAppSdk/IAirApp.cs
new file mode 100644
index 0000000..6d05f33
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/IAirApp.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Core interface for AirApp entry point.
+///
+public interface IAirApp
+{
+ ///
+ /// Initialize the AirApp and register services.
+ /// Called during host startup before the application is fully running.
+ ///
+ /// Host builder context
+ /// Service collection for dependency injection
+ void Initialize(HostBuilderContext context, IServiceCollection services);
+
+ ///
+ /// Called after the host application has started.
+ /// Use this for initialization that requires runtime services.
+ ///
+ /// AirApp runtime context
+ Task OnStartedAsync(IAirAppRuntimeContext context);
+
+ ///
+ /// Called when the host application is stopping.
+ /// Use this for cleanup and resource disposal.
+ ///
+ Task OnStoppingAsync();
+}
diff --git a/LanMountainDesktop.AirAppSdk/IAirAppAppearanceContext.cs b/LanMountainDesktop.AirAppSdk/IAirAppAppearanceContext.cs
new file mode 100644
index 0000000..e156a34
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/IAirAppAppearanceContext.cs
@@ -0,0 +1,19 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Provides appearance and theme context.
+///
+public interface IAirAppAppearanceContext
+{
+ ///
+ /// Gets the current appearance snapshot.
+ ///
+ AirAppAppearanceSnapshot CurrentSnapshot { get; }
+
+ ///
+ /// Subscribe to appearance changes.
+ ///
+ /// Change handler
+ /// Subscription token
+ IDisposable SubscribeToChanges(Action handler);
+}
diff --git a/LanMountainDesktop.AirAppSdk/IAirAppComponentContext.cs b/LanMountainDesktop.AirAppSdk/IAirAppComponentContext.cs
new file mode 100644
index 0000000..1a1492f
--- /dev/null
+++ b/LanMountainDesktop.AirAppSdk/IAirAppComponentContext.cs
@@ -0,0 +1,58 @@
+namespace LanMountainDesktop.AirAppSdk;
+
+///
+/// Context provided to an AirApp desktop component instance.
+///
+public interface IAirAppComponentContext
+{
+ ///
+ /// Gets the component identifier.
+ ///
+ string ComponentId { get; }
+
+ ///
+ /// Gets the unique placement identifier for this component instance.
+ ///
+ string PlacementId { get; }
+
+ ///
+ /// Gets the current width in grid cells.
+ ///
+ int Width { get; }
+
+ ///
+ /// Gets the current height in grid cells.
+ ///
+ int Height { get; }
+
+ ///
+ /// Gets the service provider for this component.
+ ///
+ IServiceProvider Services { get; }
+
+ ///
+ /// Gets the appearance context.
+ ///
+ IAirAppAppearanceContext Appearance { get; }
+
+ ///
+ /// Request a window to be opened.
+ ///
+ /// Window identifier
+ Task OpenWindowAsync(string windowId);
+
+ ///
+ /// Send a message to other components or AirApps.
+ ///
+ /// Message topic
+ /// Message payload
+ void SendMessage(string topic, object? payload = null);
+
+ ///
+ /// Subscribe to messages.
+ ///
+ /// Message topic
+ /// Message handler
+ /// Subscription token for unsubscribing
+ IDisposable Subscribe(string topic, Action