Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -371,16 +371,15 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
void orderingRevision;
return new Set([...flowChatState.sessions.keys()].filter(sessionNavStatusService.isRunning));
}, [flowChatState.sessions, orderingRevision]);
const catalogQueryJson = useMemo(() => {
void orderingRevision;
return JSON.stringify(workspaceSessionCatalogQuery(workspaceId, sessionOrdering, sessionShow,
sessionFilters, sourceAgent, flowChatState.sessions.values(), sessionNavStatusService.getSortTimestamp) ?? null);
}, [workspaceId, sessionOrdering, sessionShow, sessionFilters, sourceAgent, flowChatState.sessions, orderingRevision]);
const catalogQueryJson = useMemo(() => JSON.stringify(workspaceSessionCatalogQuery(
workspaceId, sessionOrdering, sessionShow, sessionFilters, sourceAgent,
) ?? null), [workspaceId, sessionOrdering, sessionShow, sessionFilters, sourceAgent]);
const captureCatalogQuery = useCallback((query: SessionCatalogQuery) => captureWorkspaceSessionCatalogQuery(
query, flowChatStore.getState().sessions.values(), sessionNavStatusService.getSortTimestamp,
), []);
// Capture the view's observed order only when beginning a catalog. Hydration
// and streaming must not restart it; a refresh or device switch must rebind it.
// Capture the view's observed order only when beginning a catalog. Hydration,
// streaming and run starts must not restart it; a refresh or device switch
// must rebind it.
const catalogQuery = useMemo(() => {
void catalogRefreshVersion;
void initialMetadataKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ describe('workspace catalog query selection', () => {
it('pins an already-observed timestamp even while it still matches local metadata', () => {
const observed = session('observed', { lastFinishedAt: 10 });
const viewQuery = workspaceSessionCatalogQuery('main', 'updated', 'all',
DEFAULT_WORKSPACE_SESSION_FILTERS, 'all', [observed], recency);
DEFAULT_WORKSPACE_SESSION_FILTERS, 'all');
const query = captureWorkspaceSessionCatalogQuery(viewQuery!, [observed], recency);
// The host can have finished another turn since this local record loaded.
// Its new timestamp must not choose a different page than the frozen view.
expect(query?.sortTimestamps).toEqual({ observed: 10 });
expect(viewQuery?.sortTimestamps).toBeUndefined();

const hydrated = session('newly-loaded', { lastFinishedAt: 9 });
const afterHydration = workspaceSessionCatalogQuery('main', 'updated', 'all',
DEFAULT_WORKSPACE_SESSION_FILTERS, 'all', [observed, hydrated], recency);
expect(sessionCatalogQueryKey(afterHydration)).toBe(sessionCatalogQueryKey(viewQuery));
expect(captureWorkspaceSessionCatalogQuery(query, [observed, hydrated], recency).sortTimestamps)
.toEqual({ observed: 10, 'newly-loaded': 9 });
});
Expand All @@ -47,16 +44,16 @@ describe('workspace catalog query selection', () => {
it('uses bounded paging for the default view and every source selector', () => {
for (const source of ['all', 'openbitfun', 'codex', 'zcode']) {
expect(workspaceSessionCatalogQuery('main', DEFAULT_WORKSPACE_SESSION_VIEW.ordering, 'all',
DEFAULT_WORKSPACE_SESSION_FILTERS, source, [], recency)).toEqual({ workspaceId: 'main', ordering: 'updated', source });
DEFAULT_WORKSPACE_SESSION_FILTERS, source)).toEqual({ workspaceId: 'main', ordering: 'updated', source });
}
});

it('retains the complete legacy projection for name, status, unread, attention and every richer filter', () => {
for (const ordering of ['name', 'status'] as const) {
expect(workspaceSessionCatalogQuery('main', ordering, 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all', [], recency)).toBeUndefined();
expect(workspaceSessionCatalogQuery('main', ordering, 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all')).toBeUndefined();
}
for (const show of ['unread', 'attention'] as const) {
expect(workspaceSessionCatalogQuery('main', 'updated', show, DEFAULT_WORKSPACE_SESSION_FILTERS, 'codex', [], recency)).toBeUndefined();
expect(workspaceSessionCatalogQuery('main', 'updated', show, DEFAULT_WORKSPACE_SESSION_FILTERS, 'codex')).toBeUndefined();
}
const filters = [
{ hiddenStatuses: ['running'] }, { hiddenEnvironments: ['remote'] },
Expand All @@ -65,21 +62,31 @@ describe('workspace catalog query selection', () => {
for (const filter of filters) {
expect(workspaceSessionCatalogQuery('main', 'updated', 'all',
{ ...DEFAULT_WORKSPACE_SESSION_FILTERS, ...filter } as typeof DEFAULT_WORKSPACE_SESSION_FILTERS,
'all', [], recency)).toBeUndefined();
'all')).toBeUndefined();
}
});

it('preserves observed run ordering only within the owning workspace, including isolated worktrees', () => {
it('does not restart the view when a session starts running', () => {
// The running row moves ahead of rows whose relative order is unchanged,
// so the host pages already read for this view remain valid.
expect(sessionCatalogQueryKey(workspaceSessionCatalogQuery('main', 'updated', 'all',
DEFAULT_WORKSPACE_SESSION_FILTERS, 'all'))).toBe(sessionCatalogQueryKey({
workspaceId: 'main', ordering: 'updated', source: 'all',
}));
});

it('pins run ordering only within the owning workspace, including isolated worktrees', () => {
const sessions = [session('historical'), session('running'), session('peer', { workspaceId: 'other' }),
session('isolated', { workspaceId: 'worktree', projectWorkspaceId: 'main', config: { executionTarget: {
kind: 'managedWorktree', rootPath: '/project/.worktrees/one',
} } }), session('linked', { workspaceId: 'worktree', projectWorkspaceId: 'main' })];
const getTimestamp = (value: Session) => value.sessionId === 'historical' ? recency(value) : 100;
const query = workspaceSessionCatalogQuery('main', 'updated', 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all', sessions, getTimestamp);
expect(query?.sortTimestamps).toEqual({ running: 100, isolated: 100 });
const query = workspaceSessionCatalogQuery('main', 'updated', 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all');
expect(captureWorkspaceSessionCatalogQuery(query!, sessions, getTimestamp).sortTimestamps)
.toEqual({ historical: 20, running: 100, isolated: 100 });
expect(workspaceSessionCatalogQuery('main', 'created', 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all', sessions, getTimestamp)?.sortTimestamps).toBeUndefined();
expect(sessionCatalogQueryKey(query)).toBe(sessionCatalogQueryKey({ ...query!, sortTimestamps: { isolated: 100, running: 100 } }));
const created = workspaceSessionCatalogQuery('main', 'created', 'all', DEFAULT_WORKSPACE_SESSION_FILTERS, 'all');
expect(captureWorkspaceSessionCatalogQuery(created!, sessions, getTimestamp).sortTimestamps).toBeUndefined();
expect(sessionCatalogQueryKey({ ...query!, sortTimestamps: { running: 100, isolated: 100 } }))
.toBe(sessionCatalogQueryKey({ ...query!, sortTimestamps: { isolated: 100, running: 100 } }));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,19 @@ export function workspaceSessionCatalogTotal(visibleCount: number, catalog: { pa
? (catalog.total ?? 0) + visibleCount - (catalog.knownCount ?? visibleCount) : 0);
}

/** Select supported views and the ordering changes that restart their reader.
* Newly hydrated rows alone must not invalidate an in-progress catalog. */
/** Select supported views. Only a view change restarts their reader: a session
* that starts running moves ahead of rows whose relative order is unchanged,
* so the local projection reorders it without discarding the host pages. */
export function workspaceSessionCatalogQuery(
workspaceId: string | undefined,
ordering: WorkspaceSessionOrdering,
show: WorkspaceSessionShow,
filters: WorkspaceSessionFilters,
source: string,
sessions: Iterable<Session>,
getSortTimestamp: (session: Session) => number,
): SessionCatalogQuery | undefined {
if (!workspaceId || (ordering !== 'updated' && ordering !== 'created')
|| show !== 'all' || hasWorkspaceSessionFilters(filters)) return;
const sortTimestamps: Record<string, number> = {};
if (ordering === 'updated') {
for (const session of sessions) {
if (!sessionBelongsToWorkspaceNavRow(session, workspaceId)) continue;
const timestamp = getSortTimestamp(session);
if (timestamp !== (session.lastFinishedAt ?? session.createdAt)) {
sortTimestamps[session.sessionId] = timestamp;
}
}
}
return { workspaceId, ordering, source,
...(Object.keys(sortTimestamps).length ? { sortTimestamps } : {}),
};
return { workspaceId, ordering, source };
}

/** Bind every observed timestamp before reading a new host catalog. Matching
Expand Down