Sessions - more improvements to the changes view (#305797)

* Changes - polish changes filter

* Add some spacing to the picker

* Changes - fix panel rendering when there are no changes
This commit is contained in:
Ladislau Szomoru
2026-03-27 20:57:29 +00:00
committed by GitHub
parent 0496f93688
commit e92b212dac
5 changed files with 117 additions and 65 deletions

View File

@@ -19,7 +19,6 @@ import { IProductService } from '../../../../platform/product/common/productServ
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { URI } from '../../../../base/common/uri.js';
@@ -170,5 +169,5 @@ MenuRegistry.appendMenuItem(MenuId.ChatEditingSessionChangesToolbar, {
title: localize2('applyActions', 'Apply Actions'),
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(IsSessionsWindowContext, ChatContextKeys.hasAgentSessionChanges),
when: IsSessionsWindowContext,
});

View File

@@ -93,6 +93,8 @@ const changesViewModeContextKey = new RawContextKey<ChangesViewMode>('changesVie
// --- Versions Mode
const enum ChangesVersionMode {
BranchChanges = 'branchChanges',
OutgoingChanges = 'outgoingChanges',
AllChanges = 'allChanges',
LastTurn = 'lastTurn'
}
@@ -102,7 +104,7 @@ const enum IsolationMode {
Worktree = 'worktree'
}
const changesVersionModeContextKey = new RawContextKey<ChangesVersionMode>('sessions.changesVersionMode', ChangesVersionMode.AllChanges);
const changesVersionModeContextKey = new RawContextKey<ChangesVersionMode>('sessions.changesVersionMode', ChangesVersionMode.BranchChanges);
const isMergeBaseBranchProtectedContextKey = new RawContextKey<boolean>('sessions.isMergeBaseBranchProtected', false);
const isolationModeContextKey = new RawContextKey<IsolationMode>('sessions.isolationMode', IsolationMode.Workspace);
const hasOpenPullRequestContextKey = new RawContextKey<boolean>('sessions.hasOpenPullRequest', false);
@@ -225,9 +227,14 @@ function buildTreeChildren(items: IChangesFileItem[]): IObjectTreeElement<Change
class ChangesViewModel extends Disposable {
readonly sessionsChangedSignal: IObservable<void>;
readonly activeSessionResourceObs: IObservable<URI | undefined>;
readonly activeSessionBranchNameObs: IObservable<string | undefined>;
readonly activeSessionBaseBranchNameObs: IObservable<string | undefined>;
readonly activeSessionUpstreamBranchNameObs: IObservable<string | undefined>;
readonly activeSessionIsolationModeObs: IObservable<IsolationMode>;
readonly activeSessionRepositoryObs: IObservableWithChange<IGitRepository | undefined>;
readonly activeSessionChangesObs: IObservable<readonly (IChatSessionFileChange | IChatSessionFileChange2)[]>;
readonly activeSessionFirstCheckpointRefObs: IObservable<string | undefined>;
readonly activeSessionLastCheckpointRefObs: IObservable<string | undefined>;
readonly versionModeObs: ISettableObservable<ChangesVersionMode>;
setVersionMode(mode: ChangesVersionMode): void {
@@ -264,14 +271,6 @@ class ChangesViewModel extends Disposable {
return activeSession?.resource;
});
// Active session isolation mode
this.activeSessionIsolationModeObs = derived(reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
return activeSession?.workspace.read(reader)?.repositories[0]?.workingDirectory === undefined
? IsolationMode.Workspace
: IsolationMode.Worktree;
});
// Active session changes
this.activeSessionChangesObs = derivedOpts({
equalsFn: arrayEqualsC<IChatSessionFileChange | IChatSessionFileChange2>()
@@ -283,6 +282,18 @@ class ChangesViewModel extends Disposable {
return activeSession.changes.read(reader) as readonly (IChatSessionFileChange | IChatSessionFileChange2)[];
});
const activeSessionRepositoryObs = derived(reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
return activeSession?.workspace.read(reader)?.repositories[0];
});
// Active session isolation mode
this.activeSessionIsolationModeObs = derived(reader => {
return activeSessionRepositoryObs.read(reader)?.workingDirectory === undefined
? IsolationMode.Workspace
: IsolationMode.Worktree;
});
// Active session repository
const activeSessionRepositoryPromiseObs = derived(reader => {
const activeSessionResource = this.activeSessionResourceObs.read(reader);
@@ -290,13 +301,12 @@ class ChangesViewModel extends Disposable {
return constObservable(undefined);
}
const activeSession = this.sessionManagementService.activeSession.read(reader);
const worktree = activeSession?.workspace.read(reader)?.repositories[0]?.workingDirectory;
if (!worktree) {
const workingDirectory = activeSessionRepositoryObs.read(reader)?.workingDirectory;
if (!workingDirectory) {
return constObservable(undefined);
}
return new ObservablePromise(this.gitService.openRepository(worktree)).resolvedValue;
return new ObservablePromise(this.gitService.openRepository(workingDirectory)).resolvedValue;
});
this.activeSessionRepositoryObs = derived<IGitRepository | undefined>(reader => {
@@ -308,11 +318,57 @@ class ChangesViewModel extends Disposable {
return activeSessionRepositoryPromise.read(reader);
});
// Active session branch name
this.activeSessionBranchNameObs = derived(reader => {
const repository = activeSessionRepositoryObs.read(reader);
const repositoryState = this.activeSessionRepositoryObs.read(reader)?.state.read(reader);
return repository?.detail ?? repositoryState?.HEAD?.name;
});
// Active session base branch name
this.activeSessionBaseBranchNameObs = derived(reader => {
return activeSessionRepositoryObs.read(reader)?.baseBranchName;
});
// Active session upstream branch name
this.activeSessionUpstreamBranchNameObs = derived(reader => {
const repositoryState = this.activeSessionRepositoryObs.read(reader)?.state.read(reader);
return repositoryState?.HEAD?.upstream
? `${repositoryState.HEAD.upstream.remote}/${repositoryState.HEAD.upstream.name}`
: undefined;
});
// Active session first checkpoint ref
this.activeSessionFirstCheckpointRefObs = derived(reader => {
const sessionResource = this.activeSessionResourceObs.read(reader);
if (!sessionResource) {
return undefined;
}
this.sessionsChangedSignal.read(reader);
const model = this.agentSessionsService.getSession(sessionResource);
return model?.metadata?.firstCheckpointRef as string | undefined;
});
// Active session last checkpoint ref
this.activeSessionLastCheckpointRefObs = derived(reader => {
const sessionResource = this.activeSessionResourceObs.read(reader);
if (!sessionResource) {
return undefined;
}
this.sessionsChangedSignal.read(reader);
const model = this.agentSessionsService.getSession(sessionResource);
return model?.metadata?.lastCheckpointRef as string | undefined;
});
// Version mode
this.versionModeObs = observableValue<ChangesVersionMode>(this, ChangesVersionMode.AllChanges);
this.versionModeObs = observableValue<ChangesVersionMode>(this, ChangesVersionMode.BranchChanges);
this._register(runOnChange(this.activeSessionResourceObs, () => {
this.setVersionMode(ChangesVersionMode.AllChanges);
this.setVersionMode(ChangesVersionMode.BranchChanges);
}));
// View mode
@@ -405,13 +461,6 @@ export class ChangesViewPane extends ViewPane {
this.bodyContainer = dom.append(container, $('.changes-view-body'));
// Welcome message for empty state
this.welcomeContainer = dom.append(this.bodyContainer, $('.changes-welcome'));
const welcomeIcon = dom.append(this.welcomeContainer, $('.changes-welcome-icon'));
welcomeIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));
const welcomeMessage = dom.append(this.welcomeContainer, $('.changes-welcome-message'));
welcomeMessage.textContent = localize('changesView.noChanges', "Changed files and other session artifacts will appear here.");
// Actions container - positioned outside and above the card
this.actionsContainer = dom.append(this.bodyContainer, $('.chat-editing-session-actions.outside-card'));
@@ -457,6 +506,13 @@ export class ChangesViewPane extends ViewPane {
// List container
this.listContainer = dom.append(this.contentContainer, $('.chat-editing-session-list'));
// Welcome message for empty state
this.welcomeContainer = dom.append(this.contentContainer, $('.changes-welcome'));
const welcomeIcon = dom.append(this.welcomeContainer, $('.changes-welcome-icon'));
welcomeIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));
const welcomeMessage = dom.append(this.welcomeContainer, $('.changes-welcome-message'));
welcomeMessage.textContent = localize('changesView.noChanges', "Changed files and other session artifacts will appear here.");
// CI Status widget — bottom pane
this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.splitViewContainer));
@@ -632,18 +688,6 @@ export class ChangesViewPane extends ViewPane {
return repository?.state.read(reader)?.HEAD?.commit;
});
const lastCheckpointRefObs = derived(reader => {
const sessionResource = this.viewModel.activeSessionResourceObs.read(reader);
if (!sessionResource) {
return undefined;
}
this.viewModel.sessionsChangedSignal.read(reader);
const model = this.agentSessionsService.getSession(sessionResource);
return model?.metadata?.lastCheckpointRef as string | undefined;
});
const lastTurnChangesObs = derived(reader => {
const repository = this.viewModel.activeSessionRepositoryObs.read(reader);
const headCommit = headCommitObs.read(reader);
@@ -652,7 +696,7 @@ export class ChangesViewPane extends ViewPane {
return constObservable(undefined);
}
const lastCheckpointRef = lastCheckpointRefObs.read(reader);
const lastCheckpointRef = this.viewModel.activeSessionLastCheckpointRefObs.read(reader);
const diffPromise = lastCheckpointRef
? repository.diffBetweenWithStats(`${lastCheckpointRef}^`, lastCheckpointRef)
@@ -694,8 +738,8 @@ export class ChangesViewPane extends ViewPane {
let sourceEntries: IChangesFileItem[];
if (versionMode === ChangesVersionMode.LastTurn) {
const lastCheckpointRef = lastCheckpointRefObs.read(reader);
const lastTurnDiffChanges = lastTurnChangesObs.read(reader).read(reader);
const lastCheckpointRef = this.viewModel.activeSessionLastCheckpointRefObs.read(reader);
const diffChanges = lastTurnDiffChanges ?? [];
@@ -915,9 +959,7 @@ export class ChangesViewPane extends ViewPane {
const { files } = topLevelStats.read(reader);
const hasEntries = files > 0;
dom.setVisibility(hasEntries, this.contentContainer!);
dom.setVisibility(hasEntries, this.actionsContainer!);
dom.setVisibility(hasEntries, this.splitViewContainer!);
dom.setVisibility(hasEntries, this.listContainer!);
dom.setVisibility(!hasEntries, this.welcomeContainer!);
if (this.filesCountBadge) {
@@ -1539,31 +1581,33 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem {
) {
const actionProvider: IActionWidgetDropdownActionProvider = {
getActions: () => {
const activeSession = sessionManagementService.activeSession.get();
const activeSessionIsolationMode = this.viewModel.activeSessionIsolationModeObs.get();
const activeSessionRepositoryState = this.viewModel.activeSessionRepositoryObs.get()?.state.get();
const activeSessionRepository = activeSession?.workspace.get()?.repositories[0];
const baseBranchName = activeSessionIsolationMode === IsolationMode.Worktree
? activeSessionRepository?.baseBranchName ?? ''
: activeSessionRepositoryState?.HEAD?.upstream
? `${activeSessionRepositoryState.HEAD.upstream.remote}/${activeSessionRepositoryState.HEAD.upstream.name}`
: activeSessionRepositoryState?.HEAD?.name ?? '';
const branchName = activeSessionRepository?.detail
?? activeSessionRepositoryState?.HEAD?.name ?? '';
const allChangesDescription = baseBranchName && branchName
? `${branchName}${baseBranchName}`
: branchName ?? localize('chatEditing.versionsAllChanges.description', 'Show all changes made in this session');
const branchName = viewModel.activeSessionBranchNameObs.get();
const baseBranchName = viewModel.activeSessionBaseBranchNameObs.get();
return [
{
...action,
id: 'chatEditing.versionsBranchChanges',
label: localize('chatEditing.versionsBranchChanges', 'Branch Changes'),
description: `${branchName}${baseBranchName}`,
checked: viewModel.versionModeObs.get() === ChangesVersionMode.BranchChanges,
category: { label: 'changes', order: 1, showHeader: false },
run: async () => {
viewModel.setVersionMode(ChangesVersionMode.BranchChanges);
if (this.element) {
this.renderLabel(this.element);
}
},
},
{
...action,
id: 'chatEditing.versionsAllChanges',
label: localize('chatEditing.versionsAllChanges', 'All Changes'),
description: allChangesDescription,
description: localize('chatEditing.versionsAllChanges.description', 'Show all changes made in this session'),
checked: viewModel.versionModeObs.get() === ChangesVersionMode.AllChanges,
category: { label: 'checkpoints', order: 2, showHeader: false },
enabled: viewModel.activeSessionFirstCheckpointRefObs.get() !== undefined &&
viewModel.activeSessionLastCheckpointRefObs.get() !== undefined,
run: async () => {
viewModel.setVersionMode(ChangesVersionMode.AllChanges);
if (this.element) {
@@ -1577,6 +1621,9 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem {
label: localize('chatEditing.versionsLastTurnChanges', "Last Turn's Changes"),
description: localize('chatEditing.versionsLastTurnChanges.description', 'Show only changes from the last turn'),
checked: viewModel.versionModeObs.get() === ChangesVersionMode.LastTurn,
category: { label: 'checkpoints', order: 3, showHeader: false },
enabled: viewModel.activeSessionFirstCheckpointRefObs.get() !== undefined &&
viewModel.activeSessionLastCheckpointRefObs.get() !== undefined,
run: async () => {
viewModel.setVersionMode(ChangesVersionMode.LastTurn);
if (this.element) {
@@ -1600,9 +1647,11 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem {
protected override renderLabel(element: HTMLElement): null {
const mode = this.viewModel.versionModeObs.get();
const label = mode === ChangesVersionMode.LastTurn
? localize('sessionsChanges.versionsLastTurn', "Last Turn's Changes")
: localize('sessionsChanges.versionsAllChanges', "All Changes");
const label = mode === ChangesVersionMode.BranchChanges
? localize('sessionsChanges.versionsBranchChanges', "Branch Changes")
: mode === ChangesVersionMode.AllChanges
? localize('sessionsChanges.versionsAllChanges', "All Changes")
: localize('sessionsChanges.versionsLastTurn', "Last Turn's Changes");
dom.reset(element, dom.$('span', undefined, label), ...renderLabelWithIcons('$(chevron-down)'));
this.updateAriaLabel();

View File

@@ -74,6 +74,10 @@
font-size: 12px;
align-items: center;
> span {
margin-left: 2px;
}
> .codicon {
font-size: 10px !important;
padding-left: 4px;

View File

@@ -41,7 +41,9 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp
tooltip,
category: CHAT_CATEGORY,
icon,
precondition: canRunSessionCodeReviewContextKey,
precondition: ContextKeyExpr.and(
ChatContextKeys.hasAgentSessionChanges,
canRunSessionCodeReviewContextKey),
menu: [
{
id: MenuId.ChatEditingSessionChangesToolbar,
@@ -49,7 +51,6 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp
order: 7,
when: ContextKeyExpr.and(
IsSessionsWindowContext,
ChatContextKeys.hasAgentSessionChanges,
ChatContextKeys.agentSessionType.notEqualsTo(AgentSessionProviders.Cloud),
),
},

View File

@@ -357,7 +357,6 @@ export class ViewAllSessionChangesAction extends Action2 {
id: MenuId.ChatEditingSessionChangesToolbar,
group: 'navigation',
order: 10,
when: ChatContextKeys.hasAgentSessionChanges
}
],
});