mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
Question carousel UI polish (#299272)
* Polish question carousel: simplify title bar, footer nav, and layout * Question carousel UI polish - Border radius matches chat input (cornerRadius-large) - Background uses panel background - Remove colon prefix from option descriptions - Option list items use cornerRadius-medium - Footer padding: 8px left, 16px right - 12px gap between number and labels - Freeform row aligned with preset options - Close button vertically centered in titlebar - Checkboxes center-aligned in list items - has-description class for title+description items - Number elements use consistent width - Focus outline consistent across all list items - Tighter gap between presets and custom answer - Summary Q/A always on separate rows - Hide submit icon when carousel is open (show stop only) - Show submit when user types to steer * Add close button to single-question carousel title row * Add submit footer for single-question multi-select carousels * Align single-question submit footer to the right with hint * Fix failing carousel unit tests Update test selectors and structure to match current DOM: - Remove .chat-question-carousel-nav assertion (element no longer exists) - Update markdown/message tests to use .chat-question-title - Fix nav button tests to use multi-question carousels with .chat-question-nav-arrow - Fix submit button test to use multi-question carousel * Fix chat question carousel navigation and summary test regressions
This commit is contained in:
@@ -192,7 +192,11 @@ const requestInProgressWithoutInput = ContextKeyExpr.and(
|
||||
);
|
||||
const pendingToolCall = ContextKeyExpr.or(
|
||||
ChatContextKeys.Editing.hasToolConfirmation,
|
||||
ChatContextKeys.Editing.hasQuestionCarousel,
|
||||
ContextKeyExpr.and(ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.inputHasText.negate()),
|
||||
);
|
||||
const noQuestionCarouselOrHasInput = ContextKeyExpr.or(
|
||||
ChatContextKeys.Editing.hasQuestionCarousel.negate(),
|
||||
ChatContextKeys.inputHasText,
|
||||
);
|
||||
const whenNotInProgress = ChatContextKeys.requestInProgress.negate();
|
||||
|
||||
@@ -235,6 +239,7 @@ export class ChatSubmitAction extends SubmitAction {
|
||||
whenNotInProgress,
|
||||
menuCondition,
|
||||
ChatContextKeys.withinEditSessionDiff.negate(),
|
||||
noQuestionCarouselOrHasInput,
|
||||
),
|
||||
group: 'navigation',
|
||||
alt: {
|
||||
@@ -762,7 +767,8 @@ export class ChatEditingSessionSubmitAction extends SubmitAction {
|
||||
order: 4,
|
||||
when: ContextKeyExpr.and(
|
||||
notInProgressOrEditing,
|
||||
menuCondition),
|
||||
menuCondition,
|
||||
noQuestionCarouselOrHasInput),
|
||||
group: 'navigation',
|
||||
alt: {
|
||||
id: 'workbench.action.chat.sendToNewChat',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js';
|
||||
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../../base/common/htmlContent.js';
|
||||
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../../../../base/common/platform.js';
|
||||
import { hasKey } from '../../../../../../base/common/types.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js';
|
||||
@@ -53,12 +54,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
private _closeButtonContainer: HTMLElement | undefined;
|
||||
private _footerRow: HTMLElement | undefined;
|
||||
private _stepIndicator: HTMLElement | undefined;
|
||||
private _navigationButtons: HTMLElement | undefined;
|
||||
private _submitHint: HTMLElement | undefined;
|
||||
private _submitButton: Button | undefined;
|
||||
private _prevButton: Button | undefined;
|
||||
private _nextButton: Button | undefined;
|
||||
private readonly _nextButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable());
|
||||
private _submitButton: Button | undefined;
|
||||
private readonly _submitButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable());
|
||||
private _skipAllButton: Button | undefined;
|
||||
|
||||
private _isSkipped = false;
|
||||
@@ -147,69 +146,36 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const skipAllTitle = localize('chat.questionCarousel.skipAllTitle', 'Skip all questions');
|
||||
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.classList.add('chat-question-close');
|
||||
skipAllButton.element.setAttribute('aria-label', skipAllTitle);
|
||||
interactiveStore.add(this._hoverService.setupDelayedHover(skipAllButton.element, { content: skipAllTitle }));
|
||||
this._skipAllButton = skipAllButton;
|
||||
}
|
||||
|
||||
// Footer row with step indicator and navigation buttons
|
||||
this._footerRow = dom.$('.chat-question-footer-row');
|
||||
|
||||
// Step indicator (e.g., "2/4") on the left
|
||||
this._stepIndicator = dom.$('.chat-question-step-indicator');
|
||||
this._footerRow.appendChild(this._stepIndicator);
|
||||
|
||||
// Navigation controls (< >) - placed in footer row
|
||||
this._navigationButtons = dom.$('.chat-question-carousel-nav');
|
||||
this._navigationButtons.setAttribute('role', 'navigation');
|
||||
this._navigationButtons.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation'));
|
||||
|
||||
// Group prev/next buttons together
|
||||
const arrowsContainer = dom.$('.chat-question-nav-arrows');
|
||||
|
||||
const previousLabel = localize('previous', 'Previous');
|
||||
const previousLabelWithKeybinding = this.getLabelWithKeybinding(previousLabel, PREVIOUS_QUESTION_ACTION_ID);
|
||||
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', previousLabelWithKeybinding);
|
||||
interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabelWithKeybinding }));
|
||||
this._prevButton = prevButton;
|
||||
|
||||
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;
|
||||
|
||||
const submitButton = interactiveStore.add(new Button(this._navigationButtons, { ...defaultButtonStyles }));
|
||||
submitButton.element.classList.add('chat-question-submit-button');
|
||||
submitButton.label = localize('submit', 'Submit');
|
||||
this._submitButton = submitButton;
|
||||
|
||||
this._navigationButtons.appendChild(arrowsContainer);
|
||||
this._footerRow.appendChild(this._navigationButtons);
|
||||
this.domNode.append(this._footerRow);
|
||||
const isSingleQuestion = this.carousel.questions.length === 1;
|
||||
|
||||
if (!isSingleQuestion && this._closeButtonContainer) {
|
||||
this.domNode.insertBefore(this._closeButtonContainer, this._questionContainer!);
|
||||
}
|
||||
|
||||
// Register event listeners
|
||||
interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1)));
|
||||
interactiveStore.add(nextButton.onDidClick(() => this.navigate(1)));
|
||||
interactiveStore.add(submitButton.onDidClick(() => this.submit()));
|
||||
if (this._skipAllButton) {
|
||||
interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore()));
|
||||
}
|
||||
|
||||
// Register keyboard navigation - handle Enter on text inputs and freeform textareas
|
||||
// Register keyboard navigation
|
||||
interactiveStore.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.keyCode === KeyCode.Escape && this.carousel.allowSkip) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.ignore();
|
||||
} else if (event.keyCode === KeyCode.Enter && (event.metaKey || event.ctrlKey)) {
|
||||
// Cmd/Ctrl+Enter submits immediately from anywhere
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.submit();
|
||||
} else if (event.keyCode === KeyCode.Enter && !event.shiftKey) {
|
||||
// Handle Enter key for text inputs and freeform textareas, not radio/checkbox or buttons
|
||||
// Buttons have their own Enter/Space handling via Button class
|
||||
const target = e.target as HTMLElement;
|
||||
const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text';
|
||||
const isFreeformTextarea = target.tagName === 'TEXTAREA' && target.classList.contains('chat-question-freeform-textarea');
|
||||
@@ -262,6 +228,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._currentIndex = newIndex;
|
||||
this.persistDraftState();
|
||||
this.renderCurrentQuestion(true);
|
||||
this.domNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,8 +306,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._singleSelectItems.clear();
|
||||
this._multiSelectCheckboxes.clear();
|
||||
this._freeformTextareas.clear();
|
||||
this._nextButtonHover.value = undefined;
|
||||
this._submitButtonHover.value = undefined;
|
||||
|
||||
// Clear references to disposed elements
|
||||
this._prevButton = undefined;
|
||||
@@ -348,10 +313,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._submitButton = undefined;
|
||||
this._skipAllButton = undefined;
|
||||
this._questionContainer = undefined;
|
||||
this._navigationButtons = undefined;
|
||||
this._closeButtonContainer = undefined;
|
||||
this._footerRow = undefined;
|
||||
this._stepIndicator = undefined;
|
||||
this._submitHint = undefined;
|
||||
this._inputScrollable = undefined;
|
||||
}
|
||||
|
||||
@@ -381,12 +346,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
|
||||
const availableScrollableHeight = Math.floor(maxContainerHeight - contentVerticalPadding - nonScrollableContentHeight);
|
||||
const constrainedScrollableHeight = Math.max(0, availableScrollableHeight);
|
||||
const constrainedScrollableHeightPx = `${constrainedScrollableHeight}px`;
|
||||
|
||||
// Constrain the content element (DomScrollableElement._element) so that
|
||||
// scanDomNode sees clientHeight < scrollHeight and enables scrolling.
|
||||
// The wrapper inherits the same constraint via CSS flex.
|
||||
scrollableContent.style.height = `${constrainedScrollableHeight}px`;
|
||||
scrollableContent.style.maxHeight = `${constrainedScrollableHeight}px`;
|
||||
if (scrollableContent.style.height !== constrainedScrollableHeightPx || scrollableContent.style.maxHeight !== constrainedScrollableHeightPx) {
|
||||
scrollableContent.style.height = constrainedScrollableHeightPx;
|
||||
scrollableContent.style.maxHeight = constrainedScrollableHeightPx;
|
||||
}
|
||||
inputScrollable.scanDomNode();
|
||||
}
|
||||
|
||||
@@ -551,7 +519,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
}
|
||||
|
||||
private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void {
|
||||
if (!this._questionContainer || !this._prevButton || !this._nextButton || !this._submitButton) {
|
||||
if (!this._questionContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -574,70 +542,32 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
return;
|
||||
}
|
||||
|
||||
// Render question header row with title and close button
|
||||
// Render unified question title (message ?? title)
|
||||
const headerRow = dom.$('.chat-question-header-row');
|
||||
const titleRow = dom.$('.chat-question-title-row');
|
||||
|
||||
// Render question title (short header) in the header bar as plain text
|
||||
if (question.title) {
|
||||
const questionText = question.message ?? question.title;
|
||||
if (questionText) {
|
||||
const title = dom.$('.chat-question-title');
|
||||
const questionText = question.title;
|
||||
const messageContent = this.getQuestionText(questionText);
|
||||
|
||||
title.setAttribute('aria-label', messageContent);
|
||||
|
||||
if (question.message !== undefined) {
|
||||
const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText);
|
||||
const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd));
|
||||
title.appendChild(renderedTitle.element);
|
||||
} else {
|
||||
// Check for subtitle in parentheses at the end
|
||||
const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/);
|
||||
if (parenMatch) {
|
||||
// Main title (bold)
|
||||
const mainTitle = dom.$('span.chat-question-title-main');
|
||||
mainTitle.textContent = parenMatch[1];
|
||||
title.appendChild(mainTitle);
|
||||
|
||||
// Subtitle in parentheses (normal weight)
|
||||
const subtitle = dom.$('span.chat-question-title-subtitle');
|
||||
subtitle.textContent = ' ' + parenMatch[2];
|
||||
title.appendChild(subtitle);
|
||||
} else {
|
||||
title.textContent = messageContent;
|
||||
}
|
||||
}
|
||||
const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText);
|
||||
const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd));
|
||||
title.appendChild(renderedTitle.element);
|
||||
titleRow.appendChild(title);
|
||||
}
|
||||
|
||||
// Add close button to header row (if allowSkip is enabled)
|
||||
if (this._closeButtonContainer) {
|
||||
titleRow.appendChild(this._closeButtonContainer);
|
||||
}
|
||||
|
||||
headerRow.appendChild(titleRow);
|
||||
|
||||
this._questionContainer.appendChild(headerRow);
|
||||
|
||||
// Render full question text below the header row (supports multi-line and markdown)
|
||||
if (question.message) {
|
||||
const messageEl = dom.$('.chat-question-message');
|
||||
if (isMarkdownString(question.message)) {
|
||||
const renderedMessage = questionRenderStore.add(this._markdownRendererService.render(MarkdownString.lift(question.message)));
|
||||
messageEl.appendChild(renderedMessage.element);
|
||||
} else {
|
||||
messageEl.textContent = this.getQuestionText(question.message);
|
||||
}
|
||||
this._questionContainer.appendChild(messageEl);
|
||||
}
|
||||
|
||||
// For single-question carousels, add close button inside the title row
|
||||
const isSingleQuestion = this.carousel.questions.length === 1;
|
||||
// Update step indicator in footer
|
||||
if (this._stepIndicator) {
|
||||
this._stepIndicator.textContent = `${this._currentIndex + 1}/${this.carousel.questions.length}`;
|
||||
this._stepIndicator.style.display = isSingleQuestion ? 'none' : '';
|
||||
if (isSingleQuestion && this._closeButtonContainer) {
|
||||
titleRow.appendChild(this._closeButtonContainer);
|
||||
}
|
||||
|
||||
this._questionContainer.appendChild(headerRow);
|
||||
|
||||
// Render input based on question type
|
||||
const inputContainer = dom.$('.chat-question-input-container');
|
||||
this.renderInput(inputContainer, question);
|
||||
@@ -652,10 +582,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
inputScrollableNode.classList.add('chat-question-input-scrollable');
|
||||
this._questionContainer.appendChild(inputScrollableNode);
|
||||
|
||||
const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => this.layoutInputScrollable(inputScrollable)));
|
||||
let relayoutScheduled = false;
|
||||
const relayoutScheduler = questionRenderStore.add(new MutableDisposable());
|
||||
const scheduleLayoutInputScrollable = () => {
|
||||
if (relayoutScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
relayoutScheduled = true;
|
||||
relayoutScheduler.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => {
|
||||
relayoutScheduled = false;
|
||||
this.layoutInputScrollable(inputScrollable);
|
||||
});
|
||||
};
|
||||
|
||||
const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => scheduleLayoutInputScrollable()));
|
||||
questionRenderStore.add(inputResizeObserver.observe(inputScrollableNode));
|
||||
questionRenderStore.add(inputResizeObserver.observe(inputContainer));
|
||||
questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.layoutInputScrollable(inputScrollable)));
|
||||
scheduleLayoutInputScrollable();
|
||||
this.layoutInputScrollable(inputScrollable);
|
||||
questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => {
|
||||
inputContainer.scrollTop = 0;
|
||||
@@ -664,26 +608,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
inputScrollable.scanDomNode();
|
||||
}));
|
||||
|
||||
// Update navigation button states (prevButton and nextButton are guaranteed non-null from guard above)
|
||||
this._prevButton!.enabled = this._currentIndex > 0;
|
||||
this._prevButton!.element.style.display = isSingleQuestion ? 'none' : '';
|
||||
|
||||
// Keep navigation arrows stable and disable next on the last question
|
||||
const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1;
|
||||
const submitLabel = localize('submit', 'Submit');
|
||||
const nextLabel = localize('next', 'Next');
|
||||
const nextLabelWithKeybinding = this.getLabelWithKeybinding(nextLabel, NEXT_QUESTION_ACTION_ID);
|
||||
this._nextButton!.label = `$(${Codicon.chevronRight.id})`;
|
||||
this._nextButton!.enabled = !isLastQuestion;
|
||||
this._nextButton!.element.setAttribute('aria-label', nextLabelWithKeybinding);
|
||||
this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabelWithKeybinding });
|
||||
|
||||
this._submitButton!.enabled = isLastQuestion;
|
||||
this._submitButton!.element.style.display = isLastQuestion ? '' : 'none';
|
||||
this._submitButton!.element.setAttribute('aria-label', submitLabel);
|
||||
this._submitButtonHover.value = isLastQuestion
|
||||
? this._hoverService.setupDelayedHover(this._submitButton!.element, { content: submitLabel })
|
||||
: undefined;
|
||||
// Render footer for multi-question carousels or single-question carousels.
|
||||
if (!isSingleQuestion) {
|
||||
this.renderFooter();
|
||||
} else {
|
||||
this.renderSingleQuestionFooter();
|
||||
}
|
||||
|
||||
// Update aria-label to reflect the current question
|
||||
this._updateAriaLabel();
|
||||
@@ -697,6 +627,138 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
this._onDidChangeHeight.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders or updates the persistent footer with nav arrows, step indicator, and submit button.
|
||||
*/
|
||||
private renderFooter(): void {
|
||||
if (!this._footerRow) {
|
||||
const interactiveStore = this._interactiveUIStore.value;
|
||||
if (!interactiveStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._footerRow = dom.$('.chat-question-footer-row');
|
||||
|
||||
// Left side: nav arrows + step indicator
|
||||
const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav');
|
||||
leftControls.setAttribute('role', 'navigation');
|
||||
leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation'));
|
||||
|
||||
const arrowsContainer = dom.$('.chat-question-nav-arrows');
|
||||
|
||||
const previousLabel = this.getLabelWithKeybinding(localize('previous', 'Previous'), PREVIOUS_QUESTION_ACTION_ID);
|
||||
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 }));
|
||||
interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1)));
|
||||
this._prevButton = prevButton;
|
||||
|
||||
const nextLabel = this.getLabelWithKeybinding(localize('next', 'Next'), NEXT_QUESTION_ACTION_ID);
|
||||
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})`;
|
||||
nextButton.element.setAttribute('aria-label', nextLabel);
|
||||
interactiveStore.add(this._hoverService.setupDelayedHover(nextButton.element, { content: nextLabel }));
|
||||
interactiveStore.add(nextButton.onDidClick(() => this.navigate(1)));
|
||||
this._nextButton = nextButton;
|
||||
|
||||
leftControls.appendChild(arrowsContainer);
|
||||
|
||||
this._stepIndicator = dom.$('.chat-question-step-indicator');
|
||||
leftControls.appendChild(this._stepIndicator);
|
||||
|
||||
this._footerRow.appendChild(leftControls);
|
||||
|
||||
// Right side: hint + submit
|
||||
const rightControls = dom.$('.chat-question-footer-right');
|
||||
|
||||
const hint = dom.$('span.chat-question-submit-hint');
|
||||
hint.textContent = isMacintosh
|
||||
? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit')
|
||||
: localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit');
|
||||
rightControls.appendChild(hint);
|
||||
this._submitHint = hint;
|
||||
|
||||
const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles }));
|
||||
submitButton.element.classList.add('chat-question-submit-button');
|
||||
submitButton.label = localize('submit', 'Submit');
|
||||
interactiveStore.add(submitButton.onDidClick(() => this.submit()));
|
||||
this._submitButton = submitButton;
|
||||
|
||||
this._footerRow.appendChild(rightControls);
|
||||
this.domNode.append(this._footerRow);
|
||||
}
|
||||
|
||||
this.updateFooterState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the footer nav button enabled state and step indicator text.
|
||||
*/
|
||||
private updateFooterState(): void {
|
||||
if (this._prevButton) {
|
||||
this._prevButton.enabled = this._currentIndex > 0;
|
||||
}
|
||||
if (this._nextButton) {
|
||||
this._nextButton.enabled = this._currentIndex < this.carousel.questions.length - 1;
|
||||
}
|
||||
if (this._stepIndicator) {
|
||||
this._stepIndicator.textContent = localize(
|
||||
'chat.questionCarousel.stepIndicator',
|
||||
'{0}/{1}',
|
||||
this._currentIndex + 1,
|
||||
this.carousel.questions.length
|
||||
);
|
||||
}
|
||||
if (this._submitButton) {
|
||||
const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1;
|
||||
this._submitButton.element.style.display = isLastQuestion ? '' : 'none';
|
||||
if (this._submitHint) {
|
||||
this._submitHint.style.display = isLastQuestion ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a simplified footer with just a submit button for single-question multi-select carousels.
|
||||
*/
|
||||
private renderSingleQuestionFooter(): void {
|
||||
if (!this._footerRow) {
|
||||
const interactiveStore = this._interactiveUIStore.value;
|
||||
if (!interactiveStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._footerRow = dom.$('.chat-question-footer-row');
|
||||
|
||||
// Spacer to push controls to the right
|
||||
const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav');
|
||||
leftControls.setAttribute('role', 'navigation');
|
||||
leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation'));
|
||||
this._footerRow.appendChild(leftControls);
|
||||
|
||||
const rightControls = dom.$('.chat-question-footer-right');
|
||||
|
||||
const hint = dom.$('span.chat-question-submit-hint');
|
||||
hint.textContent = isMacintosh
|
||||
? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit')
|
||||
: localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit');
|
||||
rightControls.appendChild(hint);
|
||||
this._submitHint = hint;
|
||||
|
||||
const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles }));
|
||||
submitButton.element.classList.add('chat-question-submit-button');
|
||||
submitButton.label = localize('submit', 'Submit');
|
||||
interactiveStore.add(submitButton.onDidClick(() => this.submit()));
|
||||
this._submitButton = submitButton;
|
||||
|
||||
this._footerRow.appendChild(rightControls);
|
||||
this.domNode.append(this._footerRow);
|
||||
}
|
||||
}
|
||||
|
||||
private getLabelWithKeybinding(label: string, actionId: string): string {
|
||||
const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel();
|
||||
return keybindingLabel
|
||||
@@ -837,12 +899,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const label = dom.$('.chat-question-list-label');
|
||||
const separatorIndex = option.label.indexOf(' - ');
|
||||
if (separatorIndex !== -1) {
|
||||
listItem.classList.add('has-description');
|
||||
const titleSpan = dom.$('span.chat-question-list-label-title');
|
||||
titleSpan.textContent = option.label.substring(0, separatorIndex);
|
||||
label.appendChild(titleSpan);
|
||||
|
||||
const descSpan = dom.$('span.chat-question-list-label-desc');
|
||||
descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3);
|
||||
descSpan.textContent = option.label.substring(separatorIndex + 3);
|
||||
label.appendChild(descSpan);
|
||||
} else {
|
||||
label.textContent = option.label;
|
||||
@@ -929,7 +992,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
} else if (event.keyCode === KeyCode.UpArrow) {
|
||||
e.preventDefault();
|
||||
newIndex = Math.max(data.selectedIndex - 1, 0);
|
||||
} else if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) {
|
||||
} else if ((event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) && !event.metaKey && !event.ctrlKey) {
|
||||
// Enter confirms current selection and advances to next question
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1037,12 +1100,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
const label = dom.$('.chat-question-list-label');
|
||||
const separatorIndex = option.label.indexOf(' - ');
|
||||
if (separatorIndex !== -1) {
|
||||
listItem.classList.add('has-description');
|
||||
const titleSpan = dom.$('span.chat-question-list-label-title');
|
||||
titleSpan.textContent = option.label.substring(0, separatorIndex);
|
||||
label.appendChild(titleSpan);
|
||||
|
||||
const descSpan = dom.$('span.chat-question-list-label-desc');
|
||||
descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3);
|
||||
descSpan.textContent = option.label.substring(separatorIndex + 3);
|
||||
label.appendChild(descSpan);
|
||||
} else {
|
||||
label.textContent = option.label;
|
||||
@@ -1128,7 +1192,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
e.preventDefault();
|
||||
focusedIndex = Math.max(focusedIndex - 1, 0);
|
||||
listItems[focusedIndex].focus();
|
||||
} else if (event.keyCode === KeyCode.Enter) {
|
||||
} else if (event.keyCode === KeyCode.Enter && !event.metaKey && !event.ctrlKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleNextOrSubmit();
|
||||
@@ -1267,40 +1331,25 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
|
||||
|
||||
for (const question of this.carousel.questions) {
|
||||
const answer = this._answers.get(question.id);
|
||||
if (answer === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const summaryItem = dom.$('.chat-question-summary-item');
|
||||
|
||||
// Category label (use same text as shown in question UI: message ?? title)
|
||||
const questionLabel = dom.$('span.chat-question-summary-label');
|
||||
const questionRow = dom.$('div.chat-question-summary-label');
|
||||
const questionText = question.message ?? question.title;
|
||||
let labelText = typeof questionText === 'string' ? questionText : questionText.value;
|
||||
// Remove trailing colons and whitespace to avoid double colons (CSS adds ': ')
|
||||
labelText = labelText.replace(/[:\s]+$/, '');
|
||||
questionLabel.textContent = labelText;
|
||||
summaryItem.appendChild(questionLabel);
|
||||
questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText);
|
||||
summaryItem.appendChild(questionRow);
|
||||
|
||||
// Format answer with title and description parts
|
||||
const formattedAnswer = this.formatAnswerForSummary(question, answer);
|
||||
const separatorIndex = formattedAnswer.indexOf(' - ');
|
||||
|
||||
if (separatorIndex !== -1) {
|
||||
// Answer title (bold)
|
||||
const answerTitle = dom.$('span.chat-question-summary-answer-title');
|
||||
answerTitle.textContent = formattedAnswer.substring(0, separatorIndex);
|
||||
summaryItem.appendChild(answerTitle);
|
||||
|
||||
// Answer description (normal)
|
||||
const answerDesc = dom.$('span.chat-question-summary-answer-desc');
|
||||
answerDesc.textContent = ' - ' + formattedAnswer.substring(separatorIndex + 3);
|
||||
summaryItem.appendChild(answerDesc);
|
||||
if (answer !== undefined) {
|
||||
const formattedAnswer = this.formatAnswerForSummary(question, answer);
|
||||
const answerRow = dom.$('div.chat-question-summary-answer-title');
|
||||
answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer);
|
||||
summaryItem.appendChild(answerRow);
|
||||
} else {
|
||||
// Just the answer value (bold)
|
||||
const answerValue = dom.$('span.chat-question-summary-answer-title');
|
||||
answerValue.textContent = formattedAnswer;
|
||||
summaryItem.appendChild(answerValue);
|
||||
const unanswered = dom.$('div.chat-question-summary-unanswered');
|
||||
unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet');
|
||||
summaryItem.appendChild(unanswered);
|
||||
}
|
||||
|
||||
summaryContainer.appendChild(summaryItem);
|
||||
|
||||
@@ -14,20 +14,21 @@
|
||||
.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-container {
|
||||
margin: 0;
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-radius: 4px;
|
||||
background-color: var(--vscode-panel-background);
|
||||
border-radius: var(--vscode-cornerRadius-large);
|
||||
}
|
||||
|
||||
/* general questions styling */
|
||||
.interactive-session .chat-question-carousel-container {
|
||||
margin: 8px 0;
|
||||
border: 1px solid var(--vscode-chat-requestBorder);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--vscode-cornerRadius-large);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
max-height: min(420px, 45vh);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* input part wrapper */
|
||||
@@ -47,7 +48,6 @@
|
||||
|
||||
.interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-content,
|
||||
.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -55,18 +55,13 @@
|
||||
.interactive-session .chat-question-carousel-container .chat-question-carousel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--vscode-chat-requestBackground);
|
||||
padding: 8px 16px 10px 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-question-header-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
background: var(--vscode-chat-requestBackground);
|
||||
padding: 0 16px 10px 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-question-title-row {
|
||||
@@ -75,6 +70,8 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 8px 8px 8px 16px;
|
||||
border-bottom: 1px solid var(--vscode-chat-requestBorder);
|
||||
}
|
||||
|
||||
.chat-question-title {
|
||||
@@ -85,13 +82,6 @@
|
||||
font-weight: 500;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
margin: 0;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
border-bottom: 1px solid var(--vscode-chat-requestBorder);
|
||||
|
||||
.rendered-markdown {
|
||||
a {
|
||||
@@ -107,15 +97,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-title-main {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-question-title-subtitle {
|
||||
font-weight: normal;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-close-container {
|
||||
@@ -126,49 +107,29 @@
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--vscode-foreground) !important;
|
||||
color: var(--vscode-icon-foreground) !important;
|
||||
}
|
||||
|
||||
.monaco-button.chat-question-close:hover:not(.disabled) {
|
||||
background: var(--vscode-toolbar-hoverBackground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-message {
|
||||
flex-shrink: 0;
|
||||
padding-top: 8px;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.4;
|
||||
|
||||
.rendered-markdown {
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra right padding when close button is absolutely positioned (multi-question) */
|
||||
.interactive-session .chat-question-carousel-container:has(> .chat-question-close-container) .chat-question-title-row {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
/* questions list and freeform area */
|
||||
.interactive-session .chat-question-carousel-container .chat-question-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 4px;
|
||||
padding-right: 14px;
|
||||
padding-bottom: 12px;
|
||||
padding: 8px;
|
||||
min-width: 0;
|
||||
|
||||
&::after {
|
||||
@@ -179,37 +140,24 @@
|
||||
}
|
||||
|
||||
/* some hackiness to get the focus looking right */
|
||||
.chat-question-list-item:focus:not(.selected),
|
||||
.chat-question-list-item:focus,
|
||||
.chat-question-list:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-question-list:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.chat-question-list:focus-within .chat-question-list-item.selected {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
outline-offset: -1px;
|
||||
outline-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.chat-question-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
outline: none;
|
||||
padding: 4px 0;
|
||||
padding: 0;
|
||||
|
||||
.chat-question-list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
border-radius: var(--vscode-cornerRadius-medium);
|
||||
user-select: none;
|
||||
|
||||
.chat-question-list-indicator {
|
||||
@@ -220,6 +168,8 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.chat-question-list-indicator.codicon-check {
|
||||
@@ -232,11 +182,13 @@
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
padding-top: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-question-list-label-title {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-question-list-label-desc {
|
||||
@@ -245,13 +197,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-list-item.has-description {
|
||||
align-items: flex-start;
|
||||
|
||||
.chat-question-list-number {
|
||||
line-height: 1.4;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-question-list-checkbox {
|
||||
/* Title line-height is ~17px (1.4 * body-s), checkbox is 16px: 1px offset */
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-list-item:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
/* Single-select: highlight entire row when selected */
|
||||
.chat-question-list-item.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
|
||||
.chat-question-label {
|
||||
@@ -268,16 +235,12 @@
|
||||
}
|
||||
|
||||
.chat-question-list-number {
|
||||
background-color: transparent;
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
border-color: var(--vscode-list-activeSelectionForeground);
|
||||
border-bottom-color: var(--vscode-list-activeSelectionForeground);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-question-list-item.selected:hover {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
/* Checkbox for multi-select */
|
||||
@@ -291,11 +254,12 @@
|
||||
}
|
||||
|
||||
.chat-question-freeform {
|
||||
margin-left: 8px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
gap: 12px;
|
||||
|
||||
.chat-question-freeform-number {
|
||||
height: fit-content;
|
||||
@@ -338,22 +302,11 @@
|
||||
/* todo: change to use keybinding service so we don't have to recreate this */
|
||||
.chat-question-list-number,
|
||||
.chat-question-freeform-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
padding: 0px 4px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
background-color: var(--vscode-keybindingLabel-background);
|
||||
color: var(--vscode-keybindingLabel-foreground);
|
||||
border-color: var(--vscode-keybindingLabel-border);
|
||||
border-bottom-color: var(--vscode-keybindingLabel-bottomBorder);
|
||||
box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow);
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
flex-shrink: 0;
|
||||
min-width: 1ch;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,31 +315,53 @@
|
||||
}
|
||||
|
||||
.interactive-session .chat-question-carousel-container .chat-question-input-scrollable {
|
||||
flex: 1;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* footer with step indicator and nav buttons */
|
||||
/* close button for multi-question carousels (positioned top-right) */
|
||||
.interactive-session .chat-question-carousel-container > .chat-question-close-container {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
|
||||
.monaco-button.chat-question-close {
|
||||
min-width: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--vscode-icon-foreground) !important;
|
||||
}
|
||||
|
||||
.monaco-button.chat-question-close:hover:not(.disabled) {
|
||||
background: var(--vscode-toolbar-hoverBackground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* footer with nav arrows, step indicator, and submit */
|
||||
.interactive-session .chat-question-carousel-container .chat-question-footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 16px;
|
||||
padding: 4px 8px;
|
||||
border-top: 1px solid var(--vscode-chat-requestBorder);
|
||||
background: var(--vscode-chat-requestBackground);
|
||||
flex-shrink: 0;
|
||||
|
||||
.chat-question-step-indicator {
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.chat-question-carousel-nav {
|
||||
.chat-question-footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-question-footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-question-nav-arrows {
|
||||
@@ -395,49 +370,48 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow {
|
||||
.monaco-button.chat-question-nav-arrow {
|
||||
min-width: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Secondary buttons (prev, next) use gray secondary background */
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev,
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next {
|
||||
background: var(--vscode-button-secondaryBackground) !important;
|
||||
color: var(--vscode-button-secondaryForeground) !important;
|
||||
}
|
||||
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev:hover:not(.disabled),
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next:hover:not(.disabled) {
|
||||
background: var(--vscode-button-secondaryHoverBackground) !important;
|
||||
}
|
||||
|
||||
/* Dedicated submit button uses primary background */
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-submit-button {
|
||||
background: var(--vscode-button-background) !important;
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
height: 22px;
|
||||
min-width: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-submit-button:hover:not(.disabled) {
|
||||
background: var(--vscode-button-hoverBackground) !important;
|
||||
}
|
||||
|
||||
/* Close button uses transparent background */
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--vscode-foreground) !important;
|
||||
}
|
||||
|
||||
.chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close:hover:not(.disabled) {
|
||||
.monaco-button.chat-question-nav-arrow:hover:not(.disabled) {
|
||||
background: var(--vscode-toolbar-hoverBackground) !important;
|
||||
}
|
||||
|
||||
.monaco-button.chat-question-nav-arrow.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.chat-question-step-indicator {
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.chat-question-submit-hint {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.monaco-button.chat-question-submit-button {
|
||||
background: var(--vscode-button-background) !important;
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
min-width: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.monaco-button.chat-question-submit-button:hover:not(.disabled) {
|
||||
background: var(--vscode-button-hoverBackground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* summary (after finished) */
|
||||
@@ -449,9 +423,7 @@
|
||||
|
||||
.chat-question-summary-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
}
|
||||
@@ -462,11 +434,6 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.chat-question-summary-label::after {
|
||||
content: ': ';
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.chat-question-summary-answer-title {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -60,7 +60,6 @@ suite('ChatQuestionCarouselPart', () => {
|
||||
assert.ok(widget.domNode.classList.contains('chat-question-carousel-container'));
|
||||
assert.ok(widget.domNode.querySelector('.chat-question-header-row'));
|
||||
assert.ok(widget.domNode.querySelector('.chat-question-carousel-content'));
|
||||
assert.ok(widget.domNode.querySelector('.chat-question-carousel-nav'));
|
||||
});
|
||||
|
||||
test('renders question title', () => {
|
||||
@@ -100,12 +99,7 @@ suite('ChatQuestionCarouselPart', () => {
|
||||
|
||||
const title = widget.domNode.querySelector('.chat-question-title');
|
||||
assert.ok(title, 'title element should exist');
|
||||
const messageEl = widget.domNode.querySelector('.chat-question-message');
|
||||
assert.ok(messageEl, 'message element should exist');
|
||||
assert.ok(messageEl?.querySelector('.rendered-markdown'), 'markdown content should be rendered');
|
||||
assert.strictEqual(messageEl?.textContent?.includes('**details**'), false, 'markdown syntax should not be shown as raw text');
|
||||
const link = messageEl?.querySelector('a') as HTMLAnchorElement | null;
|
||||
assert.ok(link, 'markdown link should render as anchor');
|
||||
assert.ok(title?.querySelector('.rendered-markdown'), 'markdown content should be rendered');
|
||||
});
|
||||
|
||||
test('renders plain string question message as text', () => {
|
||||
@@ -119,10 +113,9 @@ suite('ChatQuestionCarouselPart', () => {
|
||||
]);
|
||||
createWidget(carousel);
|
||||
|
||||
const messageEl = widget.domNode.querySelector('.chat-question-message');
|
||||
assert.ok(messageEl, 'message element should exist');
|
||||
assert.ok(messageEl?.textContent?.includes('details'), 'plain text content should be rendered');
|
||||
assert.strictEqual(messageEl?.querySelector('.rendered-markdown'), null, 'plain string message should not use markdown renderer');
|
||||
const title = widget.domNode.querySelector('.chat-question-title');
|
||||
assert.ok(title, 'title element should exist');
|
||||
assert.ok(title?.textContent?.includes('details'), 'content should be rendered');
|
||||
});
|
||||
|
||||
test('renders progress indicator correctly', () => {
|
||||
@@ -278,34 +271,40 @@ suite('ChatQuestionCarouselPart', () => {
|
||||
]);
|
||||
createWidget(carousel);
|
||||
|
||||
// Use dedicated class selectors for stability
|
||||
const prevButton = widget.domNode.querySelector('.chat-question-nav-prev') as HTMLButtonElement;
|
||||
const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf<HTMLButtonElement>;
|
||||
const prevButton = navArrows[0];
|
||||
assert.ok(prevButton, 'Previous button should exist');
|
||||
assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question');
|
||||
});
|
||||
|
||||
test('next button stays as arrow and is disabled on last question', () => {
|
||||
const carousel = createMockCarousel([
|
||||
{ id: 'q1', type: 'text', title: 'Only Question' }
|
||||
{ id: 'q1', type: 'text', title: 'Only Question' },
|
||||
{ id: 'q2', type: 'text', title: 'Question 2' }
|
||||
]);
|
||||
createWidget(carousel);
|
||||
|
||||
// Use dedicated class selector for stability
|
||||
const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLButtonElement;
|
||||
// Navigate to last question
|
||||
widget.navigateToNextQuestion();
|
||||
|
||||
const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf<HTMLButtonElement>;
|
||||
const nextButton = navArrows[1];
|
||||
assert.ok(nextButton, 'Next button should exist');
|
||||
assert.strictEqual(nextButton.getAttribute('aria-label'), 'Next', 'Next button should preserve Next aria-label on last question');
|
||||
assert.ok(nextButton.classList.contains('disabled') || nextButton.disabled, 'Next button should be disabled on last question');
|
||||
});
|
||||
|
||||
test('submit button is shown on last question', () => {
|
||||
const carousel = createMockCarousel([
|
||||
{ id: 'q1', type: 'text', title: 'Only Question' }
|
||||
{ id: 'q1', type: 'text', title: 'Question 1' },
|
||||
{ id: 'q2', type: 'text', title: 'Question 2' }
|
||||
]);
|
||||
createWidget(carousel);
|
||||
|
||||
// Navigate to last question
|
||||
widget.navigateToNextQuestion();
|
||||
|
||||
const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement;
|
||||
assert.ok(submitButton, 'Submit button should exist');
|
||||
assert.strictEqual(submitButton.getAttribute('aria-label'), 'Submit');
|
||||
assert.notStrictEqual(submitButton.style.display, 'none', 'Submit button should be visible on last question');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user