mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-02 03:49:15 +02:00
Compare commits
4 Commits
agent/lamb
...
agent/emac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd6229263 | ||
|
|
dea2b89fde | ||
|
|
e582728b98 | ||
|
|
9b306b4535 |
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
server/migrations/120_comment_source_task_id.down.sql
Normal file
2
server/migrations/120_comment_source_task_id.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE comment
|
||||
DROP COLUMN source_task_id;
|
||||
2
server/migrations/120_comment_source_task_id.up.sql
Normal file
2
server/migrations/120_comment_source_task_id.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE comment
|
||||
ADD COLUMN source_task_id UUID REFERENCES agent_task_queue(id) ON DELETE SET NULL;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user