feat(macos): add native desktop controls and hotkey integration

- Introduced MacOsDesktopControlManifest to manage desktop control configurations.
- Implemented MacOsDesktopControlService to handle the lifecycle of the macOS helper process.
- Created MacOsWorkflowRulesEditorWindowService for managing workflow rules through a native window.
- Developed a Swift helper for macOS that integrates with AppKit and Carbon for menu-bar controls and global hotkeys.
- Added support for recording lifecycle actions via HTTP endpoints, maintaining consistency with existing Windows functionality.
- Enhanced user experience by allowing macOS users to start/stop recordings and access settings through a menu-bar interface.
- Documented design decisions, implementation evidence, and verification tasks for the new macOS features.
This commit is contained in:
dh
2026-08-06 08:18:52 +02:00
parent 5f39923c27
commit 9542c05ea4
20 changed files with 981 additions and 11 deletions
+23 -1
View File
@@ -3,6 +3,7 @@ using MeetingAssistant.Calendar;
using MeetingAssistant.Hotkeys;
using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.Logging;
using MeetingAssistant.MacOs;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Recording;
using MeetingAssistant.Screenshots;
@@ -38,6 +39,7 @@ builder.Services.AddSingleton<IMeetingMetadataProvider, OutlookClassicMeetingMet
#else
PortableMeetingAudioRegistration.Add(builder.Services);
builder.Services.AddSingleton<IMeetingMetadataProvider, NoopMeetingMetadataProvider>();
builder.Services.AddHostedService<MacOsDesktopControlService>();
#endif
builder.Services.AddSingleton<ITranscriptStore, VaultTranscriptStore>();
builder.Services.AddSingleton<IMeetingNoteStore, MarkdownMeetingNoteStore>();
@@ -111,7 +113,8 @@ builder.Services.AddSingleton(services => WorkflowRulesEditorMarkdownLinkResolve
services.GetRequiredService<IOptions<MeetingAssistantOptions>>().Value));
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, WpfWorkflowRulesEditorWindowService>();
#else
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, NoopWorkflowRulesEditorWindowService>();
builder.Services.AddSingleton<MacOsWorkflowRulesEditorSession>();
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, MacOsWorkflowRulesEditorWindowService>();
#endif
builder.Services.AddSingleton<AsrDiagnosticService>();
builder.Services.AddSingleton<ICommandRunner, ProcessCommandRunner>();
@@ -152,6 +155,11 @@ app.MapGet("/recording/status", (MeetingRecordingCoordinator coordinator) => Res
app.MapGet("/profiles/{launchProfile}/recording/status", (
string launchProfile,
MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus));
app.MapPost("/application/exit", (IHostApplicationLifetime lifetime) =>
{
lifetime.StopApplication();
return Results.Accepted();
});
app.MapGet("/diagnostics/outlook/current-meeting", async (
IMeetingMetadataProvider metadataProvider,
IOptions<MeetingAssistantOptions> options,
@@ -281,6 +289,15 @@ app.MapPost("/diagnostics/settings-and-logs/show", (
rulesEditorWindow.Show();
return Results.Accepted();
});
#if !WINDOWS
app.MapGet("/diagnostics/settings-and-logs", () =>
Results.Content(MacOsWorkflowRulesEditorPage.Html, "text/html"));
app.MapPost("/diagnostics/settings-and-logs/chat", async (
MacOsWorkflowRulesEditorChatRequest request,
MacOsWorkflowRulesEditorSession session,
CancellationToken cancellationToken) =>
Results.Ok(await session.SendAsync(request.Message, cancellationToken)));
#endif
static WorkflowConfigurationReloadResponse ReloadWorkflowConfiguration(IConfiguration configuration)
{
@@ -323,6 +340,11 @@ app.MapPost("/profiles/{launchProfile}/recording/abort", async (
MeetingRecordingCoordinator coordinator,
CancellationToken cancellationToken) =>
Results.Ok(await coordinator.AbortAsync(cancellationToken)));
app.MapPost("/profiles/{launchProfile}/meetings/screenshot/capture", async (
string launchProfile,
MeetingRecordingCoordinator coordinator,
CancellationToken cancellationToken) =>
Results.Ok(await coordinator.CaptureScreenshotAsync(launchProfile, cancellationToken)));
app.MapPost("/meetings/current/summary/run", async (
MeetingRecordingCoordinator coordinator,
IMeetingSummaryPipeline summaryPipeline,