sessions: refine new chat empty state layout and picker styling (#304529)

* Update chat view layout to bottom-dock controls and adjust styling for improved user experience

* Update chat picker styles for improved readability and consistency

* Reduce font sizes for chat picker labels to enhance visual consistency

* Reduce font sizes for chat picker labels to improve visual consistency

* Remove unused header and letterpress elements from chat welcome widget

* Center align chat controls and local mode picker for improved layout consistency

* Center chat layout and adjust alignment for improved consistency

* Enhance workspace picker accessibility with ARIA attributes and improve dropdown interaction

* Refactor project picker styles for improved layout and consistency

* Remove left margin from workspace picker dropdown label for improved alignment

* Simplify dropdown chevron transform for workspace picker

* Update new-chat empty state in sessions: remove watermark, center controls, and restyle workspace picker

* Refactor chat welcome widget: rename bottom-docked controls to welcome content and update related DOM appends

* Update src/vs/sessions/contrib/chat/browser/workspacePicker.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* style: update chat picker styles and add icon rendering for workspace selection

* fix: update workspace selection labels for clarity

* fix: enhance workspace selection prompt for clarity and render option group pickers on selection

* fix: add onDidChangeSelection event to workspace picker for improved selection handling

* fix: trigger selection change event when deselecting workspace in picker

* fix: hide action widget when clearing workspace selection

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Hawk Ticehurst
2026-03-27 12:35:51 -04:00
committed by GitHub
parent 9c5d73322c
commit e727b78694
5 changed files with 83 additions and 73 deletions

View File

@@ -645,6 +645,7 @@ interface IPartVisibilityState {
|------|--------|
| 2026-03-26 | Updated the sessions sidebar appear animation so only the body content (`.part.sidebar > .content`) slides/fades in during reveal while the sidebar title/header and footer remain fixed |
| 2026-03-25 | Updated Sessions view documentation to reflect the refactored `SessionsView` implementation in `contrib/sessions/browser/views/sessionsView.ts` and documented the left-aligned "+ Session" sidebar action with its inline keybinding hint |
| 2026-03-24 | Updated the sessions new-chat empty state: removed the watermark, vertically centered the empty-state controls block, restyled the workspace picker as an inline `New session in {dropdown}` title row aligned to the chat input, and tuned empty-state dropdown icon/chevron and local-mode spacing for the final visual polish. |
| 2026-03-02 | Fixed macOS sidebar traffic light spacer to only render with custom titlebar; added `!hasNativeTitlebar()` guard to `SidebarPart.createTitleArea()` so the 70px spacer is not created when using native titlebar (traffic lights are in the OS title bar, not overlapping the sidebar) |
| 2026-02-20 | Replaced custom `EditorModal` with standard `ModalEditorPart` via `MODAL_GROUP`; main editor part created but hidden; changed `workbench.editor.useModal` from boolean to enum (`off`/`some`/`all`); sessions config uses `all`; removed `editorModal.ts` and editor modal CSS |
| 2026-02-17 | Added `-webkit-app-region: drag` to sidebar title area so it can be used to drag the window; interactive children (actions, composite bar, labels) marked `no-drag`; CSS rules scoped to `.agent-sessions-workbench` in `parts/media/sidebarPart.css` |

View File

@@ -12,48 +12,15 @@
height: 100%;
box-sizing: border-box;
overflow: hidden;
padding: 0 10px 48px 10px;
padding: 16px 16px 20px 16px;
container-type: size;
position: relative;
}
.chat-full-welcome.revealed {
justify-content: center;
}
/* Header */
.chat-full-welcome-header {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 800px;
overflow: visible;
}
/* Watermark letterpress */
.chat-full-welcome-letterpress {
width: 100%;
max-width: 200px;
aspect-ratio: 1/1;
background-image: url('./letterpress-sessions-dark.svg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-top: 8px;
margin-bottom: 20px;
}
.vs .chat-full-welcome-letterpress,
.hc-light .chat-full-welcome-letterpress {
background-image: url('./letterpress-sessions-light.svg');
}
@container (max-height: 350px) {
.chat-full-welcome-letterpress {
display: none;
}
}
/* Input slot */
.chat-full-welcome-inputSlot {
width: 100%;
@@ -73,7 +40,7 @@
display: none;
width: 100%;
max-width: 800px;
margin: 0 0 24px 0;
margin: 0 0 12px 0;
padding: 0;
box-sizing: border-box;
}
@@ -100,13 +67,19 @@
margin-bottom: 0;
}
.chat-full-welcome-content {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
align-items: stretch;
}
/* Local mode picker (Workspace / Worktree) below input */
.chat-full-welcome-local-mode {
width: 100%;
max-width: 800px;
margin-top: 8px;
padding-left: 7px;
padding-right: 7px;
box-sizing: border-box;
display: none;
flex-direction: row;
@@ -167,7 +140,8 @@
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
justify-content: flex-start;
gap: 6px;
width: 100%;
box-sizing: border-box;
padding: 0;
@@ -177,34 +151,46 @@
display: none;
}
/* Prominent project picker button */
.chat-full-welcome-pickers-label {
font-size: 18px;
line-height: 1.25;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
}
/* Project picker in inline title row */
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label {
height: auto;
padding: 8px 20px;
font-size: 15px;
border: 1px solid var(--vscode-button-border);
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border-radius: 6px;
padding: 4px;
font-size: 18px;
line-height: 1.25;
border: none;
background-color: transparent;
color: var(--vscode-foreground);
border-radius: 4px;
}
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .codicon {
font-size: 18px;
margin-right: 2px;
}
.sessions-chat-picker-slot.sessions-chat-project-picker .action-label .codicon-chevron-down {
margin-right: 0;
position: relative;
top: 1px;
background-color: var(--vscode-toolbar-hoverBackground);
color: var(--vscode-foreground);
}
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-label {
font-size: 15px;
font-size: 18px;
}
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label > .codicon:not(.sessions-chat-dropdown-chevron) {
font-size: 16px;
}
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-left: 6px;
line-height: 1;
transform: translateY(1px);
}
.sessions-chat-dropdown-label {
@@ -290,8 +276,13 @@
}
.sessions-chat-picker-slot .action-label .codicon-chevron-down {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
margin-left: 6px;
line-height: 1;
transform: translateY(1px);
}
.sessions-chat-picker-slot .action-label .chat-session-option-label {

View File

@@ -138,6 +138,9 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._sessionTypePicker = this._register(this.instantiationService.createInstance(SessionTypePicker));
// When a workspace is selected, create a new session
this._register(this._workspacePicker.onDidChangeSelection(() => {
this._renderOptionGroupPickers();
}));
this._register(this._workspacePicker.onDidSelectWorkspace(async (workspace) => {
await this._onWorkspaceSelected(workspace);
this._focusEditor();
@@ -168,15 +171,14 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
const welcomeElement = dom.append(wrapper, dom.$('.chat-full-welcome'));
// Watermark letterpress
const header = dom.append(welcomeElement, dom.$('.chat-full-welcome-header'));
dom.append(header, dom.$('.chat-full-welcome-letterpress'));
// Main empty-state content area (folder picker, input, local mode controls)
const welcomeContent = dom.append(welcomeElement, dom.$('.chat-full-welcome-content'));
// Option group pickers (above the input)
this._pickersContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-pickers-container'));
this._pickersContainer = dom.append(welcomeContent, dom.$('.chat-full-welcome-pickers-container'));
// Input slot
this._inputSlot = dom.append(welcomeElement, dom.$('.chat-full-welcome-inputSlot'));
this._inputSlot = dom.append(welcomeContent, dom.$('.chat-full-welcome-inputSlot'));
// Input area inside the input slot
const inputArea = dom.$('.sessions-chat-input-area');
@@ -193,7 +195,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._inputSlot.appendChild(inputArea);
// Below-input row: session type picker, permission control, spacer, repository config (right)
const belowInputRow = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode'));
const belowInputRow = dom.append(welcomeContent, dom.$('.chat-full-welcome-local-mode'));
this._sessionTypePicker.render(belowInputRow);
const controlContainer = dom.append(belowInputRow, dom.$('.sessions-chat-control-toolbar'));
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, controlContainer, Menus.NewSessionControl, {
@@ -438,6 +440,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
dom.clearNode(this._pickersContainer);
const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers'));
const pickersLabel = dom.append(pickersRow, dom.$('.chat-full-welcome-pickers-label'));
pickersLabel.textContent = this._workspacePicker.selectedProject
? localize('newSessionIn', "New session in")
: localize('newSessionChooseWorkspace', "Start by picking");
// Project picker (unified folder + repo picker)
this._workspacePicker.render(pickersRow);

View File

@@ -64,6 +64,8 @@ export class WorkspacePicker extends Disposable {
private readonly _onDidSelectWorkspace = this._register(new Emitter<IWorkspaceSelection>());
readonly onDidSelectWorkspace: Event<IWorkspaceSelection> = this._onDidSelectWorkspace.event;
private readonly _onDidChangeSelection = this._register(new Emitter<void>());
readonly onDidChangeSelection: Event<void> = this._onDidChangeSelection.event;
private _selectedWorkspace: IWorkspaceSelection | undefined;
@@ -105,6 +107,7 @@ export class WorkspacePicker extends Disposable {
if (restored) {
this._selectedWorkspace = restored;
this._updateTriggerLabel();
this._onDidChangeSelection.fire();
this._onDidSelectWorkspace.fire(restored);
}
}
@@ -124,6 +127,8 @@ export class WorkspacePicker extends Disposable {
const trigger = dom.append(slot, dom.$('a.action-label'));
trigger.tabIndex = 0;
trigger.role = 'button';
trigger.setAttribute('aria-haspopup', 'listbox');
trigger.setAttribute('aria-expanded', 'false');
this._triggerElement = trigger;
this._updateTriggerLabel();
@@ -164,10 +169,16 @@ export class WorkspacePicker extends Disposable {
this._selectProject(item.selection);
}
},
onHide: () => { triggerElement.focus(); },
onHide: () => {
triggerElement.setAttribute('aria-expanded', 'false');
triggerElement.focus();
},
};
const listOptions = showFilter ? { showFilter: true, filterPlaceholder: localize('workspacePicker.filter', "Search Workspaces..."), reserveSubmenuSpace: false } : { reserveSubmenuSpace: false };
const listOptions = showFilter
? { showFilter: true, filterPlaceholder: localize('workspacePicker.filter', "Search Workspaces..."), reserveSubmenuSpace: false }
: { reserveSubmenuSpace: false };
triggerElement.setAttribute('aria-expanded', 'true');
this.actionWidgetService.show<IWorkspacePickerItem>(
'workspacePicker',
@@ -197,12 +208,14 @@ export class WorkspacePicker extends Disposable {
* Clears the selected project.
*/
clearSelection(): void {
this.actionWidgetService.hide();
this._selectedWorkspace = undefined;
// Clear checked state from all recents
const recents = this._getStoredRecentWorkspaces();
const updated = recents.map(p => ({ ...p, checked: false }));
this.storageService.store(STORAGE_KEY_RECENT_WORKSPACES, JSON.stringify(updated), StorageScope.PROFILE, StorageTarget.MACHINE);
this._updateTriggerLabel();
this._onDidChangeSelection.fire();
}
/**
@@ -218,6 +231,7 @@ export class WorkspacePicker extends Disposable {
this._selectedWorkspace = selection;
this._persistSelectedWorkspace(selection);
this._updateTriggerLabel();
this._onDidChangeSelection.fire();
if (fireEvent) {
this._onDidSelectWorkspace.fire(selection);
}
@@ -367,13 +381,13 @@ export class WorkspacePicker extends Disposable {
dom.clearNode(this._triggerElement);
const workspace = this._selectedWorkspace?.workspace;
const label = workspace ? workspace.label : localize('pickWorkspace', "Pick a Workspace");
const label = workspace ? workspace.label : localize('pickWorkspace', "a workspace");
const icon = workspace ? workspace.icon : Codicon.project;
dom.append(this._triggerElement, renderIcon(icon));
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
labelSpan.textContent = label;
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown)).classList.add('sessions-chat-dropdown-chevron');
}
private _isSelectedWorkspace(selection: IWorkspaceSelection): boolean {
@@ -521,8 +535,10 @@ export class WorkspacePicker extends Disposable {
// Clear current selection if it was the removed workspace
if (this._isSelectedWorkspace(selection)) {
this.actionWidgetService.hide();
this._selectedWorkspace = undefined;
this._updateTriggerLabel();
this._onDidChangeSelection.fire();
}
}

View File

@@ -1,4 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/