perf(ui): streamline session sidebar state

This commit is contained in:
c_w_xiaohei
2026-08-26 00:42:36 +08:00
parent 3b7d5e1c34
commit 26dbc2f309
84 changed files with 5159 additions and 2809 deletions
+102 -8
View File
@@ -26,13 +26,75 @@ type GlobalSessionStatusEntry = { status: SessionStatus; directory: string };
type GlobalSessionStatusState = {
statusById: Map<string, GlobalSessionStatusEntry>;
activeSessionIds: ReadonlySet<string>;
};
export const useGlobalSessionStatusStore = create<GlobalSessionStatusState>(() => ({
statusById: new Map(),
}));
const EMPTY_ACTIVE_SESSION_IDS: ReadonlySet<string> = new Set();
const normalizeStatusType = (type: unknown): ActiveStatusType | 'idle' => {
const initialState: GlobalSessionStatusState = {
statusById: new Map(),
activeSessionIds: EMPTY_ACTIVE_SESSION_IDS,
};
export const useGlobalSessionStatusStore = create<GlobalSessionStatusState>(() => initialState);
// Runtime switching currently replaces statusById directly. Keep that boundary
// synchronized without making normal status mutations derive membership again.
const storeSetState = useGlobalSessionStatusStore.setState;
type GlobalSessionStatusStateUpdate = GlobalSessionStatusState
| Partial<GlobalSessionStatusState>
| ((state: GlobalSessionStatusState) => GlobalSessionStatusState | Partial<GlobalSessionStatusState>);
function setSynchronizedState(
partial: GlobalSessionStatusStateUpdate,
replace?: false,
): void;
function setSynchronizedState(
partial: GlobalSessionStatusState | ((state: GlobalSessionStatusState) => GlobalSessionStatusState),
replace: true,
): void;
function setSynchronizedState(partial: GlobalSessionStatusStateUpdate, replace?: boolean): void {
if (partial instanceof Function) {
if (replace === true) {
// SAFETY: Zustand's `replace: true` overload only accepts a complete state or a complete-state updater.
storeSetState(partial as GlobalSessionStatusState | ((state: GlobalSessionStatusState) => GlobalSessionStatusState), true);
} else {
storeSetState(partial, replace);
}
return;
}
if (partial.statusById === undefined || partial.activeSessionIds) {
if (replace === true) {
// SAFETY: Zustand's `replace: true` overload only accepts a complete state or a complete-state updater.
storeSetState(partial as GlobalSessionStatusState, true);
} else {
storeSetState(partial, replace);
}
return;
}
const nextStatusById = partial.statusById;
const current = useGlobalSessionStatusStore.getState();
const nextActiveSessionIds = new Set<string>();
for (const [sessionId, entry] of nextStatusById) {
if (entry.status.type === 'busy' || entry.status.type === 'retry') {
nextActiveSessionIds.add(sessionId);
}
}
const sameMembership = nextActiveSessionIds.size === current.activeSessionIds.size
&& [...nextActiveSessionIds].every((sessionId) => current.activeSessionIds.has(sessionId));
const nextState = {
...current,
...partial,
activeSessionIds: sameMembership ? current.activeSessionIds : nextActiveSessionIds,
};
if (replace === true) storeSetState(nextState, true);
else storeSetState(nextState, replace);
}
useGlobalSessionStatusStore.setState = setSynchronizedState;
const normalizeStatusType = (type: string | undefined): ActiveStatusType | 'idle' => {
if (type === 'busy') return 'busy';
if (type === 'retry') return 'retry';
return 'idle';
@@ -55,12 +117,17 @@ const setStatus = (sessionId: string, directory: string, status: SessionStatus |
if (!current) return state;
const next = new Map(state.statusById);
next.delete(sessionId);
return { statusById: next };
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 });
return { statusById: next };
if (current) return { statusById: next };
const nextActiveSessionIds = new Set(state.activeSessionIds);
nextActiveSessionIds.add(sessionId);
return { statusById: next, activeSessionIds: nextActiveSessionIds };
});
};
@@ -70,13 +137,16 @@ const setStatus = (sessionId: string, directory: string, status: SessionStatus |
export const applyGlobalSessionStatusEvent = (directory: string, payload: Event): void => {
switch (payload.type) {
case '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;
const type = normalizeStatusType(props.status?.type);
setStatus(
props.sessionID,
normalizeDirectory(directory),
type === 'idle' ? { type: 'idle' } : { ...(props.status ?? {}), type } as SessionStatus,
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.
@@ -85,6 +155,7 @@ export const applyGlobalSessionStatusEvent = (directory: string, payload: Event)
}
case 'session.idle':
case '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' });
@@ -94,9 +165,11 @@ export const applyGlobalSessionStatusEvent = (directory: string, payload: Event)
return;
}
case '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);
}
@@ -137,10 +210,25 @@ export const applyGlobalSessionStatusSnapshot = (
useGlobalSessionStatusStore.setState((state) => {
let changed = false;
const next = new Map(state.statusById);
let nextActiveSessionIds: Set<string> | null = null;
const hasActiveSession = (sessionId: string): boolean => (
(nextActiveSessionIds ?? state.activeSessionIds).has(sessionId)
);
const removeActiveSession = (sessionId: string): void => {
if (!hasActiveSession(sessionId)) return;
nextActiveSessionIds ??= new Set(state.activeSessionIds);
nextActiveSessionIds.delete(sessionId);
};
const addActiveSession = (sessionId: string): void => {
if (hasActiveSession(sessionId)) return;
nextActiveSessionIds ??= new Set(state.activeSessionIds);
nextActiveSessionIds.add(sessionId);
};
for (const [sessionId, entry] of state.statusById) {
if ((entry.directory === directory || known.has(sessionId)) && !(sessionId in raw)) {
next.delete(sessionId);
removeActiveSession(sessionId);
changed = true;
}
}
@@ -151,17 +239,23 @@ export const applyGlobalSessionStatusSnapshot = (
if (type === 'idle') {
if (current && (current.directory === directory || known.has(sessionId))) {
next.delete(sessionId);
removeActiveSession(sessionId);
changed = true;
}
continue;
}
// SAFETY: normalizeStatusType has narrowed this snapshot entry to the SDK's busy/retry status discriminator.
const normalizedStatus = { ...status, type } as SessionStatus;
if (!current || current.directory !== directory || !statusesEqual(current.status, normalizedStatus)) {
next.set(sessionId, { status: normalizedStatus, directory });
if (!current) addActiveSession(sessionId);
changed = true;
}
}
return changed ? { statusById: next } : state;
return changed ? {
statusById: next,
activeSessionIds: nextActiveSessionIds ?? state.activeSessionIds,
} : state;
});
};