diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 3e47d272d003e..95d4d3a45c4ce 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -129,7 +129,7 @@ export interface ICommonNativeHostService { openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; - openAgentsWindow(): Promise; + openAgentsWindow(options?: { readonly forceNewWindow?: boolean }): Promise; isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 19620e7bf4d9f..8a4f25b4d7be9 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -304,11 +304,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }, options); } - async openAgentsWindow(windowId: number | undefined): Promise { + async openAgentsWindow(windowId: number | undefined, options?: { readonly forceNewWindow?: boolean }): Promise { await this.windowsMainService.openAgentsWindow({ context: OpenContext.API, contextWindowId: windowId, - cli: this.environmentMainService.args + cli: this.environmentMainService.args, + forceNewWindow: options?.forceNewWindow, }); } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index bf6622eb54d6f..a392fb01cb439 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -320,6 +320,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic context: openConfig.context, contextWindowId: openConfig.contextWindowId, initialStartup: openConfig.initialStartup, + forceNewWindow: openConfig.forceNewWindow, }; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsBanner.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsBanner.ts new file mode 100644 index 0000000000000..5d2389a684ab9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsBanner.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, addDisposableListener } from '../../../../../base/browser/dom.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { ICommandService, CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; + +const OPEN_AGENTS_WINDOW_COMMAND = 'workbench.action.openAgentsWindow'; + +type AgentsBannerClickedEvent = { + source: string; + action: string; +}; + +type AgentsBannerClickedClassification = { + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Where the banner was clicked from.' }; + action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The action taken on the banner.' }; + owner: 'benibenj'; + comment: 'Tracks clicks on the agents app banner across welcome pages.'; +}; + +export interface IAgentsBannerResult { + readonly element: HTMLElement; + readonly disposables: DisposableStore; +} + +/** + * Returns whether the agents banner can be shown. + * The banner requires the `workbench.action.openAgentsWindow` command + * to be registered (desktop builds only) and is limited to Insiders quality. + */ +export function canShowAgentsBanner(productService: IProductService): boolean { + return productService.quality !== 'stable' + && !!CommandsRegistry.getCommand(OPEN_AGENTS_WINDOW_COMMAND); +} + +export interface IAgentsBannerOptions { + /** Dot-separated CSS classes for the banner container (e.g. 'my-banner' or 'foo.bar'). */ + readonly cssClass: string; + /** Identifies where the banner is displayed (e.g. 'welcomePage', 'agentSessionsWelcome'). */ + readonly source: string; + /** Override the default button label. */ + readonly label?: string; + /** Optional callback invoked when the banner button is clicked. */ + readonly onButtonClick?: () => void; +} + +/** + * Creates a banner that promotes the Agents app. + * The banner contains a button that opens the Agents window. + */ +export function createAgentsBanner( + options: IAgentsBannerOptions, + commandService: ICommandService, + telemetryService: ITelemetryService, +): IAgentsBannerResult { + const disposables = new DisposableStore(); + const label = options.label ?? localize('agentsBanner.tryAgentsAppLabel', "Try out the new Agents app"); + + const button = $('button.agents-banner-button', { + title: label, + }, + $('.codicon.codicon-agent.icon-widget'), + $('span.category-title', {}, label), + ); + disposables.add(addDisposableListener(button, 'click', () => { + options.onButtonClick?.(); + telemetryService.publicLog2('agentsBanner.clicked', { source: options.source, action: 'openAgentsWindow' }); + commandService.executeCommand(OPEN_AGENTS_WINDOW_COMMAND, { forceNewWindow: true }); + })); + + const element = $(`.${options.cssClass}`, {}, button); + + return { element, disposables }; +} diff --git a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts index 5582a1393ed44..fd7faa6fb7834 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -34,7 +34,7 @@ export class OpenAgentsWindowAction extends Action2 { }); } - async run(accessor: ServicesAccessor) { + async run(accessor: ServicesAccessor, options?: { forceNewWindow?: boolean }) { const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); const environmentService = accessor.get(IWorkbenchEnvironmentService); @@ -43,7 +43,7 @@ export class OpenAgentsWindowAction extends Action2 { await openerService.open(URI.from({ scheme: productService.embedded.urlProtocol, authority: Schemas.file }), { openExternal: true }); } else { const nativeHostService = accessor.get(INativeHostService); - await nativeHostService.openAgentsWindow(); + await nativeHostService.openAgentsWindow({ forceNewWindow: options?.forceNewWindow }); } } } diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 0e3e5ccdeb569..1e787595db73f 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -60,6 +60,7 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { canShowAgentsBanner, createAgentsBanner } from '../../chat/browser/agentSessions/agentSessionsBanner.js'; const configurationKey = 'workbench.startupEditor'; const MAX_SESSIONS = 6; @@ -593,13 +594,21 @@ export class AgentSessionsWelcomePage extends EditorPane { this.layoutSessionsControl(); })); - // "View all sessions" link - const openButton = append(container, $('button.agentSessionsWelcome-openSessionsButton')); - openButton.textContent = localize('viewAllSessions', "View All Sessions"); - openButton.onclick = () => { - this._closedBy = 'viewAllSessions'; - this.revealMaximizedChat(); - }; + // "Try out the new Agents app" banner + if (canShowAgentsBanner(this.productService)) { + const agentsBanner = createAgentsBanner( + { + cssClass: 'agentSessionsWelcome-agentsBanner', + source: 'agentSessionsWelcome', + label: localize('viewAllSessions', "View All Sessions"), + onButtonClick: () => { this._closedBy = 'viewAllSessions'; }, + }, + this.commandService, + this.telemetryService, + ); + this.sessionsControlDisposables.add(agentsBanner.disposables); + append(container, agentsBanner.element); + } } private buildWalkthroughs(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index d4bc58c5786ee..e238e6f13644b 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -210,25 +210,49 @@ display: none !important; } -.agentSessionsWelcome-openSessionsButton { +.agentSessionsWelcome-agentsBanner { display: flex; align-items: center; justify-content: center; - gap: 6px; + gap: 8px; width: 100%; - padding: 8px 16px; margin-top: 12px; + font-size: 13px; +} + +.agentSessionsWelcome-agentsBanner .agents-banner-button { + display: flex; + align-items: center; + padding: 6px 10px; border: none; - border-radius: 4px; + border-radius: 6px; background-color: var(--vscode-toolbar-hoverBackground); color: var(--vscode-foreground); font-size: 13px; cursor: pointer; + font-family: inherit; + border-radius: 50px; + width: 100%; + justify-content: center; } -.agentSessionsWelcome-openSessionsButton:hover { +.agentSessionsWelcome-agentsBanner .codicon { + color: var(--vscode-textLink-foreground) !important; +} + +.agentSessionsWelcome-agentsBanner .agents-banner-button:hover { background-color: var(--vscode-toolbar-activeBackground); - color: var(--vscode-textLink-foreground); +} + +.agentSessionsWelcome-agentsBanner .icon-widget { + font-size: 20px; + padding-right: 8px; +} + +.agentSessionsWelcome-agentsBanner .category-title { + font-size: 13px; + font-weight: normal; + margin: 0; } /* Walkthroughs section */ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index d4670f21ac95a..4a956c6a8b577 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -69,6 +69,7 @@ import { IExtensionService } from '../../../services/extensions/common/extension import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchThemeService } from '../../../services/themes/common/workbenchThemeService.js'; import { GettingStartedIndexList } from './gettingStartedList.js'; +import { canShowAgentsBanner, createAgentsBanner } from '../../chat/browser/agentSessions/agentSessionsBanner.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibleViewAction } from '../../accessibility/browser/accessibleViewActions.js'; import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; @@ -931,11 +932,25 @@ export class GettingStartedPage extends EditorPane { const recentList = this.buildRecentlyOpenedList(); const gettingStartedList = this.buildGettingStartedWalkthroughsList(); - const footer = $('.footer', {}, - $('p.showOnStartup', {}, - showOnStartupCheckbox.domNode, - showOnStartupLabel, - )); + const footerChildren: HTMLElement[] = []; + if (canShowAgentsBanner(this.productService)) { + const agentsBanner = createAgentsBanner( + { + cssClass: 'getting-started-category.agents-banner', + source: 'welcomePage', + }, + this.commandService, + this.telemetryService, + ); + this.categoriesSlideDisposables.add(agentsBanner.disposables); + footerChildren.push(agentsBanner.element); + } + footerChildren.push($('p.showOnStartup', {}, + showOnStartupCheckbox.domNode, + showOnStartupLabel, + )); + + const footer = $('.footer', {}, ...footerChildren); const layoutLists = () => { if (gettingStartedList.itemCount) { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index f72871782c5f9..c661d88076cb6 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -916,6 +916,25 @@ margin: 0; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .footer > .agents-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-bottom: 12px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .footer > .agents-banner .agents-banner-button { + display: flex; + align-items: center; + padding: 6px 12px; + border-radius: 50px; + cursor: pointer; + font-family: inherit; + font-size: 13px; + margin-bottom: 20px; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories > .gettingStartedCategoriesContainer .index-list.start-container { min-height: 156px; margin-bottom: 16px; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 767c590b60024..5be86b5e630e0 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -103,7 +103,7 @@ export class TestNativeHostService implements INativeHostService { throw new Error('Method not implemented.'); } - async openAgentsWindow(): Promise { } + async openAgentsWindow(_options?: { readonly forceNewWindow?: boolean }): Promise { } async toggleFullScreen(): Promise { } async isMaximized(): Promise { return true; }