mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-06 23:06:20 +01:00
Render addComment feedback tool call as a rich reference in chat (#323853)
* Render addComment feedback tool call as a rich reference in chat Show the agent-host `addComment` tool call as a comment-icon reference with the tool name and a quoted 20-char preview of the comment body. Clicking it reveals the comment (agent feedback) in the editor via a new `_agentFeedbackReview.revealAt` command that resolves the owning session from the file resource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback - RevealAt: prefer getSessionForFile (falls back to active session for in-scope files) before getMostRecentSessionForResource, so the rendered reference works before the resource has accumulated feedback. - isOneBasedRange: enforce integer, >= 1 coordinates so invalid ranges fall back to the server-authored message. - Use import type for IMarkdownString in the test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix compile error: narrow out Streaming state before reading toolInput ToolCallState is a union whose Streaming member has no `toolInput` (it lives on ToolCallParameterFields). Guard addCommentReference on status !== Streaming so the property access type-checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix eslint no-duplicate-imports for range.js Merge the value import of Range and the type-only import of IRange into a single import statement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
155cf0a562
commit
07a63f4c0e
@@ -31,6 +31,15 @@ export const FEEDBACK_ANNOTATION_META_KEY = 'vscode.agentFeedback';
|
||||
*/
|
||||
export const VIEW_UNREVIEWED_COMMENTS_TOOL_NAME = 'viewUnreviewedComments';
|
||||
|
||||
/**
|
||||
* Name of the agent host server tool that adds a comment (agent feedback) to a
|
||||
* file range. Shared here (in the layer-neutral `common` module) so the
|
||||
* node-side server tool implementation and the browser-side chat adapter that
|
||||
* renders its tool call agree on the name without drifting. The agent sees this
|
||||
* name directly (Copilot) or prefixed as `mcp__host__<name>` (Claude).
|
||||
*/
|
||||
export const ADD_COMMENT_TOOL_NAME = 'addComment';
|
||||
|
||||
/**
|
||||
* Whether {@link toolName} (a tool name as seen on a tool call) refers to the
|
||||
* {@link VIEW_UNREVIEWED_COMMENTS_TOOL_NAME} server tool. Accepts both the bare
|
||||
@@ -40,6 +49,15 @@ export function isViewUnreviewedCommentsTool(toolName: string): boolean {
|
||||
return toolName === VIEW_UNREVIEWED_COMMENTS_TOOL_NAME || toolName.endsWith(`__${VIEW_UNREVIEWED_COMMENTS_TOOL_NAME}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@link toolName} (a tool name as seen on a tool call) refers to the
|
||||
* {@link ADD_COMMENT_TOOL_NAME} server tool. Accepts both the bare name and the
|
||||
* Claude `mcp__<server>__<name>` prefixed form.
|
||||
*/
|
||||
export function isAddCommentTool(toolName: string): boolean {
|
||||
return toolName === ADD_COMMENT_TOOL_NAME || toolName.endsWith(`__${ADD_COMMENT_TOOL_NAME}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin of a feedback item. String values match the client-side
|
||||
* `AgentFeedbackKind` enum so a value written by either side decodes on the
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { FEEDBACK_ANNOTATION_META_KEY, readFeedbackAnnotationMeta, VIEW_UNREVIEWED_COMMENTS_TOOL_NAME, type IFeedbackAnnotationMeta } from '../../common/meta/agentFeedbackAnnotations.js';
|
||||
import { FEEDBACK_ANNOTATION_META_KEY, readFeedbackAnnotationMeta, VIEW_UNREVIEWED_COMMENTS_TOOL_NAME, ADD_COMMENT_TOOL_NAME, type IFeedbackAnnotationMeta } from '../../common/meta/agentFeedbackAnnotations.js';
|
||||
import { buildAnnotationsUri } from '../../common/annotationsUri.js';
|
||||
import type { AnnotationsAction } from '../../common/state/sessionActions.js';
|
||||
import { ActionType } from '../../common/state/protocol/common/actions.js';
|
||||
@@ -27,7 +27,7 @@ import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } f
|
||||
* the actions) lives in the caller.
|
||||
*/
|
||||
|
||||
export const addCommentToolName = 'addComment';
|
||||
export const addCommentToolName = ADD_COMMENT_TOOL_NAME;
|
||||
export const listCommentsToolName = 'listComments';
|
||||
export const deleteCommentsToolName = 'deleteComments';
|
||||
export const resolveCommentsToolName = 'resolveComments';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI, UriComponents } from '../../../../base/common/uri.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { Range, type IRange } from '../../../../editor/common/core/range.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
|
||||
import { AgentFeedbackReviewCommandId, IChatAgentFeedbackReviewComment } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
|
||||
@@ -57,6 +59,31 @@ export function registerAgentFeedbackReviewCommands(): void {
|
||||
await feedbackService.revealFeedback(URI.revive(sessionResource), commentId);
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand(AgentFeedbackReviewCommandId.RevealAt, async (accessor, resourceUri: string, range: IRange): Promise<void> => {
|
||||
const feedbackService = accessor.get(IAgentFeedbackService);
|
||||
const resource = URI.parse(resourceUri);
|
||||
// A rendered `addComment` tool call links here without knowing the
|
||||
// session URI, so resolve the owning session from the file it commented
|
||||
// on. Prefer the session the file belongs to (which falls back to the
|
||||
// active session for in-scope files, so the "open file at range"
|
||||
// affordance works even before the resource has accumulated feedback);
|
||||
// fall back to the most recent session that has feedback for it.
|
||||
const sessionResource = feedbackService.getSessionForFile(resource)?.resource
|
||||
?? feedbackService.getMostRecentSessionForResource(resource);
|
||||
if (!sessionResource) {
|
||||
return;
|
||||
}
|
||||
// Prefer revealing via the matching feedback item so its editor widget
|
||||
// expands (the navigation anchor is set from the item id); fall back to
|
||||
// opening the file at the range when no item matches.
|
||||
const match = feedbackService.getFeedback(sessionResource).find(item => isEqual(item.resourceUri, resource) && Range.equalsRange(item.range, range));
|
||||
if (match) {
|
||||
await feedbackService.revealFeedback(sessionResource, match.id);
|
||||
} else {
|
||||
await feedbackService.revealSessionComment(sessionResource, '', resource, range);
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand(AgentFeedbackReviewCommandId.Delete, (accessor, sessionResource: UriComponents, commentId: string): void => {
|
||||
const feedbackService = accessor.get(IAgentFeedbackService);
|
||||
const codeReviewService = accessor.get(ICodeReviewService);
|
||||
|
||||
+109
-4
@@ -5,6 +5,7 @@
|
||||
|
||||
import { decodeBase64 } from '../../../../../../base/common/buffer.js';
|
||||
import { escapeMarkdownLinkLabel, IMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js';
|
||||
import { escapeIcons } from '../../../../../../base/common/iconLabels.js';
|
||||
import { marked, type Token, type Tokens, type TokensList } from '../../../../../../base/common/marked/marked.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../../../base/common/uuid.js';
|
||||
@@ -14,10 +15,10 @@ import { readToolCallMeta } from '../../../../../../platform/agentHost/common/me
|
||||
import { getChatErrorDetailsFromMeta, IChatErrorContext } from '../../../common/chatErrorMessages.js';
|
||||
import { AGENT_HOST_SCHEME, toAgentHostUri } from '../../../../../../platform/agentHost/common/agentHostUri.js';
|
||||
import { getAgentFeedbackAttachmentMetadata, isAgentFeedbackAnnotationsAttachment, isAgentFeedbackAttachment } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAttachments.js';
|
||||
import { isViewUnreviewedCommentsTool } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAnnotations.js';
|
||||
import { isViewUnreviewedCommentsTool, isAddCommentTool } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAnnotations.js';
|
||||
import { MessageAttachmentKind, type FileEdit, type MessageAttachment, type StringOrMarkdown, type TextRange } from '../../../../../../platform/agentHost/common/state/protocol/state.js';
|
||||
import { normalizeFileEdit } from '../../../../../../platform/agentHost/common/fileEditDiff.js';
|
||||
import { formatCopilotCredits, type ChatExternalEditKind, type ChatMcpAppData, type IChatAgentFeedbackReviewConfirmationData, type IChatExternalEdit, type IChatModifiedFilesConfirmationData, type IChatProgress, type IChatResponseErrorDetails, type IChatSearchToolInvocationData, type IChatTerminalToolInvocationData, type IChatToolInputInvocationData, type IChatToolInvocationSerialized, type IChatUsage, ToolConfirmKind } from '../../../common/chatService/chatService.js';
|
||||
import { formatCopilotCredits, type ChatExternalEditKind, type ChatMcpAppData, type IChatAgentFeedbackReviewConfirmationData, type IChatExternalEdit, type IChatModifiedFilesConfirmationData, type IChatProgress, type IChatResponseErrorDetails, type IChatSearchToolInvocationData, type IChatTerminalToolInvocationData, type IChatToolInputInvocationData, type IChatToolInvocationSerialized, type IChatUsage, ToolConfirmKind, AgentFeedbackReviewCommandId } from '../../../common/chatService/chatService.js';
|
||||
import { type IChatSessionHistoryItem } from '../../../common/chatSessionsService.js';
|
||||
import { type IQuotaSnapshot } from '../../../../../services/chat/common/chatEntitlementService.js';
|
||||
import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js';
|
||||
@@ -907,7 +908,7 @@ function getToolErrorString(tc: ToolCallState): string | undefined {
|
||||
export function completedToolCallToSerialized(tc: ICompletedToolCall, subAgentInvocationId: string | undefined, sessionResource: URI, connectionAuthority: string): IChatToolInvocationSerialized {
|
||||
const isTerminal = isTerminalToolCall(tc);
|
||||
const isSuccess = tc.status === ToolCallStatus.Completed && tc.success;
|
||||
const invocationMsg = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? localize('ahp.running', "Running {0}...", tc.displayName);
|
||||
let invocationMsg = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? localize('ahp.running', "Running {0}...", tc.displayName);
|
||||
|
||||
// Check for subagent content
|
||||
const subagentContent = tc.status === ToolCallStatus.Completed ? getToolSubagentContent(tc) : undefined;
|
||||
@@ -957,9 +958,18 @@ export function completedToolCallToSerialized(tc: ICompletedToolCall, subAgentIn
|
||||
}
|
||||
}
|
||||
|
||||
const pastTenseMsg = isSuccess
|
||||
let pastTenseMsg = isSuccess
|
||||
? stringOrMarkdownToString(tc.pastTenseMessage, connectionAuthority) ?? invocationMsg
|
||||
: invocationMsg;
|
||||
// Tools that render a bespoke, client-authored message override both the
|
||||
// invocation and past-tense text here. Add new per-tool cases alongside.
|
||||
if (isAddCommentTool(tc.toolName)) {
|
||||
const ref = addCommentReference(tc);
|
||||
if (ref) {
|
||||
invocationMsg = ref;
|
||||
pastTenseMsg = ref;
|
||||
}
|
||||
}
|
||||
const resultDetails = (!toolSpecificData || toolSpecificData.kind === 'input' && toolSpecificData.mcpAppData)
|
||||
&& (tc.status !== ToolCallStatus.Completed || getToolFileEdits(tc).length === 0)
|
||||
? getToolInputOutputDetails(tc, !isSuccess, getToolErrorString(tc), !!(toolSpecificData?.kind === 'input' && toolSpecificData.mcpAppData), connectionAuthority)
|
||||
@@ -1209,6 +1219,82 @@ export function stringOrMarkdownToString(value: StringOrMarkdown | undefined, co
|
||||
return rawMarkdownToString(value.markdown, connectionAuthority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of comment-body characters shown inline in the {@link addCommentReference}
|
||||
* pill before it is truncated with an ellipsis.
|
||||
*/
|
||||
const ADD_COMMENT_PREVIEW_LENGTH = 20;
|
||||
|
||||
/**
|
||||
* Builds the inline preview of an `addComment` comment body: whitespace is
|
||||
* collapsed to single spaces and the text is truncated to
|
||||
* {@link ADD_COMMENT_PREVIEW_LENGTH} characters with a trailing ellipsis.
|
||||
*/
|
||||
function addCommentPreview(text: string): string {
|
||||
const singleLine = text.replace(/\s+/g, ' ').trim();
|
||||
return singleLine.length > ADD_COMMENT_PREVIEW_LENGTH
|
||||
? `${singleLine.slice(0, ADD_COMMENT_PREVIEW_LENGTH)}…`
|
||||
: singleLine;
|
||||
}
|
||||
|
||||
/** Whether {@link value} is a positive 1-based line/column coordinate. */
|
||||
function isPositiveInteger(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@link value} is a valid 1-based editor range: every coordinate must
|
||||
* be an integer >= 1, since the range is later used for editor selection and
|
||||
* reveal. Invalid input is treated as unparseable so the UI falls back to the
|
||||
* server-authored message.
|
||||
*/
|
||||
function isOneBasedRange(value: unknown): value is IRange {
|
||||
const range = value as IRange | undefined;
|
||||
return !!range && typeof range === 'object'
|
||||
&& isPositiveInteger(range.startLineNumber)
|
||||
&& isPositiveInteger(range.startColumn)
|
||||
&& isPositiveInteger(range.endLineNumber)
|
||||
&& isPositiveInteger(range.endColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a rich, clickable reference for the agent host `addComment` feedback
|
||||
* tool call — a comment icon, the tool name, and the first
|
||||
* {@link ADD_COMMENT_PREVIEW_LENGTH} characters of the comment body in quotes.
|
||||
* Clicking it runs {@link AgentFeedbackReviewCommandId.RevealAt} to open the
|
||||
* file and reveal the comment (agent feedback) in the editor.
|
||||
*
|
||||
* Only call this for the `addComment` tool (gate call sites with
|
||||
* {@link isAddCommentTool}). Returns `undefined` when the arguments can't be
|
||||
* parsed, so the caller falls back to the server-authored message.
|
||||
*/
|
||||
function addCommentReference(tc: ToolCallState): IMarkdownString | undefined {
|
||||
// `toolInput` is absent while parameters are still streaming; every other
|
||||
// state carries it (see `ToolCallParameterFields`).
|
||||
if (tc.status === ToolCallStatus.Streaming || !tc.toolInput) {
|
||||
return undefined;
|
||||
}
|
||||
const toolInput = tc.toolInput;
|
||||
let args: { resourceUri?: unknown; range?: unknown; text?: unknown };
|
||||
try {
|
||||
args = JSON.parse(toolInput);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof args.resourceUri !== 'string' || typeof args.text !== 'string' || !isOneBasedRange(args.range)) {
|
||||
return undefined;
|
||||
}
|
||||
const preview = escapeIcons(escapeMarkdownLinkLabel(addCommentPreview(args.text)));
|
||||
// The command resolves the owning session from the file resource, so the
|
||||
// link only needs the resource and range (both known here).
|
||||
const commandArgs = encodeURIComponent(JSON.stringify([args.resourceUri, args.range]));
|
||||
const link = `command:${AgentFeedbackReviewCommandId.RevealAt}?${commandArgs}`;
|
||||
return new MarkdownString(`$(comment) [addComment "${preview}"](${link})`, {
|
||||
isTrusted: { enabledCommands: [AgentFeedbackReviewCommandId.RevealAt] },
|
||||
supportThemeIcons: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a live {@link ChatToolInvocation} from the protocol's tool-call
|
||||
* state. Used during active turns to represent running tool calls in the UI.
|
||||
@@ -1303,6 +1389,12 @@ export function toolCallStateToInvocation(tc: ToolCallState, subAgentInvocationI
|
||||
const invocation = new ChatToolInvocation(undefined, toolData, tc.toolCallId, subAgentInvocationId, undefined);
|
||||
invocation.invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? localize('ahp.running', "Running {0}...", tc.displayName);
|
||||
|
||||
// Tools that render a bespoke, client-authored invocation message override
|
||||
// the server text here. Add new per-tool cases alongside this branch.
|
||||
if (isAddCommentTool(tc.toolName)) {
|
||||
invocation.invocationMessage = addCommentReference(tc) ?? invocation.invocationMessage;
|
||||
}
|
||||
|
||||
if (isTerminalToolCall(tc)) {
|
||||
// Set terminal toolSpecificData eagerly so the renderer shows a
|
||||
// terminal pill (expandable command + output area) from the start,
|
||||
@@ -1347,6 +1439,9 @@ export function updateRunningToolSpecificData(existing: ChatToolInvocation, tc:
|
||||
return;
|
||||
}
|
||||
existing.invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? existing.invocationMessage;
|
||||
if (isAddCommentTool(tc.toolName)) {
|
||||
existing.invocationMessage = addCommentReference(tc) ?? existing.invocationMessage;
|
||||
}
|
||||
|
||||
|
||||
const subagentContent = getToolSubagentContent(tc);
|
||||
@@ -1434,6 +1529,11 @@ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: ToolC
|
||||
if ((isCompleted || isCancelled) && hasKey(tc, { invocationMessage: true })) {
|
||||
invocation.invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? invocation.invocationMessage;
|
||||
}
|
||||
// Tools that render a bespoke, client-authored message override the
|
||||
// invocation text here. Add new per-tool cases alongside this branch.
|
||||
if (isAddCommentTool(tc.toolName)) {
|
||||
invocation.invocationMessage = addCommentReference(tc) ?? invocation.invocationMessage;
|
||||
}
|
||||
|
||||
// Check for subagent content — set toolSpecificData so the UI renders a subagent widget
|
||||
if (isCompleted) {
|
||||
@@ -1474,6 +1574,11 @@ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: ToolC
|
||||
} else if (isCompleted && tc.pastTenseMessage) {
|
||||
invocation.pastTenseMessage = stringOrMarkdownToString(tc.pastTenseMessage, connectionAuthority);
|
||||
}
|
||||
// Tools that render a bespoke, client-authored message override the
|
||||
// past-tense text here. Add new per-tool cases alongside this branch.
|
||||
if (isCompleted && isAddCommentTool(tc.toolName)) {
|
||||
invocation.pastTenseMessage = addCommentReference(tc) ?? invocation.pastTenseMessage;
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
const mcpAppData = getMcpAppData(tc, backendSession);
|
||||
|
||||
@@ -1154,14 +1154,18 @@ export interface IChatAgentFeedbackReviewComment {
|
||||
* Command ids the agent feedback review confirmation renderer (workbench/chat)
|
||||
* uses to fetch unreviewed comments and apply the user's selection. They are
|
||||
* implemented by the agent feedback feature in `vs/sessions`, keeping the chat
|
||||
* layer decoupled from the feedback model. All take the owning session resource
|
||||
* (`UriComponents`) as their first argument.
|
||||
* layer decoupled from the feedback model. Most take the owning session resource
|
||||
* (`UriComponents`) as their first argument; {@link AgentFeedbackReviewCommandId.RevealAt}
|
||||
* instead resolves the session from the file resource so a rendered tool call
|
||||
* can link to a comment without knowing the session URI.
|
||||
*/
|
||||
export const enum AgentFeedbackReviewCommandId {
|
||||
/** `(sessionResource)` -> `IChatAgentFeedbackReviewComment[]` (the `created` reviewable comments). */
|
||||
GetComments = '_agentFeedbackReview.getComments',
|
||||
/** `(sessionResource, commentId)` -> opens the file and reveals the comment. */
|
||||
Reveal = '_agentFeedbackReview.reveal',
|
||||
/** `(resourceUri, range)` -> resolves the owning session and reveals the comment at that file range. */
|
||||
RevealAt = '_agentFeedbackReview.revealAt',
|
||||
/** `(sessionResource, commentId)` -> deletes the comment entirely. */
|
||||
Delete = '_agentFeedbackReview.delete',
|
||||
/** `(sessionResource, commentIds)` -> accepts (reveals) the given comments. */
|
||||
|
||||
+64
@@ -6,6 +6,7 @@
|
||||
import assert from 'assert';
|
||||
import { autorun } from '../../../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { MessageKind, ToolCallStatus, ToolCallConfirmationReason, ToolResultContentType, TurnState, ResponsePartKind, type ActiveTurn, type ICompletedToolCall, type ToolCallRunningState, type Turn, type ToolCallResponsePart, ToolCallCancellationReason, type Message } from '../../../../../../platform/agentHost/common/state/sessionState.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized, type IChatMarkdownContent, type IChatProgressMessage, type IChatThinkingPart, type IChatUsage } from '../../../common/chatService/chatService.js';
|
||||
@@ -797,6 +798,69 @@ suite('stateToProgressAdapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
suite('addComment reference', () => {
|
||||
|
||||
const commentRange = { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 5 };
|
||||
|
||||
function addCommentInput(text: string): string {
|
||||
return JSON.stringify({ resourceUri: 'file:///workspace/a.ts', range: commentRange, text });
|
||||
}
|
||||
|
||||
function markdown(message: string | IMarkdownString | undefined): IMarkdownString {
|
||||
assert.ok(message && typeof message !== 'string', 'expected a markdown reference');
|
||||
return message;
|
||||
}
|
||||
|
||||
test('renders comment icon, tool name, truncated quoted preview and a reveal command link', () => {
|
||||
const tc = createToolCallState({ toolName: 'addComment', invocationMessage: 'Adding comment', toolInput: addCommentInput('This comment is quite long and should be truncated') });
|
||||
const message = markdown(toolCallStateToInvocation(tc).invocationMessage);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
value: message.value,
|
||||
supportThemeIcons: message.supportThemeIcons,
|
||||
isTrusted: message.isTrusted,
|
||||
},
|
||||
{
|
||||
value: `$(comment) [addComment "This comment is quit…"](command:_agentFeedbackReview.revealAt?${encodeURIComponent(JSON.stringify(['file:///workspace/a.ts', commentRange]))})`,
|
||||
supportThemeIcons: true,
|
||||
isTrusted: { enabledCommands: ['_agentFeedbackReview.revealAt'] },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('does not truncate a short comment', () => {
|
||||
const tc = createToolCallState({ toolName: 'addComment', invocationMessage: 'Adding comment', toolInput: addCommentInput('Short note') });
|
||||
const message = markdown(toolCallStateToInvocation(tc).invocationMessage);
|
||||
assert.ok(message.value.includes('addComment "Short note"'), message.value);
|
||||
assert.ok(!message.value.includes('…'), message.value);
|
||||
});
|
||||
|
||||
test('sets the same reference as the past-tense message on completion', () => {
|
||||
const running = createToolCallState({ toolName: 'addComment', invocationMessage: 'Adding comment', toolInput: addCommentInput('Short note') });
|
||||
const invocation = toolCallStateToInvocation(running);
|
||||
const completed = createCompletedToolCall({ toolName: 'addComment', toolInput: addCommentInput('Short note'), pastTenseMessage: 'Added comment' });
|
||||
finalizeToolInvocation(invocation, completed);
|
||||
assert.strictEqual(markdown(invocation.pastTenseMessage).value, markdown(invocation.invocationMessage).value);
|
||||
});
|
||||
|
||||
test('falls back to the server message when the input cannot be parsed', () => {
|
||||
const tc = createToolCallState({ toolName: 'addComment', invocationMessage: 'Adding comment', toolInput: 'not json' });
|
||||
assert.strictEqual(toolCallStateToInvocation(tc).invocationMessage, 'Adding comment');
|
||||
});
|
||||
|
||||
test('falls back to the server message when the range is not a valid 1-based range', () => {
|
||||
for (const range of [
|
||||
{ startLineNumber: 0, startColumn: 1, endLineNumber: 1, endColumn: 1 },
|
||||
{ startLineNumber: 1, startColumn: 1.5, endLineNumber: 1, endColumn: 2 },
|
||||
{ startLineNumber: -1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
|
||||
]) {
|
||||
const tc = createToolCallState({ toolName: 'addComment', invocationMessage: 'Adding comment', toolInput: JSON.stringify({ resourceUri: 'file:///workspace/a.ts', range, text: 'hi' }) });
|
||||
assert.strictEqual(toolCallStateToInvocation(tc).invocationMessage, 'Adding comment', JSON.stringify(range));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('finalizeToolInvocation', () => {
|
||||
|
||||
test('rewrites markdown links in pastTenseMessage through the agent host scheme', () => {
|
||||
|
||||
Reference in New Issue
Block a user