Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatPendingDragAndDrop.ts
5252 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../../base/browser/dom.js';
7
import { DragAndDropObserver } from '../../../../../base/browser/dom.js';
8
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
9
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
10
import { IChatPendingRequest } from '../../common/model/chatModel.js';
11
import { IChatRequestViewModel, IChatViewModel } from '../../common/model/chatViewModel.js';
12
13
const PENDING_REQUEST_ID_ATTR = 'data-pending-request-id';
14
const PENDING_KIND_ATTR = 'data-pending-kind';
15
const DRAGGING_CLASS = 'chat-pending-dragging';
16
17
interface IDragState {
18
readonly element: IChatRequestViewModel;
19
readonly pendingKind: ChatRequestQueueKind;
20
}
21
22
/**
23
* Manages drag-and-drop reordering for pending (steering/queued) chat messages.
24
* Attaches drag handles to pending request rows and uses event delegation on
25
* the list container to handle drop targets, keeping logic isolated from the
26
* renderer itself.
27
*/
28
export class ChatPendingDragController extends Disposable {
29
30
private _dragState: IDragState | undefined;
31
private readonly _insertIndicator: HTMLElement;
32
33
constructor(
34
listContainer: HTMLElement,
35
private readonly _getViewModel: () => IChatViewModel | undefined,
36
@IChatService private readonly _chatService: IChatService,
37
) {
38
super();
39
40
this._insertIndicator = dom.$('.chat-pending-insert-indicator');
41
listContainer.append(this._insertIndicator);
42
this._register(toDisposable(() => this._insertIndicator.remove()));
43
44
this._register(new DragAndDropObserver(listContainer, {
45
onDragOver: (e) => this._onDragOver(e),
46
onDragLeave: () => this._hideIndicator(),
47
onDragEnd: () => this._onDragEnd(),
48
onDrop: (e) => this._onDrop(e),
49
}));
50
}
51
52
/**
53
* Called by the renderer to wire up a drag handle for a pending request row.
54
*/
55
attachDragHandle(
56
element: IChatRequestViewModel,
57
handleEl: HTMLElement,
58
rowContainer: HTMLElement,
59
disposables: DisposableStore,
60
): void {
61
handleEl.setAttribute('draggable', 'true');
62
63
disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_START, (e: DragEvent) => {
64
if (!e.dataTransfer || !element.pendingKind) {
65
return;
66
}
67
68
this._dragState = { element, pendingKind: element.pendingKind };
69
rowContainer.classList.add(DRAGGING_CLASS);
70
71
// Use the row as the drag image
72
e.dataTransfer.setDragImage(rowContainer, 0, 0);
73
e.dataTransfer.effectAllowed = 'move';
74
}));
75
76
disposables.add(dom.addDisposableListener(handleEl, dom.EventType.DRAG_END, () => {
77
rowContainer.classList.remove(DRAGGING_CLASS);
78
this._onDragEnd();
79
}));
80
}
81
82
// --- drag event handlers (delegated on the container) ---
83
84
private _onDragOver(e: DragEvent): void {
85
if (!this._dragState) {
86
return;
87
}
88
89
const target = this._findDropTarget(e);
90
if (!target) {
91
this._hideIndicator();
92
return;
93
}
94
95
e.preventDefault();
96
if (e.dataTransfer) {
97
e.dataTransfer.dropEffect = 'move';
98
}
99
100
const rect = target.row.getBoundingClientRect();
101
const midY = rect.top + rect.height / 2;
102
const before = e.clientY < midY;
103
104
this._showIndicator(target.row, before);
105
}
106
107
private _onDrop(e: DragEvent): void {
108
this._hideIndicator();
109
if (!this._dragState) {
110
return;
111
}
112
113
const target = this._findDropTarget(e);
114
if (!target) {
115
return;
116
}
117
118
e.preventDefault();
119
120
const rect = target.row.getBoundingClientRect();
121
const midY = rect.top + rect.height / 2;
122
const insertBefore = e.clientY < midY;
123
124
this._reorder(this._dragState.element, target.requestId, insertBefore);
125
this._dragState = undefined;
126
}
127
128
private _onDragEnd(): void {
129
this._hideIndicator();
130
this._dragState = undefined;
131
}
132
133
// --- indicator positioning ---
134
135
private _showIndicator(targetRow: HTMLElement, before: boolean): void {
136
const rect = targetRow.getBoundingClientRect();
137
const parentRect = this._insertIndicator.parentElement!.getBoundingClientRect();
138
this._insertIndicator.style.display = 'block';
139
this._insertIndicator.style.left = `${rect.left - parentRect.left}px`;
140
this._insertIndicator.style.width = `${rect.width}px`;
141
this._insertIndicator.style.top = before
142
? `${rect.top - parentRect.top}px`
143
: `${rect.bottom - parentRect.top}px`;
144
}
145
146
private _hideIndicator(): void {
147
this._insertIndicator.style.display = 'none';
148
}
149
150
// --- target resolution ---
151
152
private _findDropTarget(e: DragEvent): { row: HTMLElement; requestId: string } | undefined {
153
if (!this._dragState) {
154
return undefined;
155
}
156
157
const target = (e.target as HTMLElement)?.closest?.<HTMLElement>(`[${PENDING_REQUEST_ID_ATTR}]`);
158
if (!target) {
159
return undefined;
160
}
161
162
const requestId = target.getAttribute(PENDING_REQUEST_ID_ATTR)!;
163
const kind = target.getAttribute(PENDING_KIND_ATTR);
164
165
// Only allow reorder within the same group
166
if (kind !== this._dragState.pendingKind || requestId === this._dragState.element.id) {
167
return undefined;
168
}
169
170
return { row: target, requestId };
171
}
172
173
// --- reorder logic ---
174
175
private _reorder(draggedElement: IChatRequestViewModel, targetId: string, insertBefore: boolean): void {
176
const viewModel = this._getViewModel();
177
if (!viewModel) {
178
return;
179
}
180
181
const pendingRequests = viewModel.model.getPendingRequests();
182
const draggedKind = draggedElement.pendingKind!;
183
184
// Split into the dragged kind's group and the rest (preserving order)
185
const group: IChatPendingRequest[] = [];
186
const rest: IChatPendingRequest[] = [];
187
for (const p of pendingRequests) {
188
(p.kind === draggedKind ? group : rest).push(p);
189
}
190
191
// Remove dragged from group
192
const draggedIdx = group.findIndex(p => p.request.id === draggedElement.id);
193
if (draggedIdx === -1) {
194
return;
195
}
196
const [dragged] = group.splice(draggedIdx, 1);
197
198
// Find target position and insert
199
let targetIdx = group.findIndex(p => p.request.id === targetId);
200
if (targetIdx === -1) {
201
return;
202
}
203
if (!insertBefore) {
204
targetIdx++;
205
}
206
group.splice(targetIdx, 0, dragged);
207
208
// Rebuild full list: steering first, then queued (matching addPendingRequest ordering)
209
const reordered = (draggedKind === ChatRequestQueueKind.Steering
210
? [...group, ...rest] // group is steering, rest is queued
211
: [...rest, ...group] // rest is steering, group is queued
212
).map(p => ({ requestId: p.request.id, kind: p.kind }));
213
214
this._chatService.setPendingRequests(viewModel.sessionResource, reordered);
215
}
216
}
217
218