mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
Sessions - refactor changes view (#303256)
* Initial cleanup * Move more things to the view model
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user