fix: prevent TreeError when focusing agent sessions list during async refresh (fixes #330875)
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
### Summary
`TreeError [AgentSessionsView] Tree element not found: [object Object]` is thrown from the Agent Sessions view when the list is focused while its async data tree is mid-refresh. It surfaced as a new stable anomaly in 1.133.0 (absent in 1.132.1), affecting ~157 users.
`AgentSessionsControl.focus()` calls `focusFirst()`, which sets focus by **list-view index**. During an async `updateChildren()` refresh the list view can momentarily reference an element that is not present in the compressed tree model's node map. Setting focus to that stale index synchronously fires `onDidChangeFocus`; the workbench list listener (`updateCollapseContextKeys` in `listService.ts`) then calls `tree.getNode(focus)`, which reaches `CompressedObjectTreeModel.getCompressedNode` and throws because the element is not in `this.nodes`.
The `try/catch` previously wrapping `focusFirst()` could never catch this: `Emitter._deliver` wraps every listener invocation in its own `try/catch` and reroutes exceptions to `onUnexpectedError` (the telemetry pipeline). The throw happens out-of-band during event delivery, not in the synchronous `focusFirst()` return path — so the guard was dead code for this crash.
Fixes microsoft/vscode\#330875
Recommended reviewer: `@bpasero`
### Culprit Commit
The exact regressing commit could not be pinned via `git merge-base`/`git show` because arbitrary-SHA object access is blocked in this environment (blobless/partial clone with no fetch credentials). Reasoning from local blame: the `focus()` → `focusFirst()` path with an ineffective `try/catch` was introduced by `150dcd72ba5` (BeniBenj, 2026-05-04, "fixes \#313042"), on top of the async list/update foundation authored by Benjamin Pasero (`b1009c98bb42`). The bucket is a genuine new anomaly in the 1.132.1 → 1.133.0 range, consistent with recent churn in this file (e.g. `406f70688322` update/pause logic).
### Code Flow
```mermaid
flowchart TD
A["AgentSessionsControl.focus()"] --> B["sessionsList.focusFirst()"]
B --> C["listWidget.focusNth(0)"]
C --> D["setFocus([index]) by list-view index"]
D --> E["abstractTree.setFocus fires onDidChangeFocus"]
E --> F["Emitter._deliver wraps listener in try/catch"]
F --> G["listService updateCollapseContextKeys()"]
G --> H["tree.getNode(focus)"]
H --> I["CompressedObjectTreeModel.getCompressedNode"]
I --> J["nodes.get(element) is undefined"]
J --> K["throw TreeError 'Tree element not found'"]
F --> L["exception routed to onUnexpectedError -> telemetry"]
K --> L
```
### Affected Files
- `src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts` — the producer/trigger. `focus()` sourced focus by stale list-view index.
- `src/vs/platform/list/browser/listService.ts` (crash consumer, shared base — not modified): `updateCollapseContextKeys` calls `tree.getNode(focus)`.
- `src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts` (throw site, shared base — not modified): `getCompressedNode` throws when the element is absent from `nodes`.
- `src/vs/base/common/event.ts` (not modified): `_deliver` reroutes listener exceptions to telemetry, which is why the old `try/catch` was ineffective.
### Repro Steps
1. Open the Agent Sessions view with sessions present.
2. Trigger an async list refresh (`update()` → `updateChildren()`), e.g. via session activity or repository label recomputation.
3. While the refresh is in-flight, focus the view (e.g. active editor change / view focus), invoking `focus()` → `focusFirst()`.
4. Focus lands on a list-view index whose element is momentarily absent from the tree model; `onDidChangeFocus` → `getNode` throws `TreeError [AgentSessionsView] Tree element not found`, reported to telemetry.
### How the Fix Works
**Chosen approach** (`agentSessionsControl.ts`): Replace `focusFirst()` with focus sourced from the tree model. The new `focus()` reads the first top-level element from `sessionsList.getNode(this.agentSessionsService.model).children[0]`, re-checks it with `hasNode()`, and only then calls `setFocus([firstElement])`. This fixes the problem at the data producer rather than the crash site: focus can now only ever point at an element the tree model actually holds, so the `onDidChangeFocus` listener's `getNode(focus)` cannot resolve a stale element. This mirrors the safe pattern already used elsewhere in the same file (`revealAndFocusActiveEditorSession` guards `setFocus` with `hasNode`, and `getNode(model)` is used to enumerate top-level children).
The ineffective `try/catch` is removed because it was dead code — exceptions thrown during synchronous event delivery are caught by `Emitter._deliver` and routed to telemetry, never back to the `focusFirst()` caller. Keeping it would falsely imply the error was handled.
After this change, `agentSessionsControl.ts` `focus()` cannot produce a focus element absent from the tree model, because the element is read from the model's own children and re-validated with `hasNode()` immediately before `setFocus`, so the `onDidChangeFocus` → `getNode` path always resolves a present node.
**Alternatives considered**:
- Guarding `getNode`/`getCompressedNode` at the crash site — rejected: it is shared base code used by every workbench tree, and would mask the producing desync instead of fixing it.
- Expanding or keeping the `try/catch` around `focusFirst()` — rejected: it cannot catch the out-of-band exception delivered via `Emitter._deliver`, so it hides nothing and fixes nothing.
### Recommended Owner
`@bpasero` — authored the Agent Sessions async list/update foundation (`b1009c98bb42`, and recent sessions work such as `b0f253eec7` on 2026-04-30) and is an active core maintainer with write access. He owns the surrounding update/refresh machinery that this fix coordinates with.
> Generated by [errors-fix](https://github.com/microsoft/vscode-engineering/actions/runs/31814659004) · opus48 · 627.5 AIC · ⌖ 11.9 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/agent-sessions-focus-tree-not-found-5fcce589c6fc07cd`](https://github.com/vscodebot-pr/vscode/tree/fix/agent-sessions-focus-tree-not-found-5fcce589c6fc07cd).
>
> **Original error:** ERR_API: [2026-08-14T15:54:46.444Z] 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: prevent TreeError when focusing agent sessions list during async refresh (fixes #330875)" --base main --head vscodebot-pr:fix/agent-sessions-focus-tree-not-found-5fcce589c6fc07cd --repo microsoft/vscode
```
Show patch preview (46 of 46 lines)
```diff
From 544dff38dfe5197a5ff786b80eb5de832c9b0352 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 6ebcc0a377ae04e130212f34df551c8340cf966a
From: "github-actions[bot]"
Date: Fri, 14 Aug 2026 15:47:38 +0000
Subject: [PATCH] fix: prevent TreeError when focusing agent sessions list
during async refresh (fixes #330875)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../agentSessions/agentSessionsControl.ts | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts
index 39cd98139ff..a854bb27d8a 100644
--- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts
+++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts
@@ -860,12 +860,19 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
focus(): void {
this.sessionsList?.domFocus();
- try {
- if ((this.sessionsList?.getFocus().length ?? 0) === 0) {
- this.sessionsList?.focusFirst();
- }
- } catch {
- // Tree model may be temporarily inconsistent during async refresh.
+ if (!this.sessionsList || this.sessionsList.getFocus().length > 0) {
+ return;
+ }
+
+ // Focus the first top-level element sourced from the tree model rather than calling
+ // focusFirst(), which sets focus by list-view index. During an async refresh the list
+ // view can reference an element that is momentarily absent from the tree model, which
+ // makes the onDidChangeFocus listener throw "Tree element not found" when it resolves
+ // the focused element. Re-check the element via hasNode() so focus only ever points at
+ // an element the model still holds.
+ const firstElement = this.sessionsList.getNode(this.agentSessionsService.model).children[0]?.element;
+ if (firstElement && this.session
... (truncated)
```
Contributor guide
Research direction
Start in src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts at AgentSessionsControl.focus(), then read the refresh path through updateChildren() and the related tree handling in listService.ts. Reproduce by focusing the Agent Sessions view during an async refresh; done means the refresh no longer reports TreeError and focus remains valid.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100