Skip to content

Conversation

@tamuratak
Copy link
Contributor

Implement resetSession method in ChatListItemRenderer and invoke it in ChatWidget for session management. Fix #277574.

gpt-5.1-codex-mini and I found another fix for #277574. I confirmed it is fixed.

A message from gpt-5.1-codex-mini:

  • What caused it? Chat templates and renderer caches were left intact when a saved chat session was reloaded, so ChatListItemRenderer reused stale DOM/events (focusing, progressive rendering, etc.) tied to the previous session because templateDataByRequestId, code block maps, hover artifacts, and in-progress rendered parts were never reset.
  • Why this fixes it? Introducing resetSession() clears every templateData (rendered parts, disposables, current element) and wipes the cached maps before a new model is bound or the widget is cleared, ensuring ListView always renders against a clean slate instead of recycling the old nodes, which eliminates the leftover rendering glitches.

cc: @connor4312

Copilot AI review requested due to automatic review settings December 5, 2025 12:10
@vs-code-engineering
Copy link

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@roblourens

Matched files:

  • src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Copy link
Contributor

Copilot AI left a 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 implements a resetSession() method in ChatListItemRenderer to fix rendering glitches when switching between chat sessions or reloading saved sessions (issue #277574). The root cause was that cached renderer state (template data, code blocks, file trees, etc.) from previous sessions was being reused when loading a new session, causing stale DOM elements and events to persist.

Key Changes

  • Added resetSession() method to ChatListItemRenderer that clears all cached session-specific state
  • Invokes resetSession() in ChatWidget at three strategic points: when clearing the chat, when setting no model, and when switching to a different session

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/vs/workbench/contrib/chat/browser/chatListRenderer.ts Implements resetSession() method that clears templateDataByRequestId map, code block caches, file tree caches, and resets element disposables for each cached template
src/vs/workbench/contrib/chat/browser/chatWidget.ts Invokes renderer?.resetSession() in three locations: in clear() method, when setModel() is called with undefined, and when setModel() switches to a different session

this.codeBlocksByResponseId.clear();
this.codeBlocksByEditorUri.clear();
this.fileTreesByResponseId.clear();
this.focusedFileTreesByResponseId.clear();
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resetSession() method should clear _announcedToolProgressKeys for consistency with updateViewModel(). This set tracks which tool progress has been announced to screen readers. When a session is reset, this state should also be cleared to ensure proper accessibility announcements in the new session.

Suggested fix:

resetSession(): void {
    for (const templateData of this.templateDataByRequestId.values()) {
        this.clearRenderedParts(templateData);
        templateData.elementDisposables.clear();
        templateData.currentElement = undefined;
    }
    this.templateDataByRequestId.clear();
    this.codeBlocksByResponseId.clear();
    this.codeBlocksByEditorUri.clear();
    this.fileTreesByResponseId.clear();
    this.focusedFileTreesByResponseId.clear();
    this._announcedToolProgressKeys.clear();  // Add this line
}
Suggested change
this.focusedFileTreesByResponseId.clear();
this.focusedFileTreesByResponseId.clear();
this._announcedToolProgressKeys.clear();

Copilot uses AI. Check for mistakes.
@roblourens
Copy link
Member

I can't repro the issue, and I don't see why this is necessary. Why isn't it enough to clear these parts the next time the template is reused?

@tamuratak
Copy link
Contributor Author

I asked gpt-5.1-codex-mini to make the issue reproducible, and it proposed the following change. It adds an artificial delay inside onDidChangeItems. Even with this change, I can still see the issue.

reproduce steps:

  1. Start a new chat.
  2. Type the prompt.
  3. Move the session to the editor area.
  4. Close the chat in the editor area.
  5. Restore the session in the chat.

prompt

output the following three times:

aaa message

```text
a


a



a




a



a



aaaa

bbb


aa
```

gpt-5.1-codex-mini's explanation:

The bug where code blocks disappear when reloading a saved chat session happens because ChatListItemRenderer keeps reusing the template and code-block caches from the previous session. The change delays onDidChangeItems by SAVED_SESSION_BUG_REPRO_DELAY_MS when a saved session is loaded, slowing down the Tree update and progressive rendering so that we deliberately hit the moment when ChatListItemRenderer still relies on the stale DOM/events/rendering parts before the cache is cleared. That makes the previously intermittent bug reproduceable every time.

When setModel detects a saved session (model.sessionResource.scheme === Schemas.vscodeLocalChatSession && model.getRequests().length > 0), _prepareSavedSessionDelayState increments the token and suppresses scrolling; onDidChangeItems sees _savedSessionDelayToken and goes through _runSavedSessionDelayedUpdate, which postpones setChildren/scrollToEnd. After the delay, if the token still matches the stored value, the real re-render runs and scrolling only happens when _scrollToEndAfterDelay is true, ensuring the renderer follows the stale cache reuse path before the Tree refresh occurs.

diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts
index e68d5b9fe87..23835258e33 100644
--- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts
@@ -169,6 +169,7 @@ const supportsAllAttachments: Required<IChatAgentAttachmentCapabilities> = {
 };
 
 const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate.");
+const SAVED_SESSION_BUG_REPRO_DELAY_MS = 64;
 
 export class ChatWidget extends Disposable implements IChatWidget {
 
@@ -283,6 +284,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
 	private _isLoadingPromptDescriptions = false;
 
 	private _mostRecentlyFocusedItemIndex: number = -1;
+	private _savedSessionDelayToken = 0;
+	private _scheduledSavedSessionDelayToken = 0;
+	private _scrollToEndAfterDelay = false;
 
 	private readonly viewModelDisposables = this._register(new DisposableStore());
 	private _viewModel: ChatViewModel | undefined;
@@ -777,6 +781,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
 
 	async clear(): Promise<void> {
 		this.logService.debug('ChatWidget#clear');
+		this._resetSavedSessionDelayState();
 		if (this._dynamicMessageLayoutData) {
 			this._dynamicMessageLayoutData.enabled = true;
 		}
@@ -800,6 +805,44 @@ export class ChatWidget extends Disposable implements IChatWidget {
 	}
 
 	private onDidChangeItems(skipDynamicLayout?: boolean) {
+		if (this._savedSessionDelayToken > 0) {
+			this._scheduleSavedSessionDelayedUpdate(skipDynamicLayout);
+			return;
+		}
+		this._performOnDidChangeItems(skipDynamicLayout);
+	}
+
+	private _scheduleSavedSessionDelayedUpdate(skipDynamicLayout?: boolean): void {
+		if (this._savedSessionDelayToken === 0 || this._scheduledSavedSessionDelayToken === this._savedSessionDelayToken) {
+			return;
+		}
+		const token = this._savedSessionDelayToken;
+		this._scheduledSavedSessionDelayToken = token;
+		void this._runSavedSessionDelayedUpdate(skipDynamicLayout, token);
+	}
+
+	private async _runSavedSessionDelayedUpdate(skipDynamicLayout: boolean | undefined, token: number): Promise<void> {
+		await this._sleepForSavedSessionRace();
+		if (token !== this._savedSessionDelayToken) {
+			this._scheduledSavedSessionDelayToken = 0;
+			return;
+		}
+		this._savedSessionDelayToken = 0;
+		this._scheduledSavedSessionDelayToken = 0;
+		this._performOnDidChangeItems(skipDynamicLayout);
+		if (this._scrollToEndAfterDelay) {
+			this._scrollToEndAfterDelay = false;
+			if (this.visible) {
+				this.scrollToEnd();
+			}
+		}
+	}
+
+	private async _sleepForSavedSessionRace(): Promise<void> {
+		await new Promise<void>(resolve => setTimeout(resolve, SAVED_SESSION_BUG_REPRO_DELAY_MS));
+	}
+
+	private _performOnDidChangeItems(skipDynamicLayout?: boolean): void {
 		if (this._visible || !this.viewModel) {
 			const treeItems = (this.viewModel?.getItems() ?? [])
 				.map((item): ITreeElement<ChatTreeItem> => {
@@ -857,6 +900,21 @@ export class ChatWidget extends Disposable implements IChatWidget {
 		}
 	}
 
+	private _resetSavedSessionDelayState(): void {
+		this._savedSessionDelayToken = 0;
+		this._scheduledSavedSessionDelayToken = 0;
+		this._scrollToEndAfterDelay = false;
+	}
+
+	private _prepareSavedSessionDelayState(): void {
+		this._savedSessionDelayToken = this._savedSessionDelayToken + 1;
+		if (this._savedSessionDelayToken === 0) {
+			this._savedSessionDelayToken = 1;
+		}
+		this._scheduledSavedSessionDelayToken = 0;
+		this._scrollToEndAfterDelay = true;
+	}
+
 	/**
 	 * Updates the DOM visibility of welcome view and chat list immediately
 	 */
@@ -1967,6 +2025,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
 		}
 
 		if (!model) {
+			this._resetSavedSessionDelayState();
 			this.viewModel = undefined;
 			this.onDidChangeItems();
 			return;
@@ -1982,6 +2041,12 @@ export class ChatWidget extends Disposable implements IChatWidget {
 
 		this.container.setAttribute('data-session-id', model.sessionId);
 		this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection);
+		const shouldDelaySavedSession = model.sessionResource.scheme === Schemas.vscodeLocalChatSession && model.getRequests().length > 0;
+		if (shouldDelaySavedSession) {
+			this._prepareSavedSessionDelayState();
+		} else {
+			this._resetSavedSessionDelayState();
+		}
 
 		// Pass input model reference to input part for state syncing
 		this.inputPart.setInputModel(model.inputModel);
@@ -2066,7 +2131,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
 
 		if (this.tree && this.visible) {
 			this.onDidChangeItems();
-			this.scrollToEnd();
+			if (!this._scrollToEndAfterDelay) {
+				this.scrollToEnd();
+			}
 		}
 
 		this.renderer.updateViewModel(this.viewModel);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codeblocks aren't rendered when restoring chat sessions

4 participants