automations: new management view - #328471
Conversation
Implements the Automations page as an AbstractCustomView using Ben J's custom view grid infrastructure. Registers via ICustomViewService and renders the cards widget (automation cards + run history with scroll). Adds ManageAutomations and MarkAllAutomationRunsRead actions.
Adds an Automations shortcut section to the sessions list sidebar. Clicking it opens the automations custom view via ICustomViewService. Shows a blue dot status indicator for active/unread automation runs.
- Use visibility instead of display for toolbar show/hide to prevent blue dot layout shift on hover - Suppress focus outline on shortcut rows (focused/selected states) - Preserve bright icon/label color when shortcut is active
…d polish - Move 'New Automation' from inline widget header to custom view header action bar via Action2 + CustomViewAutomations MenuId - Register PrimaryButtonActionViewItem via IActionViewItemService so the button renders as primary (blue) instead of secondary - Fix cards not rendering: remove stale newButtonElement references that crashed renderCards silently - Add openCreateDialog back for empty state create button - Remove inner DomScrollableElement (outer custom view handles scroll) - Remove widget tabIndex and suppress .custom-view-content focus outline - Fix mark-read race: store read ID before opening session - Use pixel spinner grid animation for running/pending history status - Remove small: true from button for better sizing
The custom view node constrains content to 950px (bandWidth), but this causes the scrollbar to sit within the content area at narrow widths. Chat sessions solve this by laying out content at full width and letting internal elements center themselves via CSS. Apply the same pattern: override the content element to full width via CSS, and let the widget self-center with max-width and margin: 0 auto.
This reverts commit e5d3c27933488671944760283a1073e4ec32f842.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change card role from 'button' to 'group' since it contains nested interactive elements (buttons inside a button is invalid ARIA) - Add Enter/Space keyboard handler on the card to open edit dialog - Add role='toolbar' and aria-label to the actions container - Improve card aria-label to include schedule info for screen readers The action buttons were already keyboard-reachable (tabindex=0) and visible on :focus-within, but the card itself lacked keyboard activation and had an invalid ARIA role.
Extract two internal classes from the 480-line monolith: - AutomationCardsSection: card grid, empty state, card interactions, edit/create/delete dialogs - AutomationHistorySection: history list, date grouping, run cards, read state management The widget becomes a thin composition layer that creates both sections, wires up observables, and handles layout. Shared helpers (formatSchedule, groupRunsByDate, runStatusIcon) are promoted to module-level functions. All classes remain in the same file, following the sessionsList.ts convention of keeping related renderers together.
Previously one autorun subscribed to automations, runs, and readStateVersion, causing both sections to fully rebuild when any observable fired. Now cards only re-render when automations change, and history only re-renders when runs, automations, or read state change.
…tly" This reverts commit d2fda41c2b09e49b220111be3f68ebcbb2591901.
|
Base:
|
There was a problem hiding this comment.
Pull request overview
Adds a dedicated Automations management view to the Agents window.
Changes:
- Adds an Automations sidebar shortcut with run indicators.
- Adds automation cards and run history.
- Adds custom-view actions, styling, and dialog focus behavior.
Show a summary per file
| File | Description |
|---|---|
sessionsViewActions.ts |
Adds automation commands. |
sessionsList.ts |
Adds the sidebar shortcut and status state. |
automationsView.ts |
Implements management cards and history. |
sessionsList.css |
Styles the shortcut and indicators. |
automationsCards.css |
Styles cards and run history. |
automationDialog.css |
Adds dialog focus styling. |
menus.ts |
Registers the custom-view menu. |
Review details
Suppressed comments (2)
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:409
- This second custom clickable card also handles only DOM
click. Per the Agents-window touch convention, register it withGesture.addTargetand handleTouchEventType.Tapso run history can be opened on iOS/touch devices.
this.disposables.add(DOM.addDisposableListener(card, 'click', () => {
const session = this.sessionsManagementService.getSession(URI.parse(run.sessionResource!));
if (!session) {
return;
}
this.markRunRead(run.id);
this.sessionsService.openSession(URI.parse(run.sessionResource!), { preserveFocus: false });
}));
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:278
- This second heading also uses an unsupported 14px size. Use the Agents heading3 type role rather than an off-ramp literal.
.automations-history-header {
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
- Files reviewed: 7/7 changed files
- Comments generated: 35
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (9)
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:430
- Session existence affects whether a run is unread and whether its card is keyboard-operable, but this view only rerenders for automation/run/read-storage changes. If providers hydrate or remove the referenced session afterward, the card remains permanently stale. Subscribe to
ISessionsManagementService.onDidChangeSessionsand rerender the history when the session catalog changes.
const hasUnread = runs.some(r =>
(r.status === 'completed' || r.status === 'failed') && r.sessionResource &&
!!this.sessionsManagementService.getSession(URI.parse(r.sessionResource)) && !readIds.has(r.id)
);
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:141
- Clearing and rebuilding every card detaches the currently focused control after create, edit, or delete, moving keyboard focus to the document body. Reconcile cards by automation id, or explicitly move focus to the updated/adjacent card (and cover create/delete/edit focus in tests).
render(automations: readonly IAutomation[]): void {
this.disposables.clear();
DOM.clearNode(this.container);
this.firstFocusableElement = undefined;
src/vs/sessions/contrib/automations/browser/media/automationDialog.css:7
- This rule also suppresses the dialog root's keyboard focus indicator. The same dialog styling in
aiCustomizationManagement.css:2466limits suppression to pointer focus; preserve:focus-visibleso keyboard users can still locate focus.
.monaco-workbench .monaco-dialog-box.automation-dialog:focus {
src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts:997
- This derived value calls the non-observable
getSession, so it is not invalidated when providers later add or remove the run's session. Existing completed/failed runs can therefore keep the wrong unread status indicator until an unrelated run or storage change occurs. Invalidate this derivation fromonDidChangeSessions.
const hasUnreadRun = (status: 'completed' | 'failed') => runs.some(run =>
run.status === status
&& !!run.sessionResource
&& !!this.sessionsManagementService.getSession(URI.parse(run.sessionResource))
&& !this.readAutomationRunIds.has(run.id)
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:203
- Run and Delete are revealed only by hover/focus, so they remain undiscoverable on phone/touch layouts where hover is unavailable and tapping the main card opens Edit. Match the established phone list behavior (
sessionsList.css:729-734) by keeping these actions visible in phone layout.
.automations-card-actions {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
flex: 0 0 auto;
opacity: 0;
}
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:336
- This custom
divbutton is gesture-enabled but lacks the Agents-window touch affordance required for custom clickable elements. Addtouch-action: manipulationso taps do not incur the mobile delay.
.automations-run-card.clickable {
cursor: pointer;
}
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:711
- These user-visible date/time combinations are assembled with template strings, so translators cannot control ordering or punctuation. Externalize each combination with
localizeplaceholders.
case 'week':
return `${date.toLocaleDateString(undefined, { weekday: 'short' })} ${time}`;
case 'month':
return `${date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${time}`;
src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts:2601
- The Sessions list spec still declares Pinned as always first and documents no Automations shortcut or its independent read/status storage. This new leading section makes that architecture documentation inaccurate; update
src/vs/sessions/SESSIONS_LIST.mdwith the shortcut's ordering, activation, status, and read-state behavior.
if (this.contextKeyService.getContextKeyValue<boolean>(ChatAutomationsEnabledContext.key)) {
children.push(renderSection({ id: AUTOMATIONS_SECTION_ID, label: localize('automations', "Automations"), sessions: [] }));
}
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:874
- Unlike the empty-state create path, the header action does not announce successful creation. After the dialog closes, screen-reader users receive no confirmation that the action completed; announce the created automation with
status().
try {
await automationService.createAutomation(result.value, () => {
if (!isEnabled()) {
throw new Error(localize('automationsDisabledBeforeSave', "Automations were disabled before the change could be saved."));
}
});
- Files reviewed: 12/12 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (7)
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:79
- The focus context only tracks the cards widget, but the “New Automation” header action is rendered outside
this.elementbyCustomViewNode. While that button has focus, both accessibility help and the accessible view stop matching. Derive this context from focus within the entire active custom-view node/part instead.
const focusContext = AutomationsCustomViewFocusContext.bindTo(contextKeyService);
const focusTracker = this._register(DOM.trackFocus(this.element));
this._register(focusTracker.onDidFocus(() => focusContext.set(true)));
this._register(focusTracker.onDidBlur(() => focusContext.set(false)));
this._register(toDisposable(() => focusContext.reset()));
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:150
- Clearing the entire grid on every automations emission removes whichever card control has focus. In particular, creating from the empty state or saving an edit replaces the focused trigger and leaves keyboard focus on the document body. Reconcile cards by automation ID (or explicitly restore focus to the corresponding replacement control) instead of rebuilding all cards.
render(automations: readonly IAutomation[]): void {
this.disposables.clear();
DOM.clearNode(this.container);
this.firstFocusableElement = undefined;
src/vs/sessions/contrib/automations/browser/media/automationDialog.css:7
- This suppresses the dialog root's focus ring for keyboard focus as well as backdrop clicks. Keep the existing keyboard-focus indication and suppress only pointer-origin focus with
:not(:focus-visible).
.monaco-workbench .monaco-dialog-box.automation-dialog:focus {
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:104
- This autorun does not observe changes to the session catalog. If a run is loaded before its linked session,
getSessionreturnsundefinedand noisReadobservable is read; when the provider later adds that session, the run remains non-clickable and cannot become unread until the runs array changes. TrackonDidChangeSessionsas an observable signal before resolving run sessions.
const session = this.sessionsManagementService.getSession(URI.parse(run.sessionResource));
if (session) {
sessions.set(run.id, { isRead: session.isRead.read(reader) });
}
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:410
- Every run-status or read-state update clears and recreates the history DOM. A focused run card is therefore detached by normal background transitions, and “Mark all as read” removes its focused button without choosing a successor. Preserve run rows by ID and explicitly move focus when the active control is intentionally removed.
render(runs: readonly IAutomationRun[], automations: readonly IAutomation[], sessions: ReadonlyMap<string, IAutomationRunSessionState>): void {
this.disposables.clear();
DOM.clearNode(this.container);
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:336
- This custom
role="button"card handles tap events, but it is missing the Agents-windowtouch-action: manipulationrequirement. Without it, touch users can incur delayed activation; add the property to the clickable state.
.automations-run-card.clickable {
cursor: pointer;
}
src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts:997
- The shortcut status has the same untracked-session-catalog gap as the history view: if
getSessioninitially misses a linked run session, lateronDidChangeSessionsadditions do not invalidate this derived value, so the unread indicator stays absent until automation runs change. Include a session-change observable in this derivation.
const hasUnreadRun = runs.some(run => {
if ((run.status !== 'completed' && run.status !== 'failed') || !run.sessionResource) {
return false;
}
const session = this.sessionsManagementService.getSession(URI.parse(run.sessionResource));
return !!session && !session.isRead.read(reader);
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (13)
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:79
- The focus context only tracks the widget body, but the custom-view header (including the New Automation button) is a sibling owned by
CustomViewNode. When keyboard focus is on that header action,AutomationsCustomViewFocusContextis false, so Open Accessibility Help/View is unavailable even though focus is still in Automations. Track focus at the whole custom-view node or expose an active-view context that includes its header.
const focusContext = AutomationsCustomViewFocusContext.bindTo(contextKeyService);
const focusTracker = this._register(DOM.trackFocus(this.element));
this._register(focusTracker.onDidFocus(() => focusContext.set(true)));
this._register(focusTracker.onDidBlur(() => focusContext.set(false)));
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:403
- Every run or linked-session read-state update clears and recreates the entire history. If a keyboard user is focused on a run card (or Mark all as read) when a background run changes status, the focused node is removed and focus falls out of the view. Preserve rows by run ID or explicitly restore focus to the equivalent run/header after rendering.
render(runs: readonly IAutomationRun[], automations: readonly IAutomation[], sessions: ReadonlyMap<string, IAutomationRunSessionState>): void {
this.disposables.clear();
DOM.clearNode(this.container);
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:104
- This lookup is not reactive to the session collection. On startup, automation runs can be loaded before their provider sessions; if
getSessionreturnsundefined, no observable dependency is recorded, so the run remains non-clickable and its unread state remains absent even afteronDidChangeSessionsreports the session. Subscribe this render pipeline to session additions/removals as well asisRead.
const session = this.sessionsManagementService.getSession(URI.parse(run.sessionResource));
if (session) {
sessions.set(run.id, { isRead: session.isRead.read(reader) });
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:321
- When an edit renames the automation, this announces the pre-edit name captured in
automation. Use the updated result so the confirmation accurately reflects what was saved.
status(localize('automationUpdatedStatus', "Updated automation {0}", automation.name));
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:265
- This decorative empty-state icon is not hidden from assistive technology. Other status icons in this view explicitly set
aria-hidden; do the same here so it does not add an unlabeled glyph before the heading.
const icon = DOM.append(this.emptyContainer, $('span.automations-cards-empty-icon'));
icon.classList.add(...ThemeIcon.asClassNameArray(automationIcon));
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:488
createPixelSpinneronly disables animation through the OSprefers-reduced-motionmedia query. It does not honor the VS Codeworkbench.reduceMotionoverride, so these run cards still animate for users who enabled that setting. UseIAccessibilityService.isMotionReduced()and react to its change event, rendering a static status icon when motion is reduced.
if (run.status === 'running' || run.status === 'pending') {
const spinnerContainer = DOM.append(statusRow, $('span.automations-run-card-icon'));
spinnerContainer.setAttribute('aria-hidden', 'true');
this.disposables.add(createPixelSpinner(spinnerContainer, { variant: 'grid' }));
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:14
focus()deliberately moves keyboard focus to this widget, but this rule removes every visible indication of that focus. The custom-view host normally gives its fallback content a:focus-visiblefocus border; provide the same keyboard-only indicator here so users can see where focus moved.
.monaco-workbench .automations-cards-widget:focus {
outline: none;
}
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:343
- This custom clickable element handles
Tap, but it omitstouch-action: manipulation. On touch devices that leaves the avoidable delayed-tap behavior that Agents-window custom triggers are expected to suppress.
.automations-run-card.clickable {
cursor: pointer;
}
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:170
- The card's accessible label omits both the disabled state and target, while the main button's explicit
aria-labelsuppresses its visible metadata from the button name. Consequently, screen-reader users cannot tell whether scheduling is disabled or which workspace/Quick Chat this automation targets. Include those fields in the group or control label.
card.setAttribute('aria-label', localize('automationCard', "{0} — {1}", automation.name, formatSchedule(automation)));
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:844
- Creating from the header action has no status announcement, unlike the empty-state create path above. A screen-reader user gets no confirmation that the dialog submission succeeded; capture the created automation and announce the same localized Created status.
await automationService.createAutomation(result.value, () => {
src/vs/sessions/contrib/sessions/browser/media/automationsCards.css:260
- The empty-state glyph is a codicon, but 28px is outside the codicon size ramp. Codicons must use the base 16px or compact 12px role; use the base token for this standalone icon.
font-size: 28px;
src/vs/sessions/contrib/sessions/browser/views/automationsView.ts:441
- This eagerly creates a DOM card for every retained run.
AutomationServicekeeps up to 50 runs per automation and does not cap the automation count, and this whole section is rebuilt on every run/read-state update, so a populated ledger can produce hundreds or thousands of nodes repeatedly. Use a virtualized list or paginate/cap the history rendered at once.
for (const run of group.runs) {
this.renderRunRow(groupGrid, run, automationMap, group.kind, sessions.get(run.id));
src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts:1286
- This hidden command is not used by the new button, so the same run filtering/deduplication logic now has two independent implementations (the other is
AutomationHistorySection.markAllRunsRead). Route the button through this command or extract a shared operation; otherwise fixes to read-state behavior can easily diverge.
const runs = automationService.runs.get();
const sessions = new Map<string, ISession>();
for (const run of runs) {
if ((run.status === 'completed' || run.status === 'failed') && run.sessionResource) {
const session = sessionsManagementService.getSession(URI.parse(run.sessionResource));
- Files reviewed: 11/11 changed files
- Comments generated: 0 new
- Review effort level: Balanced
| this.templatesById.set(element.id, template); | ||
| template.container.classList.remove(SESSION_HEADER_DROP_TARGET_CLASS); | ||
| template.container.classList.remove('session-section-shortcut'); | ||
| if (element.id === AUTOMATIONS_SECTION_ID) { |
There was a problem hiding this comment.
I considered making a new type like ISessionSection called something like ISessionNavLink, but this is the first button of this type so kept it simple here cc @benibenj
Adds a dedicated automations view to the agents window
Removal of automations management from ai customizations in a future PR