diff --git a/ui/src/context/LiveUpdatesProvider.test.ts b/ui/src/context/LiveUpdatesProvider.test.ts index f90eb2d9c22..ad0c02bcc14 100644 --- a/ui/src/context/LiveUpdatesProvider.test.ts +++ b/ui/src/context/LiveUpdatesProvider.test.ts @@ -163,6 +163,100 @@ describe("LiveUpdatesProvider issue invalidation", () => { refetchType: "inactive", }); }); + + it("keeps visible issue detail refetches inactive for downstream agent updates", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: (key: unknown) => { + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.detail("PAP-759"))) { + return { + id: "issue-1", + identifier: "PAP-759", + assigneeAgentId: "agent-1", + }; + } + return undefined; + }, + }; + + __liveUpdatesTestUtils.invalidateActivityQueries( + queryClient as never, + "company-1", + { + entityType: "issue", + entityId: "issue-1", + action: "issue.updated", + actorType: "system", + actorId: "heartbeat", + details: { + identifier: "PAP-759", + source: "deferred_comment_wake", + }, + }, + { userId: null, agentId: null }, + { pathname: "/PAP/issues/PAP-759", isForegrounded: true }, + ); + + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.detail("issue-1"), + refetchType: "inactive", + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.activity("issue-1"), + refetchType: "inactive", + }); + }); + + it("still actively refetches visible issue detail for board-authored updates", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: (key: unknown) => { + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.detail("PAP-759"))) { + return { + id: "issue-1", + identifier: "PAP-759", + assigneeAgentId: "agent-1", + }; + } + return undefined; + }, + }; + + __liveUpdatesTestUtils.invalidateActivityQueries( + queryClient as never, + "company-1", + { + entityType: "issue", + entityId: "issue-1", + action: "issue.updated", + actorType: "user", + actorId: "user-2", + details: { + identifier: "PAP-759", + status: "in_progress", + }, + }, + { userId: "user-1", agentId: null }, + { pathname: "/PAP/issues/PAP-759", isForegrounded: true }, + ); + + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.detail("issue-1"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.activity("issue-1"), + }); + expect(invalidations).not.toContainEqual({ + queryKey: queryKeys.issues.detail("issue-1"), + refetchType: "inactive", + }); + }); }); describe("LiveUpdatesProvider visible issue toast suppression", () => { diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index f5b0a4b9f69..33efd5238dd 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -254,6 +254,29 @@ function shouldSuppressAgentStatusToastForVisibleIssue( return !!agentId && agentId === context.assigneeAgentId; } +function shouldDeferIssueRefetchForVisibleAgentActivity( + queryClient: QueryClient, + pathname: string, + payload: Record, + options?: VisibleRouteOptions, +): boolean { + const entityType = readString(payload.entityType); + const entityId = readString(payload.entityId); + const actorType = readString(payload.actorType); + const action = readString(payload.action); + const details = readRecord(payload.details); + + if (entityType !== "issue" || !entityId) return false; + if (actorType !== "agent" && actorType !== "system") return false; + if (action !== "issue.updated") return false; + if (readString(details?.source) === "comment") return false; + + const context = resolveVisibleIssueRouteContext(queryClient, pathname, options); + if (!context) return false; + + return overlaps(context.issueRefs, buildIssueRefsForPayload(entityId, details)); +} + const ISSUE_TOAST_ACTIONS = new Set(["issue.created", "issue.updated", "issue.comment_added"]); const AGENT_TOAST_STATUSES = new Set(["error"]); const RUN_TOAST_STATUSES = new Set(["failed", "timed_out", "cancelled"]); @@ -481,6 +504,7 @@ function invalidateActivityQueries( companyId: string, payload: Record, currentActor: { userId: string | null; agentId: string | null }, + options?: { pathname?: string; isForegrounded?: boolean }, ) { queryClient.invalidateQueries({ queryKey: queryKeys.activity(companyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(companyId) }); @@ -504,9 +528,20 @@ function invalidateActivityQueries( (action === "issue.updated" && readString(details?.source) === "comment")) && ((actorType === "user" && !!currentActor.userId && actorId === currentActor.userId) || (actorType === "agent" && !!currentActor.agentId && actorId === currentActor.agentId)); + const visibleIssueAgentActivity = + !!options?.pathname && + shouldDeferIssueRefetchForVisibleAgentActivity( + queryClient, + options.pathname, + payload, + { isForegrounded: options.isForegrounded }, + ); const issueRefs = resolveIssueQueryRefs(queryClient, companyId, entityId, details); for (const ref of issueRefs) { - const invalidationOptions = selfCommentActivity ? { refetchType: "inactive" as const } : undefined; + const invalidationOptions = + (selfCommentActivity || visibleIssueAgentActivity) + ? { refetchType: "inactive" as const } + : undefined; queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(ref), ...invalidationOptions }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(ref), ...invalidationOptions }); if (action === "issue.comment_added") { @@ -655,7 +690,7 @@ function handleLiveEvent( } if (event.type === "activity.logged") { - invalidateActivityQueries(queryClient, expectedCompanyId, payload, currentActor); + invalidateActivityQueries(queryClient, expectedCompanyId, payload, currentActor, { pathname }); const action = readString(payload.action); const toast = buildActivityToast(queryClient, expectedCompanyId, payload, currentActor) ?? @@ -714,6 +749,7 @@ export const __liveUpdatesTestUtils = { closeSocketQuietly, invalidateActivityQueries, resolveLiveCompanyId, + shouldDeferIssueRefetchForVisibleAgentActivity, shouldSuppressActivityToastForVisibleIssue, shouldSuppressRunStatusToastForVisibleIssue, shouldSuppressAgentStatusToastForVisibleIssue, diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 827eec0617f..47e30313589 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -56,7 +56,6 @@ import { relativeTime, cn, formatTokens, visibleRunCostUsd } from "../lib/utils" import { ApprovalCard } from "../components/ApprovalCard"; import { InlineEditor } from "../components/InlineEditor"; import { IssueChatThread, type IssueChatComposerHandle } from "../components/IssueChatThread"; -import { useLiveRunTranscripts } from "../components/transcript/useLiveRunTranscripts"; import { IssueDocumentsSection } from "../components/IssueDocumentsSection"; import { IssuesList } from "../components/IssuesList"; import { IssueProperties } from "../components/IssueProperties"; @@ -79,7 +78,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { formatIssueActivityAction } from "@/lib/activity-format"; import { buildIssuePropertiesPanelKey } from "../lib/issue-properties-panel-key"; import { shouldRenderRichSubIssuesSection } from "../lib/issue-detail-subissues"; -import { resolveIssueChatTranscriptRuns } from "../lib/issueChatTranscriptRuns"; import { buildSubIssueDefaultsForViewer } from "../lib/subIssueDefaults"; import { Activity as ActivityIcon, @@ -485,7 +483,6 @@ export function IssueDetail() { const [galleryIndex, setGalleryIndex] = useState(0); const [optimisticComments, setOptimisticComments] = useState([]); const [pendingCommentComposerFocusKey, setPendingCommentComposerFocusKey] = useState(0); - const [issueChatInitialTranscriptReady, setIssueChatInitialTranscriptReady] = useState(false); const fileInputRef = useRef(null); const lastMarkedReadIssueIdRef = useRef(null); const commentComposerRef = useRef(null); @@ -509,10 +506,6 @@ export function IssueDetail() { [issueHeaderSeed, issueId, queryClient], ); - useEffect(() => { - setIssueChatInitialTranscriptReady(false); - }, [issueId]); - const { data: issue, isLoading, error } = useQuery({ queryKey: queryKeys.issues.detail(issueId!), queryFn: () => fetchIssueDetail(queryClient, issueId!), @@ -694,24 +687,6 @@ export function IssueDetail() { for (const a of agents ?? []) map.set(a.id, a); return map; }, [agents]); - const transcriptRuns = useMemo( - () => - resolveIssueChatTranscriptRuns({ - linkedRuns: timelineRuns, - liveRuns: liveRuns ?? [], - activeRun, - }), - [activeRun, liveRuns, timelineRuns], - ); - const { - transcriptByRun: issueChatTranscriptByRun, - hasOutputForRun: issueChatHasOutputForRun, - isInitialHydrating: issueChatTranscriptHydrating, - } = useLiveRunTranscripts({ - runs: transcriptRuns, - companyId: issue?.companyId ?? selectedCompanyId, - }); - const mentionOptions = useMemo(() => { const options: MentionOption[] = []; const activeAgents = [...(agents ?? [])] @@ -1803,14 +1778,7 @@ export function IssueDetail() { || (linkedRunsLoading && linkedRuns === undefined) || (liveRunsLoading && liveRuns === undefined) || (activeRunLoading && activeRun === undefined); - useEffect(() => { - if (issueChatInitialTranscriptReady) return; - if (issueChatCoreInitialLoading || issueChatTranscriptHydrating) return; - setIssueChatInitialTranscriptReady(true); - }, [issueChatCoreInitialLoading, issueChatInitialTranscriptReady, issueChatTranscriptHydrating]); - const issueChatInitialLoading = - issueChatCoreInitialLoading - || (!issueChatInitialTranscriptReady && issueChatTranscriptHydrating); + const issueChatInitialLoading = issueChatCoreInitialLoading; const activityInitialLoading = (activityLoading && activity === undefined) || (linkedRunsLoading && linkedRuns === undefined); @@ -2371,9 +2339,6 @@ export function IssueDetail() { issueStatus={issue.status} agentMap={agentMap} currentUserId={currentUserId} - enableLiveTranscriptPolling={false} - transcriptsByRunId={issueChatTranscriptByRun} - hasOutputForRun={issueChatHasOutputForRun} draftKey={`paperclip:issue-comment-draft:${issue.id}`} enableReassign reassignOptions={commentReassignOptions}