From c8487e56039b6a2575ab0c09dd925f2cc2d75a93 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 22 May 2026 11:35:29 -0700 Subject: [PATCH 1/4] feat(tray): consistent back-links across cross-page navigations Extended the existing 'Back to Connection' affordance (Sessions/Instances/ Permissions) to every cross-page link in the Hub so users always have a one-click return path. - Added Helpers/NavOriginLabels for nav-tag -> display-label mapping so destination pages can render 'Back to {origin}' dynamically. - Threaded originTag through 9 cross-page Navigate calls: About->Debug, Channels->Config (x2), Permissions->Voice, and Bindings/Cron/Usage/ Debug/Instances->Connection. - Added the inline back HyperlinkButton + Initialize-time wiring to ConfigPage, VoiceSettingsPage, DebugPage, and ConnectionPage matching the existing Sessions/Instances/Permissions pattern. - Updated AsyncListLoadingPageWiringTests to accept the originTag-bearing Navigate('connection', ...) signature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/NavOriginLabels.cs | 45 +++++++++++++++++++ .../Pages/AboutPage.xaml.cs | 2 +- .../Pages/BindingsPage.xaml.cs | 2 +- .../Pages/ChannelsPage.xaml.cs | 4 +- .../Pages/ChatPage.xaml.cs | 4 +- src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml | 16 ++++++- .../Pages/ConfigPage.xaml.cs | 27 ++++++++++- .../Pages/ConnectionPage.xaml | 12 +++++ .../Pages/ConnectionPage.xaml.cs | 25 +++++++++++ .../Pages/CronPage.xaml.cs | 2 +- src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml | 12 +++++ .../Pages/DebugPage.xaml.cs | 26 ++++++++++- .../Pages/InstancesPage.xaml.cs | 2 +- .../Pages/PermissionsPage.xaml.cs | 2 +- .../Pages/SessionsPage.xaml.cs | 3 +- .../Pages/UsagePage.xaml.cs | 2 +- .../Pages/VoiceSettingsPage.xaml | 12 +++++ .../Pages/VoiceSettingsPage.xaml.cs | 25 +++++++++++ .../Strings/en-us/Resources.resw | 8 ++++ .../Strings/fr-fr/Resources.resw | 8 ++++ .../Strings/nl-nl/Resources.resw | 8 ++++ .../Strings/zh-cn/Resources.resw | 8 ++++ .../Strings/zh-tw/Resources.resw | 8 ++++ 23 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs diff --git a/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs b/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs new file mode 100644 index 000000000..25d6f7143 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs @@ -0,0 +1,45 @@ +namespace OpenClawTray.Helpers; + +/// +/// Maps HubWindow nav tags to localised page labels for cross-page +/// "Back to {origin}" affordances. Reuses the sidebar +/// HubWindow_NavigationViewItem_*.Content resw entries so page names +/// are translated once and stay in sync with the sidebar. +/// +internal static class NavOriginLabels +{ + public static string DisplayLabel(string? tag) + { + if (string.IsNullOrWhiteSpace(tag)) return string.Empty; + var resourceKey = tag switch + { + "chat" => "HubWindow_NavigationViewItem_82.Content", + "connection" => "HubWindow_NavigationViewItem_88.Content", + "sessions" => "HubWindow_NavigationViewItem_91.Content", + "skills" => "HubWindow_NavigationViewItem_97.Content", + "channels" => "HubWindow_NavigationViewItem_109.Content", + "instances" or "nodes" => "HubWindow_NavigationViewItem_112.Content", + "agentevents" => "HubWindow_NavigationViewItem_94.Content", + "bindings" => "HubWindow_NavigationViewItem_115.Content", + "config" => "HubWindow_NavigationViewItem_118.Content", + "usage" => "HubWindow_NavigationViewItem_121.Content", + "cron" => "HubWindow_NavigationViewItem_124.Content", + "voice" => "HubWindow_NavigationViewItem_Voice.Content", + "settings" => "HubWindow_NavigationViewItem_133.Content", + "permissions" => "HubWindow_NavigationViewItem_136.Content", + "sandbox" => "HubWindow_NavigationViewItem_Sandbox.Content", + "debug" => "HubWindow_NavigationViewItem_145.Content", + "info" or "about" => "HubWindow_NavigationViewItem_148.Content", + _ => null, + }; + return resourceKey == null + ? char.ToUpperInvariant(tag![0]) + tag.Substring(1) + : LocalizationHelper.GetString(resourceKey); + } + + public static string BackToLabel(string? tag) => + LocalizationHelper.Format("BackToOriginFormat", DisplayLabel(tag)); +} + + + diff --git a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs index 93643f4ec..1d83d9046 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs @@ -154,7 +154,7 @@ private void OnCheckUpdatesClick(object sender, RoutedEventArgs e) private void OnMoreDiagnosticsClick(object sender, RoutedEventArgs e) { - ((IAppCommands)CurrentApp).Navigate("debug"); + ((IAppCommands)CurrentApp).Navigate("debug", "info"); } private void OnDocumentationClick(object sender, RoutedEventArgs e) diff --git a/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs index fcff127ec..202c3f575 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs @@ -57,7 +57,7 @@ public void Initialize() } private void OnOpenConnectionClick(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection"); + => ((IAppCommands)CurrentApp).Navigate("connection", "bindings"); private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs index df3ed1c77..2c80d68ce 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs @@ -1346,7 +1346,7 @@ private FrameworkElement BuildInlineConfigForm(ChannelRecord record) { Content = LocalizationHelper.GetString("ChannelsPage_OpenConfigPage"), }; - openConfigBtn.Click += (_, _) => ((IAppCommands)CurrentApp).Navigate("config"); + openConfigBtn.Click += (_, _) => ((IAppCommands)CurrentApp).Navigate("config", "channels"); actionRow.Children.Add(saveBtn); actionRow.Children.Add(openConfigBtn); stack.Children.Add(actionRow); @@ -1946,7 +1946,7 @@ private FrameworkElement BuildConfigPlaceholder(ChannelRecord record) }; btn.Click += (_, _) => { - ((IAppCommands)CurrentApp).Navigate("config"); + ((IAppCommands)CurrentApp).Navigate("config", "channels"); }; stack.Children.Add(btn); return stack; diff --git a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs index a7e32854a..860273f20 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs @@ -268,7 +268,7 @@ private void ShowFunctionalSurface() onStopSpeaking: () => app?.StopChatSpeaking(), onVoiceRequest: VoiceTranscribeAsync, onAttachClick: OnAttachClicked, - onSettingsClick: () => _hub?.NavigateTo("voice"), + onSettingsClick: () => _hub?.NavigateTo("voice", "chat"), onSpeakerMuteChanged: muted => (App.Current as App)?.SetChatSpeakerMuted(muted), initialMuted: CurrentApp.Settings?.VoiceTtsEnabled == false, suppressAutoDispose: true); @@ -762,7 +762,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac private void NavigateToVoiceSettings() { if (_hub is not null) - _hub.NavigateTo("voice"); + _hub.NavigateTo("voice", "chat"); else (App.Current as App)?.ShowHub("voice"); } diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml index d49cd0bb6..28b60b60e 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml @@ -17,8 +17,19 @@ - - + + + + + + + + + @@ -45,6 +56,7 @@ + + + + + + + + + diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs index b190c96b4..0728ffcbd 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs @@ -157,6 +157,31 @@ public void Initialize() UpdatePairingRequests(existingNode); if (_appState?.DevicePairList is { } existingDevice) UpdateDevicePairingRequests(existingDevice); + + RefreshBackOriginLink(); + } + + private string? _backOriginTag; + + private void RefreshBackOriginLink() + { + var hub = CurrentApp.ActiveHubWindow as OpenClawTray.Windows.HubWindow; + var origin = hub?.LastNavigationOrigin; + if (string.IsNullOrEmpty(origin)) + { + _backOriginTag = null; + BackOriginLink.Visibility = Visibility.Collapsed; + return; + } + _backOriginTag = origin; + BackOriginText.Text = Helpers.NavOriginLabels.BackToLabel(origin); + BackOriginLink.Visibility = Visibility.Visible; + } + + private void OnBackOriginClicked(object sender, RoutedEventArgs e) + { + if (!string.IsNullOrEmpty(_backOriginTag)) + ((IAppCommands)CurrentApp).Navigate(_backOriginTag); } private void OnPageUnloaded(object sender, RoutedEventArgs e) diff --git a/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs index ee0d41929..833bfa076 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs @@ -81,7 +81,7 @@ public void Initialize() } private void OnOpenConnectionClick(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection"); + => ((IAppCommands)CurrentApp).Navigate("connection", "cron"); private void OnRefreshClick(object sender, RoutedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml index dfcbe10f9..bd80c9cb6 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml @@ -50,6 +50,18 @@ Padding="24,24,24,24" Spacing="{StaticResource DiagSettingsCardSpacing}"> + + + + + + + + ((IAppCommands)CurrentApp).Navigate("connection"); + => ((IAppCommands)CurrentApp).Navigate("connection", "debug"); // ── Detail view (recent log) ───────────────────────────────────── diff --git a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs index d08d486f4..342941f57 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs @@ -126,7 +126,7 @@ private void UpdatePendingPairBanner() } private void OnPendingPairBannerClicked(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection"); + => ((IAppCommands)CurrentApp).Navigate("connection", "instances"); public void UpdateNodes(GatewayNodeInfo[] nodes) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs index 1a4598ab0..4e601c897 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs @@ -363,7 +363,7 @@ private void UpdateSttEngineHint() private void OnSttMoreSettingsClick(object sender, RoutedEventArgs e) { - ((IAppCommands)CurrentApp).Navigate("voice"); + ((IAppCommands)CurrentApp).Navigate("voice", "permissions"); } // ── Text-to-Speech card ────────────────────────────────────────── diff --git a/src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs index d7862b5c4..08e5a069a 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs @@ -247,7 +247,8 @@ private void OnOpenChat(object sender, RoutedEventArgs e) { hub.PendingChatSessionKey = key; } - ((IAppCommands)CurrentApp).Navigate("chat", "sessions"); + // No origin tag: opening chat should not surface a "Back to Sessions" link. + ((IAppCommands)CurrentApp).Navigate("chat"); } } diff --git a/src/OpenClaw.Tray.WinUI/Pages/UsagePage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/UsagePage.xaml.cs index c03297bfb..cce4bac4f 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/UsagePage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/UsagePage.xaml.cs @@ -169,7 +169,7 @@ private void RequestRefresh(IOperatorGatewayClient client) } private void OnOpenConnectionClick(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection"); + => ((IAppCommands)CurrentApp).Navigate("connection", "usage"); private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml index 892b5715e..4e72092f6 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml @@ -8,6 +8,18 @@ + + + + + + + + diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw index a410aea12..8bb7ab58c 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw @@ -3306,6 +3306,14 @@ On your gateway host (Mac/Linux), run: Back to Connection + + Back to {0} + Format string for cross-page back link text. {0} is the localised name of the page the user came from (reused from the sidebar HubWindow_NavigationViewItem_* labels). + + + Back + Fallback label for the inline back link when the origin page is unknown. Normally not shown — the link is hidden when there is no origin. + 🔗 Pending Operator/Node Pairing diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw index 6a608b643..8e1c72194 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw @@ -3258,6 +3258,14 @@ Sur votre hôte passerelle (Mac/Linux), exécutez : Retour à la connexion + + Retour à {0} + Format string for cross-page back link text. {0} is the localised name of the page the user came from (reused from the sidebar HubWindow_NavigationViewItem_* labels). + + + Retour + Fallback label for the inline back link when the origin page is unknown. Normally not shown — the link is hidden when there is no origin. + 🔗 Pending Operator/Node Pairing diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw index 886e4a6ab..3c3c84754 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw @@ -3259,6 +3259,14 @@ Voer op uw gateway-host (Mac/Linux) uit: Terug naar verbinding + + Terug naar {0} + Format string for cross-page back link text. {0} is the localised name of the page the user came from (reused from the sidebar HubWindow_NavigationViewItem_* labels). + + + Terug + Fallback label for the inline back link when the origin page is unknown. Normally not shown — the link is hidden when there is no origin. + 🔗 Pending Operator/Node Pairing diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw index 7d881d792..b8d437534 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw @@ -3258,6 +3258,14 @@ 返回连接 + + 返回 {0} + Format string for cross-page back link text. {0} is the localised name of the page the user came from (reused from the sidebar HubWindow_NavigationViewItem_* labels). + + + 返回 + Fallback label for the inline back link when the origin page is unknown. Normally not shown — the link is hidden when there is no origin. + 🔗 Pending Operator/Node Pairing diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw index 9e00a8ddd..b8636d79c 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw @@ -3258,6 +3258,14 @@ 返回連線 + + 返回 {0} + Format string for cross-page back link text. {0} is the localised name of the page the user came from (reused from the sidebar HubWindow_NavigationViewItem_* labels). + + + 返回 + Fallback label for the inline back link when the origin page is unknown. Normally not shown — the link is hidden when there is no origin. + 🔗 Pending Operator/Node Pairing From 87f4f8733ee3babfd70fd79dcb5ecdddf929b6e7 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 10:47:12 -0700 Subject: [PATCH 2/4] chore(tray): use catalog back icon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml | 3 ++- src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml index 28b60b60e..5db79521b 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml @@ -5,6 +5,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.UI.Xaml.Controls" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" + xmlns:helpers="using:OpenClawTray.Helpers" xmlns:local="using:OpenClawTray.Controls"> @@ -25,7 +26,7 @@ Visibility="Collapsed" AutomationProperties.AutomationId="BackOriginLink"> - + diff --git a/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml index 4e72092f6..b3f8b1624 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml @@ -2,7 +2,8 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:helpers="using:OpenClawTray.Helpers"> @@ -15,7 +16,7 @@ Visibility="Collapsed" AutomationProperties.AutomationId="BackOriginLink"> - + From e01665430e2b73cc2619d6f520576a1ec44923d6 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 11:50:22 -0700 Subject: [PATCH 3/4] fix(tray): resolve property resource labels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/LocalizationHelper.cs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs index 189b40ca3..f7d4fb002 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs @@ -43,9 +43,20 @@ public static string GetString(string resourceKey) { try { - var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); - var value = candidate?.ValueAsString; - return string.IsNullOrEmpty(value) ? resourceKey : value; + var value = GetResourceValue(resourceKey); + if (!string.IsNullOrEmpty(value)) + return value; + + var propertySeparatorIndex = resourceKey.LastIndexOf('.'); + if (propertySeparatorIndex > 0 && propertySeparatorIndex < resourceKey.Length - 1) + { + var propertyResourceKey = resourceKey[..propertySeparatorIndex] + "/" + resourceKey[(propertySeparatorIndex + 1)..]; + value = GetResourceValue(propertyResourceKey); + if (!string.IsNullOrEmpty(value)) + return value; + } + + return resourceKey; } catch (Exception ex) { @@ -65,6 +76,19 @@ public static string GetString(string resourceKey) } } + private static string? GetResourceValue(string resourceKey) + { + try + { + var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); + return candidate?.ValueAsString; + } + catch + { + return null; + } + } + /// /// Localized . Use for resw values that /// contain placeholders like "{0}". Catches caused by From fe294e3dbf675c3ad3e564a5496397fafc475f2c Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 15 Jun 2026 14:45:38 -0700 Subject: [PATCH 4/4] refactor(tray): use native Frame back navigation instead of custom back links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the custom cross-page back affordances with a single native title-bar back button driven by the NavigationView ContentFrame's back stack. - Add a native back button in the Hub title bar wired to ContentFrame.GoBack(), enabled/disabled by CanGoBack (mirrors NavigationView's root behavior), plus an Alt+Left accelerator. The rail re-syncs its selection on Back/Forward. - Remove all custom origin-tracking infrastructure: NavOriginLabels helper, the 2-arg IAppCommands.Navigate/NavigateTo overloads, LastNavigationOrigin and pending-origin state on HubWindow, and the LocalizationHelper origin additions. - Remove the per-page "Back to {origin}" links (Config/Connection/Debug/Voice) and the "Back to Connection" links (Sessions/Instances/Permissions), along with their code-behind handlers and visibility logic. - Revert all 2-arg navigation call sites back to single-arg Navigate across 11 pages. - Clean up obsolete resw keys and add NavBackButton strings across all five locales (en, fr, nl, zh-cn, zh-tw). In-page form/detail close buttons (ConnectionPage "Add a gateway", DebugPage log detail) are intentionally kept — they are view-state toggles within a page, not Frame navigations. Addresses PR #521 review feedback to rely on native back navigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Tray.WinUI/App.xaml.cs | 5 +- .../Helpers/LocalizationHelper.cs | 30 +--- .../Helpers/NavOriginLabels.cs | 45 ------ .../Pages/AboutPage.xaml.cs | 2 +- .../Pages/BindingsPage.xaml.cs | 2 +- .../Pages/ChannelsPage.xaml.cs | 4 +- .../Pages/ChatPage.xaml.cs | 4 +- src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml | 12 +- .../Pages/ConfigPage.xaml.cs | 26 +--- .../Pages/ConnectionPage.xaml | 12 -- .../Pages/ConnectionPage.xaml.cs | 31 +--- .../Pages/CronPage.xaml.cs | 2 +- src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml | 12 -- .../Pages/DebugPage.xaml.cs | 26 +--- .../Pages/InstancesPage.xaml | 14 -- .../Pages/InstancesPage.xaml.cs | 13 +- .../Pages/PermissionsPage.xaml | 14 -- .../Pages/PermissionsPage.xaml.cs | 13 +- .../Pages/SessionsPage.xaml | 21 +-- .../Pages/SessionsPage.xaml.cs | 13 +- .../Pages/UsagePage.xaml.cs | 2 +- .../Pages/VoiceSettingsPage.xaml | 12 -- .../Pages/VoiceSettingsPage.xaml.cs | 24 ---- .../Services/IAppCommands.cs | 1 - .../Strings/en-us/Resources.resw | 23 +-- .../Strings/fr-fr/Resources.resw | 23 +-- .../Strings/nl-nl/Resources.resw | 23 +-- .../Strings/zh-cn/Resources.resw | 23 +-- .../Strings/zh-tw/Resources.resw | 23 +-- .../Windows/HubWindow.xaml | 43 ++++-- .../Windows/HubWindow.xaml.cs | 134 ++++++++++-------- 31 files changed, 155 insertions(+), 477 deletions(-) delete mode 100644 src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 126d2d5da..441586648 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -2678,7 +2678,7 @@ private string BuildTrayTooltip() => #region Window Management - internal void ShowHub(string? navigateTo = null, bool activate = true, string? originTag = null) + internal void ShowHub(string? navigateTo = null, bool activate = true) { if (_hubWindow == null || _hubWindow.IsClosed) { @@ -2729,7 +2729,7 @@ internal void ShowHub(string? navigateTo = null, bool activate = true, string? o if (navigateTo != null) { - _hubWindow.NavigateTo(navigateTo, originTag); + _hubWindow.NavigateTo(navigateTo); } if (activate) { @@ -3242,7 +3242,6 @@ private void OpenDashboard(string? path = null) void IAppCommands.OpenDashboard(string? path) => OpenDashboard(path); void IAppCommands.Navigate(string pageTag) => ShowHub(pageTag); - void IAppCommands.Navigate(string pageTag, string? originTag) => ShowHub(pageTag, originTag: originTag); void IAppCommands.Reconnect() => _ = _connectionManager?.ReconnectAsync(); void IAppCommands.Disconnect() { diff --git a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs index f7d4fb002..189b40ca3 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs @@ -43,20 +43,9 @@ public static string GetString(string resourceKey) { try { - var value = GetResourceValue(resourceKey); - if (!string.IsNullOrEmpty(value)) - return value; - - var propertySeparatorIndex = resourceKey.LastIndexOf('.'); - if (propertySeparatorIndex > 0 && propertySeparatorIndex < resourceKey.Length - 1) - { - var propertyResourceKey = resourceKey[..propertySeparatorIndex] + "/" + resourceKey[(propertySeparatorIndex + 1)..]; - value = GetResourceValue(propertyResourceKey); - if (!string.IsNullOrEmpty(value)) - return value; - } - - return resourceKey; + var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); + var value = candidate?.ValueAsString; + return string.IsNullOrEmpty(value) ? resourceKey : value; } catch (Exception ex) { @@ -76,19 +65,6 @@ public static string GetString(string resourceKey) } } - private static string? GetResourceValue(string resourceKey) - { - try - { - var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); - return candidate?.ValueAsString; - } - catch - { - return null; - } - } - /// /// Localized . Use for resw values that /// contain placeholders like "{0}". Catches caused by diff --git a/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs b/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs deleted file mode 100644 index 25d6f7143..000000000 --- a/src/OpenClaw.Tray.WinUI/Helpers/NavOriginLabels.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace OpenClawTray.Helpers; - -/// -/// Maps HubWindow nav tags to localised page labels for cross-page -/// "Back to {origin}" affordances. Reuses the sidebar -/// HubWindow_NavigationViewItem_*.Content resw entries so page names -/// are translated once and stay in sync with the sidebar. -/// -internal static class NavOriginLabels -{ - public static string DisplayLabel(string? tag) - { - if (string.IsNullOrWhiteSpace(tag)) return string.Empty; - var resourceKey = tag switch - { - "chat" => "HubWindow_NavigationViewItem_82.Content", - "connection" => "HubWindow_NavigationViewItem_88.Content", - "sessions" => "HubWindow_NavigationViewItem_91.Content", - "skills" => "HubWindow_NavigationViewItem_97.Content", - "channels" => "HubWindow_NavigationViewItem_109.Content", - "instances" or "nodes" => "HubWindow_NavigationViewItem_112.Content", - "agentevents" => "HubWindow_NavigationViewItem_94.Content", - "bindings" => "HubWindow_NavigationViewItem_115.Content", - "config" => "HubWindow_NavigationViewItem_118.Content", - "usage" => "HubWindow_NavigationViewItem_121.Content", - "cron" => "HubWindow_NavigationViewItem_124.Content", - "voice" => "HubWindow_NavigationViewItem_Voice.Content", - "settings" => "HubWindow_NavigationViewItem_133.Content", - "permissions" => "HubWindow_NavigationViewItem_136.Content", - "sandbox" => "HubWindow_NavigationViewItem_Sandbox.Content", - "debug" => "HubWindow_NavigationViewItem_145.Content", - "info" or "about" => "HubWindow_NavigationViewItem_148.Content", - _ => null, - }; - return resourceKey == null - ? char.ToUpperInvariant(tag![0]) + tag.Substring(1) - : LocalizationHelper.GetString(resourceKey); - } - - public static string BackToLabel(string? tag) => - LocalizationHelper.Format("BackToOriginFormat", DisplayLabel(tag)); -} - - - diff --git a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs index 1d83d9046..93643f4ec 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml.cs @@ -154,7 +154,7 @@ private void OnCheckUpdatesClick(object sender, RoutedEventArgs e) private void OnMoreDiagnosticsClick(object sender, RoutedEventArgs e) { - ((IAppCommands)CurrentApp).Navigate("debug", "info"); + ((IAppCommands)CurrentApp).Navigate("debug"); } private void OnDocumentationClick(object sender, RoutedEventArgs e) diff --git a/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs index 202c3f575..fcff127ec 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/BindingsPage.xaml.cs @@ -57,7 +57,7 @@ public void Initialize() } private void OnOpenConnectionClick(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection", "bindings"); + => ((IAppCommands)CurrentApp).Navigate("connection"); private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs index 2c80d68ce..df3ed1c77 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs @@ -1346,7 +1346,7 @@ private FrameworkElement BuildInlineConfigForm(ChannelRecord record) { Content = LocalizationHelper.GetString("ChannelsPage_OpenConfigPage"), }; - openConfigBtn.Click += (_, _) => ((IAppCommands)CurrentApp).Navigate("config", "channels"); + openConfigBtn.Click += (_, _) => ((IAppCommands)CurrentApp).Navigate("config"); actionRow.Children.Add(saveBtn); actionRow.Children.Add(openConfigBtn); stack.Children.Add(actionRow); @@ -1946,7 +1946,7 @@ private FrameworkElement BuildConfigPlaceholder(ChannelRecord record) }; btn.Click += (_, _) => { - ((IAppCommands)CurrentApp).Navigate("config", "channels"); + ((IAppCommands)CurrentApp).Navigate("config"); }; stack.Children.Add(btn); return stack; diff --git a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs index 860273f20..a7e32854a 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs @@ -268,7 +268,7 @@ private void ShowFunctionalSurface() onStopSpeaking: () => app?.StopChatSpeaking(), onVoiceRequest: VoiceTranscribeAsync, onAttachClick: OnAttachClicked, - onSettingsClick: () => _hub?.NavigateTo("voice", "chat"), + onSettingsClick: () => _hub?.NavigateTo("voice"), onSpeakerMuteChanged: muted => (App.Current as App)?.SetChatSpeakerMuted(muted), initialMuted: CurrentApp.Settings?.VoiceTtsEnabled == false, suppressAutoDispose: true); @@ -762,7 +762,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac private void NavigateToVoiceSettings() { if (_hub is not null) - _hub.NavigateTo("voice", "chat"); + _hub.NavigateTo("voice"); else (App.Current as App)?.ShowHub("voice"); } diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml index 5db79521b..05821d92e 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml @@ -18,18 +18,8 @@ - + - - - - - - diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs index 9b596165b..e27782fd7 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs @@ -35,8 +35,6 @@ public sealed partial class ConfigPage : Page private ConfigEditorSnapshot _editSnapshot = ConfigEditorSnapshot.Empty; private IOperatorGatewayClient? _permissionClient; - private string? _backOriginTag; - private string _selectedPath = ""; private string _searchText = ""; private bool _showSchemaFallback; @@ -109,7 +107,6 @@ public void Initialize() _appState = CurrentApp.AppState!; _appState.PropertyChanged += OnAppStateChanged; - RefreshBackOriginLink(); SubscribePermissionClient(CurrentApp.GatewayClient); Logger.Info("[ConfigPage] Initialize"); if (CompleteReconnectIfReady()) @@ -128,27 +125,6 @@ public void Initialize() } } - private void RefreshBackOriginLink() - { - var hub = CurrentApp.ActiveHubWindow as HubWindow; - var origin = hub?.LastNavigationOrigin; - if (string.IsNullOrEmpty(origin)) - { - _backOriginTag = null; - BackOriginLink.Visibility = Visibility.Collapsed; - return; - } - _backOriginTag = origin; - BackOriginText.Text = NavOriginLabels.BackToLabel(origin); - BackOriginLink.Visibility = Visibility.Visible; - } - - private void OnBackOriginClicked(object sender, RoutedEventArgs e) - { - if (!string.IsNullOrEmpty(_backOriginTag)) - ((IAppCommands)CurrentApp).Navigate(_backOriginTag); - } - public void UpdateConfig(JsonElement config) { var configSnapshot = config.Clone(); @@ -675,7 +651,7 @@ private void OnResetSection(object sender, RoutedEventArgs e) private void OnOpenConnection(object sender, RoutedEventArgs e) { - ((IAppCommands)CurrentApp).Navigate("connection", "config"); + ((IAppCommands)CurrentApp).Navigate("connection"); } private void OnOpenDashboard(object sender, RoutedEventArgs e) diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml index e7502f866..8e37bc8d3 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml @@ -55,18 +55,6 @@ - - - - - - - - diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs index 0728ffcbd..a117d3338 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs @@ -157,31 +157,6 @@ public void Initialize() UpdatePairingRequests(existingNode); if (_appState?.DevicePairList is { } existingDevice) UpdateDevicePairingRequests(existingDevice); - - RefreshBackOriginLink(); - } - - private string? _backOriginTag; - - private void RefreshBackOriginLink() - { - var hub = CurrentApp.ActiveHubWindow as OpenClawTray.Windows.HubWindow; - var origin = hub?.LastNavigationOrigin; - if (string.IsNullOrEmpty(origin)) - { - _backOriginTag = null; - BackOriginLink.Visibility = Visibility.Collapsed; - return; - } - _backOriginTag = origin; - BackOriginText.Text = Helpers.NavOriginLabels.BackToLabel(origin); - BackOriginLink.Visibility = Visibility.Visible; - } - - private void OnBackOriginClicked(object sender, RoutedEventArgs e) - { - if (!string.IsNullOrEmpty(_backOriginTag)) - ((IAppCommands)CurrentApp).Navigate(_backOriginTag); } private void OnPageUnloaded(object sender, RoutedEventArgs e) @@ -1873,12 +1848,12 @@ private void OnInstallLocalWslGateway(object sender, RoutedEventArgs e) // ─── Operator card navigation ──────────────────────────────────── - private void OnOpenSessions(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("sessions", "connection"); - private void OnOpenInstances(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("instances", "connection"); + private void OnOpenSessions(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("sessions"); + private void OnOpenInstances(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("instances"); // ─── Node card navigation ──────────────────────────────────────── - private void OnOpenPermissions(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("permissions", "connection"); + private void OnOpenPermissions(object sender, RoutedEventArgs e) => ((IAppCommands)CurrentApp).Navigate("permissions"); private void OnCopyNodeApproveCommand(object sender, RoutedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs index 833bfa076..ee0d41929 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/CronPage.xaml.cs @@ -81,7 +81,7 @@ public void Initialize() } private void OnOpenConnectionClick(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection", "cron"); + => ((IAppCommands)CurrentApp).Navigate("connection"); private void OnRefreshClick(object sender, RoutedEventArgs e) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml index bd80c9cb6..dfcbe10f9 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml @@ -50,18 +50,6 @@ Padding="24,24,24,24" Spacing="{StaticResource DiagSettingsCardSpacing}"> - - - - - - - - ((IAppCommands)CurrentApp).Navigate("connection", "debug"); + => ((IAppCommands)CurrentApp).Navigate("connection"); // ── Detail view (recent log) ───────────────────────────────────── diff --git a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml index 3ff645f60..943f272bf 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml @@ -6,20 +6,6 @@ - - - - - - - - diff --git a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs index 342941f57..09a14aa16 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/InstancesPage.xaml.cs @@ -41,14 +41,6 @@ public InstancesPage() /// Called by HubWindow when this page becomes the navigation target. public void Initialize() { - // Show "← Back to Connection" only when the user arrived from - // Connection's cross-page link; staying hidden when the rail nav - // is used keeps the page chrome quiet for direct navigation. - var hub = CurrentApp.ActiveHubWindow as HubWindow; - BackToConnectionLink.Visibility = hub?.LastNavigationOrigin == "connection" - ? Visibility.Visible - : Visibility.Collapsed; - if (_appState != null) _appState.PropertyChanged -= OnAppStateChanged; _appState = CurrentApp.AppState!; _appState.PropertyChanged += OnAppStateChanged; @@ -71,9 +63,6 @@ public void Initialize() } } - private void OnBackToConnectionClicked(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection"); - private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -126,7 +115,7 @@ private void UpdatePendingPairBanner() } private void OnPendingPairBannerClicked(object sender, RoutedEventArgs e) - => ((IAppCommands)CurrentApp).Navigate("connection", "instances"); + => ((IAppCommands)CurrentApp).Navigate("connection"); public void UpdateNodes(GatewayNodeInfo[] nodes) { diff --git a/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml index b18a66a23..a5a0fd67e 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml @@ -10,20 +10,6 @@ - - - - - - - - - + + + + + diff --git a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs index 5b2e1d8d1..7330673a0 100644 --- a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs @@ -339,6 +339,31 @@ private void OnNavPaneToggleButtonClick(object sender, RoutedEventArgs e) NavView.IsPaneOpen = !NavView.IsPaneOpen; } + // ── Back navigation (title-bar back button + Alt+Left) ────────────────── + // + // We host a single native-style back button in the custom title bar and + // drive it off ContentFrame's real back stack. NavigationView's own back + // button is collapsed because its chrome is hoisted into the custom title + // bar; this button is the equivalent affordance. + + private void OnBackRequested(object sender, RoutedEventArgs e) => GoBack(); + + private void GoBack() + { + if (ContentFrame.CanGoBack) + ContentFrame.GoBack(); + } + + /// + /// Enable/disable the title-bar back button to mirror ContentFrame's back + /// stack (greyed out at the root, exactly like NavigationView's native + /// back button). Called after every navigation. + /// + private void UpdateBackButton() + { + NavBackButton.IsEnabled = ContentFrame.CanGoBack; + } + /// /// Navigate to the default page. Call after setting AppModel. /// @@ -357,45 +382,19 @@ public void NavigateToDefault() // (rather than relying on NavView.SelectedItem) so navigation identity // includes the tag — important for agent-scoped pages where several tags // map to the same Page type (e.g. "sessions" vs "agent:main:sessions" - // both → SessionsPage), and for the per-page "Back to ..." link logic - // that needs to know whether the user arrived via a cross-page link. + // both → SessionsPage). private string? _currentNavTag; // Set true while a programmatic SelectedItem update is in flight, to // suppress the resulting SelectionChanged from re-entering NavigateInternal. private bool _syncingNavSelection; - // Set by NavigateTo(tag, originTag); consumed by OnContentFrameNavigated - // when the new page is initialized, then surfaced as LastNavigationOrigin - // so destination pages can decide whether to show an inline back link. - private string? _pendingNavigationOrigin; - - /// - /// Tag of the page the user navigated FROM on the most recent navigation, - /// or null if the navigation didn't declare an origin (e.g. rail - /// click, deep link, app start). Destination pages read this in - /// Initialize to decide whether to surface a "Back to X" affordance. - /// - public string? LastNavigationOrigin { get; private set; } - /// /// Navigate to a specific page by tag name (e.g. "connection", "sessions", "channels"). + /// Cross-page links and the rail both flow through here; the resulting + /// back-stack entry powers the title-bar back button. /// - public void NavigateTo(string tag) => NavigateTo(tag, null); - - /// - /// Navigate to a specific page by tag, declaring which logical surface - /// initiated the navigation. The destination page can read this via - /// to render an inline "Back to ..." - /// link — used by cross-page links on the Connection page so users have - /// a one-click return path without relying on the rail or a chrome back - /// button. - /// - public void NavigateTo(string tag, string? originTag) - { - _pendingNavigationOrigin = originTag; - NavigateInternal(NormalizeNavTag(tag)); - } + public void NavigateTo(string tag) => NavigateInternal(NormalizeNavTag(tag)); private string NormalizeNavTag(string tag) { @@ -414,14 +413,7 @@ private string NormalizeNavTag(string tag) private void NavigateInternal(string tag) { var pageType = TagToPageType(tag); - if (pageType == null) - { - // Unknown tag: nothing to navigate, but we still need to discard - // any pending origin so it doesn't leak into the next real - // navigation (where it would surface a wrong "Back to ..." link). - _pendingNavigationOrigin = null; - return; - } + if (pageType == null) return; // Identity dedupe: navigation identity = (PageType, normalized tag). // Page-type-only dedupe would collapse distinct logical destinations @@ -431,22 +423,29 @@ private void NavigateInternal(string tag) if (ContentFrame.SourcePageType == pageType && _currentNavTag == tag) { _contentReady = CreateCompletedContentReady(); - // Same as above: Frame.Navigate is skipped, so - // OnContentFrameNavigated won't run to consume the origin. If the - // caller changed origin context, refresh the active page so inline - // back-link state stays accurate. - var pendingOrigin = _pendingNavigationOrigin; - _pendingNavigationOrigin = null; - if (!string.Equals(LastNavigationOrigin, pendingOrigin, StringComparison.Ordinal)) - { - LastNavigationOrigin = pendingOrigin; - InitializeCurrentPage(); - } return; } - // Best-effort rail highlight. Suppress the selection-changed callback - // so this programmatic update doesn't re-enter NavigateInternal. + // Best-effort rail highlight before the page swaps in. + SyncNavSelection(tag); + + // Pass the tag as the navigation parameter so OnContentFrameNavigated + // can recover the canonical destination on Back/Forward. + var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _contentReady = ready; + if (!ContentFrame.Navigate(pageType, tag)) + CompleteContentReady(ready); + } + + /// + /// Reflect in the NavigationView rail. Suppresses the + /// resulting SelectionChanged so this programmatic update does not re-enter + /// (which would push a duplicate back-stack + /// entry). This matters when called from Back/Forward in OnContentFrameNavigated. + /// + private void SyncNavSelection(string? tag) + { + if (tag == null) return; var item = FindNavItemForTag(NavView.MenuItems, tag) ?? FindNavItemForTag(NavView.FooterMenuItems, tag); if (item != null && !ReferenceEquals(NavView.SelectedItem, item)) @@ -455,13 +454,6 @@ private void NavigateInternal(string tag) try { NavView.SelectedItem = item; } finally { _syncingNavSelection = false; } } - - // Pass the tag as the navigation parameter so OnContentFrameNavigated - // can recover the canonical destination on Back/Forward. - var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _contentReady = ready; - if (!ContentFrame.Navigate(pageType, tag)) - CompleteContentReady(ready); } public async Task WaitForCurrentContentReadyAsync() @@ -679,17 +671,15 @@ private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelec /// /// Authoritative post-navigation hook. Runs for every successful - /// Frame.Navigate, so it's the single place that rebuilds - /// / / - /// and re-runs + /// Frame.Navigate (including Back/Forward), so it's the single place that + /// rebuilds / , + /// re-syncs the rail + back button, and re-runs /// for the page that's now visible. /// private void OnContentFrameNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e) { var tag = e.Parameter as string; _currentNavTag = tag; - LastNavigationOrigin = _pendingNavigationOrigin; - _pendingNavigationOrigin = null; // Keep _currentAgentId aligned with the page that's now visible. if (tag != null && tag.StartsWith("agent:")) @@ -702,6 +692,12 @@ private void OnContentFrameNavigated(object sender, Microsoft.UI.Xaml.Navigation } } + // Reflect the restored page in the rail. Back/Forward don't route + // through NavigateInternal, so this is the only place the rail + // highlight gets re-synced for them. + SyncNavSelection(tag); + UpdateBackButton(); + InitializeCurrentPage(); UpdateAppNotificationActionEnabledState(); ArmContentReady(e.Content as FrameworkElement); @@ -894,6 +890,18 @@ private void OnRootPreviewKeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRout e.Handled = true; TitleSearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); TitleSearchBox.Text = ""; + return; + } + + // Alt+Left → back, matching the shell-wide navigation gesture and + // NavigationView's built-in keyboard accelerator. + var alt = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread( + global::Windows.System.VirtualKey.Menu).HasFlag( + global::Windows.UI.Core.CoreVirtualKeyStates.Down); + if (alt && e.Key == global::Windows.System.VirtualKey.Left && ContentFrame.CanGoBack) + { + e.Handled = true; + GoBack(); } }