How to install sessions
npx skills add https://github.com/microsoft/vscode --skill sessionsFull 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.
fix-errors
Guidelines for fixing unhandled errors from the VS Code error telemetry dashboard. Use when investigating error-telemetry issues with stack traces, error messages, and hit/user counts. Covers tracing data flow through call stacks, identifying producers of invalid data vs. consumers that crash, enriching error messages for telemetry diagnosis, and avoiding common anti-patterns like silently swallowing errors.
azure-pipelines
Use when validating Azure DevOps pipeline changes for the VS Code build. Covers queueing builds, checking build status, viewing logs, and iterating on pipeline YAML changes without waiting for full CI runs.
accessibility
Primary accessibility skill for VS Code. REQUIRED for new feature and contribution work, and also applies to updates of existing UI. Covers accessibility help dialogs, accessible views, verbosity settings, signals, ARIA announcements, keyboard navigation, and ARIA labels/roles.
memory-leak-audit
Audit code for memory leaks and disposable issues. Use when reviewing event listeners, DOM handlers, lifecycle callbacks, or fixing leak reports. Covers addDisposableListener, Event.once, MutableDisposable, DisposableStore, and onWillDispose patterns.
hygiene
Use when making code changes to ensure they pass VS Code's hygiene checks. Covers the pre-commit hook, unicode restrictions, string quoting rules, copyright headers, indentation, formatting, ESLint, and stylelint. Run the hygiene check before declaring work complete.
author-contributions
Identify all files a specific author contributed to on a branch vs its upstream, tracing code through renames. Use when asked who edited what, what code an author contributed, or to audit authorship before a merge. This skill should be run as a subagent — it performs many git operations and returns a concise table.