From ed96f0cef6a848dde75e081ed410b33f9bb5d67b Mon Sep 17 00:00:00 2001 From: Yadi Aprianto Date: Thu, 17 Sep 2026 20:30:51 +0700 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(tray):=20open=20the=20selected=20s?= =?UTF-8?q?ession=20from=20Companion=20chat=20deep=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy WebView chat surface built its URL against the Gateway root (`/?token=...&session=...`). The Control UI honours the released `?session=` identity only at the chat route root, so at `/` it drops the parameter and restores the browser's last selected session. Every Sessions card "Open chat" click therefore opened the main session instead of the requested one. Build the deep link against the `/chat` route instead, keeping the token and the optional `session` parameter unchanged. Verified against a running Gateway: `/?token=...&session=` renders the main session, while `/chat?token=...&session=` renders the requested session. Tests: GatewayChatHelperTests now assert the `/chat` path, the exact deep link produced for a session key, and that the old root-path form is not emitted. --- .../Helpers/GatewayChatUrlBuilder.cs | 10 +++- .../GatewayChatHelperTests.cs | 54 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) 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 }