fix: guard against cleared view model in ChatWidget._acceptInput (fixes #332754)
- 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
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