diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 28f9603b6699bd..00e0f587a8f1f5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -31,6 +31,9 @@ .interactive-session .interactive-input-part > .chat-question-carousel-widget-container { width: 100%; position: relative; + display: flex; + flex-direction: column; + gap: 8px; } /* container and header */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index e4fc306046d98e..e5952be47f33e6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -2178,8 +2178,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); - private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable()); - private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore()); - private _currentQuestionCarouselResponseId: string | undefined; - private _currentQuestionCarouselSessionResource: URI | undefined; + private readonly _chatQuestionCarouselWidgets = this._register(new DisposableMap()); + private readonly _questionCarouselResponseIds = new Map(); + private readonly _questionCarouselSessionResources = new Map(); private _hasQuestionCarouselContextKey: IContextKey | undefined; private readonly _chatEditingTodosDisposables = this._register(new DisposableStore()); private _lastEditingSessionResource: URI | undefined; @@ -1919,7 +1917,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); this.updateContextUsageWidget(); - if (this._currentQuestionCarouselSessionResource && (!e.currentSessionResource || !isEqual(this._currentQuestionCarouselSessionResource, e.currentSessionResource))) { + let hasMatchingResource = false; + if (e.currentSessionResource) { + for (const r of this._questionCarouselSessionResources.values()) { + if (isEqual(r, e.currentSessionResource)) { + hasMatchingResource = true; + break; + } + } + } + if (this._questionCarouselSessionResources.size > 0 && (!e.currentSessionResource || !hasMatchingResource)) { this.clearQuestionCarousel(); } @@ -2579,60 +2586,74 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge 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; - } + const carouselKey = carousel.resolveId ?? `${isResponseVM(context.element) ? context.element.requestId : ''}_${context.contentIndex}`; - // Complete the old carousel's completion promise as skipped before clearing - // This prevents the askQuestions tool from hanging when parallel subagents invoke it - const oldCarousel = existingCarousel.carousel; - if (oldCarousel instanceof ChatQuestionCarouselData && !oldCarousel.completion.isSettled) { - oldCarousel.completion.complete({ answers: undefined }); - } - - this.clearQuestionCarousel(); + // If a carousel with the same key already exists, return it + const existing = this._chatQuestionCarouselWidgets.get(carouselKey); + if (existing) { + return existing; } - // track the response id and session - this._currentQuestionCarouselResponseId = isResponseVM(context.element) ? context.element.requestId : undefined; - this._currentQuestionCarouselSessionResource = isResponseVM(context.element) ? context.element.sessionResource : undefined; + // Track the response id and session for this carousel + if (isResponseVM(context.element)) { + this._questionCarouselResponseIds.set(carouselKey, context.element.requestId); + this._questionCarouselSessionResources.set(carouselKey, context.element.sessionResource); + } - const part = this._chatQuestionCarouselDisposables.add( - this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options) - ); - this._chatQuestionCarouselWidget.value = part; + const part = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options); + this._chatQuestionCarouselWidgets.set(carouselKey, part); this._hasQuestionCarouselContextKey?.set(true); - dom.clearNode(this.chatQuestionCarouselContainer); dom.append(this.chatQuestionCarouselContainer, part.domNode); return part; } - clearQuestionCarousel(responseId?: string): void { - if (responseId && this._currentQuestionCarouselResponseId !== responseId) { - return; + clearQuestionCarousel(responseId?: string, resolveId?: string): void { + if (resolveId !== undefined) { + // Remove a specific carousel by resolveId + const part = this._chatQuestionCarouselWidgets.get(resolveId); + if (part) { + part.domNode.remove(); + this._chatQuestionCarouselWidgets.deleteAndDispose(resolveId); + } + this._questionCarouselResponseIds.delete(resolveId); + this._questionCarouselSessionResources.delete(resolveId); + } else if (responseId !== undefined) { + // Remove all carousels associated with a given responseId + for (const [key, rid] of this._questionCarouselResponseIds) { + if (rid === responseId) { + const part = this._chatQuestionCarouselWidgets.get(key); + if (part) { + part.domNode.remove(); + this._chatQuestionCarouselWidgets.deleteAndDispose(key); + } + this._questionCarouselResponseIds.delete(key); + this._questionCarouselSessionResources.delete(key); + } + } + } else { + // Clear all carousels + this._chatQuestionCarouselWidgets.clearAndDisposeAll(); + this._questionCarouselResponseIds.clear(); + this._questionCarouselSessionResources.clear(); + dom.clearNode(this.chatQuestionCarouselContainer); } - this._chatQuestionCarouselDisposables.clear(); - this._chatQuestionCarouselWidget.clear(); - this._currentQuestionCarouselResponseId = undefined; - this._currentQuestionCarouselSessionResource = undefined; - this._hasQuestionCarouselContextKey?.set(false); - dom.clearNode(this.chatQuestionCarouselContainer); - } - get questionCarouselResponseId(): string | undefined { - return this._currentQuestionCarouselResponseId; + this._hasQuestionCarouselContextKey?.set(this._chatQuestionCarouselWidgets.size > 0); } get questionCarousel(): ChatQuestionCarouselPart | undefined { - return this._chatQuestionCarouselWidget.value; + // Return the focused carousel, or the first one + for (const part of this._chatQuestionCarouselWidgets.values()) { + if (part.hasFocus()) { + return part; + } + } + return this._chatQuestionCarouselWidgets.size > 0 ? this._chatQuestionCarouselWidgets.values().next().value : undefined; } focusQuestionCarousel(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; if (carousel) { carousel.focus(); return true; @@ -2641,17 +2662,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } isQuestionCarouselFocused(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; - return carousel?.hasFocus() ?? false; + for (const part of this._chatQuestionCarouselWidgets.values()) { + if (part.hasFocus()) { + return true; + } + } + return false; } navigateToPreviousQuestion(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; return carousel?.navigateToPreviousQuestion() ?? false; } navigateToNextQuestion(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; return carousel?.navigateToNextQuestion() ?? false; }