Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ public IntPtr GetHubWindowHandle()
private ToastService? _toastService;
private AppNotificationService? _appNotificationService;
internal AppNotificationService? AppNotifications => _appNotificationService;
private string? _lastAuthFailureNotificationMessage;
private string? _lastConnectionIssueNotificationKey;
private readonly Dictionary<string, string> _reportedChannelIssueSignatures = new(StringComparer.OrdinalIgnoreCase);
private string? _lastSandboxRiskNotificationKey;
Expand Down Expand Up @@ -2332,6 +2331,37 @@ private void ShowPairingRejectedAppNotification(string deviceId, string? detail)
id: BuildPairingRejectedNotificationId(deviceId));
}

/// <summary>
/// Publishes an immediate connection-error banner using the single
/// connection-issue notification identity. Used for transient, page-driven
/// failures (e.g. a manual gateway switch that throws) where the snapshot
/// may be briefly silent. Because it reuses the connection-issue id/dedupe
/// key it occupies the same banner slot — it cannot produce a second bar —
/// and the snapshot-driven path will replace or dismiss it on the next tick.
/// </summary>
internal void ShowTransientConnectionError(string message)
{
var body = string.IsNullOrWhiteSpace(message)
? LocalizationHelper.GetString("AppNotification_GatewayConnectionFailed_DefaultMessage")
: message;

// Keep the snapshot-driven publisher from immediately re-emitting a
// duplicate for the same underlying error.
_lastConnectionIssueNotificationKey = $"operator-error:{message}";

AppNotificationPublisher.Show(
_appNotificationService,
LocalizationHelper.GetString("AppNotification_GatewayConnectionFailed_Title"),
body,
"connection",
"lifecycle",
AppNotificationSeverity.Error,
ConnectionIssueNotificationDedupeKey,
"connection",
LocalizationHelper.GetString("AppNotification_ActionOpenConnection"),
id: ConnectionIssueNotificationId);
}

private void UpdateConnectionIssueNotification(GatewayConnectionSnapshot snapshot)
{
if (!TryBuildConnectionIssueNotification(snapshot, out var title, out var message, out var severity, out var category, out var key))
Expand Down Expand Up @@ -2375,9 +2405,12 @@ private static bool TryBuildConnectionIssueNotification(
if (snapshot.OverallState == OverallConnectionState.Error)
{
title = LocalizationHelper.GetString("AppNotification_GatewayConnectionFailed_Title");
message = snapshot.OperatorError ?? LocalizationHelper.GetString("AppNotification_GatewayConnectionFailed_DefaultMessage");
var rawError = snapshot.OperatorError;
message = string.IsNullOrWhiteSpace(rawError)
? LocalizationHelper.GetString("AppNotification_GatewayConnectionFailed_DefaultMessage")
: rawError;
severity = AppNotificationSeverity.Error;
key = $"operator-error:{message}";
key = $"operator-error:{rawError ?? "default"}";
return true;
}

Expand Down Expand Up @@ -2663,8 +2696,6 @@ private void OnGatewayConnectionStatusChanged(object? sender, ConnectionStatus s
if (status == ConnectionStatus.Connected && _appState != null)
{
_appState.AuthFailureMessage = null;
_lastAuthFailureNotificationMessage = null;
_appNotificationService?.Dismiss("connection:authentication-failed");
}

UpdateTrayIcon();
Expand Down Expand Up @@ -2714,27 +2745,18 @@ private void OnGatewayAuthenticationFailed(object? sender, string message)
{
UpdateTrayIcon();

// Store auth failure in AppState — HubWindow observes it via PropertyChanged
// Store auth failure in AppState — observed for tray tooltip / status.
if (_appState != null)
{
_appState.AuthFailureMessage = message;
}

if (!string.Equals(_lastAuthFailureNotificationMessage, message, StringComparison.Ordinal))
{
_lastAuthFailureNotificationMessage = message;
AppNotificationPublisher.Show(
_appNotificationService,
LocalizationHelper.GetString("AppNotification_GatewayAuthenticationFailed_Title"),
message,
"connection",
"authentication",
AppNotificationSeverity.Error,
"connection:authentication-failed",
"connection",
LocalizationHelper.GetString("AppNotification_ActionOpenConnection"),
id: "connection:authentication-failed");
}
// The user-facing banner is published by the single connection-issue
// notification (UpdateConnectionIssueNotification), driven off the
// snapshot's Error state + OperatorError (same string surfaced here).
// Publishing a second "authentication failed" banner here produced a
// duplicate top bar and forced the action button to degrade to
// "Show more", so it is intentionally not raised from this handler.
}

private void OnGatewaySessionCommandCompleted(object? sender, SessionCommandResult result)
Expand Down
6 changes: 0 additions & 6 deletions src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@
PINNED MODIFIERS — render across every mode
═════════════════════════════════════════════════════════ -->

<!-- Auth error (legacy, kept) -->
<InfoBar x:Name="AuthErrorBar"
x:Uid="AuthErrorBar"
Title="Connection Error"
Severity="Error" IsOpen="False" IsClosable="True"/>

<!-- Pending approvals (others waiting on the user's decision) -->
<Border x:Name="PendingApprovalsBanner"
Visibility="Collapsed"
Expand Down
60 changes: 22 additions & 38 deletions src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,6 @@ private void RefreshFromSnapshot(GatewayConnectionSnapshot snapshot)
// background highlight reflects the live snapshot. Cheap — list is
// typically < 10 entries and only re-runs on real state transitions.
LoadSavedGateways();

// Bridge auth error (lives outside the plan as a transient modifier)
var authError = CurrentApp.AppState?.AuthFailureMessage;
if (!string.IsNullOrEmpty(authError))
{
AuthErrorBar.Message = GetAuthErrorGuidance(authError!);
AuthErrorBar.IsOpen = true;
}
else
{
AuthErrorBar.IsOpen = false;
}
}

private void ApplyPlan(ConnectionPagePlan plan)
Expand Down Expand Up @@ -2001,10 +1989,23 @@ private void ShowGatewayHostFailure(string title, string message, bool preferInl
return;
}

AuthErrorBar.Title = title;
AuthErrorBar.Message = message;
AuthErrorBar.Severity = InfoBarSeverity.Error;
AuthErrorBar.IsOpen = true;
// No inline WSL-controls surface is available here (e.g. launching a
// terminal for a non-active saved gateway, where that card isn't shown).
// The in-page Connection Error bar was removed, so surface the failure as
// a transient top-bar notification rather than dropping it silently. This
// is a one-off action failure, not the persistent connection-issue banner,
// so it carries no "Open Connection" action.
AppNotificationPublisher.Show(
CurrentApp.AppNotifications,
title,
message,
"connection",
"gateway-host",
AppNotificationSeverity.Error,
$"gateway-host-action:{title}",
actionRoute: string.Empty,
actionLabel: string.Empty,
id: $"gateway-host-action:{title}");
}

private static string UppercaseFirst(string value)
Expand Down Expand Up @@ -2177,18 +2178,16 @@ private async Task OnConnectSavedGatewayAsync(object sender)
catch (Exception ex)
{
// Strip status will read the snapshot's terminal state next tick;
// surface the immediate error in the auth-error bar so the user
// gets feedback even if the snapshot is briefly silent.
// surface the immediate error in the single top connection banner so
// the user gets feedback (and an "Open Connection" action) even if
// the snapshot is briefly silent.
try
{
AuthErrorBar.Title = LocalizationHelper.GetString("ConnectionPage_ConnectFailed");
AuthErrorBar.Message = ex.Message;
AuthErrorBar.Severity = Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error;
AuthErrorBar.IsOpen = true;
CurrentApp.ShowTransientConnectionError(ex.Message);
}
catch (Exception uiEx)
{
Logger.Warn($"ConnectionPage: Failed to surface connect failure in auth error bar: {uiEx.Message}");
Logger.Warn($"ConnectionPage: Failed to surface connect failure in connection banner: {uiEx.Message}");
}
}
finally
Expand Down Expand Up @@ -3540,21 +3539,6 @@ await RunPairingDecisionAsync(approveBtn, rejectBtn, isApprove: false, async cli
return card;
}

// ─── Auth error guidance (preserved) ─────────────────────────────

private static string GetAuthErrorGuidance(string error)
{
if (error.Contains("token", StringComparison.OrdinalIgnoreCase))
return string.Format(LocalizationHelper.GetString("ConnectionPage_AuthGuidanceToken"), error);
if (error.Contains("pairing", StringComparison.OrdinalIgnoreCase))
return string.Format(LocalizationHelper.GetString("ConnectionPage_AuthGuidancePairing"), error);
if (error.Contains("password", StringComparison.OrdinalIgnoreCase))
return string.Format(LocalizationHelper.GetString("ConnectionPage_AuthGuidancePassword"), error);
if (error.Contains("signature", StringComparison.OrdinalIgnoreCase))
return string.Format(LocalizationHelper.GetString("ConnectionPage_AuthGuidanceSignature"), error);
return string.Format(LocalizationHelper.GetString("ConnectionPage_AuthGuidanceDefault"), error);
}

private static string SanitizeUrl(string url)
{
try
Expand Down
32 changes: 29 additions & 3 deletions src/OpenClaw.Tray.WinUI/Services/AppNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,48 @@ public static bool TryGetChatSessionKey(string? route, out string? sessionKey)

internal sealed class AppNotificationBannerState
{
private const string ConnectionSource = "connection";
private readonly HashSet<string> _hiddenNotificationIds = new(StringComparer.Ordinal);

public AppNotification? SelectVisibleNotification(AppNotificationSnapshot snapshot, bool revealHiddenIfNeeded = false)
{
PruneRemovedNotifications(snapshot);
var visible = snapshot.ActiveNotifications.FirstOrDefault(notification =>
!_hiddenNotificationIds.Contains(notification.Id));

var visibleCandidates = snapshot.ActiveNotifications
.Where(notification => !_hiddenNotificationIds.Contains(notification.Id))
.ToList();

// Connection issues are the most actionable banner (they route the user
// to the Connection page), so surface them ahead of any earlier,
// unrelated notification that happens to be current. Without this, a
// connection failure arriving while another notification is showing
// would be queued behind it and the user couldn't reach Connection.
// Among connection-source notifications, prefer one that actually has an
// action: an action-less connection notification (e.g. a transient
// gateway-host failure) must not mask a real connection error and
// degrade the banner to "Show more".
var visible = visibleCandidates.FirstOrDefault(IsActionableConnectionPriority)
?? visibleCandidates.FirstOrDefault(IsConnectionPriority)
?? visibleCandidates.FirstOrDefault();
if (visible is not null || !revealHiddenIfNeeded)
return visible;

var fallback = snapshot.ActiveNotifications.FirstOrDefault();
var fallback = snapshot.ActiveNotifications.FirstOrDefault(IsActionableConnectionPriority)
?? snapshot.ActiveNotifications.FirstOrDefault(IsConnectionPriority)
?? snapshot.ActiveNotifications.FirstOrDefault();
if (fallback is not null)
_hiddenNotificationIds.Remove(fallback.Id);
return fallback;
}

private static bool IsConnectionPriority(AppNotification notification) =>
string.Equals(notification.Source, ConnectionSource, StringComparison.OrdinalIgnoreCase);

private static bool IsActionableConnectionPriority(AppNotification notification) =>
IsConnectionPriority(notification)
&& !string.IsNullOrWhiteSpace(notification.ActionLabel)
&& !string.IsNullOrWhiteSpace(notification.ActionRoute);

public void HideActiveNotifications(AppNotificationSnapshot snapshot)
{
PruneRemovedNotifications(snapshot);
Expand Down
34 changes: 0 additions & 34 deletions src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -1815,9 +1815,6 @@ On your gateway host (Mac/Linux), run:
<data name="ConnectionPage_TextBlock_11.Text" xml:space="preserve">
<value>Gateway endpoint used by both the operator client and node service.</value>
</data>
<data name="AuthErrorBar.Title" xml:space="preserve">
<value>Connection Error</value>
</data>
<data name="StatusText.Text" xml:space="preserve">
<value>Disconnected</value>
</data>
Expand Down Expand Up @@ -3276,9 +3273,6 @@ On your gateway host (Mac/Linux), run:
<data name="AppNotification_ActionOpenCron" xml:space="preserve">
<value>Open Cron</value>
</data>
<data name="AppNotification_GatewayAuthenticationFailed_Title" xml:space="preserve">
<value>Gateway authentication failed</value>
</data>
<data name="AppNotification_GatewayConnectionFailed_Title" xml:space="preserve">
<value>Gateway connection failed</value>
</data>
Expand Down Expand Up @@ -5599,9 +5593,6 @@ Make sure the gateway is running.</value>
<data name="ConnectionPage_CancelAction" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ConnectionPage_ConnectFailed" xml:space="preserve">
<value>Connect failed</value>
</data>
<data name="ConnectionPage_GatewayConnectionFailed" xml:space="preserve">
<value>Gateway connection failed.</value>
</data>
Expand Down Expand Up @@ -5632,31 +5623,6 @@ Make sure the gateway is running.</value>
<data name="ConnectionPage_DenyPairingRequest" xml:space="preserve">
<value>Deny pairing request</value>
</data>
<data name="ConnectionPage_AuthGuidanceToken" xml:space="preserve">
<value>{0}

Paste a new setup code from the Add Gateway flow.</value>
</data>
<data name="ConnectionPage_AuthGuidancePairing" xml:space="preserve">
<value>{0}

Your device needs approval on the gateway host.</value>
</data>
<data name="ConnectionPage_AuthGuidancePassword" xml:space="preserve">
<value>{0}

This gateway requires password authentication.</value>
</data>
<data name="ConnectionPage_AuthGuidanceSignature" xml:space="preserve">
<value>{0}

The gateway may require a different auth protocol version.</value>
</data>
<data name="ConnectionPage_AuthGuidanceDefault" xml:space="preserve">
<value>{0}

Check your connection settings and try again.</value>
</data>
<data name="ConnectionPage_AuthModeBootstrap" xml:space="preserve">
<value>bootstrap</value>
</data>
Expand Down
34 changes: 0 additions & 34 deletions src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -1767,9 +1767,6 @@ Sur votre hôte passerelle (Mac/Linux), exécutez :
<data name="ConnectionPage_TextBlock_11.Text" xml:space="preserve">
<value>Point de terminaison de passerelle utilisé par le client opérateur et le service de nœud.</value>
</data>
<data name="AuthErrorBar.Title" xml:space="preserve">
<value>Erreur de connexion</value>
</data>
<data name="StatusText.Text" xml:space="preserve">
<value>Déconnecté</value>
</data>
Expand Down Expand Up @@ -3204,9 +3201,6 @@ Sur votre hôte passerelle (Mac/Linux), exécutez :
<data name="AppNotification_ActionOpenCron" xml:space="preserve">
<value>Ouvrir Cron</value>
</data>
<data name="AppNotification_GatewayAuthenticationFailed_Title" xml:space="preserve">
<value>Échec de l'authentification de la passerelle</value>
</data>
<data name="AppNotification_GatewayConnectionFailed_Title" xml:space="preserve">
<value>Échec de la connexion à la passerelle</value>
</data>
Expand Down Expand Up @@ -5551,9 +5545,6 @@ Assurez-vous que la passerelle est en cours d'exécution.</value>
<data name="ConnectionPage_CancelAction" xml:space="preserve">
<value>Annuler</value>
</data>
<data name="ConnectionPage_ConnectFailed" xml:space="preserve">
<value>Échec de la connexion</value>
</data>
<data name="ConnectionPage_GatewayConnectionFailed" xml:space="preserve">
<value>Échec de la connexion à la passerelle.</value>
</data>
Expand Down Expand Up @@ -5584,31 +5575,6 @@ Assurez-vous que la passerelle est en cours d'exécution.</value>
<data name="ConnectionPage_DenyPairingRequest" xml:space="preserve">
<value>Refuser la demande d’appairage</value>
</data>
<data name="ConnectionPage_AuthGuidanceToken" xml:space="preserve">
<value>{0}

Collez un nouveau code de configuration depuis le flux Ajouter une passerelle.</value>
</data>
<data name="ConnectionPage_AuthGuidancePairing" xml:space="preserve">
<value>{0}

Votre appareil nécessite une approbation sur l’hôte de la passerelle.</value>
</data>
<data name="ConnectionPage_AuthGuidancePassword" xml:space="preserve">
<value>{0}

Cette passerelle nécessite une authentification par mot de passe.</value>
</data>
<data name="ConnectionPage_AuthGuidanceSignature" xml:space="preserve">
<value>{0}

La passerelle pourrait nécessiter une version différente du protocole d’authentification.</value>
</data>
<data name="ConnectionPage_AuthGuidanceDefault" xml:space="preserve">
<value>{0}

Vérifiez vos paramètres de connexion et réessayez.</value>
</data>
<data name="ConnectionPage_AuthModeBootstrap" xml:space="preserve">
<value>amorçage</value>
</data>
Expand Down
Loading
Loading