Compare commits

...

4 Commits

Author SHA1 Message Date
Lambda
7cd6229263 MUL-3328: clean migration whitespace
Co-authored-by: multica-agent <github@multica.ai>
2026-06-17 15:25:11 +08:00
Lambda
dea2b89fde MUL-3328: restrict retry affordance to failures
Co-authored-by: multica-agent <github@multica.ai>
2026-06-17 14:38:52 +08:00
Lambda
e582728b98 MUL-3328: cover child-done system comments
Co-authored-by: multica-agent <github@multica.ai>
2026-06-17 13:32:01 +08:00
Lambda
9b306b4535 MUL-3328: add retry button for failed agent comments
Co-authored-by: multica-agent <github@multica.ai>
2026-06-17 03:19:33 +08:00
17 changed files with 254 additions and 41 deletions

View File

@@ -366,5 +366,6 @@ export function commentToTimelineEntry(comment: Comment): TimelineEntry {
resolved_at: comment.resolved_at,
resolved_by_type: comment.resolved_by_type,
resolved_by_id: comment.resolved_by_id,
source_task_id: comment.source_task_id,
};
}

View File

@@ -89,6 +89,7 @@ export const CommentSchema = z.object({
resolved_at: z.string().nullable().default(null),
resolved_by_type: z.string().nullable().default(null),
resolved_by_id: z.string().nullable().default(null),
source_task_id: z.string().nullable().optional(),
}).loose() as unknown as z.ZodType<Comment>;
export const EMPTY_COMMENT: Comment = {

View File

@@ -13,6 +13,7 @@ import {
RuntimeUsageListSchema,
SquadListSchema,
SquadSchema,
TimelineEntriesSchema,
UserSchema,
} from "./schemas";
import { parseWithFallback } from "./schema";
@@ -75,6 +76,25 @@ describe("IssueSchema (via ListIssuesResponseSchema)", () => {
});
});
describe("TimelineEntriesSchema", () => {
it("preserves source_task_id for agent failure comments", () => {
const parsed = TimelineEntriesSchema.parse([
{
type: "comment",
id: "comment-1",
actor_type: "agent",
actor_id: "agent-1",
created_at: "2026-01-01T00:00:00Z",
content: "API Error: 500 Internal server error",
comment_type: "system",
source_task_id: "task-1",
},
]);
expect(parsed[0]?.source_task_id).toBe("task-1");
});
});
// The duplicate-issue branch in create-issue.tsx feeds ApiError.body
// (typed as `unknown`) through this schema. Any future server drift that
// loses the contract MUST fail the parse so the UI falls back to a normal

View File

@@ -146,6 +146,7 @@ const TimelineEntrySchema = z.object({
comment_type: z.string().optional(),
reactions: z.array(ReactionSchema).optional(),
attachments: z.array(AttachmentSchema).optional(),
source_task_id: z.string().nullable().optional(),
coalesced_count: z.number().optional(),
}).loose();
@@ -202,6 +203,7 @@ export const CommentSchema = z.object({
attachments: z.array(AttachmentSchema).default([]),
created_at: z.string(),
updated_at: z.string(),
source_task_id: z.string().nullable().optional(),
}).loose();
export const CommentsListSchema = z.array(CommentSchema);

View File

@@ -26,7 +26,7 @@ export interface TimelineEntry {
resolved_at?: string | null;
resolved_by_type?: CommentAuthorType | null;
resolved_by_id?: string | null;
source_task_id?: string | null;
/** Set by frontend coalescing when consecutive identical activities are merged. */
coalesced_count?: number;
}

View File

@@ -29,6 +29,7 @@ export interface Comment {
resolved_at: string | null;
resolved_by_type: CommentAuthorType | null;
resolved_by_id: string | null;
source_task_id?: string | null;
}
export type CommentTriggerSource =

View File

@@ -1,7 +1,7 @@
"use client";
import { memo, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { CheckCircle2, ChevronRight, ListChevronsDownUp, Copy, MoreHorizontal, Pencil, RotateCcw, Trash2 } from "lucide-react";
import { CheckCircle2, ChevronRight, ListChevronsDownUp, Copy, Loader2, MoreHorizontal, Pencil, RotateCcw, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Card } from "@multica/ui/components/ui/card";
import { Button } from "@multica/ui/components/ui/button";
@@ -248,6 +248,60 @@ function initialStandaloneAttachmentIds(entry: TimelineEntry): Set<string> {
);
}
function retryableAgentFailureComment(entry: TimelineEntry): entry is TimelineEntry & { source_task_id: string } {
return (
entry.actor_type === "agent" &&
entry.comment_type === "system" &&
typeof entry.source_task_id === "string" &&
entry.source_task_id.length > 0
);
}
function TaskCommentRetryButton({
issueId,
taskId,
className,
}: {
issueId: string;
taskId: string;
className?: string;
}) {
const { t } = useT("issues");
const [retrying, setRetrying] = useState(false);
const handleRetry = async () => {
if (retrying) return;
setRetrying(true);
try {
await api.rerunIssue(issueId, taskId);
} catch (e) {
toast.error(e instanceof Error ? e.message : t(($) => $.execution_log.retry_failed));
} finally {
setRetrying(false);
}
};
return (
<div className={cn("flex", className)}>
<Button
type="button"
size="sm"
variant="outline"
onClick={handleRetry}
disabled={retrying}
aria-label={t(($) => $.execution_log.retry_task_aria)}
>
{retrying ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RotateCcw className="h-3.5 w-3.5" />
)}
{t(($) => $.execution_log.retry_task_tooltip)}
</Button>
</div>
);
}
// ---------------------------------------------------------------------------
// Shared edit-attachment state hook
// ---------------------------------------------------------------------------
@@ -603,6 +657,13 @@ function CommentRow({
<ReadonlyContent content={entry.content ?? ""} attachments={entry.attachments} />
</div>
<AttachmentList attachments={entry.attachments} content={entry.content} className="mt-1.5 pl-12 pr-4" />
{retryableAgentFailureComment(entry) && (
<TaskCommentRetryButton
issueId={issueId}
taskId={entry.source_task_id}
className="mt-2 pl-12 pr-4"
/>
)}
<ReactionBar
reactions={reactions}
currentUserId={currentUserId}
@@ -883,6 +944,13 @@ function CommentCardImpl({
<ReadonlyContent content={entry.content ?? ""} attachments={entry.attachments} />
</div>
<AttachmentList attachments={entry.attachments} content={entry.content} className="mt-1.5 pl-10" />
{retryableAgentFailureComment(entry) && (
<TaskCommentRetryButton
issueId={issueId}
taskId={entry.source_task_id}
className="mt-2 pl-10"
/>
)}
<ReactionBar
reactions={reactions}
currentUserId={currentUserId}

View File

@@ -208,6 +208,7 @@ const mockApiObj = vi.hoisted(() => ({
unsubscribeFromIssue: vi.fn().mockResolvedValue(undefined),
getActiveTasksForIssue: vi.fn().mockResolvedValue({ tasks: [] }),
listTasksByIssue: vi.fn().mockResolvedValue([]),
rerunIssue: vi.fn(),
listTaskMessages: vi.fn().mockResolvedValue([]),
listChildIssues: vi.fn().mockResolvedValue({ issues: [] }),
listIssues: vi.fn().mockResolvedValue({ issues: [], total: 0 }),
@@ -507,6 +508,7 @@ describe("IssueDetail (shared)", () => {
mockApiObj.listIssues.mockResolvedValue({ issues: [], total: 0 });
mockApiObj.getActiveTasksForIssue.mockResolvedValue({ tasks: [] });
mockApiObj.listTasksByIssue.mockResolvedValue([]);
mockApiObj.rerunIssue.mockResolvedValue({ id: "task-rerun" });
mockApiObj.listMembers.mockResolvedValue([
{ user_id: "user-1", name: "Test User", email: "test@test.com", role: "admin" },
]);
@@ -777,6 +779,100 @@ describe("IssueDetail (shared)", () => {
expect(screen.getByText("I can help with this")).toBeInTheDocument();
});
it("reruns the source task from an agent failure comment", async () => {
mockApiObj.listTimeline.mockResolvedValue([
...mockTimeline,
{
type: "comment",
id: "comment-failed-task",
actor_type: "agent",
actor_id: "agent-1",
content: "API Error: 500 Internal server error",
parent_id: null,
created_at: "2026-01-18T00:00:00Z",
updated_at: "2026-01-18T00:00:00Z",
comment_type: "system",
source_task_id: "task-failed",
},
]);
renderIssueDetail();
await screen.findByText("API Error: 500 Internal server error");
fireEvent.click(screen.getByRole("button", { name: "Retry task" }));
await waitFor(() => {
expect(mockApiObj.rerunIssue).toHaveBeenCalledWith("issue-1", "task-failed");
});
});
it("does not show retry for child-done system comments", async () => {
mockApiObj.listTimeline.mockResolvedValue([
...mockTimeline,
{
type: "comment",
id: "comment-child-done",
actor_type: "system",
actor_id: "00000000-0000-0000-0000-000000000000",
content: "Sub-issue MUL-123 is done.",
parent_id: null,
created_at: "2026-01-18T00:00:00Z",
updated_at: "2026-01-18T00:00:00Z",
comment_type: "system",
},
]);
renderIssueDetail();
await screen.findByText("Sub-issue MUL-123 is done.");
expect(screen.queryByRole("button", { name: "Retry task" })).not.toBeInTheDocument();
});
it("does not show retry for successful agent task comments", async () => {
mockApiObj.listTimeline.mockResolvedValue([
...mockTimeline,
{
type: "comment",
id: "comment-successful-task",
actor_type: "agent",
actor_id: "agent-1",
content: "Finished the requested work.",
parent_id: null,
created_at: "2026-01-18T00:00:00Z",
updated_at: "2026-01-18T00:00:00Z",
comment_type: "comment",
source_task_id: "task-success",
},
]);
renderIssueDetail();
await screen.findByText("Finished the requested work.");
expect(screen.queryByRole("button", { name: "Retry task" })).not.toBeInTheDocument();
});
it("does not show retry for agent system comments without a source task", async () => {
mockApiObj.listTimeline.mockResolvedValue([
...mockTimeline,
{
type: "comment",
id: "comment-agent-system",
actor_type: "agent",
actor_id: "agent-1",
content: "System coordination update.",
parent_id: null,
created_at: "2026-01-18T00:00:00Z",
updated_at: "2026-01-18T00:00:00Z",
comment_type: "system",
},
]);
renderIssueDetail();
await screen.findByText("System coordination update.");
expect(screen.queryByRole("button", { name: "Retry task" })).not.toBeInTheDocument();
});
it("collapses non-trailing activity blocks and expands the last one by default", async () => {
// Timeline shape:
// [activities: status_changed, priority_changed] ← block A (older)

View File

@@ -56,6 +56,7 @@ function commentToTimelineEntry(c: Comment): TimelineEntry {
resolved_at: c.resolved_at,
resolved_by_type: c.resolved_by_type,
resolved_by_id: c.resolved_by_id,
source_task_id: c.source_task_id,
};
}

View File

@@ -34,6 +34,7 @@ type TimelineEntry struct {
ResolvedAt *string `json:"resolved_at,omitempty"`
ResolvedByType *string `json:"resolved_by_type,omitempty"`
ResolvedByID *string `json:"resolved_by_id,omitempty"`
SourceTaskID *string `json:"source_task_id,omitempty"`
}
// timelineHardCap bounds the per-issue timeline payload. Sized as a defensive
@@ -188,6 +189,7 @@ func (h *Handler) commentsToEntries(r *http.Request, comments []db.Comment) []Ti
ResolvedAt: timestampToPtr(c.ResolvedAt),
ResolvedByType: textToPtr(c.ResolvedByType),
ResolvedByID: uuidToPtr(c.ResolvedByID),
SourceTaskID: uuidToPtr(c.SourceTaskID),
}
}
return out

View File

@@ -32,6 +32,7 @@ type CommentResponse struct {
ResolvedAt *string `json:"resolved_at"`
ResolvedByType *string `json:"resolved_by_type"`
ResolvedByID *string `json:"resolved_by_id"`
SourceTaskID *string `json:"source_task_id,omitempty"`
Reactions []ReactionResponse `json:"reactions"`
Attachments []AttachmentResponse `json:"attachments"`
// Orientation stats — populated only on the roots_only path and omitted in
@@ -68,6 +69,7 @@ func commentToResponse(c db.Comment, reactions []ReactionResponse, attachments [
ResolvedAt: timestampToPtr(c.ResolvedAt),
ResolvedByType: textToPtr(c.ResolvedByType),
ResolvedByID: uuidToPtr(c.ResolvedByID),
SourceTaskID: uuidToPtr(c.SourceTaskID),
Reactions: reactions,
Attachments: attachments,
}

View File

@@ -1283,7 +1283,7 @@ func (s *TaskService) CompleteTask(ctx context.Context, taskID pgtype.UUID, resu
"agent_id", util.UUIDToString(task.AgentID),
)
} else {
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(body), "comment", task.TriggerCommentID)
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(body), "comment", task.TriggerCommentID, pgtype.UUID{})
}
}
}
@@ -1444,7 +1444,7 @@ func (s *TaskService) FailTask(ctx context.Context, taskID pgtype.UUID, errMsg,
// want to spam the issue with "task timed out" messages on every
// daemon hiccup.
if errMsg != "" && task.IssueID.Valid && retried == nil {
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system", task.TriggerCommentID)
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system", task.TriggerCommentID, task.ID)
}
// Mirror the issue fallback for chat tasks: write an assistant
@@ -2096,7 +2096,7 @@ func (s *TaskService) getIssuePrefix(workspaceID pgtype.UUID) string {
return ws.IssuePrefix
}
func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string, parentID pgtype.UUID) {
func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string, parentID, sourceTaskID pgtype.UUID) {
if content == "" {
return
}
@@ -2118,13 +2118,14 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p
}
}
comment, err := s.Queries.CreateComment(ctx, db.CreateCommentParams{
IssueID: issueID,
WorkspaceID: issue.WorkspaceID,
AuthorType: "agent",
AuthorID: agentID,
Content: content,
Type: commentType,
ParentID: parentID,
IssueID: issueID,
WorkspaceID: issue.WorkspaceID,
AuthorType: "agent",
AuthorID: agentID,
Content: content,
Type: commentType,
ParentID: parentID,
SourceTaskID: sourceTaskID,
})
if err != nil {
return
@@ -2136,14 +2137,15 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p
ActorID: util.UUIDToString(agentID),
Payload: map[string]any{
"comment": map[string]any{
"id": util.UUIDToString(comment.ID),
"issue_id": util.UUIDToString(comment.IssueID),
"author_type": comment.AuthorType,
"author_id": util.UUIDToString(comment.AuthorID),
"content": comment.Content,
"type": comment.Type,
"parent_id": util.UUIDToPtr(comment.ParentID),
"created_at": comment.CreatedAt.Time.Format("2006-01-02T15:04:05Z"),
"id": util.UUIDToString(comment.ID),
"issue_id": util.UUIDToString(comment.IssueID),
"author_type": comment.AuthorType,
"author_id": util.UUIDToString(comment.AuthorID),
"content": comment.Content,
"type": comment.Type,
"parent_id": util.UUIDToPtr(comment.ParentID),
"source_task_id": util.UUIDToPtr(comment.SourceTaskID),
"created_at": comment.CreatedAt.Time.Format("2006-01-02T15:04:05Z"),
},
"issue_title": issue.Title,
"issue_status": issue.Status,

View File

@@ -0,0 +1,2 @@
ALTER TABLE comment
DROP COLUMN source_task_id;

View File

@@ -0,0 +1,2 @@
ALTER TABLE comment
ADD COLUMN source_task_id UUID REFERENCES agent_task_queue(id) ON DELETE SET NULL;

View File

@@ -45,7 +45,7 @@ UPDATE comment SET
WHERE comment.id IN (SELECT id FROM descendants)
AND comment.id <> $1
AND comment.resolved_at IS NOT NULL
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id
`
type ClearOtherThreadResolutionsParams struct {
@@ -87,6 +87,7 @@ func (q *Queries) ClearOtherThreadResolutions(ctx context.Context, arg ClearOthe
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
); err != nil {
return nil, err
}
@@ -154,19 +155,20 @@ func (q *Queries) CountNewCommentsSince(ctx context.Context, arg CountNewComment
}
const createComment = `-- name: CreateComment :one
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id, source_task_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id
`
type CreateCommentParams struct {
IssueID pgtype.UUID `json:"issue_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
AuthorType string `json:"author_type"`
AuthorID pgtype.UUID `json:"author_id"`
Content string `json:"content"`
Type string `json:"type"`
ParentID pgtype.UUID `json:"parent_id"`
IssueID pgtype.UUID `json:"issue_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
AuthorType string `json:"author_type"`
AuthorID pgtype.UUID `json:"author_id"`
Content string `json:"content"`
Type string `json:"type"`
ParentID pgtype.UUID `json:"parent_id"`
SourceTaskID pgtype.UUID `json:"source_task_id"`
}
func (q *Queries) CreateComment(ctx context.Context, arg CreateCommentParams) (Comment, error) {
@@ -178,6 +180,7 @@ func (q *Queries) CreateComment(ctx context.Context, arg CreateCommentParams) (C
arg.Content,
arg.Type,
arg.ParentID,
arg.SourceTaskID,
)
var i Comment
err := row.Scan(
@@ -194,6 +197,7 @@ func (q *Queries) CreateComment(ctx context.Context, arg CreateCommentParams) (C
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
@@ -214,7 +218,7 @@ func (q *Queries) DeleteComment(ctx context.Context, arg DeleteCommentParams) er
}
const getComment = `-- name: GetComment :one
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id FROM comment
WHERE id = $1
`
@@ -235,12 +239,13 @@ func (q *Queries) GetComment(ctx context.Context, id pgtype.UUID) (Comment, erro
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
const getCommentInWorkspace = `-- name: GetCommentInWorkspace :one
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id FROM comment
WHERE id = $1 AND workspace_id = $2
`
@@ -266,6 +271,7 @@ func (q *Queries) GetCommentInWorkspace(ctx context.Context, arg GetCommentInWor
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
@@ -280,7 +286,7 @@ WITH RECURSIVE root_of AS (
FROM comment p
JOIN root_of r ON p.id = r.parent_id
)
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type, c.created_at, c.updated_at, c.parent_id, c.workspace_id, c.resolved_at, c.resolved_by_type, c.resolved_by_id FROM comment c
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type, c.created_at, c.updated_at, c.parent_id, c.workspace_id, c.resolved_at, c.resolved_by_type, c.resolved_by_id, c.source_task_id FROM comment c
WHERE c.id = (SELECT id FROM root_of WHERE parent_id IS NULL LIMIT 1)
`
@@ -311,6 +317,7 @@ func (q *Queries) GetThreadRoot(ctx context.Context, arg GetThreadRootParams) (C
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
@@ -359,7 +366,7 @@ func (q *Queries) HasAgentRepliedInThread(ctx context.Context, arg HasAgentRepli
}
const listCommentsForIssue = `-- name: ListCommentsForIssue :many
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id FROM comment
WHERE issue_id = $1 AND workspace_id = $2
ORDER BY created_at ASC, id ASC
LIMIT $3
@@ -397,6 +404,7 @@ func (q *Queries) ListCommentsForIssue(ctx context.Context, arg ListCommentsForI
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
); err != nil {
return nil, err
}
@@ -409,7 +417,7 @@ func (q *Queries) ListCommentsForIssue(ctx context.Context, arg ListCommentsForI
}
const listCommentsSinceForIssue = `-- name: ListCommentsSinceForIssue :many
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id FROM comment
WHERE issue_id = $1 AND workspace_id = $2 AND created_at > $3
ORDER BY created_at ASC, id ASC
LIMIT $4
@@ -452,6 +460,7 @@ func (q *Queries) ListCommentsSinceForIssue(ctx context.Context, arg ListComment
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
); err != nil {
return nil, err
}
@@ -1080,7 +1089,7 @@ UPDATE comment SET
resolved_by_id = COALESCE(resolved_by_id, $3),
updated_at = CASE WHEN resolved_at IS NULL THEN now() ELSE updated_at END
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id
`
type ResolveCommentParams struct {
@@ -1108,6 +1117,7 @@ func (q *Queries) ResolveComment(ctx context.Context, arg ResolveCommentParams)
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
@@ -1119,7 +1129,7 @@ UPDATE comment SET
resolved_by_id = NULL,
updated_at = CASE WHEN resolved_at IS NOT NULL THEN now() ELSE updated_at END
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id
`
// Idempotent: a no-op clear (already unresolved) just returns the row.
@@ -1140,6 +1150,7 @@ func (q *Queries) UnresolveComment(ctx context.Context, id pgtype.UUID) (Comment
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}
@@ -1149,7 +1160,7 @@ UPDATE comment SET
content = $2,
updated_at = now()
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id
`
type UpdateCommentParams struct {
@@ -1174,6 +1185,7 @@ func (q *Queries) UpdateComment(ctx context.Context, arg UpdateCommentParams) (C
&i.ResolvedAt,
&i.ResolvedByType,
&i.ResolvedByID,
&i.SourceTaskID,
)
return i, err
}

View File

@@ -209,6 +209,7 @@ type Comment struct {
ResolvedAt pgtype.Timestamptz `json:"resolved_at"`
ResolvedByType pgtype.Text `json:"resolved_by_type"`
ResolvedByID pgtype.UUID `json:"resolved_by_id"`
SourceTaskID pgtype.UUID `json:"source_task_id"`
}
type CommentReaction struct {

View File

@@ -339,8 +339,8 @@ SELECT c.* FROM comment c
WHERE c.id = (SELECT id FROM root_of WHERE parent_id IS NULL LIMIT 1);
-- name: CreateComment :one
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id)
VALUES ($1, $2, $3, $4, $5, $6, sqlc.narg(parent_id))
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id, source_task_id)
VALUES ($1, $2, $3, $4, $5, $6, sqlc.narg(parent_id), sqlc.narg(source_task_id))
RETURNING *;
-- name: UpdateComment :one