fix(app): settle attachment-only submissions

Canonical providers can acknowledge image-only prompts with empty text. Reconcile those events by client identity without rendering a blank canonical row.
This commit is contained in:
Mohamed Boudra
2026-07-27 18:39:30 +02:00
parent 5794ec6c27
commit 1464818fc1
2 changed files with 13 additions and 7 deletions

View File

@@ -153,6 +153,16 @@ async function submitMessageWithImage(page: Page, prompt: string): Promise<Locat
return page.getByTestId("user-message").filter({ hasText: prompt }).last();
}
async function submitImageOnlyMessage(page: Page): Promise<Locator> {
await attachImageFromMenu(page, IMAGE);
await expectAttachmentPill(page, "composer-image-attachment-pill");
await page.getByRole("textbox", { name: "Message agent..." }).first().press("Enter");
const userMessage = page.getByTestId("user-message").last();
await expect(userMessage).toBeVisible();
await expect(userMessage.getByRole("button", { name: "Open image attachment" })).toBeVisible();
return userMessage;
}
async function expectPendingSubmission(page: Page, userMessage: Locator): Promise<void> {
await expect(userMessage).toBeVisible();
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
@@ -312,13 +322,12 @@ async function expectCompletedSubmissionClearsAfterMissedRunningTransition(
title: "Submission missed running transition",
model: "ten-second-stream",
});
const prompt = "Clear this submission from canonical history.";
try {
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
await expectComposerVisible(page);
await expectAgentIdle(page);
gate.holdNextClientRequest("send_agent_message_request");
const userMessage = await submitMessageWithImage(page, prompt);
const userMessage = await submitImageOnlyMessage(page);
await gate.waitForHeldClientRequest();
gate.setServerMessageSuppressed("agent_status", true);
gate.setServerMessageSuppressed("agent_update", true);
@@ -591,7 +600,7 @@ test.describe("Agent message submission", () => {
await expectInterruptedTurnOrderAfterReconnect(page, testInfo);
});
test("clears an accepted submission when canonical history arrives after a missed running transition", async ({
test("clears an attachment-only submission when canonical history arrives after a missed running transition", async ({
page,
}, testInfo) => {
test.setTimeout(90_000);

View File

@@ -1498,9 +1498,6 @@ function applyCanonicalUserMessageEvent(params: {
const { tail, head, event, timestamp } = params;
if (event.type !== "timeline" || event.item.type !== "user_message") return null;
const normalized = normalizeChunk(event.item.text);
if (!normalized.hasContent) {
return { tail, head, changedTail: false, changedHead: false };
}
const flushedTail = head.length > 0 ? flushHeadToTail(tail, head) : tail;
const flushedHead = head.length > 0 ? [] : head;
@@ -1517,7 +1514,7 @@ function applyCanonicalUserMessageEvent(params: {
tail: flushedTail,
head: flushedHead,
message: canonical,
insert: "tail",
insert: normalized.hasContent ? "tail" : "none",
presentation: "existing",
});
return {