Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ public enum ChatPreviewState
Thinking,
/// <summary>Composer shows the tool-permission banner with Allow/Deny.</summary>
PendingPermission,
/// <summary>
/// Force the pre-connect "Reconnecting to your last conversation…"
/// banner that <see cref="OpenClaw.Tray.WinUI.Chat.OpenClawChatRoot"/>
/// renders when <c>effectiveThread</c> is null because the gateway
/// handshake hasn't completed yet. Lets designers see the banner
/// without having to actually disconnect.
/// </summary>
Reconnecting,
}

public enum ChatComposerLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ public override Element Render()
ChatPreviewState.Loading,
ChatPreviewState.Empty,
ChatPreviewState.Thinking,
ChatPreviewState.PendingPermission)
ChatPreviewState.PendingPermission,
ChatPreviewState.Reconnecting)
);

// ── H. Tool burst (multi-step task framing) ──────────────────
Expand Down
32 changes: 29 additions & 3 deletions src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ public sealed class OpenClawChatDataProvider : IChatDataProvider
// <see cref="ChatTimelineItem"/> record.
private readonly Dictionary<string, Dictionary<string, ChatEntryMetadata>> _entryMeta = new();
private SessionInfo[] _sessions = Array.Empty<SessionInfo>();
// True once the gateway has delivered a sessions list (even an empty
// one) for the current connection. Used to gate the synthetic
// compose-only thread so the UI doesn't briefly render the welcome
// zero-state in the window between hello-ok (HasHandshakeSnapshot)
// and the first sessions.list — at that point the gateway may still
// be about to deliver real sessions for a returning user. Reset to
// false on disconnect alongside `_status`.
private bool _sessionsListReceived;
private string[] _availableModels = Array.Empty<string>();
private ConnectionStatus _status;
private bool _disposed;
Expand Down Expand Up @@ -852,6 +860,13 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
&& _status == ConnectionStatus.Connected;
_status = status;

// Reset the sessions-list-received gate whenever we leave the
// Connected state. Any cached sessions belong to the previous
// connection; the UI must treat the composer as not-yet-ready
// until the next sessions.list arrives.
if (status != ConnectionStatus.Connected)
_sessionsListReceived = false;

// On (re)connect, reload any thread that either previously loaded
// successfully or has a timeline but never completed loading.
// The second case covers initial connect: the UI may have created
Expand Down Expand Up @@ -920,6 +935,7 @@ private void OnSessionsUpdated(object? sender, SessionInfo[] sessions)
lock (_gate)
{
_sessions = sessions ?? Array.Empty<SessionInfo>();
_sessionsListReceived = true;
EnsureTimelinesForSessionsLocked();
snapshot = BuildSnapshotLocked();

Expand Down Expand Up @@ -2101,7 +2117,12 @@ private ChatTimelineState GetOrCreateTimelineLocked(string threadId)
{
if (!_timelines.TryGetValue(threadId, out var current))
{
current = ChatTimelineState.Initial() with { HistoryLoaded = true };
// HistoryLoaded stays false until LoadHistoryAsync rebuilds
// the timeline from the gateway. The UI relies on this flag
// to distinguish "session exists, history still fetching"
// (show reconnecting view) from "session truly empty"
// (show welcome zero-state).
current = ChatTimelineState.Initial();
_timelines[threadId] = current;
}
return current;
Expand All @@ -2113,7 +2134,7 @@ private void EnsureTimelinesForSessionsLocked()
{
if (string.IsNullOrEmpty(s.Key)) continue;
if (!_timelines.ContainsKey(s.Key))
_timelines[s.Key] = ChatTimelineState.Initial() with { HistoryLoaded = true };
_timelines[s.Key] = ChatTimelineState.Initial();
}
}

Expand All @@ -2131,7 +2152,12 @@ private ChatDataSnapshot BuildSnapshotLocked()
var composeKey = _bridge.MainSessionKey;
var composeReady = _bridge.HasHandshakeSnapshot
&& !string.IsNullOrWhiteSpace(composeKey)
&& _status == ConnectionStatus.Connected;
&& _status == ConnectionStatus.Connected
// Wait until sessions.list has been delivered for this
// connection — otherwise the UI may synthesize a compose-only
// thread (and render the welcome zero-state) in the brief
// window before a returning user's real sessions arrive.
&& _sessionsListReceived;

// If the compose target hasn't materialized as a real session yet but
// already has an optimistic timeline (because the user sent a message
Expand Down
Loading
Loading