diff --git a/src/vs/sessions/contrib/sessions/browser/automationsNewBadge.ts b/src/vs/sessions/contrib/sessions/browser/automationsNewBadge.ts index bd41074595a2e..0ae5c4f85939f 100644 --- a/src/vs/sessions/contrib/sessions/browser/automationsNewBadge.ts +++ b/src/vs/sessions/contrib/sessions/browser/automationsNewBadge.ts @@ -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'; @@ -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}'.`); diff --git a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts index a539ec8ccb533..c223448b2c2cf 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts @@ -72,7 +72,7 @@ Registry.as(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, diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 2cc5e689089d7..accb73e11e340 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -1426,7 +1426,7 @@ export class SessionSectionRenderer implements ITreeRenderer { 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); @@ -1443,6 +1444,9 @@ export class SessionSectionRenderer implements ITreeRenderer { }); }); - 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({ @@ -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] }, ]); }); @@ -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({ affectsConfiguration: key => key === AUTOMATIONS_NEW_BADGE_STYLE_SETTING, })); @@ -282,7 +283,7 @@ suite('AutomationsNewBadgeState', () => { treatments: fixture.assignmentService.treatments, }, { initial: 'soft', - updated: 'accent', + updated: 'unread', treatments: [], }); }); @@ -305,7 +306,7 @@ suite('AutomationsNewBadgeState', () => { const fixture = createState({ hadPriorWindowOpen: false, automations: [upcastPartial({ id: 'existing-automation' })], - style: 'accent', + style: 'unread', }); await fixture.state.initialize(); @@ -325,7 +326,7 @@ suite('AutomationsNewBadgeState', () => { }, }, { preview: { - style: 'accent', + style: 'unread', stored: undefined, }, afterActivation: { diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts index 56650591abe70..9b22d044935be 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts @@ -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; @@ -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() { override set context(_context: unknown) { } @@ -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() { }); + instantiationService.stub(ISessionsListModelService, new class extends mock() { + override getStatusIcon(_status: SessionStatus, isRead: boolean) { + return isRead ? Codicon.circleSmallFilled : Codicon.circleFilled; + } + }); const contextKeyService = disposables.add(new ContextKeyService(new TestConfigurationService())); + const runs = observableValue(disposables, []); + const badgePresentation = observableValue(disposables, 'outline'); const automationService = new class extends mock() { - override readonly runs = constObservable([]); + override readonly runs = runs; }; const renderer = new SessionSectionRenderer( true, @@ -230,7 +236,7 @@ suite('Sessions - SessionsList', () => { contextKeyService, automationService, constObservable([]), - constObservable('outline'), + badgePresentation, new class extends mock() { override readonly extUri = new ExtUri(() => true); }, @@ -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({ status: 'running' })], undefined); + const running = getPresentationSnapshot(); + + runs.set([], undefined); + const unreadRestored = getPresentationSnapshot(); + + badgePresentation.set(undefined, undefined); + const dismissed = getPresentationSnapshot(); renderer.renderElement(upcastPartial[0]>({ element: { id: 'workspace:test', label: 'Test', sessions: [] }, @@ -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, @@ -279,7 +346,7 @@ suite('Sessions - SessionsList', () => { const activeCustomView = observableValue(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() { override readonly automations = constObservable([]); override readonly runs = constObservable([]); diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts index e4b6a37a1bbe1..6999d9d275d17 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts @@ -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') }; } } @@ -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'], diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 40d817c0be0ac..4f191855eb0dc 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -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)