ref(seer): Unify explorer state with shared context and fix optimistic updates#115579
ref(seer): Unify explorer state with shared context and fix optimistic updates#115579JonasBa wants to merge 14 commits into
Conversation
Introduce SeerExplorerStateProvider that sits on top of the sessions query, enriching each session with an active/idle status derived from a reducer. Seeds the active run from sessionStorage on mount and persists changes back on dispatch. This is the first step toward replacing useSeerExplorerRunId with a unified context. Co-Authored-By: Claude <noreply@anthropic.com>
…ns providers Consumers that only dispatch (e.g. drawer) won't re-render when conversations change, and read-only consumers (e.g. topBar) won't re-render on dispatch. Co-Authored-By: Claude <noreply@anthropic.com>
Migrate all four consumers of useSeerExplorerRunId to the new SeerExplorerStateProvider. The active run is now derived from the enriched conversations list instead of a separate sessionStorage-backed hook, eliminating the implicit coupling between run ID and session data. Co-Authored-By: Claude <noreply@anthropic.com>
When a new session is created, optimistically prepend it to the sessions query cache so the conversations list picks it up immediately. Without this, the active run ID had no matching session until the list query refetched, causing a momentary loss of active state. Also ensure set active run resets all other runs to idle and move persistence side effects out of the reducer into a dispatch wrapper to keep the reducer pure. Co-Authored-By: Claude <noreply@anthropic.com>
Extract makeSeerExplorerSessionsQueryOptions so the sessions query key is derived from a single source. The previous hand-crafted key missed the query params, writing to a phantom cache entry and causing the new conversation to briefly disappear after creation. Co-Authored-By: Claude <noreply@anthropic.com>
Replaced by useSeerExplorerConversations from the shared state context. Co-Authored-By: Claude <noreply@anthropic.com>
📊 Type Coverage Diff
🔍 1 new type safety issue introduced
This is informational only and does not block the PR. |
| if (next[action.payload]) { | ||
| next[action.payload] = {...next[action.payload], status: 'active'}; | ||
| } |
There was a problem hiding this comment.
Bug: The 'set active run' reducer fails to add a new conversation's run_id to the state because it only updates runs that already exist, preventing new conversations from being marked as active.
Severity: HIGH
Suggested Fix
Modify the 'set active run' reducer to handle cases where the run_id does not yet exist in the state. If next[action.payload] is falsy, create a new entry for the run and set it as active. This will ensure that newly created conversations are correctly added to the state and marked as the active conversation.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: static/app/views/seerExplorer/seerExplorerStateContext.tsx#L22-L24
Potential issue: When a new conversation is created in the Seer Explorer, the 'set
active run' action is dispatched. The corresponding reducer attempts to update the
state, but it only modifies entries that already exist. For a new conversation, its
`run_id` is not yet present in the state object. The condition `if
(next[action.payload])` evaluates to false, causing the reducer to do nothing.
Consequently, the new run is never added to the state or marked as active, and the
derived `runId` remains `null`, breaking the user flow for creating new conversations.
Did we get this right? 👍 / 👎 to inform future reviews.
Fold seerExplorerStateContext into seerExplorerSessionContext so there
is a single provider that owns the query, reducer, and merge logic.
The context value is now {query, conversations}, letting consumers
destructure what they need. Removes a provider nesting layer from
the org layout.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Tests that depend on runId now wait for the sessions query to resolve before calling functions like interruptRun that require a non-null runId. Also fixes an incomplete mock response for the PR states polling test. Co-Authored-By: Claude <noreply@anthropic.com>
wrappedDispatch was a plain function recreated every render, causing all dispatch context consumers to re-render unnecessarily and cascading callback instability through useSeerExplorerDrawer and useSeerExplorer. Co-Authored-By: Claude <noreply@anthropic.com>
The sessions query is capped at 20 results. If the active run (from sessionStorage or switchToRun) isn't in that window, conversations never included it and runId silently resolved to null. Synthesize a minimal entry so runId always resolves from reducer state. Co-Authored-By: Claude <noreply@anthropic.com>
| const [runId, setRunId] = useSeerExplorerRunId(); | ||
| const {conversations} = useSeerExplorerSessions(); | ||
| const dispatch = useSeerExplorerDispatch(); | ||
| const runId = conversations.find(c => c.status === 'active')?.run_id ?? null; |
There was a problem hiding this comment.
Repeated active run derivation across three files
Low Severity
The expression conversations.find(c => c.status === 'active')?.run_id ?? null is duplicated identically in topBar.tsx, useSeerExplorer.tsx, and useSeerExplorerContext.tsx. Since the conversations memo in the provider already computes activeId internally, exposing it as part of the context value (or as a small helper) would eliminate the redundancy and reduce the risk of these three sites drifting apart if the active-run logic changes.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 9baff31. Configure here.
Synthetic conversation entries (injected when active run is outside the API's top N) have empty titles and timestamps, producing broken menu items and Invalid Date in TimeSince. Co-Authored-By: Claude <noreply@anthropic.com>
Runs can only be activated via the session menu or openSeerExplorerDrawer, both sourced from query results. A persisted runId not found in the session list is treated as invalid — runId resolves to null and the user starts fresh. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9685060. Configure here.
| ...session, | ||
| status: state[session.run_id]?.status ?? ('idle' as const), | ||
| })); | ||
| }, [query.data?.data, state]); |
There was a problem hiding this comment.
Duplicated active run derivation across three files
Low Severity
The expression conversations.find(c => c.status === 'active')?.run_id ?? null is duplicated in three separate files (topBar.tsx, useSeerExplorer.tsx, useSeerExplorerContext.tsx). Since the PR specifically centralizes session state into SeerExplorerSessionsProvider, the active run ID could be computed once inside the provider and exposed alongside conversations in the context value, eliminating the repeated derivation.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 9685060. Configure here.


Centralizes active run id logic inside the seer context allowing us to remove the hook sync mechanism.
sequenceDiagram participant U as User participant L as Local State<br/>(reducer + sessionStorage) participant S as Server<br/>(query cache) Note over L: Init: sessionStorage → reducer seed rect rgb(240, 240, 255) Note over U,S: Page load S->>S: useQuery /explorer-runs/ S-->>L: ExplorerSession[] L->>L: merge: sessions.map(s => {…s, status: reducer[s.id] ?? idle}) L-->>U: {query, conversations} end rect rgb(240, 255, 240) Note over U,S: Switch to existing run U->>L: dispatch('set active run', 123) L->>L: reducer marks 123 active, rest idle L-->>L: write-through → sessionStorage L->>L: re-merge with existing query data L-->>U: runId = 123 end rect rgb(255, 245, 235) Note over U,S: New conversation U->>S: POST /explorer-chat/ {query} S-->>L: onSuccess → {run_id: 456} L->>L: ① dispatch('set active run', 456) L-->>L: write-through → sessionStorage L->>S: ② setApiQueryData: prepend new session to cache Note over L: merge runs — new session appears<br/>with status: active in one render L-->>U: runId = 456, conversation visible end