sessions
microsoft/vscode
Agents window architecture reference for VS Code — layers, layout, session management, and development guidelines.
What is sessions?
Comprehensive specification for the Agents window in VS Code, covering internal architecture, layer rules, layout system, session/provider interfaces, and mobile adaptations. Use when implementing features, fixing bugs, or modifying any part of the sessions module.
- Documents layer hierarchy (core → services → contrib → providers) with ESLint-enforced import restrictions
- Specifies fixed grid layout (Sidebar | ChatBar | AuxiliaryBar) and per-session state persistence
- Defines pluggable provider model (ISessionsProvider → ISessionsProvidersService → ISessionsManagementService)
- Details sessions list sidebar with tree widget, grouping, filtering, pinning, and read/unread state
- Covers mobile-specific UI architecture (phone < 640px viewport, drawer sidebar, MobileTitlebarPart)
- Provides mandatory coding guidelines, naming conventions, disposable management, and DI patterns
How to install sessions
npx skills add https://github.com/microsoft/vscode --skill sessions- Read `.github/instructions/coding-guidelines.instructions.md` (naming, style, localization, DI)
- Read `.github/instructions/source-code-organization.instructions.md` (layers, environments, folder structure)
- Familiarity with VS Code's observable/reactive patterns and dependency injection
How to use sessions
- 1.Identify which area you are modifying (layout, sessions list, providers, mobile, etc.)
- 2.Read the corresponding spec document (LAYERS.md, LAYOUT.md, SESSIONS.md, SESSIONS_LIST.md, MOBILE.md, or AI_CUSTOMIZATIONS.md)
- 3.Check layer rules in LAYERS.md before adding cross-module imports; verify contrib/* does not import from contrib/providers/*
- 4.Use IObservable/autorun/derived for state propagation, never Event-based listeners
- 5.Ensure new contribution files are imported in the appropriate sessions.*.main.ts entry point
- 6.Update the relevant spec document if you modify the implementation to keep it in sync
Use cases
- Implementing new features in the Agents window while respecting layer boundaries and import rules
- Fixing bugs in session management, layout persistence, or provider integration
- Adding mobile-specific UI components or adapting desktop features for phone viewports
- Modifying the sessions list sidebar (grouping, filtering, renderers, actions)
- Extending the AI customizations editor or tree view
- VS Code extension developers working on the Agents window
- Contributors implementing session management or chat features
- Teams building provider integrations (Copilot, custom agents)
- Mobile/responsive UI developers adapting Agents window for phone layouts
sessions FAQ
The layer hierarchy is core → services → contrib → providers. ESLint enforces that contrib/* must NOT import from contrib/providers/*. Use ISessionsManagementService and ISession interfaces for provider-agnostic code; only the Copilot provider may import IAgentSessionsService.
Session state must always flow through IObservable (autorun/derived) for reactive UI, never through onDid* event listeners. Observables ensure deterministic state propagation and prevent race conditions.
Create your contribution file in the appropriate folder (core/, services/, contrib/, or contrib/providers/), then import it in the corresponding sessions.*.main.ts entry point (e.g., sessions.desktop.main.ts) so it is loaded at startup.
Per-session layout state (auxiliary bar, panel, editor working sets) is captured when switching sessions and restored when returning. The LayoutController manages this, and state is persisted to storage with migration support.
Use IsPhoneLayoutContext to gate views/actions, extend MobileTitlebarPart for custom titlebar logic, and use MobilePickerSheet for modal interactions. The mobile part subclass architecture maps desktop components to mobile equivalents.
Full instructions (SKILL.md)
Source of truth, from microsoft/vscode.
name: sessions description: Agents window architecture — covers the agents-first app, layering, folder structure, chat widget, menus, contributions, entry points, and development guidelines. Use when implementing features or fixing issues in the Agents window.
Before Making Any Changes
MANDATORY: Before writing or modifying any code in src/vs/sessions/, you must read these documents:
.github/instructions/coding-guidelines.instructions.md— Naming conventions, code style, string localization, disposable management, and DI patterns..github/instructions/source-code-organization.instructions.md— Layers, target environments, dependency injection, and folder structure conventions.
Then read the relevant spec for the area you are changing (see table below). If you modify the implementation, you must update the corresponding spec to keep it in sync.
Specification Documents
| Document | Path | When to read |
|---|---|---|
| Layer rules | src/vs/sessions/LAYERS.md | Before adding any cross-module imports. Defines the internal layer hierarchy (core → services → contrib → providers) with ESLint-enforced import restrictions. Key rule: contrib/* must NOT import from contrib/providers/*. |
| Layout spec | src/vs/sessions/LAYOUT.md | Before changing any part, grid structure, titlebar, or CSS. Documents the fixed grid layout (Sidebar | ChatBar | AuxiliaryBar), part positions, the modal editor system, per-session layout state persistence, and the titlebar's three-section design. |
| Layout controller spec | src/vs/sessions/LAYOUT_CONTROLLER.md | Before changing LayoutController or per-session layout state. Details how the auxiliary bar, panel, and editor working sets are captured/restored when switching sessions, multi-session suppression, the auto-reveal-on-changes flow, workspace-folder ordering, and storage/migration. |
| Sessions spec | src/vs/sessions/SESSIONS.md | Before changing session/provider interfaces or data flow. Covers the pluggable provider model (ISessionsProvider → ISessionsProvidersService → ISessionsManagementService), ISession/IChat interfaces, observable state propagation, workspace/folder model, and session type system. |
| Sessions list spec | src/vs/sessions/SESSIONS_LIST.md | Before changing the sessions sidebar list. Covers the tree widget (WorkbenchObjectTree), renderers, grouping (workspace/date), filtering (type/status/archived/read), pinning, read/unread state, workspace capping, mobile adaptations, storage keys, and registered actions. |
| Mobile spec | src/vs/sessions/MOBILE.md | Before adding any phone-specific UI. Covers the mobile part subclass architecture, viewport classification (phone < 640px), MobileTitlebarPart, drawer-based sidebar, MobilePickerSheet, view/action gating with IsPhoneLayoutContext, and the desktop → mobile component mapping. |
| AI Customizations | src/vs/sessions/AI_CUSTOMIZATIONS.md | Before working on the customization editor or tree view. Documents the management editor (in vs/workbench) and the tree view/overview (in vs/sessions/contrib/aiCustomizationTreeView). |
Common Pitfalls
- Wrong menu IDs: Never use
MenuId.*fromvs/platform/actionsfor Agents window UI. Always useMenus.*frombrowser/menus.ts. - Events instead of observables: Session state must flow through
IObservable, notEvent. Useautorun/derivedfor reactive UI, notonDid*event listeners. - Importing from providers: Non-provider
contrib/*code must never import fromcontrib/providers/*. Extract shared interfaces toservices/orcommon/. IAgentSessionsServicein shared code:IAgentSessionsService(vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService) is a Copilot-provider internal and may be imported only by the Copilot chat sessions provider (contrib/providers/copilotChatSessions/). Shared sessions code (core/services/non-provider contribs, e.g. the sessions list or visible-sessions grid) must stay provider-agnostic and go throughISession/ISessionsManagementService— never reach intomodel.observeSession(...)etc. for lazy loading. This is enforced by an ESLintno-restricted-importsban scoped tosrc/vs/sessions/**(Copilot provider exempted).- Missing entry point import: New contribution files must be imported in the appropriate
sessions.*.main.tsentry point to be loaded (for examplesessions.common.main.ts,sessions.desktop.main.ts,sessions.web.main.ts, orsessions.web.main.internal.ts). - Modifying workbench code: Prefer extending/wrapping workbench classes in the sessions layer over modifying shared workbench components.
- Timeouts as fixes: Never use
setTimeout/disposableTimeout/arbitrary delays to fix bugs or implement behaviour. They are race-prone guesses that mask the real ordering/state problem. Drive logic off deterministic signals instead — observables (autorun/derived), explicit events (onDidChange*), lifecycle phases, or awaiting the actual async operation. - Stashed state read back later (side-channels): Never stash a value on a service during one method call and read it back from a separate query later, assuming it is still valid (e.g. a
Set/flag set inopenSessionand consumed by ashouldX()pull-API). This is fragile temporal coupling. Instead, make it reactive state that is set atomically together with its source of truth and consumed reactively. Example: per-activation intent like "open in background / preserve focus" is exposed as anIObservableset in the same transaction asactiveSession(via a single internal setter so it can never go stale), and read with.read(reader)in the consumer'sautorun— never via a consume-once getter. - Provider-owned model/mode selection belongs in the loaded chat model, with draft persistence driven by debounce: For AHP-backed chats,
setModel/setAgentmust push the selection into the loadedIChatModel.inputModel(like_updateChatSessionState) and let the draft-sync debounce emitchat/draftChanged. Do not immediately dispatch a model/agent-only draft from the provider, because it can overwrite unsaved typed text before the debounced full input-state draft is persisted. - Blocking on a "pending/waiting" state instead of creating + upgrading: When an entity (e.g. a draft session) depends on something that registers asynchronously, don't withhold creation behind a pending/waiting state. Prefer creating immediately with the best available data, then replace/upgrade it once the awaited dependency arrives (driven by an
onDidChange*/observable signal), cancelling the upgrade if the user changes the inputs meanwhile. Do not bound the upgrade with a timeout or even a lifecycle milestone likeLifecyclePhase.Eventually— an agent host connects lazily and can surface its session types arbitrarily late, which would lock in the wrong fallback. Let the upgrade listener live for the consumer's lifetime instead. - Over-commenting: Don't write long explanatory comments narrating what the code does or justifying ordinary patterns. Hard rules: JSDoc = 1–2 short sentences max (never enumerate every branch/feature, restate the signature, or list what the function does NOT do); inline method comments = 1 line max, only for a genuine workaround/non-obvious constraint, never to narrate the next statement. Default to no comment — if code needs a paragraph to explain, rename/extract instead. Before writing any comment longer than one line, delete it or shorten it to one line.
- Inserting/removing DOM on demand for transient UI (e.g. inline rename inputs): Don't
insertBefore/appendChild+remove()a widget on the tab/row element itself when an interaction starts/ends — that churns the parent's child list and depends on event ordering during teardown. Also don't eagerly build a heavy widget (e.g. anInputBox) per row "just in case", since most rows never use it. Instead, create a stable, empty container alongside the label once, toggle its visibility via a CSS class on the row (e.g..editing), and create the widget inside that container lazily only while editing — disposing it and emptying the container (reset(container)) when done (InputBox.dispose()does not detach its own node). Prefer the shared themed widget (InputBox+defaultInputBoxStyles) over a hand-rolled<input>. - Collapsing distinct provider identities in pickers: Do not collapse extension-backed chat session ids (e.g.
copilotcli) and agent-host ids (e.g.agent-host-copilotcli) based only on friendly names or well-known provider enums. They can coexist in the Agents window and route to different infrastructure; keep the exact session type id through selection/delegation and hide ambiguous legacy targets when an agent-host target supersedes them. - Resolving a session's provider via the create-only tracking map: On the agent host, resolve the owning provider for any per-session operation (createChat, disposeChat, sendMessage, …) through
AgentService._findProviderForSession, never the raw_sessionToProvidermap. That map is populated only bycreateSession, so a restored session (alive in the state manager after a host restart but never created in this process) is absent from it — a direct lookup throwsno provider for sessionand silently breaks the feature (e.g. Add Chat did nothing for restored sessions while messaging worked, because messaging already used the fallback)._findProviderForSessionfalls back to the session URI's scheme provider, which is what makes restored sessions work. - Dispatching per-chat side-channel actions (agent/model) to the session URI: An agent-host session can own multiple peer chats, each with its own backend conversation (
CopilotAgent._chatSessions). Conversation side-channel actions likeSessionAgentChanged/SessionModelChangedmust be dispatched to the per-chat turn channel (_resolveTurnDispatchChannel, which carries achatIdfragment for peer chats), notsession.toString(). The session URI resolves to the session's default chat (_sessions), so dispatching there silently applies the change to the wrong conversation and an additional chat never sees the agent/model swap. The host must also forward thechatChannelthroughagentSideEffects.handleAction→changeAgent/changeModel, which apply it to_chatSessionswhen present. The protocol modelssummary.agent/summary.modelat session level only, so equality guards comparing against session summary are valid for the default chat but must be skipped for peer chats. - Do not infer or fall back from a peer chat channel after progress was emitted: Agent progress signals for chat-scoped actions, especially tool-call readiness and permission requests, must be emitted with the exact
ahp-chat://...channel that owns the tool. Do not recover by scanning active turns, remappingChatToolCallConfirmed, or usingparseDefaultChatUri(...) ?? sessionUriinAgentSideEffects; malformed/misrouted chat channels should fail loudly so the producer or dispatch path is fixed.handleToolCallConfirmedand_toolCallAgentsmust use the chat channel URI containing the tool call; keying by the parent session URI makes confirmations miss the pending SDK request. - Do not synthesize default chat URIs in the workbench handler:
AgentHostSessionHandlermust source the upstream default chat URI from hydratedSessionState.defaultChat/SessionState.chatsand store that mapping in its chat-resource-to-upstream-URI map. CallingbuildDefaultChatUri(session)in the handler assumes one server URI shape and hides protocol/provider bugs; dispatch turn lifecycle and pending/input actions through the mapped upstream chat URI instead. - Model subagents as chats, not sessions: A subagent spawned from a tool call belongs to the parent session as an additional chat with
origin.kind === "tool", hidden from the chat tab strip. Do not callrestoreSessionfor subagents; that creates_sessionStateswithout a matching_chatStatesentry, so later chat actions hit "Action for unknown chat". Add a chat on the parent session and dispatch the subagent turn to that chat URI. - Keep case-sensitive ids out of URI authority: URI authorities are case-insensitive, so do not place tool call ids in the
ahp-chatauthority. Subagent chat URIs use a stablesubagentauthority and put the encoded tool call id in the path; usebuildSubagentChatUri(...)instead ofbuildChatUri(..., \subagent-${toolCallId}`)`. - Selected custom agent must be in the SDK's
customAgents, not justpluginDirectories: The Copilot SDK validates the session-startagent:option (passed tocreateSession/resumeSession) against thecustomAgentslist by name only — it does NOT consultpluginDirectories.copilotSessionLauncher._buildSessionConfigdeliberately omits agents from file-dir plugins fromcustomAgents(relying on the SDK'spluginDirectoriesdiscovery to avoid duplicates), so selecting a plugin/extension-contributed agent (e.g. "Inbox") otherwise fails withCustom agent '<name>' not found. The fix (toSdkSessionCustomAgents) force-adds the resolved selected agent intocustomAgentswhile every other file-dir agent still loads viapluginDirectories. Note the agent picker offers VS Code chat modes fromIChatModeService, but onlyplugin/extensionstorage agents are synced to the host (SYNCABLE_STORAGE_SOURCES);user/localagents are never synced, so_resolveAgentNamereturnsundefinedfor them and noagent:is sent. - Derive SDK custom-agent names exactly like
parseAgentFile:_resolveAgentNameresolves the selected agent through the plugin parser, which trims the frontmattername(getStringValue('name')?.trim() || nameFromFile). When building the SDKcustomAgentslist (toSdkCustomAgents), derive the name the same way (?.trim() || agent.name); reading the raw frontmatternamewithout trimming yields a config name that won't match the trimmedresolvedAgentName, so the SDK still rejects the session withCustom agent '<name>' not found. - Peer chats have no server
summary, so dedup side-channel dispatch against the last value sent for that chat: equality guards before dispatchingSessionModelChanged/SessionAgentChangedcompare againstsummary.model/summary.agent, which only exist for the session's default chat. For peer chats, track the last-dispatched model/agent on theAgentHostChatSessioninstance (auto-cleaned on dispose) and diff against that — otherwise every peer-chat turn redundantly re-dispatches (and re-resolves the agent), and an intentional "clear selection" (undefined) can't be detected. - Scrollable transcript surfaces must use workbench scrollbars: Don't make Agents/voice transcript regions scrollable with native
overflow-y: autoon the content node. Wrap transcript content inDomScrollableElement/list widgets so scrollbars match VS Code theming and remain usable in narrow auxiliary-window layouts. - Background-sending a multi-chat composer must reset the composer before dispatching the send, not concurrently: in
NewChatInSessionWidget._send, creating the replacement untitled chat (openNewChatInSession({ forceNew: true })→provider.createNewChat) and the fire-and-forget backgroundsendRequestboth reach into shared chat-session state (acquireOrLoadSession/getOrCreateChatSession) for chats in the same group. Running them concurrently (send first, reset second) raced and left the sent chat stuck spinning with its message never dispatched, plus a second empty "New Chat" tab. Fullyawaitthe composer reset first, then fire the background send so it runs on its own. - Chat tab order belongs in the renderer, not the providers: providers report chats in unstable orders — the agent host re-sorts its
state.chatscatalog when a chat finishes a turn (moving the just-completed one last). Don't try to fix a "tab jumps to the end on completion" bug inside one provider (it won't cover the others). Instead keep the provider's order in the renderer's rebuild autorun (chatCompositeBar.ts) and only move in-composerUntitledchats to the end. - A new chat must report
SessionStatus.Untitleduntil its first request is sent, regardless of how the provider creates it:sessionView.tsonly shows the new-chat composer (which owns the Alt+Enter background-send handler) whenactiveChat.status === Untitled. The agent host commits a new peer chat eagerly, so its host status isCompleted— surfacing the standard chat widget and breaking background send. Gate the chat's presented status on a provider-sideisNewflag (AdditionalChat.markNew/markSent, set increateNewChatand cleared insendRequest's committed-chat branch), not on the host-reported status. - Service operations should return a result or throw, not
undefinedfor unsupported cases: capability-gated operations likeforkChatInSessionmust throw when the provider/session cannot perform them. Keep fallback decisions in the caller before invoking the service instead of encoding fallback as anundefinedservice result. - Drop a fork when its turn point is unknown, don't forward it empty: in
AgentService.createChat/createSession, if the requested forkturnId/turnIndexresolves to no source turns, setfork: undefinedand fall through to a fresh create. Forwarding the fork with an empty turn slice makes the Copilot provider callsessions.forkwith notoEventId, inheriting the entire backend conversation while the new chat UI is seeded with zero turns — an inconsistent hidden-history chat. - A responsive-layout autorun must re-baseline (not react) to controller-driven restores, holding the flag across the async reveal: the desktop [D7] responsive sidebar hides the sessions sidebar when small + editor + aux-bar are all open. Switching sessions restores layout via two async paths — the desktop aux-bar restore (
openView/openViewContainer) and the base controller's editor working-set apply (_applyWorkingSet, which reveals the editor part after anawaitand runs on aSequencermicrotask). Both reveal parts in a later autorun run, so an inline "same-run session changed" check only absorbs the synchronous transition and the async reveal still auto-hid the sidebar on navigation. Fix: a shared base-controller_withSessionLayoutRestore(work)epoch wraps both restore paths (the working-set wrap is the critical one for non-modal editors); the D7 autorun re-baselines_previousSpaceConstrainedwhile_isRestoringSessionLayoutis true. Also gate the constrained derivation on!multipleSessionsVisibleObsso the feature is disabled with multiple sessions visible. Never use asetTimeoutto bridge the async reveal — tie the flag to the actual promise. - A promise-tied "epoch" helper must decrement synchronously for void/sync work, only deferring for real Promises:
_withSessionLayoutRestoreincrements a depth counter, runswork(), and decrements when done. If it always schedules the decrement on a microtask (Promise.resolve(result).finally(...)) — even whenwork()returnsundefined(the common no-op restore, e.g. a session with no workspace) — the depth stays elevated for the entire synchronous caller/test body, so_isRestoringSessionLayoutreadstrueforever and the consumer (D7) silently stops acting. Only defer the decrement whenwork()returns a thenable; for void/sync (or throwing) work, decrement in thefinally.
Capturing Feedback (meta-rule)
Whenever the user flags a wrong pattern, rejects an approach, or gives design/rules feedback, automatically add it as a concise pitfall/learning to this Common Pitfalls section (or the most relevant spec doc) in the same change — without being asked again. Keep each entry 1–3 sentences: the anti-pattern, why it is wrong, and the preferred pattern.
Validating Changes
You must run these checks before declaring work complete:
npm run typecheck-client— TypeScript compilation check. Do not runtscdirectly.npm run valid-layers-check— MANDATORY. Catches layering violations. If this fails, fix the imports before proceeding.scripts/test.sh --grep <pattern>— unit tests for affected areas
Related skills
More from microsoft/vscode and the wider catalog.

tool-rename-deprecation
Preserve backward compatibility when renaming tool references by maintaining legacy name arrays.

update-screenshots
Download and commit screenshot baselines from VS Code CI runs after visual changes are detected.

accessibility
Ensure VS Code features are accessible to all users with keyboard navigation, screen reader support, and ARIA annotations.

agent-sessions-layout
Agents workbench layout — covers the fixed layout structure, grid configuration, part visibility, editor modal, titlebar, sidebar footer, and implementation requirements. Use when implementing features or fixing issues in the Agents workbench layout.

expect
Verify React/web code changes in a real browser before claiming completion.

react-doctor
Scan React code for security, performance, accessibility, and architecture issues with a 0–100 health score.