From dbab3a3d632a6d2a0fb15e1d2ac5a66f3bc85f13 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Thu, 23 Apr 2026 11:07:01 -0700 Subject: [PATCH] fix(ui): drop assistant-only ACP text chunks Signed-off-by: Taylor Ho --- .../shared/api/acpNotificationHandler.test.ts | 53 ++++--- .../src/shared/api/acpNotificationHandler.ts | 149 ++++++++++-------- ui/goose2/src/shared/api/textContentBlocks.ts | 7 + 3 files changed, 121 insertions(+), 88 deletions(-) diff --git a/ui/goose2/src/shared/api/acpNotificationHandler.test.ts b/ui/goose2/src/shared/api/acpNotificationHandler.test.ts index 2d36f2cbd8..9cb6df6b02 100644 --- a/ui/goose2/src/shared/api/acpNotificationHandler.test.ts +++ b/ui/goose2/src/shared/api/acpNotificationHandler.test.ts @@ -333,7 +333,7 @@ describe("acpNotificationHandler", () => { ]); }); - it("preserves live text annotations and splits chunks when audience changes", async () => { + it("drops assistant-only live text chunks while preserving visible annotations", async () => { registerSession("draft-session-5", "goose-session-5", "goose", "/tmp"); await handleSessionNotification({ @@ -358,9 +358,9 @@ describe("acpNotificationHandler", () => { messageId: "message-1", content: { type: "text", - text: "prompt", + text: "Visible ", annotations: { - audience: ["assistant"], + audience: ["user"], }, }, }, @@ -373,7 +373,10 @@ describe("acpNotificationHandler", () => { messageId: "message-1", content: { type: "text", - text: "Visible reply", + text: "reply", + annotations: { + audience: ["user"], + }, }, }, } as SessionNotification); @@ -382,21 +385,17 @@ describe("acpNotificationHandler", () => { useChatStore.getState().messagesBySession["draft-session-5"]?.[0] ?.content, ).toEqual([ - { - type: "text", - text: "internal prompt", - annotations: { - audience: ["assistant"], - }, - }, { type: "text", text: "Visible reply", + annotations: { + audience: ["user"], + }, }, ]); }); - it("preserves replay text annotations on user message chunks", async () => { + it("drops assistant-only replay text chunks while preserving visible annotations", async () => { registerSession("draft-session-6", "goose-session-6", "goose", "/tmp"); useChatStore.setState({ loadingSessionIds: new Set(["draft-session-6"]), @@ -424,23 +423,37 @@ describe("acpNotificationHandler", () => { messageId: "message-1", content: { type: "text", - text: "Visible prompt", + text: "Visible ", + annotations: { + audience: ["user"], + }, + }, + }, + } as SessionNotification); + + await handleSessionNotification({ + sessionId: "goose-session-6", + update: { + sessionUpdate: "user_message_chunk", + messageId: "message-1", + content: { + type: "text", + text: "prompt", + annotations: { + audience: ["user"], + }, }, }, } as SessionNotification); const buffer = getAndDeleteReplayBuffer("draft-session-6"); expect(buffer?.[0]?.content).toEqual([ - { - type: "text", - text: "internal prompt", - annotations: { - audience: ["assistant"], - }, - }, { type: "text", text: "Visible prompt", + annotations: { + audience: ["user"], + }, }, ]); }); diff --git a/ui/goose2/src/shared/api/acpNotificationHandler.ts b/ui/goose2/src/shared/api/acpNotificationHandler.ts index 8f220b77a7..a5e9b3ada9 100644 --- a/ui/goose2/src/shared/api/acpNotificationHandler.ts +++ b/ui/goose2/src/shared/api/acpNotificationHandler.ts @@ -29,6 +29,7 @@ import { } from "./toolCallBlockBuilders"; import { appendTextContent, + isUserVisibleTextAnnotations, normalizeTextAnnotations, } from "./textContentBlocks"; @@ -180,61 +181,69 @@ export function clearReplayPerf(sessionId: string): void { function handleReplay(sessionId: string, update: SessionUpdate): void { switch (update.sessionUpdate) { case "agent_message_chunk": { - const messageId = update.messageId ?? crypto.randomUUID(); - const buffer = ensureReplayBuffer(sessionId); - if (!getBufferedMessage(sessionId, messageId)) { - buffer.push({ - id: messageId, - role: "assistant", - created: Date.now(), - content: [], - metadata: { - userVisible: true, - agentVisible: true, - completionStatus: "inProgress", - }, - }); - } - const msg = getBufferedMessage(sessionId, messageId); - if (msg && update.content.type === "text" && "text" in update.content) { + if (update.content.type === "text" && "text" in update.content) { + const annotations = normalizeTextAnnotations( + update.content.annotations, + ); + if (!isUserVisibleTextAnnotations(annotations)) { + break; + } + + const messageId = update.messageId ?? crypto.randomUUID(); + const buffer = ensureReplayBuffer(sessionId); + if (!getBufferedMessage(sessionId, messageId)) { + buffer.push({ + id: messageId, + role: "assistant", + created: Date.now(), + content: [], + metadata: { + userVisible: true, + agentVisible: true, + completionStatus: "inProgress", + }, + }); + } + const msg = getBufferedMessage(sessionId, messageId); + if (!msg) { + break; + } msg.content = appendTextContent( msg.content, update.content.text, - normalizeTextAnnotations(update.content.annotations), + annotations, ); } break; } case "user_message_chunk": { - const messageId = update.messageId ?? crypto.randomUUID(); - const buffer = ensureReplayBuffer(sessionId); - const existing = getBufferedMessage(sessionId, messageId); - if ( - !existing && - update.content.type === "text" && - "text" in update.content - ) { + if (update.content.type === "text" && "text" in update.content) { const annotations = normalizeTextAnnotations( update.content.annotations, ); - buffer.push({ - id: messageId, - role: "user", - created: Date.now(), - content: [{ type: "text", text: update.content.text, annotations }], - metadata: { userVisible: true, agentVisible: true }, - }); - } else if ( - existing && - update.content.type === "text" && - "text" in update.content - ) { - existing.content = appendTextContent( - existing.content, - update.content.text, - normalizeTextAnnotations(update.content.annotations), - ); + if (!isUserVisibleTextAnnotations(annotations)) { + break; + } + + const messageId = update.messageId ?? crypto.randomUUID(); + const buffer = ensureReplayBuffer(sessionId); + const existing = getBufferedMessage(sessionId, messageId); + if (!existing) { + buffer.push({ + id: messageId, + role: "user", + created: Date.now(), + content: [{ type: "text", text: update.content.text, annotations }], + metadata: { userVisible: true, agentVisible: true }, + }); + } else { + existing.content = appendTextContent( + existing.content, + update.content.text, + annotations, + ); + } } break; } @@ -299,35 +308,39 @@ function handleLive( switch (update.sessionUpdate) { case "agent_message_chunk": { - const messageId = - update.messageId ?? - presetMessageIds.get(gooseSessionId) ?? - crypto.randomUUID(); - const existing = store.messagesBySession[sessionId]?.find( - (m) => m.id === messageId, - ); - - if (!existing) { - store.addMessage(sessionId, { - id: messageId, - role: "assistant", - created: Date.now(), - content: [], - metadata: { - userVisible: true, - agentVisible: true, - completionStatus: "inProgress", - }, - }); - store.setPendingAssistantProvider(sessionId, null); - store.setStreamingMessageId(sessionId, messageId); - } - if (update.content.type === "text" && "text" in update.content) { - const text = update.content.text; const annotations = normalizeTextAnnotations( update.content.annotations, ); + if (!isUserVisibleTextAnnotations(annotations)) { + break; + } + + const messageId = + update.messageId ?? + presetMessageIds.get(gooseSessionId) ?? + crypto.randomUUID(); + const existing = store.messagesBySession[sessionId]?.find( + (m) => m.id === messageId, + ); + + if (!existing) { + store.addMessage(sessionId, { + id: messageId, + role: "assistant", + created: Date.now(), + content: [], + metadata: { + userVisible: true, + agentVisible: true, + completionStatus: "inProgress", + }, + }); + store.setPendingAssistantProvider(sessionId, null); + store.setStreamingMessageId(sessionId, messageId); + } + + const text = update.content.text; store.setStreamingMessageId(sessionId, messageId); store.updateMessage(sessionId, messageId, (msg) => ({ ...msg, diff --git a/ui/goose2/src/shared/api/textContentBlocks.ts b/ui/goose2/src/shared/api/textContentBlocks.ts index 31d03b761c..37522643e7 100644 --- a/ui/goose2/src/shared/api/textContentBlocks.ts +++ b/ui/goose2/src/shared/api/textContentBlocks.ts @@ -24,6 +24,13 @@ export function normalizeTextAnnotations( : undefined; } +export function isUserVisibleTextAnnotations( + annotations?: ContentAnnotations, +): boolean { + const audience = annotations?.audience; + return !audience || audience.length === 0 || audience.includes("user"); +} + function sameTextAnnotations( left?: ContentAnnotations, right?: ContentAnnotations,