fix(ui): drop assistant-only ACP text chunks

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-04-23 11:07:01 -07:00
parent ad22d63a61
commit dbab3a3d63
3 changed files with 121 additions and 88 deletions
@@ -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"],
},
},
]);
});
@@ -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,
@@ -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,