Skip to content

Commit 551be40

Browse files
BenTaylorDevclaude
andcommitted
fix(threads): surface isLoading=true while waiting for first context dispatch
CR feedback: with the /info gating in useThreads, the underlying thread store sits at isLoading=false (its initial state) until we dispatch the first context — which we now defer until runtimeConnectionStatus === Connected. Consumers reading `isLoading` during that window would otherwise see the empty-state branch and render a momentary "no threads" flash instead of a skeleton. Track `hasDispatchedContext` in React state; synthesize isLoading=true while runtimeUrl is set but no context has been dispatched yet. Once we dispatch, fall through to the store's own loading flag (which flips true in the contextChanged reducer, then false after the fetch settles). Tests: - use-threads: extend the Connected-gate test to assert isLoading=true before Connected, false after the fetch lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 22c392b commit 551be40

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

packages/react-core/src/v2/hooks/__tests__/use-threads.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,19 @@ describe("useThreads", () => {
507507
)
508508
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
509509

510-
const { rerender } = renderHook(() => useThreads(defaultInput));
510+
const { result, rerender } = renderHook(() => useThreads(defaultInput));
511511

512512
// Give effects a tick to settle; no fetch should occur while Connecting.
513513
await new Promise((resolve) => setTimeout(resolve, 20));
514514
expect(fetchMock).not.toHaveBeenCalled();
515515

516+
// While waiting for Connected, the hook must surface isLoading=true so
517+
// consumers don't render an empty-state flash before the first fetch
518+
// is even dispatched. The store's own isLoading is false at this
519+
// point (no contextChanged action yet), so the hook synthesizes it.
520+
expect(result.current.isLoading).toBe(true);
521+
expect(result.current.threads).toEqual([]);
522+
516523
// Flip to Connected with wsUrl populated, re-render. The effect now
517524
// dispatches exactly one list fetch (+ one subscribe after it lands).
518525
mockUseCopilotKit.mockReturnValue({
@@ -539,5 +546,10 @@ describe("useThreads", () => {
539546
([url]) => typeof url === "string" && /\/threads\?agentId=/.test(url),
540547
);
541548
expect(listCalls).toHaveLength(1);
549+
550+
// After the fetch settles, isLoading returns to false.
551+
await waitFor(() => {
552+
expect(result.current.isLoading).toBe(false);
553+
});
542554
});
543555
});

packages/react-core/src/v2/hooks/use-threads.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,20 @@ export function useThreads({
220220

221221
return new Error("Runtime URL is not configured");
222222
}, [copilotkit.runtimeUrl]);
223-
const isLoading = runtimeError ? false : storeIsLoading;
223+
224+
// Tracks whether we've dispatched the first real context to the store.
225+
// The store itself starts with `isLoading: false`, so before we dispatch
226+
// consumers would otherwise see an empty, non-loading state (empty-list
227+
// flash). While runtimeUrl is set and we haven't dispatched yet, we
228+
// synthesize `isLoading: true` so the UI keeps its loading indicator until
229+
// the first fetch is in flight (at which point the store's own
230+
// isLoading takes over).
231+
const [hasDispatchedContext, setHasDispatchedContext] = useState(false);
232+
const preConnectLoading = !!copilotkit.runtimeUrl && !hasDispatchedContext;
233+
234+
const isLoading = runtimeError
235+
? false
236+
: preConnectLoading || storeIsLoading;
224237
const error = runtimeError ?? storeError;
225238

226239
useEffect(() => {
@@ -264,6 +277,7 @@ export function useThreads({
264277
};
265278

266279
store.setContext(context);
280+
setHasDispatchedContext(true);
267281
}, [
268282
store,
269283
copilotkit.runtimeUrl,

0 commit comments

Comments
 (0)