diff --git a/src/vs/sessions/contrib/chat/browser/media/chatInput.css b/src/vs/sessions/contrib/chat/browser/media/chatInput.css index 8b9d5d51bd7d13..1e83b178f923ce 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatInput.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatInput.css @@ -251,6 +251,7 @@ .sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker { box-sizing: border-box; width: 22px; + height: 22px; min-width: 22px; padding: 0; } @@ -258,9 +259,10 @@ .sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker .action-label { box-sizing: border-box; width: 22px; + height: 22px; min-width: 22px; - padding: 2px 2px 2px 8px; - justify-content: flex-start; + padding: 0; + justify-content: center; } .sessions-chat-config-toolbar .monaco-action-bar .action-item.compact-picker .action-label.model-picker-split { diff --git a/src/vs/sessions/contrib/chat/browser/media/chatView.css b/src/vs/sessions/contrib/chat/browser/media/chatView.css index c48a05618ed109..85dfa9afa1930c 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatView.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatView.css @@ -253,3 +253,22 @@ .agent-sessions-workbench .interactive-session .compact-picker .sessions-chat-dropdown-label { display: none; } + +.agent-sessions-workbench .interactive-session .compact-picker .sessions-chat-picker-slot, +.agent-sessions-workbench .interactive-session .sessions-chat-picker-slot.compact-picker, +.agent-sessions-workbench .interactive-session .compact-picker .sessions-chat-picker-slot .action-label, +.agent-sessions-workbench .interactive-session .sessions-chat-picker-slot.compact-picker .action-label { + box-sizing: border-box; + width: 22px; + height: 22px; + min-width: 22px; + padding: 0; + justify-content: center; +} + +.agent-sessions-workbench .interactive-session .compact-picker .sessions-chat-picker-slot .action-label > .codicon, +.agent-sessions-workbench .interactive-session .sessions-chat-picker-slot.compact-picker .action-label > .codicon { + width: var(--vscode-codiconFontSize-compact); + height: var(--vscode-codiconFontSize-compact); + line-height: var(--vscode-codiconFontSize-compact); +} diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index 26008a8a05829e..9d75adee29b29a 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -89,6 +89,12 @@ align-self: center; } +/* Flex centering shares attachment growth above and below the midpoint. Offset + * that growth so the bottom controls stay anchored and the composer grows up. */ +.sessions-chat-widget:not(.new-chat-in-session) > .new-chat-widget-container > .new-chat-widget-content { + position: relative; +} + .new-chat-widget-container .new-chat-bottom-container { width: 100%; max-width: 800px; @@ -164,6 +170,7 @@ .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; + height: 22px; min-width: 22px; padding: 0; } @@ -172,9 +179,10 @@ .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; + height: 22px; min-width: 22px; - justify-content: flex-start; - padding: 2px 2px 2px 8px; + justify-content: center; + padding: 0; } .agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .compact-picker.sessions-chat-picker-slot .action-label > .codicon { @@ -220,20 +228,25 @@ display: none; } -/* Icon-only action items in the bottom row (e.g. OpenTelemetry status pill) - * lack the chevron that visually balances the 7px left padding, so use - * symmetric horizontal padding and drop the picker min-width that would - * otherwise leave the icon left-aligned inside a 30px box. */ -.new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .monaco-action-bar .action-item, -.new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .monaco-action-bar .action-item .action-label { - min-width: 0; +/* Icon status actions use the compact control box; text actions remain intrinsic. */ +.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .monaco-action-bar .action-item.new-chat-status-icon-action { + width: 22px; + min-width: 22px; } -.new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .action-label { - padding: 3px 4px; +.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .new-chat-status-icon-action .action-label { + box-sizing: border-box; display: flex; align-items: center; justify-content: center; + width: 22px; + height: 22px; + min-width: 22px; + padding: 0; +} + +.agent-sessions-workbench:not(.phone-layout) .new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar .new-chat-status-icon-action .action-label.codicon { + font-size: var(--vscode-codiconFontSize-compact); } /* The category row is quiet at rest, while hover/open state reveals the active target. */ diff --git a/src/vs/sessions/contrib/chat/browser/newChatInput.ts b/src/vs/sessions/contrib/chat/browser/newChatInput.ts index 6ada9420d4d1ed..03955581cd7814 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatInput.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatInput.ts @@ -98,6 +98,7 @@ import { IChatInputNoticeHubService } from '../../../../workbench/contrib/chat/b import { ChatInputPickerResponsiveLayout, IChatInputPickerResponsiveLayoutItem } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerResponsiveLayout.js'; import { chatInputStackClass, chatInputStackSlotClass, ChatInputStackSlot, refreshChatInputStack, setChatInputStackInputFocused, setChatInputStackSlot } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputStack.js'; import { IChatSubmitRequestHandlerService } from '../../../../workbench/contrib/chat/browser/chatSubmitRequestHandlerService.js'; +import { isPhoneLayout } from '../../../browser/parts/mobile/mobileLayout.js'; import { INewChatModelPickerService, NewChatModelPickerService } from './newChatModelPicker.js'; import { ModelPicker, ModelPickerActionViewItem } from './modelPicker.js'; import { ISessionModelSelection, SessionModelSelection } from './sessionModelSelection.js'; @@ -106,6 +107,7 @@ import { ISessionContext, SessionContext } from '../../../services/sessions/brow import { AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING } from './sessionsChatHistory.js'; import { IChatStatusItemService } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusItemService.js'; import { handleTerminalCommandPaste, isTerminalCommandInput } from '../../../../workbench/contrib/chat/browser/chatTerminalCommandPaste.js'; +import { compactCodiconsIn } from '../../../../workbench/contrib/chat/browser/chatIcons.js'; import { IChatPasteTargetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { ChatDynamicVariableModel } from '../../../../workbench/contrib/chat/browser/attachments/chatDynamicVariables.js'; import { NewChatInputPasteTarget } from './newChatInputPasteTarget.js'; @@ -220,6 +222,8 @@ export function hasSendableNewChatContent(query: string, attachments: readonly I class NewChatInputStatusActionViewItem extends MenuEntryActionViewItem { private readonly hoverContentDisposables = this._register(new MutableDisposable()); + private _container: HTMLElement | undefined; + private _compactCodiconClass: string | undefined; constructor( action: MenuItemAction, @@ -238,7 +242,9 @@ class NewChatInputStatusActionViewItem extends MenuEntryActionViewItem { } override render(container: HTMLElement): void { + this._container = container; super.render(container); + this._updateIconPresentation(); if (this._commandAction.id !== OTEL_STATUS_COMMAND) { return; @@ -251,6 +257,34 @@ class NewChatInputStatusActionViewItem extends MenuEntryActionViewItem { })); } + protected override updateClass(): void { + if (this._compactCodiconClass) { + this.label?.classList.remove(this._compactCodiconClass); + this._compactCodiconClass = undefined; + } + super.updateClass(); + this._updateIconPresentation(); + } + + private _updateIconPresentation(): void { + const rendersIcon = !!this.label && (this.label.classList.contains('codicon') || this.label.classList.contains('icon')); + this._container?.classList.toggle('new-chat-status-icon-action', rendersIcon); + if (rendersIcon && this._container && this.label?.classList.contains('codicon')) { + const originalCodiconClass = this._getCodiconClass(); + compactCodiconsIn(this._container); + const compactCodiconClass = this._getCodiconClass(); + if (compactCodiconClass !== originalCodiconClass) { + this._compactCodiconClass = compactCodiconClass; + } + } + } + + private _getCodiconClass(): string | undefined { + return this.label + ? [...this.label.classList].find(className => className.startsWith('codicon-') && !className.startsWith('codicon-modifier-')) + : undefined; + } + override async onClick(event: MouseEvent): Promise { if (this._commandAction.id === OTEL_STATUS_COMMAND && this.element) { event.preventDefault(); @@ -444,6 +478,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation private readonly _compactModelPicker = observableValue(this, false); private _primaryPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined; private _secondaryPickerResponsiveLayout: ChatInputPickerResponsiveLayout | undefined; + private _updateAttachmentOffset: (() => void) | undefined; // Input state private _draftState: IDraftState | undefined = { @@ -668,6 +703,24 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation const attachRow = dom.append(inputArea, dom.$('.sessions-chat-attach-row')); const attachedContextContainer = dom.append(attachRow, dom.$('.sessions-chat-attached-context')); this._contextAttachments.renderAttachedContext(attachedContextContainer); + const updateAttachmentOffset = () => { + if (isPhoneLayout(this.layoutService)) { + parent.style.removeProperty('top'); + return; + } + parent.style.top = `${-attachRow.getBoundingClientRect().height / 2}px`; + }; + this._updateAttachmentOffset = updateAttachmentOffset; + const attachmentResizeObserver = this._register(new dom.DisposableResizeObserver( + 'NewChatInputWidget.attachments', + updateAttachmentOffset, + dom.getWindow(attachRow), + )); + this._register(attachmentResizeObserver.observe(attachRow)); + this._register(toDisposable(() => { + this._updateAttachmentOffset = undefined; + parent.style.removeProperty('top'); + })); this._register(this.instantiationService.createInstance(ChatDragAndDrop, () => undefined, { get attachments() { return contextAttachments.attachments; }, addAttachments: (entries: readonly IChatRequestVariableEntry[]) => contextAttachments.addAttachments(...entries), @@ -737,7 +790,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { primaryGroup: () => true }, actionViewItemProvider: (action, options) => { - if (action.id === OTEL_STATUS_COMMAND && action instanceof MenuItemAction) { + if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(NewChatInputStatusActionViewItem, action, options); } return undefined; @@ -1174,13 +1227,14 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation if (!element) { continue; } + const isModelPicker = element.classList.contains('model-picker-item'); items.push({ element, - canShrink: configToolbar.getItemAction(index)?.id === 'sessions.modelPicker', + canShrink: isModelPicker, isCompact: () => element.classList.contains('compact-picker'), setCompact: (compact: boolean) => { element.classList.toggle('compact-picker', compact); - if (configToolbar.getItemAction(index)?.id === 'sessions.modelPicker') { + if (isModelPicker) { this._compactModelPicker.set(compact, undefined); } }, @@ -1617,6 +1671,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation } layout(_height: number, _width: number): void { + this._updateAttachmentOffset?.(); this._editor?.layout(); this._primaryPickerResponsiveLayout?.layout(); this._secondaryPickerResponsiveLayout?.layout(); diff --git a/src/vs/sessions/contrib/chat/test/browser/chatInput.fixture.ts b/src/vs/sessions/contrib/chat/test/browser/chatInput.fixture.ts index 7f30a32c3d6ddb..fe8f8a08662beb 100644 --- a/src/vs/sessions/contrib/chat/test/browser/chatInput.fixture.ts +++ b/src/vs/sessions/contrib/chat/test/browser/chatInput.fixture.ts @@ -102,6 +102,8 @@ export default defineThemedFixtureGroup({ path: 'sessions/chat/input/' }, { }) }), ResponsiveModelResizeCycleCompact: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The Agents active-session chat input shows its compact model codicon centered with equal padding inside a 22-pixel square control while the model configuration remains visible.'], virtualTime: { enabled: false }, render: context => renderChatInput(sessionsWindowContext(context), { isSessionsWindow: true, @@ -111,6 +113,8 @@ export default defineThemedFixtureGroup({ path: 'sessions/chat/input/' }, { }) }), ResponsiveModelResizeCycleMinimal: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The Agents active-session chat input shows compact model and permission codicons centered with equal padding inside matching 22-pixel square controls, aligned with the expanded toolbar height.'], virtualTime: { enabled: false }, render: context => renderChatInput(sessionsWindowContext(context), { isSessionsWindow: true, diff --git a/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts b/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts index 51a09a38f89071..b9fe6aa6912190 100644 --- a/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts @@ -84,28 +84,39 @@ suite('Sessions - Chat View', () => { const picker = dom.append(item, dom.$('.action-label.model-picker-split.compact')); const name = dom.append(picker, dom.$('.model-picker-section.model-picker-name')); name.style.minWidth = '22px'; - dom.append(name, dom.$('span.codicon')); + const icon = dom.append(name, dom.$('span.codicon')); + icon.style.width = '12px'; + icon.style.height = '12px'; const config = dom.append(picker, dom.$('.model-picker-section.model-picker-config')); const configLabel = dom.append(config, dom.$('span.chat-input-picker-label')); configLabel.textContent = 'High'; + const nameBounds = name.getBoundingClientRect(); + const iconBounds = icon.getBoundingClientRect(); assert.deepStrictEqual({ configVisible: dom.getWindow(configLabel).getComputedStyle(configLabel).display !== 'none', configWidth: config.getBoundingClientRect().width > 0, - nameWidth: name.getBoundingClientRect().width, + name: { width: nameBounds.width, height: nameBounds.height }, + iconOffset: { + x: iconBounds.left - nameBounds.left, + y: iconBounds.top - nameBounds.top, + }, }, { configVisible: true, configWidth: true, - nameWidth: 22, + name: { width: 22, height: 22 }, + iconOffset: { x: 5, y: 5 }, }); }); - 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())); + test('centers compact empty-state picker icons inside their action item', () => { + const inputPart = dom.append(document.body, dom.$('.interactive-input-part')); + disposables.add(toDisposable(() => inputPart.remove())); + const toolbar = dom.append(inputPart, dom.$('.sessions-chat-config-toolbar')); 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 slot = dom.append(item, dom.$('.sessions-chat-picker-slot')); + const label = dom.append(slot, dom.$('a.action-label')); const icon = dom.append(label, dom.$('span.codicon')); icon.style.width = '12px'; icon.style.height = '12px'; @@ -114,17 +125,22 @@ suite('Sessions - Chat View', () => { const labelBounds = label.getBoundingClientRect(); const iconBounds = icon.getBoundingClientRect(); assert.deepStrictEqual({ - labelOffset: labelBounds.left - itemBounds.left, - iconOffset: iconBounds.left - itemBounds.left, + item: { width: itemBounds.width, height: itemBounds.height }, + label: { width: labelBounds.width, height: labelBounds.height }, + iconOffset: { + x: iconBounds.left - labelBounds.left, + y: iconBounds.top - labelBounds.top, + }, iconEscapes: iconBounds.left < itemBounds.left || iconBounds.right > itemBounds.right, }, { - labelOffset: 0, - iconOffset: 8, + item: { width: 22, height: 22 }, + label: { width: 22, height: 22 }, + iconOffset: { x: 5, y: 5 }, iconEscapes: false, }); }); - test('keeps compact bottom-row picker glyphs inside their action item', () => { + test('centers 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'); @@ -132,31 +148,121 @@ suite('Sessions - Chat View', () => { 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 slot = dom.append(item, dom.$('.sessions-chat-picker-slot.compact-picker')); + const label = dom.append(slot, 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 slotBounds = slot.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, + item: { width: itemBounds.width, height: itemBounds.height }, + slot: { width: slotBounds.width, height: slotBounds.height }, + label: { width: labelBounds.width, height: labelBounds.height }, + icon: { width: iconBounds.width, height: iconBounds.height }, + iconOffset: { + x: iconBounds.left - labelBounds.left, + y: iconBounds.top - labelBounds.top, + }, iconEscapes: iconBounds.left < itemBounds.left || iconBounds.right > itemBounds.right, }, { - itemWidth: 22, - labelWidth: 22, - labelOffset: 0, - iconWidth: 12, - iconOffset: 8, + item: { width: 22, height: 22 }, + slot: { width: 22, height: 22 }, + label: { width: 22, height: 22 }, + icon: { width: 12, height: 12 }, + iconOffset: { x: 5, y: 5 }, iconEscapes: false, }); }); + test('uses the compact control box for bottom-row status icons', () => { + 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 statusToolbar = dom.append(row, dom.$('.new-chat-status-toolbar')); + const actionBar = dom.append(statusToolbar, dom.$('.monaco-action-bar')); + const item = dom.append(actionBar, dom.$('.action-item.new-chat-status-icon-action')); + const label = dom.append(item, dom.$('a.action-label.codicon.codicon-warning')); + + const itemBounds = item.getBoundingClientRect(); + const labelBounds = label.getBoundingClientRect(); + assert.deepStrictEqual({ + item: { width: itemBounds.width, height: itemBounds.height }, + label: { width: labelBounds.width, height: labelBounds.height }, + iconFontSize: dom.getWindow(label).getComputedStyle(label).fontSize, + }, { + item: { width: 22, height: 22 }, + label: { width: 22, height: 22 }, + iconFontSize: '12px', + }); + }); + + test('leaves text-only bottom-row status actions at their intrinsic width', () => { + const workbench = dom.append(document.body, dom.$('.agent-sessions-workbench')); + disposables.add(toDisposable(() => workbench.remove())); + const widget = dom.append(workbench, dom.$('.new-chat-widget-container.revealed')); + const row = dom.append(widget, dom.$('.new-chat-bottom-container')); + const statusToolbar = dom.append(row, dom.$('.new-chat-status-toolbar')); + const actionBar = dom.append(statusToolbar, dom.$('.monaco-action-bar')); + const item = dom.append(actionBar, dom.$('.action-item')); + const label = dom.append(item, dom.$('a.action-label')); + label.textContent = 'Status'; + + assert.deepStrictEqual({ + itemIsSquareIconAction: item.classList.contains('new-chat-status-icon-action'), + itemWiderThanCompactControl: item.getBoundingClientRect().width > 22, + labelIsNotClipped: label.scrollWidth <= label.clientWidth, + text: label.textContent, + }, { + itemIsSquareIconAction: false, + itemWiderThanCompactControl: true, + labelIsNotClipped: true, + text: 'Status', + }); + }); + + test('centers compact in-session 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 session = dom.append(workbench, dom.$('.interactive-session')); + const toolbar = dom.append(session, dom.$('.chat-secondary-input-toolbar')); + const actionBar = dom.append(toolbar, dom.$('.monaco-action-bar')); + const actionsContainer = dom.append(actionBar, dom.$('.actions-container')); + actionsContainer.style.display = 'flex'; + const item = dom.append(actionsContainer, dom.$('.action-item.compact-picker')); + const slot = dom.append(item, dom.$('.sessions-chat-picker-slot')); + const label = dom.append(slot, dom.$('a.action-label')); + const icon = dom.append(label, dom.$('span.codicon')); + dom.append(label, dom.$('span.sessions-chat-dropdown-label', undefined, 'Autopilot')); + + const itemBounds = item.getBoundingClientRect(); + const slotBounds = slot.getBoundingClientRect(); + const labelBounds = label.getBoundingClientRect(); + const iconBounds = icon.getBoundingClientRect(); + assert.deepStrictEqual({ + item: { width: itemBounds.width, height: itemBounds.height }, + slot: { width: slotBounds.width, height: slotBounds.height }, + label: { width: labelBounds.width, height: labelBounds.height }, + icon: { + width: iconBounds.width, + height: iconBounds.height, + x: iconBounds.left - labelBounds.left, + y: iconBounds.top - labelBounds.top, + }, + }, { + item: { width: 22, height: 22 }, + slot: { width: 22, height: 22 }, + label: { width: 22, height: 22 }, + icon: { width: 12, height: 12, x: 5, y: 5 }, + }); + }); + test('keeps the voice toolbar visible when picker actions run out of space', () => { const session = dom.append(document.body, dom.$('.interactive-session')); disposables.add(toDisposable(() => session.remove())); diff --git a/src/vs/sessions/contrib/chat/test/browser/newChatWidget.fixture.ts b/src/vs/sessions/contrib/chat/test/browser/newChatWidget.fixture.ts index 7356e20a375686..0e688274862205 100644 --- a/src/vs/sessions/contrib/chat/test/browser/newChatWidget.fixture.ts +++ b/src/vs/sessions/contrib/chat/test/browser/newChatWidget.fixture.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { assert } from '../../../../../base/common/assert.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -13,6 +14,10 @@ import { mock } from '../../../../../base/test/common/mock.js'; import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IRemoteAgentHostService } from '../../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -27,12 +32,15 @@ import { IAICustomizationWorkspaceService } from '../../../../../workbench/contr import { ICustomizationHarnessService } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; import { IChatRequestVariableEntry, toPasteVariableEntry } from '../../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { IPromptsService } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ChatAgentLocation } from '../../../../../workbench/contrib/chat/common/constants.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../../../../workbench/contrib/chat/common/languageModels.js'; import { IHistoryService } from '../../../../../workbench/services/history/common/history.js'; import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js'; import { ISearchService } from '../../../../../workbench/services/search/common/search.js'; -import { registerChatFixtureServices } from '../../../../../workbench/test/browser/componentFixtures/chat/chatFixtureUtils.js'; +import { FixtureMenuService, registerChatFixtureServices } from '../../../../../workbench/test/browser/componentFixtures/chat/chatFixtureUtils.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; import { activeSessionViewBackground } from '../../../../common/theme.js'; +import { Menus } from '../../../../browser/menus.js'; import { AgentHostFilterConnectionStatus, IAgentHostFilterService } from '../../../../services/agentHostFilter/common/agentHostFilter.js'; import { ISessionsChatBackgroundService } from '../../../../services/chatBackground/browser/chatBackgroundService.js'; import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js'; @@ -51,6 +59,7 @@ import { INewChatVoiceTargetService, NewChatVoiceTargetService } from '../../bro import '../../../../browser/media/style.css'; import '../../../../browser/parts/media/sessionView.css'; +import '../../../../browser/parts/mobile/mobileChatShell.css'; const DEFAULT_WIDTH = 800; const DEFAULT_HEIGHT = 560; @@ -71,6 +80,43 @@ interface INewChatWidgetFixtureOptions { readonly openWorkspacePicker?: boolean; readonly openGitHubContextPicker?: boolean; readonly withAttachedContext?: boolean; + readonly withAutoModel?: boolean; + readonly primaryToolbarWidth?: number; + readonly phoneLayout?: boolean; +} + +class AutoModelFixtureMenuService extends FixtureMenuService { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService, + ) { + super(contextKeyService, commandService); + this.addItem(Menus.NewSessionConfig, { + command: { id: 'sessions.modelPicker', title: 'Model' }, + group: 'navigation', + order: 1, + }); + this.addItem(MenuId.ChatInputStatus, { + command: { id: 'fixture.autopilotStatus', title: 'Autopilot', icon: Codicon.rocket }, + group: 'navigation', + order: 1, + }); + this.addItem(MenuId.ChatInputStatus, { + command: { id: 'fixture.warningStatus', title: 'Warning', icon: Codicon.warning }, + group: 'navigation', + order: 2, + }); + this.addItem(MenuId.ChatInputStatus, { + command: { id: 'fixture.connectionStatus', title: 'Connection', icon: Codicon.radioTower }, + group: 'navigation', + order: 3, + }); + this.addItem(MenuId.ChatInputStatus, { + command: { id: 'fixture.textStatus', title: 'Status' }, + group: 'navigation', + order: 4, + }); + } } /** @@ -99,6 +145,9 @@ async function renderNewChatWidget(context: ComponentFixtureContext, options: IN openWorkspacePicker = false, openGitHubContextPicker = false, withAttachedContext = false, + withAutoModel = false, + primaryToolbarWidth, + phoneLayout = false, } = options; const feedbackItems: readonly IAgentFeedback[] = Array.from({ length: commentCount }, (_, index) => ({ id: `feedback-${index}`, @@ -111,7 +160,7 @@ async function renderNewChatWidget(context: ComponentFixtureContext, options: IN })); const workspace = createFixtureWorkspace(withRemoteWorkspace); const sessionTypes = createFixtureSessionTypes(); - const provider = createFixtureProvider(workspace, sessionTypes); + const provider = createFixtureProvider(workspace, sessionTypes, withAutoModel ? [createFixtureAutoModel()] : []); const activeSession = promptOptions || withWorkspace || withRemoteWorkspace || withAttachedContext ? createFixtureActiveSession(workspace, sessionTypes[0]) : undefined; const activeSessionObservable = observableValue('activeSession', activeSession); const composerService = disposableStore.add(new NewSessionComposerService()); @@ -123,6 +172,9 @@ async function renderNewChatWidget(context: ComponentFixtureContext, options: IN colorTheme: context.theme, additionalServices: reg => { registerChatFixtureServices(reg); + if (withAutoModel) { + reg.define(IMenuService, AutoModelFixtureMenuService); + } reg.defineInstance(IUriIdentityService, new class extends mock() { override readonly extUri = extUri; }()); @@ -267,6 +319,7 @@ async function renderNewChatWidget(context: ComponentFixtureContext, options: IN container.style.width = `${width}px`; container.style.height = `${height}px`; container.classList.add('monaco-workbench', 'agent-sessions-workbench'); + container.classList.toggle('phone-layout', phoneLayout); const sessionView = dom.append(container, dom.$('.session-view.is-active')); sessionView.style.width = '100%'; @@ -282,15 +335,48 @@ async function renderNewChatWidget(context: ComponentFixtureContext, options: IN })); sessionViewContent.appendChild(view.element); view.layout(width, height, 0, 0); + const targetWindow = dom.getWindow(container); + const nextFrame = () => new Promise(resolve => targetWindow.requestAnimationFrame(() => resolve())); + await nextFrame(); + await nextFrame(); + if (phoneLayout && withAttachedContext) { + const content = view.element.querySelector('.new-chat-widget-content'); + assert(!!content); + assert(content.style.top === ''); + } + if (withAutoModel) { + const statusItems = [...view.element.querySelectorAll('.new-chat-status-toolbar .action-item')]; + const iconItems = statusItems.filter(item => item.classList.contains('new-chat-status-icon-action')); + assert(iconItems.length === 3); + assert(iconItems.some(item => item.querySelector('.codicon-rocket-compact'))); + assert(iconItems.some(item => item.querySelector('.codicon-warning-compact'))); + + const textLabel = statusItems + .filter(item => !item.classList.contains('new-chat-status-icon-action')) + .map(item => item.querySelector('.action-label')) + .find(label => label?.textContent === 'Status'); + assert(!!textLabel); + assert(textLabel.scrollWidth <= textLabel.clientWidth); + + if (phoneLayout) { + assert(iconItems.every(item => (item.querySelector('.action-label')?.getBoundingClientRect().width ?? 0) > 22)); + } + } + if (primaryToolbarWidth !== undefined) { + const toolbar = view.element.querySelector('.sessions-chat-config-toolbar'); + if (!toolbar) { + throw new Error('Expected the new-session primary toolbar to render.'); + } + toolbar.style.flex = `0 0 ${primaryToolbarWidth}px`; + toolbar.style.width = `${primaryToolbarWidth}px`; + await nextFrame(); + await nextFrame(); + } if (openWorkspacePicker) { - const targetWindow = dom.getWindow(container); - const nextFrame = () => new Promise(resolve => targetWindow.requestAnimationFrame(() => resolve())); await nextFrame(); await nextFrame(); view.element.querySelector('.sessions-workspace-picker-trigger .action-label')?.click(); } else if (openGitHubContextPicker) { - const targetWindow = dom.getWindow(container); - const nextFrame = () => new Promise(resolve => targetWindow.requestAnimationFrame(() => resolve())); await nextFrame(); await nextFrame(); view.element.querySelector('[aria-label="Attach a GitHub issue or pull request to the new session"]')?.click(); @@ -315,6 +401,16 @@ export default defineThemedFixtureGroup({ path: 'sessions/chat/newWidget/' }, { labels: { kind: 'screenshot' }, render: context => renderNewChatWidget(context, { withWorkspace: true }), }), + NewSessionAutoModel: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The new-session input toolbar shows an Auto model picker whose background fits closely around the Copilot icon and Auto label without excessive empty horizontal space. The bottom row shows optically tuned compact rocket, warning, and connection status icons centered in matching controls, followed by the full Status text action without clipping.'], + render: context => renderNewChatWidget(context, { withWorkspace: true, withAutoModel: true }), + }), + NewSessionCompactAutoModel: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The new-session input toolbar shows the Auto model picker in compact mode as a centered Copilot icon inside a 22-pixel square control aligned with the expanded toolbar height.'], + render: context => renderNewChatWidget(context, { withWorkspace: true, withAutoModel: true, primaryToolbarWidth: 25 }), + }), NewSessionWorkspacePicker: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, expectedVisualDescriptions: ['The new-session composer shows Copilot, microsoft/vscode, and Issue/PR pills. The microsoft/vscode workspace pill has the active treatment after opening the workspace picker.'], @@ -327,9 +423,14 @@ export default defineThemedFixtureGroup({ path: 'sessions/chat/newWidget/' }, { }), NewSessionAttachedContext: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['The new-session workspace row shows Copilot, microsoft/vscode with a count badge showing 2, and Issue/PR with a count badge showing 1. The composer attachment row shows removable docs, microsoft/typescript, and microsoft/vscode#333053 context pills with compact dismiss icons. The folder icon is fully visible without cropping, and the GitHub issue pill includes an issue icon.'], + expectedVisualDescriptions: ['The new-session workspace row shows Copilot, microsoft/vscode with a count badge showing 2, and Issue/PR with a count badge showing 1. The composer attachment row shows removable docs, microsoft/typescript, and microsoft/vscode#333053 context pills with compact dismiss icons. The input expands upward for the attachment row while its bottom controls remain aligned with the default new-session composer. The folder icon is fully visible without cropping, and the GitHub issue pill includes an issue icon.'], render: context => renderNewChatWidget(context, { withWorkspace: true, withAttachedContext: true }), }), + NewSessionPhoneAttachedContext: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The phone new-session composer shows attachment pills without shifting the full-height content surface upward or leaving a gap below it. Status icons remain touch-friendly pills rather than inheriting the desktop 22-pixel square width.'], + render: context => renderNewChatWidget(context, { width: 390, height: 760, withWorkspace: true, withAttachedContext: true, withAutoModel: true, phoneLayout: true }), + }), NewSessionRemoteWorkspace: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, expectedVisualDescriptions: ['The new-session composer shows Copilot and devbox ยท microsoft/vscode pills. No Issue/PR pill is visible because the remote workspace has no associated GitHub repository metadata.'], @@ -430,7 +531,7 @@ function createFixtureSessionTypes(): readonly ISessionType[] { ]; } -function createFixtureProvider(workspace: ISessionWorkspace, sessionTypes: readonly ISessionType[]): ISessionsProvider { +function createFixtureProvider(workspace: ISessionWorkspace, sessionTypes: readonly ISessionType[], models: readonly ILanguageModelChatMetadataAndIdentifier[]): ISessionsProvider { return new class extends mock() { override readonly id = 'fixture-provider'; override readonly label = 'Fixture Provider'; @@ -487,7 +588,7 @@ function createFixtureProvider(workspace: ISessionWorkspace, sessionTypes: reado override getModelsSnapshot() { return { - models: [], + models, desiredModelResolution: { kind: 'notRequested' as const }, modelTarget: 'agent-host-copilotcli', }; @@ -507,6 +608,23 @@ function createFixtureProvider(workspace: ISessionWorkspace, sessionTypes: reado }(); } +function createFixtureAutoModel(): ILanguageModelChatMetadataAndIdentifier { + return { + identifier: 'copilot/auto', + metadata: { + extension: new ExtensionIdentifier('github.copilot-chat'), + id: 'auto', + name: 'Auto', + vendor: 'copilot', + version: '1.0', + family: 'auto', + maxInputTokens: 128000, + maxOutputTokens: 4096, + isDefaultForLocation: { [ChatAgentLocation.Chat]: true }, + }, + }; +} + function createFixtureAttachments(): readonly IChatRequestVariableEntry[] { const issueUri = URI.parse('https://github.com/microsoft/vscode/issues/333053'); return [ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/media/agentHostChatInputPicker.css b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/media/agentHostChatInputPicker.css index 5689d7f3fb9c5c..e64a748491471c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/media/agentHostChatInputPicker.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/media/agentHostChatInputPicker.css @@ -47,6 +47,10 @@ opacity: 0.75; } +.agent-host-chat-input-picker-slot .action-label .codicon { + font-size: var(--vscode-codiconFontSize-compact); +} + .agent-host-chat-input-picker-slot.disabled span.action-label { opacity: 0.6; cursor: default; @@ -99,17 +103,24 @@ display: none; } +.interactive-session .compact-picker .agent-host-chat-input-picker-slot { + width: 22px; + height: 22px; +} + .interactive-session .compact-picker .agent-host-chat-input-picker-slot .action-label { box-sizing: border-box; width: 22px; + height: 22px; min-width: 22px; - padding: 2px 2px 2px 8px; - justify-content: flex-start; + padding: 0; + justify-content: center; } .interactive-session .compact-picker .agent-host-chat-input-picker-slot .action-label .codicon { - width: auto; - height: auto; + width: var(--vscode-codiconFontSize-compact); + height: var(--vscode-codiconFontSize-compact); + line-height: var(--vscode-codiconFontSize-compact); } /* diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 7ad8f398f95ad9..18ace5d118e554 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -185,6 +185,7 @@ 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 PERMISSION_LEVEL_OPTION_ID = 'permissionLevel'; +const CHAT_INPUT_COMPACT_PICKER_WIDTH = 22; function getToolbarPickerResponsiveItems( toolbar: MenuWorkbenchToolBar, @@ -3539,9 +3540,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.modelWidget?.minimumWidth ?? 60; } if (shorterChatInputActionIds.has(action.id)) { - return 22; + return CHAT_INPUT_COMPACT_PICKER_WIDTH; } - return inputPickerCompactStates.get(action.id)?.get() ? 22 : undefined; + return inputPickerCompactStates.get(action.id)?.get() ? CHAT_INPUT_COMPACT_PICKER_WIDTH : undefined; }; this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); @@ -3747,20 +3748,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // 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([ - [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], - [OpenAgentHostFolderPickerAction.ID, 22], + [OpenSessionTargetPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenDelegationPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenWorkspacePickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenPermissionPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [ChatSessionPrimaryPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenAgentHostModePickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + ['sessions.agentHost.runningSessionModePicker', CHAT_INPUT_COMPACT_PICKER_WIDTH], + ['sessions.agentHost.runningSessionConfigPicker', CHAT_INPUT_COMPACT_PICKER_WIDTH], + ['sessions.agentHost.runningSessionPermissionModePicker', CHAT_INPUT_COMPACT_PICKER_WIDTH], + ['sessions.agentHost.runningSessionCodexApprovalsPicker', CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenAgentHostAutoApprovePickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenAgentHostPermissionModePickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenAgentHostCodexApprovalsPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], + [OpenAgentHostFolderPickerAction.ID, CHAT_INPUT_COMPACT_PICKER_WIDTH], ['sessions.tunnelHost.toggleSharing', 16], ]); // Direct-rendered chip lane for agent-host config properties that diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/media/modelPicker.css b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/media/modelPicker.css index be4d1807694a0c..ff9dc8e50ef6a4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/media/modelPicker.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/media/modelPicker.css @@ -86,14 +86,23 @@ .chat-input-picker-item .action-label.model-picker-split.compact .model-picker-name { flex: 0 0 auto; - padding: 0 var(--vscode-spacing-size60); - justify-content: flex-start; + width: 22px; + height: 22px; + padding: 0; + justify-content: center; +} + +.chat-input-picker-item .action-label.model-picker-split.compact .model-picker-name > .codicon { + width: var(--vscode-codiconFontSize-compact); + height: var(--vscode-codiconFontSize-compact); + line-height: var(--vscode-codiconFontSize-compact); } .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.model-picker-split.icon-only.minimal .model-picker-name { - width: 24px; - padding: 0 var(--vscode-spacing-size60); - justify-content: flex-start; + width: 22px; + height: 22px; + padding: 0; + justify-content: center; } .chat-input-picker-item .action-label.model-picker-split .model-picker-config { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerWidget.ts index a9c91949385653..b44caef1f00c0d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerWidget.ts @@ -51,7 +51,8 @@ const CACHE_BREAK_HINT_DISMISSED_STORAGE_KEY = 'chat.cacheBreakHintDismissed'; const MODEL_PICKER_MINIMUM_LABEL_WIDTH = 60; const MODEL_PICKER_NAME_CHROME_WIDTH = 30; const MODEL_PICKER_MINIMUM_NAME_WIDTH = MODEL_PICKER_MINIMUM_LABEL_WIDTH + MODEL_PICKER_NAME_CHROME_WIDTH; -const MODEL_PICKER_COMPACT_NAME_WIDTH = 24; +const MODEL_PICKER_AUTO_NAME_WIDTH = 50; +const MODEL_PICKER_COMPACT_NAME_WIDTH = 22; type ChatModelChangeClassification = { owner: 'lramos15'; comment: 'Reporting when the model picker is switched'; @@ -643,7 +644,12 @@ export class ModelPickerWidget extends Disposable { ? localize('chat.modelPicker.noModels', "No models available") : (name ?? localize('chat.modelPicker.auto', "Auto")); const showModelLabel = !compact || !modelIcon || noModelsAvailable; - const nameMinimumWidth = compact && !showModelLabel ? MODEL_PICKER_COMPACT_NAME_WIDTH : MODEL_PICKER_MINIMUM_NAME_WIDTH; + const showingAuto = !unavailable && !activating && !genericNoModels && (!this._selectedModel || isAutoModel(this._selectedModel)); + const nameMinimumWidth = compact && !showModelLabel + ? MODEL_PICKER_COMPACT_NAME_WIDTH + : showingAuto + ? MODEL_PICKER_AUTO_NAME_WIDTH + : MODEL_PICKER_MINIMUM_NAME_WIDTH; this._nameButton.style.minWidth = `${nameMinimumWidth}px`; if (showModelLabel) { nameChildren.push(dom.$('span.chat-input-picker-label', undefined, modelLabel)); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 79fe19d20944a4..5546bef299cd23 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2115,8 +2115,7 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-toolbar-hoverBackground); } -/* When only the icon remains, keep the expanded control's leading inset so - * the glyph does not move as the label disappears. */ +/* Compact picker controls match the 22px toolbar control tier. */ .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, @@ -2131,23 +2130,13 @@ have to be updated for changes to the rules above, or to support more deeply nes justify-content: center; .codicon { + width: var(--vscode-codiconFontSize-compact); + height: var(--vscode-codiconFontSize-compact); + line-height: var(--vscode-codiconFontSize-compact); justify-content: center; } } -.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 of chrome instead of a 16px glyph towering over the 12px send / mic buttons diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts index f770220f3c89ce..f490dd533715fd 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts @@ -4,6 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ClaudeSessionConfigKey } from '../../../../../../platform/agentHost/common/claudeSessionConfigKeys.js'; import { SessionConfigKey } from '../../../../../../platform/agentHost/common/sessionConfigKeys.js'; @@ -16,6 +20,51 @@ import { SessionType } from '../../../common/chatSessionsService.js'; import { getAgentHostPickerProperty, OpenAgentHostAutoApprovePickerAction, OpenAgentHostCodexApprovalsPickerAction, OpenAgentHostModePickerAction, OpenAgentHostPermissionModePickerAction } from '../../../browser/agentSessions/agentHost/agentHostChatInputPicker.contribution.js'; import { isAutoApproveValuePolicyRestricted, isPermissionLevelVisible, normalizeSessionConfigValue } from '../../../common/agentHostConfigPolicy.js'; import { ChatPermissionLevel } from '../../../common/constants.js'; +import '../../../browser/agentSessions/agentHost/media/agentHostChatInputPicker.css'; + +suite('AgentHostChatInputPicker - compact layout', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('keeps the Copilot harness picker height stable and centers its compact icon', () => { + const session = dom.append(document.body, dom.$('.interactive-session')); + disposables.add(toDisposable(() => session.remove())); + session.style.setProperty('--vscode-codiconFontSize-compact', '12px'); + const actionBar = dom.append(session, dom.$('.monaco-action-bar')); + const actionsContainer = dom.append(actionBar, dom.$('.actions-container')); + actionsContainer.style.display = 'flex'; + const item = dom.append(actionsContainer, dom.$('.action-item.agent-host-chat-input-picker-host')); + const slot = dom.append(item, dom.$('.agent-host-chat-input-picker-slot')); + const label = dom.append(slot, dom.$('a.action-label')); + const icon = dom.append(label, renderIcon(Codicon.rocketCompact)); + dom.append(label, dom.$('span.agent-host-chat-input-picker-label', undefined, 'Autopilot')); + + const expandedHeight = item.getBoundingClientRect().height; + item.classList.add('compact-picker'); + const itemBounds = item.getBoundingClientRect(); + const slotBounds = slot.getBoundingClientRect(); + const labelBounds = label.getBoundingClientRect(); + const iconBounds = icon.getBoundingClientRect(); + assert.deepStrictEqual({ + expandedHeight, + item: { width: itemBounds.width, height: itemBounds.height }, + slot: { width: slotBounds.width, height: slotBounds.height }, + label: { width: labelBounds.width, height: labelBounds.height }, + icon: { + width: iconBounds.width, + height: iconBounds.height, + x: iconBounds.left - labelBounds.left, + y: iconBounds.top - labelBounds.top, + }, + }, { + expandedHeight: 22, + item: { width: 22, height: 22 }, + slot: { width: 22, height: 22 }, + label: { width: 22, height: 22 }, + icon: { width: 12, height: 12, x: 5, y: 5 }, + }); + }); +}); suite('AgentHostChatInputPicker - action mapping', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatInputPickerResponsiveLayout.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatInputPickerResponsiveLayout.test.ts index 2a191963c2c771..cedb51b34c6dfa 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatInputPickerResponsiveLayout.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatInputPickerResponsiveLayout.test.ts @@ -463,27 +463,52 @@ suite('ChatInputPickerResponsiveLayout', () => { }); }); - test('keeps the primary picker icon anchored when its label disappears', () => { + test('centers compact primary and secondary picker icons', () => { host.style.setProperty('--vscode-spacing-size60', '6px'); + host.style.setProperty('--vscode-spacing-size80', '8px'); 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, + host.style.setProperty('--vscode-codiconFontSize-compact', '12px'); + + const renderPicker = (toolbarClass: string, itemClass: string) => { + const toolbar = dom.append(host, dom.$(`.${toolbarClass}`)); + const item = dom.append(toolbar, dom.$(`.${itemClass}`)); + const actionLabel = dom.append(item, dom.$('a.action-label')); + const icon = dom.append(actionLabel, dom.$('span.codicon')); + const pickerLabel = dom.append(actionLabel, dom.$('span.chat-input-picker-label')); + pickerLabel.textContent = 'Picker'; + + const expandedOffset = icon.getBoundingClientRect().left - actionLabel.getBoundingClientRect().left; + actionLabel.classList.add('icon-only'); + pickerLabel.remove(); + const actionBounds = actionLabel.getBoundingClientRect(); + const iconBounds = icon.getBoundingClientRect(); + return { + expandedOffset, + action: { width: actionBounds.width, height: actionBounds.height }, + icon: { + width: iconBounds.width, + height: iconBounds.height, + x: iconBounds.left - actionBounds.left, + y: iconBounds.top - actionBounds.top, + }, + }; + }; + + assert.deepStrictEqual({ + primary: renderPicker('chat-input-toolbar', 'chat-input-picker-item'), + secondary: renderPicker('chat-secondary-input-toolbar', 'chat-sessionPicker-item'), + }, { + primary: { + expandedOffset: 6, + action: { width: 22, height: 22 }, + icon: { width: 12, height: 12, x: 5, y: 5 }, + }, + secondary: { + expandedOffset: 8, + action: { width: 22, height: 22 }, + icon: { width: 12, height: 12, x: 5, y: 5 }, + }, }); }); + }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts index f5dd8033f5ed38..2c391a73820e13 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts @@ -42,6 +42,7 @@ import { IBrowserViewWorkbenchService } from '../../../../contrib/browserView/co import { IAgentHostService } from '../../../../../platform/agentHost/common/agentService.js'; import { IAgentHostEnablementService } from '../../../../../platform/agentHost/common/agentHostEnablementService.js'; import { IAgentSubscription } from '../../../../../platform/agentHost/common/state/agentSubscription.js'; +import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; import { RootState, StateComponents } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IAgentSessionsService } from '../../../../contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IAgentHostUntitledProvisionalSessionService } from '../../../../contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.js'; @@ -66,6 +67,7 @@ import { ChatSpeechToTextState, IChatSpeechToTextService } from '../../../../con import { IDictationOnboardingService } from '../../../../contrib/chat/browser/speechToText/dictationOnboarding.js'; import { IChatInputNoticeHubService } from '../../../../contrib/chat/browser/widget/input/chatInputNoticeHub.js'; import { ChatSubmitRequestHandlerService, IChatSubmitRequestHandlerService } from '../../../../contrib/chat/browser/chatSubmitRequestHandlerService.js'; +import { IChatStatusItemService } from '../../../../contrib/chat/browser/chatStatus/chatStatusItemService.js'; import { IChatMarkdownAnchorService } from '../../../../contrib/chat/browser/widget/chatContentParts/chatMarkdownAnchorService.js'; import { IChatWidgetHistoryService } from '../../../../contrib/chat/common/widget/chatWidgetHistoryService.js'; import { IChatModeService } from '../../../../contrib/chat/common/chatModes.js'; @@ -128,6 +130,8 @@ export interface IChatFixtureServicesOptions { readonly todos?: readonly IChatTodo[]; /** Active notification returned from IChatInputNotificationService. */ readonly notification?: IChatInputNotification; + /** Resolved Agent Host session configuration used by real chat input picker fixtures. */ + readonly agentHostSessionConfig?: ResolveSessionConfigResult; } /** @@ -163,7 +167,13 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I // `getContainer` stands in for the workbench container that widgets use to host // overflow nodes (suggest widget, post-paste selector); the fixture document body // is the closest equivalent. - reg.defineInstance(IWorkbenchLayoutService, new class extends mock() { override onDidChangePartVisibility = Event.None; override onDidChangeWindowMaximized = Event.None; override isVisible() { return true; } override getContainer(targetWindow: Window): HTMLElement { return targetWindow.document.body; } }()); + reg.defineInstance(IWorkbenchLayoutService, new class extends mock() { + override readonly mainContainer = document.body; + override onDidChangePartVisibility = Event.None; + override onDidChangeWindowMaximized = Event.None; + override isVisible() { return true; } + override getContainer(targetWindow: Window): HTMLElement { return targetWindow.document.body; } + }()); reg.defineInstance(IHostService, new class extends mock() { override readonly hasFocus = true; override readonly onDidChangeFocus = Event.None; @@ -327,6 +337,12 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I override announceRendered() { } }()); reg.defineInstance(IChatSubmitRequestHandlerService, new ChatSubmitRequestHandlerService()); + reg.defineInstance(IChatStatusItemService, new class extends mock() { + override readonly onDidChange = Event.None; + override setOrUpdateEntry() { } + override deleteEntry() { } + override getEntries() { return []; } + }()); reg.defineInstance(IAgentSessionsService, new class extends mock() { override readonly model = new class extends mock() { override readonly onDidChangeSessions = Event.None; }(); override getSession() { return undefined; } @@ -359,10 +375,14 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I override getSubscriptionUnmanaged(_kind: StateComponents, _resource: URI): IAgentSubscription | undefined { return undefined; } + override async resolveSessionConfig(): Promise { + return options.agentHostSessionConfig ?? { schema: { type: 'object', properties: {} }, values: {} }; + } }()); reg.defineInstance(IAgentHostUntitledProvisionalSessionService, new class extends mock() { override readonly onDidChange = Event.None; override get() { return undefined; } + override getOrCreate() { return Promise.resolve(undefined); } }()); reg.defineInstance(IAgentHostSessionWorkingDirectoryResolver, new class extends mock() { override resolve() { return undefined; } @@ -370,6 +390,8 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I reg.defineInstance(IAgentHostNewSessionFolderService, new class extends mock() { override readonly onDidChangeFolder = Event.None; override getFolder() { return undefined; } + override getDefaultFolder() { return undefined; } + override resolveNewSessionPrimary() { return undefined; } }()); reg.defineInstance(IAgentHostCustomizationService, new class extends mock() { override readonly onDidChangeCustomizations = Event.None; diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts index c8ea76fd58dc7f..52ea24b21ad5fd 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts @@ -7,6 +7,8 @@ import { Event } from '../../../../../base/common/event.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; +import { SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js'; +import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { ChatEditingSessionState, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../../contrib/chat/common/editing/chatEditingService.js'; import { IChatRequestDisablement } from '../../../../contrib/chat/common/model/chatModel.js'; @@ -89,11 +91,46 @@ const sampleNotification: IChatInputNotification = { autoDismissOnMessage: false, }; +const copilotHarnessSessionConfig: ResolveSessionConfigResult = { + schema: { + type: 'object', + properties: { + [SessionConfigKey.Mode]: { + type: 'string', + title: 'Mode', + enum: ['interactive', 'autopilot'], + enumLabels: ['Agent', 'Autopilot'], + default: 'interactive', + }, + [SessionConfigKey.AutoApprove]: { + type: 'string', + title: 'Permissions', + enum: ['default', 'autoApprove', 'autopilot'], + enumLabels: ['Default permissions', 'Allow all', 'Autopilot'], + default: 'default', + }, + }, + }, + values: { + [SessionConfigKey.Mode]: 'interactive', + [SessionConfigKey.AutoApprove]: 'default', + }, +}; + export default defineThemedFixtureGroup({ path: 'chat/input/' }, { Default: defineComponentFixture({ render: context => renderChatInput(context) }), WithSandboxing: defineComponentFixture({ render: context => renderChatInput(context, { sandboxingEnabled: true }) }), WithProviderIcon: defineComponentFixture({ render: context => renderChatInput(context, { models: sampleModels }) }), - CompactWithProviderIcon: defineComponentFixture({ render: context => renderChatInput(context, { models: sampleModels, width: 260 }) }), + CompactWithProviderIcon: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The editor chat input shows compact picker controls as 12-pixel codicons centered with equal padding inside matching 22-pixel square controls, aligned with the expanded toolbar height.'], + render: context => renderChatInput(context, { models: sampleModels, width: 180 }) + }), + CopilotHarnessCompactPickers: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The editor chat input renders the real Copilot Agent Host mode and permissions pickers in compact state. Each compact icon is centered with equal padding inside a 22-pixel square control.'], + render: context => renderChatInput(context, { agentHostSessionConfig: copilotHarnessSessionConfig, width: 500, resizeWidths: [180] }), + }), WithArtifacts: defineComponentFixture({ render: context => renderChatInput(context, { artifacts: sampleArtifacts }) }), // The notice/input seam, the subject of #330483. Driven through the real // notification service so the squared corner comes from the stack. diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts index 40981ce1c85e9b..573c5dab334e42 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts @@ -10,11 +10,16 @@ import { mock } from '../../../../../base/test/common/mock.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IChatWidget } from '../../../../contrib/chat/browser/chat.js'; +import { OpenAgentHostAutoApprovePickerAction, OpenAgentHostModePickerAction } from '../../../../contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.contribution.js'; import { SessionType } from '../../../../contrib/chat/common/chatSessionsService.js'; +import { getNewChatSessionResource } from '../../../../contrib/chat/common/model/chatUri.js'; import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../../../contrib/chat/browser/widget/input/chatInputPart.js'; +import { IChatModel } from '../../../../contrib/chat/common/model/chatModel.js'; +import { IChatViewModel } from '../../../../contrib/chat/common/model/chatViewModel.js'; import { IArtifactSourceGroup } from '../../../../contrib/chat/common/tools/chatArtifactsService.js'; import { IChatInputNotification } from '../../../../contrib/chat/browser/widget/input/chatInputNotificationService.js'; import { IChatEditingSession } from '../../../../contrib/chat/common/editing/chatEditingService.js'; @@ -67,6 +72,17 @@ const voiceControlRenderings: Record voiceDisconnect: { icon: Codicon.debugDisconnectCompact, containerClasses: ['voice-active'] }, }; +function createFixtureChatViewModel(sessionResource: URI): IChatViewModel { + const model = new class extends mock() { + override readonly sessionResource = sessionResource; + override readonly lastRequestObs = constObservable(undefined); + }(); + return new class extends mock() { + override readonly sessionResource = sessionResource; + override readonly model = model; + }(); +} + export interface ChatInputFixtureOptions { readonly artifacts?: readonly { label: string; uri: string; type: 'devServer' | 'screenshot' | 'plan' | undefined }[]; readonly editingSession?: IChatEditingSession; @@ -90,6 +106,8 @@ export interface ChatInputFixtureOptions { readonly resizeWidths?: readonly number[]; /** Supplies models so the picker renders provider icons. */ readonly models?: readonly ILanguageModelChatMetadataAndIdentifier[]; + /** Renders the production Copilot Agent Host mode and permissions pickers. */ + readonly agentHostSessionConfig?: ResolveSessionConfigResult; /** Renders a standalone dictation / Voice Mode control in the given state. */ readonly voiceControl?: VoiceControlState; /** @@ -104,9 +122,10 @@ export interface ChatInputFixtureOptions { export async function renderChatInput(context: ComponentFixtureContext, fixtureOptions: ChatInputFixtureOptions = {}): Promise { const { container, disposableStore } = context; - const { artifacts = [], editingSession, todos = [], isSessionsWindow = false, value, selection, sandboxingEnabled = false, width = 500, resizeWidths = [], models = [], voiceControl, notification, pet = false } = fixtureOptions; + const { artifacts = [], editingSession, todos = [], isSessionsWindow = false, value, selection, sandboxingEnabled = false, width = 500, resizeWidths = [], models = [], agentHostSessionConfig, voiceControl, notification, pet = false } = fixtureOptions; const artifactGroups: IArtifactSourceGroup[] = artifacts.length > 0 ? [{ source: { kind: 'agent' as const }, artifacts }] : []; const artifactsObs = observableValue('artifactGroups', artifactGroups); + const sessionResource = agentHostSessionConfig ? getNewChatSessionResource(SessionType.AgentHostCopilot) : undefined; // Sprite sheets are resolved against the file root. if (pet) { @@ -117,7 +136,7 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO const instantiationService = createEditorServices(disposableStore, { colorTheme: context.theme, additionalServices: (reg) => { - registerChatFixtureServices(reg, { artifactGroups: artifactsObs, todos, notification }); + registerChatFixtureServices(reg, { artifactGroups: artifactsObs, todos, notification, agentHostSessionConfig }); if (chatPetService) { reg.defineInstance(IChatPetService, chatPetService); } @@ -187,7 +206,12 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO } menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.arrowUpCompact }, group: 'navigation', order: 4 }); menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openSessionTargetPicker', title: 'Local' }, group: 'navigation', order: 0 }); - menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Permissions' }, group: 'navigation', order: 10 }); + if (agentHostSessionConfig) { + menuService.addItem(MenuId.ChatInputSecondary, { command: { id: OpenAgentHostModePickerAction.ID, title: 'Agent Mode' }, group: 'navigation', order: 0.7 }); + menuService.addItem(MenuId.ChatInputSecondary, { command: { id: OpenAgentHostAutoApprovePickerAction.ID, title: 'Auto-Approve' }, group: 'navigation', order: 0.8 }); + } else { + menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Permissions' }, group: 'navigation', order: 10 }); + } const options: IChatInputPartOptions = { renderFollowups: false, @@ -199,7 +223,11 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO isSessionsWindow, // The sandbox toggle is specific to the local harness, so present the // input as the local session type when exercising the sandboxed state. - sessionTypePickerDelegate: sandboxingEnabled ? { getActiveSessionProvider: () => SessionType.Local } : undefined, + sessionTypePickerDelegate: agentHostSessionConfig + ? { getActiveSessionProvider: () => SessionType.AgentHostCopilot } + : sandboxingEnabled + ? { getActiveSessionProvider: () => SessionType.Local } + : undefined, }; const styles: IChatInputStyles = { overlayBackground: 'var(--vscode-editor-background)', @@ -210,7 +238,7 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO const inputPart = disposableStore.add(instantiationService.createInstance(ChatInputPart, ChatAgentLocation.Chat, options, styles, false)); const mockWidget = new class extends mock() { override readonly onDidChangeViewModel = new Emitter().event; - override readonly viewModel = undefined; + override readonly viewModel = sessionResource ? createFixtureChatViewModel(sessionResource) : undefined; override readonly contribs = []; override readonly location = ChatAgentLocation.Chat; override readonly viewContext = {}; diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 3e8e18551ddde2..0344866f0b575f 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -132,6 +132,18 @@ #### chat/chatPetAccessoryRig/chatPetAccessoryRig/LiveEyeLayering/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/28743055f10abdf3c0a7809b2b3e830b7a04dc65157febf48215eecdf8b03772) +#### chat/input/chatInput/CompactWithProviderIcon/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/d49581cdd64a2c1fb30fd998d04b7a9b6ab72a7126cadb0495acdb2c30212ac2) + +#### chat/input/chatInput/CompactWithProviderIcon/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0eff7001dde59fa6a28e994095e643dc0afc593352252bf45b34a15da4be2f42) + +#### chat/input/chatInput/CopilotHarnessCompactPickers/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/4a0314153468b84bb81bd087804f19b1a3181ef322d1d88e37a9ddfb67fa4e74) + +#### chat/input/chatInput/CopilotHarnessCompactPickers/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/6f6751b15a0a8973b1adb870c688fcd4e3467744c3fa7cb5562186032255dc95) + #### chat/petAchievements/standaloneModal/chatPetAchievementsEditor/MixedSelected/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/2bd9c5e744b514d97d1ff309a1692f239c4f880464507430f9458946853f3db1) @@ -186,11 +198,35 @@ #### sessions/accountMenu/petAchievementBadges/chatPetAchievementBadges/AllBadges/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/fe4b95bf8348637bba9f8c0dda791924e6c67fd7b5d173398f9b2c0bfc9f7071) +#### sessions/chat/input/chatInput/ResponsiveModelResizeCycleCompact/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/a92c03d2a84e6fa15cfa01193f81961fb648ec03571e97030c458348ea81b759) + +#### sessions/chat/input/chatInput/ResponsiveModelResizeCycleCompact/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/53fe36871b8b384e93c928b25acad531c2bcec683521d22307d025265ed19d9c) + +#### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/8afa4c4eeb9f78079ea98efe10b841be3ba1104fe5fcf7f0e936113243017792) + +#### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0fe9bb7434b473795187b0545ccaed6aa77af4b7e9f90890dc8885c7f31ddebb) + #### sessions/chat/newWidget/newChatWidget/NewSessionAttachedContext/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/c03e1bc3f517347607d0422462dd55a50c95d01f03b5203b27829aa296f9b54e) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/19b1ebb2ae6b03f3500181ac7d35fe20ccf4f0a3bcac413211f00f3db200a69b) #### sessions/chat/newWidget/newChatWidget/NewSessionAttachedContext/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b646e331c1ecbc6859e19e42179cd7413780f139c71aad5e4ab009aed35d6f04) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/c2903c1bfd9364ff2dd3c6531a47a0b5c4ad11892c21c6db3357eb986c3ae568) + +#### sessions/chat/newWidget/newChatWidget/NewSessionAutoModel/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/828f33b8392e74ac892e8339d6b5401e5e17b9aeb424863f0c8ca0ae5982a30c) + +#### sessions/chat/newWidget/newChatWidget/NewSessionAutoModel/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/704712c8157cbb1e8b05602c92f65941f869a455400a7057057fa03330424c03) + +#### sessions/chat/newWidget/newChatWidget/NewSessionCompactAutoModel/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/08859e75e7f3a262ed2428c7b676881578fc8f62301f4f3ca63d79c21a1e6dd0) + +#### sessions/chat/newWidget/newChatWidget/NewSessionCompactAutoModel/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/e896298aaf3e160015d98455ae350254c4acb40d32117b29d8fd8284923f0f44) #### sessions/chat/newWidget/newChatWidget/NewSessionGitHubContextPicker/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/e1d5b98be6a615e7e30e3f08e1b1e80219743fbe0b478ff68a0e7b08e234cf56) @@ -198,6 +234,12 @@ #### sessions/chat/newWidget/newChatWidget/NewSessionGitHubContextPicker/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/e6b7c571945d3c311ab89529478340018496b9d4c75e13095b672c913cee52ee) +#### sessions/chat/newWidget/newChatWidget/NewSessionPhoneAttachedContext/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/92dddf0febbf9a2e7b9d1a33940fefc624ba319f7bb7bd4d911743c535908392) + +#### sessions/chat/newWidget/newChatWidget/NewSessionPhoneAttachedContext/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/4ad50b05198491cea5a200931e6070113ac5238ecf6a31185e4d13e6f9dacdfc) + #### sessions/chat/newWidget/newChatWidget/NewSessionRemoteWorkspace/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/34408877e1ac8af8668377237c835da520bfc4a1301ebd55f6dab32084f88042)