feat: conversations on a ticket are settled, and you see who is typing
A top-level comment and its replies can be resolved by whoever can
change the ticket (CommentThreadResolvedEvent), with an optional
conclusion added as a reply marked as a decision, so it shows above the
ticket. A settled conversation folds to one line (topic, who settled it
and the conclusion) and can be unfolded or reopened; the header counts
the open ones, and finishing the ticket with one open asks first.
«Ana está escribiendo…» shows in the conversation being answered, at
once: a `typing` mutation publishes to a Redis channel per ticket
(`typing` subscription), never stored. In replies, Enter sends and
Shift+Enter breaks the line, except inside lists and code.
API: resolveThread / PUT-DELETE /api/comments/{id}/resolution, Comment
.resolved/resolvedBy/resolvedAt, typing mutation and subscription. MCP:
resolve_thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7113,6 +7113,213 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/comments/{commentId}/resolution": {
|
||||
"put": {
|
||||
"description": "Whoever can change the ticket. A conclusion is added as a reply marked as a decision. The web folds a settled conversation.",
|
||||
"operationId": "CommentsController_resolveThread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "commentId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResolveThreadDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CommandResultDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or rejected Pireon token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Not a member of the project (or not allowed)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Someone changed it at the same time: retry",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Business rule (e.g. a transition the workflow forbids)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"pireon-oauth": []
|
||||
},
|
||||
{
|
||||
"pireon": []
|
||||
}
|
||||
],
|
||||
"summary": "Settle a conversation (a top-level comment and its replies)",
|
||||
"tags": [
|
||||
"Comments"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "CommentsController_reopenThread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "commentId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CommandResultDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or rejected Pireon token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Not a member of the project (or not allowed)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Someone changed it at the same time: retry",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Business rule (e.g. a transition the workflow forbids)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiErrorDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"pireon-oauth": []
|
||||
},
|
||||
{
|
||||
"pireon": []
|
||||
}
|
||||
],
|
||||
"summary": "Reopen a conversation",
|
||||
"tags": [
|
||||
"Comments"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/me/releases": {
|
||||
"get": {
|
||||
"operationId": "ReleasesController_upcoming",
|
||||
@@ -16162,6 +16369,25 @@
|
||||
"nullable": true,
|
||||
"format": "date-time"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "boolean",
|
||||
"description": "A conversation (top-level comment and its replies) that was settled"
|
||||
},
|
||||
"resolvedBy": {
|
||||
"nullable": true,
|
||||
"description": "Who settled it",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserRefDto"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resolvedAt": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"format": "date-time"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
@@ -16186,6 +16412,9 @@
|
||||
"decision",
|
||||
"decidedBy",
|
||||
"decidedAt",
|
||||
"resolved",
|
||||
"resolvedBy",
|
||||
"resolvedAt",
|
||||
"createdAt",
|
||||
"editedAt",
|
||||
"version"
|
||||
@@ -16328,6 +16557,16 @@
|
||||
"on"
|
||||
]
|
||||
},
|
||||
"ResolveThreadDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conclusion": {
|
||||
"type": "string",
|
||||
"description": "What was settled: added as a reply marked as a decision before it closes",
|
||||
"maxLength": 20000
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReleaseStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -310,6 +310,13 @@ type Comment {
|
||||
|
||||
"""The top-level comment it answers; null for a top-level one"""
|
||||
replyTo: ID
|
||||
|
||||
"""A conversation (top-level comment and its replies) that was settled"""
|
||||
resolved: Boolean!
|
||||
resolvedAt: String
|
||||
|
||||
"""Who settled it"""
|
||||
resolvedBy: UserRef
|
||||
version: Int!
|
||||
}
|
||||
|
||||
@@ -1049,6 +1056,11 @@ type Mutation {
|
||||
removeGitRepository(projectId: ID!, repositoryId: ID!): Boolean!
|
||||
removeProjectMember(projectId: ID!, userId: ID!): CommandResult!
|
||||
|
||||
"""
|
||||
Settle a conversation (a top-level comment and its replies) or reopen it (resolved: false). A conclusion is added as a reply marked as a decision. Whoever can change the ticket
|
||||
"""
|
||||
resolveThread(commentId: ID!, conclusion: String, resolved: Boolean! = true): CommandResult!
|
||||
|
||||
"""Back from the trash"""
|
||||
restoreIssue(issueId: ID!): CommandResult!
|
||||
|
||||
@@ -1105,6 +1117,16 @@ type Mutation {
|
||||
"""
|
||||
trashIssue(issueId: ID!): CommandResult!
|
||||
|
||||
"""
|
||||
I am writing on this ticket (send every few seconds while typing): those with it open see it at once via `typing`
|
||||
"""
|
||||
typing(
|
||||
issueId: ID!
|
||||
|
||||
"""The conversation being answered; null: a new comment"""
|
||||
threadId: ID
|
||||
): Boolean!
|
||||
|
||||
"""Bring an archived project back (owner only)"""
|
||||
unarchiveProject(projectId: ID!): CommandResult!
|
||||
unlinkIssues(input: LinkIssuesInput!): CommandResult!
|
||||
@@ -1988,6 +2010,11 @@ type Subscription {
|
||||
projectChanged(projectId: ID!): ProjectChange!
|
||||
releaseChanged(projectId: ID!): ReleaseChange!
|
||||
sprintChanged(projectId: ID!): SprintChange!
|
||||
|
||||
"""
|
||||
Who is writing on a ticket, as they type (one's own come too: leave them out)
|
||||
"""
|
||||
typing(issueId: ID!): Typing!
|
||||
}
|
||||
|
||||
type Suggestion {
|
||||
@@ -2038,6 +2065,16 @@ type Throughput {
|
||||
week: String!
|
||||
}
|
||||
|
||||
"""Someone writing on a ticket right now (not stored)"""
|
||||
type Typing {
|
||||
at: String!
|
||||
issueId: ID!
|
||||
|
||||
"""The conversation being answered; null: a new comment"""
|
||||
threadId: ID
|
||||
user: UserRef!
|
||||
}
|
||||
|
||||
input UpdateChecklistItemInput {
|
||||
done: Boolean
|
||||
issueId: ID!
|
||||
|
||||
Reference in New Issue
Block a user