Sessions - changes view improvements (#300780)

* Sessions - refactor context key managememt

* Changes view improvements
This commit is contained in:
Ladislau Szomoru
2026-03-11 14:10:30 +01:00
committed by GitHub
parent e25795537c
commit 755a4199cc

View File

@@ -13,7 +13,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableFromPromise, observableValue } from '../../../../base/common/observable.js';
import { autorun, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, ObservablePromise, observableValue } from '../../../../base/common/observable.js';
import { basename, dirname } from '../../../../base/common/path.js';
import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
@@ -91,6 +91,8 @@ const enum ChangesVersionMode {
}
const changesVersionModeContextKey = new RawContextKey<ChangesVersionMode>('sessions.changesVersionMode', ChangesVersionMode.AllChanges);
const isMergeBaseBranchProtectedContextKey = new RawContextKey<boolean>('sessions.isMergeBaseBranchProtected', false);
const hasOpenPullRequestContextKey = new RawContextKey<boolean>('sessions.hasOpenPullRequest', false);
const hasUncommittedChangesContextKey = new RawContextKey<boolean>('sessions.hasUncommittedChanges', false);
// --- List Item
@@ -263,7 +265,7 @@ export class ChangesViewPane extends ViewPane {
private readonly activeSessionFileCountObs: IObservableWithChange<number>;
private readonly activeSessionHasChangesObs: IObservableWithChange<boolean>;
private readonly activeSessionRepositoryChangesObs: IObservableWithChange<IChangesFileItem[] | undefined>;
private readonly activeSessionRepositoryObs: IObservableWithChange<IObservable<{ value?: IGitRepository }> | undefined>;
private readonly activeSessionRepositoryObs: IObservableWithChange<IGitRepository | undefined>;
get activeSessionHasChanges(): IObservable<boolean> {
return this.activeSessionHasChangesObs;
@@ -325,22 +327,31 @@ export class ChangesViewPane extends ViewPane {
}).recomputeInitiallyAndOnChange(this._store);
// Track active session repository changes
this.activeSessionRepositoryObs = derived(reader => {
const activeSessionRepositoryPromiseObs = derived(reader => {
const activeSessionWorktree = this.activeSession.read(reader)?.worktree;
if (!activeSessionWorktree) {
return constObservable(undefined);
}
return new ObservablePromise(this.gitService.openRepository(activeSessionWorktree)).resolvedValue;
});
this.activeSessionRepositoryObs = derived<IGitRepository | undefined>(reader => {
const activeSessionRepositoryPromise = activeSessionRepositoryPromiseObs.read(reader);
if (activeSessionRepositoryPromise === undefined) {
return undefined;
}
return observableFromPromise(this.gitService.openRepository(activeSessionWorktree));
return activeSessionRepositoryPromise.read(reader);
});
this.activeSessionRepositoryChangesObs = derived(reader => {
const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader);
const repository = this.activeSessionRepositoryObs.read(reader);
if (!repository) {
return undefined;
}
const state = repository.value?.state.read(reader);
const state = repository.state.read(reader);
const headCommit = state?.HEAD?.commit;
return (state?.workingTreeChanges ?? []).map(change => {
const isDeletion = change.modifiedUri === undefined;
@@ -615,34 +626,34 @@ export class ChangesViewPane extends ViewPane {
// Reactively computes the diff between HEAD^ and HEAD. Memoize the diff observable so
// that we only recompute it when the HEAD commit id actually changes.
const headCommitObs = derived(reader => {
const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value;
const repository = this.activeSessionRepositoryObs.read(reader);
return repository?.state.read(reader)?.HEAD?.commit;
});
const lastTurnChangesObs = derived(reader => {
const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value;
const repository = this.activeSessionRepositoryObs.read(reader);
const headCommit = headCommitObs.read(reader);
if (!repository || !headCommit) {
return undefined;
return constObservable(undefined);
}
return observableFromPromise(repository.diffBetweenWithStats(`${headCommit}^`, headCommit));
return new ObservablePromise(repository.diffBetweenWithStats(`${headCommit}^`, headCommit)).resolvedValue;
});
// Combine both entry sources for display
const combinedEntriesObs = derived(reader => {
const headCommit = headCommitObs.read(reader);
const versionMode = this.versionModeObs.read(reader);
const editEntries = editSessionEntriesObs.read(reader);
const sessionFiles = sessionFilesObs.read(reader);
const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? [];
const versionMode = this.versionModeObs.read(reader);
const lastTurnDiffChanges = lastTurnChangesObs.read(reader).read(reader);
let sourceEntries: IChangesFileItem[];
if (versionMode === ChangesVersionMode.Uncommitted) {
sourceEntries = repositoryFiles;
} else if (versionMode === ChangesVersionMode.LastTurn) {
const lastTurn = lastTurnChangesObs.read(reader);
const diffChanges = lastTurn?.read(reader).value ?? [];
const diffChanges = lastTurnDiffChanges ?? [];
const parentRef = headCommit ? `${headCommit}^` : '';
sourceEntries = diffChanges.map(change => {
const isDeletion = change.modifiedUri === undefined;
@@ -702,19 +713,18 @@ export class ChangesViewPane extends ViewPane {
if (this.actionsContainer) {
dom.clearNode(this.actionsContainer);
const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.actionsContainer));
const scopedInstantiationService = this.renderDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])));
const scopedInstantiationService = this.renderDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
// Set the chat session type context key reactively so that menu items with
// `chatSessionType == copilotcli` (e.g. Create Pull Request) are shown
const chatSessionTypeKey = scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
const chatSessionTypeKey = this.scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
this.renderDisposables.add(autorun(reader => {
const activeSession = this.activeSession.read(reader);
chatSessionTypeKey.set(activeSession?.sessionType ?? '');
}));
// Bind required context keys for the menu buttons
this.renderDisposables.add(bindContextKey(hasUndecidedChatEditingResourceContextKey, scopedContextKeyService, r => {
this.renderDisposables.add(bindContextKey(hasUndecidedChatEditingResourceContextKey, this.scopedContextKeyService, r => {
const session = activeEditingSessionObs.read(r);
if (!session) {
return false;
@@ -723,7 +733,7 @@ export class ChangesViewPane extends ViewPane {
return entries.some(entry => entry.state.read(r) === ModifiedFileEntryState.Modified);
}));
this.renderDisposables.add(bindContextKey(hasAppliedChatEditsContextKey, scopedContextKeyService, r => {
this.renderDisposables.add(bindContextKey(hasAppliedChatEditsContextKey, this.scopedContextKeyService, r => {
const session = activeEditingSessionObs.read(r);
if (!session) {
return false;
@@ -732,43 +742,40 @@ export class ChangesViewPane extends ViewPane {
return entries.length > 0;
}));
this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => {
const { files } = topLevelStats.read(r);
const hasAgentSessionChangesObs = derived(reader => {
const { files } = topLevelStats.read(reader);
return files > 0;
}));
});
// Also bind to the ViewPane's scoped context key service so the ViewTitle menu can evaluate it
this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => {
const { files } = topLevelStats.read(r);
return files > 0;
}));
this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => hasAgentSessionChangesObs.read(r)));
// Track whether there are uncommitted (working tree) changes
this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, r => {
const repositoryFiles = this.activeSessionRepositoryChangesObs.read(r);
const hasUncommittedChangesObs = derived(reader => {
const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader);
return (repositoryFiles?.length ?? 0) > 0;
}));
});
// Set context key for merge base branch protection
const isMergeBaseBranchProtectedContextKey = scopedContextKeyService.createKey<boolean>('sessions.isMergeBaseBranchProtected', false);
this.renderDisposables.add(autorun(reader => {
const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader).value;
const state = repository?.state.read(reader);
isMergeBaseBranchProtectedContextKey.set(state?.HEAD?.base?.isProtected === true);
}));
this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, r => hasUncommittedChangesObs.read(r)));
// Set context key for PR state from session metadata
const hasOpenPullRequestKey = scopedContextKeyService.createKey<boolean>('sessions.hasOpenPullRequest', false);
this.renderDisposables.add(autorun(reader => {
const isMergeBaseBranchProtectedObs = derived(reader => {
const state = this.activeSessionRepositoryObs.read(reader)?.state.read(reader);
return state?.HEAD?.base?.isProtected === true;
});
this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, r => isMergeBaseBranchProtectedObs.read(r)));
const hasOpenPullRequestObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader);
if (sessionResource) {
const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata;
hasOpenPullRequestKey.set(!!metadata?.pullRequestUrl);
} else {
hasOpenPullRequestKey.set(false);
if (!sessionResource) {
return false;
}
}));
sessionsChangedSignal.read(reader);
const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata;
return !!metadata?.pullRequestUrl;
});
this.renderDisposables.add(bindContextKey(hasOpenPullRequestContextKey, this.scopedContextKeyService, r => hasOpenPullRequestObs.read(r)));
this.renderDisposables.add(autorun(reader => {
const { isSessionMenu, added, removed } = topLevelStats.read(reader);