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/Pages/ConfigPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml index d49cd0bb6..05821d92e 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"> @@ -18,7 +19,8 @@ - + + @@ -45,6 +47,7 @@ + ((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/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 d08d486f4..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) 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(); } }