Fix optimistic user message reconciliation
This commit is contained in:
@@ -326,7 +326,7 @@ describe("processTimelineResponse", () => {
|
|||||||
...makeTimelineEntry(1, "Analyze this", "user_message"),
|
...makeTimelineEntry(1, "Analyze this", "user_message"),
|
||||||
item: {
|
item: {
|
||||||
type: "user_message",
|
type: "user_message",
|
||||||
text: "Analyze this",
|
text: "server-rendered attachment text",
|
||||||
messageId: "canonical-create-user",
|
messageId: "canonical-create-user",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -339,6 +339,7 @@ describe("processTimelineResponse", () => {
|
|||||||
expect(userMessages[0]).toMatchObject({
|
expect(userMessages[0]).toMatchObject({
|
||||||
id: "canonical-create-user",
|
id: "canonical-create-user",
|
||||||
text: "Analyze this",
|
text: "Analyze this",
|
||||||
|
timestamp: new Date(1000),
|
||||||
images: [image],
|
images: [image],
|
||||||
attachments: [attachment],
|
attachments: [attachment],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -295,16 +295,15 @@ function mergeCanonicalUserWithOptimistic(
|
|||||||
canonical: UserMessageItem,
|
canonical: UserMessageItem,
|
||||||
optimistic: UserMessageItem,
|
optimistic: UserMessageItem,
|
||||||
): UserMessageItem {
|
): UserMessageItem {
|
||||||
const shouldPreserveImages =
|
|
||||||
(!canonical.images || canonical.images.length === 0) &&
|
|
||||||
Boolean(optimistic.images && optimistic.images.length > 0);
|
|
||||||
const shouldPreserveAttachments =
|
|
||||||
(!canonical.attachments || canonical.attachments.length === 0) &&
|
|
||||||
Boolean(optimistic.attachments && optimistic.attachments.length > 0);
|
|
||||||
return {
|
return {
|
||||||
...canonical,
|
kind: "user_message",
|
||||||
...(shouldPreserveImages ? { images: optimistic.images } : {}),
|
id: canonical.id,
|
||||||
...(shouldPreserveAttachments ? { attachments: optimistic.attachments } : {}),
|
text: optimistic.text,
|
||||||
|
timestamp: optimistic.timestamp,
|
||||||
|
...(optimistic.images && optimistic.images.length > 0 ? { images: optimistic.images } : {}),
|
||||||
|
...(optimistic.attachments && optimistic.attachments.length > 0
|
||||||
|
? { attachments: optimistic.attachments }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -724,6 +724,7 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
|
|
||||||
it("preserves optimistic user message images when authoritative user message arrives", () => {
|
it("preserves optimistic user message images when authoritative user message arrives", () => {
|
||||||
const messageId = "msg-user-images";
|
const messageId = "msg-user-images";
|
||||||
|
const optimisticTimestamp = new Date("2025-01-01T11:10:00Z");
|
||||||
const optimisticImages = [
|
const optimisticImages = [
|
||||||
{
|
{
|
||||||
id: "att-optimistic",
|
id: "att-optimistic",
|
||||||
@@ -738,7 +739,7 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
kind: "user_message",
|
kind: "user_message",
|
||||||
id: messageId,
|
id: messageId,
|
||||||
text: "Analyze this image",
|
text: "Analyze this image",
|
||||||
timestamp: new Date("2025-01-01T11:10:00Z"),
|
timestamp: optimisticTimestamp,
|
||||||
optimistic: true,
|
optimistic: true,
|
||||||
images: optimisticImages,
|
images: optimisticImages,
|
||||||
},
|
},
|
||||||
@@ -760,7 +761,8 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
assert.ok(message);
|
assert.ok(message);
|
||||||
assert.strictEqual(message.id, messageId);
|
assert.strictEqual(message.id, messageId);
|
||||||
assert.deepStrictEqual(message.images, optimisticImages);
|
assert.deepStrictEqual(message.images, optimisticImages);
|
||||||
assert.strictEqual(message.timestamp.getTime(), authoritativeTimestamp.getTime());
|
assert.strictEqual(message.text, "Analyze this image");
|
||||||
|
assert.strictEqual(message.timestamp.getTime(), optimisticTimestamp.getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps canonical assistant/user/assistant order during replay", () => {
|
it("keeps canonical assistant/user/assistant order during replay", () => {
|
||||||
@@ -991,7 +993,9 @@ describe("turn lifecycle events", () => {
|
|||||||
const userMessage = userMessages[0];
|
const userMessage = userMessages[0];
|
||||||
invariant(userMessage?.kind === "user_message");
|
invariant(userMessage?.kind === "user_message");
|
||||||
assert.strictEqual(userMessage.id, "provider-owned-id");
|
assert.strictEqual(userMessage.id, "provider-owned-id");
|
||||||
assert.strictEqual(userMessage.text, "server-owned rendered text");
|
assert.strictEqual(userMessage.text, optimistic.text);
|
||||||
|
assert.strictEqual(userMessage.timestamp.getTime(), optimistic.timestamp.getTime());
|
||||||
|
assert.strictEqual(userMessage.optimistic, undefined);
|
||||||
assert.deepStrictEqual(userMessage.images, optimistic.images);
|
assert.deepStrictEqual(userMessage.images, optimistic.images);
|
||||||
assert.deepStrictEqual(userMessage.attachments, optimistic.attachments);
|
assert.deepStrictEqual(userMessage.attachments, optimistic.attachments);
|
||||||
},
|
},
|
||||||
@@ -1029,6 +1033,7 @@ describe("turn lifecycle events", () => {
|
|||||||
invariant(userMessage?.kind === "user_message");
|
invariant(userMessage?.kind === "user_message");
|
||||||
assert.strictEqual(userMessage.id, "msg_opencode_provider_owned");
|
assert.strictEqual(userMessage.id, "msg_opencode_provider_owned");
|
||||||
assert.strictEqual(userMessage.text, "typed plain text");
|
assert.strictEqual(userMessage.text, "typed plain text");
|
||||||
|
assert.strictEqual(userMessage.timestamp.getTime(), optimisticTimestamp.getTime());
|
||||||
assert.strictEqual(userMessage.optimistic, undefined);
|
assert.strictEqual(userMessage.optimistic, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1062,7 +1067,7 @@ describe("turn lifecycle events", () => {
|
|||||||
provider: "claude",
|
provider: "claude",
|
||||||
item: {
|
item: {
|
||||||
type: "user_message",
|
type: "user_message",
|
||||||
text: "Analyze this",
|
text: "server-rendered attachment text",
|
||||||
messageId: "provider-owned-canonical",
|
messageId: "provider-owned-canonical",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1075,6 +1080,8 @@ describe("turn lifecycle events", () => {
|
|||||||
const userMessage = userMessages[0];
|
const userMessage = userMessages[0];
|
||||||
invariant(userMessage?.kind === "user_message");
|
invariant(userMessage?.kind === "user_message");
|
||||||
assert.strictEqual(userMessage.id, "provider-owned-canonical");
|
assert.strictEqual(userMessage.id, "provider-owned-canonical");
|
||||||
|
assert.strictEqual(userMessage.text, "Analyze this");
|
||||||
|
assert.strictEqual(userMessage.timestamp.getTime(), optimisticTimestamp.getTime());
|
||||||
assert.strictEqual(userMessage.optimistic, undefined);
|
assert.strictEqual(userMessage.optimistic, undefined);
|
||||||
assert.deepStrictEqual(userMessage.images, [image]);
|
assert.deepStrictEqual(userMessage.images, [image]);
|
||||||
assert.deepStrictEqual(userMessage.attachments, [attachment]);
|
assert.deepStrictEqual(userMessage.attachments, [attachment]);
|
||||||
@@ -1212,8 +1219,8 @@ describe("turn lifecycle events", () => {
|
|||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
userMessages.map((item) => [item.id, item.text, item.optimistic]),
|
userMessages.map((item) => [item.id, item.text, item.optimistic]),
|
||||||
[
|
[
|
||||||
["provider-owned-first", "first server text", undefined],
|
["provider-owned-first", "first typed text", undefined],
|
||||||
["provider-owned-second", "second server text", undefined],
|
["provider-owned-second", "second typed text", undefined],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -201,18 +201,26 @@ function buildUserMessageItem(input: {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
optimistic?: UserMessageItem | null;
|
optimistic?: UserMessageItem | null;
|
||||||
}): UserMessageItem {
|
}): UserMessageItem {
|
||||||
const preservedImages = input.optimistic?.images;
|
if (input.optimistic) {
|
||||||
const preservedAttachments = input.optimistic?.attachments;
|
return {
|
||||||
|
kind: "user_message",
|
||||||
|
id: input.id,
|
||||||
|
text: input.optimistic.text,
|
||||||
|
timestamp: input.optimistic.timestamp,
|
||||||
|
...(input.optimistic.images && input.optimistic.images.length > 0
|
||||||
|
? { images: input.optimistic.images }
|
||||||
|
: {}),
|
||||||
|
...(input.optimistic.attachments && input.optimistic.attachments.length > 0
|
||||||
|
? { attachments: input.optimistic.attachments }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "user_message",
|
kind: "user_message",
|
||||||
id: input.id,
|
id: input.id,
|
||||||
text: input.text,
|
text: input.text,
|
||||||
timestamp: input.timestamp,
|
timestamp: input.timestamp,
|
||||||
...(preservedImages && preservedImages.length > 0 ? { images: preservedImages } : {}),
|
|
||||||
...(preservedAttachments && preservedAttachments.length > 0
|
|
||||||
? { attachments: preservedAttachments }
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user