mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
enhance image carousel functionality and UI improvements
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user