Spaces:
Paused
Paused
File size: 3,721 Bytes
b152fd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | import type { Issue, IssueComment } from "@paperclipai/shared";
export interface IssueCommentReassignment {
assigneeAgentId: string | null;
assigneeUserId: string | null;
}
export interface OptimisticIssueComment extends IssueComment {
clientId: string;
clientStatus: "pending" | "queued";
queueTargetRunId?: string | null;
}
export type IssueTimelineComment = IssueComment | OptimisticIssueComment;
function toTimestamp(value: Date | string) {
return new Date(value).getTime();
}
function createOptimisticCommentId() {
const randomUuid = globalThis.crypto?.randomUUID?.();
if (randomUuid) {
return `optimistic-${randomUuid}`;
}
return `optimistic-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
export function sortIssueComments<T extends { createdAt: Date | string; id: string }>(comments: T[]) {
return [...comments].sort((a, b) => {
const createdAtDiff = toTimestamp(a.createdAt) - toTimestamp(b.createdAt);
if (createdAtDiff !== 0) return createdAtDiff;
return a.id.localeCompare(b.id);
});
}
export function createOptimisticIssueComment(params: {
companyId: string;
issueId: string;
body: string;
authorUserId: string | null;
clientStatus?: OptimisticIssueComment["clientStatus"];
queueTargetRunId?: string | null;
}): OptimisticIssueComment {
const now = new Date();
const clientId = createOptimisticCommentId();
return {
id: clientId,
clientId,
companyId: params.companyId,
issueId: params.issueId,
authorAgentId: null,
authorUserId: params.authorUserId,
body: params.body,
clientStatus: params.clientStatus ?? "pending",
queueTargetRunId: params.queueTargetRunId ?? null,
createdAt: now,
updatedAt: now,
};
}
export function isQueuedIssueComment(params: {
comment: Pick<IssueTimelineComment, "createdAt"> & Partial<Pick<OptimisticIssueComment, "clientStatus">>;
activeRunStartedAt?: Date | string | null;
runId?: string | null;
interruptedRunId?: string | null;
}) {
if (params.runId) return false;
if (params.interruptedRunId) return false;
if (params.comment.clientStatus === "queued") return true;
if (!params.activeRunStartedAt) return false;
return toTimestamp(params.comment.createdAt) >= toTimestamp(params.activeRunStartedAt);
}
export function mergeIssueComments(
comments: IssueComment[] | undefined,
optimisticComments: OptimisticIssueComment[],
): IssueTimelineComment[] {
const merged = [...(comments ?? [])];
const existingIds = new Set(merged.map((comment) => comment.id));
for (const comment of optimisticComments) {
if (!existingIds.has(comment.id)) {
merged.push(comment);
}
}
return sortIssueComments(merged);
}
export function upsertIssueComment(
comments: IssueComment[] | undefined,
nextComment: IssueComment,
): IssueComment[] {
const current = comments ?? [];
const existingIndex = current.findIndex((comment) => comment.id === nextComment.id);
if (existingIndex === -1) {
return sortIssueComments([...current, nextComment]);
}
const updated = [...current];
updated[existingIndex] = nextComment;
return sortIssueComments(updated);
}
export function applyOptimisticIssueCommentUpdate(
issue: Issue | undefined,
params: {
reopen?: boolean;
reassignment?: IssueCommentReassignment;
},
) {
if (!issue) return issue;
const nextIssue: Issue = { ...issue };
if (params.reopen === true && (issue.status === "done" || issue.status === "cancelled")) {
nextIssue.status = "todo";
}
if (params.reassignment) {
nextIssue.assigneeAgentId = params.reassignment.assigneeAgentId;
nextIssue.assigneeUserId = params.reassignment.assigneeUserId;
}
return nextIssue;
}
|