Skip to content

Commit 15903df

Browse files
ulugbeknaCopilot
andauthored
[cherry-pick] Add unread treatment for Automations new badge (#334971)
Add unread treatment for Automations new badge (#334946) * automations: feat: add unread new badge treatment Reuse the standard Automations unread status indicator as an alternative first-use treatment while preserving operational status precedence and accessibility semantics. * automations: test: gate unread badge screenshots Opt the unread Automations badge fixture into the blocking screenshot gate so theme and rendering regressions are detected. * automations: test: accept unread badge screenshots Record the Linux CI hashes for the unread Automations badge in dark, high-contrast, and light themes. --------- (cherry picked from commit 1b921e3) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e9fc1f2 commit 15903df

7 files changed

Lines changed: 121 additions & 27 deletions

File tree

src/vs/sessions/contrib/sessions/browser/automationsNewBadge.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY = 'sessions.automations.newB
2121
export const AUTOMATIONS_NEW_BADGE_STYLE_SETTING = 'sessions.automations.newBadgeStyle';
2222
export const AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT = 'agentSessionsAutomationsNewBadgeStyle';
2323

24-
export type AutomationsNewBadgeStyle = 'accent' | 'soft' | 'outline';
24+
export type AutomationsNewBadgeStyle = 'accent' | 'soft' | 'outline' | 'unread';
2525

2626
const DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE: AutomationsNewBadgeStyle = 'outline';
2727

@@ -182,7 +182,7 @@ export class AutomationsNewBadgeState extends Disposable {
182182
if (value === undefined || value === DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE) {
183183
return DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE;
184184
}
185-
if (value === 'accent' || value === 'soft') {
185+
if (value === 'accent' || value === 'soft' || value === 'unread') {
186186
return value;
187187
}
188188
this.logService.warn(`[AutomationsNewBadgeState] Unsupported badge style treatment '${value}'; using '${DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE}'.`);

src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
7272
},
7373
[AUTOMATIONS_NEW_BADGE_STYLE_SETTING]: {
7474
type: 'string',
75-
enum: ['accent', 'soft', 'outline'],
75+
enum: ['accent', 'soft', 'outline', 'unread'],
7676
default: 'outline',
7777
scope: ConfigurationScope.APPLICATION,
7878
included: false,

src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1426,14 +1426,15 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
14261426
const activeCustomView = this.customViewService.activeCustomView.read(reader);
14271427
template.container.classList.toggle('active', activeCustomView?.id === AUTOMATIONS_CUSTOM_VIEW_ID);
14281428
const badgeStyle = this.automationNewBadgePresentation.read(reader);
1429-
template.newBadge.style.display = badgeStyle ? 'inline-flex' : 'none';
1429+
template.newBadge.style.display = badgeStyle && badgeStyle !== 'unread' ? 'inline-flex' : 'none';
14301430
template.newBadge.classList.toggle('session-section-new-badge-accent', badgeStyle === 'accent');
14311431
template.newBadge.classList.toggle('session-section-new-badge-soft', badgeStyle === 'soft');
14321432
template.newBadge.classList.toggle('session-section-new-badge-outline', badgeStyle === 'outline');
14331433
}));
14341434
const statusIcon = template.elementDisposables.add(this.instantiationService.createInstance(SessionStatusIcon, template.icon));
14351435
template.elementDisposables.add(autorun(reader => {
14361436
const automationStatus = this.automationStatus.read(reader);
1437+
const badgeStyle = this.automationNewBadgePresentation.read(reader);
14371438
if (automationStatus === SessionStatus.NeedsInput) {
14381439
template.icon.className = 'session-section-icon';
14391440
statusIcon.setStatus(SessionStatus.NeedsInput, true, false);
@@ -1443,6 +1444,9 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
14431444
} else if (automationStatus === SessionStatus.Completed) {
14441445
template.icon.className = 'session-section-icon';
14451446
statusIcon.setStatus(SessionStatus.Completed, false, false);
1447+
} else if (badgeStyle === 'unread') {
1448+
template.icon.className = 'session-section-icon';
1449+
statusIcon.setStatus(SessionStatus.Completed, false, false);
14461450
} else {
14471451
statusIcon.reset();
14481452
template.icon.className = `session-section-icon ${ThemeIcon.asClassName(Codicon.calendar)}`;

src/vs/sessions/contrib/sessions/test/browser/automationsNewBadge.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ suite('AutomationsNewBadgeState', () => {
138138
});
139139
});
140140

141-
test('resolves accent, soft, and outline for eligible returning users', async () => {
141+
test('resolves every supported style for eligible returning users', async () => {
142142
const snapshots = [];
143-
for (const style of ['accent', 'soft', 'outline'] as const) {
143+
for (const style of ['accent', 'soft', 'outline', 'unread'] as const) {
144144
const fixture = createState({ style });
145145
await fixture.state.initialize();
146146
snapshots.push({
@@ -153,6 +153,7 @@ suite('AutomationsNewBadgeState', () => {
153153
{ style: 'accent', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
154154
{ style: 'soft', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
155155
{ style: 'outline', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
156+
{ style: 'unread', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
156157
]);
157158
});
158159

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

274-
await fixture.configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'accent');
275+
await fixture.configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'unread');
275276
fixture.configurationService.onDidChangeConfigurationEmitter.fire(upcastPartial<IConfigurationChangeEvent>({
276277
affectsConfiguration: key => key === AUTOMATIONS_NEW_BADGE_STYLE_SETTING,
277278
}));
@@ -282,7 +283,7 @@ suite('AutomationsNewBadgeState', () => {
282283
treatments: fixture.assignmentService.treatments,
283284
}, {
284285
initial: 'soft',
285-
updated: 'accent',
286+
updated: 'unread',
286287
treatments: [],
287288
});
288289
});
@@ -305,7 +306,7 @@ suite('AutomationsNewBadgeState', () => {
305306
const fixture = createState({
306307
hadPriorWindowOpen: false,
307308
automations: [upcastPartial<IAutomationDescriptor>({ id: 'existing-automation' })],
308-
style: 'accent',
309+
style: 'unread',
309310
});
310311
await fixture.state.initialize();
311312

@@ -325,7 +326,7 @@ suite('AutomationsNewBadgeState', () => {
325326
},
326327
}, {
327328
preview: {
328-
style: 'accent',
329+
style: 'unread',
329330
stored: undefined,
330331
},
331332
afterActivation: {

src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { createListHarness, createTestSession } from './sessionsListTestUtils.js
4949
import '../../browser/views/sessionsViewActions.js';
5050
import { computePullRequestIcon, GitHubPullRequestState } from '../../../github/common/types.js';
5151
import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../../browser/automationsConstants.js';
52-
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING } from '../../browser/automationsNewBadge.js';
52+
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING, type AutomationsNewBadgeStyle } from '../../browser/automationsNewBadge.js';
5353

5454
function createSession(id: string, opts: {
5555
workspaceLabel?: string;
@@ -208,7 +208,7 @@ suite('Sessions - SessionsList', () => {
208208
});
209209
});
210210

211-
test('renders the new badge only on the Automations section when templates are recycled', () => {
211+
test('renders new badge presentations only on the Automations section when templates are recycled', () => {
212212
const instantiationService = disposables.add(new TestInstantiationService());
213213
instantiationService.stubInstance(MenuWorkbenchToolBar, new class extends mock<MenuWorkbenchToolBar>() {
214214
override set context(_context: unknown) { }
@@ -217,10 +217,16 @@ suite('Sessions - SessionsList', () => {
217217
instantiationService.stub(IAccessibilityService, new class extends TestAccessibilityService {
218218
override isMotionReduced(): boolean { return false; }
219219
}());
220-
instantiationService.stub(ISessionsListModelService, new class extends mock<ISessionsListModelService>() { });
220+
instantiationService.stub(ISessionsListModelService, new class extends mock<ISessionsListModelService>() {
221+
override getStatusIcon(_status: SessionStatus, isRead: boolean) {
222+
return isRead ? Codicon.circleSmallFilled : Codicon.circleFilled;
223+
}
224+
});
221225
const contextKeyService = disposables.add(new ContextKeyService(new TestConfigurationService()));
226+
const runs = observableValue<readonly IAutomationRun[]>(disposables, []);
227+
const badgePresentation = observableValue<AutomationsNewBadgeStyle | undefined>(disposables, 'outline');
222228
const automationService = new class extends mock<IAutomationService>() {
223-
override readonly runs = constObservable<readonly IAutomationRun[]>([]);
229+
override readonly runs = runs;
224230
};
225231
const renderer = new SessionSectionRenderer(
226232
true,
@@ -229,7 +235,7 @@ suite('Sessions - SessionsList', () => {
229235
contextKeyService,
230236
automationService,
231237
constObservable([]),
232-
constObservable('outline'),
238+
badgePresentation,
233239
new class extends mock<IUriIdentityService>() {
234240
override readonly extUri = new ExtUri(() => true);
235241
},
@@ -247,11 +253,28 @@ suite('Sessions - SessionsList', () => {
247253
collapsible: false,
248254
collapsed: false,
249255
}), 0, template);
250-
const automationSnapshot = {
251-
text: template.newBadge.textContent,
252-
display: template.newBadge.style.display,
253-
ariaHidden: template.newBadge.getAttribute('aria-hidden'),
254-
};
256+
const getPresentationSnapshot = () => ({
257+
badgeText: template.newBadge.textContent,
258+
badgeDisplay: template.newBadge.style.display,
259+
badgeAriaHidden: template.newBadge.getAttribute('aria-hidden'),
260+
hasOutlineBadge: template.newBadge.classList.contains('session-section-new-badge-outline'),
261+
hasUnreadDot: !!container.querySelector('.session-section-icon > .codicon-circle-filled:not([data-icon-fading-out="1"])'),
262+
hasSpinner: !!container.querySelector('.session-section-icon > .monaco-pixel-spinner:not([data-icon-fading-out="1"])'),
263+
hasCalendar: template.icon.classList.contains('codicon-calendar'),
264+
});
265+
const outline = getPresentationSnapshot();
266+
267+
badgePresentation.set('unread', undefined);
268+
const unread = getPresentationSnapshot();
269+
270+
runs.set([upcastPartial<IAutomationRun>({ status: 'running' })], undefined);
271+
const running = getPresentationSnapshot();
272+
273+
runs.set([], undefined);
274+
const unreadRestored = getPresentationSnapshot();
275+
276+
badgePresentation.set(undefined, undefined);
277+
const dismissed = getPresentationSnapshot();
255278

256279
renderer.renderElement(upcastPartial<Parameters<SessionSectionRenderer['renderElement']>[0]>({
257280
element: { id: 'workspace:test', label: 'Test', sessions: [] },
@@ -260,14 +283,58 @@ suite('Sessions - SessionsList', () => {
260283
}), 0, template);
261284

262285
assert.deepStrictEqual({
263-
automationSnapshot,
286+
outline,
287+
unread,
288+
running,
289+
unreadRestored,
290+
dismissed,
264291
recycledDisplay: template.newBadge.style.display,
265292
recycledShortcutClass: template.container.classList.contains('session-section-shortcut'),
266293
}, {
267-
automationSnapshot: {
268-
text: 'New',
269-
display: 'inline-flex',
270-
ariaHidden: 'true',
294+
outline: {
295+
badgeText: 'New',
296+
badgeDisplay: 'inline-flex',
297+
badgeAriaHidden: 'true',
298+
hasOutlineBadge: true,
299+
hasUnreadDot: false,
300+
hasSpinner: false,
301+
hasCalendar: true,
302+
},
303+
unread: {
304+
badgeText: 'New',
305+
badgeDisplay: 'none',
306+
badgeAriaHidden: 'true',
307+
hasOutlineBadge: false,
308+
hasUnreadDot: true,
309+
hasSpinner: false,
310+
hasCalendar: false,
311+
},
312+
running: {
313+
badgeText: 'New',
314+
badgeDisplay: 'none',
315+
badgeAriaHidden: 'true',
316+
hasOutlineBadge: false,
317+
hasUnreadDot: false,
318+
hasSpinner: true,
319+
hasCalendar: false,
320+
},
321+
unreadRestored: {
322+
badgeText: 'New',
323+
badgeDisplay: 'none',
324+
badgeAriaHidden: 'true',
325+
hasOutlineBadge: false,
326+
hasUnreadDot: true,
327+
hasSpinner: false,
328+
hasCalendar: false,
329+
},
330+
dismissed: {
331+
badgeText: 'New',
332+
badgeDisplay: 'none',
333+
badgeAriaHidden: 'true',
334+
hasOutlineBadge: false,
335+
hasUnreadDot: false,
336+
hasSpinner: false,
337+
hasCalendar: true,
271338
},
272339
recycledDisplay: 'none',
273340
recycledShortcutClass: false,
@@ -278,7 +345,7 @@ suite('Sessions - SessionsList', () => {
278345
const activeCustomView = observableValue<ICustomViewDescriptor | undefined>(disposables, undefined);
279346
const harness = createListHarness(disposables, [], instantiationService => {
280347
ChatAutomationsEnabledContext.bindTo(instantiationService.get(IContextKeyService)).set(true);
281-
void (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'outline');
348+
void (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'unread');
282349
instantiationService.stub(IAutomationService, new class extends mock<IAutomationService>() {
283350
override readonly automations = constObservable([]);
284351
override readonly runs = constObservable([]);

src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,16 @@ async function renderSessionsList(ctx: ComponentFixtureContext, options: IRender
317317
override isSessionPinned(): boolean { return false; }
318318
override migrateLegacyReadState(): void { }
319319
override getSortKey(session: ISession): number { return session.createdAt.getTime(); }
320-
override getStatusIcon(status: SessionStatus): ThemeIcon {
320+
override getStatusIcon(status: SessionStatus, isRead: boolean): ThemeIcon {
321321
switch (status) {
322322
case SessionStatus.InProgress:
323323
return { ...Codicon.sessionInProgress, color: themeColorFromId('textLink.foreground') };
324324
case SessionStatus.NeedsInput:
325325
return { ...Codicon.circleFilled, color: themeColorFromId('list.warningForeground') };
326326
default:
327+
if (!isRead) {
328+
return { ...Codicon.circleFilled, color: themeColorFromId('textLink.foreground') };
329+
}
327330
return { ...Codicon.circleSmallFilled, color: themeColorFromId('agentSessionReadIndicator.foreground') };
328331
}
329332
}
@@ -626,6 +629,16 @@ export default defineThemedFixtureGroup({ path: 'sessions/' }, {
626629
automationBadgeStyle: 'soft',
627630
}),
628631
}),
632+
SessionsList_AutomationsNewBadge_Unread: defineComponentFixture({
633+
labels: { kind: 'screenshot', blocksCi: true },
634+
additionalThemes: ['darkHighContrast'],
635+
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.'],
636+
render: ctx => renderSessionsList(ctx, {
637+
sessions: [],
638+
showAutomations: true,
639+
automationBadgeStyle: 'unread',
640+
}),
641+
}),
629642
SessionsList_AutomationsNewBadge_Narrow: defineComponentFixture({
630643
labels: { kind: 'screenshot', blocksCi: true },
631644
additionalThemes: ['darkHighContrast'],

test/componentFixtures/blocks-ci-screenshots.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@
288288
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Light
289289
![screenshot](https://hediet-screenshots.azurewebsites.net/images/af268bfa64dd8a47c8e05e756742156c27b0ff49306146e2aa343260b5838735)
290290

291+
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Dark
292+
![screenshot](https://hediet-screenshots.azurewebsites.net/images/e0a82b05f1166a334c8d48990d1dbfa3c4ae6dfa358a88ca3cf56fffbf93792e)
293+
294+
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/DarkHighContrast
295+
![screenshot](https://hediet-screenshots.azurewebsites.net/images/a1db74bd19da0772ca4caeaf341646344e2cb775708c5732f00a30fd94473960)
296+
297+
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Light
298+
![screenshot](https://hediet-screenshots.azurewebsites.net/images/38d000ecc104d74252f7bac7d47f712e8deb2bd4907657d50993cbf0ee9fcc3e)
299+
291300
#### sessions/sessionsList/SessionsList_AutomationsNewBadge/Dark
292301
![screenshot](https://hediet-screenshots.azurewebsites.net/images/4c6dfa53103d4a06dd28aa56baad6798cd36894da97bffc351618aed8b10396f)
293302

0 commit comments

Comments
 (0)