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