Path: blob/main/src/vs/sessions/browser/widget/AGENTS_CHAT_WIDGET.md
13394 views
Agent Sessions Chat Widget Architecture
This document describes the architecture of the Agent Sessions Chat Widget (AgentSessionsChatWidget), a new extensible chat widget designed for the agent sessions window. It replaces the tightly-coupled agent session logic inside ChatWidget and ChatInputPart with a clean, composable system built around the wrapper pattern.
1. Motivation: Why a New Architecture?
The Problem with Patching Core Widgets
The original approach to supporting agent sessions involved adding agent-specific logic directly into the core ChatWidget and ChatInputPart. Over time, this led to significant coupling and code complexity:
Inside ChatWidget (~100+ lines of agent-specific code):
ChatFullWelcomePartis directly instantiated insideChatWidget.render(), with the widget reaching into the welcome part's DOM to reparent the input element betweenfullWelcomePart.inputSlotandmainInputContainershowFullWelcomecreates a forked rendering path — 5+ conditional branches inrender(),updateChatViewVisibility(), andrenderWelcomeViewContentIfNeeded()lockToCodingAgent()/unlockFromCodingAgent()add ~55 lines of method code plus ~20 lines of scattered_lockedAgent-gated logic throughoutclear(),forcedAgentcomputation, welcome content generation, and scroll lock behaviorThe
_lockedToCodingAgentContextKeycontext key is set/read in many places, creating implicit coupling between agent session state and widget rendering
Inside ChatInputPart (~50+ lines of agent-specific code):
Imports
AgentSessionProviders,getAgentSessionProvider,getAgentSessionProviderNamedirectlyManages
_pendingDelegationTarget,agentSessionTypeKeycontext key, andsessionTargetWidgetHas ~15 call sites checking
sessionTypePickerDelegate?.getActiveSessionProvider()to determine option groups, picker rendering, and session type handlinggetEffectiveSessionType()resolves session type through a delegate → session context → fallback chain
Consequences:
Fragile changes — modifying agent session behavior requires touching core
ChatWidgetinternals, risking regressions in the standard chat experienceTesting difficulty — agent session logic is interleaved with general chat logic, making it hard to test either in isolation
Feature creep — every new agent session feature (target restriction, deferred creation, cached option groups) adds more conditional branches to shared code
Unclear ownership — it's hard to tell where "chat widget" ends and "agent sessions" begins
The Solution: Composition Over Modification
The AgentSessionsChatWidget wraps ChatWidget instead of patching it. Agent-specific behavior lives in self-contained components that compose with the core widget through well-defined interfaces (submitHandler, hiddenPickerIds, excludeOptionGroup, ISessionTypePickerDelegate bridge). The core ChatWidget requires no agent-specific modifications.
2. Overview
The Agent Sessions Chat Widget provides:
Deferred session creation — the UI is fully interactive before any session resource exists
Target configuration — users select which agent provider (Local, Cloud, etc.) to use
Welcome view — a branded empty-state experience with mascot, target buttons, and option pickers
Initial session options — option selections travel atomically with the first request to the extension
Configurable picker placement — pickers can be rendered in the welcome view, input toolbar, or both
Note:
AgentSessionsChatInputPartis a standalone adapter that bridgesIAgentChatTargetConfigtoChatInputPart. It is available for consumers that need aChatInputPartoutside of a fullChatWidget, butAgentSessionsChatWidgetitself creates the bridge delegate inline and passes it throughwrappedViewOptionsto theChatWidget's ownChatInputPart.
3. Key Components
3.1 AgentSessionsChatWidget
Location: src/vs/sessions/browser/widget/agentSessionsChatWidget.ts
The main wrapper around ChatWidget. It:
Owns the target config — creates
AgentSessionsChatTargetConfigfrom provided optionsIntercepts submission via two mechanisms — uses
submitHandlerto create the session on first send, and monkey-patchesacceptInputto attachinitialSessionOptionsto the session contextManages the welcome view — shows
AgentSessionsChatWelcomePartwhen the chat is emptyGathers initial options — collects all option selections and attaches them to the session context
Hides duplicate pickers — uses
hiddenPickerIdsandexcludeOptionGroupto avoid showing pickers in both the welcome view and input toolbarCaches option groups — persists extension-contributed option groups to
StorageServiceso pickers render immediately on next load before extensions activate
Submission Interception: Two Mechanisms
The widget uses two complementary interception points:
submitHandler(inwrappedViewOptions): Called byChatWidget._acceptInput()before the normal send flow. If the session hasn't been created yet, it calls_createSessionForCurrentTarget(), restores the input text (which gets cleared bysetModel()), and returnsfalseto let the normal flow continue.Monkey-patched
acceptInput: Called whenChatSubmitActiondirectly invokeschatWidget.acceptInput(). This captures the input text, creates the session if needed, then calls_gatherAllOptionSelections()to merge all option picks and attaches them tocontributedChatSession.initialSessionOptionsbefore delegating to the originalacceptInput.
Both paths converge on the same session creation and option gathering logic. The submitHandler handles the ChatWidget-internal send path, while the monkey-patch handles external callers (like ChatSubmitAction).
3.2 AgentSessionsChatTargetConfig
Location: src/vs/sessions/browser/widget/agentSessionsChatTargetConfig.ts
A reactive configuration object that tracks:
Allowed targets — which agent providers are available (e.g.,
[Background, Cloud])Selected target — which provider the user has chosen
Events — fires when the target or allowed set changes
The target config is purely UI state — changing targets does NOT create sessions or resources.
3.3 AgentSessionsChatWelcomePart
Location: src/vs/sessions/browser/parts/agentSessionsChatWelcomePart.ts
Renders the welcome view when the chat is empty:
Mascot — product branding image
Target buttons — Local / Cloud toggle with sliding indicator
Option pickers — extension-contributed option groups (repository, folder, etc.)
Input slot — where the chat input is placed when in welcome mode
The welcome part reads from IAgentChatTargetConfig and the IChatSessionsService for option groups. When the shared ChatInputPart is rendered in this slot, the sessions window preserves the core .chat-input-container.focused focus border behavior so the active chat input uses focusBorder while focused.
3.4 AgentSessionsChatInputPart
Location: src/vs/sessions/browser/parts/agentSessionsChatInputPart.ts
A standalone adapter around ChatInputPart that bridges IAgentChatTargetConfig to the existing ISessionTypePickerDelegate interface. It creates a createTargetConfigDelegate() bridge so the standard ChatInputPart can work with the new target config system without modifications.
Important: AgentSessionsChatWidget does not use this adapter directly. Instead, it creates its own bridge delegate inline and passes it to ChatWidget via wrappedViewOptions.sessionTypePickerDelegate. The AgentSessionsChatInputPart is available for consumers that need a ChatInputPart with target config integration outside the context of a full ChatWidget (e.g., a detached input field).
3.5 AgentSessionsTargetPickerActionItem
Location: src/vs/sessions/browser/widget/agentSessionsTargetPickerActionItem.ts
A dropdown picker action item for the input toolbar that reads available targets from IAgentChatTargetConfig (rather than chatSessionsService). Selection calls targetConfig.setSelectedTarget() with no session creation side effects. It renders the current target's icon and name, with a chevron to open the dropdown of allowed targets. The picker automatically re-renders when the selected target or allowed targets change.
4. Chat Input Lifecycle: First Load vs New Session
The chat input behaves differently depending on whether it's the very first load (before the extension activates) or a subsequent "New Session" after the extension is already active.
4.1 First Load (Extension Not Yet Activated)
When the agent sessions window opens for the first time:
The
ChatWidgetrenders with no model —viewModelisundefinedChatInputParthas nosessionResource, so pickers query thesessionTypePickerDelegatefor the effective session typeThe extension hasn't activated yet, so:
chatSessionHasModelscontext key isfalse(no option groups registered)lockedToCodingAgentisfalse(contribution not available yet)The
ChatSessionPrimaryPickerActionmenu item is hidden (itswhenclause requires both)
Cached option groups (from a previous run) are loaded from storage and seeded into the service, allowing pickers to render immediately with stale-but-useful data
Pending session resource —
_generatePendingSessionResource()generates a lightweight URI (e.g.,copilotcli:/untitled-<uuid>) synchronously. No async work or extension activation needed. This resource allows picker commands andnotifySessionOptionsChangeevents to flow through the existing pipeline.When the extension activates:
onDidChangeAvailabilityfires →updateWidgetLockStateFromSessionTypesetslockedToCodingAgent = trueonDidChangeOptionGroupsfires with fresh data →chatSessionHasModels = trueThe
whenclause is now satisfied → toolbar re-renders with the picker actionThe welcome part re-renders pickers with live data from the extension
Extension can now fire
notifySessionOptionsChangewith the pending resource — the service stores values in_pendingSessionOptions, firesonDidChangeSessionOptions, and the welcome part andChatInputPartmatch the resource and sync picker state.
4.2 New Session (Extension Already Active)
When the user clicks "New Session" after completing a request:
resetSession()is calledThe old model is cleared via
setModel(undefined)and the model ref is disposed_sessionCreatedis reset tofalse_pendingSessionResourceis clearedPending option selections from
ChatInputPartare cleared viatakePendingOptionSelections()The welcome view becomes visible and pickers are re-rendered via
resetSelectedOptions()_generatePendingSessionResource()generates a fresh pending resource (synchronous)The
ChatWidgetagain has no model — same as first load from the input's perspectiveBUT the extension is already active, so:
lockedToCodingAgentis alreadytrue(contribution is available)chatSessionHasModelsis alreadytrue(option groups are registered)Pickers render immediately with live data — no waiting for extension activation
Option groups are fresh (not stale cached data)
getOrCreateChatSessionresolves quickly since the content provider is already registered
4.3 Key Differences
| Aspect | First Load | New Session |
|---|---|---|
| Extension state | Not activated | Already active |
lockedToCodingAgent | false → true (async) | Already true |
chatSessionHasModels | false → true (async) | Already true |
| Input toolbar pickers | Hidden → appear on activation | Visible immediately |
| Welcome part pickers | Cached → replaced with live data | Live data from start |
| Session resource | Generated as pending, session data created eagerly | Old cleared, new pending generated |
_pendingSessionResource | Set after getOrCreateChatSession completes | Cleared and re-initialized |
_pendingOptionSelections | Empty | Cleared via takePendingOptionSelections() |
| Extension option changes | Received after pending init completes | Received immediately |
4.4 The locked Flag and Session Reset
Extensions can mark option items as locked (e.g., locking the folder picker after a request starts). This is a session-specific concept:
During an active session, the extension sends
notifySessionOptionsChangewith{ ...option, locked: true }The welcome part syncs these via
syncOptionsFromSession, but strips thelockedflag before storing in_selectedOptionsThis ensures that when the welcome view re-renders (e.g., after reset), pickers are always interactive
Locking only affects the
ChatSessionPickerActionItemwidget'scurrentOption.lockedcheck, which disables the dropdown
5. Resourceless Chat Input
The Problem
Traditional chat sessions require a session resource (URI) to exist before the user can interact. This means:
Extensions must register and load before the UI is usable
Creating a session involves an async round-trip to the extension
The user sees a loading state instead of being productive
The Solution
The Agent Sessions Chat Widget defers chat model creation to the moment of first submit, but eagerly initializes session data so extensions can interact with options before the user sends a message:
Before chat model creation (pending session state):
A pending session resource is generated via
getResourceForNewChatSession()andchatSessionsService.getOrCreateChatSession()is called eagerly. This creates session data (options store) and invokesprovideChatSessionContentso the extension knows the resource.The extension can fire
notifySessionOptionsChange(pendingResource, updates)at any time — the welcome part matches the pending resource and syncs option values.Target selection is tracked in
AgentSessionsChatTargetConfigUser option selections are cached in
_pendingOptionSelections(ChatInputPart) and_selectedOptions(welcome part), AND forwarded to the extension vianotifySessionOptionsChangeusing the pending resource.The chat input works normally — user can type, attach context, change mode
At chat model creation (triggered by either submitHandler or the patched acceptInput):
_createSessionForCurrentTarget()reads the current target from the configReuses the pending session resource (the same URI used for session data) — no new resource is generated
For non-local targets, calls
loadSessionForResource(resource, location, CancellationToken.None)which reuses the existing session data fromgetOrCreateChatSession(); for local targets, callsstartSession(location)directlySets the model on the
ChatWidgetviasetModel()(this clears the input editor, so the input text is captured and restored)_gatherAllOptionSelections()collects options from welcome part + input toolbarOptions are attached to
contributedChatSession.initialSessionOptionsviamodel.setContributedChatSession()The request proceeds through the normal
ChatWidget._acceptInputflow
6. Initial Session Options (initialSessionOptions)
The Problem
When a session is created on first submit, the extension needs to know what options the user selected (model, repository, agent, etc.). But the traditional provideHandleOptionsChange mechanism is async and fire-and-forget — there's no guarantee the extension processes it before the request arrives.
The Solution
Options travel atomically with the first request via initialSessionOptions on the ChatSessionContext:
Data Flow
| Layer | Type | Field |
|---|---|---|
| Internal model | IChatSessionContext | initialSessionOptions?: ReadonlyArray<{optionId: string, value: string | { id: string; name: string }}> |
| Protocol DTO | IChatSessionContextDto | initialSessionOptions?: ReadonlyArray<{optionId: string, value: string}> |
| Extension API | ChatSessionContext | initialSessionOptions?: ReadonlyArray<{optionId: string, value: string}> |
Note: The internal model allows
valueto be either astringor{ id, name }(matchingIChatSessionProviderOptionItem's structural type). During serialization to the protocol DTO inmainThreadChatAgents2, the value is converted tostring. The extension always receivesstringvalues.
Extension Usage
Priority Order
When _gatherAllOptionSelections() merges options:
Welcome part selections (lowest priority) — includes defaults for repository/folder pickers
Input toolbar selections (highest priority) — explicit user picks override welcome defaults
7. Picker Placement
Pickers can appear in two locations:
Welcome view — above the input, managed by
AgentSessionsChatWelcomePartInput toolbar — inside the chat input, managed by
ChatInputPart
To avoid duplication, the widget uses two mechanisms:
hiddenPickerIds
Hides entire picker actions from the input toolbar:
excludeOptionGroup
Selectively excludes specific option groups from ChatSessionPrimaryPickerAction in the input toolbar while keeping others:
This allows the input toolbar to still show model pickers from ChatSessionPrimaryPickerAction while the welcome view handles the repository picker.
8. File Structure
9. Adding a New Agent Provider
To add a new agent provider (e.g., "Codex"):
Add to
AgentSessionProvidersenum inagentSessions.tsUpdate target config in
chatViewPane.ts:Register session content provider in the extension
Handle
initialSessionOptionsin the extension's request handlerRegister option groups via
provideChatSessionProviderOptions
The welcome part and input toolbar automatically pick up new targets and option groups.
10. Comparison with Old Architecture
Side-by-Side
| Aspect | Old (ChatFullWelcomePart inside ChatWidget) | New (AgentSessionsChatWidget wrapper) |
|---|---|---|
| Session creation | Eager (on load) | Deferred (on first send) |
| Target selection | ISessionTypePickerDelegate callback | IAgentChatTargetConfig observable |
| Option delivery | provideHandleOptionsChange (async, fire-and-forget) | initialSessionOptions (atomic with request) |
| Welcome view | Inside ChatWidget via showFullWelcome flag | Separate AgentSessionsChatWelcomePart |
| Picker placement | Hardcoded in ChatInputPart | Configurable via hiddenPickerIds + excludeOptionGroup |
| Input reparenting | ChatWidget reaches into welcome part's DOM | AgentSessionsChatWidget manages its own DOM layout |
| Agent lock state | lockToCodingAgent() / unlockFromCodingAgent() on ChatWidget | Not needed — target config is external state |
| Extensibility | Requires modifying ChatWidget internals | Self-contained, composable components |
Benefits of the New Architecture
1. Clean Separation of Concerns
The old approach embeds agent session logic (target selection, welcome view, lock state, option caching) directly inside ChatWidget. This means every agent feature touches the same file that powers the standard chat experience. The new architecture keeps ChatWidget focused on its core responsibility — rendering a chat conversation — and pushes agent-specific behavior into dedicated components.
2. Reduced Risk of Regressions
In the old architecture, ChatWidget.render() has forked control flow gated on showFullWelcome, and ChatInputPart has ~15 call sites checking session type delegates. A change to how pickers render could break the standard chat. In the new architecture, AgentSessionsChatWidget composes with ChatWidget through stable, narrow interfaces (submitHandler, hiddenPickerIds, excludeOptionGroup), so changes to agent session behavior cannot break the core widget.
3. Testable in Isolation
AgentSessionsChatTargetConfig can be unit-tested independently — it's a pure observable state container with no DOM or service dependencies beyond Disposable. The old ISessionTypePickerDelegate was an ad-hoc callback interface defined inline, making it harder to mock and test.
4. Deferred Session Creation
The old architecture creates sessions eagerly, requiring an async round-trip to the extension before the UI is usable. The new architecture lets the user interact immediately (type, select targets, pick options) and only creates the session on first send. This eliminates the loading state and makes the initial experience feel instant.
5. Atomic Option Delivery
The old provideHandleOptionsChange mechanism sends option changes asynchronously — if the user changes a repository picker and immediately sends a message, there's a race condition where the extension might not have processed the option change yet. The new initialSessionOptions mechanism bundles all option selections with the first request, guaranteeing the extension sees the correct state.
6. Easier to Add New Agent Providers
Adding a new provider in the old architecture requires modifying ChatWidget, ChatInputPart, and ChatFullWelcomePart. In the new architecture, it's a matter of adding to the AgentSessionProviders enum and updating the allowedTargets config — the welcome part and input toolbar automatically discover new targets and option groups.
7. No Core Widget Modifications Required
The entire agent sessions feature works by wrapping ChatWidget with composition hooks that ChatWidget already exposes (submitHandler, viewOptions). This means the agent sessions team can iterate independently without coordinating changes to shared core widget code.