mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(ui): avoid visible issue detail refreshes during agent updates
This commit is contained in:
1 parent
f377bd32e4
commit
54c0a6572f
3 files changed
+133
-38
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -254,6 +254,29 @@ function shouldSuppressAgentStatusToastForVisibleIssue(
|
||||
return !!agentId && agentId === context.assigneeAgentId;
|
||||
}
|
||||
|
||||
function shouldDeferIssueRefetchForVisibleAgentActivity(
|
||||
queryClient: QueryClient,
|
||||
pathname: string,
|
||||
payload: Record<string, unknown>,
|
||||
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<string, unknown>,
|
||||
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,
|
||||
|
||||
@@ -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<OptimisticIssueComment[]>([]);
|
||||
const [pendingCommentComposerFocusKey, setPendingCommentComposerFocusKey] = useState(0);
|
||||
const [issueChatInitialTranscriptReady, setIssueChatInitialTranscriptReady] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const lastMarkedReadIssueIdRef = useRef<string | null>(null);
|
||||
const commentComposerRef = useRef<IssueChatComposerHandle | null>(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<MentionOption[]>(() => {
|
||||
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}
|
||||
|
||||
Reference in new issue
Block a user