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:
Justin Chen
2026-02-05 13:17:42 -08:00
committed by GitHub
parent 2b09c1f20a
commit 8470b2cbd7
7 changed files with 223 additions and 78 deletions
@@ -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();
}));
}
}
}
@@ -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;
@@ -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');
});
});