Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/store/persistMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function resetPersistedPanesCacheForTests() {

import { migrateV2ToV3 } from './persistedState.js'

let cachedPersistedLayout: { tabs: any; panes: any; tombstones: any } | null | undefined
let cachedPersistedLayout: { tabs: any; panes: any; tombstones: any; persistedAt?: number } | null | undefined

/**
* Load the combined layout from v3 key, or migrate from v2 keys.
Expand All @@ -87,6 +87,7 @@ export function loadPersistedLayout(): typeof cachedPersistedLayout {
tabs: { tabs: layoutParsed.tabs },
panes: layoutParsed.panes,
tombstones: layoutParsed.tombstones,
persistedAt: typeof layoutParsed.persistedAt === 'number' ? layoutParsed.persistedAt : undefined,
}
return cachedPersistedLayout
}
Expand Down Expand Up @@ -370,6 +371,7 @@ export const persistMiddleware: Middleware<{}, PersistState> = (store) => {
let tabsDirty = false
let panesDirty = false
let flushTimer: ReturnType<typeof setTimeout> | null = null
const sessionPersistedAt = Date.now()

const canUseStorage = () => typeof localStorage !== 'undefined'

Expand Down Expand Up @@ -413,6 +415,7 @@ export const persistMiddleware: Middleware<{}, PersistState> = (store) => {
}

const layoutPayload = {
persistedAt: sessionPersistedAt,
version: LAYOUT_SCHEMA_VERSION,
tabs: {
activeTabId: state.tabs.activeTabId,
Expand Down
2 changes: 2 additions & 0 deletions src/store/persistedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export type ParsedPersistedLayout = {
tabs: z.infer<typeof zPersistedTabsState>
panes: ParsedPersistedPanes
tombstones: Array<{ id: string; deletedAt: number }>
persistedAt?: number
}

export function parsePersistedLayoutRaw(raw: string): ParsedPersistedLayout | null {
Expand Down Expand Up @@ -158,6 +159,7 @@ export function parsePersistedLayoutRaw(raw: string): ParsedPersistedLayout | nu
paneTitleSetByUser: (panes.paneTitleSetByUser || {}) as Record<string, Record<string, boolean>>,
},
tombstones: res.data.tombstones || [],
persistedAt: typeof (res.data as any).persistedAt === 'number' ? (res.data as any).persistedAt : undefined,
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/store/sessionsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ export const sessionsSlice = createSlice({
if (!state.activeSurface || state.activeSurface === action.payload.surface) {
syncTopLevelFromWindow(state, action.payload.surface)
}
// A successful HTTP fetch establishes a valid baseline for WS patches.
if (!state.wsSnapshotReceived) {
state.wsSnapshotReceived = true
}
},
markWsSnapshotReceived: (state) => {
state.wsSnapshotReceived = true
Expand Down
26 changes: 14 additions & 12 deletions src/store/sessionsThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,8 +580,8 @@ export function fetchSessionWindow(args: FetchSessionWindowArgs) {

export function refreshActiveSessionWindow() {
return async (dispatch: AppDispatch, getState: () => RootState) => {
const surface = getState().sessions.activeSurface as SessionSurface | undefined
if (!surface) return
const active = getState().sessions.activeSurface as SessionSurface | undefined
const surface: SessionSurface = active ?? 'sidebar'
const windowState = getState().sessions.windows[surface]
if (!hasCommittedWindowData(windowState)) {
const requestedSearchContext = getRequestedWindowSearchContext(windowState)
Expand All @@ -608,9 +608,11 @@ export function refreshActiveSessionWindow() {
export function queueActiveSessionWindowRefresh() {
return async (dispatch: AppDispatch, getState: () => RootState) => {
const activeSurface = getState().sessions.activeSurface
if (!isSessionSurface(activeSurface)) return
// Default to 'sidebar' if activeSurface hasn't been initialized yet —
// sessions.changed can arrive before bootstrap sets the active surface.
const surface: SessionSurface = isSessionSurface(activeSurface) ? activeSurface : 'sidebar'

const existing = invalidationRefreshState.get(activeSurface)
const existing = invalidationRefreshState.get(surface)
if (existing?.inFlight) {
existing.queued = true
return existing.inFlight
Expand All @@ -621,13 +623,13 @@ export function queueActiveSessionWindowRefresh() {
inFlight: null as Promise<void> | null,
queued: true,
}
invalidationRefreshState.set(activeSurface, state)
invalidationRefreshState.set(surface, state)

const run = (async () => {
try {
while (generation === sessionWindowThunkGeneration) {
const activeRequest = inFlightRequests.get(activeSurface) ?? null
const windowState = getState().sessions.windows[activeSurface]
const activeRequest = inFlightRequests.get(surface) ?? null
const windowState = getState().sessions.windows[surface]
const hasCommittedWindow = hasCommittedWindowData(windowState)

if (!hasCommittedWindow) {
Expand All @@ -643,7 +645,7 @@ export function queueActiveSessionWindowRefresh() {
state.queued = false
const requestedSearchContext = getRequestedWindowSearchContext(windowState)
await dispatch(fetchSessionWindow({
surface: activeSurface,
surface,
priority: 'background',
query: requestedSearchContext.query,
searchTier: requestedSearchContext.searchTier,
Expand All @@ -663,7 +665,7 @@ export function queueActiveSessionWindowRefresh() {
await refreshVisibleSessionWindowSilently({
dispatch,
getState,
surface: activeSurface,
surface,
generation,
identity: getVisibleResultIdentity(windowState),
preserveLoadingState: activeRequest !== null,
Expand All @@ -683,15 +685,15 @@ export function queueActiveSessionWindowRefresh() {
await refreshVisibleSessionWindowSilently({
dispatch,
getState,
surface: activeSurface,
surface,
generation,
identity: getVisibleResultIdentity(windowState),
preserveLoadingState: false,
})
}
} finally {
if (invalidationRefreshState.get(activeSurface) === state) {
invalidationRefreshState.delete(activeSurface)
if (invalidationRefreshState.get(surface) === state) {
invalidationRefreshState.delete(surface)
}
}
})()
Expand Down
8 changes: 7 additions & 1 deletion src/store/tabsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ function loadInitialTabsState(): TabsState {
const tabsState = layout.tabs?.tabs as Partial<TabsState> | undefined
if (!Array.isArray(tabsState?.tabs)) return defaultState

log.debug('Loaded initial state from localStorage:', tabsState.tabs.map((t: Tab) => t.id))
const persistedAt = typeof layout.persistedAt === 'number' ? layout.persistedAt : undefined
const ageMs = persistedAt ? Date.now() - persistedAt : undefined
const ageHours = ageMs ? Math.round(ageMs / 3600000) : undefined
if (ageHours !== undefined && ageHours > 24) {
log.warn(`Restoring tab state from ${ageHours}h ago — may be stale (persistedAt: ${new Date(persistedAt!).toISOString()})`)
}
log.debug('Loaded initial state from localStorage:', tabsState.tabs.map((t: Tab) => t.id), persistedAt ? `(${ageHours}h old)` : '(no timestamp)')

const mappedTabs = tabsState.tabs.map(migrateTabFields)
const desired = tabsState.activeTabId
Expand Down
Loading