mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-08 17:54:23 +01:00
chat input: refactor and responsiveness (#332669)
* chat input: refactor and responsiveness * address comments * address comp --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
copilot-swe-agent[bot]
parent
d2eb83323f
commit
d77fa1711f
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IContextMenuProvider } from '../../contextmenu.js';
|
||||
import * as DOM from '../../dom.js';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItem, IActionViewItemProvider } from '../actionbar/actionbar.js';
|
||||
import { AnchorAlignment, IContextViewCloseAnimation } from '../contextview/contextview.js';
|
||||
import { DropdownMenuActionViewItem } from '../dropdown/dropdownActionViewItem.js';
|
||||
import { Action, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';
|
||||
@@ -30,6 +30,8 @@ export interface IToolBarResponsiveBehaviorOptions {
|
||||
readonly minItems?: number;
|
||||
readonly actionMinWidth?: number;
|
||||
readonly getActionMinWidth?: (action: IAction) => number | undefined;
|
||||
readonly allowOverflow?: boolean | (() => boolean);
|
||||
readonly getOverflowAction?: (action: IAction, getAnchor: () => HTMLElement | undefined) => IAction;
|
||||
readonly observedElement?: HTMLElement;
|
||||
readonly getAvailableWidth?: () => number;
|
||||
}
|
||||
@@ -73,6 +75,8 @@ export interface IToolBarOptions {
|
||||
* - `minItems`: The minimum number of items that should always be visible.
|
||||
* - `actionMinWidth`: The minimum width of each action item. Defaults to `ACTION_MIN_WIDTH` (24px).
|
||||
* - `getActionMinWidth`: Optional per-action minimum width override in pixels.
|
||||
* - `allowOverflow`: Whether actions may move into the overflow menu, or a callback that decides from current presentation state.
|
||||
* - `getOverflowAction`: Replaces an action only while it is rendered in the overflow menu.
|
||||
*/
|
||||
responsiveBehavior?: IToolBarResponsiveBehaviorOptions;
|
||||
}
|
||||
@@ -234,6 +238,15 @@ export class ToolBar extends Disposable {
|
||||
return this.actionBar.getWidth(index);
|
||||
}
|
||||
|
||||
getItemElement(index: number): HTMLElement | undefined {
|
||||
const element = this.actionBar.getContainer().firstElementChild?.children.item(index);
|
||||
return DOM.isHTMLElement(element) ? element : undefined;
|
||||
}
|
||||
|
||||
getItemViewItem(index: number): IActionViewItem | undefined {
|
||||
return this.actionBar.viewItems[index];
|
||||
}
|
||||
|
||||
private getUnshrunkItemWidth(index: number): number {
|
||||
const actionItem = this.actionBar.getContainer().firstElementChild?.children.item(index);
|
||||
if (!DOM.isHTMLElement(actionItem)) {
|
||||
@@ -258,6 +271,10 @@ export class ToolBar extends Disposable {
|
||||
return this.actionBar.length();
|
||||
}
|
||||
|
||||
hasOverflow(): boolean {
|
||||
return this.actionBar.hasAction(this.toggleMenuAction);
|
||||
}
|
||||
|
||||
setAriaLabel(label: string): void {
|
||||
this.actionBar.setAriaLabel(label);
|
||||
}
|
||||
@@ -416,22 +433,33 @@ export class ToolBar extends Disposable {
|
||||
// Each action is assumed to have a minimum width so that actions with a label
|
||||
// can shrink to the action's minimum width. We do this so that action visibility
|
||||
// takes precedence over the action label.
|
||||
const isActionItemVisible = (index: number): boolean => {
|
||||
const element = this.getItemElement(index);
|
||||
return !element || DOM.getWindow(element).getComputedStyle(element).display !== 'none';
|
||||
};
|
||||
const getVisiblePrimaryActionIndexes = (): number[] => {
|
||||
const indexes: number[] = [];
|
||||
for (let index = 0; index < this.actionBar.length(); index++) {
|
||||
if (this.actionBar.getAction(index) !== this.toggleMenuAction && isActionItemVisible(index)) {
|
||||
indexes.push(index);
|
||||
}
|
||||
}
|
||||
return indexes;
|
||||
};
|
||||
const actionBarMinimumWidth = () => {
|
||||
if (this.options.responsiveBehavior?.kind === 'last') {
|
||||
const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction);
|
||||
const primaryActionsCount = hasToggleMenuAction
|
||||
? this.actionBar.length() - 1
|
||||
: this.actionBar.length();
|
||||
if (primaryActionsCount === 0) {
|
||||
const primaryActionIndexes = getVisiblePrimaryActionIndexes();
|
||||
if (primaryActionIndexes.length === 0) {
|
||||
return hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0;
|
||||
}
|
||||
|
||||
let itemsWidth = 0;
|
||||
for (let i = 0; i < primaryActionsCount - 1; i++) {
|
||||
itemsWidth += this.actionBar.getWidth(i) + ACTION_PADDING;
|
||||
for (const index of primaryActionIndexes.slice(0, -1)) {
|
||||
itemsWidth += this.actionBar.getWidth(index) + ACTION_PADDING;
|
||||
}
|
||||
|
||||
const action = this.actionBar.getAction(primaryActionsCount - 1);
|
||||
const action = this.actionBar.getAction(primaryActionIndexes.at(-1)!);
|
||||
itemsWidth += this.getActionMinWidth(action); // item to shrink
|
||||
itemsWidth += hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0; // toggle menu action
|
||||
|
||||
@@ -439,7 +467,9 @@ export class ToolBar extends Disposable {
|
||||
} else {
|
||||
let itemsWidth = 0;
|
||||
for (let i = 0; i < this.actionBar.length(); i++) {
|
||||
itemsWidth += this.getActionMinWidth(this.actionBar.getAction(i));
|
||||
if (isActionItemVisible(i)) {
|
||||
itemsWidth += this.getActionMinWidth(this.actionBar.getAction(i));
|
||||
}
|
||||
}
|
||||
return itemsWidth;
|
||||
}
|
||||
@@ -448,20 +478,17 @@ export class ToolBar extends Disposable {
|
||||
const projectedActionBarMinimumWidth = (actionToAdd: IAction, keepToggleMenuAction: boolean) => {
|
||||
let itemsWidth = this.getActionMinWidth(actionToAdd);
|
||||
if (this.options.responsiveBehavior?.kind === 'last') {
|
||||
const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction);
|
||||
const primaryActionsCount = hasToggleMenuAction
|
||||
? this.actionBar.length() - 1
|
||||
: this.actionBar.length();
|
||||
for (let i = 0; i < primaryActionsCount; i++) {
|
||||
const itemWidth = i === primaryActionsCount - 1
|
||||
? this.getUnshrunkItemWidth(i)
|
||||
: this.actionBar.getWidth(i);
|
||||
const primaryActionIndexes = getVisiblePrimaryActionIndexes();
|
||||
for (const [position, index] of primaryActionIndexes.entries()) {
|
||||
const itemWidth = position === primaryActionIndexes.length - 1
|
||||
? this.getUnshrunkItemWidth(index)
|
||||
: this.actionBar.getWidth(index);
|
||||
itemsWidth += itemWidth + ACTION_PADDING;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.actionBar.length(); i++) {
|
||||
const action = this.actionBar.getAction(i);
|
||||
if (action && action !== this.toggleMenuAction) {
|
||||
if (action && action !== this.toggleMenuAction && isActionItemVisible(i)) {
|
||||
itemsWidth += this.getActionMinWidth(action);
|
||||
}
|
||||
}
|
||||
@@ -480,11 +507,14 @@ export class ToolBar extends Disposable {
|
||||
}
|
||||
|
||||
if (minimumWidth > containerWidth) {
|
||||
const allowOverflow = this.options.responsiveBehavior?.allowOverflow;
|
||||
if (allowOverflow === false || (typeof allowOverflow === 'function' && !allowOverflow())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for max items limit
|
||||
if (this.options.responsiveBehavior?.minItems !== undefined) {
|
||||
const primaryActionsCount = this.actionBar.hasAction(this.toggleMenuAction)
|
||||
? this.actionBar.length() - 1
|
||||
: this.actionBar.length();
|
||||
const primaryActionsCount = getVisiblePrimaryActionIndexes().length;
|
||||
|
||||
if (primaryActionsCount <= this.options.responsiveBehavior.minItems) {
|
||||
return;
|
||||
@@ -493,12 +523,16 @@ export class ToolBar extends Disposable {
|
||||
|
||||
// Hide actions from the right
|
||||
while (minimumWidth > containerWidth && this.actionBar.length() > 0) {
|
||||
const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1;
|
||||
if (index < 0) {
|
||||
const index = getVisiblePrimaryActionIndexes().at(-1);
|
||||
if (index === undefined) {
|
||||
break;
|
||||
}
|
||||
const action = this.originalPrimaryActions[index];
|
||||
this.hiddenActions.unshift(action);
|
||||
const action = this.actionBar.getAction(index);
|
||||
if (!action) {
|
||||
break;
|
||||
}
|
||||
this.hiddenActions.push(action);
|
||||
this.hiddenActions.sort((a, b) => this.originalPrimaryActions.indexOf(a) - this.originalPrimaryActions.indexOf(b));
|
||||
|
||||
// Remove the action
|
||||
this.actionBar.pull(index);
|
||||
@@ -534,7 +568,9 @@ export class ToolBar extends Disposable {
|
||||
icon: this.options.icon ?? true,
|
||||
label: this.options.label ?? false,
|
||||
keybinding: this.getKeybindingLabel(action),
|
||||
index: this.originalPrimaryActions.length - this.hiddenActions.length - 1
|
||||
index: this.originalPrimaryActions
|
||||
.slice(0, this.originalPrimaryActions.indexOf(action))
|
||||
.reduce((index, precedingAction) => index + (this.actionBar.hasAction(precedingAction) ? 1 : 0), 0)
|
||||
});
|
||||
|
||||
// There are no secondary actions, and there is only one hidden item left so we
|
||||
@@ -550,7 +586,10 @@ export class ToolBar extends Disposable {
|
||||
}
|
||||
|
||||
// Update overflow menu
|
||||
const hiddenActions = this.hiddenActions.slice(0);
|
||||
const hiddenActions = this.hiddenActions.map(action => this.options.responsiveBehavior?.getOverflowAction?.(
|
||||
action,
|
||||
() => this.toggleMenuActionViewItem?.element,
|
||||
) ?? action);
|
||||
if (this.originalSecondaryActions.length > 0 || hiddenActions.length > 0) {
|
||||
const secondaryActions = this.originalSecondaryActions.slice(0);
|
||||
this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions);
|
||||
|
||||
@@ -13,12 +13,13 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.j
|
||||
|
||||
class FixedWidthActionViewItem extends BaseActionViewItem {
|
||||
|
||||
constructor(action: IAction, private readonly width: number) {
|
||||
constructor(action: IAction, private readonly width: number, private readonly visible = true) {
|
||||
super(undefined, action);
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
container.style.display = this.visible ? '' : 'none';
|
||||
container.style.width = `${this.width}px`;
|
||||
container.style.boxSizing = 'border-box';
|
||||
container.style.overflow = 'hidden';
|
||||
@@ -110,7 +111,7 @@ suite('ToolBar', () => {
|
||||
assert.strictEqual(toolbar.getItemAction(1)?.id, 'workbench.action.chat.openModePicker');
|
||||
assert.strictEqual(toolbar.getItemAction(2)?.id, 'workbench.action.chat.openModelPicker');
|
||||
assert.strictEqual(toolbar.getItemAction(3)?.id, ToggleMenuAction.ID);
|
||||
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), true);
|
||||
assert.strictEqual(toolbar.hasOverflow(), true);
|
||||
});
|
||||
|
||||
test('applies per-action responsive min widths', () => {
|
||||
@@ -404,13 +405,155 @@ suite('ToolBar', () => {
|
||||
|
||||
// availableWidth = 200 is plenty for all 3 actions; the element's 0 width is ignored
|
||||
assert.strictEqual(toolbar.getItemsLength(), 3);
|
||||
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), false);
|
||||
assert.strictEqual(toolbar.hasOverflow(), false);
|
||||
|
||||
availableWidth = 60;
|
||||
toolbar.relayout();
|
||||
|
||||
// availableWidth shrank — actions overflow into the toggle menu
|
||||
assert.strictEqual(toolbar.getItemAction(toolbar.getItemsLength() - 1)?.id, ToggleMenuAction.ID);
|
||||
assert.strictEqual(toolbar.getElement().querySelector('.monaco-action-bar')?.classList.contains('has-overflow'), true);
|
||||
assert.strictEqual(toolbar.hasOverflow(), true);
|
||||
|
||||
availableWidth = 200;
|
||||
toolbar.relayout();
|
||||
|
||||
assert.strictEqual(toolbar.getItemsLength(), 3);
|
||||
assert.strictEqual(toolbar.hasOverflow(), false);
|
||||
});
|
||||
|
||||
test('ignores non-rendered actions when deciding to overflow', () => {
|
||||
const hiddenActionIds = new Set(['hidden.a', 'hidden.b', 'hidden.c']);
|
||||
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
|
||||
responsiveBehavior: {
|
||||
enabled: true,
|
||||
kind: 'all',
|
||||
minItems: 1,
|
||||
actionMinWidth: 48,
|
||||
getActionMinWidth: () => 22,
|
||||
getAvailableWidth: () => 60,
|
||||
},
|
||||
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22, !hiddenActionIds.has(action.id)),
|
||||
}));
|
||||
toolbar.setActions([
|
||||
store.add(new Action('hidden.a', 'Hidden A')),
|
||||
store.add(new Action('hidden.b', 'Hidden B')),
|
||||
store.add(new Action('visible.a', 'Visible A')),
|
||||
store.add(new Action('hidden.c', 'Hidden C')),
|
||||
store.add(new Action('visible.b', 'Visible B')),
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
visibleActions: Array.from({ length: toolbar.getItemsLength() }, (_, index) => ({
|
||||
id: toolbar.getItemAction(index)?.id,
|
||||
display: toolbar.getItemElement(index)?.style.display,
|
||||
})).filter(item => item.display !== 'none').map(item => item.id),
|
||||
overflow: toolbar.hasOverflow(),
|
||||
}, {
|
||||
visibleActions: ['visible.a', 'visible.b'],
|
||||
overflow: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('can keep compact actions visible instead of overflowing', () => {
|
||||
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
|
||||
responsiveBehavior: {
|
||||
enabled: true,
|
||||
kind: 'all',
|
||||
minItems: 1,
|
||||
actionMinWidth: 22,
|
||||
getAvailableWidth: () => 50,
|
||||
allowOverflow: false,
|
||||
},
|
||||
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
|
||||
}));
|
||||
|
||||
toolbar.setActions([
|
||||
store.add(new Action('a', 'A')),
|
||||
store.add(new Action('b', 'B')),
|
||||
store.add(new Action('c', 'C')),
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
items: Array.from({ length: toolbar.getItemsLength() }, (_, index) => toolbar.getItemAction(index)?.id),
|
||||
overflow: toolbar.hasOverflow(),
|
||||
}, {
|
||||
items: ['a', 'b', 'c'],
|
||||
overflow: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('allows overflow only after compact actions still exceed the width', () => {
|
||||
let availableWidth = 100;
|
||||
let allCompact = false;
|
||||
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
|
||||
responsiveBehavior: {
|
||||
enabled: true,
|
||||
kind: 'all',
|
||||
minItems: 1,
|
||||
actionMinWidth: 22,
|
||||
getAvailableWidth: () => availableWidth,
|
||||
allowOverflow: () => allCompact,
|
||||
},
|
||||
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
|
||||
}));
|
||||
toolbar.setActions([
|
||||
store.add(new Action('a', 'A')),
|
||||
store.add(new Action('b', 'B')),
|
||||
store.add(new Action('c', 'C')),
|
||||
]);
|
||||
|
||||
availableWidth = 50;
|
||||
toolbar.relayout();
|
||||
const beforeCompact = toolbar.hasOverflow();
|
||||
|
||||
allCompact = true;
|
||||
toolbar.relayout();
|
||||
const afterCompact = toolbar.hasOverflow();
|
||||
|
||||
assert.deepStrictEqual({ beforeCompact, afterCompact }, {
|
||||
beforeCompact: false,
|
||||
afterCompact: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('uses overflow-specific proxy actions', async () => {
|
||||
const runs: string[] = [];
|
||||
let overflowAnchor: HTMLElement | undefined;
|
||||
const toolbar = store.add(new TestToolBar(container, contextMenuProvider, {
|
||||
responsiveBehavior: {
|
||||
enabled: true,
|
||||
kind: 'all',
|
||||
minItems: 1,
|
||||
actionMinWidth: 22,
|
||||
getAvailableWidth: () => 50,
|
||||
getOverflowAction: (action, getAnchor) => ({
|
||||
...action,
|
||||
run: () => {
|
||||
overflowAnchor = getAnchor();
|
||||
runs.push(`overflow:${action.id}`);
|
||||
},
|
||||
}),
|
||||
},
|
||||
actionViewItemProvider: action => new FixedWidthActionViewItem(action, 22),
|
||||
}));
|
||||
toolbar.setActions([
|
||||
store.add(new Action('a', 'A', undefined, true, () => runs.push('original:a'))),
|
||||
store.add(new Action('b', 'B', undefined, true, () => runs.push('original:b'))),
|
||||
store.add(new Action('c', 'C', undefined, true, () => runs.push('original:c'))),
|
||||
]);
|
||||
|
||||
const overflowAction = toolbar.getItemAction(toolbar.getItemsLength() - 1);
|
||||
assert.strictEqual(overflowAction?.id, ToggleMenuAction.ID);
|
||||
await (overflowAction as ToggleMenuAction).menuActions[0].run();
|
||||
const overflowViewItem = toolbar.getItemViewItem(toolbar.getItemsLength() - 1);
|
||||
const overflowButton = overflowViewItem instanceof BaseActionViewItem ? overflowViewItem.element : undefined;
|
||||
|
||||
assert.deepStrictEqual({
|
||||
runs,
|
||||
usesOverflowButton: overflowAnchor === overflowButton,
|
||||
}, {
|
||||
runs: ['overflow:b'],
|
||||
usesOverflowButton: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -632,8 +632,7 @@
|
||||
|
||||
/* The chip row scrolls horizontally, so we never want to collapse labels
|
||||
* to icon-only — keep them visible regardless of viewport width. This
|
||||
* overrides the desktop `@container (max-width: 330px)` query that
|
||||
* hides `.sessions-chat-dropdown-label` to make icon-only chips. */
|
||||
* overrides the desktop collision-driven compact state. */
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .action-label .sessions-chat-dropdown-label {
|
||||
display: inline;
|
||||
margin-left: 4px;
|
||||
|
||||
@@ -444,6 +444,10 @@ export class AutomationIsolationGroupActionViewItem extends BaseActionViewItem {
|
||||
});
|
||||
}
|
||||
|
||||
showPicker(anchor: HTMLElement): void {
|
||||
this.branchPicker.showPicker(anchor);
|
||||
}
|
||||
|
||||
private refreshTargetCapability(): void {
|
||||
const folderUri = this.isolationModel.folderUri;
|
||||
const sessionTypeId = this.state.sessionTypeId;
|
||||
@@ -1000,6 +1004,8 @@ export function renderForm(
|
||||
listForeground: 'var(--vscode-foreground)',
|
||||
listBackground: 'var(--vscode-input-background)',
|
||||
};
|
||||
let automationIsolationAction: IAction | undefined;
|
||||
const overflowIsolationItem = disposables.add(new MutableDisposable<AutomationIsolationGroupActionViewItem>());
|
||||
|
||||
const chatInputOptions: IChatInputPartOptions = {
|
||||
renderFollowups: false,
|
||||
@@ -1025,6 +1031,34 @@ export function renderForm(
|
||||
// leaving its scrollbar floating ~24px in from the right wall.
|
||||
inputPartHorizontalPadding: 0,
|
||||
sessionTypePickerDelegate: sessionTypeDelegate,
|
||||
secondaryToolbarOverflowActionHandler: (actionId, anchor) => {
|
||||
if (actionId === AUTOMATIONS_HARNESS_CHIP_ACTION_ID) {
|
||||
sessionTypePicker.showPicker(anchor);
|
||||
return true;
|
||||
}
|
||||
if (actionId === AUTOMATIONS_WORKSPACE_PICKER_ACTION_ID) {
|
||||
workspacePicker.showPicker(false, anchor);
|
||||
return true;
|
||||
}
|
||||
if (actionId === AUTOMATIONS_ISOLATION_GROUP_ACTION_ID && automationIsolationAction) {
|
||||
const item = instantiationService.createInstance(
|
||||
AutomationIsolationGroupActionViewItem,
|
||||
automationIsolationAction,
|
||||
state,
|
||||
isolationModel,
|
||||
isolationModel.folderUriObs,
|
||||
onDidChangeSessionTarget.event,
|
||||
revalidate,
|
||||
undefined,
|
||||
workspaceControlsVisible,
|
||||
);
|
||||
overflowIsolationItem.value = item;
|
||||
item.render(DOM.$('.automation-overflow-isolation-picker'));
|
||||
item.showPicker(anchor);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
secondaryToolbarActionViewItemProvider: (action, itemOptions) => {
|
||||
if (action.id === AUTOMATIONS_HARNESS_CHIP_ACTION_ID) {
|
||||
return new AutomationPickerActionViewItem(action, container => sessionTypePicker.render(container), undefined, itemOptions);
|
||||
@@ -1036,6 +1070,7 @@ export function renderForm(
|
||||
}, undefined, itemOptions);
|
||||
}
|
||||
if (action.id === AUTOMATIONS_ISOLATION_GROUP_ACTION_ID) {
|
||||
automationIsolationAction = action;
|
||||
const item = instantiationService.createInstance(
|
||||
AutomationIsolationGroupActionViewItem,
|
||||
action,
|
||||
|
||||
@@ -201,8 +201,8 @@ export class BranchPicker extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
showPicker(): void {
|
||||
if (!this._triggerElement || this._actionWidgetService.isVisible || !this._state.canOpen) {
|
||||
showPicker(anchor = this._triggerElement): void {
|
||||
if (!anchor || this._actionWidgetService.isVisible || !this._state.canOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,15 +218,15 @@ export class BranchPicker extends Disposable {
|
||||
},
|
||||
onHide: () => {
|
||||
this._isOpen = false;
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
if (trigger.isConnected) {
|
||||
trigger?.setAttribute('aria-expanded', 'false');
|
||||
if (trigger?.isConnected) {
|
||||
trigger.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this._isOpen = true;
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
trigger?.setAttribute('aria-expanded', 'true');
|
||||
const items = this._getItems();
|
||||
const branchCount = items.filter(item => item.item?.kind === 'branch' && !item.item.unavailable).length;
|
||||
this._actionWidgetService.show(
|
||||
@@ -234,7 +234,7 @@ export class BranchPicker extends Disposable {
|
||||
false,
|
||||
items,
|
||||
delegate,
|
||||
trigger,
|
||||
anchor,
|
||||
undefined,
|
||||
[],
|
||||
{
|
||||
|
||||
@@ -175,10 +175,6 @@
|
||||
color: var(--vscode-icon-foreground);
|
||||
}
|
||||
|
||||
.sessions-chat-toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Voice mode controls (mic / stop / settings / disconnect) */
|
||||
.sessions-chat-voice-toolbar {
|
||||
display: flex;
|
||||
@@ -228,6 +224,7 @@
|
||||
.sessions-chat-config-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -248,12 +245,35 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 30px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Prevent the mode picker from shrinking so the model picker label
|
||||
* ellipsizes first rather than the mode picker collapsing to icon-only. */
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item:has(.sessions-chat-dropdown-label) {
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker {
|
||||
box-sizing: border-box;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker .action-label {
|
||||
box-sizing: border-box;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
padding: 2px 2px 2px 8px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker .action-label.model-picker-split {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker .chat-input-picker-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Expanded pickers remain intrinsic; the responsive controller switches them
|
||||
* to compact form instead of allowing their labels to truncate. */
|
||||
.sessions-chat-config-toolbar .monaco-action-bar .action-item:not(.compact-picker) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -269,7 +289,7 @@
|
||||
color: var(--vscode-icon-foreground);
|
||||
white-space: nowrap;
|
||||
min-width: 30px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sessions-chat-config-toolbar .action-label:hover {
|
||||
@@ -282,13 +302,13 @@
|
||||
font-size: var(--vscode-agents-fontSize-label2, 11px);
|
||||
}
|
||||
|
||||
/* Allow long labels (e.g. the model picker name) to ellipsize when space is tight */
|
||||
/* Expanded labels are never truncated; compact mode removes the label. */
|
||||
.sessions-chat-config-toolbar .action-label .chat-input-picker-label {
|
||||
margin-left: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* When the picker has no leading icon (e.g. model picker), drop the icon-to-label gap. */
|
||||
@@ -655,4 +675,3 @@
|
||||
.sessions-chat-attachment-remove:hover {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,3 +242,7 @@
|
||||
.agent-sessions-workbench .interactive-session .chat-input-toolbars .chat-sessionPicker-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-sessions-workbench .interactive-session .compact-picker .sessions-chat-dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 16px 16px 20px 16px;
|
||||
/* Establishes a size container so the @container (max-width: 330px) query below
|
||||
* can collapse picker labels to icon-only when the new-chat area is narrow. */
|
||||
container-type: size;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -113,6 +110,10 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-controls-container {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@@ -133,64 +134,63 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Allow nested toolbar items to shrink so labels can ellipsize when space is tight.
|
||||
* Mirrors the regular chat-input-toolbar pattern: each flex layer between the
|
||||
* bounded container and the ellipsizing label gets `min-width: 0; overflow: hidden`. */
|
||||
/* Toolbar hosts can shrink, while individual expanded pickers remain intrinsic
|
||||
* and switch to compact form before their labels would truncate. */
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-controls-container > *,
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container > *,
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot .action-label {
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container > * {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Floor each picker so the icon + chevron (+ padding) stay visible even when
|
||||
* the label is fully ellipsized. Approx: 7px padding-left + 12px icon + 2px
|
||||
* label margin + 16px chevron box + 1px padding-right ~= 38px. The floor must
|
||||
* be applied to the outermost flex item (.action-item), not just the label,
|
||||
* because the parent's `min-width: 0` would otherwise let it clip the chevron. */
|
||||
.new-chat-widget-container .new-chat-bottom-container .monaco-action-bar .action-item,
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot .action-label,
|
||||
.new-chat-widget-container .new-chat-bottom-container .monaco-action-bar .action-item .action-label {
|
||||
/* Expanded picker controls never shrink or ellipsize. */
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .monaco-action-bar .action-item:not(.compact-picker),
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot:not(.compact-picker),
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot:not(.compact-picker) .action-label {
|
||||
flex-shrink: 0;
|
||||
min-width: 30px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Below this width the bottom-row pickers can't fit their labels comfortably,
|
||||
* so collapse to icon + chevron only. The .new-chat-widget-container declares
|
||||
* `container-type: size` which makes this a size container query. The
|
||||
* permission picker (`.sessions-chat-permission-picker`) gets a more lenient
|
||||
* threshold below because its label ("Autopilot (Preview)" etc.) carries
|
||||
* important state that is worth preserving as long as there is room. */
|
||||
@container (max-width: 330px) {
|
||||
/* Bottom-row pickers (Copilot CLI, Default Permissions, Worktree, branch): icon-only */
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-permission-picker .sessions-chat-dropdown-label {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
/* Chat input config toolbar: hide mode picker label (uses sessions-chat-dropdown-label),
|
||||
* but keep the model picker label (uses chat-input-picker-label) visible. */
|
||||
.new-chat-widget-container .sessions-chat-config-toolbar .sessions-chat-dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* With both chevron and label hidden the only content is the icon. Center
|
||||
* it instead of leaving the 30px min-width as left-aligned padding.
|
||||
* Permission picker keeps its label so its action-item is excluded. */
|
||||
.new-chat-widget-container .new-chat-bottom-container .monaco-action-bar .action-item:not(.sessions-chat-permission-picker) .action-label,
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot:not(.sessions-chat-permission-picker) .action-label {
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
}
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot:not(.compact-picker) .sessions-chat-dropdown-label,
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .sessions-chat-picker-slot:not(.compact-picker) .chat-session-option-label {
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@container (max-width: 240px) {
|
||||
.new-chat-widget-container .new-chat-bottom-container .sessions-chat-permission-picker .sessions-chat-dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
/* Individual picker controls collapse from right to left as their row runs out of room. */
|
||||
.new-chat-widget-container .compact-picker .sessions-chat-dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.action-item,
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.sessions-chat-picker-slot {
|
||||
box-sizing: border-box;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.action-item .action-label,
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.sessions-chat-picker-slot .action-label {
|
||||
box-sizing: border-box;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
justify-content: flex-start;
|
||||
padding: 2px 2px 2px 8px;
|
||||
}
|
||||
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.sessions-chat-picker-slot .action-label > .codicon {
|
||||
width: var(--vscode-codiconFontSize-compact);
|
||||
height: var(--vscode-codiconFontSize-compact);
|
||||
line-height: var(--vscode-codiconFontSize-compact);
|
||||
}
|
||||
|
||||
.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.sessions-chat-checkbox-chip .monaco-checkbox {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Spacing between action items inside the bottom-row toolbars (e.g. Worktree, branch) */
|
||||
|
||||
@@ -66,12 +66,12 @@ export class MobileSessionTypePicker extends SessionTypePicker {
|
||||
super.render(container, options);
|
||||
}
|
||||
|
||||
protected override _showPicker(): void {
|
||||
if (!this._triggerElement) {
|
||||
protected override _showPicker(anchor = this._triggerElement): void {
|
||||
if (!anchor) {
|
||||
return;
|
||||
}
|
||||
if (!isPhoneLayout(this.layoutService)) {
|
||||
super._showPicker();
|
||||
super._showPicker(anchor);
|
||||
return;
|
||||
}
|
||||
if (this._folderSessionTypes.length <= 1 && this._pickServedByFolder(this._picked)) {
|
||||
@@ -114,6 +114,9 @@ export class MobileSessionTypePicker extends SessionTypePicker {
|
||||
}
|
||||
|
||||
const trigger = this._triggerElement;
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
showMobilePickerSheet(
|
||||
this.layoutService.mainContainer,
|
||||
|
||||
@@ -89,6 +89,7 @@ import { ChatInputNotificationWidget } from '../../../../workbench/contrib/chat/
|
||||
import { ChatInputNoticeHost, ChatInputNoticeLane } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputNoticeHost.js';
|
||||
import { registerChatInputOnboardingHosts } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputOnboardingHosts.js';
|
||||
import { IChatInputNoticeHubService } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputNoticeHub.js';
|
||||
import { ChatInputPickerResponsiveLayout, IChatInputPickerResponsiveLayoutItem } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerResponsiveLayout.js';
|
||||
import { chatInputStackClass, chatInputStackSlotClass, ChatInputStackSlot, refreshChatInputStack, setChatInputStackSlot } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputStack.js';
|
||||
import { IChatSubmitRequestHandlerService } from '../../../../workbench/contrib/chat/browser/chatSubmitRequestHandlerService.js';
|
||||
import { INewChatModelPickerService, NewChatModelPickerService } from './newChatModelPicker.js';
|
||||
@@ -131,6 +132,41 @@ const MIN_EDITOR_HEIGHT = 50;
|
||||
const MAX_EDITOR_HEIGHT = 200;
|
||||
const NEW_CHAT_INPUT_FONT_FAMILY = 'system-ui, -apple-system, sans-serif';
|
||||
|
||||
function getLabeledPickerResponsiveItems(container: HTMLElement): IChatInputPickerResponsiveLayoutItem[] {
|
||||
const elements = new Map<HTMLElement, HTMLElement | undefined>();
|
||||
const actionItemLabelCounts = new Map<HTMLElement, number>();
|
||||
const visit = (element: HTMLElement, pickerSlot: HTMLElement | undefined, actionItem: HTMLElement | undefined): void => {
|
||||
const currentPickerSlot = element.classList.contains('sessions-chat-picker-slot') ? element : pickerSlot;
|
||||
const currentActionItem = element.classList.contains('action-item') ? element : actionItem;
|
||||
if (element.classList.contains('sessions-chat-dropdown-label')) {
|
||||
const pickerElement = currentPickerSlot ?? currentActionItem;
|
||||
if (pickerElement) {
|
||||
elements.set(pickerElement, currentActionItem);
|
||||
if (currentActionItem) {
|
||||
actionItemLabelCounts.set(currentActionItem, (actionItemLabelCounts.get(currentActionItem) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of element.children) {
|
||||
if (dom.isHTMLElement(child)) {
|
||||
visit(child, currentPickerSlot, currentActionItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(container, undefined, undefined);
|
||||
|
||||
return Array.from(elements, ([element, actionItem]) => ({
|
||||
element,
|
||||
isCompact: () => element.classList.contains('compact-picker'),
|
||||
setCompact: compact => {
|
||||
element.classList.toggle('compact-picker', compact);
|
||||
if (actionItem && actionItem !== element && actionItemLabelCounts.get(actionItem) === 1) {
|
||||
actionItem.classList.toggle('compact-picker', compact);
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/** True while focus is in an Agents window composer that supports dictation. */
|
||||
const SessionsChatInputHasDictationFocus = new RawContextKey<boolean>('sessionsChatInputHasDictationFocus', false, localize('sessionsChatInputHasDictationFocus', "True when focus is in an Agents window chat composer that supports dictation."));
|
||||
|
||||
@@ -312,7 +348,6 @@ function getRandomChatInputPlaceholder(): string {
|
||||
// #region --- New Chat Widget ---
|
||||
|
||||
export class NewChatInputWidget extends Disposable implements IHistoryNavigationWidget, INewSessionComposer {
|
||||
private static readonly compactModelPickerWidth = 280;
|
||||
|
||||
readonly sessionTypePicker: SessionTypePicker;
|
||||
|
||||
@@ -384,6 +419,8 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
private readonly _modelSelection: SessionModelSelection;
|
||||
private readonly _canSendRequest: IObservable<boolean>;
|
||||
private readonly _compactModelPicker = observableValue(this, false);
|
||||
private _primaryPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined;
|
||||
private _secondaryPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined;
|
||||
|
||||
// Input state
|
||||
private _draftState: IDraftState | undefined = {
|
||||
@@ -650,6 +687,11 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
},
|
||||
}));
|
||||
|
||||
this._secondaryPickerResponsiveLayout = this._register(new ChatInputPickerResponsiveLayout('NewChatInput.secondaryPicker', newChatBottomContainer, {
|
||||
getItems: () => getLabeledPickerResponsiveItems(newChatBottomContainer),
|
||||
}));
|
||||
this._secondaryPickerResponsiveLayout.layout();
|
||||
|
||||
// Restore draft input state from storage
|
||||
this._restoreState();
|
||||
|
||||
@@ -967,7 +1009,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
// Session config pickers (such as model) — rendered via MenuWorkbenchToolBar
|
||||
// Visibility controlled by context keys (isActiveSessionBackgroundProvider, isNewChatSession)
|
||||
const configContainer = dom.append(toolbar, dom.$('.sessions-chat-config-toolbar'));
|
||||
this._register(this._scopedInstantiationService.createInstance(MenuWorkbenchToolBar, configContainer, Menus.NewSessionConfig, {
|
||||
const configToolbar = this._register(this._scopedInstantiationService.createInstance(MenuWorkbenchToolBar, configContainer, Menus.NewSessionConfig, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
actionViewItemProvider: (action) => {
|
||||
if (action.id === 'sessions.modelPicker') {
|
||||
@@ -978,8 +1020,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
},
|
||||
}));
|
||||
|
||||
dom.append(toolbar, dom.$('.sessions-chat-toolbar-spacer'));
|
||||
|
||||
// Dictation mic button. Shares the STT service, mic
|
||||
// device, and gating (backend support + `dictation.enabled`)
|
||||
// with the main chat input; inserts the transcript into this composer's
|
||||
@@ -1043,6 +1083,32 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
this._register(sendButton.onDidClick(e => this._send(!!this.options.supportsBackground && !!(e as MouseEvent | KeyboardEvent | undefined)?.altKey)));
|
||||
}
|
||||
updateVoiceInputActionBorder();
|
||||
|
||||
this._primaryPickerResponsiveLayout = this._register(new ChatInputPickerResponsiveLayout('NewChatInput.primaryPicker', configContainer, {
|
||||
getItems: () => {
|
||||
const items: IChatInputPickerResponsiveLayoutItem[] = [];
|
||||
for (let index = 0; index < configToolbar.getItemsLength(); index++) {
|
||||
const element = configToolbar.getItemElement(index);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
element,
|
||||
isCompact: () => element.classList.contains('compact-picker'),
|
||||
setCompact: (compact: boolean) => {
|
||||
element.classList.toggle('compact-picker', compact);
|
||||
if (configToolbar.getItemAction(index)?.id === 'sessions.modelPicker') {
|
||||
this._compactModelPicker.set(compact, undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
hasOverflow: () => configToolbar.hasOverflow(),
|
||||
relayout: () => configToolbar.relayout(),
|
||||
}));
|
||||
this._primaryPickerResponsiveLayout.layout();
|
||||
}
|
||||
|
||||
private _createVoiceInputModePill(toolbar: HTMLElement, inputContainer: HTMLElement): void {
|
||||
@@ -1435,9 +1501,10 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
}
|
||||
}
|
||||
|
||||
layout(_height: number, width: number): void {
|
||||
this._compactModelPicker.set(width < NewChatInputWidget.compactModelPickerWidth, undefined);
|
||||
layout(_height: number, _width: number): void {
|
||||
this._editor?.layout();
|
||||
this._primaryPickerResponsiveLayout?.layout();
|
||||
this._secondaryPickerResponsiveLayout?.layout();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
|
||||
@@ -401,8 +401,12 @@ export class SessionTypePicker extends Disposable {
|
||||
* the override can decide where to anchor (or that it doesn't need
|
||||
* anchoring at all, e.g. for a bottom sheet).
|
||||
*/
|
||||
protected _showPicker(): void {
|
||||
if (!this._triggerElement || this.actionWidgetService.isVisible) {
|
||||
showPicker(anchor?: HTMLElement): void {
|
||||
this._showPicker(anchor);
|
||||
}
|
||||
|
||||
protected _showPicker(anchor = this._triggerElement): void {
|
||||
if (!anchor || this.actionWidgetService.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,7 +502,11 @@ export class SessionTypePicker extends Disposable {
|
||||
this.actionWidgetService.hide();
|
||||
this._handleSelectedSessionType(item);
|
||||
},
|
||||
onHide: () => { triggerElement.focus(); },
|
||||
onHide: () => {
|
||||
if (triggerElement?.isConnected) {
|
||||
triggerElement.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.actionWidgetService.show<ISessionTypePickerItem>(
|
||||
@@ -506,7 +514,7 @@ export class SessionTypePicker extends Disposable {
|
||||
false,
|
||||
groupedItems,
|
||||
delegate,
|
||||
this._triggerElement,
|
||||
anchor,
|
||||
undefined,
|
||||
[],
|
||||
{
|
||||
|
||||
@@ -43,6 +43,73 @@ suite('Sessions - Chat View', () => {
|
||||
assert.deepStrictEqual({ forwarded, petHostVisible: isVisible.get() }, { forwarded: [false, true], petHostVisible: true });
|
||||
});
|
||||
|
||||
test('hides the phone combined picker label when compact', () => {
|
||||
const toolbar = dom.append(document.body, dom.$('.sessions-chat-config-toolbar'));
|
||||
disposables.add(toDisposable(() => toolbar.remove()));
|
||||
const actionBar = dom.append(toolbar, dom.$('.monaco-action-bar'));
|
||||
const item = dom.append(actionBar, dom.$('.action-item.compact-picker'));
|
||||
const label = dom.append(item, dom.$('.chat-input-picker-label'));
|
||||
|
||||
assert.strictEqual(dom.getWindow(label).getComputedStyle(label).display, 'none');
|
||||
});
|
||||
|
||||
test('keeps compact empty-state picker icons inside their action item', () => {
|
||||
const toolbar = dom.append(document.body, dom.$('.sessions-chat-config-toolbar'));
|
||||
disposables.add(toDisposable(() => toolbar.remove()));
|
||||
const actionBar = dom.append(toolbar, dom.$('.monaco-action-bar'));
|
||||
const item = dom.append(actionBar, dom.$('.action-item.compact-picker'));
|
||||
const label = dom.append(item, dom.$('a.action-label'));
|
||||
const icon = dom.append(label, dom.$('span.codicon'));
|
||||
icon.style.width = '12px';
|
||||
icon.style.height = '12px';
|
||||
|
||||
const itemBounds = item.getBoundingClientRect();
|
||||
const labelBounds = label.getBoundingClientRect();
|
||||
const iconBounds = icon.getBoundingClientRect();
|
||||
assert.deepStrictEqual({
|
||||
labelOffset: labelBounds.left - itemBounds.left,
|
||||
iconOffset: iconBounds.left - itemBounds.left,
|
||||
iconEscapes: iconBounds.left < itemBounds.left || iconBounds.right > itemBounds.right,
|
||||
}, {
|
||||
labelOffset: 0,
|
||||
iconOffset: 8,
|
||||
iconEscapes: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps compact bottom-row picker glyphs inside their action item', () => {
|
||||
const workbench = dom.append(document.body, dom.$('.agent-sessions-workbench'));
|
||||
disposables.add(toDisposable(() => workbench.remove()));
|
||||
workbench.style.setProperty('--vscode-codiconFontSize-compact', '12px');
|
||||
const widget = dom.append(workbench, dom.$('.new-chat-widget-container.revealed'));
|
||||
const row = dom.append(widget, dom.$('.new-chat-bottom-container'));
|
||||
const actionBar = dom.append(row, dom.$('.monaco-action-bar'));
|
||||
const item = dom.append(actionBar, dom.$('.action-item.compact-picker'));
|
||||
const label = dom.append(item, dom.$('a.action-label'));
|
||||
const icon = dom.append(label, dom.$('span.codicon'));
|
||||
icon.style.width = '12px';
|
||||
icon.style.height = '12px';
|
||||
|
||||
const itemBounds = item.getBoundingClientRect();
|
||||
const labelBounds = label.getBoundingClientRect();
|
||||
const iconBounds = icon.getBoundingClientRect();
|
||||
assert.deepStrictEqual({
|
||||
itemWidth: itemBounds.width,
|
||||
labelWidth: labelBounds.width,
|
||||
labelOffset: labelBounds.left - itemBounds.left,
|
||||
iconWidth: iconBounds.width,
|
||||
iconOffset: iconBounds.left - itemBounds.left,
|
||||
iconEscapes: iconBounds.left < itemBounds.left || iconBounds.right > itemBounds.right,
|
||||
}, {
|
||||
itemWidth: 22,
|
||||
labelWidth: 22,
|
||||
labelOffset: 0,
|
||||
iconWidth: 12,
|
||||
iconOffset: 8,
|
||||
iconEscapes: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not forward aquarium visibility to the peer chat composer', () => {
|
||||
const isVisible = observableValue(disposables, true);
|
||||
const view: NewChatView = Object.assign(Object.create(NewChatView.prototype), {
|
||||
|
||||
@@ -107,10 +107,6 @@ class TestSessionTypePicker extends SessionTypePicker {
|
||||
pick(p: IPickedSessionType): void {
|
||||
this._handleSelectedSessionType(p);
|
||||
}
|
||||
|
||||
showPicker(): void {
|
||||
this._showPicker();
|
||||
}
|
||||
}
|
||||
|
||||
function createPicker(
|
||||
|
||||
+14
-3
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { autorun, IObservable } from '../../../../../base/common/observable.js';
|
||||
import { autorun, IObservable, ISettableObservable } from '../../../../../base/common/observable.js';
|
||||
import { MenuItemAction } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
@@ -16,6 +16,7 @@ import { IOpenerService } from '../../../../../platform/opener/common/opener.js'
|
||||
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IChatInputPickerOptions } from '../../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js';
|
||||
import { IChatInputPickerResponsiveState } from '../../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerResponsiveLayout.js';
|
||||
import { PermissionPickerActionItem } from '../../../../../workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.js';
|
||||
import { IActiveSession } from '../../../../services/sessions/common/sessionsManagement.js';
|
||||
import { AgentHostPermissionPickerDelegate } from './agentHostPermissionPickerDelegate.js';
|
||||
@@ -28,13 +29,14 @@ import { AgentHostPermissionPickerDelegate } from './agentHostPermissionPickerDe
|
||||
* the active session's `autoApprove` schema doesn't match the well-known
|
||||
* shape.
|
||||
*/
|
||||
export class AgentHostPermissionPickerActionItem extends PermissionPickerActionItem {
|
||||
export class AgentHostPermissionPickerActionItem extends PermissionPickerActionItem implements IChatInputPickerResponsiveState {
|
||||
|
||||
private readonly _delegate: AgentHostPermissionPickerDelegate;
|
||||
private readonly _compact: ISettableObservable<boolean>;
|
||||
|
||||
constructor(
|
||||
action: MenuItemAction,
|
||||
pickerOptions: IChatInputPickerOptions,
|
||||
pickerOptions: IChatInputPickerOptions & { readonly compact: ISettableObservable<boolean> },
|
||||
session: IObservable<IActiveSession | undefined>,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IActionWidgetService actionWidgetService: IActionWidgetService,
|
||||
@@ -63,6 +65,7 @@ export class AgentHostPermissionPickerActionItem extends PermissionPickerActionI
|
||||
hoverService,
|
||||
);
|
||||
this._delegate = this._register(delegate);
|
||||
this._compact = pickerOptions.compact;
|
||||
|
||||
// The base widget's label is rendered on demand via `refresh()`. Keep it
|
||||
// in sync with the delegate's level observable.
|
||||
@@ -72,6 +75,14 @@ export class AgentHostPermissionPickerActionItem extends PermissionPickerActionI
|
||||
}));
|
||||
}
|
||||
|
||||
isCompact(): boolean {
|
||||
return this._compact.get();
|
||||
}
|
||||
|
||||
setCompact(compact: boolean): void {
|
||||
this._compact.set(compact, undefined);
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
// The active session can change while this view item is alive (the
|
||||
|
||||
+27
-5
@@ -14,7 +14,7 @@ import { Checkbox } from '../../../../../base/browser/ui/toggle/toggle.js';
|
||||
import { Delayer } from '../../../../../base/common/async.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { autorun, constObservable, IObservable } from '../../../../../base/common/observable.js';
|
||||
import { autorun, IObservable, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { IActionViewItemService, type IActionViewItemFactory } from '../../../../../platform/actions/browser/actionViewItemService.js';
|
||||
@@ -34,6 +34,7 @@ import { ChatContextKeyExprs, ChatContextKeys } from '../../../../../workbench/c
|
||||
import { markOnboardingTarget } from '../../../../../workbench/contrib/onboarding/browser/spotlight/onboardingTarget.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../workbench/common/contributions.js';
|
||||
import { type IChatInputPickerOptions } from '../../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js';
|
||||
import { IChatInputPickerResponsiveState } from '../../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerResponsiveLayout.js';
|
||||
import { Menus } from '../../../../browser/menus.js';
|
||||
import { SessionProviderIdContext, IsPhoneLayoutContext, IsQuickChatSessionContext } from '../../../../common/contextkeys.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js';
|
||||
@@ -989,9 +990,12 @@ class MobileAgentHostSessionConfigPicker extends AgentHostSessionConfigPicker {
|
||||
|
||||
interface IConfigPickerWidget extends IDisposable {
|
||||
render(container: HTMLElement): void;
|
||||
showPicker?(anchor: HTMLElement, onHide?: () => void): boolean | void;
|
||||
}
|
||||
|
||||
export class PickerActionViewItem extends BaseActionViewItem {
|
||||
export class PickerActionViewItem extends BaseActionViewItem implements IChatInputPickerResponsiveState {
|
||||
private _compact = false;
|
||||
|
||||
constructor(private readonly _picker: IConfigPickerWidget, disposable?: IDisposable) {
|
||||
super(undefined, { id: '', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } });
|
||||
if (disposable) {
|
||||
@@ -1000,7 +1004,25 @@ export class PickerActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
this.element = container;
|
||||
this._picker.render(container);
|
||||
container.classList.toggle('compact-picker', this._compact);
|
||||
}
|
||||
|
||||
isCompact(): boolean {
|
||||
return this._compact;
|
||||
}
|
||||
|
||||
setCompact(compact: boolean): void {
|
||||
this._compact = compact;
|
||||
this.element?.classList.toggle('compact-picker', compact);
|
||||
}
|
||||
|
||||
show(anchor?: HTMLElement): void {
|
||||
const target = anchor ?? this.element;
|
||||
if (target) {
|
||||
this._picker.showPicker?.(target);
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
@@ -1126,10 +1148,10 @@ class AgentHostSessionConfigPickerContribution extends Disposable implements IWo
|
||||
return undefined;
|
||||
}
|
||||
const { session } = instantiationService.invokeFunction(accessor => accessor.get(ISessionContext));
|
||||
const pickerOptions: IChatInputPickerOptions = {
|
||||
compact: constObservable(true),
|
||||
const pickerOptions = {
|
||||
compact: observableValue<boolean, void>(action, false),
|
||||
listOptions: { minWidth: 255 },
|
||||
};
|
||||
} satisfies IChatInputPickerOptions;
|
||||
return instantiationService.createInstance(
|
||||
AgentHostPermissionPickerActionItem,
|
||||
action,
|
||||
|
||||
+33
-1
@@ -27,7 +27,7 @@ import { IAgentHostSessionsProvider, LOCAL_AGENT_HOST_PROVIDER_ID } from '../../
|
||||
import { ISessionsProvidersService } from '../../../../../../services/sessions/browser/sessionsProvidersService.js';
|
||||
import { IActiveSession } from '../../../../../../services/sessions/common/sessionsManagement.js';
|
||||
import { ISessionsProvider } from '../../../../../../services/sessions/common/sessionsProvider.js';
|
||||
import { AgentHostSessionConfigPicker, IConfigPickerItem } from '../../../browser/agentHostSessionConfigPicker.js';
|
||||
import { AgentHostSessionConfigPicker, IConfigPickerItem, PickerActionViewItem } from '../../../browser/agentHostSessionConfigPicker.js';
|
||||
|
||||
const SESSION_ID = 'local-agent-host:s1';
|
||||
|
||||
@@ -231,6 +231,38 @@ suite('Agent Host Session Config Picker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('picker action view items expose responsive compact state', () => {
|
||||
let pickerAnchor: HTMLElement | undefined;
|
||||
const item = store.add(new PickerActionViewItem({
|
||||
render: () => { },
|
||||
showPicker: anchor => {
|
||||
pickerAnchor = anchor;
|
||||
return true;
|
||||
},
|
||||
dispose: () => { },
|
||||
}));
|
||||
const container = document.createElement('div');
|
||||
const overflowAnchor = document.createElement('button');
|
||||
item.render(container);
|
||||
const expanded = {
|
||||
compact: item.isCompact(),
|
||||
className: container.classList.contains('compact-picker'),
|
||||
};
|
||||
|
||||
item.setCompact(true);
|
||||
item.show(overflowAnchor);
|
||||
const compact = {
|
||||
compact: item.isCompact(),
|
||||
className: container.classList.contains('compact-picker'),
|
||||
usesOverflowAnchor: pickerAnchor === overflowAnchor,
|
||||
};
|
||||
|
||||
assert.deepStrictEqual({ expanded, compact }, {
|
||||
expanded: { compact: false, className: false },
|
||||
compact: { compact: true, className: true, usesOverflowAnchor: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('a picker recreated on a session switch still renders the provider-seeded chips (disabled) while resolving', () => {
|
||||
const services = setupServices(store);
|
||||
const { provider } = services;
|
||||
|
||||
+6
@@ -416,6 +416,10 @@ export class AgentHostChatInputPicker extends Disposable {
|
||||
this._renderChip();
|
||||
}
|
||||
|
||||
show(anchor: HTMLElement): void {
|
||||
void this._showPicker(anchor);
|
||||
}
|
||||
|
||||
private _reattach(): void {
|
||||
const sessionResource = this._widget.viewModel?.sessionResource;
|
||||
const provisionalBackend = sessionResource ? this._provisional.get(sessionResource) : undefined;
|
||||
@@ -504,6 +508,7 @@ export class AgentHostChatInputPicker extends Disposable {
|
||||
this._trigger = undefined;
|
||||
this._renderDisposables.clear();
|
||||
dom.clearNode(this._container);
|
||||
this._container.classList.remove('agent-host-chat-input-picker-has-icon');
|
||||
|
||||
const ctx = this._readContext();
|
||||
// For sessions that have already started (i.e. no longer untitled —
|
||||
@@ -548,6 +553,7 @@ export class AgentHostChatInputPicker extends Disposable {
|
||||
dom.clearNode(trigger);
|
||||
|
||||
const icon = getConfigIcon(this._property, value);
|
||||
this._container?.classList.toggle('agent-host-chat-input-picker-has-icon', !!icon);
|
||||
if (icon) {
|
||||
dom.append(trigger, renderIcon(getCompactCodicon(icon)));
|
||||
}
|
||||
|
||||
+4
-1
@@ -150,15 +150,18 @@ export class AgentHostFolderPickerActionItem extends ChatInputPickerActionViewIt
|
||||
const selected = this._selectedFolder();
|
||||
const folder = selected && this._workspaceContextService.getWorkspace().folders.find(f => f.uri.toString() === selected.toString());
|
||||
const label = folder ? folder.name : (selected ? basename(selected) : localize('agentHost.selectFolder', "Folder"));
|
||||
const compact = this.pickerOptions.compact.get();
|
||||
element.classList.toggle('icon-only', compact);
|
||||
dom.reset(
|
||||
element,
|
||||
...renderLabelWithIcons(`$(folder-compact)`),
|
||||
dom.$('span.chat-input-picker-label', undefined, label),
|
||||
...(!compact ? [dom.$('span.chat-input-picker-label', undefined, label)] : []),
|
||||
);
|
||||
// Set the aria label after the visible text is in place: the base class
|
||||
// derives it from `element.textContent`, so labeling first would lag one
|
||||
// selection behind.
|
||||
this.setAriaLabelAttributes(element);
|
||||
element.ariaLabel = label;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -37,6 +37,7 @@ export class AgentHostGenericConfigChips extends Disposable {
|
||||
private _container: HTMLElement | undefined;
|
||||
|
||||
private readonly _chips = this._register(new DisposableMap<string>());
|
||||
private readonly _chipElements = new Map<string, HTMLElement>();
|
||||
|
||||
/**
|
||||
* Subscription to the active session's backend state. Maintained for the
|
||||
@@ -76,6 +77,10 @@ export class AgentHostGenericConfigChips extends Disposable {
|
||||
this._sync();
|
||||
}
|
||||
|
||||
getCompactableElements(): readonly HTMLElement[] {
|
||||
return Array.from(this._chipElements.values()).filter(element => element.classList.contains('agent-host-chat-input-picker-has-icon'));
|
||||
}
|
||||
|
||||
private _reattach(): void {
|
||||
const sessionResource = this._widget.viewModel?.sessionResource;
|
||||
const provisionalBackend = sessionResource ? this._provisional.get(sessionResource) : undefined;
|
||||
@@ -186,6 +191,7 @@ export class AgentHostGenericConfigChips extends Disposable {
|
||||
for (const property of [...this._chips.keys()]) {
|
||||
if (!desired.has(property)) {
|
||||
this._chips.deleteAndDispose(property);
|
||||
this._chipElements.delete(property);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +207,12 @@ export class AgentHostGenericConfigChips extends Disposable {
|
||||
// in `chat.css` (height, padding, chevron) applies here too.
|
||||
const slot = dom.append(this._container, dom.$('.agent-host-generic-chip-slot.chat-input-picker-item'));
|
||||
chip.render(slot);
|
||||
this._chipElements.set(property, slot);
|
||||
this._chips.set(property, {
|
||||
dispose: () => {
|
||||
chip.dispose();
|
||||
slot.remove();
|
||||
this._chipElements.delete(property);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+17
-10
@@ -90,15 +90,22 @@
|
||||
|
||||
}
|
||||
|
||||
/* Collapse agent host picker labels to icon-only when the secondary toolbar gets narrow. */
|
||||
.interactive-session .chat-secondary-toolbar {
|
||||
container-type: inline-size;
|
||||
/* Individual secondary pickers collapse from right to left as the lane narrows. */
|
||||
.interactive-session .compact-picker .agent-host-chat-input-picker-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (max-width: 350px) {
|
||||
.agent-host-chat-input-picker-label {
|
||||
display: none;
|
||||
}
|
||||
.interactive-session .compact-picker .agent-host-chat-input-picker-slot .action-label {
|
||||
box-sizing: border-box;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
padding: 2px 2px 2px 8px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.interactive-session .compact-picker .agent-host-chat-input-picker-slot .action-label .codicon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -132,9 +139,9 @@
|
||||
}
|
||||
|
||||
.agent-host-chat-input-picker-label {
|
||||
max-width: 16em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -238,19 +238,24 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI
|
||||
const domChildren = [];
|
||||
element.classList.add('chat-session-option-picker');
|
||||
const group = this.delegate.getOptionGroup();
|
||||
const compact = this._pickerOptions?.compact.get() ?? false;
|
||||
element.classList.toggle('compact', compact);
|
||||
const label = this.currentOption?.name ?? group?.description ?? localize('chat.sessionPicker.label', "Pick Option");
|
||||
// If the current option is the default and has an icon, collapse the text and show only the icon
|
||||
const isDefaultWithIcon = this.currentOption?.default && this.currentOption?.icon;
|
||||
element.classList.toggle('icon-only', compact && !!this.currentOption?.icon);
|
||||
|
||||
if (this.currentOption?.icon) {
|
||||
domChildren.push(renderIcon(getCompactCodicon(this.currentOption.icon)));
|
||||
}
|
||||
|
||||
if (!isDefaultWithIcon) {
|
||||
domChildren.push(dom.$('span.chat-session-option-label', undefined, this.currentOption?.name ?? group?.description ?? localize('chat.sessionPicker.label', "Pick Option")));
|
||||
if (!isDefaultWithIcon && (!compact || !this.currentOption?.icon)) {
|
||||
domChildren.push(dom.$('span.chat-session-option-label', undefined, label));
|
||||
}
|
||||
|
||||
dom.reset(element, ...domChildren);
|
||||
this.setAriaLabelAttributes(element);
|
||||
element.ariaLabel = label;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import { ResourceSet } from '../../../../../../base/common/map.js';
|
||||
import { MarshalledId } from '../../../../../../base/common/marshallingIds.js';
|
||||
import { Schemas } from '../../../../../../base/common/network.js';
|
||||
import { mixin } from '../../../../../../base/common/objects.js';
|
||||
import { autorun, constObservable, derived, derivedOpts, IObservable, ISettableObservable, ITransaction, observableFromEvent, observableValue, transaction } from '../../../../../../base/common/observable.js';
|
||||
import { autorun, derived, derivedOpts, IObservable, ISettableObservable, ITransaction, observableFromEvent, observableValue, transaction } from '../../../../../../base/common/observable.js';
|
||||
import { isMacintosh } from '../../../../../../base/common/platform.js';
|
||||
import { isEqual } from '../../../../../../base/common/resources.js';
|
||||
import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js';
|
||||
@@ -57,6 +57,7 @@ import { SuggestController } from '../../../../../../editor/contrib/suggest/brow
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js';
|
||||
import { MenuWorkbenchButtonBar } from '../../../../../../platform/actions/browser/buttonbar.js';
|
||||
import { IActionViewItemService, type IActionViewItemFactory } from '../../../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
|
||||
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js';
|
||||
import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js';
|
||||
@@ -160,6 +161,7 @@ import { ChatInputNoticeHost, ChatInputNoticeLane } from './chatInputNoticeHost.
|
||||
import { registerChatInputOnboardingHosts } from './chatInputOnboardingHosts.js';
|
||||
import { IChatInputNoticeHubService } from './chatInputNoticeHub.js';
|
||||
import { IChatInputPickerOptions } from './chatInputPickerActionItem.js';
|
||||
import { ChatInputPickerResponsiveLayout, IChatInputPickerResponsiveLayoutItem, isChatInputPickerResponsiveState } from './chatInputPickerResponsiveLayout.js';
|
||||
import { chatInputStackClass, chatInputStackSlotClass, ChatInputStackSlot, setChatInputStackInputFocused, setChatInputStackSlot } from './chatInputStack.js';
|
||||
import { ChatSelectedTools } from './chatSelectedTools.js';
|
||||
import { ChatPetAchievementIds, didExplicitlySwitchChatPetModel } from '../../chatPetAchievements.js';
|
||||
@@ -182,9 +184,64 @@ const INPUT_EDITOR_MAX_HEIGHT = 250;
|
||||
const INPUT_EDITOR_LINE_HEIGHT = 20;
|
||||
const INPUT_EDITOR_PADDING = { compact: { top: 2, bottom: 2 }, default: { top: 12, bottom: 12 } };
|
||||
const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2';
|
||||
const CHAT_INPUT_PICKER_COLLAPSE_WIDTH = 280;
|
||||
const PERMISSION_LEVEL_OPTION_ID = 'permissionLevel';
|
||||
|
||||
function getToolbarPickerResponsiveItems(toolbar: MenuWorkbenchToolBar, compactStates: ReadonlyMap<string, ISettableObservable<boolean>>): IChatInputPickerResponsiveLayoutItem[] {
|
||||
const items: IChatInputPickerResponsiveLayoutItem[] = [];
|
||||
const visibleActionIds = new Set<string>();
|
||||
|
||||
for (let index = 0; index < toolbar.getItemsLength(); index++) {
|
||||
const action = toolbar.getItemAction(index);
|
||||
const state = action && compactStates.get(action.id);
|
||||
const viewItem = toolbar.getItemViewItem(index);
|
||||
const viewItemState = isChatInputPickerResponsiveState(viewItem) ? viewItem : undefined;
|
||||
if (!action || (!state && !viewItemState)) {
|
||||
continue;
|
||||
}
|
||||
visibleActionIds.add(action.id);
|
||||
const element = toolbar.getItemElement(index);
|
||||
items.push({
|
||||
element,
|
||||
isCompact: () => viewItemState?.isCompact() ?? state!.get(),
|
||||
setCompact: compact => {
|
||||
state?.set(compact, undefined);
|
||||
viewItemState?.setCompact(compact);
|
||||
element?.classList.toggle('compact-picker', compact);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const [actionId, state] of compactStates) {
|
||||
if (!visibleActionIds.has(actionId)) {
|
||||
items.push({
|
||||
element: undefined,
|
||||
isCompact: () => state.get(),
|
||||
setCompact: compact => state.set(compact, undefined),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
type ShowableActionViewItem = IActionViewItem & { show(anchor?: HTMLElement): void };
|
||||
|
||||
function isShowableActionViewItem(item: IActionViewItem | undefined): item is ShowableActionViewItem {
|
||||
return !!item && 'show' in item && typeof item.show === 'function';
|
||||
}
|
||||
|
||||
function createOverflowAction(action: IAction, run: () => void): IAction {
|
||||
return {
|
||||
id: action.id,
|
||||
label: action.label,
|
||||
tooltip: action.tooltip,
|
||||
class: action.class,
|
||||
enabled: action.enabled,
|
||||
checked: action.checked,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
export interface IChatInputStyles {
|
||||
overlayBackground: string;
|
||||
listForeground: string;
|
||||
@@ -236,6 +293,11 @@ export interface IChatInputPartOptions {
|
||||
* chat input part while still using menu-driven rendering.
|
||||
*/
|
||||
secondaryToolbarActionViewItemProvider?: (action: IAction, options?: IActionViewItemOptions) => IActionViewItem | undefined;
|
||||
/**
|
||||
* Opens a host-owned secondary picker when its toolbar action moves into overflow.
|
||||
* Returns true when the action was handled.
|
||||
*/
|
||||
secondaryToolbarOverflowActionHandler?: (actionId: string, anchor: HTMLElement) => boolean;
|
||||
/**
|
||||
* When true, the mode picker hides custom agents and only offers the
|
||||
* built-in modes (Agent / Ask / Edit / Plan, gated by their normal
|
||||
@@ -341,7 +403,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
private static _counter = 0;
|
||||
|
||||
private _workingSetCollapsed = observableValue('chatInputPart.workingSetCollapsed', true);
|
||||
private _stableInputPartWidth = observableValue('chatInputPart.stableInputPartWidth', 0);
|
||||
private readonly _chatInputTodoListWidget = this._register(new MutableDisposable<ChatTodoListWidget>());
|
||||
private readonly _chatArtifactsWidget = this._register(new MutableDisposable<ChatArtifactsWidget>());
|
||||
private readonly _chatQuestionCarouselWidgets = this._register(new DisposableMap<string, ChatQuestionCarouselPart>());
|
||||
@@ -609,6 +670,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
private executeToolbar!: MenuWorkbenchToolBar;
|
||||
private inputActionsToolbar!: MenuWorkbenchToolBar;
|
||||
private _inputPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined;
|
||||
private _secondaryPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined;
|
||||
|
||||
|
||||
|
||||
@@ -648,6 +711,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
private modeWidget: ModePickerActionItem | undefined;
|
||||
private permissionWidget: PermissionPickerActionItem | undefined;
|
||||
private readonly permissionWidgetDisposeListener = this._register(new MutableDisposable<IDisposable>());
|
||||
private readonly overflowPickerWidget = this._register(new MutableDisposable<IDisposable>());
|
||||
private sessionTargetWidget: SessionTypePickerActionItem | undefined;
|
||||
private delegationWidget: DelegationSessionPickerActionItem | undefined;
|
||||
private readonly chatSessionPickerWidgets = this._register(new DisposableMap<string, ChatSessionPickerActionItem>());
|
||||
@@ -866,6 +930,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
@IChatService private readonly chatService: IChatService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@IChatPetService private readonly chatPetService: IChatPetService,
|
||||
@IActionViewItemService private readonly actionViewItemService: IActionViewItemService,
|
||||
) {
|
||||
super();
|
||||
this._modelSelectionDiagnostics = new ChatModelSelectionDiagnostics(this.logService, this.storageService, () => ({
|
||||
@@ -3076,6 +3141,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
]),
|
||||
]),
|
||||
dom.h('.chat-secondary-toolbar@secondaryToolbar', [
|
||||
dom.h('.chat-responsive-picker-container@responsivePickerContainer'),
|
||||
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
|
||||
dom.h('.chat-input-status-container@statusToolbarContainer'),
|
||||
]),
|
||||
@@ -3112,6 +3178,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
]),
|
||||
]),
|
||||
dom.h('.chat-secondary-toolbar@secondaryToolbar', [
|
||||
dom.h('.chat-responsive-picker-container@responsivePickerContainer'),
|
||||
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
|
||||
dom.h('.chat-input-status-container@statusToolbarContainer'),
|
||||
]),
|
||||
@@ -3138,6 +3205,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.attachedContextContainer = elements.attachedContextContainer;
|
||||
const toolbarsContainer = elements.inputToolbars;
|
||||
this.secondaryToolbarContainer = elements.secondaryToolbar;
|
||||
const responsivePickerContainer = elements.responsivePickerContainer;
|
||||
if (this.options.renderStyle === 'compact') {
|
||||
this.secondaryToolbarContainer.style.display = 'none';
|
||||
}
|
||||
@@ -3353,27 +3421,92 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
return !hasDraftTarget && (!target || (!!resource && isEqual(target, resource)));
|
||||
});
|
||||
|
||||
const pickerOptions: IChatInputPickerOptions = {
|
||||
const inputPickerCompactStates = new Map<string, ISettableObservable<boolean>>();
|
||||
const secondaryPickerCompactStates = new Map<string, ISettableObservable<boolean>>();
|
||||
const inputOverflowPickerHandlers = new Map<string, (anchor: HTMLElement) => void>();
|
||||
const secondaryOverflowPickerHandlers = new Map<string, (anchor: HTMLElement) => void>();
|
||||
const getCompactState = (states: Map<string, ISettableObservable<boolean>>, actionId: string): ISettableObservable<boolean> => {
|
||||
let state = states.get(actionId);
|
||||
if (!state) {
|
||||
state = observableValue(this, false);
|
||||
states.set(actionId, state);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
const getInputPickerOptions = (actionId: string): IChatInputPickerOptions => ({
|
||||
getOverflowAnchor: () => this.inputActionsToolbar.getElement(),
|
||||
actionContext: { widget },
|
||||
compact: derived(reader => this._stableInputPartWidth.read(reader) < CHAT_INPUT_PICKER_COLLAPSE_WIDTH),
|
||||
};
|
||||
const primarySessionPickerOptions: IChatInputPickerOptions = {
|
||||
...pickerOptions,
|
||||
compact: constObservable(true),
|
||||
};
|
||||
const secondaryPickerOptions: IChatInputPickerOptions = {
|
||||
...pickerOptions,
|
||||
compact: getCompactState(inputPickerCompactStates, actionId),
|
||||
});
|
||||
const getSecondaryPickerOptions = (actionId: string): IChatInputPickerOptions => ({
|
||||
getOverflowAnchor: () => this.secondaryToolbar.getElement(),
|
||||
compact: constObservable(true),
|
||||
actionContext: { widget },
|
||||
compact: getCompactState(secondaryPickerCompactStates, actionId),
|
||||
});
|
||||
const showOverflowPicker = (factory: () => ShowableActionViewItem | undefined, anchor: HTMLElement): void => {
|
||||
const item = factory();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this.overflowPickerWidget.value = item;
|
||||
item.render(dom.$('.chat-overflow-picker-item'));
|
||||
item.show(anchor);
|
||||
};
|
||||
const showRegisteredOverflowPicker = (factory: IActionViewItemFactory, action: IAction, anchor: HTMLElement): boolean => {
|
||||
const item = factory(action, { hoverDelegate }, this.instantiationService, dom.getWindow(anchor).vscodeWindowId);
|
||||
if (!isShowableActionViewItem(item)) {
|
||||
item?.dispose();
|
||||
return false;
|
||||
}
|
||||
this.overflowPickerWidget.value = item;
|
||||
item.render(dom.$('.chat-overflow-picker-item'));
|
||||
item.show(anchor);
|
||||
return true;
|
||||
};
|
||||
const getOverflowAction = (
|
||||
action: IAction,
|
||||
menuId: MenuId,
|
||||
handlers: ReadonlyMap<string, (anchor: HTMLElement) => void>,
|
||||
getAnchor: () => HTMLElement | undefined,
|
||||
fallbackAnchor: HTMLElement,
|
||||
hostHandler?: (actionId: string, anchor: HTMLElement) => boolean,
|
||||
): IAction => {
|
||||
const handler = handlers.get(action.id);
|
||||
const registeredFactory = this.actionViewItemService.lookUp(menuId, action.id);
|
||||
if (!handler && !hostHandler && !registeredFactory) {
|
||||
return action;
|
||||
}
|
||||
return createOverflowAction(action, () => {
|
||||
const overflowAnchor = getAnchor();
|
||||
const anchor = overflowAnchor ?? fallbackAnchor;
|
||||
dom.getWindow(anchor).setTimeout(() => {
|
||||
overflowAnchor?.focus();
|
||||
if (handler) {
|
||||
handler(anchor);
|
||||
} else if (hostHandler?.(action.id, anchor)) {
|
||||
return;
|
||||
} else if (registeredFactory && showRegisteredOverflowPicker(registeredFactory, action, anchor)) {
|
||||
return;
|
||||
} else {
|
||||
void action.run({ widget } satisfies IChatExecuteActionContext);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
|
||||
this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
|
||||
const shorterChatInputActionIds = new Set<string>([
|
||||
OpenModePickerAction.ID,
|
||||
ConfigureToolsAction.ID,
|
||||
]);
|
||||
const getInputActionMinWidth = (action: IAction): number | undefined => {
|
||||
if (shorterChatInputActionIds.has(action.id)) {
|
||||
return 22;
|
||||
}
|
||||
return inputPickerCompactStates.get(action.id)?.get() ? 22 : undefined;
|
||||
};
|
||||
|
||||
this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
|
||||
this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
|
||||
this.inputActionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.options.renderInputToolbarBelowInput ? this.attachmentsContainer : toolbarsContainer, MenuId.ChatInput, {
|
||||
telemetrySource: this.options.menus.telemetrySource,
|
||||
menuOptions: { shouldForwardArgs: true },
|
||||
@@ -3384,7 +3517,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
kind: 'last',
|
||||
minItems: 1,
|
||||
actionMinWidth: 48,
|
||||
getActionMinWidth: action => shorterChatInputActionIds.has(action.id) ? 22 : undefined,
|
||||
getActionMinWidth: getInputActionMinWidth,
|
||||
allowOverflow: () => this._inputPickerResponsiveLayout?.areAllItemsCompact() === true,
|
||||
getOverflowAction: (action, getAnchor) => getOverflowAction(action, MenuId.ChatInput, inputOverflowPickerHandlers, getAnchor, toolbarsContainer),
|
||||
},
|
||||
actionViewItemProvider: (action, options) => {
|
||||
// Phone-layout branch: when an agents-window phone presenter
|
||||
@@ -3414,10 +3549,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
|
||||
const itemDelegate: IModelPickerDelegate = this._createModelPickerDelegate();
|
||||
return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, itemDelegate, pickerOptions);
|
||||
const createPicker = () => this.instantiationService.createInstance(ModelPickerActionItem, action, itemDelegate, getInputPickerOptions(action.id));
|
||||
inputOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return this.modelWidget = createPicker();
|
||||
} else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) {
|
||||
const delegate: IModePickerDelegate = this._createModePickerDelegate();
|
||||
return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions);
|
||||
const createPicker = () => this.instantiationService.createInstance(ModePickerActionItem, action, delegate, getInputPickerOptions(action.id));
|
||||
inputOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return this.modeWidget = createPicker();
|
||||
} else if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) {
|
||||
// Use provided delegate if available, otherwise create default delegate
|
||||
const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? {
|
||||
@@ -3434,14 +3573,23 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
};
|
||||
const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider;
|
||||
const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem;
|
||||
return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions);
|
||||
} else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) {
|
||||
// Cloud sessions render their option-group pickers (e.g. branch) on the primary toolbar
|
||||
const widgets = this.createChatSessionPickerWidgets(action, primarySessionPickerOptions);
|
||||
if (widgets.length === 0) {
|
||||
return new HiddenActionViewItem(action);
|
||||
const createPicker = () => this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, getInputPickerOptions(action.id));
|
||||
inputOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
const picker = createPicker();
|
||||
if (picker instanceof DelegationSessionPickerActionItem) {
|
||||
this.delegationWidget = picker;
|
||||
} else {
|
||||
this.sessionTargetWidget = picker;
|
||||
}
|
||||
return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets);
|
||||
return picker;
|
||||
} else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) {
|
||||
const createPicker = () => {
|
||||
// Cloud sessions render their option-group pickers (e.g. branch) on the primary toolbar
|
||||
const widgets = this.createChatSessionPickerWidgets(action, getInputPickerOptions(action.id));
|
||||
return widgets.length === 0 ? undefined : this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets);
|
||||
};
|
||||
inputOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return createPicker() ?? new HiddenActionViewItem(action);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -3460,17 +3608,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this._toolbarRelayoutScheduler.schedule();
|
||||
}
|
||||
}));
|
||||
// When compact changes, picker items change their rendered size
|
||||
// but the toolbar's ResizeObserver won't fire (the toolbar element size
|
||||
// didn't change, only its children did). Force a relayout so the
|
||||
// responsive overflow logic re-evaluates with the correct item widths.
|
||||
// The relayout is deferred by a microtask so the picker action view
|
||||
// items' own autoruns have a chance to re-render their labels first.
|
||||
this._register(autorun(reader => {
|
||||
pickerOptions.compact.read(reader);
|
||||
queueMicrotask(() => this.inputActionsToolbar.relayout());
|
||||
}));
|
||||
|
||||
// When the phone-input presenter flips between enabled/disabled (e.g.
|
||||
// device rotation crossing the phone breakpoint), the action view item
|
||||
// provider above will return different items. Force the toolbar to
|
||||
@@ -3567,13 +3704,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
|
||||
// Secondary toolbar (permissions) — below the input box.
|
||||
// Per-action minimum widths (in pixels) for pickers that collapse to an
|
||||
// icon-only label via a CSS container query in `AgentHostChatInputPicker`.
|
||||
// Most pickers reserve ~22px for the icon; the tunnel-sharing toggle has
|
||||
// no chevron, so it can collapse further to 16px.
|
||||
const agentHostShortPickerMinWidths = new Map<string, number>([
|
||||
// Compact-capable pickers use their 22px control width as the responsive
|
||||
// floor so icon-only items do not retain empty space from the labeled form.
|
||||
// The tunnel-sharing toggle has no chevron and can collapse further.
|
||||
const secondaryPickerMinWidths = new Map<string, number>([
|
||||
[OpenSessionTargetPickerAction.ID, 22],
|
||||
[OpenDelegationPickerAction.ID, 22],
|
||||
[OpenWorkspacePickerAction.ID, 22],
|
||||
[OpenPermissionPickerAction.ID, 22],
|
||||
[ChatSessionPrimaryPickerAction.ID, 22],
|
||||
[OpenAgentHostModePickerAction.ID, 22],
|
||||
['sessions.agentHost.runningSessionModePicker', 22],
|
||||
['sessions.agentHost.runningSessionConfigPicker', 22],
|
||||
['sessions.agentHost.runningSessionPermissionModePicker', 22],
|
||||
['sessions.agentHost.runningSessionCodexApprovalsPicker', 22],
|
||||
[OpenAgentHostAutoApprovePickerAction.ID, 22],
|
||||
[OpenAgentHostPermissionModePickerAction.ID, 22],
|
||||
[OpenAgentHostCodexApprovalsPickerAction.ID, 22],
|
||||
@@ -3583,16 +3727,22 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
// Direct-rendered chip lane for agent-host config properties that
|
||||
// are advertised by the agent's schema but not handled by a
|
||||
// dedicated `MenuId.ChatInputSecondary` action. Sits as a sibling
|
||||
// of the secondary toolbar so the toolbar can take the available
|
||||
// space (`flex: 1 1 0`) while the chips pin to the right next to
|
||||
// the context-usage widget.
|
||||
// of the content-sized secondary toolbar.
|
||||
const genericChipsContainer = dom.$('.chat-secondary-generic-chips');
|
||||
const genericChipsLane = this._register(this.instantiationService.createInstance(
|
||||
AgentHostGenericConfigChips,
|
||||
widget,
|
||||
));
|
||||
genericChipsLane.render(genericChipsContainer);
|
||||
this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.secondaryToolbarContainer, MenuId.ChatInputSecondary, {
|
||||
const getSecondaryToolbarAvailableWidth = (): number => {
|
||||
const laneWidth = responsivePickerContainer.getBoundingClientRect().width;
|
||||
if (genericChipsContainer.parentElement !== responsivePickerContainer || genericChipsContainer.getClientRects().length === 0) {
|
||||
return laneWidth;
|
||||
}
|
||||
const gap = Number.parseFloat(dom.getWindow(responsivePickerContainer).getComputedStyle(responsivePickerContainer).columnGap) || 0;
|
||||
return Math.max(0, laneWidth - genericChipsContainer.getBoundingClientRect().width - gap);
|
||||
};
|
||||
this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, responsivePickerContainer, MenuId.ChatInputSecondary, {
|
||||
telemetrySource: this.options.menus.telemetrySource,
|
||||
menuOptions: { shouldForwardArgs: true },
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
@@ -3602,16 +3752,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
kind: 'all',
|
||||
minItems: 1,
|
||||
actionMinWidth: 48,
|
||||
// Agent-host pickers collapse to an icon-only label via a CSS
|
||||
// container query in `AgentHostChatInputPicker` when narrow.
|
||||
// Report a smaller min-width for them so the responsive layout
|
||||
// keeps them visible instead of overflowing into the menu.
|
||||
getActionMinWidth: action => agentHostShortPickerMinWidths.get(action.id),
|
||||
getActionMinWidth: action => secondaryPickerMinWidths.get(action.id) ?? (secondaryPickerCompactStates.get(action.id)?.get() ? 22 : undefined),
|
||||
observedElement: responsivePickerContainer,
|
||||
getAvailableWidth: getSecondaryToolbarAvailableWidth,
|
||||
allowOverflow: () => this._secondaryPickerResponsiveLayout?.areAllItemsCompact() === true,
|
||||
getOverflowAction: (action, getAnchor) => getOverflowAction(action, MenuId.ChatInputSecondary, secondaryOverflowPickerHandlers, getAnchor, responsivePickerContainer, this.options.secondaryToolbarOverflowActionHandler),
|
||||
},
|
||||
actionViewItemProvider: (action, options) => {
|
||||
const agentHostPickerProperty = getAgentHostPickerProperty(action.id);
|
||||
const customSecondaryItem = this.options.secondaryToolbarActionViewItemProvider?.(action, options);
|
||||
if (customSecondaryItem) {
|
||||
getCompactState(secondaryPickerCompactStates, action.id);
|
||||
return customSecondaryItem;
|
||||
}
|
||||
if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) {
|
||||
@@ -3629,10 +3780,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
};
|
||||
const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider;
|
||||
const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem;
|
||||
return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, secondaryPickerOptions);
|
||||
const createPicker = () => this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, getSecondaryPickerOptions(action.id));
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
const picker = createPicker();
|
||||
if (picker instanceof DelegationSessionPickerActionItem) {
|
||||
this.delegationWidget = picker;
|
||||
} else {
|
||||
this.sessionTargetWidget = picker;
|
||||
}
|
||||
return picker;
|
||||
} else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) {
|
||||
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) {
|
||||
return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, secondaryPickerOptions);
|
||||
const workspacePickerDelegate = this.options.workspacePickerDelegate;
|
||||
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && workspacePickerDelegate) {
|
||||
const createPicker = () => this.instantiationService.createInstance(WorkspacePickerActionItem, action, workspacePickerDelegate, getSecondaryPickerOptions(action.id));
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return createPicker();
|
||||
} else {
|
||||
return new HiddenActionViewItem(action);
|
||||
}
|
||||
@@ -3672,7 +3834,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
},
|
||||
isSandboxToggleApplicable: () => this.getEffectiveSessionType(this.getCurrentSessionResource()) === SessionType.Local,
|
||||
};
|
||||
const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, secondaryPickerOptions);
|
||||
const createPicker = () => this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, getSecondaryPickerOptions(action.id));
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
const widget = createPicker();
|
||||
this.permissionWidget = widget;
|
||||
this.permissionWidgetDisposeListener.value = widget.onDidDispose(() => {
|
||||
if (this.permissionWidget === widget) {
|
||||
@@ -3685,28 +3849,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
if (this.options.isSessionsWindow) {
|
||||
return new HiddenActionViewItem(action);
|
||||
}
|
||||
const picker = this.instantiationService.createInstance(AgentHostChatInputPicker, widget, agentHostPickerProperty);
|
||||
return new AgentHostChatInputPickerActionViewItem(action, picker);
|
||||
getCompactState(secondaryPickerCompactStates, action.id);
|
||||
const createPicker = () => this.instantiationService.createInstance(AgentHostChatInputPicker, widget, agentHostPickerProperty);
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => {
|
||||
const picker = createPicker();
|
||||
this.overflowPickerWidget.value = picker;
|
||||
picker.show(anchor);
|
||||
});
|
||||
return new AgentHostChatInputPickerActionViewItem(action, createPicker());
|
||||
} else if (action.id === OpenAgentHostFolderPickerAction.ID && action instanceof MenuItemAction) {
|
||||
if (this.options.isSessionsWindow) {
|
||||
return new HiddenActionViewItem(action);
|
||||
}
|
||||
return this.instantiationService.createInstance(AgentHostFolderPickerActionItem, action, widget, secondaryPickerOptions);
|
||||
const createPicker = () => this.instantiationService.createInstance(AgentHostFolderPickerActionItem, action, widget, getSecondaryPickerOptions(action.id));
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return createPicker();
|
||||
} else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) {
|
||||
// Create all pickers and return a container action view item
|
||||
const widgets = this.createChatSessionPickerWidgets(action, secondaryPickerOptions);
|
||||
if (widgets.length === 0) {
|
||||
return new HiddenActionViewItem(action);
|
||||
}
|
||||
// Create a container to hold all picker widgets
|
||||
return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets);
|
||||
const createPicker = () => {
|
||||
const widgets = this.createChatSessionPickerWidgets(action, getSecondaryPickerOptions(action.id));
|
||||
return widgets.length === 0 ? undefined : this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets);
|
||||
};
|
||||
secondaryOverflowPickerHandlers.set(action.id, anchor => showOverflowPicker(createPicker, anchor));
|
||||
return createPicker() ?? new HiddenActionViewItem(action);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar');
|
||||
this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext;
|
||||
dom.append(this.secondaryToolbarContainer, genericChipsContainer);
|
||||
dom.append(responsivePickerContainer, genericChipsContainer);
|
||||
this._register(this.secondaryToolbar.onDidChangeMenuItems(() => {
|
||||
// Update container reference for the pickers when the secondary toolbar hosts one.
|
||||
// Only assign when found so we don't overwrite a valid primary container reference
|
||||
@@ -3729,6 +3900,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.statusToolbar.getElement().classList.add('chat-input-status-toolbar');
|
||||
this.statusToolbar.context = { widget } satisfies IChatExecuteActionContext;
|
||||
|
||||
const inputToolbarElement = this.inputActionsToolbar.getElement();
|
||||
this._inputPickerResponsiveLayout = this._register(new ChatInputPickerResponsiveLayout('ChatInputPart.primaryPicker', inputToolbarElement, {
|
||||
getItems: () => getToolbarPickerResponsiveItems(this.inputActionsToolbar, inputPickerCompactStates),
|
||||
hasOverflow: () => this.inputActionsToolbar.hasOverflow(),
|
||||
relayout: () => this.inputActionsToolbar.relayout(),
|
||||
}));
|
||||
|
||||
this._secondaryPickerResponsiveLayout = this._register(new ChatInputPickerResponsiveLayout('ChatInputPart.secondaryPicker', responsivePickerContainer, {
|
||||
getItems: () => [
|
||||
...getToolbarPickerResponsiveItems(this.secondaryToolbar, secondaryPickerCompactStates),
|
||||
...genericChipsLane.getCompactableElements()
|
||||
.map(element => ({
|
||||
element,
|
||||
isCompact: () => element.classList.contains('compact-picker'),
|
||||
setCompact: (compact: boolean) => element.classList.toggle('compact-picker', compact),
|
||||
})),
|
||||
],
|
||||
hasOverflow: () => this.secondaryToolbar.hasOverflow(),
|
||||
relayout: () => this.secondaryToolbar.relayout(),
|
||||
}));
|
||||
|
||||
this._inputPickerResponsiveLayout.layout();
|
||||
this._secondaryPickerResponsiveLayout.layout();
|
||||
|
||||
let inputModel = this.modelService.getModel(this.inputUri);
|
||||
let createdInputModel: ITextModel | undefined;
|
||||
if (!inputModel) {
|
||||
@@ -4783,10 +4978,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
*/
|
||||
layout(width: number) {
|
||||
this.cachedWidth = width;
|
||||
this._stableInputPartWidth.set(width, undefined);
|
||||
this._updateWorkingProgressAnimationDuration(width);
|
||||
|
||||
return this._layout(width);
|
||||
const result = this._layout(width);
|
||||
this._inputPickerResponsiveLayout?.layout();
|
||||
this._secondaryPickerResponsiveLayout?.layout();
|
||||
return result;
|
||||
}
|
||||
|
||||
private layoutForToolbarChange(): void {
|
||||
@@ -5038,6 +5235,10 @@ class ChatSessionPickersContainerActionItem extends ActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.widgets[0]?.show();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
for (const widget of this.widgets) {
|
||||
widget.dispose();
|
||||
|
||||
@@ -42,6 +42,7 @@ export function withChatInputPickerMotion(listOptions: IActionListOptions | unde
|
||||
* Provides common anchor resolution logic for dropdown positioning.
|
||||
*/
|
||||
export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdownActionViewItem {
|
||||
private _externalAnchor: HTMLElement | undefined;
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
@@ -80,12 +81,20 @@ export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdown
|
||||
* Falls back to the overflow anchor if this element is not in the DOM.
|
||||
*/
|
||||
protected getAnchorElement(): HTMLElement {
|
||||
if (this._externalAnchor?.isConnected) {
|
||||
return this._externalAnchor;
|
||||
}
|
||||
if (this.element && getActiveWindow().document.contains(this.element)) {
|
||||
return this.element;
|
||||
}
|
||||
return this.pickerOptions.getOverflowAnchor?.() ?? this.element!;
|
||||
}
|
||||
|
||||
override show(anchor?: HTMLElement): void {
|
||||
this._externalAnchor = anchor;
|
||||
super.show();
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
container.classList.add('chat-input-picker-item');
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from '../../../../../../base/browser/dom.js';
|
||||
import { Disposable, toDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
|
||||
const WIDTH_TOLERANCE = 1;
|
||||
|
||||
export interface IChatInputPickerResponsiveLayoutDelegate {
|
||||
getItems(): readonly IChatInputPickerResponsiveLayoutItem[];
|
||||
hasOverflow?(): boolean;
|
||||
relayout?(): void;
|
||||
}
|
||||
|
||||
export interface IChatInputPickerResponsiveState {
|
||||
isCompact(): boolean;
|
||||
setCompact(compact: boolean): void;
|
||||
}
|
||||
|
||||
export interface IChatInputPickerResponsiveLayoutItem extends IChatInputPickerResponsiveState {
|
||||
readonly element: HTMLElement | undefined;
|
||||
}
|
||||
|
||||
export function isChatInputPickerResponsiveState(candidate: object | undefined): candidate is IChatInputPickerResponsiveState {
|
||||
return !!candidate
|
||||
&& 'isCompact' in candidate
|
||||
&& typeof candidate.isCompact === 'function'
|
||||
&& 'setCompact' in candidate
|
||||
&& typeof candidate.setCompact === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compacts a picker lane only when its expanded contents no longer fit the width assigned by its surrounding layout.
|
||||
*/
|
||||
export class ChatInputPickerResponsiveLayout extends Disposable {
|
||||
|
||||
private readonly _mutationObserver: MutationObserver;
|
||||
private _isLayouting = false;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _delegate: IChatInputPickerResponsiveLayoutDelegate,
|
||||
) {
|
||||
super();
|
||||
|
||||
const targetWindow = dom.getWindow(_element);
|
||||
const resizeObserver = this._register(new dom.DisposableResizeObserver(name, () => this.layout(), targetWindow));
|
||||
this._register(resizeObserver.observe(_element));
|
||||
|
||||
this._mutationObserver = new targetWindow.MutationObserver(() => this.layout());
|
||||
this._observeMutations();
|
||||
this._register(toDisposable(() => this._mutationObserver.disconnect()));
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this._isLayouting || !this._element.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableWidth = this._element.getBoundingClientRect().width;
|
||||
if (availableWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isLayouting = true;
|
||||
this._mutationObserver.disconnect();
|
||||
try {
|
||||
// Restore as many hidden actions as possible in their shortest form
|
||||
// before measuring. Otherwise an overflow menu can hide the very items
|
||||
// whose expanded width should keep the lane compact.
|
||||
this._setAllCompact(true);
|
||||
this._delegate.relayout?.();
|
||||
this._setAllCompact(true);
|
||||
this._delegate.relayout?.();
|
||||
if (this._delegate.hasOverflow?.()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = this._getOrderedVisibleItems();
|
||||
for (const item of items) {
|
||||
item.setCompact(false);
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (this._fitsAvailableWidth(availableWidth)) {
|
||||
break;
|
||||
}
|
||||
item.setCompact(true);
|
||||
}
|
||||
this._delegate.relayout?.();
|
||||
} finally {
|
||||
this._observeMutations();
|
||||
this._isLayouting = false;
|
||||
}
|
||||
}
|
||||
|
||||
areAllItemsCompact(): boolean {
|
||||
return this._delegate.getItems().every(item => item.isCompact());
|
||||
}
|
||||
|
||||
private _setAllCompact(compact: boolean): void {
|
||||
for (const item of this._delegate.getItems()) {
|
||||
item.setCompact(compact);
|
||||
}
|
||||
}
|
||||
|
||||
private _getOrderedVisibleItems(): IChatInputPickerResponsiveLayoutItem[] {
|
||||
return this._delegate.getItems()
|
||||
.filter(item => item.element?.isConnected && item.element.getClientRects().length > 0)
|
||||
.sort((a, b) => b.element!.getBoundingClientRect().left - a.element!.getBoundingClientRect().left);
|
||||
}
|
||||
|
||||
private _fitsAvailableWidth(availableWidth: number): boolean {
|
||||
const items = this._getOrderedVisibleItems();
|
||||
const preferredLayout = this._measurePreferredLayout(items);
|
||||
if (preferredLayout.width > availableWidth + WIDTH_TOLERANCE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const laneBounds = this._element.getBoundingClientRect();
|
||||
const itemBounds = items
|
||||
.map(item => ({ item, bounds: item.element!.getBoundingClientRect() }))
|
||||
.sort((a, b) => a.bounds.left - b.bounds.left);
|
||||
for (let index = 0; index < itemBounds.length; index++) {
|
||||
const { item, bounds } = itemBounds[index];
|
||||
if (bounds.left < laneBounds.left - WIDTH_TOLERANCE || bounds.right > laneBounds.right + WIDTH_TOLERANCE) {
|
||||
return false;
|
||||
}
|
||||
if (index > 0 && bounds.left < itemBounds[index - 1].bounds.right - WIDTH_TOLERANCE) {
|
||||
return false;
|
||||
}
|
||||
const preferredWidth = preferredLayout.itemWidths.get(item);
|
||||
if (preferredWidth !== undefined && bounds.width < preferredWidth - WIDTH_TOLERANCE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _measurePreferredLayout(items: readonly IChatInputPickerResponsiveLayoutItem[]): { width: number; itemWidths: ReadonlyMap<IChatInputPickerResponsiveLayoutItem, number> } {
|
||||
const parent = this._element.parentElement;
|
||||
if (!parent) {
|
||||
return { width: 0, itemWidths: new Map() };
|
||||
}
|
||||
|
||||
const measurementHost = dom.$('.chat-input-picker-measurement-host');
|
||||
measurementHost.style.position = 'fixed';
|
||||
measurementHost.style.inset = '0 auto auto 0';
|
||||
measurementHost.style.width = '0';
|
||||
measurementHost.style.height = '0';
|
||||
measurementHost.style.overflow = 'hidden';
|
||||
measurementHost.style.contain = 'strict';
|
||||
measurementHost.style.visibility = 'hidden';
|
||||
measurementHost.style.pointerEvents = 'none';
|
||||
|
||||
const measurement = this._element.cloneNode(true) as HTMLElement;
|
||||
measurement.setAttribute('aria-hidden', 'true');
|
||||
measurement.setAttribute('inert', '');
|
||||
measurement.style.position = 'absolute';
|
||||
measurement.style.left = '0';
|
||||
measurement.style.top = '0';
|
||||
measurement.style.width = 'max-content';
|
||||
measurement.style.minWidth = 'max-content';
|
||||
measurement.style.maxWidth = 'none';
|
||||
measurement.style.flex = 'none';
|
||||
measurementHost.appendChild(measurement);
|
||||
parent.appendChild(measurementHost);
|
||||
try {
|
||||
const itemWidths = new Map<IChatInputPickerResponsiveLayoutItem, number>();
|
||||
for (const item of items) {
|
||||
const path = item.element ? this._getElementPath(item.element) : undefined;
|
||||
const measuredItem = path ? this._getElementAtPath(measurement, path) : undefined;
|
||||
if (measuredItem) {
|
||||
measuredItem.style.flex = 'none';
|
||||
measuredItem.style.width = 'max-content';
|
||||
measuredItem.style.minWidth = 'max-content';
|
||||
measuredItem.style.maxWidth = 'none';
|
||||
itemWidths.set(item, measuredItem.getBoundingClientRect().width);
|
||||
}
|
||||
}
|
||||
return { width: measurement.getBoundingClientRect().width, itemWidths };
|
||||
} finally {
|
||||
measurementHost.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private _getElementPath(element: HTMLElement): readonly number[] | undefined {
|
||||
const path: number[] = [];
|
||||
let current: HTMLElement | null = element;
|
||||
while (current && current !== this._element) {
|
||||
const parent: HTMLElement | null = current.parentElement;
|
||||
if (!parent) {
|
||||
return undefined;
|
||||
}
|
||||
const index = Array.from(parent.children).indexOf(current);
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
path.unshift(index);
|
||||
current = parent;
|
||||
}
|
||||
return current === this._element ? path : undefined;
|
||||
}
|
||||
|
||||
private _getElementAtPath(root: HTMLElement, path: readonly number[]): HTMLElement | undefined {
|
||||
let current: Element = root;
|
||||
for (const index of path) {
|
||||
const child = current.children.item(index);
|
||||
if (!child) {
|
||||
return undefined;
|
||||
}
|
||||
current = child;
|
||||
}
|
||||
return dom.isHTMLElement(current) ? current : undefined;
|
||||
}
|
||||
|
||||
private _observeMutations(): void {
|
||||
this._mutationObserver.observe(this._element, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'hidden', 'style'],
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -294,8 +294,6 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
}
|
||||
|
||||
protected override renderLabel(element: HTMLElement): IDisposable | null {
|
||||
this.setAriaLabelAttributes(element);
|
||||
|
||||
const currentMode = this.delegate.currentMode.get();
|
||||
const state = currentMode.label.get();
|
||||
let icon = currentMode.icon.get();
|
||||
@@ -307,6 +305,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
|
||||
const labelElements = [];
|
||||
const collapsed = this.pickerOptions.compact.get();
|
||||
element.classList.toggle('icon-only', collapsed && !!icon);
|
||||
if (icon) {
|
||||
labelElements.push(...renderLabelWithIcons(`$(${getCompactCodicon(icon).id})`));
|
||||
}
|
||||
@@ -315,6 +314,8 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
}
|
||||
|
||||
dom.reset(element, ...labelElements);
|
||||
this.setAriaLabelAttributes(element);
|
||||
element.ariaLabel = state;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -51,14 +51,17 @@
|
||||
}
|
||||
|
||||
.chat-input-picker-item .action-label.model-picker-split .model-picker-name {
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.interactive-session .chat-input-toolbar .chat-input-picker-item.compact-picker .action-label.model-picker-split.compact {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.chat-input-picker-item .action-label.model-picker-split .model-picker-name .chat-input-picker-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.chat-input-picker-item .action-label.model-picker-split .model-picker-config {
|
||||
|
||||
+2
-2
@@ -128,8 +128,8 @@ export class ModelPickerActionItem extends BaseActionViewItem {
|
||||
this._showPicker();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this._showPicker();
|
||||
public show(anchor?: HTMLElement): void {
|
||||
this._pickerWidget.show(anchor ?? this._getAnchorElement());
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
|
||||
@@ -387,7 +387,11 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem {
|
||||
|
||||
const labelElements = [];
|
||||
labelElements.push(...renderLabelWithIcons(`$(${getCompactCodicon(icon).id})`));
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
|
||||
const compact = this.pickerOptions.compact.get();
|
||||
element.classList.toggle('icon-only', compact);
|
||||
if (!compact) {
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
|
||||
}
|
||||
|
||||
dom.reset(element, ...labelElements);
|
||||
element.classList.toggle('warning', !ext && (level === ChatPermissionLevel.Autopilot || level === ChatPermissionLevel.Assisted));
|
||||
|
||||
@@ -365,7 +365,6 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
}
|
||||
|
||||
protected override renderLabel(element: HTMLElement): IDisposable | null {
|
||||
this.setAriaLabelAttributes(element);
|
||||
const currentType = this._getSelectedSessionType() ?? this._getDefaultSessionType();
|
||||
|
||||
// TODO: Remove hardcoded providers from core
|
||||
@@ -377,9 +376,15 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
|
||||
const labelElements = [];
|
||||
labelElements.push(...renderLabelWithIcons(`$(${getCompactCodicon(icon).id})`));
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
|
||||
const compact = this.pickerOptions.compact.get();
|
||||
element.classList.toggle('icon-only', compact);
|
||||
if (!compact) {
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
|
||||
}
|
||||
|
||||
dom.reset(element, ...labelElements);
|
||||
this.setAriaLabelAttributes(element);
|
||||
element.ariaLabel = label;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -103,22 +103,23 @@ export class WorkspacePickerActionItem extends ChatInputPickerActionViewItem {
|
||||
}
|
||||
|
||||
protected override renderLabel(element: HTMLElement): IDisposable | null {
|
||||
this.setAriaLabelAttributes(element);
|
||||
const currentWorkspace = this.delegate.getSelectedWorkspace();
|
||||
|
||||
const labelElements: (string | HTMLElement)[] = [];
|
||||
const label = currentWorkspace
|
||||
? currentWorkspace.label || basename(currentWorkspace.uri)
|
||||
: localize('selectWorkspace', "Workspace");
|
||||
const compact = this.pickerOptions.compact.get();
|
||||
element.classList.toggle('icon-only', compact);
|
||||
|
||||
if (currentWorkspace) {
|
||||
// Show the workspace label or folder name
|
||||
const label = currentWorkspace.label || basename(currentWorkspace.uri);
|
||||
labelElements.push(...renderLabelWithIcons(`$(folder-compact)`));
|
||||
labelElements.push(...renderLabelWithIcons(`$(folder-compact)`));
|
||||
if (!compact) {
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
|
||||
} else {
|
||||
labelElements.push(...renderLabelWithIcons(`$(folder-compact)`));
|
||||
labelElements.push(dom.$('span.chat-input-picker-label', undefined, localize('selectWorkspace', "Workspace")));
|
||||
}
|
||||
|
||||
dom.reset(element, ...labelElements);
|
||||
this.setAriaLabelAttributes(element);
|
||||
element.ariaLabel = label;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1843,6 +1843,14 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive-session .chat-secondary-toolbar .chat-responsive-picker-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.interactive-session .chat-secondary-toolbar .chat-secondary-generic-chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1850,10 +1858,14 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.interactive-session .chat-secondary-toolbar .chat-secondary-generic-chips:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive-session .chat-secondary-toolbar .chat-secondary-input-toolbar {
|
||||
overflow: hidden;
|
||||
min-width: 0px;
|
||||
flex: 1 1 0;
|
||||
flex: 0 1 auto;
|
||||
color: var(--vscode-icon-foreground);
|
||||
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
@@ -1865,16 +1877,19 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
|
||||
.chat-input-picker-item {
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
|
||||
.action-label {
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
.chat-input-picker-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codicon + .chat-input-picker-label {
|
||||
@@ -1945,16 +1960,19 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
|
||||
.chat-input-picker-item {
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
|
||||
.action-label {
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
.chat-input-picker-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-picker-badge {
|
||||
@@ -2020,11 +2038,12 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
/* When chevrons are hidden and only showing an icon (no label), size to 22x22 with centered icon */
|
||||
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.compact:not(:has(.chat-input-picker-label)),
|
||||
.interactive-session .chat-input-toolbar .chat-input-picker-item.compact .action-label:not(:has(.chat-input-picker-label)),
|
||||
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.compact:not(:has(.chat-input-picker-label)),
|
||||
.interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label.compact:not(:has(.chat-input-picker-label)) {
|
||||
/* When only the icon remains, keep the expanded control's leading inset so
|
||||
* the glyph does not move as the label disappears. */
|
||||
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.icon-only,
|
||||
.interactive-session .chat-secondary-input-toolbar .chat-input-picker-item .action-label.icon-only,
|
||||
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.icon-only,
|
||||
.interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label.icon-only {
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
@@ -2039,6 +2058,18 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.icon-only:not(.model-picker-split),
|
||||
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.icon-only {
|
||||
padding-left: var(--vscode-spacing-size60);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.interactive-session .chat-secondary-input-toolbar .chat-input-picker-item .action-label.icon-only,
|
||||
.interactive-session .chat-secondary-input-toolbar .chat-sessionPicker-item .action-label.icon-only {
|
||||
padding-left: var(--vscode-spacing-size80);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
/* Icon-only chips in the primary input toolbar (add context, configure tools,
|
||||
MCP servers) all sit on the compact tier, so the row reads as one dense strip
|
||||
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import * as dom from '../../../../../../../base/browser/dom.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
|
||||
import { ChatInputPickerResponsiveLayout } from '../../../../browser/widget/input/chatInputPickerResponsiveLayout.js';
|
||||
import '../../../../browser/widget/input/modelPicker/media/modelPicker.css';
|
||||
import '../../../../browser/widget/media/chat.css';
|
||||
|
||||
suite('ChatInputPickerResponsiveLayout', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
let host: HTMLElement;
|
||||
|
||||
setup(() => {
|
||||
host = dom.append(document.body, dom.$('.chat-input-picker-responsive-layout-test'));
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('uses the rendered picker width instead of a viewport threshold', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
lane.style.display = 'flex';
|
||||
lane.style.width = '120px';
|
||||
lane.style.overflow = 'hidden';
|
||||
|
||||
const picker = dom.append(lane, dom.$('.picker'));
|
||||
picker.style.flex = '0 0 auto';
|
||||
picker.style.width = '240px';
|
||||
|
||||
let compact = false;
|
||||
let expandedWidth = 240;
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.pickerLane', lane, {
|
||||
getItems: () => [{
|
||||
element: picker,
|
||||
isCompact: () => compact,
|
||||
setCompact: value => {
|
||||
compact = value;
|
||||
picker.style.width = value ? '20px' : `${expandedWidth}px`;
|
||||
},
|
||||
}],
|
||||
}));
|
||||
|
||||
layout.layout();
|
||||
const narrow = compact;
|
||||
|
||||
lane.style.width = '300px';
|
||||
layout.layout();
|
||||
const expandedAfterLaneGrows = compact;
|
||||
|
||||
lane.style.width = '120px';
|
||||
expandedWidth = 80;
|
||||
layout.layout();
|
||||
const wideEnoughForCurrentItems = compact;
|
||||
|
||||
assert.deepStrictEqual({ narrow, expandedAfterLaneGrows, wideEnoughForCurrentItems }, {
|
||||
narrow: true,
|
||||
expandedAfterLaneGrows: false,
|
||||
wideEnoughForCurrentItems: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('compacts picker items from right to left until the lane fits', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
lane.style.display = 'flex';
|
||||
lane.style.width = '180px';
|
||||
lane.style.overflow = 'hidden';
|
||||
|
||||
const compact = [false, false, false];
|
||||
const pickers = compact.map((_, index) => {
|
||||
const picker = dom.append(lane, dom.$(`.picker-${index}`));
|
||||
picker.style.flex = '0 0 auto';
|
||||
picker.style.width = '80px';
|
||||
return picker;
|
||||
});
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.progressivePickerLane', lane, {
|
||||
getItems: () => pickers.map((picker, index) => ({
|
||||
element: picker,
|
||||
isCompact: () => compact[index],
|
||||
setCompact: value => {
|
||||
compact[index] = value;
|
||||
picker.style.width = value ? '20px' : '80px';
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
layout.layout();
|
||||
const firstCollision = [...compact];
|
||||
|
||||
lane.style.width = '130px';
|
||||
layout.layout();
|
||||
const secondCollision = [...compact];
|
||||
|
||||
lane.style.width = '240px';
|
||||
layout.layout();
|
||||
const expanded = [...compact];
|
||||
|
||||
assert.deepStrictEqual({ firstCollision, secondCollision, expanded }, {
|
||||
firstCollision: [false, false, true],
|
||||
secondCollision: [false, true, true],
|
||||
expanded: [false, false, false],
|
||||
});
|
||||
});
|
||||
|
||||
test('treats an empty picker set as fully compact', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.emptyPickerLane', lane, {
|
||||
getItems: () => [],
|
||||
}));
|
||||
|
||||
assert.strictEqual(layout.areAllItemsCompact(), true);
|
||||
});
|
||||
|
||||
test('ignores mutations outside the responsive picker container', async () => {
|
||||
const row = dom.append(host, dom.$('.secondary-row'));
|
||||
const lane = dom.append(row, dom.$('.responsive-picker-container'));
|
||||
const picker = dom.append(lane, dom.$('.picker'));
|
||||
const unrelated = dom.append(row, dom.$('.context-usage'));
|
||||
lane.style.width = '100px';
|
||||
lane.style.height = '20px';
|
||||
let compact = false;
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.isolatedPickerLane', lane, {
|
||||
getItems: () => [{
|
||||
element: picker,
|
||||
isCompact: () => compact,
|
||||
setCompact: value => compact = value,
|
||||
}],
|
||||
}));
|
||||
|
||||
let layoutCalls = 0;
|
||||
layout.layout = () => layoutCalls++;
|
||||
const targetWindow = dom.getWindow(lane);
|
||||
await new Promise<void>(resolve => targetWindow.requestAnimationFrame(() => targetWindow.requestAnimationFrame(() => resolve())));
|
||||
layoutCalls = 0;
|
||||
unrelated.textContent = 'streamed cost update';
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
const afterUnrelatedMutation = layoutCalls;
|
||||
|
||||
picker.textContent = 'picker changed';
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
assert.strictEqual(afterUnrelatedMutation, 0);
|
||||
assert.ok(layoutCalls > 0);
|
||||
});
|
||||
|
||||
test('restores overflowed actions in compact form before considering expanded labels', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
lane.style.display = 'flex';
|
||||
lane.style.width = '50px';
|
||||
lane.style.overflow = 'hidden';
|
||||
|
||||
const actionBar = dom.append(lane, dom.$('.monaco-action-bar.has-overflow'));
|
||||
const picker = dom.append(actionBar, dom.$('.picker'));
|
||||
let compact = false;
|
||||
let overflow = true;
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.overflowedPickerLane', lane, {
|
||||
getItems: () => [{
|
||||
element: picker,
|
||||
isCompact: () => compact,
|
||||
setCompact: value => {
|
||||
compact = value;
|
||||
picker.style.width = value ? '60px' : '150px';
|
||||
},
|
||||
}],
|
||||
hasOverflow: () => overflow,
|
||||
relayout: () => {
|
||||
overflow = picker.getBoundingClientRect().width > lane.getBoundingClientRect().width;
|
||||
},
|
||||
}));
|
||||
|
||||
layout.layout();
|
||||
const tooNarrowForCompact = { compact, overflow };
|
||||
|
||||
lane.style.width = '70px';
|
||||
layout.layout();
|
||||
const compactItemsRestored = { compact, overflow };
|
||||
|
||||
lane.style.width = '160px';
|
||||
layout.layout();
|
||||
const expanded = { compact, overflow };
|
||||
|
||||
assert.deepStrictEqual({ tooNarrowForCompact, compactItemsRestored, expanded }, {
|
||||
tooNarrowForCompact: { compact: true, overflow: true },
|
||||
compactItemsRestored: { compact: true, overflow: false },
|
||||
expanded: { compact: false, overflow: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('compacts a picker whose rendered bounds escape the lane', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
lane.style.display = 'flex';
|
||||
lane.style.width = '100px';
|
||||
lane.style.overflow = 'visible';
|
||||
|
||||
const picker = dom.append(lane, dom.$('.picker'));
|
||||
picker.style.flex = '0 0 auto';
|
||||
picker.style.width = '80px';
|
||||
picker.style.transform = 'translateX(50px)';
|
||||
let compact = false;
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.visuallyOverflowedPickerLane', lane, {
|
||||
getItems: () => [{
|
||||
element: picker,
|
||||
isCompact: () => compact,
|
||||
setCompact: value => {
|
||||
compact = value;
|
||||
picker.style.width = value ? '20px' : '80px';
|
||||
},
|
||||
}],
|
||||
}));
|
||||
|
||||
layout.layout();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
compact,
|
||||
measurementHosts: host.querySelectorAll('.chat-input-picker-measurement-host').length,
|
||||
}, {
|
||||
compact: true,
|
||||
measurementHosts: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('compacts an expanded picker before its label truncates', () => {
|
||||
const lane = dom.append(host, dom.$('.picker-lane'));
|
||||
lane.style.display = 'flex';
|
||||
lane.style.width = '200px';
|
||||
|
||||
const picker = dom.append(lane, dom.$('.picker'));
|
||||
picker.style.flex = '0 1 80px';
|
||||
picker.style.width = '80px';
|
||||
picker.style.overflow = 'hidden';
|
||||
const label = dom.append(picker, dom.$('.picker-label'));
|
||||
label.style.display = 'block';
|
||||
label.style.width = '140px';
|
||||
label.textContent = 'A picker label that would otherwise ellipsize';
|
||||
|
||||
let compact = false;
|
||||
const layout = store.add(new ChatInputPickerResponsiveLayout('test.truncatedPickerLane', lane, {
|
||||
getItems: () => [{
|
||||
element: picker,
|
||||
isCompact: () => compact,
|
||||
setCompact: value => {
|
||||
compact = value;
|
||||
picker.style.width = value ? '20px' : '80px';
|
||||
label.style.display = value ? 'none' : '';
|
||||
},
|
||||
}],
|
||||
}));
|
||||
|
||||
layout.layout();
|
||||
|
||||
assert.strictEqual(compact, true);
|
||||
});
|
||||
|
||||
test('keeps the toolbar row height stable when the model picker overflows', () => {
|
||||
host.style.setProperty('--vscode-spacing-size40', '4px');
|
||||
host.style.setProperty('--vscode-spacing-size60', '6px');
|
||||
host.classList.add('interactive-session');
|
||||
|
||||
const row = dom.append(host, dom.$('.picker-row.chat-input-toolbar'));
|
||||
row.style.display = 'flex';
|
||||
row.style.alignItems = 'center';
|
||||
|
||||
const modelItem = dom.append(row, dom.$('.chat-input-picker-item'));
|
||||
const modelLabel = dom.append(modelItem, dom.$('a.action-label.model-picker-split'));
|
||||
const modelName = dom.append(modelLabel, dom.$('.model-picker-section.model-picker-name'));
|
||||
const pickerLabel = dom.append(modelName, dom.$('.chat-input-picker-label'));
|
||||
|
||||
const overflowItem = dom.append(row, dom.$('.overflow-item'));
|
||||
overflowItem.style.width = '22px';
|
||||
overflowItem.style.height = '22px';
|
||||
overflowItem.style.display = 'none';
|
||||
|
||||
const withModelPicker = row.getBoundingClientRect().height;
|
||||
const expandedIconOffset = modelName.getBoundingClientRect().left - modelLabel.getBoundingClientRect().left;
|
||||
modelLabel.style.width = '22px';
|
||||
modelItem.classList.add('compact-picker');
|
||||
modelLabel.classList.add('compact');
|
||||
const compactIconOffset = modelName.getBoundingClientRect().left - modelLabel.getBoundingClientRect().left;
|
||||
modelItem.style.display = 'none';
|
||||
overflowItem.style.display = '';
|
||||
const withOverflow = row.getBoundingClientRect().height;
|
||||
|
||||
assert.deepStrictEqual({
|
||||
withModelPicker,
|
||||
withOverflow,
|
||||
modelNameFlexShrink: dom.getWindow(modelName).getComputedStyle(modelName).flexShrink,
|
||||
labelTextOverflow: dom.getWindow(pickerLabel).getComputedStyle(pickerLabel).textOverflow,
|
||||
expandedIconOffset,
|
||||
compactIconOffset,
|
||||
}, {
|
||||
withModelPicker: 22,
|
||||
withOverflow: 22,
|
||||
modelNameFlexShrink: '0',
|
||||
labelTextOverflow: 'clip',
|
||||
expandedIconOffset: 0,
|
||||
compactIconOffset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the primary picker icon anchored when its label disappears', () => {
|
||||
host.style.setProperty('--vscode-spacing-size60', '6px');
|
||||
host.classList.add('interactive-session');
|
||||
const toolbar = dom.append(host, dom.$('.chat-input-toolbar'));
|
||||
const item = dom.append(toolbar, dom.$('.chat-input-picker-item'));
|
||||
const actionLabel = dom.append(item, dom.$('a.action-label'));
|
||||
const icon = dom.append(actionLabel, dom.$('span.codicon'));
|
||||
icon.style.width = '16px';
|
||||
icon.style.height = '16px';
|
||||
const pickerLabel = dom.append(actionLabel, dom.$('span.chat-input-picker-label'));
|
||||
pickerLabel.textContent = 'Picker';
|
||||
|
||||
const expandedOffset = icon.getBoundingClientRect().left - actionLabel.getBoundingClientRect().left;
|
||||
item.classList.add('compact');
|
||||
actionLabel.classList.add('icon-only');
|
||||
pickerLabel.remove();
|
||||
const compactOffset = icon.getBoundingClientRect().left - actionLabel.getBoundingClientRect().left;
|
||||
|
||||
assert.deepStrictEqual({ expandedOffset, compactOffset }, {
|
||||
expandedOffset: 6,
|
||||
compactOffset: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,10 +79,10 @@
|
||||

|
||||
|
||||
#### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark
|
||||

|
||||

|
||||
|
||||
#### editor/inlineChatZoneWidget/InlineChatZoneWidget/Light
|
||||

|
||||

|
||||
|
||||
#### editor/inlineChatZoneWidget/InlineChatZoneWidgetTerminated/Dark
|
||||

|
||||
|
||||
Reference in New Issue
Block a user