Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/vs/platform/actionWidget/browser/actionWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
getAnchor: () => anchor,
render: (container: HTMLElement) => {
visibleContext.set(true);
return this._renderWidget(container, list, actionBarActions ?? []);
return this._renderWidget(container, list, actionBarActions ?? [], user);
},
onHide: (didCancel) => {
visibleContext.reset();
Expand Down Expand Up @@ -116,7 +116,7 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
this._list.clear();
}

private _renderWidget(element: HTMLElement, list: ActionList<unknown>, actionBarActions: readonly IAction[]): IDisposable {
private _renderWidget(element: HTMLElement, list: ActionList<unknown>, actionBarActions: readonly IAction[], user: string): IDisposable {
const widget = document.createElement('div');
widget.classList.add('action-widget');
element.appendChild(widget);
Expand All @@ -136,7 +136,14 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
const menuBlock = document.createElement('div');
const block = element.appendChild(menuBlock);
block.classList.add('context-view-block');
renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));

if (user === 'workspacePicker') {
// Allow interactions with the sessions workspace tutorial callout,
// which is rendered alongside the action widget while this picker is open.
block.style.pointerEvents = 'none';
} else {
renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));
}

Comment on lines +139 to 147
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Setting block.style.pointerEvents = 'none' for the workspacePicker user disables the action widget’s global mouse-blocking overlay, which allows clicks to fall through to the underlying UI while the picker is open (potentially triggering unintended actions). Instead of disabling pointer events for the full-screen block, keep the block active and make the callout clickable by raising the callout above the context view (z-index/DOM placement), or by rendering the callout inside the same context view container above the block.

Suggested change
if (user === 'workspacePicker') {
// Allow interactions with the sessions workspace tutorial callout,
// which is rendered alongside the action widget while this picker is open.
block.style.pointerEvents = 'none';
} else {
renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));
}
renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));

Copilot uses AI. Check for mistakes.
// Invisible div to block mouse interaction with the menu
const pointerBlockDiv = document.createElement('div');
Expand Down Expand Up @@ -174,9 +181,10 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {

const focusTracker = renderDisposables.add(dom.trackFocus(element));
renderDisposables.add(focusTracker.onDidBlur(() => {
// Don't hide if focus moved to a hover or submenu that belongs to this action widget
// Don't hide if focus moved to a hover or submenu that belongs to this action widget,
// or to a companion callout widget (e.g. workspace picker tutorial callout).
const activeElement = dom.getActiveElement();
if (activeElement?.closest('.action-widget-hover') || activeElement?.closest('.action-list-submenu-panel')) {
if (activeElement?.closest('.action-widget-hover') || activeElement?.closest('.action-list-submenu-panel') || activeElement?.closest('.workspace-picker-callout')) {
return;
}
this.hide(true);
Expand Down
15 changes: 15 additions & 0 deletions src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

.chat-full-welcome.revealed {
justify-content: center;
overflow: visible;
}

/* Header */
Expand Down Expand Up @@ -76,6 +77,9 @@
margin: 0 0 24px 0;
padding: 0;
box-sizing: border-box;
overflow: visible;
position: relative;
z-index: 10;
}

.chat-full-welcome.revealed .chat-full-welcome-pickers-container {
Expand Down Expand Up @@ -113,6 +117,7 @@
align-items: center;
gap: 4px;
min-height: 28px;
overflow: visible;
}

.chat-full-welcome.revealed .chat-full-welcome-local-mode {
Expand All @@ -134,6 +139,8 @@
align-items: center;
gap: 2px;
min-width: 0;
position: relative;
overflow: visible;
}

/* Ensure the input editor fits properly */
Expand Down Expand Up @@ -171,12 +178,20 @@
width: 100%;
box-sizing: border-box;
padding: 0;
overflow: visible;
}

.chat-full-welcome-pickers:empty {
display: none;
}

/* Picker slot needs relative positioning for the callout to anchor to */
.sessions-chat-picker-slot.sessions-chat-workspace-picker,
.sessions-chat-picker-slot.sessions-chat-branch-picker {
position: relative;
overflow: visible;
}

/* Prominent project picker button */
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label {
height: auto;
Expand Down
247 changes: 247 additions & 0 deletions src/vs/sessions/contrib/chat/browser/media/workspacePickerCallout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/* Callout container */
.workspace-picker-callout {
position: absolute;
top: 0;
left: calc(100% + 10px);
width: 280px;
z-index: 100;

background-color: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);

white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;

animation: workspace-callout-fade-in 0.2s ease both;
}

/* Left-side variant (branch picker) */
.workspace-picker-callout.callout-left {
left: auto;
right: calc(100% + 10px);
top: auto;
bottom: -4px;
animation: workspace-callout-fade-in-left 0.2s ease both;
}

/* Fixed-position variant (new session button) — positioned via JS */
.workspace-picker-callout.callout-new-session {
position: fixed;
top: auto;
left: auto;
animation: workspace-callout-fade-in 0.2s ease both;
}

.workspace-picker-callout.callout-new-session.hiding {
animation: workspace-callout-fade-out 0.15s ease both;
}

.workspace-picker-callout.callout-left.hiding {
animation: workspace-callout-fade-out-left 0.15s ease both;
}

.workspace-picker-callout.hiding {
animation: workspace-callout-fade-out 0.15s ease both;
}

@keyframes workspace-callout-fade-in {
from {
opacity: 0;
transform: translateX(4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}

@keyframes workspace-callout-fade-out {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(4px);
}
}

@keyframes workspace-callout-fade-in-left {
from {
opacity: 0;
transform: translateX(-4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}

@keyframes workspace-callout-fade-out-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-4px);
}
}

/* Pointer arrow (left-pointing, for right-side callout) */
.workspace-picker-callout-pointer {
position: absolute;
left: -6px;
top: 24px;
transform: rotate(45deg);
width: 10px;
height: 10px;
background-color: var(--vscode-editorWidget-background);
border-bottom: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
border-left: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
}

/* Pointer arrow (right-pointing, for left-side callout) */
.workspace-picker-callout.callout-left .workspace-picker-callout-pointer {
left: auto;
right: -6px;
top: auto;
bottom: 12px;
border-bottom: none;
border-left: none;
border-top: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
border-right: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
}

/* Header bar (dismiss + snooze) — floating pill, hidden until callout is hovered */
.workspace-picker-callout-header {
position: absolute;
top: -20px;
right: 10px;
display: flex;
align-items: center;
gap: 2px;
height: 26px;
line-height: 26px;
background-color: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
border-radius: 6px;
opacity: 0;
pointer-events: none;
transition: opacity 0.1s ease-in-out;
}

.workspace-picker-callout:hover .workspace-picker-callout-header,
.workspace-picker-callout:focus-within .workspace-picker-callout-header {
opacity: 1;
pointer-events: auto;
}

.workspace-picker-callout-header button {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 1px 2px;
border: none;
border-radius: 4px;
background-color: transparent;
color: var(--vscode-descriptionForeground);
cursor: pointer;
padding: 4px;
}

.workspace-picker-callout-header button:first-child {
margin-left: 1px;
}

.workspace-picker-callout-header button:last-child {
margin-right: 1px;
}

.workspace-picker-callout-header button:hover {
background-color: var(--vscode-toolbar-hoverBackground);
color: var(--vscode-foreground);
}

.workspace-picker-callout-header button .codicon {
font-size: 14px;
}

/* Body */
.workspace-picker-callout-body {
padding: 10px 14px 12px 14px;
}

/* Description */
.workspace-picker-callout-description {
font-size: 13px;
line-height: 1.45;
color: var(--vscode-foreground);
}

/* FAQ */
.workspace-picker-callout-faq {
margin-top: 12px;
border-top: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
padding-top: 8px;
}

.workspace-picker-callout-faq-item {
margin: 0;
}

.workspace-picker-callout-faq-item + .workspace-picker-callout-faq-item {
margin-top: 2px;
}

.workspace-picker-callout-faq-question {
font-size: 12px;
font-weight: 600;
color: var(--vscode-foreground);
cursor: pointer;
list-style: none;
padding: 4px 4px;
border-radius: 4px;
user-select: none;
-webkit-user-select: none;
}

.workspace-picker-callout-faq-question::-webkit-details-marker {
display: none;
}

.workspace-picker-callout-faq-question::before {
font-family: codicon;
content: '\eab6'; /* chevron-right */
display: inline-block;
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-right: 4px;
vertical-align: middle;
transition: transform 0.15s ease;
}

details[open].workspace-picker-callout-faq-item > .workspace-picker-callout-faq-question::before {
transform: rotate(90deg);
}

.workspace-picker-callout-faq-question:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}

.workspace-picker-callout-faq-answer {
font-size: 12px;
line-height: 1.5;
color: var(--vscode-descriptionForeground);
padding: 4px 4px 8px 16px;
}
18 changes: 17 additions & 1 deletion src/vs/sessions/contrib/chat/browser/newChatViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor
import { NewChatContextAttachments } from './newChatContextAttachments.js';
import { SessionTypePicker } from './sessionTypePicker.js';
import { WorkspacePicker, IWorkspaceSelection } from './sessionWorkspacePicker.js';
import { WorkspacePickerCallout } from './workspacePickerCallout.js';
import { Menus } from '../../../browser/menus.js';
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { SlashCommandHandler } from './slashCommands.js';
Expand Down Expand Up @@ -77,6 +78,7 @@ interface IDraftState {
class NewChatWidget extends Disposable implements IHistoryNavigationWidget {

private readonly _workspacePicker: WorkspacePicker;
private readonly _workspacePickerCallout: WorkspacePickerCallout;
private readonly _sessionTypePicker: SessionTypePicker;

// IHistoryNavigationWidget
Expand Down Expand Up @@ -135,6 +137,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments));
this._workspacePicker = this._register(this.instantiationService.createInstance(WorkspacePicker));
this._workspacePickerCallout = this._register(this.instantiationService.createInstance(WorkspacePickerCallout));
this._sessionTypePicker = this._register(this.instantiationService.createInstance(SessionTypePicker));

// When a workspace is selected, create a new session
Expand Down Expand Up @@ -440,7 +443,20 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers'));

// Project picker (unified folder + repo picker)
this._workspacePicker.render(pickersRow);
const pickerSlot = this._workspacePicker.render(pickersRow);

// First-time-use callout (shown when dropdown opens, hidden when it closes)
if (this._workspacePickerCallout.shouldShow) {
this._workspacePickerCallout.render(pickerSlot);
this._register(this._workspacePicker.onDidShowPicker(() => {
if (this._workspacePickerCallout.shouldShow) {
this._workspacePickerCallout.show();
}
}));
this._register(this._workspacePicker.onDidHidePicker(() => {
this._workspacePickerCallout.hide();
}));
}
}

// --- Input History (IHistoryNavigationWidget) ---
Expand Down
Loading
Loading