Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY = 'sessions.automations.newB
export const AUTOMATIONS_NEW_BADGE_STYLE_SETTING = 'sessions.automations.newBadgeStyle';
export const AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT = 'agentSessionsAutomationsNewBadgeStyle';

export type AutomationsNewBadgeStyle = 'accent' | 'soft' | 'outline';
export type AutomationsNewBadgeStyle = 'accent' | 'soft' | 'outline' | 'unread';

const DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE: AutomationsNewBadgeStyle = 'outline';

Expand Down Expand Up @@ -182,7 +182,7 @@ export class AutomationsNewBadgeState extends Disposable {
if (value === undefined || value === DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE) {
return DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE;
}
if (value === 'accent' || value === 'soft') {
if (value === 'accent' || value === 'soft' || value === 'unread') {
return value;
}
this.logService.warn(`[AutomationsNewBadgeState] Unsupported badge style treatment '${value}'; using '${DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE}'.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
},
[AUTOMATIONS_NEW_BADGE_STYLE_SETTING]: {
type: 'string',
enum: ['accent', 'soft', 'outline'],
enum: ['accent', 'soft', 'outline', 'unread'],
default: 'outline',
scope: ConfigurationScope.APPLICATION,
included: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1426,14 +1426,15 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
const activeCustomView = this.customViewService.activeCustomView.read(reader);
template.container.classList.toggle('active', activeCustomView?.id === AUTOMATIONS_CUSTOM_VIEW_ID);
const badgeStyle = this.automationNewBadgePresentation.read(reader);
template.newBadge.style.display = badgeStyle ? 'inline-flex' : 'none';
template.newBadge.style.display = badgeStyle && badgeStyle !== 'unread' ? 'inline-flex' : 'none';
template.newBadge.classList.toggle('session-section-new-badge-accent', badgeStyle === 'accent');
template.newBadge.classList.toggle('session-section-new-badge-soft', badgeStyle === 'soft');
template.newBadge.classList.toggle('session-section-new-badge-outline', badgeStyle === 'outline');
}));
const statusIcon = template.elementDisposables.add(this.instantiationService.createInstance(SessionStatusIcon, template.icon));
template.elementDisposables.add(autorun(reader => {
const automationStatus = this.automationStatus.read(reader);
const badgeStyle = this.automationNewBadgePresentation.read(reader);
if (automationStatus === SessionStatus.NeedsInput) {
template.icon.className = 'session-section-icon';
statusIcon.setStatus(SessionStatus.NeedsInput, true, false);
Expand All @@ -1443,6 +1444,9 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
} else if (automationStatus === SessionStatus.Completed) {
template.icon.className = 'session-section-icon';
statusIcon.setStatus(SessionStatus.Completed, false, false);
} else if (badgeStyle === 'unread') {
template.icon.className = 'session-section-icon';
statusIcon.setStatus(SessionStatus.Completed, false, false);
} else {
statusIcon.reset();
template.icon.className = `session-section-icon ${ThemeIcon.asClassName(Codicon.calendar)}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ suite('AutomationsNewBadgeState', () => {
});
});

test('resolves accent, soft, and outline for eligible returning users', async () => {
test('resolves every supported style for eligible returning users', async () => {
const snapshots = [];
for (const style of ['accent', 'soft', 'outline'] as const) {
for (const style of ['accent', 'soft', 'outline', 'unread'] as const) {
const fixture = createState({ style });
await fixture.state.initialize();
snapshots.push({
Expand All @@ -153,6 +153,7 @@ suite('AutomationsNewBadgeState', () => {
{ style: 'accent', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
{ style: 'soft', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
{ style: 'outline', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
{ style: 'unread', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
]);
});

Expand Down Expand Up @@ -271,7 +272,7 @@ suite('AutomationsNewBadgeState', () => {
await fixture.state.initialize();
const initial = fixture.state.presentation.get();

await fixture.configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'accent');
await fixture.configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'unread');
fixture.configurationService.onDidChangeConfigurationEmitter.fire(upcastPartial<IConfigurationChangeEvent>({
affectsConfiguration: key => key === AUTOMATIONS_NEW_BADGE_STYLE_SETTING,
}));
Expand All @@ -282,7 +283,7 @@ suite('AutomationsNewBadgeState', () => {
treatments: fixture.assignmentService.treatments,
}, {
initial: 'soft',
updated: 'accent',
updated: 'unread',
treatments: [],
});
});
Expand All @@ -305,7 +306,7 @@ suite('AutomationsNewBadgeState', () => {
const fixture = createState({
hadPriorWindowOpen: false,
automations: [upcastPartial<IAutomationDescriptor>({ id: 'existing-automation' })],
style: 'accent',
style: 'unread',
});
await fixture.state.initialize();

Expand All @@ -325,7 +326,7 @@ suite('AutomationsNewBadgeState', () => {
},
}, {
preview: {
style: 'accent',
style: 'unread',
stored: undefined,
},
afterActivation: {
Expand Down
99 changes: 83 additions & 16 deletions src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { createListHarness, createTestSession, ISortChangeRecord } from './sessi
import '../../browser/views/sessionsViewActions.js';
import { computePullRequestIcon, GitHubPullRequestState } from '../../../github/common/types.js';
import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../../browser/automationsConstants.js';
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING } from '../../browser/automationsNewBadge.js';
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING, type AutomationsNewBadgeStyle } from '../../browser/automationsNewBadge.js';

function createSession(id: string, opts: {
workspaceLabel?: string;
Expand Down Expand Up @@ -209,7 +209,7 @@ suite('Sessions - SessionsList', () => {
});
});

test('renders the new badge only on the Automations section when templates are recycled', () => {
test('renders new badge presentations only on the Automations section when templates are recycled', () => {
const instantiationService = disposables.add(new TestInstantiationService());
instantiationService.stubInstance(MenuWorkbenchToolBar, new class extends mock<MenuWorkbenchToolBar>() {
override set context(_context: unknown) { }
Expand All @@ -218,10 +218,16 @@ suite('Sessions - SessionsList', () => {
instantiationService.stub(IAccessibilityService, new class extends TestAccessibilityService {
override isMotionReduced(): boolean { return false; }
}());
instantiationService.stub(ISessionsListModelService, new class extends mock<ISessionsListModelService>() { });
instantiationService.stub(ISessionsListModelService, new class extends mock<ISessionsListModelService>() {
override getStatusIcon(_status: SessionStatus, isRead: boolean) {
return isRead ? Codicon.circleSmallFilled : Codicon.circleFilled;
}
});
const contextKeyService = disposables.add(new ContextKeyService(new TestConfigurationService()));
const runs = observableValue<readonly IAutomationRun[]>(disposables, []);
const badgePresentation = observableValue<AutomationsNewBadgeStyle | undefined>(disposables, 'outline');
const automationService = new class extends mock<IAutomationService>() {
override readonly runs = constObservable<readonly IAutomationRun[]>([]);
override readonly runs = runs;
};
const renderer = new SessionSectionRenderer(
true,
Expand All @@ -230,7 +236,7 @@ suite('Sessions - SessionsList', () => {
contextKeyService,
automationService,
constObservable([]),
constObservable('outline'),
badgePresentation,
new class extends mock<IUriIdentityService>() {
override readonly extUri = new ExtUri(() => true);
},
Expand All @@ -248,11 +254,28 @@ suite('Sessions - SessionsList', () => {
collapsible: false,
collapsed: false,
}), 0, template);
const automationSnapshot = {
text: template.newBadge.textContent,
display: template.newBadge.style.display,
ariaHidden: template.newBadge.getAttribute('aria-hidden'),
};
const getPresentationSnapshot = () => ({
badgeText: template.newBadge.textContent,
badgeDisplay: template.newBadge.style.display,
badgeAriaHidden: template.newBadge.getAttribute('aria-hidden'),
hasOutlineBadge: template.newBadge.classList.contains('session-section-new-badge-outline'),
hasUnreadDot: !!container.querySelector('.session-section-icon > .codicon-circle-filled:not([data-icon-fading-out="1"])'),
hasSpinner: !!container.querySelector('.session-section-icon > .monaco-pixel-spinner:not([data-icon-fading-out="1"])'),
hasCalendar: template.icon.classList.contains('codicon-calendar'),
});
const outline = getPresentationSnapshot();

badgePresentation.set('unread', undefined);
const unread = getPresentationSnapshot();

runs.set([upcastPartial<IAutomationRun>({ status: 'running' })], undefined);
const running = getPresentationSnapshot();

runs.set([], undefined);
const unreadRestored = getPresentationSnapshot();

badgePresentation.set(undefined, undefined);
const dismissed = getPresentationSnapshot();

renderer.renderElement(upcastPartial<Parameters<SessionSectionRenderer['renderElement']>[0]>({
element: { id: 'workspace:test', label: 'Test', sessions: [] },
Expand All @@ -261,14 +284,58 @@ suite('Sessions - SessionsList', () => {
}), 0, template);

assert.deepStrictEqual({
automationSnapshot,
outline,
unread,
running,
unreadRestored,
dismissed,
recycledDisplay: template.newBadge.style.display,
recycledShortcutClass: template.container.classList.contains('session-section-shortcut'),
}, {
automationSnapshot: {
text: 'New',
display: 'inline-flex',
ariaHidden: 'true',
outline: {
badgeText: 'New',
badgeDisplay: 'inline-flex',
badgeAriaHidden: 'true',
hasOutlineBadge: true,
hasUnreadDot: false,
hasSpinner: false,
hasCalendar: true,
},
unread: {
badgeText: 'New',
badgeDisplay: 'none',
badgeAriaHidden: 'true',
hasOutlineBadge: false,
hasUnreadDot: true,
hasSpinner: false,
hasCalendar: false,
},
running: {
badgeText: 'New',
badgeDisplay: 'none',
badgeAriaHidden: 'true',
hasOutlineBadge: false,
hasUnreadDot: false,
hasSpinner: true,
hasCalendar: false,
},
unreadRestored: {
badgeText: 'New',
badgeDisplay: 'none',
badgeAriaHidden: 'true',
hasOutlineBadge: false,
hasUnreadDot: true,
hasSpinner: false,
hasCalendar: false,
},
dismissed: {
badgeText: 'New',
badgeDisplay: 'none',
badgeAriaHidden: 'true',
hasOutlineBadge: false,
hasUnreadDot: false,
hasSpinner: false,
hasCalendar: true,
},
recycledDisplay: 'none',
recycledShortcutClass: false,
Expand All @@ -279,7 +346,7 @@ suite('Sessions - SessionsList', () => {
const activeCustomView = observableValue<ICustomViewDescriptor | undefined>(disposables, undefined);
const harness = createListHarness(disposables, [], instantiationService => {
ChatAutomationsEnabledContext.bindTo(instantiationService.get(IContextKeyService)).set(true);
void (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'outline');
void (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'unread');
instantiationService.stub(IAutomationService, new class extends mock<IAutomationService>() {
override readonly automations = constObservable([]);
override readonly runs = constObservable([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,16 @@ async function renderSessionsList(ctx: ComponentFixtureContext, options: IRender
override isSessionPinned(): boolean { return false; }
override migrateLegacyReadState(): void { }
override getSortKey(session: ISession): number { return session.createdAt.getTime(); }
override getStatusIcon(status: SessionStatus): ThemeIcon {
override getStatusIcon(status: SessionStatus, isRead: boolean): ThemeIcon {
switch (status) {
case SessionStatus.InProgress:
return { ...Codicon.sessionInProgress, color: themeColorFromId('textLink.foreground') };
case SessionStatus.NeedsInput:
return { ...Codicon.circleFilled, color: themeColorFromId('list.warningForeground') };
default:
if (!isRead) {
return { ...Codicon.circleFilled, color: themeColorFromId('textLink.foreground') };
}
return { ...Codicon.circleSmallFilled, color: themeColorFromId('agentSessionReadIndicator.foreground') };
}
}
Expand Down Expand Up @@ -626,6 +629,16 @@ export default defineThemedFixtureGroup({ path: 'sessions/' }, {
automationBadgeStyle: 'soft',
}),
}),
SessionsList_AutomationsNewBadge_Unread: defineComponentFixture({
labels: { kind: 'screenshot', blocksCi: true },
additionalThemes: ['darkHighContrast'],
expectedVisualDescriptions: ['The Automations row uses the standard filled blue unread indicator in its leading icon slot to signal the new feature and does not show a trailing NEW capsule, while the Sessions header retains its outlined New button.'],
render: ctx => renderSessionsList(ctx, {
sessions: [],
showAutomations: true,
automationBadgeStyle: 'unread',
}),
}),
SessionsList_AutomationsNewBadge_Narrow: defineComponentFixture({
labels: { kind: 'screenshot', blocksCi: true },
additionalThemes: ['darkHighContrast'],
Expand Down
9 changes: 9 additions & 0 deletions test/componentFixtures/blocks-ci-screenshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,15 @@
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Light
![screenshot](https://hediet-screenshots.azurewebsites.net/images/af268bfa64dd8a47c8e05e756742156c27b0ff49306146e2aa343260b5838735)

#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Dark
![screenshot](https://hediet-screenshots.azurewebsites.net/images/e0a82b05f1166a334c8d48990d1dbfa3c4ae6dfa358a88ca3cf56fffbf93792e)

#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/DarkHighContrast
![screenshot](https://hediet-screenshots.azurewebsites.net/images/a1db74bd19da0772ca4caeaf341646344e2cb775708c5732f00a30fd94473960)

#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Light
![screenshot](https://hediet-screenshots.azurewebsites.net/images/38d000ecc104d74252f7bac7d47f712e8deb2bd4907657d50993cbf0ee9fcc3e)

#### sessions/sessionsList/SessionsList_AutomationsNewBadge/Dark
![screenshot](https://hediet-screenshots.azurewebsites.net/images/4c6dfa53103d4a06dd28aa56baad6798cd36894da97bffc351618aed8b10396f)

Expand Down