mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-02 23:05:15 +01:00
move questions carousel above input part + many ux fixes (#292990)
* move questions carousel to input part, many fixes * some fixes
This commit is contained in:
+59
-31
@@ -5,8 +5,6 @@
|
||||
|
||||
import * as dom from '../../../../../../base/browser/dom.js';
|
||||
import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js';
|
||||
import { getBaseLayerHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
|
||||
import { Emitter, Event } from '../../../../../../base/common/event.js';
|
||||
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
@@ -21,6 +19,8 @@ import { IChatContentPart, IChatContentPartRenderContext } from './chatContentPa
|
||||
import { IChatRendererContent, isResponseVM } from '../../../common/model/chatViewModel.js';
|
||||
import { ChatTreeItem } from '../../chat.js';
|
||||
import { Codicon } from '../../../../../../base/common/codicons.js';
|
||||
import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js';
|
||||
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
|
||||
import './media/chatQuestionCarousel.css';
|
||||
|
||||
export interface IChatQuestionCarouselOptions {
|
||||
@@ -44,6 +44,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
private _navigationButtons: HTMLElement | undefined;
|
||||
private _prevButton: Button | undefined;
|
||||
private _nextButton: Button | undefined;
|
||||
private readonly _nextButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable());
|
||||
private _skipAllButton: Button | undefined;
|
||||
|
||||
private _isSkipped = false;
|
||||
@@ -61,9 +62,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
private readonly _interactiveUIStore: MutableDisposable<DisposableStore> = this._register(new MutableDisposable());
|
||||
|
||||
constructor(
|
||||
private readonly carousel: IChatQuestionCarousel,
|
||||
public readonly carousel: IChatQuestionCarousel,
|
||||
context: IChatContentPartRenderContext,
|
||||
private readonly _options: IChatQuestionCarouselOptions,
|
||||
@IHoverService private readonly _hoverService: IHoverService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -98,10 +100,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
if (carousel.allowSkip) {
|
||||
this._closeButtonContainer = dom.$('.chat-question-close-container');
|
||||
const skipAllTitle = localize('chat.questionCarousel.skipAllTitle', 'Skip all questions');
|
||||
const skipAllButton = interactiveStore.add(new Button(this._closeButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: skipAllTitle }));
|
||||
const skipAllButton = interactiveStore.add(new Button(this._closeButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true }));
|
||||
skipAllButton.label = `$(${Codicon.close.id})`;
|
||||
skipAllButton.element.classList.add('chat-question-nav-arrow', 'chat-question-close');
|
||||
skipAllButton.element.setAttribute('aria-label', skipAllTitle);
|
||||
interactiveStore.add(this._hoverService.setupDelayedHover(skipAllButton.element, { content: skipAllTitle }));
|
||||
this._skipAllButton = skipAllButton;
|
||||
}
|
||||
|
||||
@@ -121,14 +124,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const arrowsContainer = dom.$('.chat-question-nav-arrows');
|
||||
|
||||
const previousLabel = localize('previous', 'Previous');
|
||||
const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: previousLabel }));
|
||||
const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true }));
|
||||
prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev');
|
||||
prevButton.label = `$(${Codicon.chevronLeft.id})`;
|
||||
prevButton.element.setAttribute('aria-label', previousLabel);
|
||||
interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabel }));
|
||||
this._prevButton = prevButton;
|
||||
|
||||
const nextLabel = localize('next', 'Next');
|
||||
const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: nextLabel }));
|
||||
const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true }));
|
||||
nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next');
|
||||
nextButton.label = `$(${Codicon.chevronRight.id})`;
|
||||
this._nextButton = nextButton;
|
||||
@@ -430,16 +433,16 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const nextLabel = localize('next', 'Next');
|
||||
if (isLastQuestion) {
|
||||
this._nextButton!.label = submitLabel;
|
||||
this._nextButton!.element.title = submitLabel;
|
||||
this._nextButton!.element.setAttribute('aria-label', submitLabel);
|
||||
// Switch to primary style for submit
|
||||
this._nextButton!.element.classList.add('chat-question-nav-submit');
|
||||
this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: submitLabel });
|
||||
} else {
|
||||
this._nextButton!.label = `$(${Codicon.chevronRight.id})`;
|
||||
this._nextButton!.element.title = nextLabel;
|
||||
this._nextButton!.element.setAttribute('aria-label', nextLabel);
|
||||
// Keep secondary style for next
|
||||
this._nextButton!.element.classList.remove('chat-question-nav-submit');
|
||||
this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabel });
|
||||
}
|
||||
|
||||
this._onDidChangeHeight.fire();
|
||||
@@ -520,7 +523,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
options.forEach((option, index) => {
|
||||
if (previousSelectedValue !== undefined && option.value === previousSelectedValue) {
|
||||
selectedIndex = index;
|
||||
} else if (selectedIndex === -1 && defaultOptionId !== undefined && option.id === defaultOptionId) {
|
||||
} else if (selectedIndex === -1 && !previousFreeform && defaultOptionId !== undefined && option.id === defaultOptionId) {
|
||||
selectedIndex = index;
|
||||
}
|
||||
});
|
||||
@@ -589,13 +592,22 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
listItem.classList.add('selected');
|
||||
}
|
||||
|
||||
this._inputBoxes.add(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), listItem, option.label));
|
||||
|
||||
// Click handler
|
||||
// if we select an option, clear text and go to next question
|
||||
this._inputBoxes.add(dom.addDisposableListener(listItem, dom.EventType.CLICK, (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateSelection(index);
|
||||
const freeform = this._freeformTextareas.get(question.id);
|
||||
if (freeform) {
|
||||
freeform.value = '';
|
||||
}
|
||||
this.handleNext();
|
||||
}));
|
||||
|
||||
this._inputBoxes.add(this._hoverService.setupDelayedHover(listItem, {
|
||||
content: option.label,
|
||||
position: { hoverPosition: HoverPosition.BELOW },
|
||||
appearance: { showPointer: true }
|
||||
}));
|
||||
|
||||
selectContainer.appendChild(listItem);
|
||||
@@ -683,16 +695,22 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize()));
|
||||
}
|
||||
|
||||
// focus on the row when first rendered
|
||||
if (this._options.shouldAutoFocus !== false && listItems.length > 0) {
|
||||
const focusIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
||||
// if no default, select the first answer
|
||||
if (selectedIndex < 0) {
|
||||
updateSelection(0);
|
||||
// focus on the row when first rendered or textarea if it has content
|
||||
if (this._options.shouldAutoFocus !== false) {
|
||||
if (previousFreeform) {
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => {
|
||||
freeformTextarea.focus();
|
||||
}));
|
||||
} else if (listItems.length > 0) {
|
||||
const focusIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
||||
// if no default and no freeform text, select the first answer
|
||||
if (selectedIndex < 0) {
|
||||
updateSelection(0);
|
||||
}
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(selectContainer), () => {
|
||||
listItems[focusIndex]?.focus();
|
||||
}));
|
||||
}
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(selectContainer), () => {
|
||||
listItems[focusIndex]?.focus();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,7 +747,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
let isChecked = false;
|
||||
if (previousSelectedValues && previousSelectedValues.length > 0) {
|
||||
isChecked = previousSelectedValues.includes(option.value);
|
||||
} else if (defaultOptionIds.includes(option.id)) {
|
||||
} else if (!previousFreeform && defaultOptionIds.includes(option.id)) {
|
||||
isChecked = true;
|
||||
}
|
||||
|
||||
@@ -791,7 +809,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
}
|
||||
}));
|
||||
|
||||
this._inputBoxes.add(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), listItem, option.label));
|
||||
this._inputBoxes.add(this._hoverService.setupDelayedHover(listItem, {
|
||||
content: option.label,
|
||||
position: { hoverPosition: HoverPosition.BELOW },
|
||||
appearance: { showPointer: true }
|
||||
}));
|
||||
|
||||
selectContainer.appendChild(listItem);
|
||||
checkboxes.push(checkbox);
|
||||
@@ -868,13 +890,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize()));
|
||||
}
|
||||
|
||||
// Focus on the appropriate row when rendered (first checked row, or first row if none)
|
||||
if (this._options.shouldAutoFocus !== false && listItems.length > 0) {
|
||||
const initialFocusIndex = firstCheckedIndex >= 0 ? firstCheckedIndex : 0;
|
||||
focusedIndex = initialFocusIndex;
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(selectContainer), () => {
|
||||
listItems[initialFocusIndex]?.focus();
|
||||
}));
|
||||
// Focus on the appropriate row when rendered or textarea if it has content
|
||||
if (this._options.shouldAutoFocus !== false) {
|
||||
if (previousFreeform) {
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => {
|
||||
freeformTextarea.focus();
|
||||
}));
|
||||
} else if (listItems.length > 0) {
|
||||
const initialFocusIndex = firstCheckedIndex >= 0 ? firstCheckedIndex : 0;
|
||||
focusedIndex = initialFocusIndex;
|
||||
this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(selectContainer), () => {
|
||||
listItems[initialFocusIndex]?.focus();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-22
@@ -62,10 +62,14 @@
|
||||
.chat-question-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding-bottom: 12px;
|
||||
padding-bottom: 5px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
border-bottom: 1px solid var(--vscode-chat-requestBorder);
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 16px;
|
||||
border-top: 1px solid var(--vscode-chat-requestBorder);
|
||||
background: var(--vscode-chat-requestBackground);
|
||||
}
|
||||
@@ -172,7 +176,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vscode-chat-requestBackground);
|
||||
padding: 12px 16px;
|
||||
padding: 8px 16px 10px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -214,7 +218,7 @@
|
||||
.chat-question-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
gap: 3px;
|
||||
outline: none;
|
||||
padding: 4px 0;
|
||||
}
|
||||
@@ -228,7 +232,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
@@ -238,7 +242,7 @@
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.interactive-session .interactive-response .value {
|
||||
.interactive-input-part .chat-question-carousel-widget-container .chat-question-input-container {
|
||||
.chat-question-list-item:focus:not(.selected),
|
||||
.chat-question-list:focus {
|
||||
outline: none;
|
||||
@@ -270,7 +274,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
padding: 2px 4px;
|
||||
padding: 0px 4px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
@@ -285,7 +289,6 @@
|
||||
}
|
||||
|
||||
.chat-question-freeform-number {
|
||||
margin-top: 4px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -325,16 +328,6 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.chat-question-list-checkbox.monaco-custom-toggle.checked {
|
||||
background-color: var(--vscode-button-background) !important;
|
||||
border-color: var(--vscode-button-background) !important;
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.chat-question-list-checkbox.monaco-custom-toggle.checked .codicon {
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
}
|
||||
|
||||
/* Label in list item */
|
||||
.chat-question-list-label {
|
||||
@@ -457,7 +450,7 @@
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -468,9 +461,9 @@
|
||||
|
||||
.chat-question-freeform-textarea {
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
min-height: 24px;
|
||||
max-height: 200px;
|
||||
padding: 6px 8px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--vscode-input-border, var(--vscode-chat-requestBorder));
|
||||
background-color: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
|
||||
@@ -1967,46 +1967,101 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
|
||||
const widget = isResponseVM(context.element) ? this.chatWidgetService.getWidgetBySessionResource(context.element.sessionResource) : undefined;
|
||||
const shouldAutoFocus = widget ? widget.getInput() === '' : true;
|
||||
const responseId = isResponseVM(context.element) ? context.element.requestId : undefined;
|
||||
|
||||
const part = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, {
|
||||
shouldAutoFocus,
|
||||
onSubmit: async (answers) => {
|
||||
// Mark the carousel as used and store the answers
|
||||
const answersRecord = answers ? Object.fromEntries(answers) : undefined;
|
||||
if (answersRecord) {
|
||||
carousel.data = answersRecord;
|
||||
}
|
||||
carousel.isUsed = true;
|
||||
|
||||
// Notify the extension about the carousel answers to resolve the deferred promise
|
||||
if (isResponseVM(context.element) && carousel.resolveId) {
|
||||
this.chatService.notifyQuestionCarouselAnswer(context.element.requestId, carousel.resolveId, answersRecord);
|
||||
}
|
||||
|
||||
// Remove from pending carousels
|
||||
this.removeCarouselFromTracking(context, part);
|
||||
const handleSubmit = async (answers: Map<string, unknown> | undefined, part: ChatQuestionCarouselPart) => {
|
||||
// Mark the carousel as used and store the answers
|
||||
const answersRecord = answers ? Object.fromEntries(answers) : undefined;
|
||||
if (answersRecord) {
|
||||
carousel.data = answersRecord;
|
||||
}
|
||||
carousel.isUsed = true;
|
||||
|
||||
// Notify the extension about the carousel answers to resolve the deferred promise
|
||||
if (isResponseVM(context.element) && carousel.resolveId) {
|
||||
this.chatService.notifyQuestionCarouselAnswer(context.element.requestId, carousel.resolveId, answersRecord);
|
||||
}
|
||||
|
||||
// Remove from pending carousels
|
||||
this.removeCarouselFromTracking(context, part);
|
||||
|
||||
// Clear from input part (always clear on submit, no response check needed)
|
||||
widget?.input.clearQuestionCarousel();
|
||||
};
|
||||
|
||||
// If carousel is already used or response is complete/canceled, render summary inline in the list
|
||||
const responseIsComplete = isResponseVM(context.element) && context.element.isComplete;
|
||||
const inputPartHasCarousel = widget?.input.questionCarousel !== undefined;
|
||||
|
||||
if (carousel.isUsed || responseIsComplete) {
|
||||
// Clear the carousel from input part when response completes (stopped/canceled)
|
||||
// Only clear if this response's carousel is currently displayed (pass responseId)
|
||||
if (responseIsComplete && inputPartHasCarousel && responseId) {
|
||||
widget?.input.clearQuestionCarousel(responseId);
|
||||
}
|
||||
|
||||
const part = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, {
|
||||
shouldAutoFocus: false,
|
||||
onSubmit: async (answers) => handleSubmit(answers, part)
|
||||
});
|
||||
return part;
|
||||
}
|
||||
|
||||
// Render the active carousel in the input part (above the input box)
|
||||
const part = widget?.input.renderQuestionCarousel(carousel, context, {
|
||||
shouldAutoFocus,
|
||||
onSubmit: async (answers) => handleSubmit(answers, part!)
|
||||
});
|
||||
|
||||
// If we couldn't render in the input part, fall back to inline rendering
|
||||
if (!part) {
|
||||
const fallbackPart = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, {
|
||||
shouldAutoFocus,
|
||||
onSubmit: async (answers) => handleSubmit(answers, fallbackPart)
|
||||
});
|
||||
return fallbackPart;
|
||||
}
|
||||
|
||||
// If global auto-approve (yolo mode) is enabled, skip with defaults immediately
|
||||
if (!carousel.isUsed && this.configService.getValue<boolean>(ChatConfiguration.GlobalAutoApprove)) {
|
||||
part.skip();
|
||||
}
|
||||
|
||||
// Track the carousel for auto-skip when user submits a new message
|
||||
// Only add tracking if not already tracked (prevents duplicate tracking on re-render)
|
||||
if (isResponseVM(context.element) && carousel.allowSkip && !carousel.isUsed) {
|
||||
let carousels = this.pendingQuestionCarousels.get(context.element.sessionResource);
|
||||
if (!carousels) {
|
||||
carousels = new Set();
|
||||
this.pendingQuestionCarousels.set(context.element.sessionResource, carousels);
|
||||
}
|
||||
carousels.add(part);
|
||||
if (!carousels.has(part)) {
|
||||
carousels.add(part);
|
||||
|
||||
// Clean up when the part is disposed
|
||||
part.addDisposable({ dispose: () => this.removeCarouselFromTracking(context, part) });
|
||||
// Clean up when the part is disposed
|
||||
part.addDisposable({ dispose: () => this.removeCarouselFromTracking(context, part) });
|
||||
}
|
||||
}
|
||||
|
||||
return part;
|
||||
// Return a placeholder that will re-render as a summary when the carousel is used or response is complete/stopped
|
||||
return this.renderNoContent((other, _followingContent, element) => {
|
||||
// Re-render (return false) if:
|
||||
// - carousel was used/submitted
|
||||
// - response is complete (stopped)
|
||||
if (carousel.isUsed || (isResponseVM(element) && element.isComplete)) {
|
||||
return false;
|
||||
}
|
||||
// Use resolveId for comparison instead of object identity to handle re-rendering during scrolling
|
||||
if (other.kind === 'questionCarousel') {
|
||||
const otherCarousel = other as IChatQuestionCarousel;
|
||||
// Compare by resolveId if available, otherwise fall back to object identity
|
||||
if (carousel.resolveId && otherCarousel.resolveId) {
|
||||
return carousel.resolveId === otherCarousel.resolveId;
|
||||
}
|
||||
return other === carousel;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private _notifyOnQuestionCarousel(context: IChatContentPartRenderContext, carousel: IChatQuestionCarousel): void {
|
||||
|
||||
@@ -85,14 +85,14 @@ import { IChatViewTitleActionContext } from '../../../common/actions/chatActions
|
||||
import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js';
|
||||
import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js';
|
||||
import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js';
|
||||
import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js';
|
||||
import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js';
|
||||
import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js';
|
||||
import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js';
|
||||
import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js';
|
||||
import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js';
|
||||
import { getChatSessionType } from '../../../common/model/chatUri.js';
|
||||
import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js';
|
||||
import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chatViewModel.js';
|
||||
import { IChatAgentService } from '../../../common/participants/chatAgents.js';
|
||||
import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js';
|
||||
import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js';
|
||||
@@ -110,6 +110,8 @@ import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../c
|
||||
import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js';
|
||||
import { IChatContextService } from '../../contextContrib/chatContextService.js';
|
||||
import { IDisposableReference } from '../chatContentParts/chatCollections.js';
|
||||
import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../chatContentParts/chatQuestionCarouselPart.js';
|
||||
import { IChatContentPartRenderContext } from '../chatContentParts/chatContentParts.js';
|
||||
import { CollapsibleListPool, IChatCollapsibleListItem } from '../chatContentParts/chatReferencesContentPart.js';
|
||||
import { ChatTodoListWidget } from '../chatContentParts/chatTodoListWidget.js';
|
||||
import { ChatDragAndDrop } from '../chatDragAndDrop.js';
|
||||
@@ -203,6 +205,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
private _workingSetCollapsed = observableValue('chatInputPart.workingSetCollapsed', true);
|
||||
private readonly _chatInputTodoListWidget = this._register(new MutableDisposable<ChatTodoListWidget>());
|
||||
private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable<ChatQuestionCarouselPart>());
|
||||
private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore());
|
||||
private _currentQuestionCarouselResponseId: string | undefined;
|
||||
private readonly _chatEditingTodosDisposables = this._register(new DisposableStore());
|
||||
private _lastEditingSessionResource: URI | undefined;
|
||||
|
||||
@@ -283,6 +288,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
private chatEditingSessionWidgetContainer!: HTMLElement;
|
||||
private chatInputTodoListWidgetContainer!: HTMLElement;
|
||||
private chatQuestionCarouselContainer!: HTMLElement;
|
||||
private chatInputWidgetsContainer!: HTMLElement;
|
||||
private readonly _widgetController = this._register(new MutableDisposable<ChatInputPartWidgetController>());
|
||||
|
||||
@@ -1733,12 +1739,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.refreshChatSessionPickers();
|
||||
this.tryUpdateWidgetController();
|
||||
this.updateContextUsageWidget();
|
||||
this.clearQuestionCarousel();
|
||||
}));
|
||||
|
||||
let elements;
|
||||
if (this.options.renderStyle === 'compact') {
|
||||
elements = dom.h('.interactive-input-part', [
|
||||
dom.h('.interactive-input-and-edit-session', [
|
||||
dom.h('.chat-question-carousel-widget-container@chatQuestionCarouselContainer'),
|
||||
dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'),
|
||||
dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'),
|
||||
dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),
|
||||
@@ -1758,6 +1766,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
]);
|
||||
} else {
|
||||
elements = dom.h('.interactive-input-part', [
|
||||
dom.h('.chat-question-carousel-widget-container@chatQuestionCarouselContainer'),
|
||||
dom.h('.interactive-input-followups@followupsContainer'),
|
||||
dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'),
|
||||
dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'),
|
||||
@@ -1795,6 +1804,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
const attachmentToolbarContainer = elements.attachmentToolbar;
|
||||
this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;
|
||||
this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer;
|
||||
this.chatQuestionCarouselContainer = elements.chatQuestionCarouselContainer;
|
||||
this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer;
|
||||
this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer;
|
||||
|
||||
@@ -2400,6 +2410,48 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this._chatInputTodoListWidget.value?.clear(sessionResource, force);
|
||||
}
|
||||
|
||||
renderQuestionCarousel(carousel: IChatQuestionCarousel, context: IChatContentPartRenderContext, options: IChatQuestionCarouselOptions): ChatQuestionCarouselPart {
|
||||
|
||||
if (this._chatQuestionCarouselWidget.value) {
|
||||
const existingCarousel = this._chatQuestionCarouselWidget.value;
|
||||
const existingResolveId = existingCarousel.carousel.resolveId;
|
||||
if (existingResolveId && carousel.resolveId && existingResolveId === carousel.resolveId) {
|
||||
return existingCarousel;
|
||||
}
|
||||
this.clearQuestionCarousel();
|
||||
}
|
||||
|
||||
// track the response id and session
|
||||
this._currentQuestionCarouselResponseId = isResponseVM(context.element) ? context.element.requestId : undefined;
|
||||
|
||||
const part = this._chatQuestionCarouselDisposables.add(
|
||||
this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options)
|
||||
);
|
||||
this._chatQuestionCarouselWidget.value = part;
|
||||
|
||||
dom.clearNode(this.chatQuestionCarouselContainer);
|
||||
dom.append(this.chatQuestionCarouselContainer, part.domNode);
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
clearQuestionCarousel(responseId?: string): void {
|
||||
if (responseId && this._currentQuestionCarouselResponseId !== responseId) {
|
||||
return;
|
||||
}
|
||||
this._chatQuestionCarouselDisposables.clear();
|
||||
this._chatQuestionCarouselWidget.clear();
|
||||
this._currentQuestionCarouselResponseId = undefined;
|
||||
dom.clearNode(this.chatQuestionCarouselContainer);
|
||||
}
|
||||
get questionCarouselResponseId(): string | undefined {
|
||||
return this._currentQuestionCarouselResponseId;
|
||||
}
|
||||
|
||||
get questionCarousel(): ChatQuestionCarouselPart | undefined {
|
||||
return this._chatQuestionCarouselWidget.value;
|
||||
}
|
||||
|
||||
setWorkingSetCollapsed(collapsed: boolean): void {
|
||||
this._workingSetCollapsed.set(collapsed, undefined);
|
||||
}
|
||||
|
||||
@@ -1053,6 +1053,23 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* question carousel - this is above edits and todos */
|
||||
.interactive-session .interactive-input-part > .chat-question-carousel-widget-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.interactive-session .interactive-input-part > .chat-question-carousel-widget-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-container {
|
||||
margin: 0px;
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Chat Todo List Widget Container - mirrors chat-editing-session styling */
|
||||
.interactive-session .interactive-input-part > .chat-todo-list-widget-container {
|
||||
margin-bottom: -4px;
|
||||
|
||||
@@ -1354,7 +1354,7 @@ interface ISerializableChatResponseData {
|
||||
timeSpentWaiting?: number;
|
||||
}
|
||||
|
||||
export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized;
|
||||
export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel;
|
||||
|
||||
export interface ISerializableChatRequestData extends ISerializableChatResponseData {
|
||||
requestId: string;
|
||||
|
||||
+1
-1
@@ -253,7 +253,7 @@ suite('ChatQuestionCarouselPart', () => {
|
||||
// Use dedicated class selector for stability
|
||||
const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLElement;
|
||||
assert.ok(nextButton, 'Next button should exist');
|
||||
assert.strictEqual(nextButton.title, 'Submit', 'Next button should have Submit title on last question');
|
||||
assert.strictEqual(nextButton.getAttribute('aria-label'), 'Submit', 'Next button should have Submit aria-label on last question');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user