Path: blob/main/src/vs/workbench/contrib/chat/browser/planReviewFeedback/planReviewFeedbackService.ts
13406 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 { Emitter, Event } from '../../../../../base/common/event.js';6import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { generateUuid } from '../../../../../base/common/uuid.js';9import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';10import { IChatPlanReviewResult } from '../../common/chatService/chatService.js';1112export interface IPlanReviewFeedbackItem {13readonly id: string;14readonly line: number;15readonly column: number;16readonly text: string;17}1819export const IPlanReviewFeedbackService = createDecorator<IPlanReviewFeedbackService>('planReviewFeedbackService');2021export interface IPlanReviewFeedbackService {22readonly _serviceBrand: undefined;2324readonly onDidChangeFeedback: Event<URI>;25readonly onDidChangeNavigation: Event<URI>;26readonly onDidChangeRegistrations: Event<void>;2728registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable;29isActivePlanReview(uri: URI): boolean;30addFeedback(planUri: URI, line: number, column: number, text: string): string;31removeFeedback(planUri: URI, feedbackId: string): void;32updateFeedback(planUri: URI, feedbackId: string, newText: string): void;33getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[];34clearFeedback(planUri: URI): void;35getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined;36getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number };37setNavigationAnchor(planUri: URI, itemId: string | undefined): void;38submitAllFeedback(planUri: URI): void;39}4041interface IPlanReviewRegistration {42readonly onSubmit: (result: IChatPlanReviewResult) => void;43readonly items: IPlanReviewFeedbackItem[];44navigationAnchor: string | undefined;45}4647export class PlanReviewFeedbackService extends Disposable implements IPlanReviewFeedbackService {4849declare readonly _serviceBrand: undefined;5051private readonly _registrations = new Map<string, IPlanReviewRegistration>();5253private readonly _onDidChangeFeedback = this._register(new Emitter<URI>());54readonly onDidChangeFeedback: Event<URI> = this._onDidChangeFeedback.event;5556private readonly _onDidChangeNavigation = this._register(new Emitter<URI>());57readonly onDidChangeNavigation: Event<URI> = this._onDidChangeNavigation.event;5859private readonly _onDidChangeRegistrations = this._register(new Emitter<void>());60readonly onDidChangeRegistrations: Event<void> = this._onDidChangeRegistrations.event;6162registerPlanReview(planUri: URI, onSubmit: (result: IChatPlanReviewResult) => void): IDisposable {63const key = planUri.toString();64this._registrations.set(key, { onSubmit, items: [], navigationAnchor: undefined });65this._onDidChangeRegistrations.fire();66return toDisposable(() => {67this._registrations.delete(key);68this._onDidChangeRegistrations.fire();69});70}7172isActivePlanReview(uri: URI): boolean {73return this._registrations.has(uri.toString());74}7576addFeedback(planUri: URI, line: number, column: number, text: string): string {77const key = planUri.toString();78const registration = this._registrations.get(key);79if (!registration) {80return '';81}8283const id = generateUuid();84registration.items.push({ id, line, column, text });85// Keep items sorted by line number86registration.items.sort((a, b) => a.line - b.line || a.column - b.column);87this._onDidChangeFeedback.fire(planUri);88return id;89}9091removeFeedback(planUri: URI, feedbackId: string): void {92const key = planUri.toString();93const registration = this._registrations.get(key);94if (!registration) {95return;96}9798const idx = registration.items.findIndex(item => item.id === feedbackId);99if (idx >= 0) {100registration.items.splice(idx, 1);101this._onDidChangeFeedback.fire(planUri);102}103}104105updateFeedback(planUri: URI, feedbackId: string, newText: string): void {106const key = planUri.toString();107const registration = this._registrations.get(key);108if (!registration) {109return;110}111112const idx = registration.items.findIndex(item => item.id === feedbackId);113if (idx >= 0) {114const old = registration.items[idx];115registration.items[idx] = { id: old.id, line: old.line, column: old.column, text: newText };116this._onDidChangeFeedback.fire(planUri);117}118}119120getFeedback(planUri: URI): readonly IPlanReviewFeedbackItem[] {121const key = planUri.toString();122return this._registrations.get(key)?.items ?? [];123}124125clearFeedback(planUri: URI): void {126const key = planUri.toString();127const registration = this._registrations.get(key);128if (!registration || registration.items.length === 0) {129return;130}131registration.items.length = 0;132registration.navigationAnchor = undefined;133this._onDidChangeFeedback.fire(planUri);134}135136getNextFeedback(planUri: URI, next: boolean): IPlanReviewFeedbackItem | undefined {137const key = planUri.toString();138const registration = this._registrations.get(key);139if (!registration || registration.items.length === 0) {140return undefined;141}142143const items = registration.items;144const anchorIdx = registration.navigationAnchor145? items.findIndex(item => item.id === registration.navigationAnchor)146: -1;147148let targetIdx: number;149if (anchorIdx === -1) {150targetIdx = next ? 0 : items.length - 1;151} else {152targetIdx = next153? (anchorIdx + 1) % items.length154: (anchorIdx - 1 + items.length) % items.length;155}156157const target = items[targetIdx];158this.setNavigationAnchor(planUri, target.id);159return target;160}161162getNavigationBearing(planUri: URI): { activeIdx: number; totalCount: number } {163const key = planUri.toString();164const registration = this._registrations.get(key);165if (!registration) {166return { activeIdx: -1, totalCount: 0 };167}168169const totalCount = registration.items.length;170if (!registration.navigationAnchor) {171return { activeIdx: -1, totalCount };172}173174const activeIdx = registration.items.findIndex(item => item.id === registration.navigationAnchor);175return { activeIdx, totalCount };176}177178setNavigationAnchor(planUri: URI, itemId: string | undefined): void {179const key = planUri.toString();180const registration = this._registrations.get(key);181if (registration) {182registration.navigationAnchor = itemId;183this._onDidChangeNavigation.fire(planUri);184}185}186187submitAllFeedback(planUri: URI): void {188const key = planUri.toString();189const registration = this._registrations.get(key);190if (!registration || registration.items.length === 0) {191return;192}193194const formatted = this._formatFeedback(registration.items);195registration.onSubmit({ rejected: false, feedback: formatted });196}197198private _formatFeedback(items: readonly IPlanReviewFeedbackItem[]): string {199const parts: string[] = ['Here\'s the feedback:'];200for (const item of items) {201if (item.column > 1) {202parts.push(`Line ${item.line}: Column ${item.column}: ${item.text}`);203} else {204parts.push(`Line ${item.line}: ${item.text}`);205}206}207return parts.join('\n');208}209}210211212