Sessions: Replace badge with inline title count in Changes view (#303482)

* Sessions: Replace Changes view badge with inline title count

Replace the NumberBadge on the Changes view tab with an inline title
that shows the file count directly, e.g. '7 Changes' instead of a
badge overlay.

- Export dynamic changesContainerTitle with a getter-based value
- Add refreshContainerInfo() to IViewContainerModel interface and
  ViewContainerModel implementation
- Remove IActivityService/NumberBadge dependency from ChangesViewPane

* Fix incorrect file count by using topLevelStats

The inline title count was reading from activeSessionChangesObs (raw
session changes only) instead of topLevelStats which accounts for
deduplication and version mode filtering. Move the title update into
onVisible() where topLevelStats is available, and reset the title
when the view is hidden.

* Address review feedback

- Add blank line separator after changesContainerTitle block
- Add constructor-level fallback autorun to keep title in sync when
  the view is hidden and the active session changes
- Reset title to 'Changes' on dispose to avoid stale counts

* Keep inline file count when switching tabs

Remove the updateContainerTitle(0) call from the hide handler so
the count persists when the user switches to another tab. The
fallback autorun in the constructor still handles session switches
while the view is hidden.

* Fix grammar: use singular '1 Change' instead of '1 Changes'

---------

Co-authored-by: mrleemurray <mrleemurray@users.noreply.github.com>
This commit is contained in:
Lee Murray
2026-03-20 20:04:51 +00:00
committed by GitHub
parent 877aceccc2
commit 69ca0c3f58
4 changed files with 60 additions and 17 deletions

View File

@@ -12,13 +12,14 @@ 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 { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { Disposable, DisposableStore } 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';
import { URI } from '../../../../base/common/uri.js';
import { localize, localize2 } from '../../../../nls.js';
import { ILocalizedString } from '../../../../platform/action/common/action.js';
import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js';
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { MenuId, Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
@@ -52,7 +53,6 @@ import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actio
import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.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';
@@ -70,6 +70,16 @@ const $ = dom.$;
export const CHANGES_VIEW_CONTAINER_ID = 'workbench.view.agentSessions.changesContainer';
export const CHANGES_VIEW_ID = 'workbench.view.agentSessions.changes';
// Dynamic title for the Changes view container tab.
// Uses a getter so that ViewContainerModel.updateContainerInfo() picks up
// the latest value each time it re-reads viewContainer.title.value.
let _changesContainerTitleValue = localize('changes', 'Changes');
export const changesContainerTitle: ILocalizedString = {
original: 'Changes',
get value() { return _changesContainerTitleValue; }
};
const RUN_SESSION_CODE_REVIEW_ACTION_ID = 'sessions.codeReview.run';
// --- View Mode
@@ -335,7 +345,6 @@ export class ChangesViewPane extends ViewPane {
@IThemeService themeService: IThemeService,
@IHoverService hoverService: IHoverService,
@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,
@@ -365,20 +374,16 @@ export class ChangesViewPane extends ViewPane {
return activeSession?.providerType ?? '';
}));
// Badge
const badgeDisposable = this._register(new MutableDisposable());
// Fallback title update: when the view is not visible (renderDisposables
// cleared), keep the container title in sync with the raw session changes
// so the tab still shows a count when the user switches sessions.
this._register(autorun(reader => {
const changes = this.viewModel.activeSessionChangesObs.read(reader);
if (changes.length === 0) {
badgeDisposable.clear();
if (this.isBodyVisible()) {
// onVisible() drives the title from topLevelStats while visible
return;
}
const message = changes.length === 1
? localize('changesView.oneFileChanged', '1 file changed')
: localize('changesView.filesChanged', '{0} files changed', changes.length);
badgeDisposable.value = this.activityService.showViewActivity(CHANGES_VIEW_ID, { badge: new NumberBadge(changes.length, () => message) });
const changes = this.viewModel.activeSessionChangesObs.read(reader);
this.updateContainerTitle(changes.length);
}));
}
@@ -433,6 +438,27 @@ export class ChangesViewPane extends ViewPane {
}
}
private updateContainerTitle(fileCount: number): void {
let nextTitle: string;
if (fileCount === 0) {
nextTitle = localize('changes', 'Changes');
} else if (fileCount === 1) {
nextTitle = localize('changesView.titleWithCountOne', '1 Change');
} else {
nextTitle = localize('changesView.titleWithCount', '{0} Changes', fileCount);
}
if (nextTitle === _changesContainerTitleValue) {
return;
}
_changesContainerTitleValue = nextTitle;
const viewContainer = this.viewDescriptorService.getViewContainerById(CHANGES_VIEW_CONTAINER_ID);
if (viewContainer) {
this.viewDescriptorService.getViewContainerModel(viewContainer).refreshContainerInfo();
}
}
private onVisible(): void {
this.renderDisposables.clear();
@@ -762,7 +788,12 @@ export class ChangesViewPane extends ViewPane {
dom.setVisibility(!hasEntries, this.welcomeContainer!);
}));
// Update summary text (line counts only, file count is shown in badge)
// Update inline title count from the same stats the tree uses
this.renderDisposables.add(autorun(reader => {
this.updateContainerTitle(topLevelStats.read(reader).files);
}));
// Update summary text (line counts only)
if (this.summaryContainer) {
dom.clearNode(this.summaryContainer);
@@ -1026,6 +1057,7 @@ export class ChangesViewPane extends ViewPane {
}
override dispose(): void {
this.updateContainerTitle(0);
this.tree?.dispose();
this.tree = undefined;
super.dispose();