mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
Fix issue thread typing lag
Reduce idle issue-detail polling, avoid duplicate live-run polling inside the embedded widget, and memoize the comment thread so long timelines do not rerender during unrelated page updates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
08fea10ce1
commit
2779701aba
4 files changed
+94
-37
No files matched your search
@@ -545,7 +545,7 @@ const TimelineList = memo(function TimelineList({
|
||||
);
|
||||
});
|
||||
|
||||
export function CommentThread({
|
||||
export const CommentThread = memo(function CommentThread({
|
||||
comments,
|
||||
queuedComments = [],
|
||||
feedbackVotes = [],
|
||||
@@ -891,4 +891,6 @@ export function CommentThread({
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
CommentThread.displayName = "CommentThread";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats";
|
||||
import { heartbeatsApi, type ActiveRunForIssue, type LiveRunForIssue } from "../api/heartbeats";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { formatDateTime } from "../lib/utils";
|
||||
import { ExternalLink, Square } from "lucide-react";
|
||||
@@ -13,6 +13,8 @@ import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts";
|
||||
interface LiveRunWidgetProps {
|
||||
issueId: string;
|
||||
companyId?: string | null;
|
||||
liveRunsData?: LiveRunForIssue[];
|
||||
activeRunData?: ActiveRunForIssue | null;
|
||||
}
|
||||
|
||||
function toIsoString(value: string | Date | null | undefined): string | null {
|
||||
@@ -24,24 +26,34 @@ function isRunActive(status: string): boolean {
|
||||
return status === "queued" || status === "running";
|
||||
}
|
||||
|
||||
export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
||||
export function LiveRunWidget({
|
||||
issueId,
|
||||
companyId,
|
||||
liveRunsData,
|
||||
activeRunData,
|
||||
}: LiveRunWidgetProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [cancellingRunIds, setCancellingRunIds] = useState(new Set<string>());
|
||||
const shouldFetchLiveRuns = liveRunsData === undefined;
|
||||
const shouldFetchActiveRun = activeRunData === undefined;
|
||||
|
||||
const { data: liveRuns } = useQuery({
|
||||
const { data: fetchedLiveRuns } = useQuery({
|
||||
queryKey: queryKeys.issues.liveRuns(issueId),
|
||||
queryFn: () => heartbeatsApi.liveRunsForIssue(issueId),
|
||||
enabled: !!issueId,
|
||||
enabled: !!issueId && shouldFetchLiveRuns,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const { data: activeRun } = useQuery({
|
||||
const { data: fetchedActiveRun } = useQuery({
|
||||
queryKey: queryKeys.issues.activeRun(issueId),
|
||||
queryFn: () => heartbeatsApi.activeRunForIssue(issueId),
|
||||
enabled: !!issueId,
|
||||
enabled: !!issueId && shouldFetchActiveRun,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const liveRuns = liveRunsData ?? fetchedLiveRuns;
|
||||
const activeRun = activeRunData ?? fetchedActiveRun;
|
||||
|
||||
const runs = useMemo(() => {
|
||||
const deduped = new Map<string, LiveRunForIssue>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
|
||||
@@ -549,7 +549,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||
characterData: true,
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, [decorateProjectMentions, value]);
|
||||
}, [decorateProjectMentions]);
|
||||
|
||||
const selectMention = useCallback(
|
||||
(option: AutocompleteOption) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
||||
import { pickTextColorForPillBg } from "@/lib/color-contrast";
|
||||
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -91,6 +91,11 @@ type IssueDetailComment = (IssueComment | OptimisticIssueComment) & {
|
||||
queueTargetRunId?: string | null;
|
||||
};
|
||||
|
||||
const ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS = 3000;
|
||||
const IDLE_ISSUE_RUN_POLL_INTERVAL_MS = 30000;
|
||||
const ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 5000;
|
||||
const IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 30000;
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
"issue.created": "created the issue",
|
||||
"issue.updated": "updated the issue",
|
||||
@@ -333,13 +338,6 @@ export function IssueDetail() {
|
||||
enabled: !!issueId,
|
||||
});
|
||||
|
||||
const { data: linkedRuns } = useQuery({
|
||||
queryKey: queryKeys.issues.runs(issueId!),
|
||||
queryFn: () => activityApi.runsForIssue(issueId!),
|
||||
enabled: !!issueId,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const { data: linkedApprovals } = useQuery({
|
||||
queryKey: queryKeys.issues.approvals(issueId!),
|
||||
queryFn: () => issuesApi.listApprovals(issueId!),
|
||||
@@ -356,17 +354,33 @@ export function IssueDetail() {
|
||||
queryKey: queryKeys.issues.liveRuns(issueId!),
|
||||
queryFn: () => heartbeatsApi.liveRunsForIssue(issueId!),
|
||||
enabled: !!issueId,
|
||||
refetchInterval: 3000,
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data as Array<unknown> | undefined;
|
||||
return data && data.length > 0
|
||||
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
||||
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: activeRun } = useQuery({
|
||||
queryKey: queryKeys.issues.activeRun(issueId!),
|
||||
queryFn: () => heartbeatsApi.activeRunForIssue(issueId!),
|
||||
enabled: !!issueId,
|
||||
refetchInterval: 3000,
|
||||
refetchInterval: (query) =>
|
||||
query.state.data
|
||||
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
||||
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS,
|
||||
});
|
||||
|
||||
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
||||
const { data: linkedRuns } = useQuery({
|
||||
queryKey: queryKeys.issues.runs(issueId!),
|
||||
queryFn: () => activityApi.runsForIssue(issueId!),
|
||||
enabled: !!issueId,
|
||||
refetchInterval: hasLiveRuns
|
||||
? ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS
|
||||
: IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS,
|
||||
});
|
||||
const runningIssueRun = useMemo(
|
||||
() => (
|
||||
activeRun?.status === "running"
|
||||
@@ -957,6 +971,39 @@ export function IssueDetail() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleInterruptQueued = useCallback(
|
||||
async (runId: string) => {
|
||||
await interruptQueuedComment.mutateAsync(runId);
|
||||
},
|
||||
[interruptQueuedComment],
|
||||
);
|
||||
|
||||
const handleCommentImageUpload = useCallback(
|
||||
async (file: File) => {
|
||||
const attachment = await uploadAttachment.mutateAsync(file);
|
||||
return attachment.contentPath;
|
||||
},
|
||||
[uploadAttachment],
|
||||
);
|
||||
|
||||
const handleCommentAttachImage = useCallback(
|
||||
async (file: File) => {
|
||||
await uploadAttachment.mutateAsync(file);
|
||||
},
|
||||
[uploadAttachment],
|
||||
);
|
||||
|
||||
const handleCommentAdd = useCallback(
|
||||
async (body: string, reopen?: boolean, reassignment?: CommentReassignment) => {
|
||||
if (reassignment) {
|
||||
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment });
|
||||
return;
|
||||
}
|
||||
await addComment.mutateAsync({ body, reopen });
|
||||
},
|
||||
[addComment, addCommentAndReassign],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const titleLabel = issue?.title ?? issueId ?? "Issue";
|
||||
setBreadcrumbs([
|
||||
@@ -1532,9 +1579,7 @@ export function IssueDetail() {
|
||||
currentAssigneeValue={actualAssigneeValue}
|
||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||
mentions={mentionOptions}
|
||||
onInterruptQueued={async (runId) => {
|
||||
await interruptQueuedComment.mutateAsync(runId);
|
||||
}}
|
||||
onInterruptQueued={handleInterruptQueued}
|
||||
interruptingQueuedRunId={interruptQueuedComment.isPending ? runningIssueRun?.id ?? null : null}
|
||||
composerDisabledReason={commentComposerDisabledReason}
|
||||
onVote={async (commentId, vote, options) => {
|
||||
@@ -1547,21 +1592,19 @@ export function IssueDetail() {
|
||||
sharingPreferenceAtSubmit: feedbackDataSharingPreference,
|
||||
});
|
||||
}}
|
||||
onAdd={async (body, reopen, reassignment) => {
|
||||
if (reassignment) {
|
||||
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment });
|
||||
return;
|
||||
}
|
||||
await addComment.mutateAsync({ body, reopen });
|
||||
}}
|
||||
imageUploadHandler={async (file) => {
|
||||
const attachment = await uploadAttachment.mutateAsync(file);
|
||||
return attachment.contentPath;
|
||||
}}
|
||||
onAttachImage={async (file) => {
|
||||
await uploadAttachment.mutateAsync(file);
|
||||
}}
|
||||
liveRunSlot={<LiveRunWidget issueId={issueId!} companyId={issue.companyId} />}
|
||||
onAdd={handleCommentAdd}
|
||||
imageUploadHandler={handleCommentImageUpload}
|
||||
onAttachImage={handleCommentAttachImage}
|
||||
liveRunSlot={
|
||||
hasLiveRuns ? (
|
||||
<LiveRunWidget
|
||||
issueId={issueId!}
|
||||
companyId={issue.companyId}
|
||||
liveRunsData={liveRuns ?? []}
|
||||
activeRunData={activeRun ?? null}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
Reference in new issue
Block a user