feat: replies to comments
A comment can answer another one (replyTo), one level deep: answering a reply joins the same thread, and it must be on the same ticket. Whoever wrote the comment gets a new REPLIED notification, and the activity says "respondió a un comentario". A comment deleted while it has replies stays, without its text (deleted: true), so the thread still reads; it goes with its last reply and does not count in commentCount. The ticket page shows threads with their replies indented and a "Responder" box; REST, GraphQL and the MCP (add_comment, get_issue) take and show replyTo. Existing comments are unchanged. Docs, Word and PDF updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4406,7 +4406,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CommentBodyDto"
|
||||
"$ref": "#/components/schemas/AddCommentDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4491,7 +4491,7 @@
|
||||
"pireon": []
|
||||
}
|
||||
],
|
||||
"summary": "Comment on the ticket",
|
||||
"summary": "Comment on the ticket, or answer a comment (replyTo)",
|
||||
"tags": [
|
||||
"Issues"
|
||||
]
|
||||
@@ -11423,7 +11423,18 @@
|
||||
"$ref": "#/components/schemas/UserRefDto"
|
||||
},
|
||||
"body": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Empty on a deleted comment kept for its replies"
|
||||
},
|
||||
"replyTo": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"format": "uuid",
|
||||
"description": "The top-level comment it answers; null for a top-level one"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
"description": "Deleted, but kept (without its text) while it has replies"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
@@ -11443,6 +11454,8 @@
|
||||
"issueId",
|
||||
"author",
|
||||
"body",
|
||||
"replyTo",
|
||||
"deleted",
|
||||
"createdAt",
|
||||
"editedAt",
|
||||
"version"
|
||||
@@ -11466,13 +11479,18 @@
|
||||
"total"
|
||||
]
|
||||
},
|
||||
"CommentBodyDto": {
|
||||
"AddCommentDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string",
|
||||
"example": "Reproducido en Safari 17.",
|
||||
"maxLength": 20000
|
||||
},
|
||||
"replyTo": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Comment it answers (one level: answering a reply joins its thread)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11497,6 +11515,19 @@
|
||||
"file"
|
||||
]
|
||||
},
|
||||
"CommentBodyDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string",
|
||||
"example": "Reproducido en Safari 17.",
|
||||
"maxLength": 20000
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"body"
|
||||
]
|
||||
},
|
||||
"ReleaseStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -11929,6 +11960,7 @@
|
||||
"QA_FAILED",
|
||||
"COMMENTED",
|
||||
"MENTIONED",
|
||||
"REPLIED",
|
||||
"WATCHED",
|
||||
"ADDED_TO_PROJECT",
|
||||
"REMINDER"
|
||||
|
||||
@@ -196,11 +196,19 @@ type CommandResult {
|
||||
|
||||
type Comment {
|
||||
author: UserRef!
|
||||
|
||||
"""Empty on a deleted comment kept for its replies"""
|
||||
body: String!
|
||||
createdAt: String!
|
||||
|
||||
"""Deleted, but kept (without its text) while it has replies"""
|
||||
deleted: Boolean!
|
||||
editedAt: String
|
||||
id: ID!
|
||||
issueId: ID!
|
||||
|
||||
"""The top-level comment it answers; null for a top-level one"""
|
||||
replyTo: ID
|
||||
version: Int!
|
||||
}
|
||||
|
||||
@@ -602,7 +610,11 @@ type Me {
|
||||
type Mutation {
|
||||
"""Add a step to a ticket's checklist"""
|
||||
addChecklistItem(input: AddChecklistItemInput!): CommandResult!
|
||||
addComment(body: String!, issueId: ID!): CommandResult!
|
||||
|
||||
"""
|
||||
Comment on a ticket, or answer a comment (replyTo; one level of replies)
|
||||
"""
|
||||
addComment(body: String!, issueId: ID!, replyTo: ID): CommandResult!
|
||||
|
||||
"""
|
||||
Add a repository with its own secret (the first turns the integration on)
|
||||
@@ -776,6 +788,7 @@ enum NotificationKind {
|
||||
MENTIONED
|
||||
QA_FAILED
|
||||
REMINDER
|
||||
REPLIED
|
||||
STATUS_CHANGED
|
||||
WATCHED
|
||||
}
|
||||
|
||||
@@ -635,7 +635,7 @@ export interface paths {
|
||||
/** Comments of the ticket (oldest first) */
|
||||
get: operations["IssuesController_listComments"];
|
||||
put?: never;
|
||||
/** Comment on the ticket */
|
||||
/** Comment on the ticket, or answer a comment (replyTo) */
|
||||
post: operations["IssuesController_addComment"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -2087,7 +2087,15 @@ export interface components {
|
||||
/** Format: uuid */
|
||||
issueId: string;
|
||||
author: components["schemas"]["UserRefDto"];
|
||||
/** @description Empty on a deleted comment kept for its replies */
|
||||
body: string;
|
||||
/**
|
||||
* Format: uuid
|
||||
* @description The top-level comment it answers; null for a top-level one
|
||||
*/
|
||||
replyTo: string | null;
|
||||
/** @description Deleted, but kept (without its text) while it has replies */
|
||||
deleted: boolean;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** Format: date-time */
|
||||
@@ -2098,9 +2106,14 @@ export interface components {
|
||||
items: components["schemas"]["CommentDto"][];
|
||||
total: number;
|
||||
};
|
||||
CommentBodyDto: {
|
||||
AddCommentDto: {
|
||||
/** @example Reproducido en Safari 17. */
|
||||
body: string;
|
||||
/**
|
||||
* Format: uuid
|
||||
* @description Comment it answers (one level: answering a reply joins its thread)
|
||||
*/
|
||||
replyTo?: string;
|
||||
};
|
||||
AttachFileDto: {
|
||||
/**
|
||||
@@ -2114,6 +2127,10 @@ export interface components {
|
||||
*/
|
||||
commentId?: string;
|
||||
};
|
||||
CommentBodyDto: {
|
||||
/** @example Reproducido en Safari 17. */
|
||||
body: string;
|
||||
};
|
||||
/** @enum {string} */
|
||||
ReleaseStatus: "PLANNED" | "RELEASED" | "ARCHIVED";
|
||||
ReleaseDto: {
|
||||
@@ -2250,7 +2267,7 @@ export interface components {
|
||||
goodFirst: components["schemas"]["ShowcaseIssueDto"][];
|
||||
};
|
||||
/** @enum {string} */
|
||||
NotificationKind: "ASSIGNED" | "STATUS_CHANGED" | "QA_FAILED" | "COMMENTED" | "MENTIONED" | "WATCHED" | "ADDED_TO_PROJECT" | "REMINDER";
|
||||
NotificationKind: "ASSIGNED" | "STATUS_CHANGED" | "QA_FAILED" | "COMMENTED" | "MENTIONED" | "REPLIED" | "WATCHED" | "ADDED_TO_PROJECT" | "REMINDER";
|
||||
NotificationDto: {
|
||||
id: string;
|
||||
kind: components["schemas"]["NotificationKind"];
|
||||
@@ -5497,7 +5514,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CommentBodyDto"];
|
||||
"application/json": components["schemas"]["AddCommentDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
||||
@@ -224,9 +224,10 @@ export class IssuesResolver {
|
||||
)
|
||||
).items.map((c) => ({
|
||||
id: c.id,
|
||||
author: c.author.name,
|
||||
replyTo: c.replyTo,
|
||||
author: c.deleted ? null : c.author.name,
|
||||
at: c.createdAt,
|
||||
body: c.body,
|
||||
body: c.deleted ? '(deleted; kept for its replies)' : c.body,
|
||||
}))
|
||||
: undefined;
|
||||
const history = params.withHistory
|
||||
@@ -411,19 +412,27 @@ export class IssuesResolver {
|
||||
|
||||
@Tool({
|
||||
name: 'add_comment',
|
||||
description: 'Comment on a ticket (decisions, explanations, steps).',
|
||||
description:
|
||||
'Comment on a ticket (decisions, explanations, steps), or answer a comment with replyTo (one level: answering a reply joins its thread).',
|
||||
paramsSchema: z.object({
|
||||
issue: issueRef,
|
||||
body: z.string().min(1).max(20000),
|
||||
replyTo: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Id of the comment it answers (see get_issue)'),
|
||||
}),
|
||||
annotations: WRITE,
|
||||
})
|
||||
comment({ issue, body }: { issue: string; body: string }, ctx: McpContext) {
|
||||
comment(
|
||||
{ issue, body, replyTo }: { issue: string; body: string; replyTo?: string },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
return toolResult(() =>
|
||||
this.api.data(
|
||||
this.api.as(ctx).POST('/api/issues/{issue}/comments', {
|
||||
params: { path: { issue } },
|
||||
body: { body },
|
||||
body: { body, replyTo },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user