Skip to content

Commit 1fe6f59

Browse files
murrayjuOx Agent
andauthored
Preserve SessionsList filters across screen switches (#107)
## Summary - move SessionsList filter text, filter mode, and scope mode into the Zustand session store - keep repo-aware scope initialization so switching screens does not reset the current filter state - add store tests covering the new filter and scope behavior ## Testing - ./bun run check --------- Co-authored-by: Ox Agent <ox@tigerdata.com>
1 parent 543e8f2 commit 1fe6f59

File tree

3 files changed

+123
-12
lines changed

3 files changed

+123
-12
lines changed

src/components/SessionsList.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
import { useBackgroundTaskStore } from '../stores/backgroundTaskStore';
2525
import { useRepoStore } from '../stores/repoStore.ts';
2626
import { useRouterStore } from '../stores/routerStore.ts';
27-
import { useSessionStore } from '../stores/sessionStore';
27+
import {
28+
type FilterMode,
29+
type ScopeMode,
30+
useSessionStore,
31+
} from '../stores/sessionStore';
2832
import { useSessionWorkflowStore } from '../stores/sessionWorkflowStore.ts';
2933
import { useTheme } from '../stores/themeStore';
3034
import { useToastStore } from '../stores/toastStore';
@@ -37,9 +41,6 @@ import { SessionDetailPanel } from './SessionDetailPanel.tsx';
3741
/** Cache TTL in milliseconds (60 seconds) */
3842
const PR_CACHE_TTL = 60_000;
3943

40-
export type FilterMode = 'all' | 'running' | 'completed';
41-
export type ScopeMode = 'local' | 'global';
42-
4344
const FILTER_LABELS: Record<FilterMode, string> = {
4445
all: 'All',
4546
running: 'Running',
@@ -69,6 +70,13 @@ export function SessionsList() {
6970
clearPrCache,
7071
addPendingDelete,
7172
removePendingDelete,
73+
filterText,
74+
setFilterText,
75+
filterMode,
76+
setFilterMode,
77+
scopeMode,
78+
setScopeMode,
79+
syncScopeModeWithRepo,
7280
} = useSessionStore();
7381
const [sessions, setSessions] = useState<OxSession[]>([]);
7482
// Ref to hold the latest sessions list so callbacks/effects can read it
@@ -79,12 +87,6 @@ export function SessionsList() {
7987
// Null until the first load completes (avoids false notifications on startup).
8088
const prevSessionStatusesRef = useRef<Map<string, string> | null>(null);
8189
const [loading, setLoading] = useState(true);
82-
const [filterText, setFilterText] = useState('');
83-
const [filterMode, setFilterMode] = useState<FilterMode>('all');
84-
// Default to 'local' if in a repo, otherwise 'global'
85-
const [scopeMode, setScopeMode] = useState<ScopeMode>(
86-
currentRepo ? 'local' : 'global',
87-
);
8890
const [deleteModal, setDeleteModal] = useState<OxSession | null>(null);
8991
const [stopModal, setStopModal] = useState<OxSession | null>(null);
9092
const [actionInProgress, setActionInProgress] = useState(false);
@@ -115,6 +117,10 @@ export function SessionsList() {
115117
);
116118
const containerStats = useContainerStats(runningIds, getStats);
117119

120+
useEffect(() => {
121+
syncScopeModeWithRepo(!!currentRepo);
122+
}, [currentRepo, syncScopeModeWithRepo]);
123+
118124
// Filter sessions: first by scope/mode, then fuzzy text search
119125
const filteredSessions = useMemo(() => {
120126
// Pre-filter by scope and mode (boolean filters)
@@ -233,14 +239,14 @@ export function SessionsList() {
233239
const nextIdx = (currentIdx + 1) % FILTER_ORDER.length;
234240
const nextMode = FILTER_ORDER[nextIdx];
235241
if (nextMode) setFilterMode(nextMode);
236-
}, [filterMode]);
242+
}, [filterMode, setFilterMode]);
237243

238244
const toggleScope = useCallback(() => {
239245
const currentIdx = SCOPE_ORDER.indexOf(scopeMode);
240246
const nextIdx = (currentIdx + 1) % SCOPE_ORDER.length;
241247
const nextScope = SCOPE_ORDER[nextIdx];
242248
if (nextScope) setScopeMode(nextScope);
243-
}, [scopeMode]);
249+
}, [scopeMode, setScopeMode]);
244250

245251
// Delete session handler
246252
const handleDelete = useCallback(() => {

src/stores/sessionStore.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import { afterEach, describe, expect, test } from 'bun:test';
22
import { useSessionStore } from './sessionStore';
33

4+
afterEach(() => {
5+
useSessionStore.setState({
6+
selectedSessionId: null,
7+
filterText: '',
8+
filterMode: 'all',
9+
scopeMode: 'global',
10+
prCache: {},
11+
pendingDeletes: new Set(),
12+
});
13+
});
14+
415
describe('sessionStore - pendingDeletes', () => {
516
afterEach(() => {
617
// Reset pendingDeletes
@@ -42,3 +53,43 @@ describe('sessionStore - pendingDeletes', () => {
4253
expect(useSessionStore.getState().isPendingDelete('c')).toBe(true);
4354
});
4455
});
56+
57+
describe('sessionStore - filters', () => {
58+
test('stores filter text', () => {
59+
useSessionStore.getState().setFilterText('bugfix');
60+
expect(useSessionStore.getState().filterText).toBe('bugfix');
61+
});
62+
63+
test('composes consecutive filter text updates from the latest state', () => {
64+
const { setFilterText } = useSessionStore.getState();
65+
66+
setFilterText((prev) => `${prev}a`);
67+
setFilterText((prev) => `${prev}b`);
68+
69+
expect(useSessionStore.getState().filterText).toBe('ab');
70+
});
71+
72+
test('stores filter mode', () => {
73+
useSessionStore.getState().setFilterMode('running');
74+
expect(useSessionStore.getState().filterMode).toBe('running');
75+
});
76+
77+
test('syncScopeModeWithRepo defaults to local when entering a repo', () => {
78+
useSessionStore.getState().syncScopeModeWithRepo(true);
79+
expect(useSessionStore.getState().scopeMode).toBe('local');
80+
});
81+
82+
test('syncScopeModeWithRepo resets to global when leaving a repo', () => {
83+
const store = useSessionStore.getState();
84+
store.setScopeMode('local');
85+
store.syncScopeModeWithRepo(false);
86+
expect(useSessionStore.getState().scopeMode).toBe('global');
87+
});
88+
89+
test('syncScopeModeWithRepo preserves explicit local scope in a repo', () => {
90+
const store = useSessionStore.getState();
91+
store.setScopeMode('local');
92+
store.syncScopeModeWithRepo(true);
93+
expect(useSessionStore.getState().scopeMode).toBe('local');
94+
});
95+
});

src/stores/sessionStore.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import type { PrInfo } from '../services/github';
99
// Types
1010
// ============================================================================
1111

12+
export type FilterMode = 'all' | 'running' | 'completed';
13+
export type ScopeMode = 'local' | 'global';
14+
type StateUpdater<T> = T | ((prev: T) => T);
15+
1216
export interface PrCacheEntry {
1317
prInfo: PrInfo | null;
1418
lastChecked: number; // Date.now() timestamp
@@ -25,6 +29,27 @@ export interface SessionState {
2529
/** Set the selected session ID */
2630
setSelectedSessionId: (id: string | null) => void;
2731

32+
/** Free-text filter applied to the sessions list */
33+
filterText: string;
34+
35+
/** Set the free-text filter */
36+
setFilterText: (text: StateUpdater<string>) => void;
37+
38+
/** Status filter applied to the sessions list */
39+
filterMode: FilterMode;
40+
41+
/** Set the status filter */
42+
setFilterMode: (mode: FilterMode) => void;
43+
44+
/** Repo scope applied to the sessions list */
45+
scopeMode: ScopeMode;
46+
47+
/** Set the repo scope */
48+
setScopeMode: (mode: ScopeMode) => void;
49+
50+
/** Initialize scope based on whether a repo is currently active */
51+
syncScopeModeWithRepo: (hasCurrentRepo: boolean) => void;
52+
2853
/** PR info cache keyed by session ID (containerId) */
2954
prCache: Record<string, PrCacheEntry>;
3055

@@ -52,12 +77,41 @@ export interface SessionState {
5277

5378
export const useSessionStore = create<SessionState>()((set, get) => ({
5479
selectedSessionId: null,
80+
filterText: '',
81+
filterMode: 'all',
82+
scopeMode: 'global',
5583
prCache: {},
5684

5785
setSelectedSessionId: (id: string | null) => {
5886
set({ selectedSessionId: id });
5987
},
6088

89+
setFilterText: (text: StateUpdater<string>) => {
90+
set((state) => ({
91+
filterText:
92+
typeof text === 'function'
93+
? (text as (prev: string) => string)(state.filterText)
94+
: text,
95+
}));
96+
},
97+
98+
setFilterMode: (mode: FilterMode) => {
99+
set({ filterMode: mode });
100+
},
101+
102+
setScopeMode: (mode: ScopeMode) => {
103+
set({ scopeMode: mode });
104+
},
105+
106+
syncScopeModeWithRepo: (hasCurrentRepo: boolean) => {
107+
const { scopeMode } = get();
108+
if (hasCurrentRepo && scopeMode === 'global') {
109+
set({ scopeMode: 'local' });
110+
} else if (!hasCurrentRepo && scopeMode === 'local') {
111+
set({ scopeMode: 'global' });
112+
}
113+
},
114+
61115
setPrInfo: (sessionId: string, prInfo: PrInfo | null) => {
62116
set((state) => ({
63117
prCache: {

0 commit comments

Comments
 (0)