Sessions - refactor changes view (#303256)

* Initial cleanup

* Move more things to the view model
This commit is contained in:
Ladislau Szomoru
2026-03-19 17:29:11 +00:00
committed by GitHub
parent 970e6fb2ba
commit 000a053bc6

View File

@@ -12,8 +12,8 @@ import { IObjectTreeElement, ITreeNode } from '../../../../base/browser/ui/tree/
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, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, ObservablePromise, observableValue } from '../../../../base/common/observable.js';
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, ISettableObservable, ObservablePromise, observableSignalFromEvent, observableValue, runOnChange } 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';
@@ -23,7 +23,7 @@ import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/but
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { MenuId, Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { FileKind } from '../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
@@ -35,7 +35,7 @@ import { ILogService } from '../../../../platform/log/common/log.js';
import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
@@ -48,21 +48,21 @@ import { IViewsService } from '../../../../workbench/services/views/common/views
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { chatEditingWidgetFileStateContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';
import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';
import { createFileIconThemableTreeContainerScope } from '../../../../workbench/contrib/files/browser/views/explorerView.js';
import { IActivityService, NumberBadge } from '../../../../workbench/services/activity/common/activity.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js';
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionWorkspace.js';
import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js';
import { IGitRepository, IGitService } from '../../../../workbench/contrib/git/common/gitService.js';
import { IGitHubService } from '../../github/browser/githubService.js';
import { CIStatusWidget } from './ciStatusWidget.js';
import { arrayEqualsC } from '../../../../base/common/equals.js';
const $ = dom.$;
@@ -202,6 +202,100 @@ function buildTreeChildren(items: IChangesFileItem[]): IObjectTreeElement<Change
return convert(root);
}
// --- View Model
class ChangesViewModel extends Disposable {
readonly sessionsChangedSignal: IObservable<void>;
readonly activeSessionResourceObs: IObservable<URI | undefined>;
readonly activeSessionRepositoryObs: IObservableWithChange<IGitRepository | undefined>;
readonly activeSessionChangesObs: IObservable<readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]>;
readonly versionModeObs: ISettableObservable<ChangesVersionMode>;
setVersionMode(mode: ChangesVersionMode): void {
if (this.versionModeObs.get() === mode) {
return;
}
this.versionModeObs.set(mode, undefined);
}
readonly viewModeObs: ISettableObservable<ChangesViewMode>;
setViewMode(mode: ChangesViewMode): void {
if (this.viewModeObs.get() === mode) {
return;
}
this.viewModeObs.set(mode, undefined);
}
constructor(
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IGitService private readonly gitService: IGitService,
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
// Active session changes
this.sessionsChangedSignal = observableSignalFromEvent(this,
this.agentSessionsService.model.onDidChangeSessions);
// Active session resource
this.activeSessionResourceObs = derivedOpts({ equalsFn: isEqual }, reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
return activeSession?.resource;
});
// Active session changes
this.activeSessionChangesObs = derivedOpts({
equalsFn: arrayEqualsC<IChatSessionFileChange | IChatSessionFileChange2>()
}, reader => {
const sessionResource = this.activeSessionResourceObs.read(reader);
if (!sessionResource) {
return Iterable.empty();
}
this.sessionsChangedSignal.read(reader);
const model = this.agentSessionsService.getSession(sessionResource);
return model?.changes instanceof Array ? model.changes : Iterable.empty();
});
// Active session repository
const activeSessionRepositoryPromiseObs = derived(reader => {
const activeSessionResource = this.activeSessionResourceObs.read(reader);
if (!activeSessionResource) {
return constObservable(undefined);
}
const activeSession = this.sessionManagementService.getActiveSession();
if (!activeSession?.worktree) {
return constObservable(undefined);
}
return new ObservablePromise(this.gitService.openRepository(activeSession.worktree)).resolvedValue;
});
this.activeSessionRepositoryObs = derived<IGitRepository | undefined>(reader => {
const activeSessionRepositoryPromise = activeSessionRepositoryPromiseObs.read(reader);
if (activeSessionRepositoryPromise === undefined) {
return undefined;
}
return activeSessionRepositoryPromise.read(reader);
});
// Version mode
this.versionModeObs = observableValue<ChangesVersionMode>(this, ChangesVersionMode.AllChanges);
this._register(runOnChange(this.activeSessionResourceObs, () => {
this.setVersionMode(ChangesVersionMode.AllChanges);
}));
// View mode
const storedMode = this.storageService.get('changesView.viewMode', StorageScope.WORKSPACE);
const initialMode = storedMode === ChangesViewMode.Tree ? ChangesViewMode.Tree : ChangesViewMode.List;
this.viewModeObs = observableValue<ChangesViewMode>(this, initialMode);
}
}
// --- View Pane
export class ChangesViewPane extends ViewPane {
@@ -224,44 +318,7 @@ export class ChangesViewPane extends ViewPane {
private currentBodyHeight = 0;
private currentBodyWidth = 0;
// View mode (list vs tree)
private readonly viewModeObs: ReturnType<typeof observableValue<ChangesViewMode>>;
private readonly viewModeContextKey: IContextKey<ChangesViewMode>;
get viewMode(): ChangesViewMode { return this.viewModeObs.get(); }
set viewMode(mode: ChangesViewMode) {
if (this.viewModeObs.get() === mode) {
return;
}
this.viewModeObs.set(mode, undefined);
this.viewModeContextKey.set(mode);
this.storageService.store('changesView.viewMode', mode, StorageScope.WORKSPACE, StorageTarget.USER);
}
// Version mode (all changes, last turn, uncommitted)
private readonly versionModeObs = observableValue<ChangesVersionMode>(this, ChangesVersionMode.AllChanges);
private readonly versionModeContextKey: IContextKey<ChangesVersionMode>;
setVersionMode(mode: ChangesVersionMode): void {
if (this.versionModeObs.get() === mode) {
return;
}
this.versionModeObs.set(mode, undefined);
this.versionModeContextKey.set(mode);
}
// Track the active session used by this view
private readonly activeSession: IObservableWithChange<IActiveSessionItem | undefined>;
private readonly activeSessionFileCountObs: IObservableWithChange<number>;
private readonly activeSessionHasChangesObs: IObservableWithChange<boolean>;
private readonly activeSessionRepositoryObs: IObservableWithChange<IGitRepository | undefined>;
get activeSessionHasChanges(): IObservable<boolean> {
return this.activeSessionHasChangesObs;
}
// Badge for file count
private readonly badgeDisposable = this._register(new MutableDisposable());
readonly viewModel: ChangesViewModel;
constructor(
options: IViewPaneOptions,
@@ -274,123 +331,51 @@ export class ChangesViewPane extends ViewPane {
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IHoverService hoverService: IHoverService,
@IChatEditingService private readonly chatEditingService: IChatEditingService,
@IEditorService private readonly editorService: IEditorService,
@IActivityService private readonly activityService: IActivityService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
@ILabelService private readonly labelService: ILabelService,
@IStorageService private readonly storageService: IStorageService,
@ICodeReviewService private readonly codeReviewService: ICodeReviewService,
@IGitService private readonly gitService: IGitService,
@IGitHubService private readonly gitHubService: IGitHubService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
// View mode
const storedMode = this.storageService.get('changesView.viewMode', StorageScope.WORKSPACE);
const initialMode = storedMode === ChangesViewMode.Tree ? ChangesViewMode.Tree : ChangesViewMode.List;
this.viewModeObs = observableValue<ChangesViewMode>(this, initialMode);
this.viewModeContextKey = changesViewModeContextKey.bindTo(contextKeyService);
this.viewModeContextKey.set(initialMode);
this.viewModel = this.instantiationService.createInstance(ChangesViewModel);
// Version mode
this.versionModeContextKey = changesVersionModeContextKey.bindTo(contextKeyService);
this.versionModeContextKey.set(ChangesVersionMode.AllChanges);
// Track active session from sessions management service
this.activeSession = derivedOpts<IActiveSessionItem | undefined>({
equalsFn: (a, b) => isEqual(a?.resource, b?.resource),
}, reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
if (!activeSession?.resource) {
return undefined;
}
return activeSession;
}).recomputeInitiallyAndOnChange(this._store);
// Track active session repository changes
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 activeSessionRepositoryPromise.read(reader);
});
this.activeSessionFileCountObs = this.createActiveSessionFileCountObservable();
this.activeSessionHasChangesObs = this.activeSessionFileCountObs.map(fileCount => fileCount > 0).recomputeInitiallyAndOnChange(this._store);
// Set chatSessionType on the view's context key service so ViewTitle
// menu items can use it in their `when` clauses. Update reactively
// when the active session changes.
const viewSessionTypeKey = this.scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
this._register(autorun(reader => {
const activeSession = this.activeSession.read(reader);
viewSessionTypeKey.set(activeSession?.providerType ?? '');
this._register(bindContextKey(changesVersionModeContextKey, this.scopedContextKeyService, reader => {
return this.viewModel.versionModeObs.read(reader);
}));
}
private createActiveSessionFileCountObservable(): IObservableWithChange<number> {
const activeSessionResource = this.activeSession.map(a => a?.resource);
// View mode
this._register(bindContextKey(changesViewModeContextKey, this.scopedContextKeyService, reader => {
return this.viewModel.viewModeObs.read(reader);
}));
const sessionsChangedSignal = observableFromEvent(
this,
this.agentSessionsService.model.onDidChangeSessions,
() => ({}),
);
// Set chatSessionType on the view's context key service so ViewTitlev menu items
// can use it in their `when` clauses. Update reactively when the active session
// changes.
this._register(bindContextKey(ChatContextKeys.agentSessionType, this.scopedContextKeyService, reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
return activeSession?.providerType ?? '';
}));
const sessionFileChangesObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader);
if (!sessionResource) {
return Iterable.empty();
// Badge
const badgeDisposable = this._register(new MutableDisposable());
this._register(autorun(reader => {
const changes = this.viewModel.activeSessionChangesObs.read(reader);
if (changes.length === 0) {
badgeDisposable.clear();
return;
}
const model = this.agentSessionsService.getSession(sessionResource);
return model?.changes instanceof Array ? model.changes : Iterable.empty();
});
return derived(reader => {
const activeSession = this.activeSession.read(reader);
if (!activeSession) {
return 0;
}
let editingSessionCount = 0;
if (activeSession.providerType !== AgentSessionProviders.Background) {
const sessions = this.chatEditingService.editingSessionsObs.read(reader);
const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, activeSession.resource));
editingSessionCount = session ? session.entries.read(reader).length : 0;
}
const sessionFiles = [...sessionFileChangesObs.read(reader)];
const sessionFilesCount = sessionFiles.length;
return editingSessionCount + sessionFilesCount;
}).recomputeInitiallyAndOnChange(this._store);
}
private updateBadge(fileCount: number): void {
if (fileCount > 0) {
const message = fileCount === 1
const message = changes.length === 1
? localize('changesView.oneFileChanged', '1 file changed')
: localize('changesView.filesChanged', '{0} files changed', fileCount);
this.badgeDisposable.value = this.activityService.showViewActivity(CHANGES_VIEW_ID, { badge: new NumberBadge(fileCount, () => message) });
} else {
this.badgeDisposable.clear();
}
: localize('changesView.filesChanged', '{0} files changed', changes.length);
badgeDisposable.value = this.activityService.showViewActivity(CHANGES_VIEW_ID, { badge: new NumberBadge(changes.length, () => message) });
}));
}
protected override renderBody(container: HTMLElement): void {
@@ -446,80 +431,10 @@ export class ChangesViewPane extends ViewPane {
private onVisible(): void {
this.renderDisposables.clear();
const activeSessionResource = this.activeSession.map(a => a?.resource);
// Create observable for the active editing session
// Note: We must read editingSessionsObs to establish a reactive dependency,
// so that the view updates when a new editing session is added (e.g., cloud sessions)
const activeEditingSessionObs = derived(reader => {
const activeSession = this.activeSession.read(reader);
if (!activeSession) {
return undefined;
}
const sessions = this.chatEditingService.editingSessionsObs.read(reader);
return sessions.find(candidate => isEqual(candidate.chatSessionResource, activeSession.resource));
});
// Create observable for edit session entries from the ACTIVE session only (local editing sessions)
const editSessionEntriesObs = derived(reader => {
const activeSession = this.activeSession.read(reader);
// Background chat sessions render the working set based on the session files, not the editing session
if (activeSession?.providerType === AgentSessionProviders.Background) {
return [];
}
const session = activeEditingSessionObs.read(reader);
if (!session) {
return [];
}
const entries = session.entries.read(reader);
const items: IChangesFileItem[] = [];
for (const entry of entries) {
const isDeletion = entry.isDeletion ?? false;
const linesAdded = entry.linesAdded?.read(reader) ?? 0;
const linesRemoved = entry.linesRemoved?.read(reader) ?? 0;
items.push({
type: 'file',
uri: entry.modifiedURI,
originalUri: entry.originalURI,
state: entry.state.read(reader),
isDeletion,
changeType: isDeletion ? 'deleted' : 'modified',
linesAdded,
linesRemoved,
reviewCommentCount: 0,
});
}
return items;
});
// Signal observable that triggers when sessions data changes
const sessionsChangedSignal = observableFromEvent(
this.renderDisposables,
this.agentSessionsService.model.onDidChangeSessions,
() => ({}),
);
// Observable for session file changes from agentSessionsService (cloud/background sessions)
// Reactive to both activeSession changes AND session data changes
const sessionFileChangesObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader);
if (!sessionResource) {
return Iterable.empty();
}
const model = this.agentSessionsService.getSession(sessionResource);
return model?.changes instanceof Array ? model.changes : Iterable.empty();
});
const reviewCommentCountByFileObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
const sessionChanges = [...sessionFileChangesObs.read(reader)];
const sessionResource = this.viewModel.activeSessionResourceObs.read(reader);
const changes = [...this.viewModel.activeSessionChangesObs.read(reader)];
if (!sessionResource) {
return new Map<string, number>();
@@ -534,11 +449,11 @@ export class ChangesViewPane extends ViewPane {
}
}
if (sessionChanges.length === 0) {
if (changes.length === 0) {
return result;
}
const reviewFiles = getCodeReviewFilesFromSessionChanges(sessionChanges as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]);
const reviewFiles = getCodeReviewFilesFromSessionChanges(changes as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]);
const reviewVersion = getCodeReviewVersion(reviewFiles);
const reviewState = this.codeReviewService.getReviewState(sessionResource).read(reader);
@@ -557,8 +472,9 @@ export class ChangesViewPane extends ViewPane {
// Convert session file changes to list items (cloud/background sessions)
const sessionFilesObs = derived(reader => {
const reviewCommentCountByFile = reviewCommentCountByFileObs.read(reader);
const changes = [...this.viewModel.activeSessionChangesObs.read(reader)];
return [...sessionFileChangesObs.read(reader)].map((entry): IChangesFileItem => {
return changes.map((entry): IChangesFileItem => {
const isDeletion = entry.modifiedUri === undefined;
const isAddition = entry.originalUri === undefined;
const uri = isIChatSessionFileChange2(entry)
@@ -578,21 +494,18 @@ export class ChangesViewPane extends ViewPane {
});
});
// Create observable for last turn changes using diffBetweenWithStats
// 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);
const repository = this.viewModel.activeSessionRepositoryObs.read(reader);
return repository?.state.read(reader)?.HEAD?.commit;
});
const lastCheckpointRefObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
const sessionResource = this.viewModel.activeSessionResourceObs.read(reader);
if (!sessionResource) {
return undefined;
}
sessionsChangedSignal.read(reader);
this.viewModel.sessionsChangedSignal.read(reader);
const model = this.agentSessionsService.getSession(sessionResource);
return model?.metadata?.lastCheckpointRef as string | undefined;
@@ -615,7 +528,7 @@ export class ChangesViewPane extends ViewPane {
});
const lastTurnChangesObs = derived(reader => {
const repository = this.activeSessionRepositoryObs.read(reader);
const repository = this.viewModel.activeSessionRepositoryObs.read(reader);
const headCommit = headCommitObs.read(reader);
if (!repository || !headCommit) {
@@ -633,10 +546,9 @@ export class ChangesViewPane extends ViewPane {
// 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 lastTurnDiffChanges = lastTurnChangesObs.read(reader).read(reader);
const versionMode = this.viewModel.versionModeObs.read(reader);
let sourceEntries: IChangesFileItem[];
if (versionMode === ChangesVersionMode.LastTurn) {
@@ -679,7 +591,7 @@ export class ChangesViewPane extends ViewPane {
} satisfies IChangesFileItem;
});
} else {
sourceEntries = [...editEntries, ...sessionFiles];
sourceEntries = [...sessionFiles];
}
const resources = new Set();
@@ -695,8 +607,6 @@ export class ChangesViewPane extends ViewPane {
// Calculate stats from combined entries
const topLevelStats = derived(reader => {
const editEntries = editSessionEntriesObs.read(reader);
const sessionFiles = sessionFilesObs.read(reader);
const entries = combinedEntriesObs.read(reader);
let added = 0, removed = 0;
@@ -706,10 +616,7 @@ export class ChangesViewPane extends ViewPane {
removed += entry.linesRemoved;
}
const files = entries.length;
const isSessionMenu = editEntries.length === 0 && sessionFiles.length > 0;
return { files, added, removed, isSessionMenu };
return { files: entries.length, added, removed };
});
// Setup context keys and actions toolbar
@@ -718,66 +625,30 @@ export class ChangesViewPane extends ViewPane {
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 = this.scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
this.renderDisposables.add(autorun(reader => {
const activeSession = this.activeSession.read(reader);
chatSessionTypeKey.set(activeSession?.providerType ?? '');
}));
// Bind required context keys for the menu buttons
this.renderDisposables.add(bindContextKey(hasUndecidedChatEditingResourceContextKey, this.scopedContextKeyService, r => {
const session = activeEditingSessionObs.read(r);
if (!session) {
return false;
}
const entries = session.entries.read(r);
return entries.some(entry => entry.state.read(r) === ModifiedFileEntryState.Modified);
}));
this.renderDisposables.add(bindContextKey(hasAppliedChatEditsContextKey, this.scopedContextKeyService, r => {
const session = activeEditingSessionObs.read(r);
if (!session) {
return false;
}
const entries = session.entries.read(r);
return entries.length > 0;
}));
const hasAgentSessionChangesObs = derived(reader => {
this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, reader => {
const { files } = topLevelStats.read(reader);
return files > 0;
});
}));
this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => hasAgentSessionChangesObs.read(r)));
const isMergeBaseBranchProtectedObs = derived(reader => {
const activeSession = this.activeSession.read(reader);
this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
return activeSession?.worktreeBaseBranchProtected === true;
});
}));
this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, r => isMergeBaseBranchProtectedObs.read(r)));
const hasOpenPullRequestObs = derived(reader => {
const sessionResource = activeSessionResource.read(reader);
this.renderDisposables.add(bindContextKey(hasOpenPullRequestContextKey, this.scopedContextKeyService, reader => {
this.viewModel.sessionsChangedSignal.read(reader);
const sessionResource = this.viewModel.activeSessionResourceObs.read(reader);
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)));
return metadata?.pullRequestUrl !== undefined;
}));
this.renderDisposables.add(autorun(reader => {
const { isSessionMenu, added, removed } = topLevelStats.read(reader);
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader); // Re-evaluate when session metadata changes (e.g. pullRequestUrl)
const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar;
const { added, removed } = topLevelStats.read(reader);
const sessionResource = this.viewModel.activeSessionResourceObs.read(reader);
// Read code review state to update the button label dynamically
let reviewCommentCount: number | undefined;
@@ -807,11 +678,11 @@ export class ChangesViewPane extends ViewPane {
reader.store.add(scopedInstantiationService.createInstance(
MenuWorkbenchButtonBar,
this.actionsContainer!,
menuId,
MenuId.ChatEditingSessionChangesToolbar,
{
telemetrySource: 'changesView',
disableWhileRunning: isSessionMenu,
menuOptions: isSessionMenu && sessionResource
disableWhileRunning: true,
menuOptions: sessionResource
? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] }
: { shouldForwardArgs: true },
buttonConfigProvider: (action) => {
@@ -860,11 +731,6 @@ export class ChangesViewPane extends ViewPane {
dom.setVisibility(!hasEntries, this.welcomeContainer!);
}));
// Update badge when file count changes
this.renderDisposables.add(autorun(reader => {
this.updateBadge(topLevelStats.read(reader).files);
}));
// Update summary text (line counts only, file count is shown in badge)
if (this.summaryContainer) {
dom.clearNode(this.summaryContainer);
@@ -925,7 +791,7 @@ export class ChangesViewPane extends ViewPane {
},
compressionEnabled: true,
twistieAdditionalCssClass: (e: unknown) => {
return this.viewMode === ChangesViewMode.List
return this.viewModel.viewModeObs.get() === ChangesViewMode.List
? 'force-no-twistie'
: undefined;
},
@@ -1020,7 +886,7 @@ export class ChangesViewPane extends ViewPane {
// Update tree data with combined entries
this.renderDisposables.add(autorun(reader => {
const entries = combinedEntriesObs.read(reader);
const viewMode = this.viewModeObs.read(reader);
const viewMode = this.viewModel.viewModeObs.read(reader);
if (!this.tree) {
return;
@@ -1367,7 +1233,7 @@ class SetChangesListViewModeAction extends ViewAction<ChangesViewPane> {
}
async runInView(_: ServicesAccessor, view: ChangesViewPane): Promise<void> {
view.viewMode = ChangesViewMode.List;
view.viewModel.setViewMode(ChangesViewMode.List);
}
}
@@ -1390,7 +1256,7 @@ class SetChangesTreeViewModeAction extends ViewAction<ChangesViewPane> {
}
async runInView(_: ServicesAccessor, view: ChangesViewPane): Promise<void> {
view.viewMode = ChangesViewMode.Tree;
view.viewModel.setViewMode(ChangesViewMode.Tree);
}
}
@@ -1426,7 +1292,7 @@ class AllChangesAction extends Action2 {
override async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService);
const view = viewsService.getActiveViewWithId<ChangesViewPane>(CHANGES_VIEW_ID);
view?.setVersionMode(ChangesVersionMode.AllChanges);
view?.viewModel.setVersionMode(ChangesVersionMode.AllChanges);
}
}
registerAction2(AllChangesAction);
@@ -1449,7 +1315,7 @@ class LastTurnChangesAction extends Action2 {
override async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService);
const view = viewsService.getActiveViewWithId<ChangesViewPane>(CHANGES_VIEW_ID);
view?.setVersionMode(ChangesVersionMode.LastTurn);
view?.viewModel.setVersionMode(ChangesVersionMode.LastTurn);
}
}
registerAction2(LastTurnChangesAction);