feat: what is new since my last visit, and turning a comment into a ticket

Opening a ticket notes the visit and answers the one before (issue_visits),
so the page marks others' comments and changes since. A comment can become
a ticket of its own, as a subtask or linked; it remembers where it went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:05:44 +02:00
parent 429b52e6f5
commit 285bd5c454
50 changed files with 1953 additions and 79 deletions

View File

@@ -632,6 +632,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/issues/{issue}/visit": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* I opened this ticket now
* @description Answers when I had opened it before (null: never), to show what others did since. Private to me.
*/
put: operations["IssuesController_visit"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}/follow-tree": {
parameters: {
query?: never;
@@ -1241,6 +1261,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/comments/{commentId}/issue": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Make a ticket of a comment
* @description Its text becomes the description, as a subtask of its ticket or linked to it (RELATES_TO); the comment shows where it went. Answers the new ticket. Whoever can change tickets.
*/
post: operations["CommentsController_turnIntoIssue"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/comments/{commentId}/resolution": {
parameters: {
query?: never;
@@ -3181,6 +3221,13 @@ export interface components {
verdict: components["schemas"]["ReviewVerdict"];
answer?: string;
};
IssueVisitDto: {
/**
* Format: date-time
* @description My visit before this one (null: first time)
*/
previousVisit: string | null;
};
IssuePageDto: {
items: components["schemas"]["IssueDto"][];
/** @description Total matches (for pagination) */
@@ -3377,6 +3424,11 @@ export interface components {
/** @description Index of the winning option once closed (null on a tie) */
winner: number | null;
};
CommentSpawnDto: {
id: string;
/** @example WEB-45 */
key: string;
};
CommentDto: {
/** Format: uuid */
id: string;
@@ -3407,6 +3459,8 @@ export interface components {
resolvedBy: components["schemas"]["UserRefDto"] | null;
/** Format: date-time */
resolvedAt: string | null;
/** @description Tickets made from it («Convertir en ticket»), oldest first */
spawned: components["schemas"]["CommentSpawnDto"][];
/** Format: date-time */
createdAt: string;
/** Format: date-time */
@@ -3490,6 +3544,17 @@ export interface components {
*/
decide: boolean;
};
TurnCommentIntoIssueDto: {
/** @example También falla en Android */
title: string;
/** @description One of the project's ticket type ids */
type: string;
/**
* @description Hang it from the ticket of the comment; otherwise it is linked to it (RELATES_TO)
* @default false
*/
asSubtask: boolean;
};
ResolveThreadDto: {
/** @description What was settled: added as a reply marked as a decision before it closes */
conclusion?: string;
@@ -6732,6 +6797,82 @@ export interface operations {
};
};
};
IssuesController_visit: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["IssueVisitDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_followTree: {
parameters: {
query?: never;
@@ -9935,6 +10076,85 @@ export interface operations {
};
};
};
CommentsController_turnIntoIssue: {
parameters: {
query?: never;
header?: never;
path: {
commentId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TurnCommentIntoIssueDto"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
CommentsController_resolveThread: {
parameters: {
query?: never;

View File

@@ -599,6 +599,37 @@ export class IssuesResolver {
);
}
@Tool({
name: 'turn_comment_into_issue',
description:
"Make a ticket of a comment that turned out to be work of its own: its text becomes the description, as a subtask of the comment's ticket (asSubtask) or linked to it. Read type ids with get_project. Answers the new ticket's key.",
paramsSchema: z.object({
commentId: z.string().min(1),
title: z.string().min(1).max(200),
type: z.string().min(1).describe("One of the project's ticket type ids"),
asSubtask: z.boolean().default(false),
}),
annotations: WRITE,
})
turnCommentIntoIssue(
{
commentId,
title,
type,
asSubtask,
}: { commentId: string; title: string; type: string; asSubtask: boolean },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).POST('/api/comments/{commentId}/issue', {
params: { path: { commentId } },
body: { title, type, asSubtask },
}),
),
);
}
@Tool({
name: 'link_issues',
description: