Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/browser/newChatInput.ts
13401 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 './media/chatInput.css';
7
import * as dom from '../../../../base/browser/dom.js';
8
import { Codicon } from '../../../../base/common/codicons.js';
9
import { Emitter } from '../../../../base/common/event.js';
10
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
11
import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import { Button } from '../../../../base/browser/ui/button/button.js';
14
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
15
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
16
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
17
import { IModelService } from '../../../../editor/common/services/model.js';
18
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
19
import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';
20
import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js';
21
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
22
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
23
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
24
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
25
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
26
import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js';
27
import { AccessibilityCommandId } from '../../../../workbench/contrib/accessibility/common/accessibilityCommands.js';
28
import { ILogService } from '../../../../platform/log/common/log.js';
29
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
30
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
31
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
32
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
33
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
34
import { localize } from '../../../../nls.js';
35
import * as aria from '../../../../base/browser/ui/aria/aria.js';
36
import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js';
37
import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js';
38
import { NewChatContextAttachments } from './newChatContextAttachments.js';
39
import { SessionTypePicker } from './sessionTypePicker.js';
40
import { Menus } from '../../../browser/menus.js';
41
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
42
import { SlashCommandHandler } from './slashCommands.js';
43
import { VariableCompletionHandler } from './variableCompletions.js';
44
import { IChatModelInputState } from '../../../../workbench/contrib/chat/common/model/chatModel.js';
45
import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
46
import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
47
import { ChatHistoryNavigator } from '../../../../workbench/contrib/chat/common/widget/chatWidgetHistoryService.js';
48
import { IHistoryNavigationWidget } from '../../../../base/browser/history.js';
49
import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
50
import { autorun, IObservable } from '../../../../base/common/observable.js';
51
import { ChatInputNotificationWidget } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputNotificationWidget.js';
52
53
54
const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState';
55
const MIN_EDITOR_HEIGHT = 50;
56
const MAX_EDITOR_HEIGHT = 200;
57
58
interface IDraftState {
59
inputText: string;
60
attachments: readonly IChatRequestVariableEntry[];
61
}
62
63
/**
64
* Randomized, friendly placeholders shown in the new-session chat input
65
* to add a bit of personality. One is picked per widget instance, avoiding
66
* an immediate repeat of the previous pick.
67
*/
68
const RANDOM_PLACEHOLDERS = [
69
localize('sessionsChatInput.placeholder.whatAreYouBuilding', "What are you building?"),
70
localize('sessionsChatInput.placeholder.whatWillYouShipToday', "What will you ship today?"),
71
localize('sessionsChatInput.placeholder.describeWhatYouWantToBuild', "Describe what you want to build"),
72
localize('sessionsChatInput.placeholder.whatsYourNextMilestone', "What's your next milestone?"),
73
localize('sessionsChatInput.placeholder.whatAreYouTryingToAchieve', "What are you trying to achieve?"),
74
localize('sessionsChatInput.placeholder.pitchYourIdea', "Pitch your idea"),
75
localize('sessionsChatInput.placeholder.whatsTheGoal', "What's the goal?"),
76
localize('sessionsChatInput.placeholder.whatWillYouCreate', "What will you create?"),
77
localize('sessionsChatInput.placeholder.whatFeatureAreYouDreamingUp', "What feature are you dreaming up?"),
78
localize('sessionsChatInput.placeholder.describeTheOutcome', "Describe the outcome you want"),
79
localize('sessionsChatInput.placeholder.whatProblemAreYouSolving', "What problem are you solving?"),
80
localize('sessionsChatInput.placeholder.whatsNextOnYourRoadmap', "What's next on your roadmap?"),
81
localize('sessionsChatInput.placeholder.whatWouldYouLikeToAutomate', "What would you like to automate?"),
82
localize('sessionsChatInput.placeholder.whatWillYouLaunch', "What will you launch?"),
83
localize('sessionsChatInput.placeholder.describeYourMission', "Describe your mission"),
84
];
85
86
let lastPlaceholderIndex = -1;
87
function getRandomChatInputPlaceholder(): string {
88
let index = Math.floor(Math.random() * RANDOM_PLACEHOLDERS.length);
89
if (index === lastPlaceholderIndex) {
90
index = (index + 1) % RANDOM_PLACEHOLDERS.length;
91
}
92
lastPlaceholderIndex = index;
93
return RANDOM_PLACEHOLDERS[index];
94
}
95
96
// #region --- New Chat Widget ---
97
98
export class NewChatInputWidget extends Disposable implements IHistoryNavigationWidget {
99
100
readonly sessionTypePicker: SessionTypePicker;
101
102
// IHistoryNavigationWidget
103
private readonly _onDidFocus = this._register(new Emitter<void>());
104
readonly onDidFocus = this._onDidFocus.event;
105
private readonly _onDidBlur = this._register(new Emitter<void>());
106
readonly onDidBlur = this._onDidBlur.event;
107
get element(): HTMLElement { return this._editorContainer; }
108
109
// Input
110
private _editor!: CodeEditorWidget;
111
private _editorContainer!: HTMLElement;
112
113
// Send button
114
private _sendButton: Button | undefined;
115
private _sending = false;
116
117
// Loading state
118
private _loadingSpinner: HTMLElement | undefined;
119
private readonly _loadingDelayDisposable = this._register(new MutableDisposable());
120
121
// Attached context
122
private readonly _contextAttachments: NewChatContextAttachments;
123
124
// Slash commands
125
private _slashCommandHandler: SlashCommandHandler | undefined;
126
127
// Input state
128
private _draftState: IDraftState | undefined = {
129
inputText: '',
130
attachments: [],
131
};
132
133
// Input history
134
private readonly _history: ChatHistoryNavigator;
135
private _historyNavigationBackwardsEnablement!: IHistoryNavigationContext['historyNavigationBackwardsEnablement'];
136
private _historyNavigationForwardsEnablement!: IHistoryNavigationContext['historyNavigationForwardsEnablement'];
137
138
constructor(
139
private readonly options: {
140
getContextFolderUri: () => URI | undefined;
141
sendRequest: (query: string, attachments?: IChatRequestVariableEntry[]) => Promise<void>;
142
canSendRequest: IObservable<boolean>;
143
loading: IObservable<boolean>;
144
minEditorHeight?: number;
145
placeholder?: string;
146
},
147
@IInstantiationService private readonly instantiationService: IInstantiationService,
148
@IModelService private readonly modelService: IModelService,
149
@IConfigurationService private readonly configurationService: IConfigurationService,
150
@IContextKeyService private readonly contextKeyService: IContextKeyService,
151
@ILogService private readonly logService: ILogService,
152
@IHoverService private readonly hoverService: IHoverService,
153
@IStorageService private readonly storageService: IStorageService,
154
@IKeybindingService private readonly keybindingService: IKeybindingService,
155
) {
156
super();
157
this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
158
this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments));
159
this.sessionTypePicker = this._register(this.instantiationService.createInstance(SessionTypePicker));
160
this._register(this._contextAttachments.onDidChangeContext(() => {
161
this._updateDraftState();
162
this.focus();
163
}));
164
this._register(autorun(reader => {
165
this.options.canSendRequest.read(reader);
166
const isLoading = this.options.loading.read(reader);
167
this._loadingSpinner?.classList.toggle('visible', isLoading);
168
this._updateSendButtonState();
169
}));
170
}
171
172
// --- Rendering ---
173
174
render(parent: HTMLElement, root: HTMLElement): void {
175
// Input slot
176
const chatInputContainer = dom.append(parent, dom.$('.new-chat-input-container'));
177
178
// Overflow widget DOM node at the top level so the suggest widget
179
// is not clipped by any overflow:hidden ancestor.
180
const editorOverflowWidgetsDomNode = dom.append(root, dom.$('.sessions-chat-editor-overflow.monaco-editor'));
181
this._register({ dispose: () => editorOverflowWidgetsDomNode.remove() });
182
183
// Notification widget above the input area
184
const notificationContainer = dom.append(chatInputContainer, dom.$('.chat-input-notification-container'));
185
const notificationWidget = this._register(this.instantiationService.createInstance(ChatInputNotificationWidget));
186
notificationContainer.appendChild(notificationWidget.domNode);
187
188
// Input area inside the input slot
189
const inputArea = dom.append(chatInputContainer, dom.$('.new-chat-input-area'));
190
191
// Attachments row (pills only) inside input area, above editor
192
const attachRow = dom.append(inputArea, dom.$('.sessions-chat-attach-row'));
193
const attachedContextContainer = dom.append(attachRow, dom.$('.sessions-chat-attached-context'));
194
this._contextAttachments.renderAttachedContext(attachedContextContainer);
195
this._contextAttachments.registerDropTarget(root);
196
this._contextAttachments.registerPasteHandler(inputArea);
197
198
this._createEditor(inputArea, editorOverflowWidgetsDomNode);
199
this._createInputToolbar(inputArea);
200
201
const newChatBottomContainer = dom.append(parent, dom.$('.new-chat-bottom-container'));
202
const newChatControlsContainer = dom.append(newChatBottomContainer, dom.$('.new-chat-controls-container'));
203
this.sessionTypePicker.render(newChatControlsContainer);
204
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, dom.append(newChatControlsContainer, dom.$('')), Menus.NewSessionControl, {
205
hiddenItemStrategy: HiddenItemStrategy.NoHide,
206
}));
207
208
const repoConfigContainer = dom.append(newChatBottomContainer, dom.$('.new-chat-repo-config-container'));
209
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, repoConfigContainer, Menus.NewSessionRepositoryConfig, {
210
hiddenItemStrategy: HiddenItemStrategy.NoHide,
211
}));
212
213
// Restore draft input state from storage
214
this._restoreState();
215
216
// Layout editor after the input slot fade-in animation completes
217
this._register(dom.addDisposableListener(chatInputContainer, 'animationend', () => {
218
this._editor?.layout();
219
}, { once: true }));
220
}
221
222
private _updateInputLoadingState(): void {
223
const loading = this._sending;
224
if (loading) {
225
if (!this._loadingDelayDisposable.value) {
226
const timer = setTimeout(() => {
227
this._loadingDelayDisposable.clear();
228
if (this._sending) {
229
this._loadingSpinner?.classList.add('visible');
230
}
231
}, 500);
232
this._loadingDelayDisposable.value = toDisposable(() => clearTimeout(timer));
233
}
234
} else {
235
this._loadingDelayDisposable.clear();
236
this._loadingSpinner?.classList.remove('visible');
237
}
238
}
239
240
// --- Editor ---
241
242
private _getAriaLabel(): string {
243
const verbose = this.configurationService.getValue<boolean>(AccessibilityVerbositySettingId.SessionsChat);
244
if (verbose) {
245
const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
246
return kbLabel
247
? localize('chatInput.accessibilityHelp', "Chat input. Press Enter to send out the request. Use {0} for Chat Accessibility Help.", kbLabel)
248
: localize('chatInput.accessibilityHelpNoKb', "Chat input. Press Enter to send out the request. Use the Chat Accessibility Help command for more information.");
249
}
250
return localize('chatInput', "Chat input");
251
}
252
253
private _createEditor(container: HTMLElement, overflowWidgetsDomNode: HTMLElement): void {
254
const editorContainer = this._editorContainer = dom.append(container, dom.$('.sessions-chat-editor'));
255
const minHeight = this.options.minEditorHeight ?? MIN_EDITOR_HEIGHT;
256
editorContainer.style.height = `${minHeight}px`;
257
258
// Create scoped context key service and register history navigation
259
// BEFORE creating the editor, so the editor's context key scope is a child
260
const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(container));
261
const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this));
262
this._historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement;
263
this._historyNavigationForwardsEnablement = historyNavigationForwardsEnablement;
264
265
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])));
266
267
const uri = URI.from({ scheme: 'sessions-chat', path: `input-${Date.now()}` });
268
const textModel = this._register(this.modelService.createModel('', null, uri, true));
269
270
const editorOptions: IEditorConstructionOptions = {
271
...getSimpleEditorOptions(this.configurationService),
272
readOnly: false,
273
ariaLabel: this._getAriaLabel(),
274
placeholder: this.options.placeholder ?? getRandomChatInputPlaceholder(),
275
fontFamily: 'system-ui, -apple-system, sans-serif',
276
fontSize: 13,
277
lineHeight: 20,
278
cursorWidth: 1,
279
padding: { top: 8, bottom: 2 },
280
wrappingStrategy: 'advanced',
281
stickyScroll: { enabled: false },
282
renderWhitespace: 'none',
283
overflowWidgetsDomNode,
284
suggest: {
285
showIcons: true,
286
showSnippets: false,
287
showWords: true,
288
showStatusBar: false,
289
insertMode: 'insert',
290
},
291
};
292
293
const widgetOptions: ICodeEditorWidgetOptions = {
294
isSimpleWidget: true,
295
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
296
ContextMenuController.ID,
297
SuggestController.ID,
298
SnippetController2.ID,
299
PlaceholderTextContribution.ID,
300
]),
301
};
302
303
this._editor = this._register(scopedInstantiationService.createInstance(
304
CodeEditorWidget, editorContainer, editorOptions, widgetOptions,
305
));
306
this._editor.setModel(textModel);
307
308
// Ensure suggest widget renders above the input (not clipped by container)
309
SuggestController.get(this._editor)?.forceRenderingAbove();
310
311
// Update aria label when accessibility verbosity setting changes
312
this._register(this.configurationService.onDidChangeConfiguration(e => {
313
if (e.affectsConfiguration(AccessibilityVerbositySettingId.SessionsChat)) {
314
this._editor.updateOptions({ ariaLabel: this._getAriaLabel() });
315
}
316
}));
317
318
this._register(this._editor.onDidFocusEditorWidget(() => this._onDidFocus.fire()));
319
this._register(this._editor.onDidBlurEditorWidget(() => this._onDidBlur.fire()));
320
321
this._register(this._editor.onKeyDown(e => {
322
if (e.keyCode === KeyCode.Enter && !e.shiftKey && !e.ctrlKey && !e.altKey) {
323
// Don't send if the suggest widget is visible (let it accept the completion)
324
if (this._editor.contextKeyService.getContextKeyValue<boolean>('suggestWidgetVisible')) {
325
return;
326
}
327
e.preventDefault();
328
e.stopPropagation();
329
this._send();
330
}
331
if (e.keyCode === KeyCode.Enter && !e.shiftKey && !e.ctrlKey && e.altKey) {
332
e.preventDefault();
333
e.stopPropagation();
334
this._send();
335
}
336
// Cmd+/ / Ctrl+/ — open the context picker (same as the attach button)
337
if (e.equals(KeyMod.CtrlCmd | KeyCode.Slash)) {
338
e.preventDefault();
339
e.stopPropagation();
340
this._contextAttachments.showPicker(this.options.getContextFolderUri());
341
}
342
}));
343
344
// Update history navigation enablement based on cursor position
345
const updateHistoryNavigationEnablement = () => {
346
const model = this._editor.getModel();
347
const position = this._editor.getPosition();
348
if (!model || !position) {
349
return;
350
}
351
this._historyNavigationBackwardsEnablement.set(position.lineNumber === 1 && position.column === 1);
352
this._historyNavigationForwardsEnablement.set(position.lineNumber === model.getLineCount() && position.column === model.getLineMaxColumn(position.lineNumber));
353
};
354
this._register(this._editor.onDidChangeCursorPosition(() => updateHistoryNavigationEnablement()));
355
updateHistoryNavigationEnablement();
356
357
let previousHeight = -1;
358
this._register(this._editor.onDidContentSizeChange(e => {
359
if (!e.contentHeightChanged) {
360
return;
361
}
362
const contentHeight = this._editor.getContentHeight();
363
const clampedHeight = Math.min(MAX_EDITOR_HEIGHT, Math.max(this.options.minEditorHeight ?? MIN_EDITOR_HEIGHT, contentHeight));
364
if (clampedHeight === previousHeight) {
365
return;
366
}
367
previousHeight = clampedHeight;
368
this._editorContainer.style.height = `${clampedHeight}px`;
369
this._editor.layout();
370
}));
371
372
// Slash commands
373
this._slashCommandHandler = this._register(this.instantiationService.createInstance(SlashCommandHandler, this._editor));
374
375
// Variable completions (#file, #folder)
376
this._register(this.instantiationService.createInstance(
377
VariableCompletionHandler, this._editor, this._contextAttachments, () => this.options.getContextFolderUri(),
378
));
379
380
this._register(this._editor.onDidChangeModelContent(() => {
381
this._updateDraftState();
382
this._updateSendButtonState();
383
}));
384
}
385
386
private _createAttachButton(container: HTMLElement): void {
387
const attachButton = dom.append(container, dom.$('.sessions-chat-attach-button'));
388
const attachButtonLabel = localize('addContext', "Add Context...");
389
attachButton.tabIndex = 0;
390
attachButton.role = 'button';
391
attachButton.ariaLabel = attachButtonLabel;
392
this._register(this.hoverService.setupDelayedHover(attachButton, {
393
content: attachButtonLabel,
394
position: { hoverPosition: HoverPosition.BELOW },
395
appearance: { showPointer: true }
396
}));
397
dom.append(attachButton, renderIcon(Codicon.add));
398
this._register(dom.addDisposableListener(attachButton, dom.EventType.CLICK, () => {
399
this._contextAttachments.showPicker(this.options.getContextFolderUri());
400
}));
401
}
402
403
private _createInputToolbar(container: HTMLElement): void {
404
const toolbar = dom.append(container, dom.$('.sessions-chat-toolbar'));
405
406
this._createAttachButton(toolbar);
407
408
// Session config pickers (mode, model) — rendered via MenuWorkbenchToolBar
409
// Visibility controlled by context keys (isActiveSessionBackgroundProvider, isNewChatSession)
410
const configContainer = dom.append(toolbar, dom.$('.sessions-chat-config-toolbar'));
411
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, configContainer, Menus.NewSessionConfig, {
412
hiddenItemStrategy: HiddenItemStrategy.NoHide,
413
}));
414
415
dom.append(toolbar, dom.$('.sessions-chat-toolbar-spacer'));
416
417
this._loadingSpinner = dom.append(toolbar, dom.$('.sessions-chat-loading-spinner'));
418
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._loadingSpinner, localize('loading', "Loading...")));
419
420
const sendButtonContainer = dom.append(toolbar, dom.$('.sessions-chat-send-button'));
421
const sendButton = this._sendButton = this._register(new Button(sendButtonContainer, {
422
secondary: true,
423
title: localize('send', "Send"),
424
ariaLabel: localize('send', "Send"),
425
}));
426
sendButton.icon = Codicon.arrowUp;
427
this._register(sendButton.onDidClick(() => this._send()));
428
}
429
430
// --- Input History (IHistoryNavigationWidget) ---
431
432
showPreviousValue(): void {
433
if (this._history.isAtStart()) {
434
return;
435
}
436
if (this._draftState?.inputText || this._draftState?.attachments.length) {
437
this._history.overlay(this._toHistoryEntry(this._draftState));
438
}
439
this._navigateHistory(true);
440
}
441
442
showNextValue(): void {
443
if (this._history.isAtEnd()) {
444
return;
445
}
446
if (this._draftState?.inputText || this._draftState?.attachments.length) {
447
this._history.overlay(this._toHistoryEntry(this._draftState));
448
}
449
this._navigateHistory(false);
450
}
451
452
private _updateDraftState(): void {
453
this._draftState = {
454
inputText: this._editor?.getModel()?.getValue() ?? '',
455
attachments: [...this._contextAttachments.attachments],
456
};
457
}
458
459
private _toHistoryEntry(draft: IDraftState): IChatModelInputState {
460
return {
461
...draft,
462
mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent },
463
selectedModel: undefined,
464
selections: [],
465
contrib: {},
466
};
467
}
468
469
private _navigateHistory(previous: boolean): void {
470
const entry = previous ? this._history.previous() : this._history.next();
471
const inputText = entry?.inputText ?? '';
472
if (entry) {
473
this._editor?.getModel()?.setValue(inputText);
474
this._contextAttachments.setAttachments(entry.attachments);
475
}
476
aria.status(inputText);
477
if (previous) {
478
this._editor.setPosition({ lineNumber: 1, column: 1 });
479
} else {
480
const model = this._editor.getModel();
481
if (model) {
482
const lastLine = model.getLineCount();
483
this._editor.setPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) });
484
}
485
}
486
}
487
488
// --- Send ---
489
490
491
private async _send(): Promise<void> {
492
let query = this._editor.getModel()?.getValue().trim();
493
if (!query || this._sending) {
494
return;
495
}
496
497
// Check for slash commands first
498
if (this._slashCommandHandler?.tryExecuteSlashCommand(query)) {
499
this._editor.getModel()?.setValue('');
500
return;
501
}
502
503
// Expand prompt/skill slash commands into a CLI-friendly reference
504
const expanded = this._slashCommandHandler?.tryExpandPromptSlashCommand(query);
505
if (expanded) {
506
query = expanded;
507
}
508
509
const attachedContext = this._contextAttachments.attachments.length > 0
510
? [...this._contextAttachments.attachments]
511
: undefined;
512
513
if (this._draftState) {
514
this._history.append(this._toHistoryEntry(this._draftState));
515
}
516
this._clearDraftState();
517
518
this._sending = true;
519
this._editor.updateOptions({ readOnly: true });
520
this._updateSendButtonState();
521
this._updateInputLoadingState();
522
523
try {
524
await this.options.sendRequest(query, attachedContext);
525
this._contextAttachments.clear();
526
this._editor.getModel()?.setValue('');
527
} catch (e) {
528
this.logService.error('Failed to send request:', e);
529
}
530
531
this._sending = false;
532
this._editor.updateOptions({ readOnly: false });
533
this._updateSendButtonState();
534
this._updateInputLoadingState();
535
}
536
537
private _updateSendButtonState(): void {
538
if (!this._sendButton) {
539
return;
540
}
541
const hasText = !!this._editor?.getModel()?.getValue().trim();
542
this._sendButton.enabled = !this._sending && hasText && this.options.canSendRequest.get();
543
}
544
545
private _restoreState(): void {
546
const draft = this._getDraftState();
547
if (draft) {
548
this._editor?.getModel()?.setValue(draft.inputText);
549
if (draft.attachments?.length) {
550
this._contextAttachments.setAttachments(draft.attachments.map(IChatRequestVariableEntry.fromExport));
551
}
552
}
553
}
554
555
private _getDraftState(): IDraftState | undefined {
556
const raw = this.storageService.get(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE);
557
if (!raw) {
558
return undefined;
559
}
560
try {
561
return JSON.parse(raw);
562
} catch {
563
return undefined;
564
}
565
}
566
567
private _clearDraftState(): void {
568
this._draftState = { inputText: '', attachments: [] };
569
this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(this._draftState), StorageScope.WORKSPACE, StorageTarget.MACHINE);
570
}
571
572
saveState(): void {
573
if (this._draftState) {
574
const state = {
575
...this._draftState,
576
attachments: this._draftState.attachments.map(IChatRequestVariableEntry.toExport),
577
};
578
this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE);
579
}
580
}
581
582
layout(_height: number, _width: number): void {
583
this._editor?.layout();
584
}
585
586
focus(): void {
587
this._editor?.focus();
588
}
589
590
prefillInput(text: string): void {
591
const editor = this._editor;
592
const model = editor?.getModel();
593
if (editor && model) {
594
model.setValue(text);
595
const lastLine = model.getLineCount();
596
const maxColumn = model.getLineMaxColumn(lastLine);
597
editor.setPosition({ lineNumber: lastLine, column: maxColumn });
598
editor.focus();
599
}
600
}
601
602
sendQuery(text: string): void {
603
const model = this._editor?.getModel();
604
if (model) {
605
model.setValue(text);
606
this._send();
607
}
608
}
609
}
610
611
// #endregion
612
613