microsoft / microsoft/vscode

fix: guard against cleared view model in ChatWidget._acceptInput (fixes #332754)

Open
#332,763 0 comments 0 reactions 0 assignees View on GitHub
agentic-workflows errors-fix
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

### Summary

`TypeError: Cannot read properties of undefined (reading 'model')` is thrown from `ChatWidget._acceptInput` when the chat view model is torn down (session switch or model disposal) while a submission is still in flight. The method validates `this.viewModel` once near the top, but then executes several more `await` points before dereferencing `this.viewModel.model`; a concurrent `setModel(undefined)` / `onDidDisposeModel` can null the field in between, so the later unconditional read throws. Impact: benign-looking unhandled error surfaced to telemetry whenever a user submits chat input as the session is being replaced or disposed.

Fixes microsoft/vscode\#332754
Recommended reviewer: `@connor4312`

### Culprit Commit

| Field | Value |
|-------|-------|
| Commit | [`798ef5910034`](https://github.com/microsoft/vscode/commit/798ef5910034) |
| Author | `@connor4312` |
| PR | #277944 |
| Message | chat: remove `waitForReady` (#277944) |
| Why | This commit reworked `_acceptInput` to wait for a view model and added the single early-return guard at the top of the method (`if (!this.viewModel) { return; }`). It did not re-validate `this.viewModel` after the subsequent `await` points (submit handler, `saveAllBeforeChatSend`, slash-command execution, request cancellation, `finishedEditing`), so a concurrent teardown between the guard and the `const model = this.viewModel.model` read can null the field and produce the `TypeError`. |

### Code Flow

```mermaid
sequenceDiagram
participant User as User
participant Accept as _acceptInput()
participant Teardown as setModel(undefined) / onDidDisposeModel
participant Crash as this.viewModel.model

User->>Accept: submit input
Note over Accept: guard: if (!this.viewModel) return (passes)
Accept->>Accept: await submitHandler / saveAll / slash cmd / cancel / finishedEditing
Teardown-->>Accept: this.viewModel = undefined (concurrent session switch/dispose)
Accept->>Crash: const model = this.viewModel.model
Note over Crash: 💥 TypeError: Cannot read properties of undefined (reading 'model')
```

### Affected Files

| File | Role | Evidence |
|------|------|----------|
| `src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts` | crash site | L3209 (from stack): `const model = this.viewModel.model;` |
| `src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts` | producer | L2640: `this.viewModel = undefined;` in `setModel(undefined)`, and L2721: `this.viewModel = undefined;` in the `onDidDisposeModel` handler |
| `src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts` | existing guard pattern | L3100-3102: `if (!this.viewModel) { return; }` and L3165: `const isEditing = this.viewModel?.editing;` |

### Repro Steps

This is a timing-dependent teardown race; it is not deterministic.

1. Open a chat session and type a message.
2. Submit the message (Enter) while simultaneously switching to another chat session or triggering session disposal (e.g. closing/clearing the chat) so that `setModel` runs during the in-flight `_acceptInput` awaits.
3. To increase likelihood, use a custom `submitHandler` or a slow `saveAllBeforeChatSend` (many dirty editors) to widen the await window between the top-of-method guard and the `this.viewModel.model` read, then dispose/replace the model during that window.
4. The unhandled `TypeError: Cannot read properties of undefined (reading 'model')` is thrown from `_acceptInput`.

### How the Fix Works

**Chosen approach**: In `src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts` → `_acceptInput()`, re-validate `this.viewModel` immediately before the `const model = this.viewModel.model` dereference and bail out (`return`) when it has been cleared. This mirrors the method's own existing patterns: the top-of-method guard at L3100 and the optional access at L3165 (`this.viewModel?.editing`) already acknowledge that the view model can disappear across awaits. The fix stops the in-flight submission when the session it belonged to no longer exists — the correct behavior, since there is no model left to submit to.

This is a lifecycle/race error, so the guard is placed to stop the consumer from acting on state that a concurrent teardown invalidated. The producer (`setModel(undefined)` / `onDidDisposeModel`) legitimately clears `this.viewModel` as part of normal session lifecycle; it cannot avoid nulling the field, and the in-flight `_acceptInput` continuation is the party that must observe the change. Re-reading the field under the same synchronous section as the dereference makes the invalid ordering unrepresentable at the crash site without swallowing any error — no `logService.error` or throw is removed, and the guard fires only for the genuine “session went away mid-submit” case.

**Lifecycle pattern**: stale cached identifier / use-after-clear across await boundaries.
**Producer site**: `src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts:2640` and `:2721` — the two paths that set `this.viewModel = undefined` during session switch/dispose.

**Alternatives considered**: Wrapping the dereference in `try/catch` — rejected because it would hide the lifecycle error from telemetry rather than making the invalid ordering explicit. Adding `?.` and continuing — rejected because the rest of the method unconditionally uses `model`/`this.viewModel`, so a null model must abort the submission, not proceed with an undefined value.

### Recommended Owner

`@connor4312` — culprit commit author of #277944 which introduced the `_acceptInput` view-model wait/guard logic; active VS Code team member with write access (recent commits within the last day).

> Generated by [errors-fix](https://github.com/microsoft/vscode-engineering/actions/runs/32983003043) · opus48 · 395.2 AIC · ⌖ 11.6 AIC · ⊞ 18.6K · [◷](https://github.com/search?q=repo%3Amicrosoft%2Fvscode+%22gh-aw-workflow-id%3A+errors-fix%22&type=pullrequests)

---

> [!NOTE]
> This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch [`fix/chatwidget-acceptinput-viewmodel-race-c4024fe24de868cc`](https://github.com/vscodebot-pr/vscode/tree/fix/chatwidget-acceptinput-viewmodel-race-c4024fe24de868cc).
>
> **Original error:** ERR_API: [2026-08-26T15:10:12.344Z] create pull request in microsoft/vscode failed (attempt 1)

Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.

To create the pull request manually:

```sh
gh pr create --title "fix: guard against cleared view model in ChatWidget._acceptInput (fixes #332754)" --base main --head vscodebot-pr:fix/chatwidget-acceptinput-viewmodel-race-c4024fe24de868cc --repo microsoft/vscode
```

Show patch (32 lines)

```diff
From bbd69fa88579ef1d240077379baa5bf46fc16b3d Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 12dcb6bdba0ada99a44db9df918ee6bfd3b33cb3
From: "github-actions[bot]"
Date: Wed, 26 Aug 2026 15:04:11 +0000
Subject: [PATCH] fix: guard cleared view model in _acceptInput

---
src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 7 +++++++
1 file changed, 7 insertions(+)

diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
index c8e5824923f..bb4d2617997 100644
--- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
+++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
@@ -3206,6 +3206,13 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
}

+ if (!this.viewModel) {
+ // A concurrent session switch or teardown (e.g. setModel(undefined) or
+ // onDidDisposeModel) can clear the view model across the await points
+ // above. Bail out rather than dereferencing an undefined model.
+ return;
+ }
+
const model = this.viewModel.model;
if (options.cancelCurrentRequest && model.requestInProgress.get() && !cancelledCurrentRequest) {
await this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-stopAndSend');
--
2.54.0

```

Contributor guide

Open the contributing guide

Research direction

Start in src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts at ChatWidget._acceptInput(), then inspect setModel(undefined) and the onDidDisposeModel handler as the teardown paths. Verify the in-flight submission handles a cleared view model before reading its model, and check the affected chat widget behavior around session switching and disposal.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.