Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatPendingDragAndDrop.ts
5252 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../../base/browser/dom.js';6import { DragAndDropObserver } from '../../../../../base/browser/dom.js';7import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';8import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';9import { IChatPendingRequest } from '../../common/model/chatModel.js';10import { IChatRequestViewModel, IChatViewModel } from '../../common/model/chatViewModel.js';1112const PENDING_REQUEST_ID_ATTR = 'data-pending-request-id';13const PENDING_KIND_ATTR = 'data-pending-kind';14const DRAGGING_CLASS = 'chat-pending-dragging';1516interface IDragState {17readonly element: IChatRequestViewModel;18readonly pendingKind: ChatRequestQueueKind;19}2021/**22* Manages drag-and-drop reordering for pending (steering/queued) chat messages.23* Attaches drag handles to pending request rows and uses event delegation on24* the list container to handle drop targets, keeping logic isolated from the25* renderer itself.26*/27export class ChatPendingDragController extends Disposable {2829private _dragState: IDragState | undefined;30private readonly _insertIndicator: HTMLElement;3132constructor(33listContainer: HTMLElement,34private readonly _getViewModel: () => IChatViewModel | undefined,35@IChatService private readonly _chatService: IChatService,36) {37super();3839this._insertIndicator = dom.$('.chat-pending-insert-indicator');40listContainer.append(this._insertIndicator);41this._register(toDisposable(() => this._insertIndicator.remove()));4243this._register(new DragAndDropObserver(listContainer, {44onDragOver: (e) => this._onDragOver(e),45onDragLeave: () => this._hideIndicator(),46onDragEnd: () => this._onDragEnd(),47onDrop: (e) => this._onDrop(e),48}));49}5051/**52* Called by the renderer to wire up a drag handle for a pending request row.53*/54attachDragHandle(55element: IChatRequestViewModel,56handleEl: HTMLElement,57rowContainer: HTMLElement,58disposables: DisposableStore,59): void {60handleEl.setAttribute('draggable', 'true');6162disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_START, (e: DragEvent) => {63if (!e.dataTransfer || !element.pendingKind) {64return;65}6667this._dragState = { element, pendingKind: element.pendingKind };68rowContainer.classList.add(DRAGGING_CLASS);6970// Use the row as the drag image71e.dataTransfer.setDragImage(rowContainer, 0, 0);72e.dataTransfer.effectAllowed = 'move';73}));7475disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_END, () => {76rowContainer.classList.remove(DRAGGING_CLASS);77this._onDragEnd();78}));79}8081// --- drag event handlers (delegated on the container) ---8283private _onDragOver(e: DragEvent): void {84if (!this._dragState) {85return;86}8788const target = this._findDropTarget(e);89if (!target) {90this._hideIndicator();91return;92}9394e.preventDefault();95if (e.dataTransfer) {96e.dataTransfer.dropEffect = 'move';97}9899const rect = target.row.getBoundingClientRect();100const midY = rect.top + rect.height / 2;101const before = e.clientY < midY;102103this._showIndicator(target.row, before);104}105106private _onDrop(e: DragEvent): void {107this._hideIndicator();108if (!this._dragState) {109return;110}111112const target = this._findDropTarget(e);113if (!target) {114return;115}116117e.preventDefault();118119const rect = target.row.getBoundingClientRect();120const midY = rect.top + rect.height / 2;121const insertBefore = e.clientY < midY;122123this._reorder(this._dragState.element, target.requestId, insertBefore);124this._dragState = undefined;125}126127private _onDragEnd(): void {128this._hideIndicator();129this._dragState = undefined;130}131132// --- indicator positioning ---133134private _showIndicator(targetRow: HTMLElement, before: boolean): void {135const rect = targetRow.getBoundingClientRect();136const parentRect = this._insertIndicator.parentElement!.getBoundingClientRect();137this._insertIndicator.style.display = 'block';138this._insertIndicator.style.left = `${rect.left - parentRect.left}px`;139this._insertIndicator.style.width = `${rect.width}px`;140this._insertIndicator.style.top = before141? `${rect.top - parentRect.top}px`142: `${rect.bottom - parentRect.top}px`;143}144145private _hideIndicator(): void {146this._insertIndicator.style.display = 'none';147}148149// --- target resolution ---150151private _findDropTarget(e: DragEvent): { row: HTMLElement; requestId: string } | undefined {152if (!this._dragState) {153return undefined;154}155156const target = (e.target as HTMLElement)?.closest?.<HTMLElement>(`[${PENDING_REQUEST_ID_ATTR}]`);157if (!target) {158return undefined;159}160161const requestId = target.getAttribute(PENDING_REQUEST_ID_ATTR)!;162const kind = target.getAttribute(PENDING_KIND_ATTR);163164// Only allow reorder within the same group165if (kind !== this._dragState.pendingKind || requestId === this._dragState.element.id) {166return undefined;167}168169return { row: target, requestId };170}171172// --- reorder logic ---173174private _reorder(draggedElement: IChatRequestViewModel, targetId: string, insertBefore: boolean): void {175const viewModel = this._getViewModel();176if (!viewModel) {177return;178}179180const pendingRequests = viewModel.model.getPendingRequests();181const draggedKind = draggedElement.pendingKind!;182183// Split into the dragged kind's group and the rest (preserving order)184const group: IChatPendingRequest[] = [];185const rest: IChatPendingRequest[] = [];186for (const p of pendingRequests) {187(p.kind === draggedKind ? group : rest).push(p);188}189190// Remove dragged from group191const draggedIdx = group.findIndex(p => p.request.id === draggedElement.id);192if (draggedIdx === -1) {193return;194}195const [dragged] = group.splice(draggedIdx, 1);196197// Find target position and insert198let targetIdx = group.findIndex(p => p.request.id === targetId);199if (targetIdx === -1) {200return;201}202if (!insertBefore) {203targetIdx++;204}205group.splice(targetIdx, 0, dragged);206207// Rebuild full list: steering first, then queued (matching addPendingRequest ordering)208const reordered = (draggedKind === ChatRequestQueueKind.Steering209? [...group, ...rest] // group is steering, rest is queued210: [...rest, ...group] // rest is steering, group is queued211).map(p => ({ requestId: p.request.id, kind: p.kind }));212213this._chatService.setPendingRequests(viewModel.sessionResource, reordered);214}215}216217218