fix(frontend): refresh active artifact content (#4584)

* fix(frontend): refresh active artifact content

* fix(frontend): remove 1Hz polling, keep only final refetch after run

Address review feedback: the 1Hz refetchInterval could poll
indefinitely when a write tool call is left unresolved (abort,
error, or missing ToolMessage). Removing the polling entirely
eliminates this risk while still satisfying the core requirement:
artifact content is refreshed once when the run settles, so edits
are visible without a manual reload.

- Remove refetchInterval / hasActiveWrite logic from hooks.ts
- Delete refresh.ts (hasActiveWriteForArtifact helper)
- Delete refresh.test.ts

* docs: align README and AGENTS.md with settle-time refetch behavior
This commit is contained in:
qin-chenghan 2026-07-30 23:47:16 +08:00 committed by GitHub
parent 0d8e11ad49
commit 60fe0c4433
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 19 additions and 6 deletions

View File

@ -926,7 +926,7 @@ Image bytes loaded for a vision-model call are transient: DeerFlow removes the h
After each run, DeerFlow records a workspace change summary for the run-owned `workspace` and `outputs` directories. The Web UI shows a compact "files changed" badge on the assistant turn; opening it reveals created, modified, and deleted files with text diffs when safe to display. Uploads are excluded because they are user inputs, not agent-generated changes. Large, binary, or sensitive-looking files are shown as metadata only.
Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh.
Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload.
With `AioSandboxProvider`, shell execution runs inside isolated containers. With `LocalSandboxProvider`, file tools still map to per-thread directories on the host, but host `bash` is disabled by default because it is not a secure isolation boundary. Re-enable host bash only for fully trusted local workflows. Host bash commands have a wall-clock timeout, and long-lived processes should be started in the background with output redirected to a workspace log.

View File

@ -69,6 +69,7 @@ The frontend is a stateful chat application. Users create **threads** (conversat
2. Stream events update thread state (messages, artifacts, todos, goal). The main thread stream uses the LangGraph SDK's `throttle: true` mode so updates received in the same macrotask coalesce before React is notified; do not replace it with a numeric delay without validating the SDK's trailing-debounce behavior on a continuous stream.
File-tool artifact auto-open work must run in an effect with timer cleanup; never schedule timers while rendering streamed `write_file` or `str_replace` updates.
`ThreadState.artifacts` remains the authoritative artifact list. The artifacts provider persists only thread-scoped panel UI state (`open`, selected path, and a refresh bootstrap cache) in session storage; an initial empty stream value must not overwrite that restored state before history finishes loading.
Formal artifact content is refreshed once when the run finishes; transient `write-file:` previews remain message-driven.
3. `useThreadHistory` loads persisted conversation pages from `GET /api/threads/{id}/messages/page`, preserving the backend's thread-global event `seq`; rendering overlays checkpoint/live copies at their matching canonical identities (a summarized checkpoint may contain a protected early input plus a recent tail). Context-compaction rescue diffs every retained visible identity rather than slicing at the first anchor, and keeps a run-scoped ledger of committed visible messages so replacement updates and repeated rolling checkpoint windows cannot erase an already displayed step. The resolver suppresses checkpoint/transient prefixes whose canonical position is still behind an unloaded cursor page instead of collapsing that unknown gap before a recent anchor, then adds optimistic messages without timestamp re-sorting. History invalidation preserves already-loaded pages so their established ordering positions are not discarded.
4. Stop actions call the LangGraph SDK stream stop path; `core/threads/hooks.ts` invalidates current-thread, thread-history, token-usage, and sidebar/search caches immediately and schedules one follow-up refetch because SDK stop may finish via abort + fire-and-forget cancel before backend title finalization commits
5. TanStack Query manages server state; localStorage stores user settings

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useEffect, useMemo, useRef } from "react";
import { useThread } from "@/components/workspace/messages/context";
@ -25,15 +25,27 @@ export function useArtifactContent({
return null;
}, [filepath, isWriteFile, thread]);
const { data, isLoading, error } = useQuery({
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["artifact", filepath, threadId, isMock],
queryFn: () => {
return loadArtifactContent({ filepath, threadId, isMock });
},
enabled,
// Cache artifact content for 5 minutes to avoid repeated fetches (especially for .skill ZIP extraction)
staleTime: 5 * 60 * 1000,
staleTime: 0,
refetchOnWindowFocus: true,
});
// Refetch once when the run settles so edits made during the run are
// visible without a manual reload.
const wasLoadingRef = useRef(thread.isLoading);
useEffect(() => {
const wasLoading = wasLoadingRef.current;
wasLoadingRef.current = thread.isLoading;
if (wasLoading && !thread.isLoading && enabled && !isWriteFile) {
void refetch().catch(() => undefined);
}
}, [enabled, isWriteFile, refetch, thread.isLoading]);
return {
content: isWriteFile ? content : data?.content,
url: isWriteFile ? undefined : data?.url,

View File

@ -19,7 +19,7 @@ export async function loadArtifactContent({
enhancedFilepath = filepath + "/SKILL.md";
}
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId, isMock });
const response = await fetch(url);
const response = await fetch(url, { cache: "no-store" });
const text = await response.text();
return { content: text, url };
}