Skip to content

Commit 17edc51

Browse files
saltboCopilot
andcommitted
fix(providers): emit block.start with empty text to avoid duplicate messages
RelayRuntimeProvider's updateOrAppend() searches backwards for a part with text=="" to fill in on block.done. Copilot was emitting block.start with full text, causing block.done to find no empty slot and append a second identical message. Fix: emit block.start with text="" for text/thinking blocks, and block.done with the full content — matching the streaming contract Claude uses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e2277b5 commit 17edc51

2 files changed

Lines changed: 12 additions & 13 deletions

File tree

packages/cli/src/providers/copilot.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,8 @@ export function* mapCopilotEvent(event: SessionEvent, state: MapState): Generato
108108
case "assistant.reasoning": {
109109
const text = event.data.content;
110110
if (text) {
111-
const block: ContentBlock = { type: "thinking", text };
112-
yield { type: "block.start", block };
113-
yield { type: "block.done", block };
111+
yield { type: "block.start", block: { type: "thinking", text: "" } };
112+
yield { type: "block.done", block: { type: "thinking", text } };
114113
}
115114
return;
116115
}
@@ -124,15 +123,15 @@ export function* mapCopilotEvent(event: SessionEvent, state: MapState): Generato
124123
}
125124

126125
if (reasoningText) {
127-
const block: ContentBlock = { type: "thinking", text: reasoningText };
128-
yield { type: "block.start", block };
129-
yield { type: "block.done", block };
126+
// block.start with empty text → block.done with full text, matching the streaming
127+
// contract that RelayRuntimeProvider expects (it fills in text on block.done)
128+
yield { type: "block.start", block: { type: "thinking", text: "" } };
129+
yield { type: "block.done", block: { type: "thinking", text: reasoningText } };
130130
}
131131

132132
if (content) {
133-
const block: ContentBlock = { type: "text", text: content };
134-
yield { type: "block.start", block };
135-
yield { type: "block.done", block };
133+
yield { type: "block.start", block: { type: "text", text: "" } };
134+
yield { type: "block.done", block: { type: "text", text: content } };
136135
}
137136

138137
if (toolRequests) {

tests/providers/copilot.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe("assistant.reasoning", () => {
6464
const event = { type: "assistant.reasoning", data: { content: "I am thinking" } } as SessionEvent;
6565
const events = collect(mapCopilotEvent(event, state));
6666
expect(events).toEqual([
67-
{ type: "block.start", block: { type: "thinking", text: "I am thinking" } },
67+
{ type: "block.start", block: { type: "thinking", text: "" } },
6868
{ type: "block.done", block: { type: "thinking", text: "I am thinking" } },
6969
]);
7070
});
@@ -107,7 +107,7 @@ describe("assistant.message with content only", () => {
107107
const state = makeState({ turnOpen: true });
108108
const event = { type: "assistant.message", data: { content: "Hello" } } as SessionEvent;
109109
const events = collect(mapCopilotEvent(event, state));
110-
expect(events).toContainEqual({ type: "block.start", block: { type: "text", text: "Hello" } });
110+
expect(events).toContainEqual({ type: "block.start", block: { type: "text", text: "" } });
111111
expect(events).toContainEqual({ type: "block.done", block: { type: "text", text: "Hello" } });
112112
});
113113

@@ -143,7 +143,7 @@ describe("assistant.message with reasoningText and content", () => {
143143
} as SessionEvent;
144144
const events = collect(mapCopilotEvent(event, state)) as { type: string; block?: { type: string; text: string } }[];
145145
const thinkingStart = events.find((e) => e.type === "block.start" && e.block?.type === "thinking");
146-
expect(thinkingStart?.block?.text).toBe("Thinking...");
146+
expect(thinkingStart?.block?.text).toBe("");
147147
});
148148

149149
it("emits block.start text with content", () => {
@@ -154,7 +154,7 @@ describe("assistant.message with reasoningText and content", () => {
154154
} as SessionEvent;
155155
const events = collect(mapCopilotEvent(event, state)) as { type: string; block?: { type: string; text: string } }[];
156156
const textStart = events.find((e) => e.type === "block.start" && e.block?.type === "text");
157-
expect(textStart?.block?.text).toBe("Answer");
157+
expect(textStart?.block?.text).toBe("");
158158
});
159159
});
160160

0 commit comments

Comments
 (0)