diff --git a/components/canvas/chat-panel/canvas-chat-media.ts b/components/canvas/chat-panel/canvas-chat-media.ts index 0d022f2d6..88d2e2269 100644 --- a/components/canvas/chat-panel/canvas-chat-media.ts +++ b/components/canvas/chat-panel/canvas-chat-media.ts @@ -381,11 +381,15 @@ export function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMess .sort((a, b) => { const messageA = a.message; const messageB = b.message; - const timeA = getMessageTimestamp(messageA) ?? 0; - const timeB = getMessageTimestamp(messageB) ?? 0; - if (timeA !== timeB) return timeA - timeB; + const timeA = getMessageTimestamp(messageA); + const timeB = getMessageTimestamp(messageB); + const hasTimeA = timeA !== null; + const hasTimeB = timeB !== null; - if (messageA.role !== messageB.role) { + if (hasTimeA && hasTimeB && timeA !== timeB) return timeA - timeB; + if (hasTimeA !== hasTimeB) return hasTimeA ? -1 : 1; + + if (hasTimeA && hasTimeB && messageA.role !== messageB.role) { if (messageA.role === "user") return -1; if (messageB.role === "user") return 1; } diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index d51b0caec..6edd96b14 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -4,6 +4,7 @@ import { extractAssistantImageUrls, extractVideoUrls, normalizeMediaUrl, + sortCanvasMessagesByCreatedAt, stripMediaMarkup, } from "@/components/canvas/chat-panel/canvas-chat-media"; import { @@ -211,6 +212,54 @@ import type { CanvasAgentContext } from "@/types/canvas/agent-protocol"; import type { ChatMessage } from "@/lib/types"; test.describe("canvas chat pure helpers", () => { + test("keeps optimistic messages without timestamps after persisted history", () => { + const messages = [ + { + id: "optimistic-latest-user", + role: "user", + parts: [{ type: "text", text: "你能为我做什么?列出表给我" }], + }, + { + id: "persisted-first-user", + role: "user", + createdAt: "2026-07-07T01:00:00.000Z", + parts: [{ type: "text", text: "你好" }], + }, + { + id: "persisted-first-assistant", + role: "assistant", + createdAt: "2026-07-07T01:00:01.000Z", + parts: [{ type: "text", text: "你好,我在。" }], + }, + ] as ChatMessage[]; + + expect(sortCanvasMessagesByCreatedAt(messages).map((message) => message.id)).toEqual([ + "persisted-first-user", + "persisted-first-assistant", + "optimistic-latest-user", + ]); + }); + + test("preserves relative order among messages without timestamps", () => { + const messages = [ + { + id: "pending-user", + role: "user", + parts: [{ type: "text", text: "生成一张图" }], + }, + { + id: "pending-assistant", + role: "assistant", + parts: [{ type: "text", text: "分析中" }], + }, + ] as ChatMessage[]; + + expect(sortCanvasMessagesByCreatedAt(messages).map((message) => message.id)).toEqual([ + "pending-user", + "pending-assistant", + ]); + }); + test("formats uploaded spreadsheet rows as safe markdown tables for OpenClaw", () => { const markdown = rowsToOpenClawMarkdownTable([ ["名称", "备注"],