Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackService.ts
13406 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 { Emitter, Event } from '../../../../../base/common/event.js';
7
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { generateUuid } from '../../../../../base/common/uuid.js';
10
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
11
import { IChatPlanReviewResult } from '../../common/chatService/chatService.js';
12
13
export interface IPlanReviewFeedbackItem {
14
readonly id: string;
15
readonly line: number;
16
readonly column: number;
17
readonly text: string;
18
}
19
20
export const IPlanReviewFeedbackService = createDecorator<IPlanReviewFeedbackService>('planReviewFeedbackService');
21
22
export interface IPlanReviewFeedbackService {
23
readonly _serviceBrand: undefined;
24
25
readonly onDidChangeFeedback: Event<URI>;
26
readonly onDidChangeNavigation: Event<URI>;
27
readonly onDidChangeRegistrations: Event<void>;
28
29
registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable;
30
isActivePlanReview(uri: URI): boolean;
31
addFeedback(planUri: URI, line: number, column: number, text: string): string;
32
removeFeedback(planUri: URI, feedbackId: string): void;
33
updateFeedback(planUri: URI, feedbackId: string, newText: string): void;
34
getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[];
35
clearFeedback(planUri: URI): void;
36
getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined;
37
getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number };
38
setNavigationAnchor(planUri: URI, itemId: string | undefined): void;
39
submitAllFeedback(planUri: URI): void;
40
}
41
42
interface IPlanReviewRegistration {
43
readonly onSubmit: (result: IChatPlanReviewResult) => void;
44
readonly items: IPlanReviewFeedbackItem[];
45
navigationAnchor: string | undefined;
46
}
47
48
export class PlanReviewFeedbackService extends Disposable implements IPlanReviewFeedbackService {
49
50
declare readonly _serviceBrand: undefined;
51
52
private readonly _registrations = new Map<string, IPlanReviewRegistration>();
53
54
private readonly _onDidChangeFeedback = this._register(new Emitter<URI>());
55
readonly onDidChangeFeedback: Event<URI> = this._onDidChangeFeedback.event;
56
57
private readonly _onDidChangeNavigation = this._register(new Emitter<URI>());
58
readonly onDidChangeNavigation: Event<URI> = this._onDidChangeNavigation.event;
59
60
private readonly _onDidChangeRegistrations = this._register(new Emitter<void>());
61
readonly onDidChangeRegistrations: Event<void> = this._onDidChangeRegistrations.event;
62
63
registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable {
64
const key = planUri.toString();
65
this._registrations.set(key, { onSubmit, items: [], navigationAnchor: undefined });
66
this._onDidChangeRegistrations.fire();
67
return toDisposable(() => {
68
this._registrations.delete(key);
69
this._onDidChangeRegistrations.fire();
70
});
71
}
72
73
isActivePlanReview(uri: URI): boolean {
74
return this._registrations.has(uri.toString());
75
}
76
77
addFeedback(planUri: URI, line: number, column: number, text: string): string {
78
const key = planUri.toString();
79
const registration = this._registrations.get(key);
80
if (!registration) {
81
return '';
82
}
83
84
const id = generateUuid();
85
registration.items.push({ id, line, column, text });
86
// Keep items sorted by line number
87
registration.items.sort((a, b) => a.line - b.line || a.column - b.column);
88
this._onDidChangeFeedback.fire(planUri);
89
return id;
90
}
91
92
removeFeedback(planUri: URI, feedbackId: string): void {
93
const key = planUri.toString();
94
const registration = this._registrations.get(key);
95
if (!registration) {
96
return;
97
}
98
99
const idx = registration.items.findIndex(item => item.id === feedbackId);
100
if (idx >= 0) {
101
registration.items.splice(idx, 1);
102
this._onDidChangeFeedback.fire(planUri);
103
}
104
}
105
106
updateFeedback(planUri: URI, feedbackId: string, newText: string): void {
107
const key = planUri.toString();
108
const registration = this._registrations.get(key);
109
if (!registration) {
110
return;
111
}
112
113
const idx = registration.items.findIndex(item => item.id === feedbackId);
114
if (idx >= 0) {
115
const old = registration.items[idx];
116
registration.items[idx] = { id: old.id, line: old.line, column: old.column, text: newText };
117
this._onDidChangeFeedback.fire(planUri);
118
}
119
}
120
121
getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[] {
122
const key = planUri.toString();
123
return this._registrations.get(key)?.items ?? [];
124
}
125
126
clearFeedback(planUri: URI): void {
127
const key = planUri.toString();
128
const registration = this._registrations.get(key);
129
if (!registration || registration.items.length === 0) {
130
return;
131
}
132
registration.items.length = 0;
133
registration.navigationAnchor = undefined;
134
this._onDidChangeFeedback.fire(planUri);
135
}
136
137
getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined {
138
const key = planUri.toString();
139
const registration = this._registrations.get(key);
140
if (!registration || registration.items.length === 0) {
141
return undefined;
142
}
143
144
const items = registration.items;
145
const anchorIdx = registration.navigationAnchor
146
? items.findIndex(item => item.id === registration.navigationAnchor)
147
: -1;
148
149
let targetIdx: number;
150
if (anchorIdx === -1) {
151
targetIdx = next ? 0 : items.length - 1;
152
} else {
153
targetIdx = next
154
? (anchorIdx + 1) % items.length
155
: (anchorIdx - 1 + items.length) % items.length;
156
}
157
158
const target = items[targetIdx];
159
this.setNavigationAnchor(planUri, target.id);
160
return target;
161
}
162
163
getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number } {
164
const key = planUri.toString();
165
const registration = this._registrations.get(key);
166
if (!registration) {
167
return { activeIdx: -1, totalCount: 0 };
168
}
169
170
const totalCount = registration.items.length;
171
if (!registration.navigationAnchor) {
172
return { activeIdx: -1, totalCount };
173
}
174
175
const activeIdx = registration.items.findIndex(item => item.id === registration.navigationAnchor);
176
return { activeIdx, totalCount };
177
}
178
179
setNavigationAnchor(planUri: URI, itemId: string | undefined): void {
180
const key = planUri.toString();
181
const registration = this._registrations.get(key);
182
if (registration) {
183
registration.navigationAnchor = itemId;
184
this._onDidChangeNavigation.fire(planUri);
185
}
186
}
187
188
submitAllFeedback(planUri: URI): void {
189
const key = planUri.toString();
190
const registration = this._registrations.get(key);
191
if (!registration || registration.items.length === 0) {
192
return;
193
}
194
195
const formatted = this._formatFeedback(registration.items);
196
registration.onSubmit({ rejected: false, feedback: formatted });
197
}
198
199
private _formatFeedback(items: readonly IPlanReviewFeedbackItem[]): string {
200
const parts: string[] = ['Here\'s the feedback:'];
201
for (const item of items) {
202
if (item.column > 1) {
203
parts.push(`Line ${item.line}: Column ${item.column}: ${item.text}`);
204
} else {
205
parts.push(`Line ${item.line}: ${item.text}`);
206
}
207
}
208
return parts.join('\n');
209
}
210
}
211
212