mirror of
https://github.com/microsoft/vscode.git
synced 2026-10-02 05:21:20 +01:00
Implement new badge design for automations in sidebar (#334091)
* sessions: feat: show New badge for Automations Show a theme-aware first-use badge until the Automations view is activated. Preserve the decision across local profiles and cover migration, accessibility, recycled templates, and visual states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Agent Host changes for ulugbekna/agents/sidebar-automation-new-badge-designs * sessions: fix: address Automations badge review feedback Fall back when treatment resolution fails, preserve high-contrast badge colors, cover observable accessible labels, and render the production Sessions header in fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 922f1dc5-a262-4914-a0b4-7fed0cb79b4b * sessions: test: accept Automations badge screenshots Add the Linux-rendered blocks-CI hashes for the new badge style, narrow, running, and high-contrast fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 922f1dc5-a262-4914-a0b4-7fed0cb79b4b --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 922f1dc5-a262-4914-a0b4-7fed0cb79b4b
This commit is contained in:
co-authored by
Copilot
parent
80c7fc21e4
commit
71bb7d0ed7
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun, derived, observableValue } from '../../../../base/common/observable.js';
|
||||
import { onUnexpectedError } from '../../../../base/common/errors.js';
|
||||
import { IConfigurationService, isConfigured } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { observableMemento, type ObservableMemento } from '../../../../platform/observable/common/observableMemento.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IAutomationService } from '../../../../workbench/contrib/chat/common/automations/automationService.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../workbench/services/assignment/common/assignmentService.js';
|
||||
import { ICustomViewService } from '../../../services/customView/browser/customViewService.js';
|
||||
import { AUTOMATIONS_CUSTOM_VIEW_ID } from './automationsConstants.js';
|
||||
|
||||
export const AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY = 'sessions.automations.newBadgeSeen';
|
||||
export const AUTOMATIONS_NEW_BADGE_STYLE_SETTING = 'sessions.automations.newBadgeStyle';
|
||||
export const AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT = 'agentSessionsAutomationsNewBadgeStyle';
|
||||
|
||||
export type AutomationsNewBadgeStyle = 'accent' | 'soft' | 'outline';
|
||||
|
||||
const DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE: AutomationsNewBadgeStyle = 'outline';
|
||||
|
||||
const automationsNewBadgeSeenMemento = observableMemento<boolean>({
|
||||
key: AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY,
|
||||
defaultValue: false,
|
||||
toStorage: value => String(value),
|
||||
fromStorage: value => value === 'true',
|
||||
});
|
||||
|
||||
/** Owns the first-use state for the Automations shortcut badge. */
|
||||
export class AutomationsNewBadgeState extends Disposable {
|
||||
|
||||
private readonly seen: ObservableMemento<boolean>;
|
||||
private readonly resolvedStyle = observableValue<AutomationsNewBadgeStyle | undefined>(this, undefined);
|
||||
private observingActiveView = false;
|
||||
private initializationPromise: Promise<void> | undefined;
|
||||
private styleRequest = 0;
|
||||
readonly presentation = derived(this, reader => this.seen.read(reader) ? undefined : this.resolvedStyle.read(reader));
|
||||
readonly showNewBadge = derived(this, reader => this.presentation.read(reader) !== undefined);
|
||||
|
||||
constructor(
|
||||
@IAutomationService private readonly automationService: IAutomationService,
|
||||
@ICustomViewService private readonly customViewService: ICustomViewService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
this.seen = this._register(automationsNewBadgeSeenMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, storageService));
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
if (!this.observingActiveView) {
|
||||
this.observingActiveView = true;
|
||||
this._register(autorun(reader => {
|
||||
if (this.customViewService.activeCustomView.read(reader)?.id === AUTOMATIONS_CUSTOM_VIEW_ID) {
|
||||
this.markSeen();
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (!this.initializationPromise) {
|
||||
this.initializationPromise = this.doInitialize();
|
||||
this._register(this.configurationService.onDidChangeConfiguration(event => {
|
||||
if (event.affectsConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING)) {
|
||||
void this.updateStyle().catch(onUnexpectedError);
|
||||
}
|
||||
}));
|
||||
this._register(this.assignmentService.onDidRefetchAssignments(() => {
|
||||
void this.updateStyle().catch(onUnexpectedError);
|
||||
}));
|
||||
}
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
this.seen.set(false, undefined);
|
||||
this.storageService.remove(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
this.resolvedStyle.set(undefined, undefined);
|
||||
await this.updateStyle();
|
||||
}
|
||||
|
||||
private async doInitialize(): Promise<void> {
|
||||
const hasPriorUse = this.seen.get()
|
||||
|| this.automationService.automations.get().length > 0
|
||||
|| this.automationService.runs.get().length > 0;
|
||||
if (hasPriorUse) {
|
||||
this.markSeen();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateStyle();
|
||||
}
|
||||
|
||||
private async updateStyle(): Promise<void> {
|
||||
if (this.seen.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = ++this.styleRequest;
|
||||
const inspection = this.configurationService.inspect<string>(AUTOMATIONS_NEW_BADGE_STYLE_SETTING);
|
||||
let value: string | undefined;
|
||||
if (isConfigured(inspection)) {
|
||||
value = inspection.value;
|
||||
} else {
|
||||
try {
|
||||
value = await this.assignmentService.getTreatment<string>(AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT);
|
||||
} catch (error) {
|
||||
this.logService.warn(`[AutomationsNewBadgeState] Failed to resolve badge style treatment; using '${DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE}'.`, error);
|
||||
}
|
||||
}
|
||||
if (request !== this.styleRequest || this.seen.get()) {
|
||||
return;
|
||||
}
|
||||
this.resolvedStyle.set(this.normalizeStyle(value), undefined);
|
||||
}
|
||||
|
||||
private normalizeStyle(value: string | undefined): AutomationsNewBadgeStyle {
|
||||
if (value === undefined || value === DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE) {
|
||||
return DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE;
|
||||
}
|
||||
if (value === 'accent' || value === 'soft') {
|
||||
return value;
|
||||
}
|
||||
this.logService.warn(`[AutomationsNewBadgeState] Unsupported badge style treatment '${value}'; using '${DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE}'.`);
|
||||
return DEFAULT_AUTOMATIONS_NEW_BADGE_STYLE;
|
||||
}
|
||||
|
||||
private markSeen(): void {
|
||||
if (!this.seen.get()) {
|
||||
this.seen.set(true, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -724,6 +724,39 @@
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.session-section-new-badge {
|
||||
display: none;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: var(--vscode-spacing-size160);
|
||||
margin-left: var(--vscode-spacing-size40);
|
||||
padding: 0 var(--vscode-spacing-size40);
|
||||
box-sizing: border-box;
|
||||
border: var(--vscode-strokeThickness) solid transparent;
|
||||
border-radius: var(--vscode-cornerRadius-circle);
|
||||
color: var(--vscode-foreground);
|
||||
font-size: var(--vscode-fontSize-label3);
|
||||
font-weight: var(--vscode-fontWeight-semiBold);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.session-section-new-badge-accent {
|
||||
border-color: var(--vscode-activityBarBadge-background);
|
||||
background-color: var(--vscode-activityBarBadge-background);
|
||||
color: var(--vscode-activityBarBadge-foreground);
|
||||
}
|
||||
|
||||
.session-section-new-badge-soft {
|
||||
background-color: color-mix(in srgb, var(--vscode-foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
.session-section-new-badge-outline {
|
||||
border-color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.session-section-toolbar {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
@@ -750,6 +783,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hc-black .session-section-new-badge,
|
||||
.hc-light .session-section-new-badge {
|
||||
border-color: var(--vscode-contrastBorder);
|
||||
}
|
||||
|
||||
.monaco-list-row:hover .session-section .session-section-toolbar,
|
||||
.monaco-list-row.focused .session-section .session-section-toolbar,
|
||||
.monaco-list-row .session-section.dropdown-active .session-section-toolbar {
|
||||
|
||||
@@ -18,8 +18,9 @@ import { SessionsView, SessionsViewId } from './views/sessionsView.js';
|
||||
import { AutomationsCustomViewContribution } from './views/automationsView.js';
|
||||
import './views/sessionsViewActions.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
|
||||
import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
|
||||
import { SESSIONS_LIST_SHOW_EMPTY_DEFAULT_GROUPS_SETTING } from './views/sessionsList.js';
|
||||
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING, AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT } from './automationsNewBadge.js';
|
||||
import { SessionsMouseNavigationContribution } from './sessionsMouseNavigation.js';
|
||||
import './sessionDetailsAction.js';
|
||||
import { SessionsWindowNotifier } from './sessionsWindowNotifier.js';
|
||||
@@ -69,6 +70,19 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
|
||||
default: true,
|
||||
experiment: { mode: 'auto' }
|
||||
},
|
||||
[AUTOMATIONS_NEW_BADGE_STYLE_SETTING]: {
|
||||
type: 'string',
|
||||
enum: ['accent', 'soft', 'outline'],
|
||||
default: 'outline',
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
included: false,
|
||||
tags: ['experimental'],
|
||||
experiment: {
|
||||
mode: 'auto',
|
||||
name: AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT,
|
||||
},
|
||||
description: localize('sessions.automations.newBadgeStyle', "Controls the visual style of the Automations first-use badge."),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/
|
||||
import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js';
|
||||
import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js';
|
||||
import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../automationsConstants.js';
|
||||
import { AutomationsNewBadgeState, type AutomationsNewBadgeStyle } from '../automationsNewBadge.js';
|
||||
import { Menus } from '../../../../browser/menus.js';
|
||||
import { getSessionConversationStatusAriaLabel } from '../../../../browser/sessionConversationGroups.js';
|
||||
import { getAgentMergeAwarePullRequestIcon, getSessionAgentMergeConfigurationObservable, ISessionAgentMergeConfiguration, isAgentMergePullRequestIcon } from '../../../../browser/sessionAgentMerge.js';
|
||||
@@ -1266,6 +1267,7 @@ interface ISessionSectionTemplate extends ISessionHeaderTemplate {
|
||||
readonly icon: HTMLElement;
|
||||
readonly label: HTMLElement;
|
||||
readonly count: HTMLElement;
|
||||
readonly newBadge: HTMLElement;
|
||||
readonly chevron: HTMLElement;
|
||||
readonly contextKeyService: IContextKeyService;
|
||||
readonly disposables: DisposableStore;
|
||||
@@ -1318,6 +1320,7 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
|
||||
private readonly contextKeyService: IContextKeyService,
|
||||
private readonly automationService: IAutomationService,
|
||||
private readonly automationSessions: IObservable<readonly ISession[]>,
|
||||
private readonly automationNewBadgePresentation: IObservable<AutomationsNewBadgeStyle | undefined>,
|
||||
private readonly uriIdentityService: IUriIdentityService,
|
||||
private readonly customViewService: ICustomViewService,
|
||||
private readonly menuService: IMenuService,
|
||||
@@ -1339,6 +1342,9 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
const label = DOM.append(container, $('span.session-section-label'));
|
||||
const count = DOM.append(container, $('span.session-section-count'));
|
||||
const newBadge = DOM.append(container, $('span.session-section-new-badge'));
|
||||
newBadge.setAttribute('aria-hidden', 'true');
|
||||
newBadge.textContent = localize('automationsNewBadge', "New");
|
||||
const toolbarContainer = DOM.append(container, $('.session-section-toolbar'));
|
||||
|
||||
const contextKeyService = disposables.add(this.contextKeyService.createScoped(container));
|
||||
@@ -1383,7 +1389,7 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
|
||||
},
|
||||
}));
|
||||
|
||||
return { container, icon, label, count, toolbarContainer, toolbar, chevron, contextKeyService, elementDisposables, disposables };
|
||||
return { container, icon, label, count, newBadge, toolbarContainer, toolbar, chevron, contextKeyService, elementDisposables, disposables };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<SessionListItem, FuzzyScore>, _index: number, template: ISessionSectionTemplate): void {
|
||||
@@ -1397,6 +1403,12 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
|
||||
this.templatesById.set(element.id, template);
|
||||
template.container.classList.remove(SESSION_HEADER_DROP_TARGET_CLASS);
|
||||
template.container.classList.remove('session-section-shortcut');
|
||||
template.newBadge.style.display = 'none';
|
||||
template.newBadge.classList.remove(
|
||||
'session-section-new-badge-accent',
|
||||
'session-section-new-badge-soft',
|
||||
'session-section-new-badge-outline',
|
||||
);
|
||||
if (element.id === AUTOMATIONS_SECTION_ID) {
|
||||
template.container.classList.add('session-section-shortcut');
|
||||
}
|
||||
@@ -1412,6 +1424,11 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
|
||||
template.elementDisposables.add(autorun(reader => {
|
||||
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.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 => {
|
||||
@@ -1747,6 +1764,7 @@ interface ISessionsAccessibilityProviderOptions {
|
||||
readonly isPinned: (session: ISession) => boolean;
|
||||
readonly isRenderedInCustomGroup?: (session: ISession) => boolean;
|
||||
readonly includeQuickChatInAriaLabel?: boolean;
|
||||
readonly automationNewBadgeVisible?: IObservable<boolean>;
|
||||
/** Mirrors {@link SessionItemRenderer}'s option of the same name — see there for rationale. */
|
||||
readonly deriveStatusFromMainChat?: boolean;
|
||||
}
|
||||
@@ -1776,20 +1794,23 @@ class SessionsAccessibilityProvider {
|
||||
}
|
||||
if (isSessionSection(element)) {
|
||||
if (element.id === AUTOMATIONS_SECTION_ID) {
|
||||
return this.automationStatus
|
||||
? derived(this, reader => {
|
||||
switch (this.automationStatus?.read(reader)) {
|
||||
case SessionStatus.NeedsInput:
|
||||
return localize('automationsNeedsInputAria', "{0}, run needs input", element.label);
|
||||
case SessionStatus.InProgress:
|
||||
return localize('automationsActiveAria', "{0}, run in progress", element.label);
|
||||
case SessionStatus.Completed:
|
||||
return localize('automationsUnreadRunAria', "{0}, unread run", element.label);
|
||||
default:
|
||||
return element.label;
|
||||
}
|
||||
})
|
||||
: element.label;
|
||||
return derived(this, reader => {
|
||||
let label = element.label;
|
||||
switch (this.automationStatus?.read(reader)) {
|
||||
case SessionStatus.NeedsInput:
|
||||
label = localize('automationsNeedsInputAria', "{0}, run needs input", element.label);
|
||||
break;
|
||||
case SessionStatus.InProgress:
|
||||
label = localize('automationsActiveAria', "{0}, run in progress", element.label);
|
||||
break;
|
||||
case SessionStatus.Completed:
|
||||
label = localize('automationsUnreadRunAria', "{0}, unread run", element.label);
|
||||
break;
|
||||
}
|
||||
return this.options?.automationNewBadgeVisible?.read(reader)
|
||||
? localize('automationsNewFeatureAria', "{0}, new feature", label)
|
||||
: label;
|
||||
});
|
||||
}
|
||||
return `${element.label}, ${element.sessions.length}`;
|
||||
}
|
||||
@@ -2323,6 +2344,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
*/
|
||||
private readonly chatApprovalHeightReconcile = this._register(new MutableDisposable());
|
||||
private readonly automationSessions = observableValue<readonly ISession[]>(this, []);
|
||||
private readonly automationsNewBadgeState: AutomationsNewBadgeState;
|
||||
/**
|
||||
* Session IDs whose hierarchy indent/connector guides should be visible:
|
||||
* the union of the currently-hovered session (if any) and every session
|
||||
@@ -2396,6 +2418,13 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
|
||||
get element(): HTMLElement { return this.listContainer; }
|
||||
|
||||
async resetAutomationsNewBadge(): Promise<void> {
|
||||
if (this.customViewService.activeCustomView.get()?.id === AUTOMATIONS_CUSTOM_VIEW_ID) {
|
||||
this.customViewService.hideCustomView();
|
||||
}
|
||||
await this.automationsNewBadgeState.reset();
|
||||
}
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
private readonly options: ISessionsListControlOptions,
|
||||
@@ -2424,6 +2453,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
@IPreferencesService private readonly preferencesService: IPreferencesService,
|
||||
) {
|
||||
super();
|
||||
this.automationsNewBadgeState = this._register(instantiationService.createInstance(AutomationsNewBadgeState));
|
||||
|
||||
// Load excluded session types from storage
|
||||
this.excludedSessionTypes = this.loadExcludedSessionTypes();
|
||||
@@ -2518,7 +2548,18 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
this.tree.setFocus([element], event);
|
||||
this.tree.setSelection([element], event);
|
||||
};
|
||||
const sectionRenderer = new SessionSectionRenderer(true /* hideSectionCount */, selectHeader, instantiationService, contextKeyService, this.automationService, this.automationSessions, this.uriIdentityService, this.customViewService, this.menuService);
|
||||
const sectionRenderer = new SessionSectionRenderer(
|
||||
true /* hideSectionCount */,
|
||||
selectHeader,
|
||||
instantiationService,
|
||||
contextKeyService,
|
||||
this.automationService,
|
||||
this.automationSessions,
|
||||
this.automationsNewBadgeState.presentation,
|
||||
this.uriIdentityService,
|
||||
this.customViewService,
|
||||
this.menuService,
|
||||
);
|
||||
this._sectionRenderer = sectionRenderer;
|
||||
const groupRenderer = new SessionGroupRenderer({
|
||||
commitEdit: (group, name) => this.commitGroupEdit(group, name),
|
||||
@@ -2561,6 +2602,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
isPinned: session => this.isSessionPinned(session),
|
||||
isRenderedInCustomGroup: session => this.isRenderedInCustomGroup(session),
|
||||
deriveStatusFromMainChat: true,
|
||||
automationNewBadgeVisible: this.automationsNewBadgeState.showNewBadge,
|
||||
}),
|
||||
dnd: this._register(new SessionsListDragAndDrop({
|
||||
isReorderable: session => this.isReorderable(session),
|
||||
@@ -3200,6 +3242,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
};
|
||||
|
||||
if (this.contextKeyService.getContextKeyValue<boolean>(ChatAutomationsEnabledContext.key)) {
|
||||
void this.automationsNewBadgeState.initialize().catch(onUnexpectedError);
|
||||
children.push(renderSection({ id: AUTOMATIONS_SECTION_ID, label: localize('automations', "Automations"), sessions: [] }));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,40 @@ export const SessionsViewGroupingContext = new RawContextKey<string>('sessionsVi
|
||||
export const SessionsViewSortingContext = new RawContextKey<string>('sessionsViewPane.sorting', SessionsSorting.Created);
|
||||
export const IsWorkspaceGroupCappedContext = new RawContextKey<boolean>('sessionsViewPane.workspaceGroupCapped', true);
|
||||
|
||||
export interface ISessionsHeaderElements {
|
||||
readonly row: HTMLElement;
|
||||
readonly label: HTMLElement;
|
||||
readonly actions: HTMLElement;
|
||||
readonly toolbar: MenuWorkbenchToolBar | undefined;
|
||||
}
|
||||
|
||||
export function renderSessionsHeader(
|
||||
parent: HTMLElement,
|
||||
phoneLayout: boolean,
|
||||
instantiationService: IInstantiationService,
|
||||
contextKeyService: IContextKeyService,
|
||||
disposables: DisposableStore,
|
||||
): ISessionsHeaderElements {
|
||||
const row = DOM.append(parent, $('.agent-sessions-header-row'));
|
||||
const label = DOM.append(row, $('.agent-sessions-header-label'));
|
||||
const actions = DOM.append(row, $('.agent-sessions-header-actions'));
|
||||
let toolbar: MenuWorkbenchToolBar | undefined;
|
||||
|
||||
if (!phoneLayout) {
|
||||
label.textContent = localize('sessionsHeader', "Sessions");
|
||||
const scopedInstantiationService = disposables.add(instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
|
||||
toolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, actions, Menus.SidebarSessionsHeader, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
telemetrySource: 'sessionsView.header',
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
}));
|
||||
} else {
|
||||
row.classList.add('phone-layout-empty');
|
||||
}
|
||||
|
||||
return { row, label, actions, toolbar };
|
||||
}
|
||||
|
||||
export class SessionsView extends ViewPane {
|
||||
|
||||
private viewPaneContainer: HTMLElement | undefined;
|
||||
@@ -156,32 +190,15 @@ export class SessionsView extends ViewPane {
|
||||
// Sessions content container
|
||||
const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content'));
|
||||
|
||||
// Header row: "Sessions" label (left) + compact "New" button (right)
|
||||
const headerRow = this.headerRow = DOM.append(sessionsContent, $('.agent-sessions-header-row'));
|
||||
const headerLabel = this.headerLabel = DOM.append(headerRow, $('.agent-sessions-header-label'));
|
||||
|
||||
const headerActions = this.headerActions = DOM.append(headerRow, $('.agent-sessions-header-actions'));
|
||||
|
||||
// On phone, the desktop header content (label + new button + filter/find toolbar)
|
||||
// is hidden in favor of the mobile filter chip row + the (+) button in the
|
||||
// MobileTitlebarPart. We still create the row container because the find
|
||||
// widget mounts inside it.
|
||||
const phoneLayout = isPhoneLayout(this.layoutService);
|
||||
if (!phoneLayout) {
|
||||
headerLabel.textContent = localize('sessionsHeader', "Sessions");
|
||||
|
||||
// Header actions (visual order: New, Filter, Search). The "New" button is
|
||||
// contributed to Menus.SidebarSessionsHeader and rendered as a compact pill
|
||||
// by NewSessionActionViewItem.
|
||||
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
|
||||
this._register(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, headerActions, Menus.SidebarSessionsHeader, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
telemetrySource: 'sessionsView.header',
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
}));
|
||||
} else {
|
||||
headerRow.classList.add('phone-layout-empty');
|
||||
}
|
||||
const header = renderSessionsHeader(sessionsContent, phoneLayout, this.instantiationService, this.scopedContextKeyService, this._register(new DisposableStore()));
|
||||
const headerRow = this.headerRow = header.row;
|
||||
this.headerLabel = header.label;
|
||||
this.headerActions = header.actions;
|
||||
|
||||
// Container for the tree's find widget (toggled by the toolbar's Find action)
|
||||
const findWidgetContainer = this.findWidgetContainer = DOM.append(headerRow, $('.agent-sessions-find-widget-container'));
|
||||
|
||||
@@ -9,10 +9,12 @@ import { KeyChord, KeyCode, KeyMod } from '../../../../../base/common/keyCodes.j
|
||||
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { isMobile, isWeb } from '../../../../../base/common/platform.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IsDevelopmentContext } from '../../../../../platform/contextkey/common/contextkeys.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
@@ -40,6 +42,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
|
||||
import { registerExternalSessionsFilterMenu } from '../../../../../workbench/contrib/chat/browser/agentSessions/externalSessionsFilterMenu.js';
|
||||
import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js';
|
||||
import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js';
|
||||
import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js';
|
||||
import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../automationsConstants.js';
|
||||
|
||||
const CLOSE_SESSION_COMMAND_ID = 'sessionsViewPane.closeSession';
|
||||
@@ -1336,6 +1339,23 @@ registerAction2(class ManageAutomationsAction extends Action2 {
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class ResetAutomationsNewBadgeAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'sessions.developer.resetAutomationsNewBadge',
|
||||
title: localize2('resetAutomationsNewBadge', "Reset Automations New Badge"),
|
||||
category: Categories.Developer,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(IsDevelopmentContext, IsSessionsWindowContext, ChatAutomationsEnabledContext),
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const view = await accessor.get(IViewsService).openView<SessionsView>(SessionsViewId, false);
|
||||
await view?.sessionsControl?.resetAutomationsNewBadge();
|
||||
}
|
||||
});
|
||||
|
||||
const MARK_ALL_AUTOMATION_RUNS_READ_COMMAND_ID = 'sessionsView.markAllAutomationRunsRead';
|
||||
|
||||
registerAction2(class MarkAllAutomationRunsReadAction extends Action2 {
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { Emitter } from '../../../../../base/common/event.js';
|
||||
import { observableValue } from '../../../../../base/common/observable.js';
|
||||
import { mock, upcastPartial } from '../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import type { IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { NullLogService } from '../../../../../platform/log/common/log.js';
|
||||
import { InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import type { IAutomationDescriptor, IAutomationRun } from '../../../../../workbench/contrib/chat/common/automations/automation.js';
|
||||
import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../../workbench/services/assignment/common/assignmentService.js';
|
||||
import type { ICustomViewDescriptor } from '../../../../services/customView/browser/customView.js';
|
||||
import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js';
|
||||
import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../../browser/automationsConstants.js';
|
||||
import { AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, AUTOMATIONS_NEW_BADGE_STYLE_SETTING, AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT, AutomationsNewBadgeState, type AutomationsNewBadgeStyle } from '../../browser/automationsNewBadge.js';
|
||||
|
||||
suite('AutomationsNewBadgeState', () => {
|
||||
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
class TestAssignmentService extends mock<IWorkbenchAssignmentService>() {
|
||||
readonly treatments: string[] = [];
|
||||
override readonly onDidRefetchAssignments;
|
||||
|
||||
constructor(
|
||||
private readonly style: AutomationsNewBadgeStyle | undefined,
|
||||
refetchAssignments: Emitter<void>,
|
||||
private readonly error?: Error,
|
||||
) {
|
||||
super();
|
||||
this.onDidRefetchAssignments = refetchAssignments.event;
|
||||
}
|
||||
|
||||
override async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
|
||||
this.treatments.push(name);
|
||||
if (this.error) {
|
||||
throw this.error;
|
||||
}
|
||||
return this.style as T | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createState(options: {
|
||||
readonly automations?: readonly IAutomationDescriptor[];
|
||||
readonly runs?: readonly IAutomationRun[];
|
||||
readonly activeView?: ICustomViewDescriptor;
|
||||
readonly seen?: boolean;
|
||||
readonly style?: AutomationsNewBadgeStyle;
|
||||
readonly configuredStyle?: AutomationsNewBadgeStyle;
|
||||
readonly treatmentError?: Error;
|
||||
} = {}) {
|
||||
const storageService = disposables.add(new InMemoryStorageService());
|
||||
if (options.seen) {
|
||||
storageService.store(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
}
|
||||
const automations = observableValue<readonly IAutomationDescriptor[]>(disposables, options.automations ?? []);
|
||||
const runs = observableValue<readonly IAutomationRun[]>(disposables, options.runs ?? []);
|
||||
const activeView = observableValue<ICustomViewDescriptor | undefined>(disposables, options.activeView);
|
||||
const automationService = new class extends mock<IAutomationService>() {
|
||||
override readonly automations = automations;
|
||||
override readonly runs = runs;
|
||||
};
|
||||
const customViewService = new class extends mock<ICustomViewService>() {
|
||||
override readonly activeCustomView = activeView;
|
||||
};
|
||||
const refetchAssignments = disposables.add(new Emitter<void>());
|
||||
const assignmentService = new TestAssignmentService(options.style, refetchAssignments, options.treatmentError);
|
||||
const configurationService = new TestConfigurationService();
|
||||
if (options.configuredStyle) {
|
||||
void configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, options.configuredStyle);
|
||||
}
|
||||
const state = disposables.add(new AutomationsNewBadgeState(
|
||||
automationService,
|
||||
customViewService,
|
||||
storageService,
|
||||
assignmentService,
|
||||
configurationService,
|
||||
new NullLogService(),
|
||||
));
|
||||
return { state, storageService, automations, runs, activeView, assignmentService, configurationService, refetchAssignments };
|
||||
}
|
||||
|
||||
test('keeps the resolved style stable until Automations is activated', async () => {
|
||||
const { state, storageService, automations, runs, activeView } = createState();
|
||||
|
||||
await state.initialize();
|
||||
automations.set([upcastPartial<IAutomationDescriptor>({ id: 'late-automation' })], undefined);
|
||||
runs.set([upcastPartial<IAutomationRun>({ id: 'late-run' })], undefined);
|
||||
const beforeActivation = {
|
||||
showNewBadge: state.showNewBadge.get(),
|
||||
style: state.presentation.get(),
|
||||
stored: storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
};
|
||||
|
||||
activeView.set(upcastPartial<ICustomViewDescriptor>({ id: AUTOMATIONS_CUSTOM_VIEW_ID }), undefined);
|
||||
const afterActivation = {
|
||||
showNewBadge: state.showNewBadge.get(),
|
||||
stored: storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual({ beforeActivation, afterActivation }, {
|
||||
beforeActivation: { showNewBadge: true, style: 'outline', stored: undefined },
|
||||
afterActivation: {
|
||||
showNewBadge: false,
|
||||
stored: 'true',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves accent, soft, and outline from the hidden treatment', async () => {
|
||||
const snapshots = [];
|
||||
for (const style of ['accent', 'soft', 'outline'] as const) {
|
||||
const fixture = createState({ style });
|
||||
await fixture.state.initialize();
|
||||
snapshots.push({
|
||||
style: fixture.state.presentation.get(),
|
||||
treatments: fixture.assignmentService.treatments,
|
||||
});
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(snapshots, [
|
||||
{ style: 'accent', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
|
||||
{ style: 'soft', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
|
||||
{ style: 'outline', treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('falls back to outline when treatment resolution fails', async () => {
|
||||
const fixture = createState({ treatmentError: new Error('Unavailable') });
|
||||
|
||||
await fixture.state.initialize();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
style: fixture.state.presentation.get(),
|
||||
treatments: fixture.assignmentService.treatments,
|
||||
}, {
|
||||
style: 'outline',
|
||||
treatments: [AUTOMATIONS_NEW_BADGE_STYLE_TREATMENT],
|
||||
});
|
||||
});
|
||||
|
||||
test('lets the hidden setting override and live-update the treatment', async () => {
|
||||
const fixture = createState({ style: 'outline', configuredStyle: 'soft' });
|
||||
await fixture.state.initialize();
|
||||
const initial = fixture.state.presentation.get();
|
||||
|
||||
await fixture.configurationService.setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, 'accent');
|
||||
fixture.configurationService.onDidChangeConfigurationEmitter.fire(upcastPartial<IConfigurationChangeEvent>({
|
||||
affectsConfiguration: (key: string) => key === AUTOMATIONS_NEW_BADGE_STYLE_SETTING,
|
||||
}));
|
||||
|
||||
assert.deepStrictEqual({
|
||||
initial,
|
||||
updated: fixture.state.presentation.get(),
|
||||
treatments: fixture.assignmentService.treatments,
|
||||
}, {
|
||||
initial: 'soft',
|
||||
updated: 'accent',
|
||||
treatments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('resets seen state for development even when prior Automation evidence exists', async () => {
|
||||
const fixture = createState({
|
||||
automations: [upcastPartial<IAutomationDescriptor>({ id: 'existing-automation' })],
|
||||
style: 'accent',
|
||||
});
|
||||
await fixture.state.initialize();
|
||||
|
||||
await fixture.state.reset();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
showNewBadge: fixture.state.showNewBadge.get(),
|
||||
style: fixture.state.presentation.get(),
|
||||
stored: fixture.storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
}, {
|
||||
showNewBadge: true,
|
||||
style: 'accent',
|
||||
stored: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('suppresses the badge when synchronous Automation evidence exists', async () => {
|
||||
const definition = createState({
|
||||
automations: [upcastPartial<IAutomationDescriptor>({ id: 'existing-automation' })],
|
||||
});
|
||||
const run = createState({
|
||||
runs: [upcastPartial<IAutomationRun>({ id: 'existing-run' })],
|
||||
});
|
||||
|
||||
await definition.state.initialize();
|
||||
await run.state.initialize();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
definition: {
|
||||
showNewBadge: definition.state.showNewBadge.get(),
|
||||
stored: definition.storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
},
|
||||
run: {
|
||||
showNewBadge: run.state.showNewBadge.get(),
|
||||
stored: run.storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
},
|
||||
}, {
|
||||
definition: { showNewBadge: false, stored: 'true' },
|
||||
run: { showNewBadge: false, stored: 'true' },
|
||||
});
|
||||
});
|
||||
|
||||
test('honors persisted and restored seen state before the row renders', async () => {
|
||||
const persisted = createState({ seen: true });
|
||||
const restored = createState({
|
||||
activeView: upcastPartial<ICustomViewDescriptor>({ id: AUTOMATIONS_CUSTOM_VIEW_ID }),
|
||||
});
|
||||
|
||||
await persisted.state.initialize();
|
||||
await restored.state.initialize();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
persisted: {
|
||||
showNewBadge: persisted.state.showNewBadge.get(),
|
||||
},
|
||||
restored: {
|
||||
showNewBadge: restored.state.showNewBadge.get(),
|
||||
stored: restored.storageService.get(AUTOMATIONS_NEW_BADGE_SEEN_STORAGE_KEY, StorageScope.APPLICATION),
|
||||
},
|
||||
}, {
|
||||
persisted: { showNewBadge: false },
|
||||
restored: { showNewBadge: false, stored: 'true' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import { IMenuService } from '../../../../../platform/actions/common/actions.js'
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js';
|
||||
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { ILabelService } from '../../../../../platform/label/common/label.js';
|
||||
@@ -27,12 +28,14 @@ import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import { IAutomationRun } from '../../../../../workbench/contrib/chat/common/automations/automation.js';
|
||||
import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js';
|
||||
import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js';
|
||||
import { IPreferencesService, IOpenSettingsOptions } from '../../../../../workbench/services/preferences/common/preferences.js';
|
||||
import { AgentMergeSessionState } from '../../../../../platform/agentHost/common/agentMerge.js';
|
||||
import { getSessionChatDragData, isSessionChatDrag, SessionsDataTransfers } from '../../../../browser/dnd.js';
|
||||
import { IsPhoneLayoutContext } from '../../../../common/contextkeys.js';
|
||||
import { IAgentHostSessionsProvider, LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../../common/agentHostSessionsProvider.js';
|
||||
import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js';
|
||||
import type { ICustomViewDescriptor } from '../../../../services/customView/browser/customView.js';
|
||||
import { ISessionsListModelService } from '../../../../services/sessions/browser/sessionsListModelService.js';
|
||||
import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js';
|
||||
import { ChatInteractivity, ChatOriginKind, IChat, ISession, SessionStatus } from '../../../../services/sessions/common/session.js';
|
||||
@@ -45,6 +48,8 @@ import { getSessionSummaryHoverData } from '../../browser/sessionHoverContent.js
|
||||
import { createListHarness, createTestSession } from './sessionsListTestUtils.js';
|
||||
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';
|
||||
|
||||
function createSession(id: string, opts: {
|
||||
workspaceLabel?: string;
|
||||
@@ -119,6 +124,7 @@ suite('Sessions - SessionsList', () => {
|
||||
contextKeyService,
|
||||
automationService,
|
||||
constObservable([]),
|
||||
constObservable(undefined),
|
||||
new class extends mock<IUriIdentityService>() {
|
||||
override readonly extUri = new ExtUri(() => true);
|
||||
},
|
||||
@@ -171,6 +177,7 @@ suite('Sessions - SessionsList', () => {
|
||||
contextKeyService,
|
||||
automationService,
|
||||
constObservable([]),
|
||||
constObservable(undefined),
|
||||
new class extends mock<IUriIdentityService>() {
|
||||
override readonly extUri = new ExtUri(() => true);
|
||||
},
|
||||
@@ -201,6 +208,106 @@ suite('Sessions - SessionsList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the new badge 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) { }
|
||||
override dispose(): void { }
|
||||
});
|
||||
instantiationService.stub(IAccessibilityService, new class extends TestAccessibilityService {
|
||||
override isMotionReduced(): boolean { return false; }
|
||||
}());
|
||||
instantiationService.stub(ISessionsListModelService, new class extends mock<ISessionsListModelService>() { });
|
||||
const contextKeyService = disposables.add(new ContextKeyService(new TestConfigurationService()));
|
||||
const automationService = new class extends mock<IAutomationService>() {
|
||||
override readonly runs = constObservable<readonly IAutomationRun[]>([]);
|
||||
};
|
||||
const renderer = new SessionSectionRenderer(
|
||||
true,
|
||||
() => { },
|
||||
instantiationService,
|
||||
contextKeyService,
|
||||
automationService,
|
||||
constObservable([]),
|
||||
constObservable('outline'),
|
||||
new class extends mock<IUriIdentityService>() {
|
||||
override readonly extUri = new ExtUri(() => true);
|
||||
},
|
||||
new class extends mock<ICustomViewService>() {
|
||||
override readonly activeCustomView = constObservable(undefined);
|
||||
},
|
||||
new class extends mock<IMenuService>() { },
|
||||
);
|
||||
const container = document.createElement('div');
|
||||
const template = renderer.renderTemplate(container);
|
||||
disposables.add(template.disposables);
|
||||
|
||||
renderer.renderElement(upcastPartial<Parameters<SessionSectionRenderer['renderElement']>[0]>({
|
||||
element: { id: 'automations', label: 'Automations', sessions: [] },
|
||||
collapsible: false,
|
||||
collapsed: false,
|
||||
}), 0, template);
|
||||
const automationSnapshot = {
|
||||
text: template.newBadge.textContent,
|
||||
display: template.newBadge.style.display,
|
||||
ariaHidden: template.newBadge.getAttribute('aria-hidden'),
|
||||
};
|
||||
|
||||
renderer.renderElement(upcastPartial<Parameters<SessionSectionRenderer['renderElement']>[0]>({
|
||||
element: { id: 'workspace:test', label: 'Test', sessions: [] },
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
}), 0, template);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
automationSnapshot,
|
||||
recycledDisplay: template.newBadge.style.display,
|
||||
recycledShortcutClass: template.container.classList.contains('session-section-shortcut'),
|
||||
}, {
|
||||
automationSnapshot: {
|
||||
text: 'New',
|
||||
display: 'inline-flex',
|
||||
ariaHidden: 'true',
|
||||
},
|
||||
recycledDisplay: 'none',
|
||||
recycledShortcutClass: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('updates the Automations row accessible label when the new badge is dismissed', () => {
|
||||
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');
|
||||
instantiationService.stub(IAutomationService, new class extends mock<IAutomationService>() {
|
||||
override readonly automations = constObservable([]);
|
||||
override readonly runs = constObservable([]);
|
||||
});
|
||||
instantiationService.stub(ICustomViewService, new class extends mock<ICustomViewService>() {
|
||||
override readonly activeCustomView = activeCustomView;
|
||||
});
|
||||
});
|
||||
const container = harness.createContainer();
|
||||
const list = harness.store.add(harness.instantiationService.createInstance(SessionsList, container, {
|
||||
grouping: () => SessionsGrouping.Date,
|
||||
sorting: () => SessionsSorting.Created,
|
||||
onSessionOpen: () => { },
|
||||
}));
|
||||
list.layout(300, 400);
|
||||
const row = container.querySelector<HTMLElement>('.monaco-list-row');
|
||||
const before = row?.getAttribute('aria-label');
|
||||
|
||||
activeCustomView.set(upcastPartial<ICustomViewDescriptor>({ id: AUTOMATIONS_CUSTOM_VIEW_ID }), undefined);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
before,
|
||||
after: row?.getAttribute('aria-label'),
|
||||
}, {
|
||||
before: 'Automations, new feature',
|
||||
after: 'Automations',
|
||||
});
|
||||
});
|
||||
|
||||
test('derives terminal automation status from the supplied session snapshot', () => {
|
||||
const session = createSession('automation', {
|
||||
isRead: false,
|
||||
@@ -234,6 +341,7 @@ suite('Sessions - SessionsList', () => {
|
||||
new class extends mock<IContextKeyService>() { },
|
||||
automationService,
|
||||
automationSessions,
|
||||
constObservable(undefined),
|
||||
uriIdentityService,
|
||||
new class extends mock<ICustomViewService>() { },
|
||||
new class extends mock<IMenuService>() { },
|
||||
@@ -301,6 +409,7 @@ suite('Sessions - SessionsList', () => {
|
||||
new class extends mock<IContextKeyService>() { },
|
||||
automationService,
|
||||
constObservable([runningSession, needsInputSession]),
|
||||
constObservable(undefined),
|
||||
uriIdentityService,
|
||||
new class extends mock<ICustomViewService>() { },
|
||||
new class extends mock<IMenuService>() { },
|
||||
|
||||
@@ -4,21 +4,26 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import * as DOM from '../../../../../base/browser/dom.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { constObservable, IObservable } from '../../../../../base/common/observable.js';
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { constObservable, IObservable, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { ExtUri } from '../../../../../base/common/resources.js';
|
||||
import { ThemeIcon, themeColorFromId } from '../../../../../base/common/themables.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { IActionViewItemFactory, IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { IListService, ListService } from '../../../../../platform/list/browser/listService.js';
|
||||
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IMenu, IMenuService, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IMenu, IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
|
||||
import { EditorMarkdownCodeBlockRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/editorMarkdownCodeBlockRenderer.js';
|
||||
import { IMarkdownRendererService, MarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IAgentHostConnectionsService } from '../../../../../platform/agentHost/common/agentHostConnectionsService.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { IAgentHostFilterService } from '../../../../../sessions/services/agentHostFilter/common/agentHostFilter.js';
|
||||
@@ -35,25 +40,64 @@ import { ISessionsService } from '../../../../../sessions/services/sessions/brow
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { ICustomViewService } from '../../../../../sessions/services/customView/browser/customViewService.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { Menus } from '../../../../../sessions/browser/menus.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { IChat, ISession, ISessionChangesSummary, ISessionFolder, ISessionWorkspace, SessionStatus, ChatInteractivity } from '../../../../../sessions/services/sessions/common/session.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { IActiveSession, ISessionsManagementService } from '../../../../../sessions/services/sessions/common/sessionsManagement.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { SessionsGrouping, SessionsList, SessionsSorting } from '../../../../../sessions/contrib/sessions/browser/views/sessionsList.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { AUTOMATIONS_NEW_BADGE_STYLE_SETTING, type AutomationsNewBadgeStyle } from '../../../../../sessions/contrib/sessions/browser/automationsNewBadge.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { renderSessionsHeader } from '../../../../../sessions/contrib/sessions/browser/views/sessionsView.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { NewSessionActionViewItemContribution } from '../../../../../sessions/contrib/sessions/browser/sessionsActions.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { NEW_SESSION_ACTION_ID } from '../../../../../sessions/contrib/chat/common/constants.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { IsPhoneLayoutContext } from '../../../../../sessions/common/contextkeys.js';
|
||||
import { AgentSessionApprovalKind, AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../../contrib/chat/browser/agentSessions/agentSessionApprovalModel.js';
|
||||
import { IAgentSessionsService } from '../../../../contrib/chat/browser/agentSessions/agentSessionsService.js';
|
||||
import { IAgentSession, IAgentSessionsModel } from '../../../../contrib/chat/browser/agentSessions/agentSessionsModel.js';
|
||||
import { IAutomationService } from '../../../../contrib/chat/common/automations/automationService.js';
|
||||
import type { IAutomationRun } from '../../../../contrib/chat/common/automations/automation.js';
|
||||
import { ChatAutomationsEnabledContext } from '../../../../contrib/chat/common/automations/automationsEnabled.js';
|
||||
import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatModel } from '../../../../contrib/chat/common/model/chatModel.js';
|
||||
import { IVoicePlaybackService } from '../../../../contrib/chat/common/voicePlaybackService.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
|
||||
import { TestProductService } from '../../../common/workbenchTestServices.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js';
|
||||
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import '../../../../../sessions/contrib/sessions/browser/media/sessionsList.css';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import '../../../../../sessions/contrib/sessions/browser/media/sessionsViewPane.css';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import '../../../../../sessions/contrib/sessions/browser/media/newSessionActionViewItem.css';
|
||||
|
||||
class FixtureActionViewItemService extends Disposable implements IActionViewItemService {
|
||||
declare _serviceBrand: undefined;
|
||||
|
||||
private readonly providers = new Map<string, IActionViewItemFactory>();
|
||||
private readonly changeEmitter = this._register(new Emitter<MenuId>());
|
||||
readonly onDidChange = this.changeEmitter.event;
|
||||
|
||||
register(menu: MenuId, commandId: string | MenuId, provider: IActionViewItemFactory, event?: Event<unknown>): IDisposable {
|
||||
const key = `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`;
|
||||
this.providers.set(key, provider);
|
||||
const listener = event?.(() => this.changeEmitter.fire(menu));
|
||||
return toDisposable(() => {
|
||||
listener?.dispose();
|
||||
this.providers.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
lookUp(menu: MenuId, commandId: string | MenuId): IActionViewItemFactory | undefined {
|
||||
return this.providers.get(`${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface IChatSpec {
|
||||
readonly id: string;
|
||||
@@ -172,15 +216,20 @@ interface IRenderOptions {
|
||||
readonly width?: number;
|
||||
readonly phone?: boolean;
|
||||
readonly revealHierarchyGuides?: boolean;
|
||||
readonly showAutomations?: boolean;
|
||||
readonly automationRunStatus?: IAutomationRun['status'];
|
||||
readonly automationBadgeStyle?: AutomationsNewBadgeStyle;
|
||||
readonly showFocusedToolbar?: boolean;
|
||||
}
|
||||
|
||||
function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOptions): void | Promise<void> {
|
||||
async function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOptions): Promise<void> {
|
||||
const { container, disposableStore } = ctx;
|
||||
const approvals = new Map<string, IAgentSessionApprovalInfo>();
|
||||
const sessions = options.sessions.map(spec => createSession(spec, approvals));
|
||||
const approvalModel = createApprovalModel(approvals);
|
||||
const groups = options.groups ?? [];
|
||||
const automationRuns = observableValue<readonly IAutomationRun[]>(disposableStore, []);
|
||||
const actionViewItemService = disposableStore.add(new FixtureActionViewItemService());
|
||||
const membership = new Map<string, string>();
|
||||
for (const spec of options.sessions) {
|
||||
if (spec.group) {
|
||||
@@ -192,6 +241,7 @@ function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOption
|
||||
colorTheme: ctx.theme,
|
||||
additionalServices: reg => {
|
||||
registerWorkbenchServices(reg);
|
||||
reg.defineInstance(IProductService, TestProductService);
|
||||
if (options.showFocusedToolbar) {
|
||||
const archiveAction = new class extends mock<MenuItemAction>() {
|
||||
override readonly id = 'sessions.fixture.archive';
|
||||
@@ -279,7 +329,8 @@ function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOption
|
||||
override hasPendingResponse() { return false; }
|
||||
}());
|
||||
reg.defineInstance(IAutomationService, new class extends mock<IAutomationService>() {
|
||||
override readonly runs = constObservable([]);
|
||||
override readonly automations = constObservable([]);
|
||||
override readonly runs = automationRuns;
|
||||
}());
|
||||
reg.defineInstance(IWorkbenchAssignmentService, new class extends mock<IWorkbenchAssignmentService>() {
|
||||
override readonly onDidRefetchAssignments = Event.None;
|
||||
@@ -288,13 +339,40 @@ function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOption
|
||||
reg.defineInstance(IUriIdentityService, new class extends mock<IUriIdentityService>() {
|
||||
override readonly extUri = new ExtUri(() => true);
|
||||
}());
|
||||
reg.defineInstance(ICustomViewService, new class extends mock<ICustomViewService>() { }());
|
||||
reg.defineInstance(ICustomViewService, new class extends mock<ICustomViewService>() {
|
||||
override readonly activeCustomView = constObservable(undefined);
|
||||
}());
|
||||
},
|
||||
});
|
||||
if (options.showAutomations) {
|
||||
const contextKeyService = instantiationService.get(IContextKeyService);
|
||||
const newSessionAction = new MenuItemAction(
|
||||
{ id: NEW_SESSION_ACTION_ID, title: 'New Session' },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contextKeyService,
|
||||
instantiationService.get(ICommandService),
|
||||
);
|
||||
instantiationService.stub(IActionViewItemService, actionViewItemService);
|
||||
instantiationService.stub(IMenuService, new class extends mock<IMenuService>() {
|
||||
override createMenu(id: MenuId): IMenu {
|
||||
return {
|
||||
onDidChange: Event.None,
|
||||
getActions: () => id === Menus.SidebarSessionsHeader ? [['navigation', [newSessionAction]]] : [],
|
||||
dispose: () => { },
|
||||
};
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
// Render terminal-approval labels as real (monospace) code blocks — otherwise
|
||||
// the markdown renderer emits empty code-block spans and the command is blank.
|
||||
(instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration('editor', { fontFamily: 'monospace' });
|
||||
if (options.automationBadgeStyle) {
|
||||
await (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(AUTOMATIONS_NEW_BADGE_STYLE_SETTING, options.automationBadgeStyle);
|
||||
}
|
||||
instantiationService.get(IMarkdownRendererService).setDefaultCodeBlockRenderer(instantiationService.createInstance(EditorMarkdownCodeBlockRenderer));
|
||||
|
||||
// Phone layout is driven by both a CSS class (visual) and a context key (row
|
||||
@@ -303,6 +381,9 @@ function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOption
|
||||
if (options.phone) {
|
||||
IsPhoneLayoutContext.bindTo(instantiationService.get(IContextKeyService)).set(true);
|
||||
}
|
||||
if (options.showAutomations) {
|
||||
ChatAutomationsEnabledContext.bindTo(instantiationService.get(IContextKeyService)).set(true);
|
||||
}
|
||||
|
||||
const width = options.width ?? 340;
|
||||
container.style.width = `${width}px`;
|
||||
@@ -312,15 +393,41 @@ function renderSessionsList(ctx: ComponentFixtureContext, options: IRenderOption
|
||||
container.classList.add('agent-sessions-workbench', 'phone-layout');
|
||||
}
|
||||
|
||||
const listHost = container.ownerDocument.createElement('div');
|
||||
container.appendChild(listHost);
|
||||
let listParent = container;
|
||||
if (options.showAutomations) {
|
||||
container.classList.add('agent-sessions-viewpane', 'agent-sessions-section');
|
||||
const content = DOM.append(container, DOM.$('.agent-sessions-content'));
|
||||
disposableStore.add(instantiationService.createInstance(NewSessionActionViewItemContribution));
|
||||
renderSessionsHeader(content, false, instantiationService, instantiationService.get(IContextKeyService), disposableStore).toolbar?.refresh();
|
||||
listParent = content;
|
||||
}
|
||||
const listHost = DOM.append(listParent, DOM.$(options.showAutomations ? '.agent-sessions-control-container' : 'div'));
|
||||
const list = disposableStore.add(instantiationService.createInstance(SessionsList, listHost, {
|
||||
grouping: () => options.grouping ?? SessionsGrouping.Workspace,
|
||||
sorting: () => SessionsSorting.Created,
|
||||
onSessionOpen: () => { },
|
||||
approvalModel,
|
||||
}));
|
||||
list.layout(options.phone ? 260 : 220, width);
|
||||
list.layout(options.phone ? 260 : options.showAutomations ? 180 : 220, width);
|
||||
|
||||
if (options.automationRunStatus) {
|
||||
automationRuns.set([{
|
||||
id: 'fixture-run',
|
||||
automationId: 'fixture-automation',
|
||||
status: options.automationRunStatus,
|
||||
trigger: 'schedule',
|
||||
startedAt: new Date().toISOString(),
|
||||
leaderWindowId: 1,
|
||||
}], undefined);
|
||||
}
|
||||
await Promise.resolve();
|
||||
if (options.showAutomations && !container.querySelector('.agent-sessions-compact-new-button')) {
|
||||
const menu = instantiationService.get(IMenuService).createMenu(Menus.SidebarSessionsHeader, instantiationService.get(IContextKeyService));
|
||||
const actionCount = menu.getActions().flatMap(([, actions]) => actions).length;
|
||||
menu.dispose();
|
||||
const hasProvider = !!instantiationService.get(IActionViewItemService).lookUp(Menus.SidebarSessionsHeader, NEW_SESSION_ACTION_ID);
|
||||
throw new Error(`Expected the production New Session action; found ${actionCount} menu action(s), provider=${hasProvider}.`);
|
||||
}
|
||||
|
||||
if (options.showFocusedToolbar) {
|
||||
return Promise.resolve().then(() => {
|
||||
@@ -413,6 +520,55 @@ export default defineThemedFixtureGroup({ path: 'sessions/' }, {
|
||||
sessions: [{ id: 'c', title: 'Update onboarding copy', workspace: 'vscode-docs', minutesAgo: 180 }],
|
||||
}),
|
||||
}),
|
||||
SessionsList_AutomationsNewBadge: defineComponentFixture({
|
||||
labels: { kind: 'screenshot', blocksCi: true },
|
||||
additionalThemes: ['darkHighContrast'],
|
||||
expectedVisualDescriptions: ['The Sessions header has an outlined New button. Directly below it, the Automations row has a smaller right-aligned outlined NEW capsule that reads as a non-interactive feature badge rather than a second button.'],
|
||||
render: ctx => renderSessionsList(ctx, {
|
||||
sessions: [],
|
||||
showAutomations: true,
|
||||
}),
|
||||
}),
|
||||
SessionsList_AutomationsNewBadge_Accent: defineComponentFixture({
|
||||
labels: { kind: 'screenshot', blocksCi: true },
|
||||
additionalThemes: ['darkHighContrast'],
|
||||
expectedVisualDescriptions: ['The Automations row has a compact right-aligned NEW pill using the prominent activity badge colors, while the larger outlined New button remains visually distinct in the Sessions header.'],
|
||||
render: ctx => renderSessionsList(ctx, {
|
||||
sessions: [],
|
||||
showAutomations: true,
|
||||
automationBadgeStyle: 'accent',
|
||||
}),
|
||||
}),
|
||||
SessionsList_AutomationsNewBadge_Soft: defineComponentFixture({
|
||||
labels: { kind: 'screenshot', blocksCi: true },
|
||||
additionalThemes: ['darkHighContrast'],
|
||||
expectedVisualDescriptions: ['The Automations row has a compact right-aligned NEW pill with a subtle neutral fill, while the larger outlined New button remains visually distinct in the Sessions header.'],
|
||||
render: ctx => renderSessionsList(ctx, {
|
||||
sessions: [],
|
||||
showAutomations: true,
|
||||
automationBadgeStyle: 'soft',
|
||||
}),
|
||||
}),
|
||||
SessionsList_AutomationsNewBadge_Narrow: defineComponentFixture({
|
||||
labels: { kind: 'screenshot', blocksCi: true },
|
||||
additionalThemes: ['darkHighContrast'],
|
||||
expectedVisualDescriptions: ['At the 170px minimum sidebar width, the Automations label remains readable and the outlined NEW capsule stays right-aligned without changing the row height.'],
|
||||
render: ctx => renderSessionsList(ctx, {
|
||||
sessions: [],
|
||||
showAutomations: true,
|
||||
width: 170,
|
||||
}),
|
||||
}),
|
||||
SessionsList_AutomationsNewBadge_Running: defineComponentFixture({
|
||||
labels: { kind: 'screenshot', blocksCi: true },
|
||||
additionalThemes: ['darkHighContrast'],
|
||||
expectedVisualDescriptions: ['The Automations row shows its running status icon and the outlined NEW capsule together without overlap or layout shift.'],
|
||||
render: ctx => renderSessionsList(ctx, {
|
||||
sessions: [],
|
||||
showAutomations: true,
|
||||
automationRunStatus: 'running',
|
||||
}),
|
||||
}),
|
||||
SessionsList_CustomGroup_Phone: defineComponentFixture({
|
||||
render: ctx => renderSessionsList(ctx, { sessions: GROUPED_SESSIONS, groups: [GROUP], phone: true, width: 340 }),
|
||||
}),
|
||||
|
||||
@@ -210,6 +210,51 @@
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionWorkspacePicker/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/Dark
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/DarkHighContrast
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/Dark
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/DarkHighContrast
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/Dark
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/DarkHighContrast
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Dark
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/DarkHighContrast
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge/Dark
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge/DarkHighContrast
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_AutomationsNewBadge/Light
|
||||

|
||||
|
||||
#### sessions/sessionsList/SessionsList_NarrowHoverToolbar/Dark
|
||||

|
||||
|
||||
|
||||
Reference in New Issue
Block a user