src/vs/platform/agentHost/node/shared/agentFeedbackServerTools.ts

611 LOC · 598 covered · 13 uncovered · 105 ranges · 2763 concepts · 33 introducers · 1280 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentFeedbackServerTools.ts ×23
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 { generateUuid } from '../../../../base/common/uuid.js';
7 > import { localize } from '../../../../nls.js';
8 > import { FEEDBACK_ANNOTATION_META_KEY, readFeedbackAnnotationMeta, VIEW_UNREVIEWED_COMMENTS_TOOL_NAME, ADD_COMMENT_TOOL_NAME, type IFeedbackAnnotationMeta } from '../../common/meta/agentFeedbackAnnotations.js';
9 > import { buildAnnotationsUri } from '../../common/annotationsUri.js';
10 > import type { AnnotationsAction } from '../../common/state/sessionActions.js';
11 > import { ActionType } from '../../common/state/protocol/common/actions.js';
12 > import { parseChatUri, type Annotation, type AnnotationsState, type StringOrMarkdown, type TextRange, type ToolDefinition } from '../../common/state/sessionState.js';
13 > import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } from './agentServerToolHost.js';
14 >
15 > /**
16 > * Server-side implementation of the agent feedback ("comments") tools.
17 > *
18 > * These tools used to be registered on the client (agents window) and keyed
19 > * off an in-memory store. For agent-host sessions they now execute on the
20 > * server against the session's annotations channel: each comment is an
21 > * {@link Annotation} on `<session>/annotations`, with feedback semantics
22 > * carried in {@link Annotation._meta} under {@link FEEDBACK_ANNOTATION_META_KEY}
23 > * (see `agentFeedbackAnnotations.ts`). The functions here are pure — they read
24 > * the current {@link AnnotationsState} and return the annotation actions to
25 > * dispatch plus a textual tool result — so they can be unit tested without a
26 > * running state manager. The host wiring (reading the snapshot, dispatching
27 > * the actions) lives in the caller.
28 > */
29 >
30 > export const addCommentToolName = ADD_COMMENT_TOOL_NAME;
31 > export const listCommentsToolName = 'listComments';
32 > export const deleteCommentsToolName = 'deleteComments';
33 > export const resolveCommentsToolName = 'resolveComments';
34 > export const viewUnreviewedCommentsToolName = VIEW_UNREVIEWED_COMMENTS_TOOL_NAME;
35 >
36 > /**
37 > * Feedback kinds that originate from a review the user is expected to triage
38 > * (a pull request review or an in-product code review) rather than being
39 > * authored by the user directly. Comments of these kinds that are still in the
40 > * `created` state are surfaced to the agent via the {@link listCommentsToolName}
41 > * note and revealed through {@link viewUnreviewedCommentsToolName}.
42 > */
43 > const REVIEWABLE_FEEDBACK_KINDS: ReadonlySet<string> = new Set(['prReview', 'codeReview']);
44 >
45 > /**
46 > * Server tools that must not be auto-approved: invoking them surfaces a
47 > * confirmation to the user (rendered by a custom client content part) before
48 > * the tool body runs. Providers consult {@link feedbackToolRequiresConfirmation}
49 > * (via the host) to exclude these from their server-tool auto-approve lists.
50 > */
51 > const feedbackConfirmationToolNames: ReadonlySet<string> = new Set([viewUnreviewedCommentsToolName]);
52 >
53 > /** Whether the given feedback server tool requires user confirmation before it runs. */
54 > export function feedbackToolRequiresConfirmation(toolName: string): boolean {
55 > return feedbackConfirmationToolNames.has(toolName); agentFeedbackServerTools.ts ×1
56 > }
58 > const addCommentInputSchema: ToolDefinition['inputSchema'] = {
59 > type: 'object',
60 > properties: {
61 > resourceUri: { type: 'string', description: 'URI of the file to add a comment to.' },
62 > range: {
63 > type: 'object',
64 > description: 'One-based text range to comment on.',
65 > properties: {
66 > startLineNumber: { type: 'number', description: 'One-based start line number.' },
67 > startColumn: { type: 'number', description: 'One-based start column.' },
68 > endLineNumber: { type: 'number', description: 'One-based end line number.' },
69 > endColumn: { type: 'number', description: 'One-based end column.' },
70 > },
71 > required: ['startLineNumber', 'startColumn', 'endLineNumber', 'endColumn'],
72 > },
73 > text: { type: 'string', description: 'Comment text to add.' },
74 > },
75 > required: ['resourceUri', 'range', 'text'],
76 > };
77 >
78 > const listCommentsInputSchema: ToolDefinition['inputSchema'] = {
79 > type: 'object',
80 > properties: {},
81 > };
82 >
83 > const viewUnreviewedCommentsInputSchema: ToolDefinition['inputSchema'] = {
84 > type: 'object',
85 > properties: {},
86 > };
87 >
88 > const deleteCommentsInputSchema: ToolDefinition['inputSchema'] = {
89 > type: 'object',
90 > properties: {
91 > commentIds: { type: 'array', items: { type: 'string' }, description: 'Comment IDs to delete.' },
92 > },
93 > required: ['commentIds'],
94 > };
95 >
96 > const resolveCommentsInputSchema: ToolDefinition['inputSchema'] = {
97 > type: 'object',
98 > properties: {
99 > commentIds: { type: 'array', items: { type: 'string' }, description: 'Comment IDs to update.' },
100 > resolved: { type: 'boolean', description: 'Whether the comments should be marked as resolved. Defaults to true.' },
101 > },
102 > required: ['commentIds'],
103 > };
104 >
105 > /**
106 > * Protocol {@link ToolDefinition}s for the feedback server tools, advertised on
107 > * {@link SessionState.serverTools} so clients know these tools are owned and
108 > * executed by the agent host.
109 > */
110 > export const feedbackServerToolDefinitions: ToolDefinition[] = [
111 > {
112 > name: addCommentToolName,
113 > title: 'Add Comment (Agent Feedback)',
114 > description: 'Add a comment to a file range.',
115 > inputSchema: addCommentInputSchema,
116 > annotations: { readOnlyHint: false },
117 > },
118 > {
119 > name: listCommentsToolName,
120 > title: 'List Comments (Agent Feedback)',
121 > description: 'List comments for this session.',
122 > inputSchema: listCommentsInputSchema,
123 > annotations: { readOnlyHint: true },
124 > },
125 > {
126 > name: deleteCommentsToolName,
127 > title: 'Delete Comments (Agent Feedback)',
128 > description: 'Delete comments for this session.',
129 > inputSchema: deleteCommentsInputSchema,
130 > annotations: { readOnlyHint: false, destructiveHint: true },
131 > },
132 > {
133 > name: resolveCommentsToolName,
134 > title: 'Resolve Comments (Agent Feedback)',
135 > description: 'Mark comments for this session as resolved or unresolved.',
136 > inputSchema: resolveCommentsInputSchema,
137 > annotations: { readOnlyHint: false },
138 > },
139 > {
140 > name: viewUnreviewedCommentsToolName,
141 > title: 'View Unreviewed Comments (Agent Feedback)',
142 > description: 'View pull request or code review comments that the user has not reviewed yet. Calling this asks the user to choose which of those comments to reveal; only the comments the user reveals are returned.',
143 > inputSchema: viewUnreviewedCommentsInputSchema,
144 > annotations: { readOnlyHint: true },
145 > },
146 > ];
147 >
148 > // --- Argument validation ------------------------------------------------------
149 >
150 > interface IOneBasedRange {
151 > readonly startLineNumber: number;
152 > readonly startColumn: number;
153 > readonly endLineNumber: number;
154 > readonly endColumn: number;
155 > }
156 >
157 > interface IAddCommentArgs {
158 > readonly resourceUri?: unknown;
159 > readonly range?: unknown;
160 > readonly text?: unknown;
161 > }
162 >
163 > interface IDeleteCommentsArgs {
164 > readonly commentIds?: unknown;
165 > }
166 >
167 > interface IResolveCommentsArgs {
168 > readonly commentIds?: unknown;
169 > readonly resolved?: unknown;
170 > }
171 >
172 > function getRequiredString(value: unknown, field: string, toolName: string): string { agentFeedbackServerTools.ts ×2
173 > if (typeof value !== 'string' || value.length === 0) {
174 > throw new Error(`Invalid ${toolName} input: ${field} must be a non-empty string.`); agentFeedbackServerTools.ts ×2
175 > }
176 > return value; agentFeedbackServerTools.ts ×2
177 > }
179 > function getRequiredPositiveInteger(value: unknown, field: string, toolName: string): number { agentFeedbackServerTools.ts ×4
180 > if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
181 throw new Error(`Invalid ${toolName} input: ${field} must be a positive integer.`);
182 }
183 > return value; agentFeedbackServerTools.ts ×4
184 > }
186 > function getAddCommentArgs(rawArgs: unknown): { resourceUri: string; range: IOneBasedRange; text: string } { agentFeedbackServerTools.ts ×2
187 > const args = (rawArgs ?? {}) as IAddCommentArgs;
188 > const resourceUri = getRequiredString(args.resourceUri, 'resourceUri', addCommentToolName);
189 > const text = getRequiredString(args.text, 'text', addCommentToolName);
190 > if (!args.range || typeof args.range !== 'object' || Array.isArray(args.range)) {
191 > throw new Error(`Invalid ${addCommentToolName} input: range must be an object.`); agentFeedbackServerTools.ts ×2
192 > }
193 > const range = args.range as Partial<IOneBasedRange>; agentFeedbackServerTools.ts ×4
194 > return {
195 > resourceUri,
196 > text,
197 > range: {
198 > startLineNumber: getRequiredPositiveInteger(range.startLineNumber, 'range.startLineNumber', addCommentToolName),
199 > startColumn: getRequiredPositiveInteger(range.startColumn, 'range.startColumn', addCommentToolName),
200 > endLineNumber: getRequiredPositiveInteger(range.endLineNumber, 'range.endLineNumber', addCommentToolName),
201 > endColumn: getRequiredPositiveInteger(range.endColumn, 'range.endColumn', addCommentToolName),
202 > },
203 > };
204 > }
206 > function getUniqueCommentIds(value: unknown, toolName: string): readonly string[] { agentFeedbackServerTools.ts ×2
207 > if (!Array.isArray(value) || value.length === 0) {
208 throw new Error(`Invalid ${toolName} input: commentIds must be a non-empty string array.`);
209 }
210 > const ids: string[] = []; agentFeedbackServerTools.ts ×2
211 > for (const item of value) {
212 > ids.push(getRequiredString(item, 'commentIds[]', toolName));
213 > }
214 > return [...new Set(ids)];
215 > }
217 > function getResolvedFlag(value: unknown): boolean { agentFeedbackServerTools.ts ×3
218 > if (value === undefined) {
219 > return true; agentFeedbackServerTools.ts ×1
220 > }
221 > if (typeof value !== 'boolean') { agentFeedbackServerTools.ts ×2
222 throw new Error(`Invalid ${resolveCommentsToolName} input: resolved must be a boolean.`);
223 }
224 > return value; agentFeedbackServerTools.ts ×2
225 > }
227 > // --- Annotation <-> feedback conversion ---------------------------------------
228 >
229 > function toTextRange(range: IOneBasedRange): TextRange { agentFeedbackServerTools.ts ×4
230 > return {
231 > start: { line: range.startLineNumber - 1, character: range.startColumn - 1 },
232 > end: { line: range.endLineNumber - 1, character: range.endColumn - 1 },
233 > };
234 > }
236 > function fromTextRange(range: TextRange | undefined): IOneBasedRange { agentFeedbackServerTools.ts ×4
237 > if (!range) {
238 return { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 };
239 }
241 > startLineNumber: range.start.line + 1,
242 > startColumn: range.start.character + 1,
243 > endLineNumber: range.end.line + 1,
244 > endColumn: range.end.character + 1,
245 > };
246 > }
248 > function entryText(text: StringOrMarkdown): string { agentFeedbackServerTools.ts ×4
249 > return typeof text === 'string' ? text : text.markdown;
250 > }
252 > function readMeta(annotation: Annotation): IFeedbackAnnotationMeta | undefined { agentFeedbackAnnotations.ts ×5
253 > return readFeedbackAnnotationMeta(annotation);
254 > }
256 > interface ISerializedComment {
257 > readonly id: string;
258 > readonly resourceUri: string;
259 > readonly range: IOneBasedRange;
260 > readonly text: string;
261 > readonly kind: string;
262 > readonly resolved: boolean;
263 > readonly replies?: readonly string[];
264 > }
265 >
266 > function serializeComment(annotation: Annotation): ISerializedComment { agentFeedbackServerTools.ts ×4
267 > const entries = annotation.entries ?? [];
268 > const meta = readMeta(annotation);
269 > const replies = entries.slice(1).map(e => entryText(e.text));
270 > return {
271 > id: annotation.id,
272 > resourceUri: annotation.resource,
273 > range: fromTextRange(annotation.range),
274 > text: entries.length ? entryText(entries[0].text) : '',
275 > kind: meta?.kind ?? 'user',
276 > resolved: annotation.resolved,
277 > ...(replies.length ? { replies } : {}),
278 > };
279 > }
281 > /**
282 > * Comments visible to the agent: everything except items still in the
283 > * `created` state (the agent added them but the user has not accepted them
284 > * yet). Mirrors the client `getListableFeedback` behavior.
285 > */
286 > function listableAnnotations(state: AnnotationsState): Annotation[] { agentFeedbackServerTools.ts ×2
287 > return state.annotations.filter(annotation => {
288 > const meta = readMeta(annotation);
289 > // The annotations channel is generic and may carry annotations produced
290 > // by other features. Only annotations that carry feedback metadata are
291 > // feedback comments; the feedback tools must never list, delete, or
292 > // resolve unrelated annotations.
293 > if (!meta || !annotation.entries?.length) {
294 > return false; agentFeedbackServerTools.ts ×3
295 > }
296 > const effectiveState = annotation.resolved ? 'resolved' : (meta.state ?? 'accepted'); agentFeedbackServerTools.ts ×2
297 > return effectiveState !== 'created';
298 > });
299 > }
301 > /**
302 > * Feedback annotations of a {@link REVIEWABLE_FEEDBACK_KINDS reviewable kind}
303 > * the user has flagged for reveal to the agent (via the confirmation of the
304 > * {@link viewUnreviewedCommentsToolName} tool). These are exactly the comments
305 > * the user chose to reveal for the current invocation; everything else
306 > * (including review comments that happen to be accepted from a previous reveal
307 > * or a manual accept) is excluded.
308 > */
309 > function pendingRevealAnnotations(state: AnnotationsState): Annotation[] { agentFeedbackServerTools.ts ×5
310 > return state.annotations.filter(annotation => {
311 > const meta = readMeta(annotation);
312 > if (!meta || !annotation.entries?.length) {
313 return false;
314 }
315 > return REVIEWABLE_FEEDBACK_KINDS.has(meta.kind) && meta.pendingAgentReveal === true; agentFeedbackServerTools.ts ×5
316 > });
317 > }
319 > /** Returns a copy of {@link annotation} with the {@link IFeedbackAnnotationMeta.pendingAgentReveal} flag cleared. */
320 > function clearPendingReveal(annotation: Annotation): Annotation { agentFeedbackServerTools.ts ×5
321 > const meta = readMeta(annotation);
322 > if (!meta) {
323 return annotation;
324 }
325 > const nextMeta: IFeedbackAnnotationMeta = { ...meta, pendingAgentReveal: undefined }; agentFeedbackServerTools.ts ×5
326 > return { ...annotation, _meta: { ...annotation._meta, [FEEDBACK_ANNOTATION_META_KEY]: nextMeta } };
327 > }
329 > /**
330 > * Reviewable (PR / code review) feedback annotations the user has not reviewed
331 > * yet, i.e. still in the `created` state. Used to build the
332 > * {@link listCommentsToolName} note.
333 > */
334 > function createdReviewableAnnotations(state: AnnotationsState): Annotation[] { agentFeedbackServerTools.ts ×6
335 > return state.annotations.filter(annotation => {
336 > const meta = readMeta(annotation);
337 > if (!meta || !annotation.entries?.length) {
338 > return false; agentFeedbackServerTools.ts ×3
339 > }
340 > return REVIEWABLE_FEEDBACK_KINDS.has(meta.kind) && !annotation.resolved && (meta.state ?? 'accepted') === 'created'; agentFeedbackServerTools.ts ×6
341 > });
342 > }
344 > /**
345 > * A short note appended to the {@link listCommentsToolName} result when there
346 > * are reviewable comments the user has not accepted yet, pointing the agent at
347 > * {@link viewUnreviewedCommentsToolName}. Returns `undefined` (no note) when
348 > * there are no such comments.
349 > */
350 > function buildUnreviewedCommentsNote(state: AnnotationsState): string | undefined { agentFeedbackServerTools.ts ×6
351 > const created = createdReviewableAnnotations(state);
352 > if (!created.length) {
353 > return undefined; agentFeedbackServerTools.ts ×1
354 > }
355 > let prCount = 0; agentFeedbackServerTools.ts ×4
356 > let codeReviewCount = 0;
357 > for (const annotation of created) {
358 > const kind = readMeta(annotation)?.kind;
359 > if (kind === 'prReview') {
361 > } else if (kind === 'codeReview') { agentFeedbackServerTools.ts ×4
362 > codeReviewCount++;
363 > }
364 > }
365 > const clauses: string[] = [];
366 > if (prCount > 0) {
367 > clauses.push(`${prCount} pull request comment${prCount === 1 ? '' : 's'}`); agentFeedbackServerTools.ts ×2
368 > }
369 > if (codeReviewCount > 0) { agentFeedbackServerTools.ts ×4
370 > clauses.push(`${codeReviewCount} code review comment${codeReviewCount === 1 ? '' : 's'}`);
371 > }
372 > const subject = clauses.join(' and ');
373 > const verb = created.length === 1 ? 'is' : 'are'; agentFeedbackServerTools.ts ×6
374 > return `There ${verb} ${subject} which the user has not reviewed yet. If the user wants you to tackle them, call the \`${viewUnreviewedCommentsToolName}\` tool to view them.`;
375 > }
377 > // --- Tool execution -----------------------------------------------------------
378 >
379 > export interface IFeedbackToolOutcome {
380 > /** Annotation actions to dispatch on the session's annotations channel. */
381 > readonly actions: readonly AnnotationsAction[];
382 > /** Textual tool result returned to the agent. */
383 > readonly result: string;
384 > }
385 >
386 > /**
387 > * Executes a feedback server tool against the current annotation state.
388 > *
389 > * Pure: it does not mutate {@link state}, instead returning the annotation
390 > * actions the caller should dispatch (so the authoritative state manager
391 > * remains the single writer) along with the textual tool result.
392 > *
393 > * @throws if {@link toolName} is unknown or the arguments are invalid.
394 > */
395 > export function applyFeedbackTool(state: AnnotationsState, sessionResource: string, toolName: string, rawArgs: unknown): IFeedbackToolOutcome {
396 > switch (toolName) { agentFeedbackServerTools.ts ×7
397 > case addCommentToolName: {
398 > const { resourceUri, range, text } = getAddCommentArgs(rawArgs); agentFeedbackServerTools.ts ×2
399 > const id = generateUuid();
400 > // The agent adds comments in the `created` state; the user accepts
401 > // them before they are acted upon.
402 > const meta: IFeedbackAnnotationMeta = { kind: 'codeReview', state: 'created', sessionResource };
403 > const annotation: Annotation = {
404 > id,
405 > turnId: '',
406 > resource: resourceUri,
407 > range: toTextRange(range),
408 > resolved: false,
409 > entries: [{ id: `${id}:0`, text }],
410 > _meta: { [FEEDBACK_ANNOTATION_META_KEY]: meta },
411 > };
412 > return {
413 > actions: [{ type: ActionType.AnnotationsSet, annotation }],
414 > result: 'Comment added.',
415 > };
416 > }
417 > case listCommentsToolName: { agentFeedbackServerTools.ts ×7
418 > const payload: { comments: ISerializedComment[]; note?: string } = { agentFeedbackServerTools.ts ×6
419 > comments: listableAnnotations(state).map(serializeComment),
420 > };
421 > const note = buildUnreviewedCommentsNote(state);
422 > if (note) {
423 > payload.note = note; agentFeedbackServerTools.ts ×4
424 > }
425 > return { actions: [], result: JSON.stringify(payload, undefined, 2) }; agentFeedbackServerTools.ts ×6
426 > }
427 > case viewUnreviewedCommentsToolName: { agentFeedbackServerTools.ts ×7
428 > // The confirmation gate runs before this body. When the user accepts agentFeedbackServerTools.ts ×5
429 > // the confirmation, the client flags exactly the comments they chose
430 > // to reveal with `pendingAgentReveal` on the shared annotations
431 > // channel. Return those comments and clear the flag so a later
432 > // invocation does not re-return them; comments the user left
433 > // unchecked (and review comments accepted by other means) are not
434 > // flagged and so are excluded.
435 > const pending = pendingRevealAnnotations(state);
436 > const comments = pending.map(serializeComment);
437 > const actions: AnnotationsAction[] = pending.map(annotation => ({
438 > type: ActionType.AnnotationsSet,
439 > annotation: clearPendingReveal(annotation),
440 > }));
441 > return { actions, result: JSON.stringify({ comments }, undefined, 2) };
442 > }
443 > case deleteCommentsToolName: { agentFeedbackServerTools.ts ×7
444 > const ids = getUniqueCommentIds((rawArgs as IDeleteCommentsArgs)?.commentIds, deleteCommentsToolName); agentFeedbackServerTools.ts ×2
445 > const listable = listableAnnotations(state);
446 > const existing = new Map(listable.map(a => [a.id, a]));
447 > const actions: AnnotationsAction[] = [];
448 > const deleted: string[] = [];
449 > const notFound: string[] = [];
450 > for (const id of ids) {
451 > if (existing.has(id)) {
452 > actions.push({ type: ActionType.AnnotationsRemoved, annotationId: id }); agentFeedbackServerTools.ts ×1
453 > deleted.push(id);
455 > notFound.push(id);
456 > }
457 > }
458 > const remaining = listable.filter(a => !deleted.includes(a.id)).map(serializeComment);
459 > return {
460 > actions,
461 > result: JSON.stringify({ deletedCommentIds: deleted, notFoundCommentIds: notFound, remainingComments: remaining }, undefined, 2),
462 > };
463 > }
464 > case resolveCommentsToolName: { agentFeedbackServerTools.ts ×7
465 > const args = (rawArgs ?? {}) as IResolveCommentsArgs; agentFeedbackServerTools.ts ×3
466 > const ids = getUniqueCommentIds(args.commentIds, resolveCommentsToolName);
467 > const resolved = getResolvedFlag(args.resolved);
468 > const listable = listableAnnotations(state);
469 > const existing = new Map(listable.map(a => [a.id, a]));
470 > const actions: AnnotationsAction[] = [];
471 > const updated: string[] = [];
472 > const notFound: string[] = [];
473 > for (const id of ids) {
474 > const annotation = existing.get(id);
475 > if (!annotation) {
476 > notFound.push(id); agentFeedbackServerTools.ts ×3
477 > continue;
478 > }
479 > const meta = readMeta(annotation); agentFeedbackServerTools.ts ×1
480 > const nextMeta: IFeedbackAnnotationMeta = {
481 > ...meta,
482 > kind: meta?.kind ?? 'user', agentFeedbackServerTools.ts ×3
483 > state: resolved ? 'resolved' : 'submitted',
484 > sessionResource: meta?.sessionResource ?? sessionResource,
485 > };
486 > const nextAnnotation: Annotation = {
487 > ...annotation,
488 > resolved,
489 > _meta: { ...annotation._meta, [FEEDBACK_ANNOTATION_META_KEY]: nextMeta },
490 > };
491 > actions.push({ type: ActionType.AnnotationsSet, annotation: nextAnnotation });
492 > updated.push(id);
493 > }
494 > const comments = listable.map(a => updated.includes(a.id) ? serializeComment({ ...a, resolved }) : serializeComment(a));
495 > return {
496 > actions,
497 > result: JSON.stringify({ resolved, updatedCommentIds: updated, notFoundCommentIds: notFound, comments }, undefined, 2),
498 > };
499 > }
501 > throw new Error(`Unknown feedback server tool: ${toolName}`); agentFeedbackServerTools.ts ×1
503 > }
505 > /**
506 > * Parses the number of comments returned by the {@link listCommentsToolName}
507 > * tool from its JSON result (`{ comments: [...] }`). Returns `undefined` when
508 > * the result is missing or not in the expected shape, so the caller can fall
509 > * back to a count-less message.
510 > */
511 > function parseListedCommentCount(resultText: string | undefined): number | undefined { agentFeedbackServerTools.ts ×4
512 > if (!resultText) {
513 > return undefined; agentFeedbackServerTools.ts ×3
514 > }
516 > const parsed = JSON.parse(resultText) as { comments?: unknown };
517 > return Array.isArray(parsed.comments) ? parsed.comments.length : undefined;
518 > } catch {
519 > return undefined; agentFeedbackServerTools.ts ×3
520 > }
523 > /**
524 > * Display strings for the feedback ("comments") tools, authored here so every
525 > * provider (Copilot, Claude, Codex, …) renders them identically instead of
526 > * each provider's display layer re-deriving the strings from the tool name.
527 > * Returns `undefined` for tools this group does not own, so the caller falls
528 > * back to its generic display.
529 > *
530 > * {@link toolName} is the bare tool name (any transport prefix such as Claude's
531 > * `mcp__<server>__` has already been stripped by the dispatcher).
532 > */
533 > function getFeedbackToolDisplay(toolName: string, _args: unknown, result?: IServerToolDisplayResult): IServerToolDisplay | undefined { agentFeedbackServerTools.ts ×8
534 > switch (toolName) {
535 > case addCommentToolName:
537 > displayName: localize('toolName.addComment', "Add Comment"),
538 > invocationMessage: localize('toolInvoke.addComment', "Adding comment"),
539 > pastTenseMessage: localize('toolComplete.addComment', "Added comment"),
540 > };
541 > case listCommentsToolName: { agentFeedbackServerTools.ts ×8
542 > let pastTenseMessage: StringOrMarkdown; agentFeedbackServerTools.ts ×2
543 > const count = result ? parseListedCommentCount(result.text) : undefined;
544 > if (count === undefined) {
545 > pastTenseMessage = localize('toolComplete.listComments', "Checked comments");
546 > } else if (count === 1) {
547 > pastTenseMessage = localize('toolComplete.listComments.one', "Checked 1 comment"); agentFeedbackServerTools.ts ×3
549 > pastTenseMessage = localize('toolComplete.listComments.many', "Checked {0} comments", count);
550 > }
552 > displayName: localize('toolName.listComments', "List Comments"),
553 > invocationMessage: localize('toolInvoke.listComments', "Checking comments"),
554 > pastTenseMessage,
555 > };
556 > }
557 > case deleteCommentsToolName: agentFeedbackServerTools.ts ×8
559 > displayName: localize('toolName.deleteComments', "Delete Comments"),
560 > invocationMessage: localize('toolInvoke.deleteComments', "Deleting comments"),
561 > pastTenseMessage: localize('toolComplete.deleteComments', "Deleted comments"),
562 > };
563 > case resolveCommentsToolName: agentFeedbackServerTools.ts ×8
565 > displayName: localize('toolName.resolveComments', "Resolve Comments"),
566 > invocationMessage: localize('toolInvoke.resolveComments', "Resolving comments"),
567 > pastTenseMessage: localize('toolComplete.resolveComments', "Resolved comments"),
568 > };
569 > case viewUnreviewedCommentsToolName: agentFeedbackServerTools.ts ×8
571 > displayName: localize('toolName.viewUnreviewedComments', "View Comments"),
572 > invocationMessage: localize('toolInvoke.viewUnreviewedComments', "Viewing comments"),
573 > pastTenseMessage: localize('toolComplete.viewUnreviewedComments', "Viewed comments"),
574 > };
576 return undefined;
578 > }
580 > /**
581 > * The feedback ("comments") server-tool group, contributed to the
582 > * {@link AgentServerToolHost} at startup (see `node/agentService.ts`). Wraps
583 > * the pure {@link applyFeedbackTool} executor with the annotations-channel I/O:
584 > * it reads the session's current {@link AnnotationsState}, applies the tool,
585 > * and dispatches the resulting annotation actions through the state manager
586 > * (the single writer).
587 > */
588 > export const feedbackServerToolGroup: IServerToolGroup = {
589 > definitions: feedbackServerToolDefinitions,
590 > requiresConfirmation(toolName): boolean {
591 > return feedbackToolRequiresConfirmation(toolName); agentFeedbackServerTools.ts ×1
592 > },
593 > getDisplay(toolName, args, result): IServerToolDisplay | undefined { agentFeedbackServerTools.ts ×23
594 > return getFeedbackToolDisplay(toolName, args, result); agentFeedbackServerTools.ts ×8
595 > },
596 > execute(stateManager, chatUri, toolName, rawArgs): string { agentFeedbackServerTools.ts ×23
597 > // A session can contain multiple chats, each addressed by its own reducer.ts ×7
598 > // `ahp-chat` URI but sharing the same context/workspace. Comments belong
599 > // to the session as a whole, so always resolve a chat URI back to its
600 > // owning session and operate on the main session's annotations channel.
601 > const mainSessionUri = parseChatUri(chatUri)?.session ?? chatUri;
602 > const annotationsUri = buildAnnotationsUri(mainSessionUri);
603 > const snapshot = stateManager.getSnapshot(annotationsUri);
604 > const state: AnnotationsState = (snapshot?.state as AnnotationsState | undefined) ?? { annotations: [] };
605 > const outcome = applyFeedbackTool(state, mainSessionUri, toolName, rawArgs);
606 > for (const action of outcome.actions) {
607 > stateManager.dispatchServerAction(annotationsUri, action);
608 > }
609 > return outcome.result;
610 > },