Skip to content
87 changes: 62 additions & 25 deletions src/vs/sessions/browser/parts/chatCompositeBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import { ISessionsProvidersService } from '../../services/sessions/browser/sessi
import { isAgentHostProvider } from '../../common/agentHostSessionsProvider.js';
import { ICommandService } from '../../../platform/commands/common/commands.js';
import { CLOSE_CHAT_COMMAND_ID } from '../../common/sessionCommands.js';
import { MenuItemAction } from '../../../platform/actions/common/actions.js';
import { ChatPillActionViewItem } from '../../../workbench/browser/chatPills.js';
import { SessionActivatingActionRunner } from '../sessionActionRunner.js';
import { ISessionsService } from '../../services/sessions/browser/sessionsService.js';

interface IChatTab {
readonly chat: IChat;
Expand All @@ -57,7 +61,7 @@ export interface IChatCompositeBarDelegate {
/**
* The session whose chats are partitioned across groups. The bar reads it for
* the contributed tab menus (whose actions act on `{ session, chat }`), chat
* capabilities, rename/delete, and the trailing "New Chat" gating.
* drag data, and rename/delete operations.
*/
readonly session: IActiveSession;

Expand All @@ -73,6 +77,9 @@ export interface IChatCompositeBarDelegate {
/** Whether the tab strip should be shown. */
readonly visible: IObservable<boolean>;

/** Whether this single group's tab row replaces the session header and shows its actions. */
readonly showSessionActions: IObservable<boolean>;

/** Activate (show + focus) the given chat within this group. */
openChat(resource: URI): void;

Expand Down Expand Up @@ -100,15 +107,20 @@ export class ChatCompositeBar extends Disposable {
private readonly _tabsRow: HTMLElement;
private readonly _tabsContainer: HTMLElement;
private readonly _tabsScrollbar: ScrollableElement;
private readonly _newChatAction: Action;
private readonly _newChatContainer: HTMLElement;
private readonly _sessionActionsContainer: HTMLElement;
private readonly _sessionToolbar: MenuWorkbenchToolBar;
private readonly _metaRow: HTMLElement;
private readonly _metaToolbar: MenuWorkbenchToolBar;
private readonly _tabs: IChatTab[] = [];
private readonly _tabDisposables = this._register(new DisposableStore());

private readonly _groupDisposables = this._register(new MutableDisposable<DisposableStore>());
private readonly _editingDisposables = this._register(new MutableDisposable<DisposableStore>());
private _editingTab: IChatTab | undefined;
private _delegate: IChatCompositeBarDelegate | undefined;
private readonly _newChatAction: Action;
private readonly _newChatContainer: HTMLElement;
private _showSessionActions = false;

private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
Expand Down Expand Up @@ -139,6 +151,7 @@ export class ChatCompositeBar extends Disposable {
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
@ICommandService private readonly _commandService: ICommandService,
@ISessionsService sessionsService: ISessionsService,
) {
super();

Expand All @@ -159,6 +172,43 @@ export class ChatCompositeBar extends Disposable {
}));
this._tabsRow.appendChild(this._tabsScrollbar.getDomNode());

this._newChatAction = this._register(new Action(
'sessions.chatCompositeBar.addChat',
localize('chatCompositeBar.addChat', "New Chat in This Session"),
ThemeIcon.asClassName(Codicon.add),
true,
async () => this._delegate?.newChat(),
));
const newChatActionBar = this._register(new ActionBar(this._tabsRow));
newChatActionBar.push(this._newChatAction, { icon: true, label: false });
this._newChatContainer = newChatActionBar.getContainer();
this._newChatContainer.classList.add('chat-composite-bar-new-chat');

this._sessionActionsContainer = $('.session-chat-tabs-actions');
this._tabsRow.appendChild(this._sessionActionsContainer);
const sessionToolbarContainer = $('.chat-composite-bar-toolbar');
this._sessionActionsContainer.appendChild(sessionToolbarContainer);
this._sessionToolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, sessionToolbarContainer, Menus.SessionBarToolbar, {
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { shouldForwardArgs: true },
highlightToggledItems: true,
}));

this._metaRow = $('.chat-composite-bar-meta-row');
this._container.appendChild(this._metaRow);
const metaToolbarContainer = $('.chat-composite-bar-meta-toolbar');
this._metaRow.appendChild(metaToolbarContainer);
const metaActionRunner = this._register(new SessionActivatingActionRunner(() => this._delegate?.session, sessionsService));
this._metaToolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, metaToolbarContainer, Menus.SessionHeaderMeta, {
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { shouldForwardArgs: true },
actionRunner: metaActionRunner,
actionViewItemProvider: (action, options) => action instanceof MenuItemAction
? this._instantiationService.createInstance(ChatPillActionViewItem, undefined, action, options)
: undefined,
}));
this._register(this._metaToolbar.onDidChangeMenuItems(() => this._updateMetaRowVisibility()));

const preventMiddleButtonDefault = (e: MouseEvent) => {
if (e.button === 1 && !this._isInTabInput(e)) {
e.preventDefault();
Expand All @@ -170,21 +220,6 @@ export class ChatCompositeBar extends Disposable {
this._register(addDisposableGenericMouseUpListener(this._tabsContainer, preventMiddleButtonDefault));
}

// "New Chat" button pinned at the end of the tab strip. Starting a new chat
// is offered here while the tabs are shown; when the session has a single
// chat the session header toolbar offers it instead.
const newChatAction = this._newChatAction = this._register(new Action(
'chatCompositeBar.addChat',
localize('chatCompositeBar.addChat', "New Chat"),
ThemeIcon.asClassName(Codicon.add),
true,
async () => this._delegate?.newChat(),
));
const newChatActionBar = this._register(new ActionBar(this._tabsRow, { actionViewItemProvider: undefined }));
newChatActionBar.push(newChatAction, { icon: true, label: false });
this._newChatContainer = newChatActionBar.getContainer();
this._newChatContainer.classList.add('chat-composite-bar-new-chat');

// Keep the visual scrollbar in sync with native scrolling inside the tabs container
this._register(addDisposableListener(this._tabsContainer, EventType.SCROLL, () => {
this._tabsScrollbar.setScrollPosition({ scrollLeft: this._tabsContainer.scrollLeft });
Expand Down Expand Up @@ -225,6 +260,8 @@ export class ChatCompositeBar extends Disposable {
}

this._delegate = delegate;
this._sessionToolbar.context = delegate?.session;
this._metaToolbar.context = delegate?.session;

const store = new DisposableStore();
this._groupDisposables.value = store;
Expand All @@ -242,21 +279,21 @@ export class ChatCompositeBar extends Disposable {
const activeChatUri = delegate.activeChatResource.read(reader);
const mainChatUri = delegate.mainChatResource.read(reader);
this._rebuildTabs(chats, activeChatUri, mainChatUri);

// The trailing "New Chat" action only applies to sessions that support
// user-created peer chats. Subagent (read-only) tabs can surface in
// sessions without that capability, so gate the action on the
// capability rather than on tab-strip visibility.
const supportsMultipleChats = delegate.session.capabilities.read(reader).supportsMultipleChats;
this._newChatContainer.classList.toggle('hidden', !supportsMultipleChats);
// Archived sessions are read-only, so disable the trailing New Chat
// action (mirrors the header action's SessionIsArchivedContext gating).
this._newChatAction.enabled = supportsMultipleChats && !delegate.session.isArchived.read(reader);
this._showSessionActions = delegate.showSessionActions.read(reader);
this._sessionActionsContainer.classList.toggle('hidden', !this._showSessionActions);
this._updateMetaRowVisibility();

this._setVisible(delegate.visible.read(reader));
}));
}

private _updateMetaRowVisibility(): void {
this._metaRow.style.display = this._showSessionActions && !this._metaToolbar.isEmpty() ? '' : 'none';
}

setAriaLabel(label: string): void {
this._tabsContainer.setAttribute('aria-label', label);
}
Expand Down
4 changes: 4 additions & 0 deletions src/vs/sessions/browser/parts/chatGroupView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export interface IChatGroupContext {
/** Whether the group's tab strip should be shown. */
readonly tabsVisible: IObservable<boolean>;

/** Whether this group's tab row replaces the session header and shows its actions. */
readonly showSessionActions: IObservable<boolean>;

/** Activate (show + focus) the given chat within this group. */
openChat(resource: URI): void;

Expand Down Expand Up @@ -170,6 +173,7 @@ export class ChatGroupView extends Disposable implements ISerializableView {
activeChatResource: context.activeChatResource,
mainChatResource: context.mainChatResource,
visible: context.tabsVisible,
showSessionActions: context.showSessionActions,
openChat: resource => context.openChat(resource),
newChat: () => context.newChat(),
onTabDragStart: resource => context.onTabDragStart(resource),
Expand Down
2 changes: 2 additions & 0 deletions src/vs/sessions/browser/parts/chatGroupsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export class ChatGroupsView extends Themable {
}
return session.shouldShowChatTabs.read(reader);
});
const showSessionActions = derived(reader => this._groupCount.read(reader) === 1 && tabsVisible.read(reader));

const view = store.add(this._instantiationService.createInstance(ChatGroupView));
const entry: IGroupEntry = { id, view, resourceIds, activeResourceId, chats, tabsVisible };
Expand All @@ -295,6 +296,7 @@ export class ChatGroupsView extends Themable {
activeChatResource: activeResourceId,
mainChatResource: this._mainChatResource!,
tabsVisible,
showSessionActions,
openChat: resource => this._openChat(entry, resource),
newChat: () => this._newChat(entry).catch(onUnexpectedError),
onTabDragStart: () => { },
Expand Down
113 changes: 40 additions & 73 deletions src/vs/sessions/browser/parts/media/chatCompositeBar.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,15 @@
overflow: hidden;
}

/* Header host: title row + meta row, with the top padding for the whole bar area */
/* Header host: title row + meta row. */
.chat-composite-bar.session-header-bar {
padding: 6px 10px 0;
padding: 0 var(--vscode-spacing-size100);
box-sizing: border-box;
}

/* Tabs host: the chat tab strip, shown only when the session has multiple chats.
It lives in the same centered session-view content host as the header.
Symmetric 10px gutter on both sides, matching the small, even inset editor
tabs use — the tab strip no longer tries to align its first tab under the
header's status-icon column, since that produced a much larger left gutter
than the shared modern-tab convention. */
/* Tabs host: the chat tab strip, shown only when the session has multiple chats. */
.chat-composite-bar.session-chat-tabs-bar {
padding: 0 10px;
padding: 0 var(--vscode-spacing-size100);
box-sizing: border-box;
container-type: inline-size;

Expand All @@ -41,8 +36,7 @@
flex-direction: row;
align-items: flex-start;
gap: 6px;
padding-bottom: 6px;
border-bottom: 1px solid color-mix(in srgb, var(--session-view-foreground) 12%, transparent);
border-bottom: var(--vscode-strokeThickness) solid color-mix(in srgb, var(--session-view-foreground, var(--chat-tab-active-foreground)) 12%, transparent);
}

/* Main column stacks the title row and the meta row */
Expand All @@ -59,7 +53,7 @@
display: flex;
align-items: center;
gap: 6px;
height: 26px;
height: 34px;
}

/* Status icon column — sits beside the main column, centered on the title line.
Expand All @@ -70,7 +64,7 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
height: 26px;
height: 34px;
font-size: var(--vscode-codiconFontSize, 16px);
color: var(--session-view-foreground);
}
Expand All @@ -81,7 +75,7 @@
overflow: hidden;
display: flex;
align-items: center;
font-weight: var(--vscode-agents-fontWeight-semiBold, 600);
font-weight: var(--vscode-agents-fontWeight-regular, 400);
font-size: var(--vscode-agents-fontSize-heading3, 13px);
color: var(--chat-tab-active-foreground, var(--session-view-foreground));
border-radius: var(--vscode-cornerRadius-small);
Expand All @@ -100,45 +94,6 @@
white-space: nowrap;
}

.chat-composite-bar-workspace-meta {
display: inline-flex;
align-items: center;
gap: var(--vscode-spacing-size40);
flex: 0 1 auto;
min-width: 0;
max-width: 40%;
color: var(--vscode-descriptionForeground);
font-size: var(--vscode-agents-fontSize-label1);
font-weight: var(--vscode-agents-fontWeight-regular);
white-space: nowrap;
}

.chat-composite-bar-workspace-meta.hidden {
display: none;
}

/* Compact glyph at the compact size. The compound selector outranks the base
`.codicon` font shorthand; the clamped box keeps combined glyphs (wider
advance) tight against the label, and the padding optically centers it. */
.monaco-workbench .chat-composite-bar-workspace-meta-icon.codicon[class*='codicon-'] {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--vscode-codiconFontSize-compact);
height: var(--vscode-codiconFontSize-compact);
margin: 0;
padding: 3px 1px 0 2px;
font-size: var(--vscode-codiconFontSize-compact);
flex-shrink: 0;
}

.chat-composite-bar-workspace-meta-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* Hover feedback: only when the title can actually be renamed and we aren't
currently editing it. */
.chat-composite-bar-session-title.editable {
Expand Down Expand Up @@ -227,45 +182,38 @@
display: flex;
align-items: center;
height: 35px;
box-sizing: border-box;
overflow: hidden;
}

.chat-groups-view.single-group .chat-composite-bar-tabs-row {
border-bottom: var(--vscode-strokeThickness) solid color-mix(in srgb, var(--session-view-foreground, var(--chat-tab-active-foreground)) 12%, transparent);
}
Comment thread
sandy081 marked this conversation as resolved.

/* The ScrollableElement wrapper holding the tabs is the shrinkable flex item */
.chat-composite-bar-tabs-row > .monaco-scrollable-element {
flex: 0 1 auto;
min-width: 0;
height: 100%;
}

.chat-composite-bar-tabs {
.chat-composite-bar-new-chat {
display: flex;
align-items: center;
height: 100%;
min-height: calc(var(--vscode-spacing-size240) + var(--vscode-spacing-size40) * 2);
}

/* "New Chat" button pinned at the end of the tab strip, after the Conversations menu. */
.chat-composite-bar-tabs-row > .chat-composite-bar-new-chat {
flex-shrink: 0;
display: flex;
align-items: center;
margin-left: 4px;
}

.chat-composite-bar-tabs-row > .chat-composite-bar-new-chat.hidden {
.chat-composite-bar-new-chat.hidden {
display: none;
}

/* Include the tab-row owner to outrank `.monaco-action-bar .action-item .codicon`,
* which otherwise resets this button's width and height from 26px to 16px. */
.chat-composite-bar-tabs-row > .chat-composite-bar-new-chat .action-item .action-label {
box-sizing: border-box;
width: 26px;
height: 26px;
padding: 0;
.chat-composite-bar-new-chat .action-item .action-label {
display: flex;
align-items: center;
justify-content: center;
width: var(--editor-group-tab-height, var(--vscode-spacing-size240));
height: var(--editor-group-tab-height, var(--vscode-spacing-size240));
padding: 0;
border-radius: var(--vscode-cornerRadius-small);
color: var(--chat-tab-inactive-foreground, currentColor);
}
Expand All @@ -276,8 +224,27 @@
}

.chat-composite-bar-new-chat .action-item .action-label:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder);
outline-offset: calc(-1 * var(--vscode-strokeThickness));
}

.session-chat-tabs-actions {
display: flex;
align-items: center;
margin-left: auto;
padding-right: var(--vscode-spacing-size40);
flex-shrink: 0;
}

.session-chat-tabs-actions.hidden {
display: none;
}

.chat-composite-bar-tabs {
display: flex;
align-items: center;
height: 100%;
min-height: calc(var(--vscode-spacing-size240) + var(--vscode-spacing-size40) * 2);
}

.chat-composite-bar-toolbar {
Expand Down
Loading
Loading