enhance image carousel functionality and UI improvements

This commit is contained in:
Peng Lyu
2026-03-12 19:45:00 -07:00
parent e7e6cbe3a9
commit e08c8efd77
5 changed files with 98 additions and 53 deletions

View File

@@ -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 => {

View File

@@ -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<IHoverOptions> = {
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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {