Skip to content
Open
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
10 changes: 8 additions & 2 deletions src/OpenClaw.Tray.WinUI/Helpers/GatewayChatUrlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public static class GatewayChatUrlBuilder
{
/// <summary>
/// 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.
/// </summary>
public static bool TryBuildChatUrl(
string gatewayUrl,
Expand Down Expand Up @@ -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)}";
Expand Down
54 changes: 53 additions & 1 deletion tests/OpenClaw.Tray.Tests/GatewayChatHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
}