Skip to content
Closed
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 +140 to +145
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Special-casing behavior based on the magic user === 'workspacePicker' string is fragile (easy to mistype and not discoverable to other callers). Consider using a typed options argument (e.g. { allowCompanionElementSelector: ... } / { allowOutsideInteractionWith: ... }) or a shared constant/enum so this coupling is explicit and maintainable.

Copilot uses AI. Check for mistakes.
}
Comment on lines +140 to +146
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Setting pointerEvents = 'none' on the full-screen .context-view-block means clicks outside the action widget will reach (and potentially trigger) the underlying UI before/while the context view hides (ContextView hides on captured click but does not cancel the event). This can cause accidental interactions while the workspace picker is open. Please keep outside clicks blocked and instead make the tutorial callout clickable by rendering it within the context view layer (or otherwise ensuring it sits above the block) rather than disabling pointer events on 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
11 changes: 11 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 @@ -171,12 +175,19 @@
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 {
position: relative;
overflow: visible;
}

/* Prominent project picker button */
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label {
height: auto;
Expand Down
184 changes: 184 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,184 @@
/*---------------------------------------------------------------------------------------------
* 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: 100%;
left: calc(100% + 14px);
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);

animation: workspace-callout-fade-in 0.2s 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);
}
}

/* Pointer arrow (left-pointing) */
.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));
}

/* Header bar (dismiss + snooze) */
.workspace-picker-callout-header {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 2px;
padding: 6px 6px 0 6px;
}

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

.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: 4px 14px 12px 14px;
}

/* Description lines */
.workspace-picker-callout-description {
display: flex;
flex-direction: column;
gap: 8px;
}

.workspace-picker-callout-line {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.45;
color: var(--vscode-foreground);
}

.workspace-picker-callout-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 19px; /* align with first line of text */
}

.workspace-picker-callout-icon .codicon {
font-size: 14px;
color: var(--vscode-descriptionForeground);
}

/* 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 {
content: '';
display: inline-block;
width: 0;
height: 0;
border-style: solid;
border-width: 4px 0 4px 6px;
border-color: transparent transparent transparent var(--vscode-descriptionForeground);
margin-right: 6px;
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;
}
16 changes: 15 additions & 1 deletion src/vs/sessions/contrib/chat/browser/newChatViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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 @@ -440,7 +441,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