perf(sidebar): index session ownership and narrow live subscriptions
Replace repeated project-by-session directory matching across sidebar hooks with a shared ownership index that resolves each unique directory once and exposes direct project and folder-scope buckets. Gate destructive folder reconciliation on authoritative session data and topology readiness, preserve last-known worktrees after discovery failures, and retain nested-project, VS Code, active/archive dedupe, and Windows drive-root semantics. Narrow cross-directory subscriptions to session and status slices so streaming deltas no longer trigger global aggregation. Reuse a cached session ID index for permission lineage checks instead of rebuilding it on every session switch. On the reported 15-project, 67-worktree, 14,561-session shape, ownership indexing averages 3.81 ms versus roughly 450 ms for the cache-only hotfix. Validation: 28 targeted tests, UI type-check, UI lint, and dead-code analysis.
This commit is contained in:
@@ -4,7 +4,7 @@ let fetchImpl: (input: string, init?: RequestInit) => Promise<Response>;
|
||||
mock.module('@/lib/runtime-fetch', () => ({
|
||||
runtimeFetch: (input: string, init?: RequestInit) => fetchImpl(input, init),
|
||||
}));
|
||||
mock.module('@/sync/sync-refs', () => ({ getAllSyncSessions: () => [] }));
|
||||
mock.module('@/sync/sync-refs', () => ({ getAllSyncSessionMap: () => new Map() }));
|
||||
mock.module('@/sync/session-ui-store', () => ({
|
||||
useSessionUIStore: { getState: () => ({ getDirectoryForSession: () => '/project' }) },
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client";
|
||||
import { autoRespondsPermission, type PermissionAutoAcceptMap } from "./utils/permissionAutoAccept";
|
||||
import { getAllSyncSessions } from "@/sync/sync-refs";
|
||||
import { getAllSyncSessionMap } from "@/sync/sync-refs";
|
||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||
import { isVSCodeRuntime } from "@/lib/desktop";
|
||||
import { createDeferredSafeJSONStorage } from "./utils/safeStorage";
|
||||
@@ -39,8 +39,11 @@ const readSnapshot = async (response: Response): Promise<PermissionPolicySnapsho
|
||||
|
||||
const requestSnapshot = async (path: string, init?: RequestInit) => readSnapshot(await runtimeFetch(path, init));
|
||||
|
||||
const isAutoAccepting = (autoAccept: PermissionAutoAcceptMap, sessions: Session[], sessionId: string) =>
|
||||
autoRespondsPermission({ autoAccept, sessions, sessionID: sessionId });
|
||||
const isAutoAccepting = (
|
||||
autoAccept: PermissionAutoAcceptMap,
|
||||
sessionById: ReadonlyMap<string, Session>,
|
||||
sessionId: string,
|
||||
) => autoRespondsPermission({ autoAccept, sessions: [], sessionById, sessionID: sessionId });
|
||||
|
||||
export const usePermissionStore = create<PermissionStore>()(persist((set, get) => ({
|
||||
autoAccept: {},
|
||||
@@ -79,7 +82,9 @@ export const usePermissionStore = create<PermissionStore>()(persist((set, get) =
|
||||
|
||||
isSessionAutoAccepting: (sessionId) => {
|
||||
if (!sessionId) return false;
|
||||
return isAutoAccepting(get().autoAccept, getAllSyncSessions(), sessionId);
|
||||
const autoAccept = get().autoAccept;
|
||||
if (Object.keys(autoAccept).length === 0) return false;
|
||||
return isAutoAccepting(autoAccept, getAllSyncSessionMap(), sessionId);
|
||||
},
|
||||
|
||||
setSessionAutoAccept: async (sessionId, enabled) => {
|
||||
|
||||
@@ -60,6 +60,17 @@ describe("autoRespondsPermission", () => {
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test("uses a prebuilt session index for lineage lookup", () => {
|
||||
const parent = makeSession("parent")
|
||||
const child = makeSession("child", "parent")
|
||||
expect(autoRespondsPermission({
|
||||
autoAccept: { parent: true },
|
||||
sessions: [],
|
||||
sessionById: new Map([[parent.id, parent], [child.id, child]]),
|
||||
sessionID: "child",
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when only sibling has autoAccept enabled", () => {
|
||||
const autoAccept: PermissionAutoAcceptMap = { sibling: true }
|
||||
const sessions = [
|
||||
|
||||
@@ -10,8 +10,12 @@ const buildSessionMap = (sessions: Session[]): Map<string, Session> => {
|
||||
return map;
|
||||
};
|
||||
|
||||
const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
|
||||
const map = buildSessionMap(sessions);
|
||||
const resolveLineage = (
|
||||
sessionID: string,
|
||||
sessions: Session[],
|
||||
sessionById?: ReadonlyMap<string, Session>,
|
||||
): string[] => {
|
||||
const map = sessionById ?? buildSessionMap(sessions);
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
let current: string | undefined = sessionID;
|
||||
@@ -28,10 +32,12 @@ const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
|
||||
export const autoRespondsPermission = (input: {
|
||||
autoAccept: PermissionAutoAcceptMap;
|
||||
sessions: Session[];
|
||||
sessionById?: ReadonlyMap<string, Session>;
|
||||
sessionID: string;
|
||||
}): boolean => {
|
||||
const { autoAccept, sessions, sessionID } = input;
|
||||
const lineage = resolveLineage(sessionID, sessions);
|
||||
const { autoAccept, sessions, sessionById, sessionID } = input;
|
||||
if (Object.keys(autoAccept).length === 0) return false;
|
||||
const lineage = resolveLineage(sessionID, sessions, sessionById);
|
||||
|
||||
for (const id of lineage) {
|
||||
if (!Object.prototype.hasOwnProperty.call(autoAccept, id)) {
|
||||
|
||||
Reference in New Issue
Block a user