diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 79f58702fea..4d2bdc87111 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -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({ ); -} +}); + +CommentThread.displayName = "CommentThread"; diff --git a/ui/src/components/LiveRunWidget.tsx b/ui/src/components/LiveRunWidget.tsx index 2c0f702e7f3..c9b8f71706a 100644 --- a/ui/src/components/LiveRunWidget.tsx +++ b/ui/src/components/LiveRunWidget.tsx @@ -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()); + 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(); for (const run of liveRuns ?? []) { diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index d528d4933c9..223b5755631 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -549,7 +549,7 @@ export const MarkdownEditor = forwardRef characterData: true, }); return () => observer.disconnect(); - }, [decorateProjectMentions, value]); + }, [decorateProjectMentions]); const selectMention = useCallback( (option: AutocompleteOption) => { diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index d16a31031ae..88a177cb78a 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -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 = { "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 | 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={} + onAdd={handleCommentAdd} + imageUploadHandler={handleCommentImageUpload} + onAttachImage={handleCommentAttachImage} + liveRunSlot={ + hasLiveRuns ? ( + + ) : null + } />