Skip to content

Commit 5812984

Browse files
committed
fix(acp): drop [MODE_UPDATE] agent text echoes from chat
1 parent b566bc4 commit 5812984

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/supervisor/agents/acp/canonicalMapping.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ describe("mapAcpSessionUpdate", () => {
4141
expect((second[0] as { delta: string }).delta).toBe(" world");
4242
});
4343

44+
it("drops [MODE_UPDATE] agent text echoes — mode is chosen in the launcher, not chat", () => {
45+
// Gemini's ACP server emits `[MODE_UPDATE] <mode>` as a fresh
46+
// agent_message_chunk every time a session starts (or switches) into a
47+
// specific approval mode. The user already picked that mode in the
48+
// launcher UI; replaying it as a chat message on every turn is pure
49+
// noise, so the mapper must drop the chunk before opening an assistant
50+
// item.
51+
const state = createAcpMapperState("t-mode");
52+
const events = mapAcpSessionUpdate(
53+
note({
54+
sessionUpdate: "agent_message_chunk",
55+
content: { type: "text", text: "[MODE_UPDATE] yolo" },
56+
}),
57+
state,
58+
);
59+
expect(events).toEqual([]);
60+
expect(state.openAssistantItemId).toBeUndefined();
61+
});
62+
4463
it("drops user_message_chunk echoes — supervisor/renderer own the user_message item", () => {
4564
// Some ACP servers (Copilot) echo the user's prompt back as
4665
// `user_message_chunk` updates after we send `session/prompt`. The

src/supervisor/agents/acp/canonicalMapping.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ export function mapAcpSessionUpdate(
169169

170170
switch (update.sessionUpdate) {
171171
case "agent_message_chunk": {
172+
const content = (update as { content?: ContentBlock }).content;
173+
// Gemini echoes `[MODE_UPDATE] <mode>` as an agent text chunk whenever the
174+
// session is launched (or switched) into a specific approval mode. The
175+
// user already chose the mode in the launcher; surfacing the echo as
176+
// chat noise on every turn is just clutter. Drop it before we open an
177+
// assistant item so the chat stays clean.
178+
if (
179+
!state.openAssistantItemId &&
180+
content?.type === "text" &&
181+
/^\[MODE_UPDATE\]/.test(content.text)
182+
) {
183+
break;
184+
}
172185
// Open an assistant item on first chunk; emit deltas thereafter.
173186
if (!state.openAssistantItemId) {
174187
// Close any prior reasoning/user items — assistant is starting fresh.
@@ -181,7 +194,6 @@ export function mapAcpSessionUpdate(
181194
itemType: "assistant_message",
182195
});
183196
}
184-
const content = (update as { content?: ContentBlock }).content;
185197
if (content) {
186198
if (content.type === "text") {
187199
events.push({

0 commit comments

Comments
 (0)