diff --git a/CHANGELOG.md b/CHANGELOG.md index 93bc9570301..348a366e29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai - ACP: wait for the configured runtime backend to become healthy before startup identity reconciliation, avoiding transient acpx warnings during Gateway boot. Fixes #40566. - Channels/ACP bindings: time out configured binding readiness checks instead of letting Discord preflight hang forever when an ACP target never settles. Fixes #68776. - Control UI: hide the chat loading skeleton during background history reloads when existing messages or active stream content are already visible, avoiding reload flashes on high-latency local gateways. Fixes #71844. Thanks @WolvenRA. +- Control UI: keep locally optimistic chat messages visible when a history reload temporarily returns empty, avoiding lost first-turn messages on high-latency gateways. Fixes #71878. Thanks @WolvenRA. - Agents/images: scrub old `[media attached: ...]`, `[Image: source: ...]`, and `media://inbound/...` markers from pruned model replay context so stale media refs are not rehydrated as fresh prompt images. Fixes #71868. Thanks diff --git a/ui/src/ui/controllers/chat.test.ts b/ui/src/ui/controllers/chat.test.ts index 9f21e6c7567..beb2fe1d745 100644 --- a/ui/src/ui/controllers/chat.test.ts +++ b/ui/src/ui/controllers/chat.test.ts @@ -818,6 +818,33 @@ describe("loadChatHistory", () => { expect(state.chatStream).toBeNull(); }); + it("keeps local optimistic messages when history reload returns empty", async () => { + const optimisticUser = { + role: "user", + content: [{ type: "text", text: "first ask" }], + timestamp: 10, + }; + const optimisticAssistant = { + role: "assistant", + content: [{ type: "text", text: "first answer" }], + timestamp: 11, + }; + const request = vi.fn().mockResolvedValue({ + messages: [], + thinkingLevel: "low", + }); + const state = createState({ + connected: true, + client: { request } as unknown as ChatState["client"], + chatMessages: [optimisticUser, optimisticAssistant], + }); + + await loadChatHistory(state); + + expect(state.chatMessages).toEqual([optimisticUser, optimisticAssistant]); + expect(state.chatStream).toBeNull(); + }); + it("does not duplicate optimistic tail messages after history catches up", async () => { const optimisticUser = { role: "user", diff --git a/ui/src/ui/controllers/chat.ts b/ui/src/ui/controllers/chat.ts index ad7848bd7d7..5c29ae51944 100644 --- a/ui/src/ui/controllers/chat.ts +++ b/ui/src/ui/controllers/chat.ts @@ -268,9 +268,17 @@ function preserveOptimisticTailMessages( historyMessages: unknown[], previousMessages: unknown[], ): unknown[] { - if (historyMessages.length === 0 || previousMessages.length === 0) { + if (previousMessages.length === 0) { return historyMessages; } + if (historyMessages.length === 0) { + const optimisticMessages = previousMessages.filter( + (message) => isLocallyOptimisticHistoryMessage(message) && !shouldHideHistoryMessage(message), + ); + return optimisticMessages.length === previousMessages.length + ? previousMessages + : historyMessages; + } const historySignatures = new Set( historyMessages .map((message) => messageDisplaySignature(message))