diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 8660a9a084393..bd4eec5706288 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -31,6 +31,7 @@ import { ICommandActionTitle } from 'vs/platform/action/common/action'; import { mainWindow } from 'vs/base/browser/window'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { TitlebarStyle } from 'vs/platform/window/common/window'; +import { TogglePearOverlayAction } from 'vs/workbench/browser/parts/pearai/pearOverlayActions'; // Register Icons const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); @@ -1341,6 +1342,7 @@ ToggleVisibilityActions.push(...[ CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }), CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon), CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon), + CreateToggleLayoutItem(TogglePearOverlayAction.ID, PearAIVisibleContext, 'PearAI', pearaiIcon) ]); const MoveSideBarActions: CustomizeLayoutItem[] = [ diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 4e2809e149c2a..31d5e942b9984 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -264,6 +264,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private auxiliaryBarPartView!: ISerializableView; private editorPartView!: ISerializableView; private statusBarPartView!: ISerializableView; + private pearOverlayPartView!: ISerializableView; private environmentService!: IBrowserWorkbenchEnvironmentService; private extensionService!: IExtensionService; @@ -1483,6 +1484,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const auxiliaryBarPart = this.getPart(Parts.AUXILIARYBAR_PART); const sideBar = this.getPart(Parts.SIDEBAR_PART); const statusBar = this.getPart(Parts.STATUSBAR_PART); + const pearOverlayPart = this.getPart(Parts.PEAROVERLAY_PART); // View references for all parts this.titleBarPartView = titleBar; @@ -1493,6 +1495,21 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.panelPartView = panelPart; this.auxiliaryBarPartView = auxiliaryBarPart; this.statusBarPartView = statusBar; + this.pearOverlayPartView = pearOverlayPart; + + // Create a new container for PearOverlayPart + const pearOverlayPartContainer = document.createElement("div"); + pearOverlayPartContainer.style.position = "absolute"; + pearOverlayPartContainer.style.top = "0"; + pearOverlayPartContainer.style.left = "0"; + pearOverlayPartContainer.style.right = "0"; + pearOverlayPartContainer.style.bottom = "0"; + pearOverlayPartContainer.style.zIndex = "-10"; + pearOverlayPartContainer.style.display = "absolute"; + pearOverlayPartContainer.classList.add("pearoverlay-part-container"); + this.mainContainer.appendChild(pearOverlayPartContainer); + // Initialize PearOverlayPart in this new container + pearOverlayPart.create(pearOverlayPartContainer); const viewMap = { [Parts.ACTIVITYBAR_PART]: this.activityBarPartView, @@ -1502,7 +1519,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi [Parts.PANEL_PART]: this.panelPartView, [Parts.SIDEBAR_PART]: this.sideBarPartView, [Parts.STATUSBAR_PART]: this.statusBarPartView, - [Parts.AUXILIARYBAR_PART]: this.auxiliaryBarPartView + [Parts.AUXILIARYBAR_PART]: this.auxiliaryBarPartView, + [Parts.PEAROVERLAY_PART]: this.pearOverlayPartView, }; const fromJSON = ({ type }: { type: Parts }) => viewMap[type]; @@ -1571,6 +1589,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Layout the grid widget this.workbenchGrid.layout(this._mainContainerDimension.width, this._mainContainerDimension.height); + this.pearOverlayPartView.layout(this._mainContainerDimension.width, this._mainContainerDimension.height, 0, 0); this.initialized = true; // Emit as event diff --git a/src/vs/workbench/browser/parts/pearai/pearOverlayActions.ts b/src/vs/workbench/browser/parts/pearai/pearOverlayActions.ts new file mode 100644 index 0000000000000..13aaa3823311c --- /dev/null +++ b/src/vs/workbench/browser/parts/pearai/pearOverlayActions.ts @@ -0,0 +1,50 @@ +import { registerAction2, Action2 } from "vs/platform/actions/common/actions"; +import { ServicesAccessor } from "vs/platform/instantiation/common/instantiation"; +import { IPearOverlayService } from "./pearOverlayService"; +import { KeyCode, KeyMod } from "vs/base/common/keyCodes"; + +export class ClosePearOverlayAction extends Action2 { + static readonly ID = "workbench.action.closePearAI"; + + constructor() { + super({ + id: ClosePearOverlayAction.ID, + title: { value: "Close PearAI Popup", original: "Close PearAI Popup" }, + f1: true, + keybinding: { + weight: 200, + primary: KeyCode.Escape, + }, + }); + } + + run(accessor: ServicesAccessor): void { + const pearaiOverlayService = accessor.get(IPearOverlayService); + pearaiOverlayService.hide(); + } +} + +export class TogglePearOverlayAction extends Action2 { + static readonly ID = "workbench.action.togglePearAI"; + + constructor() { + super({ + id: TogglePearOverlayAction.ID, + title: { value: "Toggle PearAI Popup", original: "Toggle PearAI Popup" }, + f1: true, + keybinding: { + weight: 200, + primary: KeyMod.CtrlCmd | KeyCode.KeyE, + }, + }); + } + + run(accessor: ServicesAccessor): void { + const pearaiOverlayService = accessor.get(IPearOverlayService); + console.log("TOGGLED PEARAI SERVICE 2"); + pearaiOverlayService.toggle(); + } +} + +registerAction2(TogglePearOverlayAction); +registerAction2(ClosePearOverlayAction); diff --git a/src/vs/workbench/browser/parts/pearai/pearOverlayPart.ts b/src/vs/workbench/browser/parts/pearai/pearOverlayPart.ts new file mode 100644 index 0000000000000..500aa3a1b16c5 --- /dev/null +++ b/src/vs/workbench/browser/parts/pearai/pearOverlayPart.ts @@ -0,0 +1,314 @@ +import { Part } from "vs/workbench/browser/part"; +import { + IWorkbenchLayoutService, + Parts, +} from "vs/workbench/services/layout/browser/layoutService"; +import { IThemeService } from "vs/platform/theme/common/themeService"; +import { IStorageService } from "vs/platform/storage/common/storage"; +import { $, getActiveWindow } from "vs/base/browser/dom"; +// import { CancellationTokenSource } from "vs/base/common/cancellation"; +import { IInstantiationService } from "vs/platform/instantiation/common/instantiation"; + +// imports that violate vscode source organization +import { + // IWebviewViewService, + WebviewView, +} from "vs/workbench/contrib/webviewView/browser/webviewViewService"; +import { WebviewService } from "vs/workbench/contrib/webview/browser/webviewService"; + +export class PearOverlayPart extends Part { + static readonly ID = "workbench.parts.pearoverlay"; + + //#region IView + + readonly minimumWidth: number = 300; + readonly maximumWidth: number = 800; + readonly minimumHeight: number = 200; + readonly maximumHeight: number = 600; + + //#endregion + + private fullScreenOverlay: HTMLElement | undefined; + private popupAreaOverlay: HTMLElement | undefined; + private webviewView: WebviewView | undefined; + private _webviewService: WebviewService | undefined; + + private state: "loading" | "open" | "closed" = "loading"; + + constructor( + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + // @IWebviewViewService + // private readonly _webviewViewService: IWebviewViewService, + @IInstantiationService + private readonly _instantiationService: IInstantiationService, + ) { + super( + PearOverlayPart.ID, + { hasTitle: false }, + themeService, + storageService, + layoutService, + ); + + this._webviewService = + this._instantiationService.createInstance(WebviewService); + + this.initialize(); + } + + private async initialize() { + // 1. create an IOverlayWebview + const webview = this._webviewService!.createWebviewOverlay({ + title: "PearAI", + options: { + enableFindWidget: false, + }, + contentOptions: { + allowScripts: true, + localResourceRoots: [ + // Uri.joinPath(this._pearaiUri, 'out'), + // URI.joinPath(pearaiUri, 'webview-ui/build'), + ], + }, + extension: undefined, + }); + + webview.claim(this, getActiveWindow(), undefined); + + webview.setHtml(this.getTestWebviewContent()); + + // 2. initialize this.webviewView by creating a WebviewView + this.webviewView = { + webview, + onDidChangeVisibility: () => { + return { dispose: () => {} }; + }, + onDispose: () => { + return { dispose: () => {} }; + }, + + get title(): string | undefined { + return "PearAI"; + }, + set title(value: string | undefined) {}, + + get description(): string | undefined { + return undefined; + }, + set description(value: string | undefined) {}, + + get badge() { + return undefined; + }, + set badge(badge) {}, + + dispose: () => { + // Only reset and clear the webview itself. Don't dispose of the view container + // this._activated = false; + // this._webview.clear(); + // // this._webviewDisposables.clear(); + }, + + show: (preserveFocus) => { + // this.viewService.openView(this.id, !preserveFocus); + }, + }; + + // this.webviewView = this.getTestWebviewContent(); + + // 3. ask the webviewViewService to connect our webviewView to the webviewViewProvider, i.e., HelloWorldPanel + // const source = new CancellationTokenSource(); // todo add to disposables + // await this._webviewViewService.resolve( + // "pearai.magicWebview", + // this.webviewView!, + // source.token, + // ); + + // if both content and webview are ready, end loading state and open + if (this.popupAreaOverlay && this.webviewView) { + this.webviewView?.webview.layoutWebviewOverElement(this.popupAreaOverlay); + this.open(); + } else { + // hide stuff while we load + this.webviewView!.webview.container.style.display = "none"; + } + } + + private getTestWebviewContent(): string { + return ` + + + + + + PearAI Test Webview + + + +
+

Hello from PearAI!

+

This is a test webview content.

+
+ + + `; + } + + protected override createContentArea(element: HTMLElement): HTMLElement { + // create the full screen overlay. this serves as a click target for closing pearai + this.element = element; + this.fullScreenOverlay = element; // use the pearOverlayPart root element as the fullScreenOverlay + this.fullScreenOverlay.style.zIndex = "-10"; + this.fullScreenOverlay.style.position = "absolute"; + this.fullScreenOverlay.style.top = "0"; + this.fullScreenOverlay.style.left = "0"; + this.fullScreenOverlay.style.right = "0"; + this.fullScreenOverlay.style.bottom = "0"; + this.fullScreenOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.2)"; + + // create the popup area overlay. this is just a target for webview to layout over + this.popupAreaOverlay = $("div.pearai-popup-area-overlay"); + this.popupAreaOverlay.style.position = "absolute"; // couldn't get it to work with relative for some reason + this.popupAreaOverlay.style.margin = "0px"; + this.popupAreaOverlay.style.top = "0"; + this.popupAreaOverlay.style.left = "0"; + this.popupAreaOverlay.style.right = "0"; + this.popupAreaOverlay.style.bottom = "0"; + this.element.appendChild(this.popupAreaOverlay); + + // if both content and webview are ready, end loading state and open + if (this.popupAreaOverlay && this.webviewView) { + this.webviewView?.webview.layoutWebviewOverElement(this.popupAreaOverlay); + this.open(); + } else { + // hide stuff while we load + this.fullScreenOverlay!.style.display = "none"; + } + + return this.fullScreenOverlay!; + } + + override layout( + width: number, + height: number, + top: number, + left: number, + ): void { + super.layout(width, height, top, left); + if (this.fullScreenOverlay) { + this.fullScreenOverlay!.style.width = `${width}px`; + this.fullScreenOverlay!.style.height = `${height}px`; + } + + if (this.state === "open") { + this.webviewView!.webview.layoutWebviewOverElement( + this.popupAreaOverlay!, + ); + } + } + + private open() { + this.state = "open"; + this.fullScreenOverlay!.style.zIndex = "95"; + + const container = this.webviewView!.webview.container; + container.style.display = "flex"; + container.style.boxSizing = "border-box"; + container.style.boxShadow = "0 0 20px 0 rgba(0, 0, 0, 0.5)"; + // container.style.borderRadius = '12px'; + container.style.backgroundColor = "white"; + container.style.zIndex = "1000"; + + // Add faster bounce animation + container.style.animation = + "pearaiBounceIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)"; + container.style.transformOrigin = "center"; + + // Define keyframes for faster bounce animation and fade out + const style = document.createElement("style"); + style.textContent = ` + @keyframes pearaiBounceIn { + 0% { transform: scale(0.95); opacity: 0; } + 70% { transform: scale(1.02); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } + } + @keyframes pearaiFadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; } + } + `; + document.head.appendChild(style); + + this.fullScreenOverlay?.addEventListener("click", () => { + this.close(); + }); + + this.webviewView!.webview.layoutWebviewOverElement(this.popupAreaOverlay!); + this.focus(); + } + + private close() { + this.state = "closed"; + const container = this.webviewView!.webview.container; + + // Apply fade-out animation + container.style.animation = "pearaiFadeOut 0.2s ease-out"; + + // Hide elements after animation completes + setTimeout(() => { + this.fullScreenOverlay!.style.zIndex = "-10"; + container.style.display = "none"; + }, 20); // 20ms matches the animation duration + } + + private toggleOpenClose() { + this.state === "open" ? this.close() : this.open(); + } + + focus(): void { + if (this.webviewView) { + this.webviewView.webview.focus(); + } + } + + show(): void { + if (this.state === "loading") { + console.warn("Can't open PearAI while loading"); + return; + } + + this.open(); + } + + hide(): void { + if (this.state === "loading") { + console.warn("Can't close PearAI while loading"); + return; + } + this.close(); + } + + toggle(): void { + if (this.state === "loading") { + console.warn("Can't toggle PearAI while loading"); + return; + } + this.toggleOpenClose(); + } + + toJSON(): object { + return { + type: Parts.PEAROVERLAY_PART, + }; + } +} diff --git a/src/vs/workbench/browser/parts/pearai/pearOverlayService.ts b/src/vs/workbench/browser/parts/pearai/pearOverlayService.ts new file mode 100644 index 0000000000000..ec6fa9a4ea2e8 --- /dev/null +++ b/src/vs/workbench/browser/parts/pearai/pearOverlayService.ts @@ -0,0 +1,102 @@ +import { + registerSingleton, + InstantiationType, +} from "vs/platform/instantiation/common/extensions"; +import { Disposable, IDisposable } from "vs/base/common/lifecycle"; +import { PearOverlayPart } from "./pearOverlayPart"; +import { + createDecorator, + IInstantiationService, +} from "vs/platform/instantiation/common/instantiation"; +import { IEditorService } from "vs/workbench/services/editor/common/editorService"; +import { ITerminalService } from "vs/workbench/contrib/terminal/browser/terminal"; + +export const IPearOverlayService = createDecorator( + "pearaiOverlayService", +); + +export interface IPearOverlayService extends IDisposable { + readonly _serviceBrand: undefined; + + /** + * Returns the PearOverlayPart instance. + */ + readonly pearOverlayPart: PearOverlayPart; + + /** + * Shows the PearAI popup. + */ + show(): void; + + /** + * Hides the PearAI popup. + */ + hide(): void; + + /** + * Toggles the visibility of the PearAI popup. + */ + toggle(): void; +} + +export class PearOverlayService + extends Disposable + implements IPearOverlayService +{ + declare readonly _serviceBrand: undefined; + + private readonly _pearOverlayPart: PearOverlayPart; + + constructor( + @IInstantiationService + private readonly instantiationService: IInstantiationService, + @IEditorService private readonly _editorService: IEditorService, + @ITerminalService private readonly _terminalService: ITerminalService, + ) { + super(); + this._pearOverlayPart = + this.instantiationService.createInstance(PearOverlayPart); + this.registerListeners(); + } + + private registerListeners(): void { + this._register( + this._editorService.onDidActiveEditorChange(() => { + this.hide(); + }), + ); + + this._register( + this._terminalService.onDidFocusInstance(() => { + this.hide(); + }), + ); + } + + get pearOverlayPart(): PearOverlayPart { + return this._pearOverlayPart; + } + + show(): void { + this._pearOverlayPart.show(); + } + + hide(): void { + this._pearOverlayPart.hide(); + } + + toggle(): void { + this._pearOverlayPart.toggle(); + } + + override dispose(): void { + super.dispose(); + this._pearOverlayPart.dispose(); + } +} + +registerSingleton( + IPearOverlayService, + PearOverlayService, + InstantiationType.Eager, +); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index b0688133537d3..d59aec4e033c4 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -50,6 +50,7 @@ import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilityS import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; +import { IPearOverlayService } from 'vs/workbench/browser/parts/pearai/pearOverlayService'; export interface IWorkbenchOptions { @@ -368,10 +369,18 @@ export class Workbench extends Layout { { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.willRestoreEditors() } }, { id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] }, { id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] }, - { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } + { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] }, + { id: Parts.PEAROVERLAY_PART, role: 'none', classes: [] } ]) { const partContainer = this.createPart(id, role, classes); + if (id === Parts.PEAROVERLAY_PART) { + // Ensure PearOverlayService is instantiated here + instantiationService.invokeFunction(accessor => { + accessor.get(IPearOverlayService); + }); + } + mark(`code/willCreatePart/${id}`); this.getPart(id).create(partContainer, options); mark(`code/didCreatePart/${id}`); diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 97937218bbb36..91f01fb387588 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -148,6 +148,7 @@ export const PanelPositionContext = new RawContextKey('panelPosition', ' export const PanelAlignmentContext = new RawContextKey('panelAlignment', 'center', localize('panelAlignment', "The alignment of the panel, either 'center', 'left', 'right' or 'justify'")); export const PanelVisibleContext = new RawContextKey('panelVisible', false, localize('panelVisible', "Whether the panel is visible")); export const PanelMaximizedContext = new RawContextKey('panelMaximized', false, localize('panelMaximized', "Whether the panel is maximized")); +export const PearAIVisibleContext = new RawContextKey('pearaiVisible', true, localize('pearaiVisible', "Whether the PearAI popup is visible")); //#endregion diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 3357adabc8228..df674eddfe5b0 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -26,7 +26,8 @@ export const enum Parts { PANEL_PART = 'workbench.parts.panel', AUXILIARYBAR_PART = 'workbench.parts.auxiliarybar', EDITOR_PART = 'workbench.parts.editor', - STATUSBAR_PART = 'workbench.parts.statusbar' + STATUSBAR_PART = 'workbench.parts.statusbar', + PEAROVERLAY_PART = 'workbench.parts.pearoverlay' } export const enum ZenModeSettings {