diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index 61fca9cb54877..527562a37ccb1 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -177,3 +177,4 @@ You **must** run these checks before declaring work complete: - **Save the outgoing session's working set eagerly on the *raw* active session change, not on the workspace-gated `activeSessionForWorkingSet` derive**: the derive (`baseSessionLayoutController`) holds back while the incoming session's workspace folders resolve, and other autoruns driven by the *raw* `ISessionsService.activeSession` (e.g. the single-pane managed-tabs sync) **async-close the outgoing session's docked editors** (`_closeInactiveChangesEditors`) during that lag. If the working-set save is on the lagged derive it runs *after* those closes, so the outgoing session's Changes tab (or whichever editor was active) is already gone and its active state is lost — on return the working set restores the wrong active editor (symptom: switching back to a session whose Changes tab was active shows a different tab active). Fix: a dedicated `[B2] runOnChange(activeSession, …)` saves the previous session's working set **synchronously** (guarded by resource-inequality, `!Untitled`, `!_isRestoringSessionLayout`) — this runs before the managed-tab sequencer microtask closes anything — and the save is removed from the gated apply `runOnChange` (which now only applies). Save doesn't need workspace readiness; only apply does. Pairs with making the managed Changes tab open non-stealing (`inactive: true` + `EditorActivation.PRESERVE`) so the restored active editor is preserved. The unit harness can't reproduce the derive-lag directly, so assert the decoupling: switching to a session whose workspace isn't in the folders (gated apply holds back) still records a `saveWorkingSet` for the outgoing session. - **Docked side-pane width persistence must be symmetric about the detail (aux) width**: in single-pane the docked detail (auxiliary bar) lives *inside* the editor grid node, so the workbench persists the pure editor-content width (`_persistedEditorWidth` = node − detail) and the grid descriptor reconstructs node = editor-content + detail. These must use the **same condition** for including the detail: only when the detail is visible (`partVisibility.auxiliaryBar`). Subtracting the detail width *unconditionally* at save while adding it back only when the detail is visible shrank an **Editor-only** session's side pane by the detail width on *every* reload, compounding toward zero ("side pane always tiny on reload"). Fix `_persistedEditorWidth` to subtract only when the detail is visible. - **Side-pane (editor grid node) size is workbench-level, not per session**: the editor grid node width is owned by the workbench grid and persisted globally (`workbench.sessions.partSizes` via `_savePartSizes`/`createDesktopGridDescriptor`), so switching sessions keeps the same width and reload restores it in one paint. Do **not** add a per-session width map in the layout controller that re-applies a width on session switch/reveal — it makes switching sessions jump the side pane around, and a post-paint restore on reload flickers. The 60% first-open default (`SIDE_PANE_WIDTH_RATIO` in `parts/editorPartSizing.ts`, applied by the single-pane `_applyEditorSplitSize` override on the first reveal that has no size to restore) is the only width the layout intentionally sets; everything else is the user's persisted grid size. +- **Per-session menu visibility must flow through scoped context keys, not widget DOM hiding or global active-session keys**: toolbars evaluate `when` against their scoped `IContextKeyService`, while their pickers act on scoped `ISessionContext`. Expose provider-specific availability as an observable on `ISession` and publish it through `setSessionContextKeys`, so every visible session surface evaluates the same scoped state declaratively. diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index d5372044441db..d4a848bd3d4e5 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -205,6 +205,7 @@ The contract is small and provider-agnostic: - **`ISessionsManagementService.getQuickChatSessionTypes()`** — every session type advertised by quick-chat-capable providers, for the inline composer type picker. - **`ISessionsService.openQuickChat(options?)`** — view-layer entry point; opens the quick chat as a normal session. - **`ISession.isQuickChat`** (optional `IObservable`) — set only by quick-chat-capable providers (absent ⇒ `false`). Consumers read it via the `isQuickChatSession(session)` helper. The agent-host adapter derives it from the host's `workspaceless` tag, **not** from `workspace === undefined`, which can be transiently undefined for workspace-bound sessions too. +- **`ISession.hasGitRepository`** (optional `IObservable`) — provider-refined usable Git availability. `setSessionContextKeys` publishes it as `SessionHasGitRepositoryContext` in both root and per-session scoped context-key services, falling back to workspace repository metadata when absent. This keeps declarative menu visibility aligned with each toolbar's scoped `ISessionContext` when multiple sessions are visible. Presentation: a quick chat is a **single-chat** session that uses the normal session header (no peer-chat tab strip); only the Done/archive affordance is hidden. Its untitled-title fallback is **"New Chat"** (not "New Session") — every fallback site (titlebar, session header, list hover, sessions picker) routes through the shared `getUntitledSessionTitle(isQuickChat)` helper (`services/sessions/common/session.ts`). **Cmd+N always creates a new session** (`NewChatInSessionsWindowAction` → `openNewSession`); a quick chat is created **only** via the "Chats"-section **"+"** (`NewQuickChatAction`, also bound to **Cmd+K Cmd+N**), which opens the composer with the inline session-type picker feeding `openQuickChat({ sessionTypeId })` on send. Peer chats within a session are a third gesture (chat **"+"** / Cmd+T). Keep these three creation actions distinct. diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index fb1e09adf13a5..e2592e7307b7f 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -13,7 +13,7 @@ export const SessionIdContext = new RawContextKey('sessionId', '', local export const SessionProviderIdContext = new RawContextKey('sessionProviderId', '', localize('sessionProviderId', "The provider ID of the session in scope (the active session globally, or a specific session within an isolated component such as the session view or a context menu overlay)")); export const SessionTypeContext = new RawContextKey('sessionType', '', localize('sessionType', "The session type of the session in scope (the active session globally, or a specific session within an isolated component such as the session view or a context menu overlay)")); export const SessionWorkspaceIsVirtualContext = new RawContextKey('sessionWorkspaceIsVirtual', true, localize('sessionWorkspaceIsVirtual', "Whether the session's workspace is virtual")); -export const SessionHasGitRepositoryContext = new RawContextKey('sessionHasGitRepository', false, localize('sessionHasGitRepository', "Whether the session has an associated git repository")); +export const SessionHasGitRepositoryContext = new RawContextKey('sessionHasGitRepository', false, localize('sessionHasGitRepository', "Whether the session has a usable git repository")); export const SessionHasGitSyncActionRunningContext = new RawContextKey('sessionHasGitSyncActionRunning', false, localize('sessionHasGitSyncActionRunning', "Whether the session has a git sync action currently running")); export const SessionUsesCombinedConfigPickerContext = new RawContextKey('sessionUsesCombinedConfigPicker', false, localize('sessionUsesCombinedConfigPicker', "Whether the session's provider offers a combined mode and model configuration picker (used on phone layouts in place of the standalone pickers)")); export const SessionSupportsRenameContext = new RawContextKey('sessionSupportsRename', false, localize('sessionSupportsRename', "Whether the session can be renamed")); diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/COPILOT_CHAT_SESSIONS_PROVIDER.md b/src/vs/sessions/contrib/providers/copilotChatSessions/COPILOT_CHAT_SESSIONS_PROVIDER.md index baeee74397cfe..1078cb405408b 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/COPILOT_CHAT_SESSIONS_PROVIDER.md +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/COPILOT_CHAT_SESSIONS_PROVIDER.md @@ -42,6 +42,7 @@ When `createNewSession(workspace)` is called, the provider creates one of two co **`CopilotCLISession`** — For local `file://` workspaces: - Implements `ISession` plus provider-specific observable fields (`permissionLevel`, `branchObservable`, `isolationModeObservable`) - Performs async git repository resolution during construction (sets `loading` to true until resolved) +- Exposes `hasGitRepository` as an observable that becomes true only when the repository has a HEAD commit, so scoped session context keys hide worktree/branch controls for non-Git and empty repositories - Configuration methods: `setIsolationMode()`, `setBranch()`, `setModelId()`, `setMode()`, `setPermissionLevel()`, `setModeById()` - Tracks selected options via `Map` and syncs to `IChatSessionsService` - Uses `IGitService` to open the repository and resolve branch information @@ -133,6 +134,8 @@ Each picker action uses a `when` clause to show only for the correct session typ | `IsActiveSessionCopilotChatCloud` | Copilot Cloud sessions | | `IsActiveSessionCopilotChatClaudeCode` | Claude sessions | +Repository controls additionally require `SessionHasGitRepositoryContext`. The provider publishes usable Git availability through `ISession.hasGitRepository`, and `setSessionContextKeys` binds that observable into each session view's scoped context-key service. Menu visibility therefore follows the toolbar's own `ISessionContext`, not the window's globally active session. + These are composed from `SessionTypeContext` (the session type ID) and `SessionProviderIdContext` (the provider ID). ### Adding a New Picker diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsActions.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsActions.ts index b3f129fc9f754..7c1ea49dd9a34 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsActions.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsActions.ts @@ -5,12 +5,12 @@ import { BaseActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { IReader, autorun } from '../../../../../base/common/observable.js'; +import { autorun } from '../../../../../base/common/observable.js'; import { isWeb } from '../../../../../base/common/platform.js'; import { localize2 } from '../../../../../nls.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../../workbench/common/contributions.js'; import { Menus } from '../../../../browser/menus.js'; @@ -24,9 +24,8 @@ import { LocalSessionType } from '../../localChatSessions/browser/localChatSessi import { IsolationPicker } from './isolationPicker.js'; import { ModePicker, ModePickerModel } from './modePicker.js'; import { CopilotPermissionPickerDelegate, PermissionPicker } from './permissionPicker.js'; -import { BaseAgentHostSessionsProvider, CopilotCLISessionType } from '../../agentHost/browser/baseAgentHostSessionsProvider.js'; +import { CopilotCLISessionType } from '../../agentHost/browser/baseAgentHostSessionsProvider.js'; import { ISessionContext } from '../../../../services/sessions/browser/sessionContext.js'; -import { LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../../common/agentHostSessionsProvider.js'; const IsActiveSessionCopilotCLI = ContextKeyExpr.equals(SessionTypeContext.key, CopilotCLISessionType.id); const IsActiveSessionLocal = ContextKeyExpr.equals(SessionTypeContext.key, LocalSessionType.id); @@ -248,51 +247,4 @@ class CopilotPickerActionViewItemContribution extends Disposable implements IWor } -// -- Context Key Contribution -- - -class CopilotActiveSessionContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.copilotActiveSession'; - - constructor( - @ISessionsService sessionsService: ISessionsService, - @ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(); - - const hasRepositoryKey = SessionHasGitRepositoryContext.bindTo(contextKeyService); - - this._register(autorun((reader: IReader) => { - const session = sessionsService.activeSession.read(reader); - if (session?.providerId === COPILOT_PROVIDER_ID) { - const provider = sessionsProvidersService.getProvider(session.providerId); - const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session.sessionId) : undefined; - const isLoading = providerSession?.loading.read(reader); - const gitRepository = providerSession?.gitRepository; - // An empty repository (no HEAD commit) cannot run worktree - // isolation, so treat it as having no usable git repository — - // mirrors CopilotCLISession's own worktree gating. - const hasHeadCommit = !!gitRepository?.state.read(reader).HEAD?.commit; - hasRepositoryKey.set(!isLoading && !!gitRepository && hasHeadCommit); - } else if (session?.providerId === LOCAL_AGENT_HOST_PROVIDER_ID) { - const provider = sessionsProvidersService.getProvider(session.providerId); - const providerSession = provider instanceof BaseAgentHostSessionsProvider - ? provider.getSessionByResource(session.resource) - : undefined; - - const isLoading = providerSession?.loading.read(reader); - const workspace = providerSession?.workspace.read(reader); - const hasGitRepository = workspace?.folders - .some(folder => folder.gitRepository !== undefined) ?? false; - - hasRepositoryKey.set(!isLoading && hasGitRepository); - } else { - hasRepositoryKey.set(false); - } - })); - } -} - registerWorkbenchContribution2(CopilotPickerActionViewItemContribution.ID, CopilotPickerActionViewItemContribution, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(CopilotActiveSessionContribution.ID, CopilotActiveSessionContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts index 45eb3bba5b6a3..482f126c13744 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -104,6 +104,8 @@ export interface ICopilotChatSession { readonly mode: IObservable<{ readonly id: string; readonly kind: string } | undefined>; /** Whether the session is still initializing (e.g., resolving git repository). */ readonly loading: IObservable; + /** Whether the session's repository supports worktree-backed operations. */ + readonly hasGitRepository?: IObservable; /** Whether the session is archived. */ readonly isArchived: IObservable; /** Whether the session has been read. */ @@ -255,6 +257,8 @@ class CopilotCLISession extends Disposable implements ICopilotChatSession { private readonly _loading = observableValue(this, true); readonly loading: IObservable = this._loading; + private readonly _hasGitRepository = observableValue(this, false); + readonly hasGitRepository: IObservable = this._hasGitRepository; private readonly _changes: ReturnType>; readonly changes: IObservable; @@ -372,13 +376,17 @@ class CopilotCLISession extends Disposable implements ICopilotChatSession { this.setIsolationMode('workspace'); } } - if (this._gitRepository) { - this._loadBranches(this._gitRepository); + const gitRepository = this._gitRepository; + if (gitRepository) { + this._register(autorun(reader => { + this._hasGitRepository.set(!!gitRepository.state.read(reader).HEAD?.commit, undefined); + })); + this._loadBranches(gitRepository); // Automatically update the selected branch when the repository // state changes. This is done only for the Folder sessions. const currentBranchName = derived(reader => { - const state = this._gitRepository?.state.read(reader); + const state = gitRepository.state.read(reader); return state?.HEAD?.commit ? state.HEAD.name : undefined; }); @@ -3093,7 +3101,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions // Resolve the main (first) chat in the group — session-level properties come from it const mainChatIds = this._getChatIdsInGroup(sessionId); const firstChatId = mainChatIds[0]; - const primaryChat = firstChatId + const primaryChat: ICopilotChatSession = firstChatId ? this._sessionCache.get(this._localIdFromchatId(firstChatId)) ?? chat : chat; @@ -3146,6 +3154,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions icon: primaryChat.icon, createdAt: primaryChat.createdAt, workspace: primaryChat.workspace, + hasGitRepository: primaryChat.hasGitRepository, title: primaryChat.title, updatedAt: chatsObs.map((chats, reader) => this._latestDate(chats, c => c.updatedAt.read(reader))!), status: chatsObs.map((chats, reader) => this._aggregateStatus(chats, reader)), @@ -3187,6 +3196,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions icon: chat.icon, createdAt: chat.createdAt, workspace: chat.workspace, + hasGitRepository: chat.hasGitRepository, title: chat.title, updatedAt: chat.updatedAt, status: chat.status, diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/isolationPicker.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/isolationPicker.ts index 8969434556fec..1f6d89ff1df65 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/isolationPicker.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/isolationPicker.ts @@ -80,10 +80,8 @@ export class IsolationPicker extends Disposable { const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined; if (providerSession) { const gitRepo = providerSession.gitRepository; - const repoState = gitRepo?.state?.read?.(reader); - const hasHeadCommit = repoState ? !!repoState.HEAD?.commit : true; - // Enable only when git repo exists and HEAD has a valid commit (not an empty repo) - this._hasGitRepo = !isLoading && !!gitRepo && hasHeadCommit; + const repoState = gitRepo?.state.read(reader); + this._hasGitRepo = !isLoading && !!repoState?.HEAD?.commit; // Read isolation mode from session — session is the source of truth providerSession.isolationMode.read(reader); } else { diff --git a/src/vs/sessions/services/sessions/common/session.ts b/src/vs/sessions/services/sessions/common/session.ts index a03f0964e4097..2b9b69231fc4d 100644 --- a/src/vs/sessions/services/sessions/common/session.ts +++ b/src/vs/sessions/services/sessions/common/session.ts @@ -495,6 +495,8 @@ export interface ISession { readonly createdAt: Date; /** Workspace this session operates on. */ readonly workspace: IObservable; + /** Whether the session has a usable Git repository. Providers may refine this beyond workspace metadata. */ + readonly hasGitRepository?: IObservable; /** Whether this is a workspace-less "quick chat". Only quick-chat-capable providers set this; absent means `false`. */ readonly isQuickChat?: IObservable; diff --git a/src/vs/sessions/services/sessions/common/sessionContextKeys.ts b/src/vs/sessions/services/sessions/common/sessionContextKeys.ts index 4738c7dbf12bf..5ca878b71b855 100644 --- a/src/vs/sessions/services/sessions/common/sessionContextKeys.ts +++ b/src/vs/sessions/services/sessions/common/sessionContextKeys.ts @@ -29,6 +29,7 @@ import { SessionActiveChatIsClosableContext, SessionActiveChatIsDeletableContext, SessionActiveChatHasSubagentsContext, + SessionHasGitRepositoryContext, } from '../../../common/contextkeys.js'; import { ChatOriginKind, getChatCapabilities, ISession, SessionStatus } from './session.js'; import { IActiveSession } from './sessionsManagement.js'; @@ -47,6 +48,7 @@ interface ISessionContextKeys { readonly supportsRename: IContextKey; readonly supportsDelete: IContextKey; readonly workspaceIsVirtual: IContextKey; + readonly hasGitRepository: IContextKey; readonly hasChanges: IContextKey; readonly hasPullRequest: IContextKey; readonly hasWorkspace: IContextKey; @@ -85,6 +87,7 @@ function getBoundKeys(contextKeyService: IContextKeyService): ISessionContextKey supportsRename: SessionSupportsRenameContext.bindTo(contextKeyService), supportsDelete: SessionSupportsDeleteContext.bindTo(contextKeyService), workspaceIsVirtual: SessionWorkspaceIsVirtualContext.bindTo(contextKeyService), + hasGitRepository: SessionHasGitRepositoryContext.bindTo(contextKeyService), hasChanges: SessionHasChangesContext.bindTo(contextKeyService), hasPullRequest: SessionHasPullRequestContext.bindTo(contextKeyService), hasWorkspace: SessionHasWorkspaceContext.bindTo(contextKeyService), @@ -129,7 +132,9 @@ export function setSessionContextKeys(session: ISession | undefined, contextKeyS keys.supportsFork.set(capabilities?.supportsFork ?? false); keys.supportsRename.set(capabilities?.supportsRename ?? false); keys.supportsDelete.set(capabilities?.supportsDelete ?? false); - keys.workspaceIsVirtual.set(session?.workspace.read(reader)?.isVirtualWorkspace ?? true); + const workspace = session?.workspace.read(reader); + keys.workspaceIsVirtual.set(workspace?.isVirtualWorkspace ?? true); + keys.hasGitRepository.set(session?.hasGitRepository?.read(reader) ?? workspace?.folders.some(folder => folder.gitRepository !== undefined) ?? false); // Mirror the changes pill: the default changeset, falling back to the session's changes. const defaultChangeset = session?.changesets.read(reader)?.find(c => c.isDefault.read(reader)); diff --git a/src/vs/sessions/services/sessions/test/common/sessionContextKeys.test.ts b/src/vs/sessions/services/sessions/test/common/sessionContextKeys.test.ts new file mode 100644 index 0000000000000..af6ce5d864773 --- /dev/null +++ b/src/vs/sessions/services/sessions/test/common/sessionContextKeys.test.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { constObservable, observableValue, autorun, ISettableObservable } from '../../../../../base/common/observable.js'; +import { upcastPartial } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { SessionHasGitRepositoryContext } from '../../../../common/contextkeys.js'; +import { ISession } from '../../common/session.js'; +import { setSessionContextKeys } from '../../common/sessionContextKeys.js'; + +function createSession(hasGitRepository: ISettableObservable): ISession { + return upcastPartial({ + sessionId: 'session', + providerId: 'provider', + sessionType: 'type', + workspace: constObservable(undefined), + hasGitRepository, + isArchived: constObservable(false), + isRead: constObservable(true), + capabilities: constObservable({ supportsMultipleChats: false }), + changesets: constObservable(undefined), + changes: constObservable([]), + }); +} + +suite('Session Context Keys', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('publishes Git availability independently to scoped context key services', () => { + const firstHasGit = observableValue('firstHasGit', false); + const secondHasGit = observableValue('secondHasGit', true); + const firstContext = new MockContextKeyService(); + const secondContext = new MockContextKeyService(); + const firstSession = createSession(firstHasGit); + const secondSession = createSession(secondHasGit); + + store.add(autorun(reader => setSessionContextKeys(firstSession, firstContext, reader))); + store.add(autorun(reader => setSessionContextKeys(secondSession, secondContext, reader))); + firstHasGit.set(true, undefined); + + assert.deepStrictEqual({ + first: firstContext.getContextKeyValue(SessionHasGitRepositoryContext.key), + second: secondContext.getContextKeyValue(SessionHasGitRepositoryContext.key), + }, { + first: true, + second: true, + }); + + firstHasGit.set(false, undefined); + + assert.deepStrictEqual({ + first: firstContext.getContextKeyValue(SessionHasGitRepositoryContext.key), + second: secondContext.getContextKeyValue(SessionHasGitRepositoryContext.key), + }, { + first: false, + second: true, + }); + }); +});