Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions app/src/hooks/__tests__/useRefetchSnapshotOnTurnEnd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { useCoreState } from '../../providers/CoreStateProvider';
import { userApi } from '../../services/api/userApi';
import { useRefetchSnapshotOnTurnEnd } from '../useRefetchSnapshotOnTurnEnd';

vi.mock('../../providers/CoreStateProvider', () => ({
useCoreState: vi.fn(),
}));

vi.mock('../../services/api/userApi', () => ({
userApi: {
getMe: vi.fn(),
},
}));

describe('useRefetchSnapshotOnTurnEnd', () => {
const mockPatchSnapshot = vi.fn();

beforeEach(() => {
vi.useFakeTimers();
vi.mocked(useCoreState).mockReturnValue({
patchSnapshot: mockPatchSnapshot,
} as any);
});

afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});

it('refetches and patches snapshot after 750ms', async () => {
const mockUser1 = { _id: 'user1', firstName: 'Jules' };
vi.mocked(userApi.getMe).mockResolvedValue(mockUser1 as any);

const { result } = renderHook(() => useRefetchSnapshotOnTurnEnd());

act(() => {
result.current.refetch();
});

expect(userApi.getMe).not.toHaveBeenCalled();

await act(async () => {
vi.advanceTimersByTime(750);
});

expect(userApi.getMe).toHaveBeenCalledTimes(1);
expect(mockPatchSnapshot).toHaveBeenCalledWith({ currentUser: mockUser1 });
});

it('three rapid finalize events → one getMe call', async () => {
const mockUser1 = { _id: 'user1', firstName: 'Jules' };
vi.mocked(userApi.getMe).mockResolvedValue(mockUser1 as any);

const { result } = renderHook(() => useRefetchSnapshotOnTurnEnd());

act(() => {
result.current.refetch();
vi.advanceTimersByTime(300);
result.current.refetch();
vi.advanceTimersByTime(300);
result.current.refetch();
});

expect(userApi.getMe).not.toHaveBeenCalled();

await act(async () => {
vi.advanceTimersByTime(750);
});

expect(userApi.getMe).toHaveBeenCalledTimes(1);
});

it('mock userApi.getMe() to return two payloads in sequence → UI re-reads second value', async () => {
const mockUser1 = { _id: 'user1', firstName: 'Jules', onboarding_status: { finished: false } };
const mockUser2 = { _id: 'user1', firstName: 'Jules', onboarding_status: { finished: true } };

vi.mocked(userApi.getMe)
.mockResolvedValueOnce(mockUser1 as any)
.mockResolvedValueOnce(mockUser2 as any);

const { result } = renderHook(() => useRefetchSnapshotOnTurnEnd());

// First refetch
act(() => {
result.current.refetch();
});
await act(async () => {
vi.advanceTimersByTime(750);
});
expect(mockPatchSnapshot).toHaveBeenLastCalledWith({ currentUser: mockUser1 });

// Second refetch
act(() => {
result.current.refetch();
});
await act(async () => {
vi.advanceTimersByTime(750);
});
expect(mockPatchSnapshot).toHaveBeenLastCalledWith({ currentUser: mockUser2 });
});
});
37 changes: 37 additions & 0 deletions app/src/hooks/useRefetchSnapshotOnTurnEnd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useCallback, useRef } from 'react';

import { useCoreState } from '../providers/CoreStateProvider';
import { userApi } from '../services/api/userApi';

/**
* Hook to refetch the authoritative user state from the backend after a chat
* turn finishes. Updates the global snapshot in CoreStateProvider.
*
* Includes a 750ms debounce to collapse multiple rapid turn-finalized events.
*/
export function useRefetchSnapshotOnTurnEnd() {
const { patchSnapshot } = useCoreState();
const debounceTimerRef = useRef<number | null>(null);

const refetch = useCallback(() => {
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
}

debounceTimerRef.current = window.setTimeout(() => {
debounceTimerRef.current = null;

// Fire-and-forget on a microtask
void (async () => {
try {
const user = await userApi.getMe();
patchSnapshot({ currentUser: user });
} catch (error) {
console.warn('[useRefetchSnapshotOnTurnEnd] failed to refetch user state:', error);
}
})();
}, 750);
}, [patchSnapshot]);

return { refetch };
}
39 changes: 27 additions & 12 deletions app/src/pages/onboarding/steps/ContextGatheringStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
* External calls still go through core (auth, proxy, billing). Only the
* stage-by-stage orchestration lives in the renderer.
*/
import { useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';

import Button from '../../../components/ui/Button';
import WhatLeavesLink from '../../../features/privacy/WhatLeavesLink';
import { useCoreState } from '../../../providers/CoreStateProvider';
import { callCoreRpc } from '../../../services/coreRpcClient';
import OnboardingNextButton from '../components/OnboardingNextButton';

Expand Down Expand Up @@ -148,22 +149,36 @@ const ContextGatheringStep = ({
onNext,
onBack: _onBack,
}: ContextGatheringStepProps) => {
const [stageStatuses, setStageStatuses] = useState<Record<string, StageStatus>>(() => {
const { snapshot } = useCoreState();
const [localStageStatuses, setLocalStageStatuses] = useState<Record<string, StageStatus>>(() => {
const initial: Record<string, StageStatus> = {};
for (const s of STAGES) initial[s.id] = 'pending';
return initial;
});
const [stageDetails, setStageDetails] = useState<Record<string, string>>({});
const [finished, setFinished] = useState(false);
const [localStageDetails, setLocalStageDetails] = useState<Record<string, string>>({});
const [localFinished, setLocalFinished] = useState(false);
const [started, setStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const ranRef = useRef(false);

const hasGmail = connectedSources.some(s => s.includes('gmail'));

const stageStatuses = useMemo(
() => snapshot.currentUser?.onboarding_status?.stages ?? localStageStatuses,
[snapshot.currentUser?.onboarding_status?.stages, localStageStatuses]
);
const stageDetails = useMemo(
() => snapshot.currentUser?.onboarding_status?.details ?? localStageDetails,
[snapshot.currentUser?.onboarding_status?.details, localStageDetails]
);
const finished = useMemo(
() => snapshot.currentUser?.onboarding_status?.finished ?? localFinished,
[snapshot.currentUser?.onboarding_status?.finished, localFinished]
);

const setStage = (id: Stage['id'], status: StageStatus, detail?: string) => {
setStageStatuses(prev => ({ ...prev, [id]: status }));
if (detail !== undefined) setStageDetails(prev => ({ ...prev, [id]: detail }));
setLocalStageStatuses(prev => ({ ...prev, [id]: status }));
if (detail !== undefined) setLocalStageDetails(prev => ({ ...prev, [id]: detail }));
};

const handleStart = () => {
Expand All @@ -174,9 +189,9 @@ const ContextGatheringStep = ({
if (!hasGmail) {
const skipped: Record<string, StageStatus> = {};
for (const s of STAGES) skipped[s.id] = 'skipped';
setStageStatuses(skipped);
setStageDetails({ 'gmail-search': 'Gmail not connected' });
setFinished(true);
setLocalStageStatuses(skipped);
setLocalStageDetails({ 'gmail-search': 'Gmail not connected' });
setLocalFinished(true);
return;
}

Expand All @@ -197,15 +212,15 @@ const ContextGatheringStep = ({
setStage('gmail-search', 'skipped', 'No LinkedIn URL found in mailbox');
setStage('linkedin-scrape', 'skipped');
setStage('build-profile', 'skipped');
setFinished(true);
setLocalFinished(true);
return;
}
} catch (e) {
console.warn('[onboarding:context] gmail stage failed', e);
setStage('gmail-search', 'error', e instanceof Error ? e.message : String(e));
setStage('linkedin-scrape', 'skipped');
setStage('build-profile', 'skipped');
setFinished(true);
setLocalFinished(true);
return;
}

Expand Down Expand Up @@ -238,7 +253,7 @@ const ContextGatheringStep = ({
setStage('build-profile', 'error', e instanceof Error ? e.message : String(e));
}

setFinished(true);
setLocalFinished(true);
}

const completedCount = STAGES.filter(s => {
Expand Down
6 changes: 5 additions & 1 deletion app/src/providers/ChatRuntimeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import debug from 'debug';
import { useCallback, useEffect, useRef } from 'react';

import { useRefetchSnapshotOnTurnEnd } from '../hooks/useRefetchSnapshotOnTurnEnd';
import { requestUsageRefresh } from '../hooks/usageRefresh';
import {
type ChatInferenceStartEvent,
Expand Down Expand Up @@ -56,6 +57,7 @@ function rtLog(message: string, fields?: Record<string, string | number | null |

const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
const dispatch = useAppDispatch();
const { refetch: refetchSnapshot } = useRefetchSnapshotOnTurnEnd();
const socketStatus = useAppSelector(selectSocketStatus);
const toolTimelineByThread = useAppSelector(state => state.chatRuntime.toolTimelineByThread);
const inferenceStatusByThread = useAppSelector(
Expand Down Expand Up @@ -556,6 +558,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
reason: 'chat_done',
});
requestUsageRefresh();
refetchSnapshot();
dispatch(endInferenceTurn({ threadId: event.thread_id }));
dispatch(setActiveThread(null));
})();
Expand All @@ -574,6 +577,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
reason: 'chat_done',
});
requestUsageRefresh();
refetchSnapshot();
dispatch(endInferenceTurn({ threadId: event.thread_id }));
dispatch(setActiveThread(null));
},
Expand Down Expand Up @@ -636,7 +640,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
rtLog('unsubscribe_chat_events');
cleanup();
};
}, [dispatch, resolveVisibleThreadForProactive, socketStatus]);
}, [dispatch, resolveVisibleThreadForProactive, socketStatus, refetchSnapshot]);

return <>{children}</>;
};
Expand Down
13 changes: 13 additions & 0 deletions app/src/providers/CoreStateProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
setOnboardingCompletedFlag: (value: boolean) => Promise<void>;
setEncryptionKey: (value: string | null) => Promise<void>;
setPrimaryWalletAddress: (value: string | null) => Promise<void>;
patchSnapshot: (patch: Partial<CoreAppSnapshot>) => void;
setOnboardingTasks: (value: CoreOnboardingTasks | null) => Promise<void>;
storeSessionToken: (token: string, user?: object) => Promise<void>;
clearSession: () => Promise<void>;
Expand Down Expand Up @@ -424,13 +425,24 @@
});
}, [commitState, refresh]);

const patchSnapshot = useCallback(
(patch: Partial<CoreAppSnapshot>) => {
commitState(previous => ({
...previous,
snapshot: { ...previous.snapshot, ...patch },
}));
},
[commitState]
);

const value = useMemo<CoreStateContextValue>(
() => ({
...state,
refresh,
refreshTeams,
refreshTeamMembers,
refreshTeamInvites,
patchSnapshot,
setAnalyticsEnabled,
setOnboardingCompletedFlag,
setEncryptionKey: value => updateLocalState({ encryptionKey: value }),
Expand All @@ -445,6 +457,7 @@
refreshTeamInvites,
refreshTeamMembers,
refreshTeams,
patchSnapshot,
setAnalyticsEnabled,
setOnboardingCompletedFlag,
state,
Expand All @@ -459,7 +472,7 @@
export function useCoreState() {
const context = useContext(CoreStateContext);
if (!context) {
throw new Error('useCoreState must be used within CoreStateProvider');

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > mid-turn streaming invariants > terminates running tool-timeline rows on chat_done

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > mid-turn streaming invariants > sets inference status to thinking on inference_start and clears it on chat_done

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > mid-turn streaming invariants > replaces streaming state when request_id changes mid-turn

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > mid-turn streaming invariants > accumulates text_delta chunks within the same request_id

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > proactive thread resolution > deduplicates identical proactive messages from the same sender

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > proactive thread resolution > creates a new thread when no visible thread exists for proactive handoff

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > proactive thread resolution > reuses the selected thread when resolving a proactive: sender

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > dedupe > processes tool_call for different rounds as distinct events

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > dedupe > drops duplicate chat_done events with the same thread/request

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

Check failure on line 475 in app/src/providers/CoreStateProvider.tsx

View workflow job for this annotation

GitHub Actions / Frontend Unit Tests

src/providers/__tests__/ChatRuntimeProvider.test.tsx > ChatRuntimeProvider — dedupe, proactive resolution, mid-turn invariants > dedupe > drops duplicate tool_call events with the same thread/request/round/tool

Error: useCoreState must be used within CoreStateProvider ❯ useCoreState src/providers/CoreStateProvider.tsx:475:11 ❯ useRefetchSnapshotOnTurnEnd src/hooks/useRefetchSnapshotOnTurnEnd.ts:13:29 ❯ ChatRuntimeProvider src/providers/ChatRuntimeProvider.tsx:60:40 ❯ Object.react_stack_bottom_frame ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20 ❯ renderWithHooks ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22 ❯ updateFunctionComponent ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19 ❯ beginWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18 ❯ runWithFiberInDEV ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ performUnitOfWork ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22 ❯ workLoopSync ../node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
}
return context;
}
24 changes: 24 additions & 0 deletions app/src/providers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# App Providers

This directory contains the React context providers that manage the global state and services of the application.

## CoreStateProvider

Manages the authoritative global state of the application, including user authentication, session tokens, and the application snapshot.

### Turn-Boundary Refetch Contract

To ensure that the UI stays in sync with the backend state (especially during onboarding and context gathering), the application follows a refetch-on-turn-end contract:

- **Refetch Timing**: After every agent reply completes (the `chat_done` event in `ChatRuntimeProvider`), the application refetches the authoritative user state via `userApi.getMe()`.
- **Debounce**: Multiple rapid turn-finalized events within 750ms are collapsed into a single refetch call to avoid unnecessary network traffic.
- **Single Source of Truth**: The refetched state is merged into the global snapshot using `patchSnapshot`. Components should bind to this global snapshot to ensure they reflect the latest backend state without requiring a full remount.
- **Fire-and-Forget**: The refetch operation is non-blocking and fires on a microtask after the chat UI has painted the final response.

## ChatRuntimeProvider

Manages the live chat state, including message streaming, tool execution timeline, and subagent orchestration. It subscribes to socket events and updates the Redux store.

## SocketProvider

Manages the Socket.IO connection to the Rust core, providing the underlying transport for real-time chat events.
5 changes: 5 additions & 0 deletions app/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export interface User {
languageCode?: string;
waitlist?: string;
activeTeamId: string;
onboarding_status?: {
stages: Record<string, 'pending' | 'active' | 'done' | 'skipped' | 'error'>;
details: Record<string, string>;
finished: boolean;
};
}

// Billing types
Expand Down
Loading