Skip to content

automations: new management view - #328471

Merged
benvillalobos merged 23 commits into
mainfrom
benibenj/marginal-centipede
Aug 1, 2026
Merged

automations: new management view#328471
benvillalobos merged 23 commits into
mainfrom
benibenj/marginal-centipede

Conversation

@benvillalobos

@benvillalobos benvillalobos commented Jul 31, 2026

Copy link
Copy Markdown
Member

Adds a dedicated automations view to the agents window

  • Adds an automations shortcut to the sessions sidebar with active and status indicators cc @benibenj @sandy081
  • Displays automation cards and run history
  • Includes keyboard accessibility, focus behavior, layout, and styling

Removal of automations management from ai customizations in a future PR

image

benvillalobos and others added 13 commits July 31, 2026 10:58
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.
Copilot AI review requested due to automatic review settings July 31, 2026 18:30
@benvillalobos benvillalobos self-assigned this Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Base: 91663e91 Current: 74976360

No screenshot changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with Gesture.addTarget and handle TouchEventType.Tap so 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

Comment thread src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts
Comment thread src/vs/sessions/contrib/sessions/browser/media/sessionsList.css Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/media/automationsCards.css
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/views/automationsView.ts Outdated
Comment thread src/vs/sessions/contrib/sessions/browser/media/automationsCards.css
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.onDidChangeSessions and 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:2466 limits suppression to pointer focus; preserve :focus-visible so 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 from onDidChangeSessions.
		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 div button is gesture-enabled but lacks the Agents-window touch affordance required for custom clickable elements. Add touch-action: manipulation so 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 localize placeholders.
		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.md with 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

benvillalobos and others added 2 commits July 31, 2026 14:09
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.element by CustomViewNode. 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, getSession returns undefined and no isRead observable is read; when the provider later adds that session, the run remains non-clickable and cannot become unread until the runs array changes. Track onDidChangeSessions as 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-window touch-action: manipulation requirement. 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 getSession initially misses a linked run session, later onDidChangeSessions additions 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, AutomationsCustomViewFocusContext is 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 getSession returns undefined, no observable dependency is recorded, so the run remains non-clickable and its unread state remains absent even after onDidChangeSessions reports the session. Subscribe this render pipeline to session additions/removals as well as isRead.
				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

  • createPixelSpinner only disables animation through the OS prefers-reduced-motion media query. It does not honor the VS Code workbench.reduceMotion override, so these run cards still animate for users who enabled that setting. Use IAccessibilityService.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-visible focus 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 omits touch-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-label suppresses 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. AutomationService keeps 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

@benvillalobos
benvillalobos marked this pull request as ready for review July 31, 2026 23:32
roblourens
roblourens previously approved these changes Jul 31, 2026
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) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@benvillalobos
benvillalobos enabled auto-merge (squash) August 1, 2026 00:12
@benvillalobos
benvillalobos merged commit 9bc8e15 into main Aug 1, 2026
29 checks passed
@benvillalobos
benvillalobos deleted the benibenj/marginal-centipede branch August 1, 2026 00:26
@vs-code-engineering vs-code-engineering Bot added this to the 1.132.0 milestone Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants