Path: blob/main/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../../base/common/event.js';6import { observableValue } from '../../../../../base/common/observable.js';7import { URI } from '../../../../../base/common/uri.js';8import { mock } from '../../../../../base/test/common/mock.js';9import { Codicon } from '../../../../../base/common/codicons.js';10import { IMenuService, IMenu, MenuId, MenuItemAction, IMenuItem } from '../../../../../platform/actions/common/actions.js';11import { ICommandService } from '../../../../../platform/commands/common/commands.js';12import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';13import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';14import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';1516import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';17import { IFileService } from '../../../../../platform/files/common/files.js';18import { ISharedWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';19import { IDecorationsService } from '../../../../services/decorations/common/decorations.js';20import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';21import { IExtensionService } from '../../../../services/extensions/common/extensions.js';22import { IPathService } from '../../../../services/path/common/pathService.js';23import { IChatWidgetHistoryService } from '../../../../contrib/chat/common/widget/chatWidgetHistoryService.js';24import { IChatContextPickService } from '../../../../contrib/chat/browser/attachments/chatContextPickService.js';25import { IWorkspaceContextService, IWorkspace } from '../../../../../platform/workspace/common/workspace.js';26import { IViewDescriptorService } from '../../../../common/views.js';27import { IChatWidget } from '../../../../contrib/chat/browser/chat.js';28import { IAgentSessionsService } from '../../../../contrib/chat/browser/agentSessions/agentSessionsService.js';29import { IChatAttachmentResolveService } from '../../../../contrib/chat/browser/attachments/chatAttachmentResolveService.js';30import { IChatAttachmentWidgetRegistry } from '../../../../contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.js';31import { IChatContextService } from '../../../../contrib/chat/browser/contextContrib/chatContextService.js';32import { IChatImageCarouselService } from '../../../../contrib/chat/browser/chatImageCarouselService.js';33import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../../../contrib/chat/browser/widget/input/chatInputPart.js';34import { IArtifactSourceGroup, IChatArtifacts, IChatArtifactsService } from '../../../../contrib/chat/common/tools/chatArtifactsService.js';35import { ChatEditingSessionState, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../../contrib/chat/common/editing/chatEditingService.js';36import { IChatRequestDisablement } from '../../../../contrib/chat/common/model/chatModel.js';37import { IChatTodo, IChatTodoListService } from '../../../../contrib/chat/common/tools/chatTodoListService.js';38import { ChatAgentLocation, ChatConfiguration } from '../../../../contrib/chat/common/constants.js';39import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';40import { IChatModeService } from '../../../../contrib/chat/common/chatModes.js';41import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js';42import { IChatSessionsService } from '../../../../contrib/chat/common/chatSessionsService.js';43import { ILanguageModelsService } from '../../../../contrib/chat/common/languageModels.js';44import { IChatAgentService } from '../../../../contrib/chat/common/participants/chatAgents.js';45import { ILanguageModelToolsService } from '../../../../contrib/chat/common/tools/languageModelToolsService.js';46import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';47import { IEditorService } from '../../../../services/editor/common/editorService.js';48import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';49import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';50import { IProductService } from '../../../../../platform/product/common/productService.js';51import { IUpdateService, StateType } from '../../../../../platform/update/common/update.js';52import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';53import { IListService, ListService } from '../../../../../platform/list/browser/listService.js';54import { INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';55import { ISCMService } from '../../../../contrib/scm/common/scm.js';56import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js';5758import '../../../../contrib/chat/browser/widget/media/chat.css';5960class FixtureMenuService implements IMenuService {61declare readonly _serviceBrand: undefined;62private readonly _items = new Map<string, IMenuItem[]>();63constructor(64@IContextKeyService private readonly _contextKeyService: IContextKeyService,65@ICommandService private readonly _commandService: ICommandService,66) { }67addItem(menuId: MenuId, item: IMenuItem): void {68const key = menuId.id;69let items = this._items.get(key);70if (!items) {71items = [];72this._items.set(key, items);73}74items.push(item);75}76createMenu(id: MenuId): IMenu {77const actions: [string, MenuItemAction[]][] = [];78for (const item of this._items.get(id.id) ?? []) {79const group = item.group ?? '';80let entry = actions.find(a => a[0] === group);81if (!entry) {82entry = [group, []];83actions.push(entry);84}85entry[1].push(new MenuItemAction(item.command, item.alt, {}, undefined, undefined, this._contextKeyService, this._commandService));86}87return { onDidChange: Event.None, dispose() { }, getActions: () => actions };88}89getMenuActions() { return []; }90getMenuContexts() { return new Set<string>(); }91resetHiddenStates() { }92}9394interface ChatInputFixtureOptions {95readonly artifacts?: readonly { label: string; uri: string; type: 'devServer' | 'screenshot' | 'plan' | undefined }[];96readonly editingSession?: IChatEditingSession;97readonly todos?: IChatTodo[];98}99100async function renderChatInput(context: ComponentFixtureContext, fixtureOptions: ChatInputFixtureOptions = {}): Promise<void> {101const { container, disposableStore } = context;102const { artifacts = [], editingSession, todos = [] } = fixtureOptions;103const artifactGroups: IArtifactSourceGroup[] = artifacts.length > 0 ? [{ source: { kind: 'agent' as const }, artifacts }] : [];104const artifactsObs = observableValue<readonly IArtifactSourceGroup[]>('artifactGroups', artifactGroups);105106const instantiationService = createEditorServices(disposableStore, {107colorTheme: context.theme,108additionalServices: (reg) => {109registerWorkbenchServices(reg);110reg.define(IMenuService, FixtureMenuService);111reg.defineInstance(IDecorationsService, new class extends mock<IDecorationsService>() { override onDidChangeDecorations = Event.None; }());112reg.defineInstance(ITextFileService, new class extends mock<ITextFileService>() { override readonly untitled = new class extends mock<ITextFileService['untitled']>() { override readonly onDidChangeLabel = Event.None; }(); }());113reg.defineInstance(ILanguageModelsService, new class extends mock<ILanguageModelsService>() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }());114reg.defineInstance(IFileService, new class extends mock<IFileService>() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; }());115reg.defineInstance(IEditorService, new class extends mock<IEditorService>() { override onDidActiveEditorChange = Event.None; }());116reg.defineInstance(IChatAgentService, new class extends mock<IChatAgentService>() { override onDidChangeAgents = Event.None; override getAgents() { return []; } override getActivatedAgents() { return []; } }());117reg.defineInstance(ISharedWebContentExtractorService, new class extends mock<ISharedWebContentExtractorService>() { }());118reg.defineInstance(IWorkbenchAssignmentService, new class extends mock<IWorkbenchAssignmentService>() { override async getCurrentExperiments() { return []; } override async getTreatment() { return undefined; } override onDidRefetchAssignments = Event.None; }());119reg.defineInstance(IChatEntitlementService, new class extends mock<IChatEntitlementService>() { }());120reg.defineInstance(IChatModeService, new class extends mock<IChatModeService>() { override readonly onDidChangeChatModes = Event.None; override getModes() { return { builtin: [], custom: [] }; } override findModeById() { return undefined; } }());121reg.defineInstance(ILanguageModelToolsService, new class extends mock<ILanguageModelToolsService>() { override onDidChangeTools = Event.None; override getTools() { return []; } }());122reg.defineInstance(IChatService, new class extends mock<IChatService>() { override onDidSubmitRequest = Event.None; }());123reg.defineInstance(IChatSessionsService, new class extends mock<IChatSessionsService>() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; }());124reg.defineInstance(IChatContextService, new class extends mock<IChatContextService>() { }());125reg.defineInstance(IAgentSessionsService, new class extends mock<IAgentSessionsService>() { override readonly model = new class extends mock<IAgentSessionsService['model']>() { override readonly onDidChangeSessions = Event.None; }(); }());126reg.defineInstance(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { override onDidChangeWorkspaceFolders = Event.None; override getWorkspace(): IWorkspace { return { id: '', folders: [], configuration: undefined }; } }());127reg.defineInstance(IWorkbenchLayoutService, new class extends mock<IWorkbenchLayoutService>() { override onDidChangePartVisibility = Event.None; override onDidChangeWindowMaximized = Event.None; override isVisible() { return true; } }());128reg.defineInstance(IViewDescriptorService, new class extends mock<IViewDescriptorService>() { override onDidChangeLocation = Event.None; }());129reg.defineInstance(IChatAttachmentWidgetRegistry, new class extends mock<IChatAttachmentWidgetRegistry>() { }());130reg.defineInstance(IChatAttachmentResolveService, new class extends mock<IChatAttachmentResolveService>() { }());131reg.defineInstance(IExtensionService, new class extends mock<IExtensionService>() { override readonly onDidChangeExtensions = Event.None; }());132reg.defineInstance(IPathService, new class extends mock<IPathService>() { }());133reg.defineInstance(IChatWidgetHistoryService, new class extends mock<IChatWidgetHistoryService>() { override getHistory() { return []; } override readonly onDidChangeHistory = Event.None; }());134reg.defineInstance(IChatContextPickService, new class extends mock<IChatContextPickService>() { }());135reg.defineInstance(IListService, new ListService());136reg.defineInstance(INotebookDocumentService, new class extends mock<INotebookDocumentService>() { }());137reg.defineInstance(ISCMService, new class extends mock<ISCMService>() {138override readonly onDidAddRepository = Event.None;139override readonly onDidRemoveRepository = Event.None;140override readonly repositories = [];141override readonly repositoryCount = 0;142}());143reg.defineInstance(IActionWidgetService, new class extends mock<IActionWidgetService>() { override show() { } override hide() { } override get isVisible() { return false; } }());144reg.defineInstance(IFileDialogService, new class extends mock<IFileDialogService>() { }());145reg.defineInstance(IProductService, new class extends mock<IProductService>() { }());146reg.defineInstance(IChatImageCarouselService, new class extends mock<IChatImageCarouselService>() { }());147reg.defineInstance(IUpdateService, new class extends mock<IUpdateService>() { override onStateChange = Event.None; override get state() { return { type: StateType.Uninitialized as const }; } }());148reg.defineInstance(IUriIdentityService, new class extends mock<IUriIdentityService>() { }());149reg.defineInstance(IChatArtifactsService, new class extends mock<IChatArtifactsService>() {150override getArtifacts(): IChatArtifacts {151return new class extends mock<IChatArtifacts>() {152override readonly artifactGroups = artifactsObs;153override setAgentArtifacts() { }154override clearAgentArtifacts() { }155override clearSubagentArtifacts() { }156override migrate() { }157}();158}159}());160reg.defineInstance(IChatTodoListService, new class extends mock<IChatTodoListService>() {161override readonly onDidUpdateTodos = Event.None;162override getTodos() { return [...todos]; }163override setTodos() { }164override migrateTodos() { }165}());166},167});168169if (artifacts.length > 0) {170const configService = instantiationService.get(IConfigurationService) as TestConfigurationService;171await configService.setUserConfiguration(ChatConfiguration.ArtifactsEnabled, true);172}173174container.style.width = '500px';175container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))';176container.classList.add('monaco-workbench');177178const session = document.createElement('div');179session.classList.add('interactive-session');180container.appendChild(session);181182const menuService = instantiationService.get(IMenuService) as FixtureMenuService;183menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.attachContext', title: '+', icon: Codicon.add }, group: 'navigation', order: -1 });184menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModePicker', title: 'Agent' }, group: 'navigation', order: 1 });185menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModelPicker', title: 'GPT-5.3-Codex' }, group: 'navigation', order: 3 });186menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.configureTools', title: '', icon: Codicon.settingsGear }, group: 'navigation', order: 100 });187menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.arrowUp }, group: 'navigation', order: 4 });188menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openSessionTargetPicker', title: 'Local' }, group: 'navigation', order: 0 });189menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Approvals' }, group: 'navigation', order: 10 });190191const options: IChatInputPartOptions = {192renderFollowups: false,193renderInputToolbarBelowInput: false,194renderWorkingSet: !!editingSession,195menus: { executeToolbar: MenuId.ChatExecute, telemetrySource: 'fixture' },196widgetViewKindTag: 'view',197inputEditorMinLines: 2,198};199const styles: IChatInputStyles = {200overlayBackground: 'var(--vscode-editor-background)',201listForeground: 'var(--vscode-foreground)',202listBackground: 'var(--vscode-editor-background)',203};204205const inputPart = disposableStore.add(instantiationService.createInstance(ChatInputPart, ChatAgentLocation.Chat, options, styles, false));206const mockWidget = new class extends mock<IChatWidget>() {207override readonly onDidChangeViewModel = new Emitter<never>().event;208override readonly viewModel = undefined;209override readonly contribs = [];210override readonly location = ChatAgentLocation.Chat;211override readonly viewContext = {};212}();213214inputPart.render(session, '', mockWidget);215inputPart.layout(500);216await new Promise(r => setTimeout(r, 100));217inputPart.layout(500);218inputPart.renderArtifactsWidget(URI.parse('chat-session:test-session'));219await inputPart.renderChatTodoListWidget(URI.parse('chat-session:test-session'));220await new Promise(r => setTimeout(r, 50));221222if (editingSession) {223inputPart.renderChatEditingSessionState(editingSession);224await new Promise(r => setTimeout(r, 50));225inputPart.layout(500);226}227}228229const sampleArtifacts = [230{ label: 'Dev Server', uri: 'http://localhost:3000', type: 'devServer' as const },231{ label: 'Screenshot', uri: 'file:///tmp/screenshot.png', type: 'screenshot' as const },232{ label: 'Plan', uri: 'file:///tmp/plan.md', type: 'plan' as const },233];234235function createMockEditingSession(files: { uri: string; added: number; removed: number }[]): IChatEditingSession {236const entries = files.map(f => {237const entry = new class extends mock<IModifiedFileEntry>() {238override readonly entryId = f.uri;239override readonly modifiedURI = URI.parse(f.uri);240override readonly originalURI = URI.parse(f.uri);241override readonly state = observableValue('state', ModifiedFileEntryState.Modified);242override readonly linesAdded = observableValue('linesAdded', f.added);243override readonly linesRemoved = observableValue('linesRemoved', f.removed);244override readonly lastModifyingRequestId = 'request-1';245override readonly changesCount = observableValue('changesCount', 1);246override readonly isCurrentlyBeingModifiedBy = observableValue('isCurrentlyBeingModifiedBy', undefined);247override readonly lastModifyingResponse = observableValue('lastModifyingResponse', undefined);248override readonly rewriteRatio = observableValue('rewriteRatio', 0);249override readonly waitsForLastEdits = observableValue('waitsForLastEdits', false);250override readonly reviewMode = observableValue('reviewMode', false);251override readonly autoAcceptController = observableValue('autoAcceptController', undefined);252}();253return entry;254});255256return new class extends mock<IChatEditingSession>() {257override readonly isGlobalEditingSession = false;258override readonly chatSessionResource = URI.parse('chat-session:test-session');259override readonly onDidDispose = Event.None;260override readonly state = observableValue('state', ChatEditingSessionState.Idle);261override readonly entries = observableValue('entries', entries);262override readonly requestDisablement = observableValue<IChatRequestDisablement[]>('requestDisablement', []);263}();264}265266const sampleTodos: IChatTodo[] = [267{ id: 1, title: 'Set up project structure', status: 'completed' },268{ id: 2, title: 'Implement auth service', status: 'in-progress' },269{ id: 3, title: 'Add unit tests', status: 'not-started' },270];271272export default defineThemedFixtureGroup({ path: 'chat/input/' }, {273Default: defineComponentFixture({ render: context => renderChatInput(context) }),274WithArtifacts: defineComponentFixture({ render: context => renderChatInput(context, { artifacts: sampleArtifacts }) }),275WithFileChanges: defineComponentFixture({276render: context => renderChatInput(context, { editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) })277}),278WithTodos: defineComponentFixture({279render: context => renderChatInput(context, { todos: sampleTodos })280}),281WithTodosAndFileChanges: defineComponentFixture({282render: context => renderChatInput(context, { todos: sampleTodos, editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) })283}),284WithArtifactsAndFileChanges: defineComponentFixture({285render: context => renderChatInput(context, { artifacts: sampleArtifacts, editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) })286}),287Full: defineComponentFixture({288render: context => renderChatInput(context, {289artifacts: sampleArtifacts,290editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]),291todos: sampleTodos,292})293}),294});295296297