Extract shared "segment separator" helper to prevent fused assistant text across SDK integrations
@ulugbekna is already working on this.
Since Jun 4, 2026.
Assessment
This issue has not been assessed yet.
Description
Background
We've now shipped the same fix to the same bug pattern twice:
- OpenAI Responses API ([#312173](https://github.com/microsoft/vscode/pull/
lastTextDeltaOutputIndexinextensions/copilot/src/platform/endpoint/node/responsesApi.ts312173)) - Copilot CLI ([#319727](https://github.com/microsoft/vscode/pull/
lastEmittedAssistantMessageIdinextensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts319727))
In both cases the symptom in the Agents window chat was the same: consecutive assistant text items fused into one run-on paragraph (e.g. "...wiring:Now add..." instead of two paragraphs).
The Claude Code SDK path (extensions/copilot/src/extension/chatSessions/claude/common/claudeMessageDispatch.ts) likely has the same latent multiple text content blocks in one SDKAssistantMessage (or consecutive messages in one turn) are forwarded via stream.markdown(item.text) with no but no user-visible report has come in yet.separator bug
Why this keeps happening
- Impedance mismatch. Every model SDK models a turn as discrete items (
content_blocks,output_index-tagged events,messageId-tagged messages).vscode.ChatResponseStream.markdown(...)is a purely concatenative sink. The natural way to write the handler (for (item) stream.markdown(item.text)) silently fuses items. - Hand-rolled each time. No shared helper exists, so each new integration reinvents the same ~5-line state or forgets to.machine
- Hides in dev testing. Only surfaces when the model emits multiple text items in one turn; short answers don't trigger it. There is no invariant test that would catch the omission at PR review or CI time.
Proposal
Extract a tiny, sink-agnostic segment-boundary helper, then refactor both existing call sites onto it. Sketch:
// e.g. extensions/copilot/src/util/common/markdownSegmentSeparator.ts
export class MarkdownSegmentSeparator {
private lastSegmentKey: string | number | undefined;
constructor(private readonly emitSeparator: () => void) { }
/** Call before emitting text from `segmentKey`. Emits `\n\n` if the
* segment has changed and this is not the first emission. Defined-on-both
* keys are compared; otherwise no separator is emitted (legacy fallback). */
onSegment(segmentKey: string | number | undefined): void {
if (
segmentKey !== undefined &&
this.lastSegmentKey !== undefined &&
segmentKey !== this.lastSegmentKey
) {
this.emitSeparator();
}
if (segmentKey !== undefined) {
this.lastSegmentKey = segmentKey;
}
}
/** Call at request/turn boundaries to avoid spurious separators on the next turn. */
reset(): void { this.lastSegmentKey = undefined; }
}
Call sites become:
// Copilot CLI (assistant.message_delta / assistant.message)
const sep = new MarkdownSegmentSeparator(() => requestStream?.markdown('\n\n'));
// ... in handler:
sep.onSegment(event.data.messageId);
requestStream?.markdown(event.data.deltaContent);
// OpenAI Responses API (response.output_text.delta)
const sep = new MarkdownSegmentSeparator(() => onProgress({ text: '\n\n' }));
// ... in handler:
sep.onSegment(capiChunk.output_index);
return onProgress({ text: capiChunk.delta, ... });
The value isn't saving 5 lines per it's:site
- Naming the operation so the intent (
onSegment(...)) is visible at every call site, making a missing call easier to spot during review. - One place for the invariant test that locks in the behavior so the pattern can't regress silently.
- Lower friction for the next SDK integration to do the right thing by default.
Scope of work
- Add
MarkdownSegmentSeparator(location probablyextensions/copilot/src/util/common/) with focused unit tests covering: first emission (no separator), same key repeated (no separator), key change (separator),undefined-key legacy fallback (no separator), andreset().TBD - Refactor
responsesApi.tsOpenAIResponsesProcessorto use the helper (replacelastTextDeltaOutputIndex). - Refactor
copilotcliSession.tsto use the helper (replacelastEmittedAssistantMessageId+maybeEmitMessageSeparator). - Audit and fix the Claude Code SDK path (
claudeMessageDispatch.tshandleAssistantMessage) using the same helper, keyed on<message.uuid>:<contentIndex>. This is the third site likely affected; fixing it preemptively is cheap once the helper exists. - Migrate the existing tests in
responsesApi.spec.tsandcopilotcliSession.spec.tsto remain passing; add a Claude Code SDK test.
Non-goals
- Changing the separator string (
\n\nis correct and markdown-safe between blocks). - Touching the
chunkMessageIds/assistantMessageChunksdedup logic incopilotcliSession. that's pre-existing and out of scope.ts - Auto-detecting fusion at the chat-rendering layer (heuristic would break legitimate intra-word streaming like
"writ"+"ing").
Acceptance
- All three integrations use
MarkdownSegmentSeparator. - Existing tests still pass; new tests assert no regression on each site.
- A future new SDK integration that forgets to call
onSegment(...)is caught either by review (intent is now explicit) or by a written test pattern this issue establishes.
References
- Original fix for OpenAI Responses API: #312173
- Original fix for Copilot CLI: #319727
- Most recent screenshot of the user-visible bug: in #319727
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.9k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/vscode
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
new release
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
All issues in microsoft/vscode
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
code-quality refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
github/gh-aw-firewall#8816 ·
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
vercel/react-tweet#225 ·