mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user