perf(ui): index and batch session updates

Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once.

Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
This commit is contained in:
c_w_xiaohei
2026-08-26 00:42:37 +08:00
parent df924aa532
commit 701173e399
16 changed files with 1153 additions and 376 deletions
+68 -56
View File
@@ -2,15 +2,16 @@ import { create } from 'zustand';
import type { Event, SessionStatus } from '@opencode-ai/sdk/v2/client';
import { normalizeProjectPath } from '@/lib/projectResolution';
import {
observeSessionActivityEvent,
applySessionOrderingMutations,
reconcileSessionActivitySnapshot,
removeSessionOrdering,
type SessionOrderingMutation,
} from './session-ordering';
import {
observeSessionActivityTiming,
applySessionActivityTimingMutations,
reconcileSessionActivityTiming,
removeSessionActivityTiming,
type SessionActivityTimingMutation,
} from './session-activity-timing';
import { countSyncPerformance } from './performance-diagnostics';
// Shared live busy/retry index for every directory. Global events update it
// incrementally and authoritative directory snapshots reconcile it, so each
@@ -37,6 +38,7 @@ const initialState: GlobalSessionStatusState = {
};
export const useGlobalSessionStatusStore = create<GlobalSessionStatusState>(() => initialState);
useGlobalSessionStatusStore.subscribe(() => countSyncPerformance('globalStatusPublications'));
// Runtime switching currently replaces statusById directly. Keep that boundary
// synchronized without making normal status mutations derive membership again.
@@ -110,74 +112,84 @@ const statusesEqual = (left: SessionStatus, right: SessionStatus): boolean => (
const normalizeDirectory = (directory: string): string =>
normalizeProjectPath(directory) ?? directory;
const setStatus = (sessionId: string, directory: string, status: SessionStatus | { type: 'idle' }): void => {
useGlobalSessionStatusStore.setState((state) => {
const current = state.statusById.get(sessionId);
if (status.type === 'idle') {
if (!current) return state;
const next = new Map(state.statusById);
next.delete(sessionId);
const nextActiveSessionIds = new Set(state.activeSessionIds);
nextActiveSessionIds.delete(sessionId);
return { statusById: next, activeSessionIds: nextActiveSessionIds };
}
if (current && current.directory === directory && statusesEqual(current.status, status)) return state;
const next = new Map(state.statusById);
next.set(sessionId, { status, directory });
if (current) return { statusById: next };
const nextActiveSessionIds = new Set(state.activeSessionIds);
nextActiveSessionIds.add(sessionId);
return { statusById: next, activeSessionIds: nextActiveSessionIds };
});
};
// Event-driven path: called by the sync dispatcher for status-bearing events
// whose directory has no child store. Mirrors the child reducer's semantics
// (`session.idle` / `session.error` both resolve to idle).
export const applyGlobalSessionStatusEvent = (directory: string, payload: Event): void => {
switch (payload.type) {
case 'session.status': {
export const applyGlobalSessionStatusEvents = (directory: string, payloads: readonly Event[]): void => {
if (payloads.length === 0) return;
const normalizedDirectory = normalizeDirectory(directory);
const state = useGlobalSessionStatusStore.getState();
let statusById: Map<string, GlobalSessionStatusEntry> | null = null;
let activeSessionIds: Set<string> | null = null;
const orderingMutations: SessionOrderingMutation[] = [];
const timingMutations: SessionActivityTimingMutation[] = [];
const currentStatuses = (): ReadonlyMap<string, GlobalSessionStatusEntry> => statusById ?? state.statusById;
const draftStatuses = (): Map<string, GlobalSessionStatusEntry> => (statusById ??= new Map(state.statusById));
const draftActiveIds = (): Set<string> => (activeSessionIds ??= new Set(state.activeSessionIds));
const settle = (sessionId: string): void => {
if (currentStatuses().has(sessionId)) {
draftStatuses().delete(sessionId);
draftActiveIds().delete(sessionId);
}
orderingMutations.push({ type: 'observe', sessionId, phase: 'settled' });
timingMutations.push({ type: 'observe', sessionId, phase: 'settled' });
};
for (const payload of payloads) {
if (payload.type === 'session.status') {
// SAFETY: OpenCode event properties for this event contain the optional session ID and status payload.
const props = payload.properties as { sessionID?: string; status?: { type?: string } } | undefined;
if (typeof props?.sessionID !== 'string' || !props.sessionID) return;
if (typeof props?.sessionID !== 'string' || !props.sessionID) continue;
const type = normalizeStatusType(props.status?.type);
setStatus(
props.sessionID,
normalizeDirectory(directory),
type === 'idle' ? { type: 'idle' } : ( // SAFETY: the normalized discriminator is busy or retry.
{ ...(props.status ?? {}), type } as SessionStatus
),
);
observeSessionActivityEvent(props.sessionID, type === 'idle' ? 'settled' : 'active');
// `retry` is still a running turn, so the elapsed counter keeps going.
observeSessionActivityTiming(props.sessionID, type === 'idle' ? 'settled' : 'active');
return;
if (type === 'idle') {
settle(props.sessionID);
continue;
}
// SAFETY: the normalized discriminator is one of the SDK's active status types.
const status = { ...(props.status ?? {}), type } as SessionStatus;
const current = currentStatuses().get(props.sessionID);
if (!current || current.directory !== normalizedDirectory || !statusesEqual(current.status, status)) {
draftStatuses().set(props.sessionID, { status, directory: normalizedDirectory });
if (!current) draftActiveIds().add(props.sessionID);
}
orderingMutations.push({ type: 'observe', sessionId: props.sessionID, phase: 'active' });
timingMutations.push({ type: 'observe', sessionId: props.sessionID, phase: 'active' });
continue;
}
case 'session.idle':
case 'session.error': {
if (payload.type === 'session.idle' || payload.type === 'session.error') {
// SAFETY: OpenCode terminal event properties contain the optional addressed session ID.
const props = payload.properties as { sessionID?: string } | undefined;
if (typeof props?.sessionID === 'string' && props.sessionID) {
setStatus(props.sessionID, normalizeDirectory(directory), { type: 'idle' });
observeSessionActivityEvent(props.sessionID, 'settled');
observeSessionActivityTiming(props.sessionID, 'settled');
}
return;
if (typeof props?.sessionID === 'string' && props.sessionID) settle(props.sessionID);
continue;
}
case 'session.deleted': {
if (payload.type === 'session.deleted') {
// SAFETY: OpenCode deletion event properties identify the deleted session directly or through info.id.
const props = payload.properties as { sessionID?: string; info?: { id?: string } } | undefined;
const sessionId = props?.sessionID ?? props?.info?.id;
if (sessionId) {
setStatus(sessionId, normalizeDirectory(directory), { type: 'idle' });
removeSessionOrdering(sessionId);
removeSessionActivityTiming(sessionId);
if (!sessionId) continue;
if (currentStatuses().has(sessionId)) {
draftStatuses().delete(sessionId);
draftActiveIds().delete(sessionId);
}
return;
orderingMutations.push({ type: 'remove', sessionId });
timingMutations.push({ type: 'remove', sessionId });
}
default:
return;
}
if (statusById) {
useGlobalSessionStatusStore.setState({
statusById,
activeSessionIds: activeSessionIds ?? state.activeSessionIds,
});
}
applySessionOrderingMutations(orderingMutations);
applySessionActivityTimingMutations(timingMutations);
};
export const applyGlobalSessionStatusEvent = (directory: string, payload: Event): void => {
applyGlobalSessionStatusEvents(directory, [payload]);
};
// Polled path: an authoritative `/session/status?directory=X` snapshot. Entries