diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index db862b6096c..49694484671 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -284,7 +284,8 @@ export class ModalEditorPart { // Create label const label = disposables.add(scopedInstantiationService.createInstance(ResourceLabel, titleElement, {})); - disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, () => { + const labelChangeDisposable = disposables.add(new MutableDisposable()); + const updateLabel = () => { const activeEditor = editorPart.activeGroup.activeEditor; if (activeEditor) { const { labelFormat } = editorPart.partOptions; @@ -300,10 +301,14 @@ export class ModalEditorPart { extraClasses: activeEditor.getLabelExtraClasses(), } ); + + labelChangeDisposable.value = activeEditor.onDidChangeLabel(() => updateLabel()); } else { label.element.clear(); + labelChangeDisposable.clear(); } - })); + }; + disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, updateLabel)); // Handle double-click on header to toggle maximize disposables.add(addDisposableListener(headerElement, EventType.DBLCLICK, e => { diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 36766e78d57..57de2a5eedb 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -73,7 +73,7 @@ import { IChatWidgetService } from '../chat.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { IChatResponseViewModel, isResponseVM } from '../../common/model/chatViewModel.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { extractImagesFromChatResponse } from '../../common/chatImageExtraction.js'; +import { extractImagesFromChatResponse, IChatExtractedImage } from '../../common/chatImageExtraction.js'; const commonHoverOptions: Partial = { style: HoverStyle.Pointer, @@ -477,26 +477,44 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { const widget = this.chatWidgetService.lastFocusedWidget; if (widget?.viewModel) { const responses = widget.viewModel.getItems().filter((item): item is IChatResponseViewModel => isResponseVM(item)); - for (let i = responses.length - 1; i >= 0; i--) { - const extracted = extractImagesFromChatResponse(responses[i]); + + // Collect all responses that have images, one section per response. + // The loop continues after finding the clicked image to gather all sections for the carousel. + const sections: { title: string; images: IChatExtractedImage[] }[] = []; + let clickedGlobalIndex = -1; + let globalOffset = 0; + let collectionId: string | undefined; + + for (const response of responses) { + const extracted = extractImagesFromChatResponse(response); if (extracted && extracted.images.length > 0) { - // Match by URI (unique per tool output) to avoid ambiguity with identical image content - const startIndex = referenceUri - ? extracted.images.findIndex(img => isEqual(img.uri, referenceUri)) - : extracted.images.findIndex(img => img.data.equals(VSBuffer.wrap(data))); - if (startIndex !== -1) { - await this.commandService.executeCommand('workbench.action.chat.openImageInCarousel', { - collection: { - id: extracted.id, - title: extracted.title, - sections: [{ title: '', images: extracted.images }], - }, - startIndex, - }); - return; + sections.push({ title: extracted.title, images: extracted.images }); + + if (clickedGlobalIndex === -1) { + const localIndex = referenceUri + ? extracted.images.findIndex(img => isEqual(img.uri, referenceUri)) + : extracted.images.findIndex(img => img.data.equals(VSBuffer.wrap(data))); + if (localIndex !== -1) { + clickedGlobalIndex = globalOffset + localIndex; + collectionId = extracted.id; + } } + + globalOffset += extracted.images.length; } } + + if (clickedGlobalIndex !== -1 && collectionId && sections.length > 0) { + await this.commandService.executeCommand('workbench.action.chat.openImageInCarousel', { + collection: { + id: collectionId, + title: sections.length === 1 ? sections[0].title : localize('chat.imageCarousel.allImages', "Chat Images"), + sections, + }, + startIndex: clickedGlobalIndex, + }); + return; + } } // Fallback: open just the single clicked image diff --git a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditor.ts b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditor.ts index 16c359189b4..aceeab9deee 100644 --- a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditor.ts +++ b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditor.ts @@ -168,15 +168,10 @@ export class ImageCarouselEditor extends EditorPane { let flatIndex = 0; for (let s = 0; s < this._sections.length; s++) { const section = this._sections[s]; - const sectionEl = h('div.thumbnail-section@root', [ - h('div.thumbnail-section-title@title'), - h('div.thumbnail-section-images@images'), - ]); - if (section.title && this._sections.length > 1) { - sectionEl.title.textContent = section.title; - } else { - sectionEl.title.style.display = 'none'; + // Add separator between sections (not before the first) + if (s > 0 && this._sections.length > 1) { + this._elements.sectionsContainer.appendChild(h('div.thumbnail-separator').root); } for (let i = 0; i < section.images.length; i++) { @@ -201,12 +196,10 @@ export class ImageCarouselEditor extends EditorPane { this.updateCurrentImage(); })); - sectionEl.images.appendChild(btn); + this._elements.sectionsContainer.appendChild(btn); this._thumbnailElements.push(btn); flatIndex++; } - - this._elements.sectionsContainer.appendChild(sectionEl.root); } this._container.appendChild(elements.root); @@ -216,7 +209,7 @@ export class ImageCarouselEditor extends EditorPane { } /** - * Update only the changing parts: main image src, caption, button states, thumbnail selection. + * Update only the changing parts: main image src, caption, counter, button states, thumbnail selection. * No DOM teardown/rebuild — eliminates the blank flash. */ private updateCurrentImage(): void { @@ -254,10 +247,25 @@ export class ImageCarouselEditor extends EditorPane { thumbnail.classList.toggle('active', isActive); if (isActive) { thumbnail.setAttribute('aria-current', 'page'); + // Scroll only the thumbnail strip, not the entire editor + const container = this._elements.sectionsContainer; + const containerRect = container.getBoundingClientRect(); + const thumbRect = thumbnail.getBoundingClientRect(); + if (thumbRect.left < containerRect.left) { + container.scrollLeft += thumbRect.left - containerRect.left; + } else if (thumbRect.right > containerRect.right) { + container.scrollLeft += thumbRect.right - containerRect.right; + } } else { thumbnail.removeAttribute('aria-current'); } } + + // Update editor title to reflect current section + if (this.input instanceof ImageCarouselEditorInput) { + const currentSection = this._sections[entry.sectionIndex]; + this.input.setName(currentSection.title || this.input.collection.title); + } } previous(): void { diff --git a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts index 826db15c86b..5d0b17ba2e5 100644 --- a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts +++ b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts @@ -13,6 +13,7 @@ export class ImageCarouselEditorInput extends EditorInput { static readonly ID = 'workbench.input.imageCarousel'; private _resource: URI; + private _name: string; constructor( public readonly collection: IImageCarouselCollection, @@ -23,6 +24,7 @@ export class ImageCarouselEditorInput extends EditorInput { scheme: Schemas.vscodeImageCarousel, path: `/${encodeURIComponent(collection.id)}`, }); + this._name = collection.title; } get typeId(): string { @@ -34,7 +36,14 @@ export class ImageCarouselEditorInput extends EditorInput { } override getName(): string { - return this.collection.title; + return this._name; + } + + setName(name: string): void { + if (this._name !== name) { + this._name = name; + this._onDidChangeLabel.fire(); + } } override matches(other: EditorInput | IUntypedEditorInput): boolean { diff --git a/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css b/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css index 75b0a3ead83..83fc4ca185f 100644 --- a/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css +++ b/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css @@ -26,6 +26,7 @@ height: 100%; padding: 12px; gap: 8px; + overflow: hidden; } .image-carousel-editor .slideshow-container:focus, @@ -117,6 +118,8 @@ gap: 2px; padding: 4px 0 8px; flex-shrink: 0; + min-width: 0; + max-width: 100%; } /* Extra gap before thumbnails */ @@ -132,31 +135,33 @@ padding: 0 24px; } -/* Sections container */ +/* Sections container — flat horizontal thumbnail strip */ .image-carousel-editor .sections-container { display: flex; - flex-direction: column; - gap: 8px; - overflow-y: auto; - padding: 0; - flex-shrink: 0; -} - -/* Section */ -.image-carousel-editor .thumbnail-section-title { - color: var(--vscode-descriptionForeground); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - padding: 0 4px 2px; -} - -.image-carousel-editor .thumbnail-section-images { - display: flex; + flex-direction: row; + align-items: center; gap: 6px; overflow-x: auto; - justify-content: center; + justify-content: safe center; + padding: 0; + flex-shrink: 0; + min-width: 0; + max-width: 100%; + scrollbar-width: none; +} + +.image-carousel-editor .sections-container::-webkit-scrollbar { + display: none; +} + +/* Subtle separator between sections */ +.image-carousel-editor .thumbnail-separator { + width: 1px; + height: 24px; + background: var(--vscode-editorWidget-border, var(--vscode-widget-border)); + opacity: 0.4; + flex-shrink: 0; + margin: 0 6px; } .image-carousel-editor .thumbnail {