|
|
|
|
@@ -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();
|
|
|
|
|
|