Install
openclaw skills install skills-sh:microsoft/vscode/sessionsBefore Making Any Changes MANDATORY: Before writing or modifying any code in src/vs/sessions/, you must read these documents: 1. .github/instructions/coding-guidelines.instructions.md — Naming conventions, code style, string localization, disposable management, and DI…
openclaw skills install skills-sh:microsoft/vscode/sessionsContent is truncated to the stored 64 KiB snapshot.
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.
| 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). |
Paired experiment treatments must resolve atomically: when a prompt and its editable placeholder are separate treatment values, use them only when both are non-empty; otherwise use both defaults so copy from different variants is never mixed. The prompt may omit the placeholder token entirely, in which case it is used literally and placeholder highlighting is simply absent.
Onboarding variations share structural steps and vary only their run step: keep one scenario for workspace selection, then resolve the experiment/developer variation when the run step executes. Personalized GitHub prompts use existing authentication silently, stay within a bounded cancellable lookup, verify that the selected draft workspace is still current, and fall back to the default prompt without surfacing an error.
Agent-host onboarding readiness comes from advertised session types, not provider registration: an agent-host provider exists before its root state connects, while its sessionTypes stay empty. Gate tours that need a usable host on a context key derived from any local-agent-host/agenthost-* provider exposing a session type, and update it from ISessionsManagementService.onDidChangeSessionTypes.
Diagnostic log text is not a unit-test contract: add consistently prefixed, actionable logs, but do not add tests that assert log messages or levels. Validate the underlying behavior and keep diagnostics free to evolve.
Shared visual-module gates must not activate broader layout contracts: the Agents window may opt into shared editor-tab styles through a tab-specific root class, but must not apply the broad style-override class unless it also loads every matching Modern UI layout module. Keep shared tab runtime metrics aware of both gates, and preserve structural behavior such as a sticky add-tab action when removing a Sessions-owned stylesheet. Since the Agents workbench is always modern, chat-tab presentation belongs in the owning chatCompositeBar.css, scoped through .session-chat-tabs-bar and chat-specific classes while consuming shared state tokens; do not add Sessions selectors to or rewrite the shared editor stylesheet.
Minimum-size activation across the Sessions/Editor split must be symmetric and layout-aware: when either part is at minimum width, pointer or keyboard activation expands it by shrinking its sibling to minimum width. In single-pane layout, the Editor grid node's effective minimum includes the visible docked Auxiliary Bar width; using editorPartView.minimumWidth alone collapses Details.
Do not inject ISessionsService into editor-part construction: the sessions service depends on editor parts through the sessions-part graph, so injecting it into SinglePaneMainEditorPart causes recursive service instantiation during startup. Prefer lower-level services such as ILabelService when the editor only needs resource presentation.
Workspace-folder labels must distinguish physical paths from repository identity: a worktree URI basename is the worktree directory, not the repository name. Route breadcrumb and workspace-projection labels through the delayed IWorkspaceFolderLabelService; the Agents implementation may read ISessionsService.activeSession because BreadcrumbsModel is created from BreadcrumbsControl.update() after editor-part construction, but never inject ISessionsService into SinglePaneMainEditorPart itself. Breadcrumbs omit a workspace root whenever only one folder exists in any VS Code window; folder changes rebuild the model and recompute labels.
Session repository labels come from ISessionFolder.name, not the repository URI basename: providers may supply a richer display identity such as org/repo. Use the session folder name for plain and verbose labels, and use URI basenames only as the final no-session fallback.
Breadcrumb item equality must include presentation overrides: BreadcrumbsWidget.setItems retains equal prefix items, so a custom FileElement.label must participate in equality. Otherwise folder or session-label changes for the same URI update the model but leave stale DOM text.
Animation performance must preserve perceptual smoothness: reducing a continuous title shimmer to 10 stepped updates per second makes the sweep visibly choppy even if paint counts improve. Use a smooth baseline such as 30 updates per second, then measure the remaining performance win; do not optimize decorative motion by callback counts alone.
Pet placement must align the visible sprite, not only its absolute-positioning box: anchoring the button at bottom: 100% leaves the pet visually detached because the input stack has top padding and transient confirmation/question surfaces add their own top margin. Keep the host on the complete stack and derive the optical offset from the actual input-to-host inset, capped at the confirmation/question alignment; one fixed deeper offset makes the bare input look overlapped.
Keep only the rendering pet's speech bubble inside its input bounds without turning the pet around: the speech sprite's visible pixels overhang the button on the right, so dragging the rendering pet to the input's right edge can clip the ellipsis. Move the mirrored bubble fully to the pet's left with only its tail touching; do not center it over the pet, and do not apply the special treatment to yapping or any other state.
Do not conflate custom-agent selection with Agent Host execution mode: chat.modeChange describes workbench mode/custom-agent picker selections, while interactive, plan, and autopilot are Agent Host execution modes and belong in agentHost.executionModeChanged. Preserve the SDK-native mode event as a peer rather than reshaping either axis into the other.
Picker telemetry must use the scoped session and active chat: Agents Window action view items can belong to a non-active visible session or peer chat. Resolve previous selection and request counts from scoped ISessionContext.session.activeChat, never a window-global picker model or parent session resource.
A sash element's left/top is the hit-area edge, not the split boundary: SplitView.getSashPosition returns the exact boundary after the preceding view, then Sash.layout subtracts half the sash size so the draggable element is centered on that boundary. Align the Sessions/Editor and bottom-Panel grid sash hit areas to agents.layout.floatingPanelGap; do not apply that token to independent geometry such as the Auxiliary Bar's leading padding.
Wrong menu IDs: Never use MenuId.* from vs/platform/actions for Agents window UI. Always use Menus.* from browser/menus.ts.
Editor-group header existence must not depend on an editor-provided scope: the group-level showHeader option enables both header actions and header-hosted breadcrumbs; each menu item gates itself with an active-editor when clause. An editor's optional scopedInstantiationService property only supplies context for evaluating those clauses, and breadcrumbs compose into the same fixed-height header row rather than creating a second persistent slot.
The empty Files breadcrumb belongs to the editor area, not the docked detail: expose the session working directory as the empty Files input's breadcrumb resource only while the editor area is visible. A detail-only layout may keep header actions visible, but it must not show a root breadcrumb for hidden editor content; preserve the underlying resource separately for serialization and refresh breadcrumbs from the editor-part visibility signal.
Pass the complete session workspace to the empty Files input, not a preselected folder URI: the input currently derives the first mounted working directory as its breadcrumb resource at one documented fallback point. A complete multi-root design can then change that input-owned decision to a workspace-level breadcrumb that identifies the workspace and exposes all roots, without rewiring every caller.
A superseded managed-tab reconcile must not publish input state after an await: foreign-editor cleanup can yield while a newer session queues a successor reconcile. Check the reconcile generation immediately after cleanup and before updating the retained Files input's workspace, so only the current reconcile can publish breadcrumbs.
Editor-header DOM must mirror its visual groups: breadcrumbs are a direct child of the header and a sibling of one actions container; that actions container directly owns the primary and secondary action hosts. Create the actions container and its hosts inside the showHeader construction branch, then attach it after breadcrumbs so ownership and DOM order stay explicit.
Editor-header state belongs to the owned nodes and widgets, not their parent or CSS marker classes: do not add placement/state classes to the title parent, branch layout on the showHeader option after construction, or infer action presence from has-no-actions. Use the created header element to distinguish placement and MenuWorkbenchToolBar.getItemsLength() to derive visibility; the toolbar may still set its internal compatibility class, but header logic and styling must not depend on it.
Secondary editor-header actions need a structural trailing column: custom secondary view items can stretch their host, so margin-left: auto plus content-sized flex is not sufficient. Use an actions grid with a flexible leading column and explicit secondary/separator/layout columns so secondary-only states remain trailing regardless of child sizing.
Breadcrumbs and editor-header actions share an edge, not a parent gap: do not add column-gap to the header row; it creates a visible hole between the flexed breadcrumb box and actions box. Breadcrumb content owns its trailing breathing room, while the two sibling containers remain contiguous.
Header actions must not retain an empty leading grid track beside breadcrumbs: when breadcrumbs are visible, make the actions grid content-sized with auto columns; otherwise its flexible primary column becomes a visible hole inside the actions element. Only use the full-width 1fr primary column when breadcrumbs are absent, where it keeps primary actions left-anchored and secondary/layout actions trailing.
Header breadcrumb layout measures the control, not its padded wrapper: .breadcrumbs-below-tabs has Sessions-owned left padding, and clientWidth includes that padding. Pass breadcrumbsControl.domNode.clientWidth to BreadcrumbsControl.layout() so the widget receives its actual usable width and does not overrun or clip its trailing scroll range.
Header-hosted breadcrumbs inherit the shared header background: the breadcrumb widget generates a light-theme background on its inner .monaco-breadcrumbs surface, which contrasts with the editor-header tab background inherited by sibling actions. Override both the control and inner widget surface to transparent in the Sessions-scoped header breadcrumb stylesheet so the entire row uses one theme-owned surface color.
Hidden header widgets must collapse their parent box: hiding only an inner control leaves the stable parent with its previous inline width, which can push sibling header actions to the right. Mirror visibility onto the parent container and define the common row height on the shared header, not on one child widget, so Changes actions and file breadcrumbs align without stale geometry.
An empty editor-group header must collapse completely: keep the stable header content hidden by default and show it only when breadcrumbs are visible or a primary/secondary menu host has actions. Hiding only the children can leave the header border or reserved height visible as an empty row.
Title content spans the full editor-group width: breadcrumbs and tabs use the full group width in every layout. Only the editor pane is narrowed beside the docked detail panel; do not thread the editor content inset through EditorTitleControl.layout().
Agents header styling must not modify shared editor CSS: keep vs/workbench editor styles byte-for-byte unchanged so normal VS Code windows cannot regress. Add an explicit header-placement class in shared DOM code, then put every visual override under a Sessions-owned stylesheet scoped to .agent-sessions-workbench.dock-detail-panel.
Header presentation has its own control: EditorGroupView passes menuIds and showHeader to EditorTitleControl, which creates EditorHeaderControl when enabled. EditorHeaderControl owns header DOM, breadcrumbs, menu toolbars, scoped action rendering, visibility, and fixed height; EditorTitleControl owns tabs and includes the header height in its total.
The title-owned header is a child of the title container and follows its recreate lifecycle: create it after the tabs inside EditorTitleControl.parent, include its height in the title control's layout result, and rebuild it with tabs/breadcrumbs after clearNode(parent) when editor options change. Do not make it a sibling that requires separate DOM cleanup and duplicate group layout subtraction.
Header visibility updates distinguish creation from live changes: updateHeaderVisibility(relayout) updates DOM/state in both cases, but creation and option-driven reconstruction pass false because their caller already owns layout. Live menu-item and breadcrumb changes pass true so the parent group is explicitly relaid out.
Durable chat source/origin references: Store only turnId in durable fork/side-chat references. Active versus historical is mutable lifecycle state that consumers must resolve against the current activeTurn and retained turns when needed; do not encode lifecycle state in the reference type.
Selected side-chat text is an immutable snapshot, not a live range: SideChatSource.selection / ChatOrigin.selection preserve the exact text captured at side-chat creation time. Never model it as offsets into the source transcript or try to recompute it from later DOM/protocol state.
ChatSource is fully discriminated: Fork and side-chat sources both require explicit kind plus stable top-level turnId. Do not add no-kind compatibility helpers or route by structural property presence; switch directly on source.kind.
Sessions menu ids must live in the shared menu registry: Do not declare sessions-owned new MenuId(...) constants ad hoc inside individual parts. Add them to browser/menus.ts under Menus with discoverable SessionsEditor... names so ownership and reuse stay obvious.
Events instead of observables: Session state must flow through IObservable, not Event. Use autorun/derived for reactive UI, not onDid* event listeners.
Importing from providers: Non-provider contrib/* code must never import from contrib/providers/*. Extract shared interfaces to services/ or common/.
IAgentSessionsService in 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 through ISession/ISessionsManagementService — never reach into model.observeSession(...) etc. for lazy loading. This is enforced by an ESLint no-restricted-imports ban scoped to src/vs/sessions/** (Copilot provider exempted).
Missing entry point import: New contribution files must be imported in the appropriate sessions.*.main.ts entry point to be loaded (for example sessions.common.main.ts, sessions.desktop.main.ts, sessions.web.main.ts, or sessions.web.main.internal.ts).
Modifying workbench code: Prefer extending/wrapping workbench classes in the sessions layer over modifying shared workbench components.
Do not repeat subagent identity beside the open-chat pill: The subagent pill's title is the complete inline affordance. Do not render the agent name or a generic "Subagent" phrase before it; that duplicates identity and adds visual noise.
Subagent model metadata is differential inline, but complete in the hover: Show the subagent's model beside the pill by default and hide it only when it concretely matches the parent chat's selected model. Compare canonical ids, registered display names, and the parent input's selected-model metadata (active turns may not yet expose resolved response metadata). Keep the unfiltered subagent model in the hover/ARIA label regardless of inline visibility; if the parent model is unresolved, still show it inline because no match can be established.
Optional metadata owns its separator: Keep separators such as · on the conditional metadata element and create that element hidden. An empty optional label must never leave punctuation behind while its reactive visibility is still resolving.
Live numeric labels must not jitter: Apply font-variant-numeric: tabular-nums with font-feature-settings: "tnum" as a fallback to elapsed-time and other numeric labels that update in place.
Collapsed work summaries must quantify what they hide: Prefer outcome-oriented copy such as "Completed 6 steps in 2m" over vague elapsed-only text such as "Worked for a few minutes." Count the visible items placed inside the disclosure so the summary matches what expanding it reveals.
Created-session result pills belong with the final response, not the collapsed work steps: A completed create_session / create_chat pill is a durable outcome and navigation affordance. Render it only once the response completes and order it after the final response markdown, so it stays visible beside the completed-turn adjuncts without being repositioned while focused.
Logical final markdown is not necessarily rendered final markdown: With incremental rendering, response completion accelerates the buffer but does not synchronously drain it to the DOM. Delay completed-work disclosure and bottom-summary placement until the final markdown part reports that its morpher has drained, then retry from that signal.
Custom action proxies must propagate owner-observed state: When an action view wraps a menu action in a proxy, state that controls surrounding UI (such as enabled/available) must also be written to the original menu action observed by the owner. Re-subscribe on IActionViewItemService.onDidChange for late factory registration, but do not assume the replacement proxy's state automatically reaches the menu action.
Editor feedback glyph placement: Use Monaco's lineNumberClassName when the feedback affordance should replace the number only while its line is hovered; it eliminates a dedicated glyph lane while preserving the number at rest. Style the line-number pseudo-element as the full feedback control, including its themed hover background, so its visual and click target match.
Line-number decoration tooltips belong in Monaco decoration options: A lineNumberClassName node is regenerated as the editor renders and scrolls, so DOM-managed hovers can silently attach to a stale or never-decorated element. Set the localized lineNumberHoverMessage with the same decoration instead; Monaco's glyph hover controller follows the rendered line-number lifecycle.
Compact multi-diff control alignment: The file-header twistie, unchanged-region expand control, and fold control form one visual column in the Agents editor. Remove the header content's left padding and use the same small inset for both unchanged-region controls; do not let the shared multi-diff defaults leave each control at a separate horizontal offset.
Embedded multi-diff gutters need a shared minimum width: Each embedded editor otherwise sizes line numbers from its own largest line number, causing the content and nearby feedback glyph to appear to drift between file entries. Set a common lineNumbersMinChars width for the compact multi-diff; it remains stable through three-digit line numbers and grows only when a file exceeds that reserved capacity.
Editor-content overlays must anchor to the inset pane, not the full editor group: In single-pane mode the editor group spans both the editor and docked detail panel, while EditorGroupView.editorPaneContainer bounds only the editor content. Mount submit/navigation overlays to that pane container so their bottom-right position stays inside the diff when the detail panel is visible or resized.
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.
The prompt timeline dock treats the bottom as the latest prompt and pairs both hover directions: when the transcript is fully scrolled down, resolve the final prompt directly rather than scanning every prompt's top offset. Hovering either a dot or its prompt row must preview both elements; when the dots are capped, map the row to the nearest sampled dot.
Sticky prompt navigation must match the rail/title reveal: Previous/Next and the sticky title must all reveal the prompt (request) row aligned to the top via the shared reveal(requestId) — the same path the dock/ruler rail uses. Do not align the following response to keep the header pinned, and do not add a "navigation pin" that forces the header to stay visible after a jump: the header is a top:0 overlay, so it would cover the freshly top-aligned prompt (the prompt shows only a sliver). Let the header follow scroll tracking (it hides once the prompt is at the top), consistent with the dock. Use the chat request-bubble hover background (--vscode-chat-requestBubbleHoverBackground, toolbar hover as fallback), composited over the opaque panel base, for the sticky title affordance rather than an underline.
Sticky prompt header transition is a label roll, not a moving band: on prompt change the label text rolls (WAAPI slide+fade of absolutely-positioned line elements inside an overflow:hidden clip viewport), while the opaque band stays fixed. Do NOT translate the whole band to get an Explorer-style push-off: the band would move above the transcript top (its container .interactive-session is overflow:visible, so it'd overlap the session header) and clipping it would cut the band's soft drop-shadow. The roll gives the "header gives way to the next" feel with none of that risk. Gate the roll on the header already being visible (snap on first appearance/jumps) and honor prefers-reduced-motion.
Sticky prompt header must mirror the session header's box model, not the message column: the band lives in a two-level structure — an outer full-width .prompt-timeline-sticky (positioning + reveal only), a .prompt-timeline-sticky-content centered host (max-width: 950px; margin: 0 auto; padding: 0 10px, matching .session-view-centered-content + .chat-composite-bar.session-header-bar's 10px side padding), and an inner .prompt-timeline-sticky-band (flex: 1) that carries the background/border/shadow (mirroring .chat-composite-bar-header). This makes the band's background align exactly with the session header's bottom-border line above it. Do NOT paint the background on the full-width outer (bleeds past the header and over the scrollbar gutter — a ~1px right-edge "bump") or on the 950 message column (20px wider than the header, since the header is inset 10px). The band's padding-left: 22px (16px icon + 6px gap) reproduces the header's status-icon column so the prompt text lines up with the title and the 32px-inset prompts below.
Grid onDidChange is not a sash-drag signal: the workbench SerializableGrid/GridView onDidChange fires for size changes and view add/remove, but not internal splitview sash drags. If logic must react to a part node being resized by a sash, route it through that part's layout(width, ...) callback, which receives the in-progress node width.
Docked detail collapse must use the raw sash width before clamping: the docked auxiliary bar keeps a minimum visible width, so checking the clamped width can never detect a drag-to-zero collapse. Decide collapse from the raw requested sash width, then route the hide through setPartHidden(AUXILIARYBAR_PART) so context keys and per-session capture stay in sync.
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 in openSession and consumed by a shouldX() 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 an IObservable set in the same transaction as activeSession (via a single internal setter so it can never go stale), and read with .read(reader) in the consumer's autorun — 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 / setAgent must push the selection into the loaded IChatModel.inputModel (like _updateChatSessionState) and let the draft-sync debounce emit chat/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 like LifecyclePhase.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. an InputBox) 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.
Permission picker copy must stay provider-neutral and aligned: Reuse the same labels and descriptions across Copilot Chat and Agent Host permission pickers. Avoid provider-specific phrasing such as "Copilot uses..." when the same choice appears in the Agents window.
Interactive tool denial must use the SDK's interactive-denial result, not a hard reject: In the Agent Host Copilot provider, a user choosing Skip must resolve the permission request with denied-interactively-by-user so the SDK feeds the denial back to the model and continues the turn. Reserve reject for abort, disposal, or requests that cannot be presented to the user; returning it for a renderer decision terminates the session turn.
Internal Agents workspace in recent history: Collapse every internal User/agent-sessions.code-workspace variant into one canonical Agents Window entry. Recognize the reserved path across profile and worktree user-data directories, and make the single picker entry point at the current environment's Agents workspace.
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 _sessionToProvider map. That map is populated only by createSession, 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 throws no provider for session and silently breaks the feature (e.g. Add Chat did nothing for restored sessions while messaging worked, because messaging already used the fallback). _findProviderForSession falls 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 like SessionAgentChanged/SessionModelChanged must be dispatched to the per-chat turn channel (_resolveTurnDispatchChannel, which carries a chatId fragment for peer chats), not session.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 the chatChannel through agentSideEffects.handleAction → changeAgent/changeModel, which apply it to _chatSessions when present. The protocol models summary.agent/summary.model at 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, remapping ChatToolCallConfirmed, or using parseDefaultChatUri(...) ?? sessionUri in AgentSideEffects; malformed/misrouted chat channels should fail loudly so the producer or dispatch path is fixed. handleToolCallConfirmed and _toolCallAgents must 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: AgentHostSessionHandler must source the upstream default chat URI from hydrated SessionState.defaultChat / SessionState.chats and store that mapping in its chat-resource-to-upstream-URI map. Calling buildDefaultChatUri(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.
Opening a subagent editor must carry the exact upstream chat channel: A fragment-only editor resource forces AgentHostSessionHandler to rediscover the child in SessionState.chats, which races catalog hydration and renders "Cannot resolve chat". Encode the exact ahp-chat://subagent/... channel in an internal query parameter, validate its chat id and owning backend session, and subscribe directly.
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 call restoreSession for subagents; that creates _sessionStates without a matching _chatStates entry, 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-chat authority. Subagent chat URIs use a stable subagent authority and put the encoded tool call id in the path; use buildSubagentChatUri(...) instead of buildChatUri(..., \subagent-${toolCallId}`)`.
Selected custom agent must be in the SDK's customAgents, not just pluginDirectories: The Copilot SDK validates the session-start agent: option (passed to createSession/resumeSession) against the customAgents list by name only — it does NOT consult pluginDirectories. copilotSessionLauncher._buildSessionConfig deliberately omits agents from file-dir plugins from customAgents (relying on the SDK's pluginDirectories discovery to avoid duplicates), so selecting a plugin/extension-contributed agent (e.g. "Inbox") otherwise fails with Custom agent '<name>' not found. The fix (toSdkSessionCustomAgents) force-adds the resolved selected agent into customAgents while every other file-dir agent still loads via pluginDirectories. Note the agent picker offers VS Code chat modes from IChatModeService, but only plugin/extension storage agents are synced to the host (SYNCABLE_STORAGE_SOURCES); user/local agents are never synced, so _resolveAgentName returns undefined for them and no agent: is sent.
Derive SDK custom-agent names exactly like parseAgentFile: _resolveAgentName resolves the selected agent through the plugin parser, which trims the frontmatter name (getStringValue('name')?.trim() || nameFromFile). When building the SDK customAgents list (toSdkCustomAgents), derive the name the same way (?.trim() || agent.name); reading the raw frontmatter name without trimming yields a config name that won't match the trimmed resolvedAgentName, so the SDK still rejects the session with Custom 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 dispatching SessionModelChanged/SessionAgentChanged compare against summary.model/summary.agent, which only exist for the session's default chat. For peer chats, track the last-dispatched model/agent on the AgentHostChatSession instance (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: auto on the content node. Wrap transcript content in DomScrollableElement/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 background sendRequest both 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. Fully await the composer reset first, then fire the background send so it runs on its own.
Chat tab order is the provider's stable creation order; don't reorder in the renderer: the agent host delivers state.chats in stable creation order (append on add, replace-in-place on update — see agentHostStateManager/the session reducer), and a genuinely new chat is appended last. The renderer's rebuild autorun (chatCompositeBar.ts) must render that order as-is. Do not partition/move in-composer Untitled chats to the end: a draft is already last, and reordering by status makes a tab jump when a draft commits out of creation order (e.g. sending the 3rd of three drafts first moved it to the front). A chat's Untitled presentation (via AdditionalChat._isNew, needed so sessionView.ts shows the composer) is independent of tab order and must not drive it. Also note _restorePeerChats (agentService.ts) must seed restored chats in getChats() order, not in Promise.all resolution order, or the catalog scrambles on reload.
A new chat must report SessionStatus.Untitled until its first request is sent, regardless of how the provider creates it: sessionView.ts only shows the new-chat composer (which owns the Alt+Enter background-send handler) when activeChat.status === Untitled. The agent host commits a new peer chat eagerly, so its host status is Completed — surfacing the standard chat widget and breaking background send. Gate the chat's presented status on a provider-side isNew flag (AdditionalChat.markNew/markSent, set in createNewChat and cleared in sendRequest's committed-chat branch), not on the host-reported status.
Service operations should return a result or throw, not undefined for unsupported cases: capability-gated operations like forkChatInSession must throw when the provider/session cannot perform them. Keep fallback decisions in the caller before invoking the service instead of encoding fallback as an undefined service result.
A provisional session abandoned during commit detection must not be returned as successful: its status can remain InProgress after its lifecycle owner is disposed, so consumers waiting for a terminal status never settle. Clean up the provisional session and reject the send when commit detection times out or the connection is lost.
Drop a fork when its turn point is unknown, don't forward it empty: in AgentService.createChat/createSession, if the requested fork turnId/turnIndex resolves to no source turns, set fork: undefined and fall through to a fresh create. Forwarding the fork with an empty turn slice makes the Copilot provider call sessions.fork with no toEventId, inheriting the entire backend conversation while the new chat UI is seeded with zero turns — an inconsistent hidden-history chat.
Side-chat context belongs to the provider, not AgentService message mutation: AgentService records and forwards the side-chat origin, but must not synthesize a first-turn Chat attachment or strip provider-added context on restore. Each supporting provider establishes hidden backend context and removes inherited/provider-added history from the turns it returns.
MessageAttachmentKind.Chat is generic and may reference unloaded chats: resolve chat attachments through a generic async path, enforce same-session ownership before hydration, and restore the referenced session/chat when it is absent from the state manager. Do not name this logic after side chats or assume /btw is its only producer.
Agent capabilities are provider-specific: do not implement side chats for an agent merely because the protocol supports them. Advertise multipleChats.sideChat and run shared side-chat tests only for providers with a complete provider-owned context/restore implementation.
User-created side chats use the standard peer-chat tab model; only tool-origin subagents stay hidden by default: a ChatOriginKind.SideChat chat is a normal user-facing peer chat, so it must flow through visibleChatTabs, the Conversations menu, pickers, and close/reopen like any other peer chat. Do not create a separate editor/detail surface for it; reserve the hidden/read-only default only for tool-origin subagents.
/btw must bypass queue/steer, may anchor to activeTurn, and should activate the new peer chat through the normal sessions API: mark the silent slash command executeDuringRequest so the chat widget invokes it independently, validate its anchor against completed turns or the current active turn, and after creating the side chat activate it via ISessionsService.openChat(...) before sending on that chat. Provider side-chat creation must lock on the new chat, not the source send key. Wrap the first provider prompt with a succinct instruction to prefer explanation over action and avoid work unless explicitly requested; include bounded user-visible active-turn markdown when native forks omit it, then strip the private wrapper from reconstructed visible history. Never inject reasoning or tool payloads.
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 an await and runs on a Sequencer microtask). 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 _previousSpaceConstrained while _isRestoringSessionLayout is true. Also gate the constrained derivation on !multipleSessionsVisibleObs so the feature is disabled with multiple sessions visible. Never use a setTimeout to 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: _withSessionLayoutRestore increments a depth counter, runs work(), and decrements when done. If it always schedules the decrement on a microtask (Promise.resolve(result).finally(...)) — even when work() returns undefined (the common no-op restore, e.g. a session with no workspace) — the depth stays elevated for the entire synchronous caller/test body, so _isRestoringSessionLayout reads true forever and the consumer (D7) silently stops acting. Only defer the decrement when work() returns a thenable; for void/sync (or throwing) work, decrement in the finally.
A quick-chat's workspace-less kind is seeded at adapter construction and only ever promoted — every path that can carry _meta must carry it: AgentHostSessionAdapter seeds its session-kind (QuickChatSessionKind vs WorkspaceSessionKind) from readSessionWorkspaceless(metadata._meta) in the constructor, and _promoteToQuickChatIfWorkspaceless (from update()/setMeta()) later flips it to a quick chat the first time an authoritative _meta says workspace-less — never back, since an absent marker means "not included", not "cleared". So the _meta.workspaceless tag must ride on every metadata path: _refreshSessions()/listSessions, the live _handleSessionAdded(summary) notification, and both ends of the AHP root/listSessions round-trip (protocolServerHandler.ts and remoteAgentHostProtocolClient.ts build their wire items field-by-field, and satisfies SessionSummary does not catch a dropped optional field — this is exactly how the bug shipped twice). Dropping it makes a committed quick chat render under a section header labelled with its raw session UUID, leaking <userHome>/.copilot/chats/<id> as a workspace (breaking the archive-on-delete fallback, list badges, changes/files) until a later _meta heals it. _persistCache must overlay the adapter's live isQuickChat rather than the _metaByRawId snapshot, or the next updateAdapter silently strips the marker from the startup cache. On the host, AgentService.listSessions() overlays _meta.workspaceless onto the provider listing from the persisted agentHost.workspaceless session-database key (AH_META_WORKSPACELESS_DB_KEY) (the providers themselves, e.g. CopilotAgent.listSessions(), do not emit it) so restored sessions carry the tag once the state-manager live summary is gone. The host still keeps the tag on both the summary _meta and SessionState._meta (createSessionState(summary) copies it) so the channels stay consistent. Note the provider tests cannot catch a wire-level drop — MockAgentHostService.listSessions returns stored IAgentSessionMetadata verbatim, bypassing both mappers — so wire regressions need tests at the protocol layer.
Don't infer "quick chat" from workspace === undefined: a session's workspace observable is undefined for genuine quick chats but also transiently/edge-case for workspace-bound sessions, so keying quick-chat UI (context keys, list grouping) on it is imprecise. Expose the intent explicitly via the optional ISession.isQuickChat: IObservable<boolean> (only quick-chat-capable providers set it; absent ⇒ false). The agent-host adapter exposes it as a monotonic observable seeded from readSessionWorkspaceless(metadata._meta) at construction and promoted by later authoritative _meta; non-quick-chat providers omit it. Consume it through isQuickChatSession(session) / session.isQuickChat?.read(reader) ?? false.
Workspace-less is inferred from absent workingDirectory — exclude forks from that inference: in CopilotAgent.createSession, isWorkspaceless must be !sessionConfig.fork && !sessionConfig.workingDirectory. A fork that arrives without an explicit workingDirectory should inherit the source session's context, not be tagged agentHost.workspaceless and dropped into a scratch dir + quick-chat system prompt.
On a failed quick-chat create, don't activate an unrelated draft: SessionsService.openQuickChat must, on createQuickChat throwing, log and return undefined without falling back to _activate(newSession.get()) — that observable can hold a workspace-bound draft from a different call site, so activating it is surprising. Return the activated IActiveSession from _activate/openQuickChat (setActive is synchronous and yields the wrapper) and have the caller focus that exact value, rather than re-reading activeSession.get() afterwards.
Hide an aux-bar view CONTAINER via hideIfEmpty: true + a view when, not a container when: IViewContainerDescriptor has no when property (only IViewDescriptor does). To conditionally hide a whole container (its tab/title) — e.g. the Agents-window Changes/Files containers for workspace-less sessions — put the context-key when on the inner view(s) and set hideIfEmpty: true on the container. Container visibility is hideIfEmpty && activeViewDescriptors.length === 0 (viewsService.updateViewContainerEnablementContextKey), and activeViewDescriptors already respects each view's when, so the container hides reactively when all its views' when go false.
Don't gate "is this a quick chat?" routing on isCreated && isQuickChat: a quick-chat draft is Untitled, so isCreated (= status !== Untitled, visibleSessions.ts) is false — yet it is still a quick chat. Gating quick-chat routing on isCreated && isQuickChat (e.g. the "New" action) makes a draft fall through to the workspace path (scratch-dir composer, no session-type picker, "No models available"). Route on isQuickChat alone so drafts and committed quick chats behave identically; use isCreated only when you genuinely need to distinguish a committed session from an in-composer draft.
Cmd+N in the Agents window is a new-session gesture only — don't fold quick-chat/peer-chat creation into it: session creation (Cmd+N, NewChatInSessionsWindowAction/workbench.action.sessions.newChat → always openNewSession), quick-chat creation (Chats-section "+", Cmd+K Cmd+N, NewQuickChatAction → openQuickChat), and peer-chat creation (chat "+", Cmd+T, AddChatToSessionAction) are three distinct keybindable actions. Keep them separate — Cmd+N must not become context-aware/mirror-route to a quick chat based on the active session's kind.
The New action must never inherit a quick chat's folder into the workspace composer: openNewSessionFromActive seeds the new-session composer with activeSession.workspace.get()?.uri. A quick chat is workspace-less by intent, but if its workspace observable is ever non-undefined (e.g. a stale-build/coupling leak where _kind resolved to WorkspaceSessionKind because _meta.workspaceless was dropped, exposing the host's scratch ~/.copilot/chats/<id> cwd as a workspace), Cmd+N would carry that scratch dir into createNewSession → "New session in <scratch>", single/no session type, no picker, "No models available". Gate the folder inheritance on activeSession.isQuickChat so a quick chat always falls to the clean folder-picker composer regardless of any leaked workspace value.
A reused new-session composer must re-seed its workspace draft when it swaps out of quick-chat mode: the session-type picker hides itself when it has no folder types (sessionTypePicker _folderSessionTypes.length === 0), which is the case whenever the composer has no active session (refresh(undefined) clears the types). A freshly opened new-session composer avoids this by seeding a workspace draft from the restored folder in its constructor — but the same NewChatWidget instance is reused across the quick-chat→new-session transition (sessionView.ts keeps kind==='newSession'), and Cmd+N's openNewSession discard branch only _activate(undefined), leaving the reused composer session-less → picker hidden. Fix by re-running the constructor's seed (_seedWorkspaceDraft()) from an autorun when _isQuickChatComposer flips true→false with no active session, so the reused composer matches a fresh one (folder + visible picker). Don't assume the constructor-time restore covers a reused composer.
Every untitled-session-title fallback must be quick-chat aware: an untitled session's title observable is '', so a hardcoded localize(…, "New Session") fallback shows "New Session" even for a quick chat (whose composer says "New Chat"). Route all such fallbacks through the shared getUntitledSessionTitle(isQuickChat) helper (services/sessions/common/session.ts, boolean param so each caller controls reader-tracked .read(reader) vs .get()). There are ≥5 sites — titlebar (sessionsTitleBarWidget), session header (×2: title + rename placeholder), list-row hover (sessionHoverContent), sessions picker (sessionsActions) — keep them on the helper; never hardcode "New Session". (The Cmd+N action title stays "New Session" — that action creates a session, unrelated to a session's own title.)
NeedsInput is still an active turn for live turn UI: agent-host tool and input confirmations intentionally transition a running chat from InProgress to NeedsInput without ending activeTurn. Live status surfaces such as the chat input pills must use isActiveSessionStatus so they do not disappear until the next output returns the chat to InProgress.
Chat file pills classify files against the owning session, never the window-global workspace context: multiple sessions can render concurrently, so IChat.lastTurnChanges carries isOutsideWorkspace derived from that session's workspace/worktree roots, and per-response file edits carry the same metadata. AgentHostSessionAdapter owns a generic session-output cache passed to output reducers; workspace classification uses the namespaced key isOutsideWorkspace:${uri.toString()}, and workspace changes clear the cache. Keep change counts/diffs workspace-only, preview only external markdown, and open resources through chat.editorAssociations rather than invoking markdown.showPreview directly.
Agent-host-only exclusions for built-in client tools belong in ClientToolSetsContribution, not the global tool registration: AgentHostActiveClientService.getClientTools advertises enabled members of every non-deprecated tool set, including extension-contributed sets. Omit an unsupported built-in tool from the client tool sets so normal Copilot chat can continue using it; do not treat this contribution as the sole Agent Host allowlist.
Non-interactive MCP authentication probes must not create dynamic authentication providers: Provider creation can prompt for manual client registration when dynamic registration is unsupported. With allowInteraction: false, only inspect existing providers and sessions; defer metadata discovery and provider creation until the user invokes the mcpAuthenticationRequired action.
Use structured maps for the state that is actually multi-keyed, not for an incidental cache: If MCP tracking is addressed by session + server, model that source of truth directly with NKeyMap. Do not add a separate NKeyMap that merely caches serialized storage keys while leaving the real tracking state in nested or synchronized maps.
Subagent activity rows must preserve rich tool presentation, stable height, tool identity, and protocol intent: Do not flatten markdown invocation messages into text, omit the shared tool icon, or show a raw terminal command when ToolCallBase.intention exists. Render invocation markdown with the shared chat/file-widget path and the registered/inferred compact tool icon, keep it constrained to one line within a static minimum-height slot so text, code, and file chips do not shift surrounding content, and use terminal intention before invocation-message fallback.
Subagent reasoning preserves the last tool activity: Show "Working on it..." only during startup (before any tool is known) and while child markdown is streaming. Child reasoning must not replace the activity row; retain the most recent tool presentation, or keep the startup placeholder when no tool has run yet.
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.
Menu-order changes must update every registration assertion: action ordering can be covered by tests outside the action's owning contribution. Search for the previous order and command id, then update all affected expectations so focused tests do not leave the broader suite stale.
Shared commands must delegate behavior to the layout service, not inspect a layout implementation: workbench.action.toggleAuxiliaryBar must call the semantic IWorkbenchLayoutService.toggleSecondarySideBar() operation. Do not branch on optional layout properties or concrete workbench shape in the shared action; each workbench owns how its secondary-sidebar affordance maps to visible parts.
Definitive session deletion and temporary list eviction are different operations: deletion clears durable provenance and pending state; filtering a still-existing session only removes its visible list entry. Keep the list-removal helper side-effect-free, and let each caller explicitly update its mutation generation instead of passing an "already incremented" boolean.
Durable user intent must never be discarded on onDidChangeSessions.removed: pins, manual sort keys, and group membership (SessionsListModelService, SessionGroupsService) are cleared only on ISessionsManagementService.onDidDeleteSession (or archive), never on the provider's removed delta. removed is an eviction, not a deletion: BaseAgentHostSessionsProvider._refreshSessions reconciles against one listing that the host aggregates across all its agents, and an agent that cannot answer yet returns [] instead of failing (CodexAgent.listSessions returns [] for a missing _githubToken, a not-yet-downloaded SDK, or a failed thread/list; ClaudeAgent.listSessions does the same). Persisting the removal turned a ~300 ms startup race into permanent loss of the user's pins and groups. Runtime-only consumers of removed (terminals, grid slots, layout) are fine as-is — only persisted state needs the delete event.
_refreshSessions must not evict a cached session whose agent contributed no rows: a listing with zero rows for an agent means "unknown", not "empty", so scope eviction to listedAgentProviders (the set of AgentSession.provider(...) schemes actually present in the response) and compare against adapter.agentProvider. Real deletions still arrive through deleteSessions and the sessionRemoved notification; the only cost is that an agent's last session, deleted elsewhere, lingers until it lists something again.
Keep session-list refresh filtering linear: when retention pruning needs the complete backend key set, collect those keys while filtering entries in the original loop, then reconcile last-seen/pruning afterward. Do not introduce a candidate-map/filter/map pipeline when one loop plus one reconciliation call expresses the lifecycle more clearly.
Centralize session workspace filtering behind a semantic predicate: refresh, add-notification, and summary-update paths should call one _isSessionInWorkspace(entry)-style helper. Keep key construction, working-directory parsing, pending-local lookup, and provenance checks out of each caller so the high-level list flow stays readable and all paths apply identical rules.
Multi-root Editor filtering belongs to durable session metadata, not a workspace memento: sessions with _meta.multiRoot.workspaceFile match a multi-root Editor window by URI identity against IWorkspace.configuration. Metadata-less sessions use containment against any current folder; do not retain a parallel workspace-scoped membership store whose lifecycle can drift from the host-owned session metadata.
Name semantic layout operations after the user-facing surface: a shared operation must use the stable UI concept (toggleSecondarySideBar()), not the implementation term (AuxiliaryBar) that happens to back it in classic layouts. This keeps single-pane mappings clear and avoids leaking layout internals through the API.
Semantic layout commands need matching visibility and focus semantics: when a shared command maps to a different surface, expose a semantic visibility query for its toggled state and transfer focus before hiding the currently focused mapped surface. Otherwise menu labels lie about the action and keyboard users retain focus in hidden content.
In the single-pane workbench, the secondary side bar is the side pane: SinglePaneWorkbench.toggleSecondarySideBar() (backing workbench.action.toggleAuxiliaryBar) delegates to toggleSidePane() and isSecondarySideBarVisible() reports the side pane's visibility. The docked detail lives inside the editor part, so hiding transfers focus from either side-pane part to the sessions list. Do not add a separate editor-pane toggle operation; consolidate on toggleSidePane().
ISession.capabilities must be observable, not a live plain getter: capabilities can hydrate/change after a session first surfaces (e.g. an agent host whose root state arrives after the session's first SessionState). A plain getter cannot be tracked by the context-key autorun (setActiveSessionContextKeys reads it inside an autorun), so supportsMultipleChats/sessionSupportsFork would stay stale, and a multi-chat catalog processed while supportsMultipleChats was still false would stay collapsed to [defaultChat]. Expose capabilities as IObservable<ISessionCapabilities> (agent host derives it from connection.rootState via observableFromEvent + derivedOpts with structuralEquals; static providers use constObservable), have consumers read .read(reader)/.get(), and re-apply the chat catalog from the last SessionState in an autorun on capability change. Do not fix this by firing _onDidChangeSessions — the active-session context autorun tracks the session's own observables, not the provider's session-list event.
A managed/default editor tab must be re-ensured every sync, not opened once: the per-session editor working-set restore (baseSessionLayoutController [B2] _applyWorkingSet) runs on session activation and is not docked-gated, so it reinstates the session's
b285c0292b56