From 37c09edeff984a8dc4d54667523664cb5cecd5e0 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Tue, 5 May 2026 19:14:17 -0400 Subject: [PATCH 1/9] feat: add recording state tracking to NodeService Add IsScreenRecording/IsCameraRecording properties and RecordingStateChanged event to NodeService. Wrap OnScreenRecord and OnCameraClip handlers to set state and raise events before/after async recording calls. This enables downstream UI components (tray icon, toasts, activity log) to react to recording lifecycle changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/NodeService.cs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs index ac78cade..269b20ac 100644 --- a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs @@ -137,7 +137,12 @@ public sealed class NodeService : IDisposable public event EventHandler? ChannelHealthUpdated; public event EventHandler? InvokeCompleted; public event EventHandler? GatewaySelfUpdated; + public event EventHandler? RecordingStateChanged; + public bool IsScreenRecording { get; private set; } + public bool IsCameraRecording { get; private set; } + public bool IsAnyRecording => IsScreenRecording || IsCameraRecording; + public bool IsConnected => _nodeClient?.IsConnected ?? false; public string? NodeId => _nodeClient?.NodeId; public bool IsPendingApproval => _nodeClient?.IsPendingApproval ?? false; @@ -1223,14 +1228,23 @@ private async Task OnScreenCapture(ScreenCaptureArgs args) return await _screenCaptureService.CaptureAsync(args); } - private Task OnScreenRecord(ScreenRecordArgs args) + private async Task OnScreenRecord(ScreenRecordArgs args) { if (_screenRecordingService == null) { throw new InvalidOperationException("Screen recording service not available"); } - return _screenRecordingService.RecordAsync(args); + SetRecordingState(RecordingType.Screen, true, args.DurationMs); + try + { + var result = await _screenRecordingService.RecordAsync(args); + return result; + } + finally + { + SetRecordingState(RecordingType.Screen, false); + } } #endregion @@ -1282,6 +1296,7 @@ private async Task OnCameraClip(CameraClipArgs args) throw new InvalidOperationException("Camera capture service not available"); } + SetRecordingState(RecordingType.Camera, true, args.DurationMs); try { return await _cameraCaptureService.ClipAsync(args); @@ -1301,6 +1316,10 @@ private async Task OnCameraClip(CameraClipArgs args) "Camera access blocked. Enable camera access for desktop apps in Windows Privacy settings.", ex); } + finally + { + SetRecordingState(RecordingType.Camera, false); + } } private async Task GetLocationAsync(LocationGetArgs args) @@ -1436,6 +1455,26 @@ private Task OnSttStatusAsync(CancellationToken cancellationTok }); } + #endregion + + #region Recording State + + private void SetRecordingState(RecordingType type, bool isActive, int durationMs = 0) + { + switch (type) + { + case RecordingType.Screen: IsScreenRecording = isActive; break; + case RecordingType.Camera: IsCameraRecording = isActive; break; + } + + RecordingStateChanged?.Invoke(this, new RecordingStateEventArgs + { + Type = type, + IsActive = isActive, + DurationMs = durationMs + }); + } + #endregion public void Dispose() @@ -1478,3 +1517,16 @@ public void Dispose() } } } + +public enum RecordingType +{ + Screen, + Camera +} + +public sealed class RecordingStateEventArgs : EventArgs +{ + public RecordingType Type { get; init; } + public bool IsActive { get; init; } + public int DurationMs { get; init; } +} From 45826021aa7bd124fdb00be9736011f99f94d3a0 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Tue, 5 May 2026 21:17:21 -0400 Subject: [PATCH 2/9] feat: add toast notifications for screen and camera recording Show toasts on recording start, completion, and failure for both screen recording and camera clips. Extract reusable ShowToast helper and add localized strings for all 5 locales (en-us, fr-fr, zh-cn, zh-tw, nl-nl). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/NodeService.cs | 33 ++++++++++++----- .../Strings/en-us/Resources.resw | 37 ++++++++++++++++++- .../Strings/fr-fr/Resources.resw | 37 ++++++++++++++++++- .../Strings/nl-nl/Resources.resw | 37 ++++++++++++++++++- .../Strings/zh-cn/Resources.resw | 37 ++++++++++++++++++- .../Strings/zh-tw/Resources.resw | 37 ++++++++++++++++++- 6 files changed, 204 insertions(+), 14 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs index 269b20ac..bb855f8c 100644 --- a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs @@ -1215,14 +1215,7 @@ private async Task OnScreenCapture(ScreenCaptureArgs args) if ((now - _lastScreenCaptureNotification).TotalSeconds > 10) { _lastScreenCaptureNotification = now; - try - { - new ToastContentBuilder() - .AddText(LocalizationHelper.GetString("Toast_ScreenCaptured")) - .AddText(LocalizationHelper.GetString("Toast_ScreenCapturedDetail")) - .Show(); - } - catch { /* ignore notification errors */ } + ShowToast("Toast_ScreenCaptured", "Toast_ScreenCapturedDetail"); } return await _screenCaptureService.CaptureAsync(args); @@ -1238,9 +1231,16 @@ private async Task OnScreenRecord(ScreenRecordArgs args) SetRecordingState(RecordingType.Screen, true, args.DurationMs); try { + ShowToast("Toast_ScreenRecordingStarted", "Toast_ScreenRecordingStartedDetail"); var result = await _screenRecordingService.RecordAsync(args); + ShowToast("Toast_ScreenRecordingComplete", "Toast_ScreenRecordingCompleteDetail"); return result; } + catch (Exception ex) when (ex is not InvalidOperationException) + { + ShowToast("Toast_ScreenRecordingFailed", "Toast_ScreenRecordingFailedDetail"); + throw; + } finally { SetRecordingState(RecordingType.Screen, false); @@ -1299,7 +1299,10 @@ private async Task OnCameraClip(CameraClipArgs args) SetRecordingState(RecordingType.Camera, true, args.DurationMs); try { - return await _cameraCaptureService.ClipAsync(args); + ShowToast("Toast_CameraRecordingStarted", "Toast_CameraRecordingStartedDetail"); + var result = await _cameraCaptureService.ClipAsync(args); + ShowToast("Toast_CameraRecordingComplete", "Toast_CameraRecordingCompleteDetail"); + return result; } catch (UnauthorizedAccessException ex) { @@ -1475,6 +1478,18 @@ private void SetRecordingState(RecordingType type, bool isActive, int durationMs }); } + private static void ShowToast(string titleKey, string detailKey) + { + try + { + new ToastContentBuilder() + .AddText(LocalizationHelper.GetString(titleKey)) + .AddText(LocalizationHelper.GetString(detailKey)) + .Show(); + } + catch { /* ignore notification errors */ } + } + #endregion public void Dispose() diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw index 5001c01e..64ff0e0f 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw @@ -688,6 +688,41 @@ Use one of these options: Enable camera access in Windows Privacy settings for OpenClaw Tray + + + + + 🔴 Screen Recording Started + + + OpenClaw agent is recording your screen + + + ✅ Screen Recording Complete + + + Screen recording has been sent to the agent + + + ❌ Screen Recording Failed + + + An error occurred while recording the screen + + + 🔴 Camera Recording Started + + + OpenClaw agent is recording from your camera + + + ✅ Camera Recording Complete + + + Camera clip has been sent to the agent + + + ⚡ New: Activity Stream @@ -2572,4 +2607,4 @@ On your gateway host (Mac/Linux), run: ▶ Preview Voice - \ No newline at end of file + diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw index 71520ac1..d6d19e16 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw @@ -688,6 +688,41 @@ Utilisez l'une de ces options : Activez l'accès à la caméra dans les paramètres de confidentialité de Windows pour OpenClaw Tray + + + + + 🔴 Enregistrement d'écran démarré + + + L'agent OpenClaw enregistre votre écran + + + ✅ Enregistrement d'écran terminé + + + L'enregistrement d'écran a été envoyé à l'agent + + + ❌ Échec de l'enregistrement d'écran + + + Une erreur est survenue lors de l'enregistrement de l'écran + + + 🔴 Enregistrement caméra démarré + + + L'agent OpenClaw enregistre depuis votre caméra + + + ✅ Enregistrement caméra terminé + + + Le clip caméra a été envoyé à l'agent + + + ⚡ Nouveau: Fil d'activité @@ -2572,4 +2607,4 @@ Sur votre hôte passerelle (Mac/Linux), exécutez : ▶ Aperçu de la voix - \ No newline at end of file + diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw index 5a0a5525..25fc26e1 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw @@ -688,6 +688,41 @@ Gebruik een van deze opties: Schakel cameratoegang in via Windows-privacyinstellingen voor OpenClaw Tray + + + + + 🔴 Schermopname gestart + + + OpenClaw-agent neemt uw scherm op + + + ✅ Schermopname voltooid + + + Schermopname is naar de agent verzonden + + + ❌ Schermopname mislukt + + + Er is een fout opgetreden bij het opnemen van het scherm + + + 🔴 Camera-opname gestart + + + OpenClaw-agent neemt op vanaf uw camera + + + ✅ Camera-opname voltooid + + + Cameraclip is naar de agent verzonden + + + ⚡ Nieuw: Activiteitenstroom @@ -2572,4 +2607,4 @@ Voer op uw gateway-host (Mac/Linux) uit: ▶ Voorbeeld van stem - \ No newline at end of file + diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw index 063c1304..2d3612c8 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw @@ -688,6 +688,41 @@ 请在 Windows 隐私设置中为 OpenClaw Tray 启用相机访问 + + + + + 🔴 屏幕录制已开始 + + + OpenClaw 代理正在录制您的屏幕 + + + ✅ 屏幕录制已完成 + + + 屏幕录制已发送给代理 + + + ❌ 屏幕录制失败 + + + 录制屏幕时发生错误 + + + 🔴 摄像头录制已开始 + + + OpenClaw 代理正在从您的摄像头录制 + + + ✅ 摄像头录制已完成 + + + 摄像头录制片段已发送给代理 + + + ⚡ 新功能: 活动流 @@ -2572,4 +2607,4 @@ ▶ 预览语音 - \ No newline at end of file + diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw index f51d7b0f..722a71e4 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw @@ -688,6 +688,41 @@ 請在 Windows 隱私設定中為 OpenClaw Tray 啟用相機訪問 + + + + + 🔴 螢幕錄製已開始 + + + OpenClaw 代理正在錄製您的螢幕 + + + ✅ 螢幕錄製已完成 + + + 螢幕錄製已傳送給代理 + + + ❌ 螢幕錄製失敗 + + + 錄製螢幕時發生錯誤 + + + 🔴 攝影機錄製已開始 + + + OpenClaw 代理正在從您的攝影機錄製 + + + ✅ 攝影機錄製已完成 + + + 攝影機錄製片段已傳送給代理 + + + ⚡ 新功能: 串流活動 @@ -2572,4 +2607,4 @@ ▶ 預覽語音 - \ No newline at end of file + From 7a1fdf7655bba7562ee0cd2ea76f2c5e0c447360 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Tue, 5 May 2026 22:14:27 -0400 Subject: [PATCH 3/9] feat: log recording events to activity stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add recording start/complete events with emoji indicators (🔴/✅) to the activity stream. Render emoji in a separate TextBlock element to prevent color emoji clipping by the card's CornerRadius clip mask. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Tray.WinUI/App.xaml.cs | 21 +++++++++++++++++++ .../Pages/ActivityPage.xaml | 7 ++++++- .../Pages/ActivityPage.xaml.cs | 4 ++++ .../Services/ActivityStreamService.cs | 3 +++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index fc4f49af..4869e378 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -760,6 +760,7 @@ private static string TruncateMenuText(string text, int maxLength = 96) => private void AddRecentActivity( string line, string category = "general", + string? icon = null, string? dashboardPath = null, string? details = null, string? sessionKey = null, @@ -768,6 +769,7 @@ private void AddRecentActivity( ActivityStreamService.Add( category: category, title: line, + icon: icon, details: details, dashboardPath: dashboardPath, sessionKey: sessionKey, @@ -1670,6 +1672,7 @@ private void InitializeNodeService() _nodeService.ChannelHealthUpdated += OnChannelHealthUpdated; _nodeService.InvokeCompleted += OnNodeInvokeCompleted; _nodeService.GatewaySelfUpdated += OnGatewaySelfUpdated; + _nodeService.RecordingStateChanged += OnRecordingStateChanged; if (canRunGateway) { @@ -1866,6 +1869,24 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) } } + private void OnRecordingStateChanged(object? sender, RecordingStateEventArgs args) + { + var source = args.Type == RecordingType.Screen ? "Screen" : "Camera"; + if (args.IsActive) + { + var duration = args.DurationMs > 0 ? $" ({args.DurationMs / 1000.0:0.#}s)" : ""; + AddRecentActivity($"{source} recording started{duration}", category: "node", + icon: "🔴", + details: $"{source} recording requested by agent"); + } + else + { + AddRecentActivity($"{source} recording complete", category: "node", + icon: "✅", + details: $"{source} recording sent to agent"); + } + } + private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatusEventArgs args) { Logger.Info($"Pairing status: {args.Status}"); diff --git a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml index 0254552b..fe4de731 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml @@ -58,7 +58,12 @@ - + + + + diff --git a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs index 4cb8fe29..d5855bb3 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs @@ -96,6 +96,8 @@ private static ActivityViewModel MapToViewModel(ActivityStreamItem item) return new ActivityViewModel { Title = item.Title, + Icon = item.Icon, + IconVisibility = string.IsNullOrWhiteSpace(item.Icon) ? Visibility.Collapsed : Visibility.Visible, Category = item.Category, TimeAgo = GetTimeAgo(item.Timestamp), DetailText = detailText, @@ -135,6 +137,8 @@ private void OnClearAll(object sender, RoutedEventArgs e) private class ActivityViewModel { public string Title { get; set; } = ""; + public string Icon { get; set; } = ""; + public Visibility IconVisibility { get; set; } public string Category { get; set; } = ""; public string TimeAgo { get; set; } = ""; public string DetailText { get; set; } = ""; diff --git a/src/OpenClaw.Tray.WinUI/Services/ActivityStreamService.cs b/src/OpenClaw.Tray.WinUI/Services/ActivityStreamService.cs index 409f6d43..d2027e4b 100644 --- a/src/OpenClaw.Tray.WinUI/Services/ActivityStreamService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/ActivityStreamService.cs @@ -19,6 +19,7 @@ public static void Add( string category, string title, string? details = null, + string? icon = null, string? dashboardPath = null, string? sessionKey = null, string? nodeId = null) @@ -32,6 +33,7 @@ public static void Add( Timestamp = DateTime.Now, Category = string.IsNullOrWhiteSpace(category) ? "general" : category, Title = title, + Icon = icon ?? "", Details = details ?? "", DashboardPath = dashboardPath, SessionKey = sessionKey, @@ -125,6 +127,7 @@ public class ActivityStreamItem public DateTime Timestamp { get; set; } = DateTime.Now; public string Category { get; set; } = "general"; public string Title { get; set; } = ""; + public string Icon { get; set; } = ""; public string Details { get; set; } = ""; public string? DashboardPath { get; set; } public string? SessionKey { get; set; } From 97056163b524dab637bac5ff581c4b4da8c61595 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Wed, 6 May 2026 11:25:32 -0400 Subject: [PATCH 4/9] feat: add recording consent dialog before first recording Show a standalone WindowEx consent dialog the first time an agent requests screen or camera recording. Consent is tracked separately per recording type (ScreenRecordingConsentGiven, CameraRecordingConsentGiven) so users can allow screen recording without granting camera access. The dialog uses extend-into-titlebar styling, Mica backdrop, and SetForegroundWindow to ensure visibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/SettingsData.cs | 2 + .../Dialogs/RecordingConsentDialog.cs | 183 ++++++++++++++++++ .../Services/NodeService.cs | 50 ++++- .../Services/SettingsManager.cs | 6 + .../Strings/en-us/Resources.resw | 27 +++ .../Strings/fr-fr/Resources.resw | 27 +++ .../Strings/nl-nl/Resources.resw | 27 +++ .../Strings/zh-cn/Resources.resw | 27 +++ .../Strings/zh-tw/Resources.resw | 27 +++ 9 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs diff --git a/src/OpenClaw.Shared/SettingsData.cs b/src/OpenClaw.Shared/SettingsData.cs index a8096c0c..f0685f5b 100644 --- a/src/OpenClaw.Shared/SettingsData.cs +++ b/src/OpenClaw.Shared/SettingsData.cs @@ -32,6 +32,8 @@ public class SettingsData public bool NodeCanvasEnabled { get; set; } = true; public bool NodeScreenEnabled { get; set; } = true; public bool NodeCameraEnabled { get; set; } = true; + public bool ScreenRecordingConsentGiven { get; set; } = false; + public bool CameraRecordingConsentGiven { get; set; } = false; public bool NodeLocationEnabled { get; set; } = true; public bool NodeBrowserProxyEnabled { get; set; } = true; public bool NodeSttEnabled { get; set; } = false; diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs new file mode 100644 index 00000000..e4a82c7b --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs @@ -0,0 +1,183 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using OpenClaw.Shared.Capabilities; +using OpenClawTray.Helpers; +using OpenClawTray.Services; +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using WinUIEx; + +namespace OpenClawTray.Dialogs; + +/// +/// Privacy consent dialog shown before the first screen or camera recording. +/// Parameterized by recording type so each capability gets its own consent. +/// +public sealed class RecordingConsentDialog : WindowEx +{ + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + private readonly TaskCompletionSource _tcs = new(); + private bool _consented; + + public RecordingConsentDialog(RecordingType type) + { + var isScreen = type == RecordingType.Screen; + var headingKey = isScreen ? "RecordingConsent_ScreenTitle" : "RecordingConsent_CameraTitle"; + var descriptionKey = isScreen ? "RecordingConsent_ScreenDescription" : "RecordingConsent_CameraDescription"; + var emoji = isScreen ? "🖥️" : "📷"; + + Title = LocalizationHelper.GetString("RecordingConsent_WindowTitle"); + this.SetWindowSize(460, 340); + this.CenterOnScreen(); + this.SetIcon("Assets\\openclaw.ico"); + + SystemBackdrop = new MicaBackdrop(); + ExtendsContentIntoTitleBar = true; + + // Custom title bar + var titleBar = new Grid + { + Height = 48, + Padding = new Thickness(16, 0, 140, 0) + }; + titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var titleIcon = new TextBlock + { + Text = "🦞", + FontSize = 16, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0) + }; + Grid.SetColumn(titleIcon, 0); + titleBar.Children.Add(titleIcon); + + var titleText = new TextBlock + { + Text = LocalizationHelper.GetString("RecordingConsent_WindowTitle"), + FontSize = 13, + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"] + }; + Grid.SetColumn(titleText, 1); + titleBar.Children.Add(titleText); + + SetTitleBar(titleBar); + + // Main layout + var outerGrid = new Grid(); + outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(48) }); + outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + + Grid.SetRow(titleBar, 0); + outerGrid.Children.Add(titleBar); + + var root = new Grid + { + Padding = new Thickness(32, 16, 32, 32), + RowSpacing = 16 + }; + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + // Header + var header = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12 + }; + header.Children.Add(new TextBlock { Text = emoji, FontSize = 36 }); + header.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString(headingKey), + Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"], + VerticalAlignment = VerticalAlignment.Center + }); + Grid.SetRow(header, 0); + root.Children.Add(header); + + // Content + var content = new StackPanel { Spacing = 12 }; + content.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString(descriptionKey), + TextWrapping = TextWrapping.Wrap + }); + content.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString("RecordingConsent_Privacy"), + TextWrapping = TextWrapping.Wrap, + Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"] + }); + Grid.SetRow(content, 1); + root.Children.Add(content); + + // Buttons + var buttonPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 8 + }; + + var denyButton = new Button + { + Content = LocalizationHelper.GetString("RecordingConsent_Deny") + }; + denyButton.Click += (s, e) => + { + Logger.Info($"[RecordingConsent] User denied {type} recording consent"); + _consented = false; + Close(); + }; + buttonPanel.Children.Add(denyButton); + + var allowButton = new Button + { + Content = LocalizationHelper.GetString("RecordingConsent_Allow"), + Background = new SolidColorBrush(new global::Windows.UI.Color { A = 255, R = 0, G = 102, B = 204 }), + Foreground = new SolidColorBrush(Microsoft.UI.Colors.White) + }; + allowButton.Click += (s, e) => + { + Logger.Info($"[RecordingConsent] User allowed {type} recording consent"); + _consented = true; + Close(); + }; + buttonPanel.Children.Add(allowButton); + + Grid.SetRow(buttonPanel, 2); + root.Children.Add(buttonPanel); + + Grid.SetRow(root, 1); + outerGrid.Children.Add(root); + + Content = outerGrid; + + Closed += (s, e) => _tcs.TrySetResult(_consented); + + Logger.Info($"[RecordingConsent] {type} recording consent dialog shown"); + } + + public new Task ShowAsync() + { + Activate(); + + // Force to foreground since this may be triggered from a background context + try + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + if (hwnd != IntPtr.Zero) + SetForegroundWindow(hwnd); + } + catch { /* best-effort */ } + + return _tcs.Task; + } +} diff --git a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs index bb855f8c..30687885 100644 --- a/src/OpenClaw.Tray.WinUI/Services/NodeService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/NodeService.cs @@ -1228,6 +1228,8 @@ private async Task OnScreenRecord(ScreenRecordArgs args) throw new InvalidOperationException("Screen recording service not available"); } + await EnsureRecordingConsentAsync(RecordingType.Screen); + SetRecordingState(RecordingType.Screen, true, args.DurationMs); try { @@ -1295,7 +1297,9 @@ private async Task OnCameraClip(CameraClipArgs args) { throw new InvalidOperationException("Camera capture service not available"); } - + + await EnsureRecordingConsentAsync(RecordingType.Camera); + SetRecordingState(RecordingType.Camera, true, args.DurationMs); try { @@ -1478,6 +1482,50 @@ private void SetRecordingState(RecordingType type, bool isActive, int durationMs }); } + private async Task EnsureRecordingConsentAsync(RecordingType type) + { + var hasConsent = type == RecordingType.Screen + ? _settings?.ScreenRecordingConsentGiven == true + : _settings?.CameraRecordingConsentGiven == true; + if (hasConsent) return; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (!_dispatcherQueue.TryEnqueue(async () => + { + try + { + var dialog = new Dialogs.RecordingConsentDialog(type); + var consented = await dialog.ShowAsync(); + + if (consented && _settings != null) + { + if (type == RecordingType.Screen) + _settings.ScreenRecordingConsentGiven = true; + else + _settings.CameraRecordingConsentGiven = true; + _settings.Save(); + } + + tcs.TrySetResult(consented); + } + catch (Exception ex) + { + _logger.Error($"[RecordingConsent] Dialog error: {ex.Message}"); + tcs.TrySetResult(false); + } + })) + { + throw new InvalidOperationException("Recording denied: unable to show consent prompt"); + } + + var consented = await tcs.Task; + if (!consented) + { + throw new InvalidOperationException("Recording denied: user has not given consent"); + } + } + private static void ShowToast(string titleKey, string detailKey) { try diff --git a/src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs b/src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs index 733c27ea..eb744cd2 100644 --- a/src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs +++ b/src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs @@ -64,6 +64,8 @@ public class SettingsManager public bool NodeCanvasEnabled { get; set; } = true; public bool NodeScreenEnabled { get; set; } = true; public bool NodeCameraEnabled { get; set; } = true; + public bool ScreenRecordingConsentGiven { get; set; } = false; + public bool CameraRecordingConsentGiven { get; set; } = false; public bool NodeLocationEnabled { get; set; } = true; public bool NodeBrowserProxyEnabled { get; set; } = true; public bool NodeSttEnabled { get; set; } = false; @@ -157,6 +159,8 @@ public void Load() NodeCanvasEnabled = loaded.NodeCanvasEnabled; NodeScreenEnabled = loaded.NodeScreenEnabled; NodeCameraEnabled = loaded.NodeCameraEnabled; + ScreenRecordingConsentGiven = loaded.ScreenRecordingConsentGiven; + CameraRecordingConsentGiven = loaded.CameraRecordingConsentGiven; NodeLocationEnabled = loaded.NodeLocationEnabled; NodeBrowserProxyEnabled = loaded.NodeBrowserProxyEnabled; NodeSttEnabled = loaded.NodeSttEnabled; @@ -240,6 +244,8 @@ public void Save() NodeCanvasEnabled = NodeCanvasEnabled, NodeScreenEnabled = NodeScreenEnabled, NodeCameraEnabled = NodeCameraEnabled, + ScreenRecordingConsentGiven = ScreenRecordingConsentGiven, + CameraRecordingConsentGiven = CameraRecordingConsentGiven, NodeLocationEnabled = NodeLocationEnabled, NodeBrowserProxyEnabled = NodeBrowserProxyEnabled, NodeSttEnabled = NodeSttEnabled, diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw index 64ff0e0f..3603439c 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw @@ -722,6 +722,33 @@ Use one of these options: Camera clip has been sent to the agent + + + + OpenClaw · Permission Request + + + Allow screen recording? + + + Allow camera recording? + + + An agent is requesting to record your screen. This will capture video from your display and send it to the agent. + + + An agent is requesting to record from your camera. This will capture video from your webcam and send it to the agent. + + + You can change this later in Settings. + + + Allow recording + + + Deny + + ⚡ New: Activity Stream diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw index d6d19e16..91cddd71 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw @@ -722,6 +722,33 @@ Utilisez l'une de ces options : Le clip caméra a été envoyé à l'agent + + + + OpenClaw · Demande d'autorisation + + + Autoriser l'enregistrement d'écran ? + + + Autoriser l'enregistrement de la caméra ? + + + Un agent demande d'enregistrer votre écran. Cela capturera la vidéo de votre affichage et l'enverra à l'agent. + + + Un agent demande d'enregistrer depuis votre caméra. Cela capturera la vidéo de votre webcam et l'enverra à l'agent. + + + Vous pouvez modifier cela ultérieurement dans les Paramètres. + + + Autoriser l'enregistrement + + + Refuser + + ⚡ Nouveau: Fil d'activité diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw index 25fc26e1..b6e9c098 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw @@ -722,6 +722,33 @@ Gebruik een van deze opties: Cameraclip is naar de agent verzonden + + + + OpenClaw · Toestemmingsverzoek + + + Schermopname toestaan? + + + Camera-opname toestaan? + + + Een agent vraagt om uw scherm op te nemen. Dit zal video van uw beeldscherm vastleggen en naar de agent sturen. + + + Een agent vraagt om op te nemen vanaf uw camera. Dit zal video van uw webcam vastleggen en naar de agent sturen. + + + U kunt dit later wijzigen in Instellingen. + + + Opname toestaan + + + Weigeren + + ⚡ Nieuw: Activiteitenstroom diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw index 2d3612c8..69bb7d6c 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw @@ -722,6 +722,33 @@ 摄像头录制片段已发送给代理 + + + + OpenClaw · 权限请求 + + + 允许屏幕录制? + + + 允许摄像头录制? + + + 代理正在请求录制您的屏幕。这将从您的显示器捕获视频并发送给代理。 + + + 代理正在请求从您的摄像头录制。这将从您的网络摄像头捕获视频并发送给代理。 + + + 您可以稍后在设置中更改此项。 + + + 允许录制 + + + 拒绝 + + ⚡ 新功能: 活动流 diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw index 722a71e4..c136a394 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw @@ -722,6 +722,33 @@ 攝影機錄製片段已傳送給代理 + + + + OpenClaw · 權限請求 + + + 允許螢幕錄製? + + + 允許攝影機錄製? + + + 代理正在請求錄製您的螢幕。這將從您的顯示器擷取視訊並傳送給代理。 + + + 代理正在請求從您的攝影機錄製。這將從您的網路攝影機擷取視訊並傳送給代理。 + + + 您可以稍後在設定中變更此項。 + + + 允許錄製 + + + 拒絕 + + ⚡ 新功能: 串流活動 From abdd334a015d071c027aa3a18bbf22831ec6f358 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Wed, 6 May 2026 17:11:38 -0400 Subject: [PATCH 5/9] feat: add privacy settings UI and polish consent dialog - Add Privacy section to Settings with screen/camera recording toggles - Settings toggles auto-refresh when consent changes externally - Fix consent dialog z-order with HWND_TOPMOST technique - Fix button width (MinWidth instead of fixed Width) - Add SettingsManager.Saved event for cross-component reactivity - Allow button uses AccentButtonStyle for consistency - Remove misleading 'only asked once' from privacy text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Tray.WinUI/App.xaml.cs | 14 ++++-- .../Dialogs/RecordingConsentDialog.cs | 16 ++++++- .../Pages/SettingsPage.xaml | 22 ++++++++- .../Pages/SettingsPage.xaml.cs | 32 +++++++++++++ .../Services/SettingsManager.cs | 4 ++ .../Strings/en-us/Resources.resw | 48 +++++++++++++++++++ .../Strings/fr-fr/Resources.resw | 48 +++++++++++++++++++ .../Strings/nl-nl/Resources.resw | 48 +++++++++++++++++++ .../Strings/zh-cn/Resources.resw | 48 +++++++++++++++++++ .../Strings/zh-tw/Resources.resw | 48 +++++++++++++++++++ 10 files changed, 320 insertions(+), 8 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 4869e378..dfdf2748 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -1874,16 +1874,22 @@ private void OnRecordingStateChanged(object? sender, RecordingStateEventArgs arg var source = args.Type == RecordingType.Screen ? "Screen" : "Camera"; if (args.IsActive) { + var title = args.Type == RecordingType.Screen + ? LocalizationHelper.GetString("Activity_ScreenRecordingStarted") + : LocalizationHelper.GetString("Activity_CameraRecordingStarted"); var duration = args.DurationMs > 0 ? $" ({args.DurationMs / 1000.0:0.#}s)" : ""; - AddRecentActivity($"{source} recording started{duration}", category: "node", + AddRecentActivity($"{title}{duration}", category: "node", icon: "🔴", - details: $"{source} recording requested by agent"); + details: string.Format(LocalizationHelper.GetString("Activity_RecordingRequestedByAgent"), source)); } else { - AddRecentActivity($"{source} recording complete", category: "node", + var title = args.Type == RecordingType.Screen + ? LocalizationHelper.GetString("Activity_ScreenRecordingComplete") + : LocalizationHelper.GetString("Activity_CameraRecordingComplete"); + AddRecentActivity(title, category: "node", icon: "✅", - details: $"{source} recording sent to agent"); + details: string.Format(LocalizationHelper.GetString("Activity_RecordingSentToAgent"), source)); } } diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs index e4a82c7b..5eee5db4 100644 --- a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs +++ b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs @@ -20,6 +20,14 @@ public sealed class RecordingConsentDialog : WindowEx [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + private static readonly IntPtr HWND_TOPMOST = new(-1); + private static readonly IntPtr HWND_NOTOPMOST = new(-2); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + private readonly TaskCompletionSource _tcs = new(); private bool _consented; @@ -141,8 +149,7 @@ public RecordingConsentDialog(RecordingType type) var allowButton = new Button { Content = LocalizationHelper.GetString("RecordingConsent_Allow"), - Background = new SolidColorBrush(new global::Windows.UI.Color { A = 255, R = 0, G = 102, B = 204 }), - Foreground = new SolidColorBrush(Microsoft.UI.Colors.White) + Style = (Style)Application.Current.Resources["AccentButtonStyle"] }; allowButton.Click += (s, e) => { @@ -174,7 +181,12 @@ public RecordingConsentDialog(RecordingType type) { var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); if (hwnd != IntPtr.Zero) + { + // Briefly set topmost to guarantee visibility, then remove topmost flag + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); SetForegroundWindow(hwnd); + } } catch { /* best-effort */ } diff --git a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml index 733d3ad9..5efabf24 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml @@ -73,6 +73,24 @@ + + + + + + + + + + + + @@ -82,9 +100,9 @@ BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" Padding="24,16"> -