feat: comments marked as decisions, on top of the ticket and searchable
Whoever can change the ticket flags a comment as a decision (new event
CommentDecisionMarkedEvent on the comment). The ticket page lists its
decisions above the description, with a link to the comment in the
conversation (`?comment=<id>` scrolls to it and points at it), and the
search palette finds them by their text across my projects.
API: markDecision / PUT-DELETE /api/comments/{id}/decision; Issue.decisions
and GET /api/issues/{issue}/decisions; decisions(projectId?, search) and
GET /api/projects/{project}/decisions, GET /api/me/decisions. MCP:
mark_decision, search_decisions, and get_issue includes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,7 +254,7 @@ export class IssuesResolver {
|
||||
@Tool({
|
||||
name: 'get_issue',
|
||||
description:
|
||||
'A ticket in full: description, parent, checklist (with item ids), links, commits and pull requests, followers, allowed next statuses, and optionally its comments and history.',
|
||||
'A ticket in full: description, parent, checklist (with item ids), links, commits and pull requests, followers, allowed next statuses, the comments marked as decisions, and optionally its comments and history.',
|
||||
paramsSchema: z.object({
|
||||
issue: issueRef,
|
||||
withComments: z.boolean().default(true),
|
||||
@@ -289,8 +289,20 @@ export class IssuesResolver {
|
||||
emoji: r.emoji,
|
||||
by: r.users.map((u) => u.name),
|
||||
})),
|
||||
decision: c.decision || undefined,
|
||||
}))
|
||||
: undefined;
|
||||
const decisions = (
|
||||
await this.api.data(
|
||||
client.GET('/api/issues/{issue}/decisions', { params: { path } }),
|
||||
)
|
||||
).map((c) => ({
|
||||
commentId: c.id,
|
||||
by: c.author.name,
|
||||
at: c.createdAt,
|
||||
markedBy: c.decidedBy?.name ?? null,
|
||||
body: c.body,
|
||||
}));
|
||||
const history = params.withHistory
|
||||
? (
|
||||
await this.api.data(
|
||||
@@ -322,6 +334,7 @@ export class IssuesResolver {
|
||||
attachments: issue.attachments.length,
|
||||
trashedAt: issue.trashedAt,
|
||||
createdAt: issue.createdAt,
|
||||
decisions,
|
||||
comments,
|
||||
history,
|
||||
};
|
||||
@@ -734,6 +747,67 @@ export class IssuesResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@Tool({
|
||||
name: 'mark_decision',
|
||||
description:
|
||||
'Mark a comment as a decision of its ticket (what was agreed), or unmark it (on: false). Whoever can change the ticket. Comment ids come with get_issue.',
|
||||
paramsSchema: z.object({
|
||||
commentId: z.string().min(1),
|
||||
on: z.boolean().default(true),
|
||||
}),
|
||||
annotations: { ...WRITE, idempotentHint: true },
|
||||
})
|
||||
markDecision(
|
||||
{ commentId, on }: { commentId: string; on: boolean },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
const client = this.api.as(ctx);
|
||||
const options = { params: { path: { commentId } } };
|
||||
return toolResult(() =>
|
||||
this.api.data(
|
||||
on
|
||||
? client.PUT('/api/comments/{commentId}/decision', options)
|
||||
: client.DELETE('/api/comments/{commentId}/decision', options),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Tool({
|
||||
name: 'search_decisions',
|
||||
description:
|
||||
'What was decided, by words in the decision: comments marked as decisions, most recent first, with their ticket. Of one project, or of all my projects without one.',
|
||||
paramsSchema: z.object({
|
||||
project: projectRef.optional(),
|
||||
search: z.string().max(200).optional(),
|
||||
limit: limit(100, 20),
|
||||
}),
|
||||
annotations: READ,
|
||||
})
|
||||
searchDecisions(
|
||||
params: { project?: string; search?: string; limit: number },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
return toolResult(async () => {
|
||||
const client = this.api.as(ctx);
|
||||
const query = { search: params.search, limit: params.limit };
|
||||
const found = await this.api.data(
|
||||
params.project
|
||||
? client.GET('/api/projects/{project}/decisions', {
|
||||
params: { path: { project: params.project }, query },
|
||||
})
|
||||
: client.GET('/api/me/decisions', { params: { query } }),
|
||||
);
|
||||
return found.map((d) => ({
|
||||
issue: d.issue.key,
|
||||
title: d.issue.title,
|
||||
commentId: d.comment.id,
|
||||
by: d.comment.author.name,
|
||||
at: d.comment.createdAt,
|
||||
body: d.comment.body,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Tool({
|
||||
name: 'vote_issue',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user