diff --git a/src/OpenClaw.Tray.WinUI/Helpers/GatewayChatUrlBuilder.cs b/src/OpenClaw.Tray.WinUI/Helpers/GatewayChatUrlBuilder.cs index dd37da45a..751100cdc 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/GatewayChatUrlBuilder.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/GatewayChatUrlBuilder.cs @@ -10,7 +10,8 @@ public static class GatewayChatUrlBuilder { /// /// Build the HTTP(S) chat URL from a WebSocket gateway URL. - /// Converts ws:// → http://, wss:// → https://, appends token and optional session key. + /// Converts ws:// to http://, wss:// to https://, targets the Control UI chat + /// route, and appends the token and optional session key. /// public static bool TryBuildChatUrl( string gatewayUrl, @@ -46,7 +47,12 @@ public static bool TryBuildChatUrl( }; var baseUrl = builder.Uri.GetLeftPart(UriPartial.Authority); - url = $"{baseUrl}?token={Uri.EscapeDataString(token)}"; + + // Target the chat route. The Control UI honours the released ?session= + // identity only at the chat route root (/chat). At / it drops the + // parameter and restores the browser's last selected session, so every + // session card deep link opened the main session instead. + url = $"{baseUrl}/chat?token={Uri.EscapeDataString(token)}"; if (!string.IsNullOrEmpty(sessionKey)) url += $"&session={Uri.EscapeDataString(sessionKey)}"; diff --git a/tests/OpenClaw.Tray.Tests/GatewayChatHelperTests.cs b/tests/OpenClaw.Tray.Tests/GatewayChatHelperTests.cs index 01d3fc78d..a67f47a5f 100644 --- a/tests/OpenClaw.Tray.Tests/GatewayChatHelperTests.cs +++ b/tests/OpenClaw.Tray.Tests/GatewayChatHelperTests.cs @@ -14,6 +14,7 @@ public void TryBuildChatUrl_WsScheme_ConvertsToHttp() Assert.True(ok); Assert.StartsWith("http://localhost:18789", url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); } [Fact] @@ -24,6 +25,7 @@ public void TryBuildChatUrl_WssScheme_ConvertsToHttps() Assert.True(ok); Assert.StartsWith("https://gateway.example.com", url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); } #endregion @@ -38,6 +40,7 @@ public void TryBuildChatUrl_TokenIsUrlEncoded() Assert.True(ok); Assert.Contains("token=a%20b%26c%3Dd", url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); } #endregion @@ -51,7 +54,7 @@ public void TryBuildChatUrl_SessionKeyAppendedWhenProvided() "ws://localhost:18789", "tok", out var url, out _, sessionKey: "sess123"); Assert.True(ok); - Assert.Contains("&session=sess123", url); + Assert.Equal("http://localhost:18789/chat?token=tok&session=sess123", url); } [Fact] @@ -61,6 +64,7 @@ public void TryBuildChatUrl_SessionKeyOmittedWhenNull() "ws://localhost:18789", "tok", out var url, out _, sessionKey: null); Assert.True(ok); + Assert.Equal("http://localhost:18789/chat?token=tok", url); Assert.DoesNotContain("session=", url); } @@ -71,6 +75,7 @@ public void TryBuildChatUrl_SessionKeyOmittedWhenEmpty() "ws://localhost:18789", "tok", out var url, out _, sessionKey: ""); Assert.True(ok); + Assert.Equal("http://localhost:18789/chat?token=tok", url); Assert.DoesNotContain("session=", url); } @@ -96,6 +101,7 @@ public void TryBuildChatUrl_LocalhostHttp_Accepted() Assert.True(ok); Assert.StartsWith("http://localhost", url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); } [Fact] @@ -106,6 +112,7 @@ public void TryBuildChatUrl_127001_AcceptedAsLocal() Assert.True(ok); Assert.StartsWith("http://127.0.0.1:18789", url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); } #endregion @@ -133,4 +140,49 @@ public void TryBuildChatUrl_EmptyUrl_ReturnsFalse() } #endregion + + #region TryBuildChatUrl — chat route boundary + + // The Control UI only honours the released ?session= identity at the chat + // route root. A root-path link drops it and restores the browser's last + // selected session, so the deep link must target /chat. + + [Fact] + public void TryBuildChatUrl_TargetsChatRouteWithoutSessionKey() + { + var ok = GatewayChatUrlBuilder.TryBuildChatUrl( + "ws://127.0.0.1:18789", "tok", out var url, out _); + + Assert.True(ok); + Assert.Equal("http://127.0.0.1:18789/chat?token=tok", url); + Assert.DoesNotContain("18789/?", url); + } + + [Fact] + public void TryBuildChatUrl_SessionKeyRidesOnChatRoute() + { + var ok = GatewayChatUrlBuilder.TryBuildChatUrl( + "ws://127.0.0.1:18789", "tok", out var url, out _, + sessionKey: "agent:main:session-1789313422342"); + + Assert.True(ok); + Assert.Equal( + "http://127.0.0.1:18789/chat?token=tok&session=agent%3Amain%3Asession-1789313422342", + url); + Assert.Equal("/chat", new Uri(url).AbsolutePath); + } + + [Fact] + public void TryBuildChatUrl_MainSessionKeyStillTargetsChatRoute() + { + var ok = GatewayChatUrlBuilder.TryBuildChatUrl( + "ws://127.0.0.1:18789", "tok", out var url, out _, + sessionKey: "agent:main:main"); + + Assert.True(ok); + Assert.Equal("http://127.0.0.1:18789/chat?token=tok&session=agent%3Amain%3Amain", url); + Assert.DoesNotContain("18789/?token=", url); + } + + #endregion }