-
Notifications
You must be signed in to change notification settings - Fork 37.6k
fix streaming tool calls still showing #290981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes issue #290893 where streaming tool entries were being shown for failed edits, resulting in duplicate tool entries in the chat interface. The fix introduces a mechanism to track and remove tool invocations that transition from streaming to non-streaming state with hidden presentation.
Changes:
- Adds tracking of tool DOM wrappers by call ID and a pending removals queue
- Implements removal logic for streaming tool entries that become hidden
- Processes pending removals when new items are appended
Comments suppressed due to low confidence (1)
src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts:910
- The new removal logic for streaming tool entries with hidden presentation lacks test coverage. Consider adding tests that verify: 1) tool entries are queued for removal when transitioning from streaming to non-streaming with hidden presentation, 2) pending removals are processed when appendItem is called, and 3) the tool entry is removed from the DOM and all internal tracking structures.
// removes the tool entry that was previously streaming and now is not. removes item from dom and internal tracking.
private removeStreamingToolEntry(toolCallId: string, toolLabel: string): void {
const wrapper = this.toolWrappersByCallId.get(toolCallId);
if (wrapper) {
wrapper.remove();
this.toolWrappersByCallId.delete(toolCallId);
}
// make sure to remove any lazy item as well
const lazyIndex = this.lazyItems.findIndex(item =>
item.kind === 'tool' &&
item.toolInvocationOrMarkdown &&
(item.toolInvocationOrMarkdown.kind === 'toolInvocation' || item.toolInvocationOrMarkdown.kind === 'toolInvocationSerialized') &&
item.toolInvocationOrMarkdown.toolCallId === toolCallId
);
if (lazyIndex !== -1) {
this.lazyItems.splice(lazyIndex, 1);
}
this.appendedItemCount = Math.max(0, this.appendedItemCount - 1);
this.toolInvocationCount = Math.max(0, this.toolInvocationCount - 1);
const toolInvocationsIndex = this.toolInvocations.findIndex(t =>
(t.kind === 'toolInvocation' || t.kind === 'toolInvocationSerialized') && t.toolCallId === toolCallId
);
if (toolInvocationsIndex !== -1) {
this.toolInvocations.splice(toolInvocationsIndex, 1);
}
const titleIndex = this.extractedTitles.indexOf(toolLabel);
if (titleIndex !== -1) {
this.extractedTitles.splice(titleIndex, 1);
}
this.updateDropdownClickability();
this._onDidChangeHeight.fire();
}
| // remove any queued removals | ||
| for (const pending of this.pendingRemovals) { | ||
| this.removeStreamingToolEntry(pending.toolCallId, pending.toolLabel); | ||
| } | ||
| this.pendingRemovals = []; |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pending removals are only processed when appendItem is called. If a tool transitions to hidden presentation after streaming completes but no new items are appended, the tool entry will remain visible in the UI. Consider processing pendingRemovals in finalizeTitleIfDefault or markAsInactive to ensure hidden tool entries are removed even when no more items are appended.
| item.toolInvocationOrMarkdown.toolCallId === toolCallId | ||
| ); | ||
| if (lazyIndex !== -1) { | ||
| this.lazyItems.splice(lazyIndex, 1); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When removing a lazy item that has been materialized, its disposable is not disposed. The lazy item may have been materialized via materializeLazyItem, which would have registered its disposable to the class (see materializeLazyItem lines 1119-1121). Consider checking if the lazy item has been materialized using item.lazy.hasValue and disposing of the disposable before removing the item from the array to prevent resource leaks.
| this.lazyItems.splice(lazyIndex, 1); | |
| const [lazyItem] = this.lazyItems.splice(lazyIndex, 1); | |
| if (lazyItem.lazy && lazyItem.lazy.hasValue) { | |
| const disposable = lazyItem.lazy.value as unknown as IDisposable; | |
| if (typeof disposable.dispose === 'function') { | |
| disposable.dispose(); | |
| } | |
| } |
| private removeStreamingToolEntry(toolCallId: string, toolLabel: string): void { | ||
| const wrapper = this.toolWrappersByCallId.get(toolCallId); | ||
| if (wrapper) { | ||
| wrapper.remove(); | ||
| this.toolWrappersByCallId.delete(toolCallId); | ||
| } | ||
|
|
||
| // make sure to remove any lazy item as well | ||
| const lazyIndex = this.lazyItems.findIndex(item => | ||
| item.kind === 'tool' && | ||
| item.toolInvocationOrMarkdown && | ||
| (item.toolInvocationOrMarkdown.kind === 'toolInvocation' || item.toolInvocationOrMarkdown.kind === 'toolInvocationSerialized') && | ||
| item.toolInvocationOrMarkdown.toolCallId === toolCallId | ||
| ); | ||
| if (lazyIndex !== -1) { | ||
| this.lazyItems.splice(lazyIndex, 1); | ||
| } | ||
|
|
||
| this.appendedItemCount = Math.max(0, this.appendedItemCount - 1); | ||
| this.toolInvocationCount = Math.max(0, this.toolInvocationCount - 1); | ||
| const toolInvocationsIndex = this.toolInvocations.findIndex(t => | ||
| (t.kind === 'toolInvocation' || t.kind === 'toolInvocationSerialized') && t.toolCallId === toolCallId | ||
| ); | ||
| if (toolInvocationsIndex !== -1) { | ||
| this.toolInvocations.splice(toolInvocationsIndex, 1); | ||
| } | ||
|
|
||
| const titleIndex = this.extractedTitles.indexOf(toolLabel); | ||
| if (titleIndex !== -1) { | ||
| this.extractedTitles.splice(titleIndex, 1); | ||
| } | ||
|
|
||
| this.updateDropdownClickability(); | ||
| this._onDidChangeHeight.fire(); | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The autorun created in trackToolMetadata is registered to the class's dispose store but is never individually disposed when removeStreamingToolEntry removes a tool. This means the autorun will continue to observe state changes and potentially update titles for a tool that has been removed from the UI. Consider storing the autorun disposable in a map keyed by toolCallId so it can be properly disposed when the tool entry is removed.
fix #290893