44 useState ,
55 useMemo ,
66 useEffect ,
7+ useRef ,
78} from 'react'
89import { ChevronLeft , ChevronRight } from 'lucide-react'
910import { useShallow } from 'zustand/react/shallow'
@@ -17,11 +18,15 @@ import { useSidebar } from '@/components/ui/use-sidebar'
1718import type {
1819 DashboardCheckpointDetailResponse ,
1920 DashboardInteractionSessionDto ,
21+ DashboardInteractionUpdateDto ,
2022} from '@/features/dashboard/api-types'
2123import { CheckpointSheet } from '@/features/dashboard/components/checkpoint-sheet'
2224import type { CheckpointDetailLoadState } from '@/features/dashboard/types'
2325import type { Checkpoint } from '@/features/dashboard/types'
24- import { fetchDashboardCheckpointDetail } from '@/features/dashboard/graphql/fetch-dashboard-data'
26+ import {
27+ fetchDashboardCheckpointDetail ,
28+ subscribeDashboardInteractionUpdates ,
29+ } from '@/features/dashboard/graphql/fetch-dashboard-data'
2530import { QueryExplorerLayout } from '@/features/query-explorer/components/query-explorer'
2631import { EditorHistoryContainer } from '@/features/query-explorer/components/editor-history-container'
2732import { useResizeWidth } from '@/features/query-explorer/hooks/use-resize-width'
@@ -54,6 +59,32 @@ const EDITOR_PANEL_DEFAULT = 780
5459/** Keeps tab panel height stable for at most `SESSIONS_LANDING_PAGE_SIZE` table rows (+ toolbar/chrome). */
5560const SESSIONS_TAB_PANEL_MIN_HEIGHT = 'min-h-[19.5rem]'
5661
62+ /**
63+ * How often we poll for dashboard interaction updates when the WebSocket
64+ * subscription fails. Mirrors the constant in `use-dashboard-data.ts` — kept
65+ * locally to avoid coupling this page to the legacy dashboard hook.
66+ */
67+ const INTERACTION_UPDATES_POLL_INTERVAL_MS = 30_000
68+
69+ /**
70+ * Deterministic identity for a `DashboardInteractionUpdateDto` payload. Two
71+ * payloads with the same key represent the same observable state of the
72+ * server — used to (a) drop the priming event we get right after subscribing
73+ * and (b) ignore redundant re-broadcasts. Mirrors `interactionUpdateKey` in
74+ * `use-dashboard-data.ts`.
75+ */
76+ function interactionUpdateKey ( update : DashboardInteractionUpdateDto ) : string {
77+ return [
78+ update . repo_id ,
79+ update . session_count ,
80+ update . turn_count ,
81+ update . latest_session_id ?? '' ,
82+ update . latest_session_updated_at ?? '' ,
83+ update . latest_turn_id ?? '' ,
84+ update . latest_turn_updated_at ?? '' ,
85+ ] . join ( '|' )
86+ }
87+
5788type SessionsMainTab = 'sessions' | 'checkpoints'
5889
5990export function SessionsView ( ) {
@@ -70,6 +101,8 @@ export function SessionsView() {
70101 minWidth : EDITOR_PANEL_MIN ,
71102 maxWidth : EDITOR_PANEL_MAX ,
72103 } )
104+ // Bumped whenever a dashboard query result lands successfully.
105+ const [ sessionDetailRefreshToken , setSessionDetailRefreshToken ] = useState ( 0 )
73106
74107 const {
75108 setQuery,
@@ -135,6 +168,18 @@ export function SessionsView() {
135168
136169 useSessionsResultSync ( { variables } )
137170
171+ // Force-refresh the session detail sidebar whenever a new query result
172+ // arrives successfully. The table-row data already re-syncs via
173+ // `useSessionsResultSync`, but the sidebar fetches its own detail and is
174+ // keyed by (repoId, sessionId, refreshToken) — bumping the token is what
175+ // tells it to refetch turns/tool-use/token data for the still-selected
176+ // session.
177+ useEffect ( ( ) => {
178+ if ( result . status === 'success' ) {
179+ setSessionDetailRefreshToken ( ( value ) => value + 1 )
180+ }
181+ } , [ result ] )
182+
138183 const parsedVars = parseSessionsVariablesJson ( variables )
139184 const [ resolvedRepoId , setResolvedRepoId ] = useState < string | null > ( null )
140185
@@ -153,6 +198,94 @@ export function SessionsView() {
153198 } )
154199 } , [ sessionsLandingDefaultsApplied , resolvedRepoId ] )
155200
201+ const lastInteractionUpdateKeyRef = useRef < string | null > ( null )
202+ const interactionRefreshInFlightRef = useRef ( false )
203+ const interactionRefreshQueuedRef = useRef ( false )
204+ const [
205+ interactionUpdatesPollingFallback ,
206+ setInteractionUpdatesPollingFallback ,
207+ ] = useState ( false )
208+
209+ useEffect ( ( ) => {
210+ setInteractionUpdatesPollingFallback ( false )
211+ } , [ resolvedRepoId ] )
212+
213+ const refreshInteractionSessions = useCallback ( async ( ) => {
214+ if ( ! resolvedRepoId ) return
215+ if ( interactionRefreshInFlightRef . current ) {
216+ interactionRefreshQueuedRef . current = true
217+ return
218+ }
219+ interactionRefreshInFlightRef . current = true
220+ try {
221+ const state = rootStoreInstance . getState ( )
222+ await runDashboardQueryExplorerQuery ( {
223+ query : state . query ,
224+ variables : state . variables ,
225+ } )
226+ } catch ( error : unknown ) {
227+ console . error (
228+ 'Failed to refresh sessions from interaction subscription' ,
229+ error ,
230+ )
231+ } finally {
232+ interactionRefreshInFlightRef . current = false
233+ if ( interactionRefreshQueuedRef . current ) {
234+ interactionRefreshQueuedRef . current = false
235+ void refreshInteractionSessions ( )
236+ }
237+ }
238+ } , [ resolvedRepoId ] )
239+
240+ const refreshInteractionSessionsRef = useRef ( refreshInteractionSessions )
241+ useEffect ( ( ) => {
242+ refreshInteractionSessionsRef . current = refreshInteractionSessions
243+ } , [ refreshInteractionSessions ] )
244+
245+ useEffect ( ( ) => {
246+ if ( ! resolvedRepoId ) return
247+ if ( interactionUpdatesPollingFallback ) return
248+
249+ lastInteractionUpdateKeyRef . current = null
250+ interactionRefreshInFlightRef . current = false
251+ interactionRefreshQueuedRef . current = false
252+
253+ return subscribeDashboardInteractionUpdates (
254+ { repoId : resolvedRepoId } ,
255+ {
256+ onUpdate : ( update ) => {
257+ const nextKey = interactionUpdateKey ( update )
258+ const previousKey = lastInteractionUpdateKeyRef . current
259+ lastInteractionUpdateKeyRef . current = nextKey
260+
261+ // Priming event (first push) and idempotent re-broadcasts: ignore.
262+ if ( previousKey == null || previousKey === nextKey ) {
263+ return
264+ }
265+
266+ void refreshInteractionSessionsRef . current ( )
267+ } ,
268+ onError : ( error : unknown ) => {
269+ console . warn (
270+ 'Sessions interaction subscription unavailable; falling back to polling' ,
271+ error ,
272+ )
273+ setInteractionUpdatesPollingFallback ( true )
274+ } ,
275+ } ,
276+ )
277+ } , [ resolvedRepoId , interactionUpdatesPollingFallback ] )
278+
279+ useEffect ( ( ) => {
280+ if ( ! interactionUpdatesPollingFallback || ! resolvedRepoId ) return
281+ const timer = window . setInterval ( ( ) => {
282+ void refreshInteractionSessionsRef . current ( )
283+ } , INTERACTION_UPDATES_POLL_INTERVAL_MS )
284+ return ( ) => {
285+ window . clearInterval ( timer )
286+ }
287+ } , [ interactionUpdatesPollingFallback , resolvedRepoId ] )
288+
156289 const checkpointRows = useMemo (
157290 ( ) => deriveDedupedCheckpointsFromSessions ( sessionRows ) ,
158291 [ sessionRows ] ,
@@ -382,6 +515,7 @@ export function SessionsView() {
382515 sessionId = { selectedSessionId }
383516 repoId = { resolvedRepoId }
384517 userName = { userName }
518+ refreshToken = { sessionDetailRefreshToken }
385519 onClose = { ( ) => {
386520 setRightOpen ( false )
387521 } }
0 commit comments