Skip to content

Commit 4679d5e

Browse files
Merge pull request #38 from bitloops/cli-1867-play-button-doesnt-update-existing-session-content
CLI-1867: Fix play button to update existing sessions
2 parents 67d400a + 7595643 commit 4679d5e

1 file changed

Lines changed: 135 additions & 1 deletion

File tree

‎src/features/sessions/sessions-view.tsx‎

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useState,
55
useMemo,
66
useEffect,
7+
useRef,
78
} from 'react'
89
import { ChevronLeft, ChevronRight } from 'lucide-react'
910
import { useShallow } from 'zustand/react/shallow'
@@ -17,11 +18,15 @@ import { useSidebar } from '@/components/ui/use-sidebar'
1718
import type {
1819
DashboardCheckpointDetailResponse,
1920
DashboardInteractionSessionDto,
21+
DashboardInteractionUpdateDto,
2022
} from '@/features/dashboard/api-types'
2123
import { CheckpointSheet } from '@/features/dashboard/components/checkpoint-sheet'
2224
import type { CheckpointDetailLoadState } from '@/features/dashboard/types'
2325
import 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'
2530
import { QueryExplorerLayout } from '@/features/query-explorer/components/query-explorer'
2631
import { EditorHistoryContainer } from '@/features/query-explorer/components/editor-history-container'
2732
import { 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). */
5560
const 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+
5788
type SessionsMainTab = 'sessions' | 'checkpoints'
5889

5990
export 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

Comments
 (0)