Skip to content

Commit fd8ff7d

Browse files
fix(onboarding): restore post-wizard chat handoff
Restores the post-wizard handoff into Hub Chat, waits for operator/chat readiness, sends the first-run hatching prompt through gateway chat.send with subscribe-first run-completion observation, and suppresses duplicate local node pairing behavior. Reviewed after prior feedback. Local validation passed: .\\build.ps1, Shared tests, and Tray tests. GitHub checks are green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8864e96 commit fd8ff7d

19 files changed

Lines changed: 875 additions & 576 deletions

src/OpenClaw.Shared/IOperatorGatewayClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public interface IOperatorGatewayClient
3535
event EventHandler<JsonElement>? AgentsListUpdated;
3636
event EventHandler<JsonElement>? AgentFilesListUpdated;
3737
event EventHandler<JsonElement>? AgentFileContentUpdated;
38+
event EventHandler<AgentEventInfo>? ChatEventReceived;
3839

3940
// ─── Query ───
4041
string? OperatorDeviceId { get; }
@@ -53,6 +54,7 @@ public interface IOperatorGatewayClient
5354

5455
// ─── Request Methods ───
5556
Task SendChatMessageAsync(string message, string? sessionKey = null);
57+
Task<ChatSendResult> SendChatMessageForRunAsync(string message, string? sessionKey = null);
5658
Task CheckHealthAsync();
5759
Task RequestSessionsAsync(string? agentId = null);
5860
Task RequestUsageAsync();

src/OpenClaw.Shared/Models.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,13 @@ public string DataJson
16601660
}
16611661
}
16621662

1663+
public sealed class ChatSendResult
1664+
{
1665+
public string? RunId { get; init; }
1666+
public string? SessionKey { get; init; }
1667+
public bool Cached { get; init; }
1668+
}
1669+
16631670
// ── Node/Device Pairing ──
16641671

16651672
public class PairingRequest

src/OpenClaw.Shared/OpenClawGatewayClient.cs

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class OpenClawGatewayClient : WebSocketClientBase, IOperatorGatewayClient
3838
private GatewayUsageStatusInfo? _usageStatus;
3939
private GatewayCostUsageInfo? _usageCost;
4040
private readonly Dictionary<string, string> _pendingRequestMethods = new();
41-
private readonly Dictionary<string, TaskCompletionSource<bool>> _pendingChatSendRequests = new();
41+
private readonly Dictionary<string, TaskCompletionSource<ChatSendResult>> _pendingChatSendRequests = new();
4242
private readonly object _pendingRequestLock = new();
4343
private readonly object _pendingChatSendLock = new();
4444
private readonly object _sessionsLock = new();
@@ -175,6 +175,7 @@ protected override void OnDisposing()
175175
public event EventHandler<JsonElement>? AgentsListUpdated;
176176
public event EventHandler<JsonElement>? AgentFilesListUpdated;
177177
public event EventHandler<JsonElement>? AgentFileContentUpdated;
178+
public event EventHandler<AgentEventInfo>? ChatEventReceived;
178179

179180
/// <summary>Raised when a device token is received from the gateway during hello-ok handshake.</summary>
180181
public event EventHandler<DeviceTokenReceivedEventArgs>? DeviceTokenReceived;
@@ -251,6 +252,11 @@ public async Task CheckHealthAsync()
251252
}
252253

253254
public async Task SendChatMessageAsync(string message, string? sessionKey = null)
255+
{
256+
_ = await SendChatMessageForRunAsync(message, sessionKey).ConfigureAwait(false);
257+
}
258+
259+
public async Task<ChatSendResult> SendChatMessageForRunAsync(string message, string? sessionKey = null)
254260
{
255261
if (!IsConnected)
256262
throw new InvalidOperationException("Gateway connection is not open");
@@ -262,7 +268,7 @@ public async Task SendChatMessageAsync(string message, string? sessionKey = null
262268
: sessionKey.Trim();
263269

264270
var requestId = Guid.NewGuid().ToString();
265-
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
271+
var completion = new TaskCompletionSource<ChatSendResult>(TaskCreationOptions.RunContinuationsAsynchronously);
266272
TrackPendingChatSend(requestId, completion);
267273

268274
var req = new
@@ -287,8 +293,9 @@ public async Task SendChatMessageAsync(string message, string? sessionKey = null
287293
throw new TimeoutException("Timed out waiting for chat.send response from gateway");
288294
}
289295

290-
await completion.Task;
296+
var result = await completion.Task.ConfigureAwait(false);
291297
_logger.Info($"Sent chat message ({message.Length} chars)");
298+
return result;
292299
}
293300

294301
/// <summary>
@@ -886,7 +893,7 @@ private void ClearPendingRequests()
886893
_pendingWizardResponses.Clear();
887894
}
888895

889-
private void TrackPendingChatSend(string requestId, TaskCompletionSource<bool> completion)
896+
private void TrackPendingChatSend(string requestId, TaskCompletionSource<ChatSendResult> completion)
890897
{
891898
lock (_pendingChatSendLock)
892899
{
@@ -902,7 +909,7 @@ private void RemovePendingChatSend(string requestId)
902909
}
903910
}
904911

905-
private TaskCompletionSource<bool>? TakePendingChatSend(string? requestId)
912+
private TaskCompletionSource<ChatSendResult>? TakePendingChatSend(string? requestId)
906913
{
907914
if (string.IsNullOrWhiteSpace(requestId))
908915
{
@@ -975,7 +982,7 @@ private void HandleResponse(JsonElement root)
975982
return;
976983
}
977984

978-
pendingChatSend.TrySetResult(true);
985+
pendingChatSend.TrySetResult(ParseChatSendResult(root));
979986
return;
980987
}
981988

@@ -1205,6 +1212,36 @@ private bool HandleKnownResponse(string method, JsonElement payload)
12051212
}
12061213
}
12071214

1215+
private static ChatSendResult ParseChatSendResult(JsonElement root)
1216+
{
1217+
string? runId = null;
1218+
string? sessionKey = null;
1219+
var cached = false;
1220+
1221+
if (root.TryGetProperty("payload", out var payload) && payload.ValueKind == JsonValueKind.Object)
1222+
{
1223+
if (payload.TryGetProperty("runId", out var runIdProp))
1224+
runId = runIdProp.GetString();
1225+
if (payload.TryGetProperty("sessionKey", out var sessionKeyProp))
1226+
sessionKey = sessionKeyProp.GetString();
1227+
}
1228+
1229+
if (root.TryGetProperty("meta", out var meta) &&
1230+
meta.ValueKind == JsonValueKind.Object &&
1231+
meta.TryGetProperty("cached", out var cachedProp) &&
1232+
cachedProp.ValueKind is JsonValueKind.True or JsonValueKind.False)
1233+
{
1234+
cached = cachedProp.GetBoolean();
1235+
}
1236+
1237+
return new ChatSendResult
1238+
{
1239+
RunId = runId,
1240+
SessionKey = sessionKey,
1241+
Cached = cached
1242+
};
1243+
}
1244+
12081245
private void HandleRequestError(string? method, JsonElement root)
12091246
{
12101247
var message = TryGetErrorMessage(root) ?? "request failed";
@@ -1979,6 +2016,7 @@ private void HandleChatEvent(JsonElement root)
19792016
_logger.Debug($"Chat event received: {rawText[..Math.Min(200, rawText.Length)]}");
19802017

19812018
if (!root.TryGetProperty("payload", out var payload)) return;
2019+
EmitRawChatEvent(payload);
19822020

19832021
// Try new format: payload.message.role + payload.message.content[].text
19842022
if (payload.TryGetProperty("message", out var message))
@@ -2021,6 +2059,38 @@ private void HandleChatEvent(JsonElement root)
20212059
}
20222060
}
20232061

2062+
private void EmitRawChatEvent(JsonElement payload)
2063+
{
2064+
try
2065+
{
2066+
var stream = "chat";
2067+
if (payload.TryGetProperty("message", out var message) &&
2068+
message.TryGetProperty("role", out var roleProp))
2069+
{
2070+
stream = roleProp.GetString() ?? stream;
2071+
}
2072+
else if (payload.TryGetProperty("role", out var legacyRoleProp))
2073+
{
2074+
stream = legacyRoleProp.GetString() ?? stream;
2075+
}
2076+
2077+
var evt = new AgentEventInfo
2078+
{
2079+
RunId = payload.TryGetProperty("runId", out var rid) ? rid.GetString() ?? "" : "",
2080+
Seq = payload.TryGetProperty("seq", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number ? seqProp.GetInt32() : 0,
2081+
Stream = stream,
2082+
Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
2083+
Data = payload.Clone(),
2084+
SessionKey = payload.TryGetProperty("sessionKey", out var sk) ? sk.GetString() : null
2085+
};
2086+
ChatEventReceived?.Invoke(this, evt);
2087+
}
2088+
catch (Exception ex)
2089+
{
2090+
_logger.Warn($"Failed to emit chat event: {ex.Message}");
2091+
}
2092+
}
2093+
20242094
private void EmitChatNotification(string text)
20252095
{
20262096
var displayText = text.Length > 200 ? text[..200] + "…" : text;

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args)
482482
nodeConnector: nodeConnector,
483483
isNodeEnabled: ShouldInitializeNodeService,
484484
diagnostics: diagnostics,
485-
tunnelManager: tunnelManager);
485+
tunnelManager: tunnelManager,
486+
shouldStartNodeConnection: ShouldInitializeNodeService);
486487
_connectionManager.OperatorClientChanged += OnOperatorClientChanged;
487488
_connectionManager.StateChanged += OnManagerStateChanged;
488489

@@ -2189,6 +2190,27 @@ private bool ShouldInitializeNodeService()
21892190
return _settings?.EnableNodeMode == true || _settings?.EnableMcpServer == true;
21902191
}
21912192

2193+
private bool ShouldInitializeNodeService(GatewayRecord activeGateway, string managerIdentityPath)
2194+
{
2195+
if (!ShouldInitializeNodeService()) return false;
2196+
2197+
if (LocalNodeServiceOwnsIdentityFor(activeGateway))
2198+
{
2199+
Logger.Info("[ConnMgr] Suppressing manager-owned NodeConnector because local NodeService owns the active local gateway identity");
2200+
return false;
2201+
}
2202+
2203+
return true;
2204+
}
2205+
2206+
private bool LocalNodeServiceOwnsIdentityFor(GatewayRecord activeGateway)
2207+
{
2208+
if (!activeGateway.IsLocal || _settings == null) return false;
2209+
if (!StartupSetupState.HasStoredNodeDeviceToken(IdentityDataPath)) return false;
2210+
2211+
return EnsureNodeServiceForLocalGatewaySetup(_settings) != null;
2212+
}
2213+
21922214
private void OnNodeStatusChanged(object? sender, ConnectionStatus status)
21932215
{
21942216
Logger.Info($"Node status: {status}");

src/OpenClaw.Tray.WinUI/Onboarding/OnboardingWindow.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,6 @@ private async Task InitializeChatWebViewAsync()
446446
})();
447447
");
448448

449-
_ = SendBootstrapMessageAsync();
450449
}
451450
});
452451
};
@@ -517,25 +516,6 @@ private void ShowChatError(string message)
517516
}
518517
}
519518

520-
private bool _bootstrapSent;
521-
522-
/// <summary>
523-
/// Auto-sends the bootstrap kickoff message after the web chat loads.
524-
/// Delegates to <see cref="BootstrapMessageInjector"/> so the same gated
525-
/// kickoff fires from both the (legacy) onboarding chat overlay and from
526-
/// post-wizard HubWindow chat navigation — guarded by
527-
/// <see cref="SettingsManager.HasInjectedFirstRunBootstrap"/>.
528-
/// </summary>
529-
private async Task SendBootstrapMessageAsync()
530-
{
531-
if (_bootstrapSent || _chatWebView?.CoreWebView2 == null) return;
532-
_bootstrapSent = true;
533-
534-
await BootstrapMessageInjector.InjectAsync(
535-
script => _chatWebView.CoreWebView2.ExecuteScriptAsync(script).AsTask(),
536-
_settings);
537-
}
538-
539519
/// <summary>
540520
/// Captures the current window content to a PNG file.
541521
/// Called automatically on page navigation when OPENCLAW_VISUAL_TEST=1.

src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,23 @@
5656

5757
<!-- Loading -->
5858
<ProgressRing x:Name="LoadingRing" Grid.Row="1" IsActive="False"
59-
HorizontalAlignment="Center" VerticalAlignment="Center"/>
59+
HorizontalAlignment="Center" VerticalAlignment="Center"/>
60+
61+
<StackPanel x:Name="WaitingPanel" Grid.Row="1"
62+
Visibility="Collapsed"
63+
VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="12">
64+
<TextBlock Text="💬" FontSize="48" HorizontalAlignment="Center"/>
65+
<TextBlock Text="Waiting for chat to start…"
66+
Style="{StaticResource SubtitleTextBlockStyle}"
67+
HorizontalAlignment="Center"/>
68+
<TextBlock x:Name="WaitingStatusText"
69+
Text="The gateway is connected; the chat surface is still coming online."
70+
Style="{StaticResource CaptionTextBlockStyle}"
71+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
72+
HorizontalAlignment="Center" TextWrapping="Wrap" MaxWidth="360"/>
73+
<Button x:Name="RetryChatButton" Content="Retry" Click="OnRetryChat" Visibility="Collapsed"
74+
HorizontalAlignment="Center"/>
75+
</StackPanel>
6076

6177
<!-- Placeholder (shown when not connected) -->
6278
<StackPanel x:Name="PlaceholderPanel" Grid.Row="1"

0 commit comments

Comments
 (0)