fix(FN-8764): refresh stale task activity feed
This commit is contained in:
7
.changeset/refresh-empty-task-activity-feed.md
Normal file
7
.changeset/refresh-empty-task-activity-feed.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Refresh an empty task Activity Feed when operators open it after execution has started.
|
||||
category: fix
|
||||
dev: Shared task-detail hosts refetch complete task logs when an empty Feed is selected.
|
||||
@@ -4355,14 +4355,46 @@ export function TaskDetailContent({
|
||||
}
|
||||
}, [oversightActive, activitySegment]);
|
||||
|
||||
/*
|
||||
FNXC:TaskActivityFeedFreshness 2026-08-07-08:30:
|
||||
Task list and SSE snapshots intentionally strip task.log. If a shared detail host captured an
|
||||
empty full-detail snapshot before activity was written, selecting Feed must retry that complete
|
||||
read instead of preserving "(no activity)" forever. Populated feeds remain snapshot-stable and
|
||||
incur no extra request; Live and Raw keep their independent streaming paths.
|
||||
*/
|
||||
const activityFeedIsEmpty = !workingTask.log?.length;
|
||||
const refreshEmptyActivityFeed = useCallback(() => {
|
||||
if (!activityFeedIsEmpty) return;
|
||||
|
||||
const requestGeneration = ++detailRequestGenerationRef.current;
|
||||
requestTaskDetail(task.id, projectId)
|
||||
.then((detail) => {
|
||||
if (!mountedRef.current
|
||||
|| detailRequestGenerationRef.current !== requestGeneration
|
||||
|| activeTaskIdRef.current !== detail.id) return;
|
||||
|
||||
const promptResponse = latestPromptResponseRef.current;
|
||||
const promptResponseMatchesDetail = promptResponse?.key === `${projectId ?? ""}:${detail.id}`;
|
||||
const detailWithLatestPrompt = promptResponseMatchesDetail
|
||||
? { ...detail, prompt: promptResponse.prompt } as TaskDetail
|
||||
: detail;
|
||||
setFullDetail((previous) => previous?.id === detail.id
|
||||
? mergeTaskSnapshot(previous, detailWithLatestPrompt, { fullSnapshot: true })
|
||||
: detailWithLatestPrompt);
|
||||
setDetailLoading(false);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [activityFeedIsEmpty, task.id, projectId, requestTaskDetail]);
|
||||
|
||||
const selectActivityView = useCallback((value: ActivitySegment) => {
|
||||
activityViewMenuViewportGuardUntilRef.current = 0;
|
||||
setActiveTab("chat");
|
||||
setActivitySegment(value);
|
||||
if (value === "feed") refreshEmptyActivityFeed();
|
||||
setShowActivityViewMenu(false);
|
||||
setActivityViewMenuPosition(null);
|
||||
requestAnimationFrame(() => activityViewButtonRef.current?.focus());
|
||||
}, []);
|
||||
}, [refreshEmptyActivityFeed]);
|
||||
|
||||
const handleActivityTabKeyDown = useCallback((event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
const shouldOpenMenu = event.key === "ArrowDown" || (event.altKey && event.key === "ArrowDown");
|
||||
|
||||
@@ -3,7 +3,7 @@ FNXC:TaskDetailTabs 2026-06-17-08:20:
|
||||
FN-7324 keeps the stable internal `chat` tab as Activity for explicit legacy links, but the omitted non-done default is now planner Chat. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import React, { type ComponentProps } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import {
|
||||
@@ -692,11 +692,14 @@ describe("TaskDetailModal GitHub tracking CTA", () => {
|
||||
});
|
||||
|
||||
describe("TaskDetailModal Activity feed loading", () => {
|
||||
function renderActivityFeedModal(task: ReturnType<typeof makeTask> | Record<string, unknown>) {
|
||||
function renderActivityFeedModal(
|
||||
task: ReturnType<typeof makeTask> | Record<string, unknown>,
|
||||
initialTab: ComponentProps<typeof TaskDetailModal>["initialTab"] = "logs",
|
||||
) {
|
||||
return render(
|
||||
<TaskDetailModal
|
||||
task={task as any}
|
||||
initialTab="logs"
|
||||
initialTab={initialTab}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -783,6 +786,39 @@ describe("TaskDetailModal Activity feed loading", () => {
|
||||
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:TaskActivityFeedFreshness 2026-08-07-08:30:
|
||||
Modal, main-panel, split-list, dock, popup, desktop, and mobile task details all render the shared
|
||||
TaskDetailContent feed. Entering Feed must refresh its full task snapshot because board/SSE rows
|
||||
deliberately carry log=[]; otherwise a detail opened before activity exists stays empty forever.
|
||||
*/
|
||||
it("refreshes an empty Feed when the persisted task log has gained activity", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { fetchTaskDetail } = await import("../../api");
|
||||
const refresh = createDeferred<ReturnType<typeof makeTask>>();
|
||||
vi.mocked(fetchTaskDetail).mockReset();
|
||||
vi.mocked(fetchTaskDetail).mockReturnValueOnce(refresh.promise as any);
|
||||
|
||||
renderActivityFeedModal(
|
||||
makeTask({ id: "FN-FEED-REFRESH", prompt: "# Already loaded", log: [] }),
|
||||
"chat",
|
||||
);
|
||||
|
||||
await selectActivityView(user, "feed");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-FEED-REFRESH", undefined);
|
||||
|
||||
await act(async () => {
|
||||
refresh.resolve(makeTask({
|
||||
id: "FN-FEED-REFRESH",
|
||||
prompt: "# Already loaded",
|
||||
log: [{ timestamp: "2026-08-07T08:20:00.000Z", action: "Executor started" }],
|
||||
}));
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Executor started")).toBeInTheDocument();
|
||||
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("preserves truncated activity message after detail load", async () => {
|
||||
const { fetchTaskDetail } = await import("../../api");
|
||||
vi.mocked(fetchTaskDetail).mockReset();
|
||||
|
||||
Reference in New Issue
Block a user