import { scopedThreadKey } from "@t3tools/client-runtime/environment"; import type { ScopedThreadRef, TurnId } from "@t3tools/contracts"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { resolveStorage } from "./lib/storage"; export type DiffPanelSelection = | { kind: "branch"; baseRef: string | null } | { kind: "unstaged" } | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number }; export type DiffRenderMode = "stacked" | "split"; const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null }; const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" }; interface DiffPanelStoreState { byThreadKey: Record; branchBaseRefByThreadKey: Record; diffRenderMode: DiffRenderMode; setDiffRenderMode: (mode: DiffRenderMode) => void; selectGitScope: (ref: ScopedThreadRef, scope: "branch" | "unstaged") => void; selectBranchBaseRef: (ref: ScopedThreadRef, baseRef: string | null) => void; selectTurn: (ref: ScopedThreadRef, turnId: TurnId, filePath?: string) => void; reconcileTurnSelection: (ref: ScopedThreadRef, availableTurnIds: ReadonlyArray) => void; removeThread: (ref: ScopedThreadRef) => void; } function normalizeBaseRef(baseRef: string | null): string | null { const normalized = baseRef?.trim(); return normalized ? normalized : null; } export const useDiffPanelStore = create()( persist( (set) => ({ byThreadKey: {}, branchBaseRefByThreadKey: {}, diffRenderMode: "stacked", setDiffRenderMode: (diffRenderMode) => set({ diffRenderMode }), selectGitScope: (ref, scope) => set((state) => { const threadKey = scopedThreadKey(ref); const previous = state.byThreadKey[threadKey]; const previousBaseRef = previous?.kind === "branch" ? previous.baseRef : (state.branchBaseRefByThreadKey[threadKey] ?? null); return { byThreadKey: { ...state.byThreadKey, [threadKey]: scope === "branch" ? { kind: "branch", baseRef: previousBaseRef } : { kind: "unstaged" }, }, branchBaseRefByThreadKey: previous?.kind === "branch" ? { ...state.branchBaseRefByThreadKey, [threadKey]: previous.baseRef } : state.branchBaseRefByThreadKey, }; }), selectBranchBaseRef: (ref, baseRef) => set((state) => { const threadKey = scopedThreadKey(ref); const normalizedBaseRef = normalizeBaseRef(baseRef); return { byThreadKey: { ...state.byThreadKey, [threadKey]: { kind: "branch", baseRef: normalizedBaseRef }, }, branchBaseRefByThreadKey: { ...state.branchBaseRefByThreadKey, [threadKey]: normalizedBaseRef, }, }; }), selectTurn: (ref, turnId, filePath) => set((state) => { const threadKey = scopedThreadKey(ref); const previous = state.byThreadKey[threadKey]; return { byThreadKey: { ...state.byThreadKey, [threadKey]: { kind: "turn", turnId, filePath: filePath?.trim() || null, revealRequestId: previous?.kind === "turn" ? previous.revealRequestId + 1 : 1, }, }, }; }), reconcileTurnSelection: (ref, availableTurnIds) => set((state) => { const threadKey = scopedThreadKey(ref); const previous = state.byThreadKey[threadKey]; const latestTurnId = availableTurnIds[0]; if ( previous?.kind !== "turn" || latestTurnId === undefined || availableTurnIds.includes(previous.turnId) ) { return state; } return { byThreadKey: { ...state.byThreadKey, [threadKey]: { ...previous, turnId: latestTurnId }, }, }; }), removeThread: (ref) => set((state) => { const threadKey = scopedThreadKey(ref); if (!(threadKey in state.byThreadKey) && !(threadKey in state.branchBaseRefByThreadKey)) { return state; } const { [threadKey]: _removed, ...byThreadKey } = state.byThreadKey; const { [threadKey]: _removedBaseRef, ...branchBaseRefByThreadKey } = state.branchBaseRefByThreadKey; return { byThreadKey, branchBaseRefByThreadKey }; }), }), { name: "t3code:diff-panel-state:v1", version: 1, storage: createJSONStorage(() => resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined), ), partialize: (state) => ({ byThreadKey: state.byThreadKey, branchBaseRefByThreadKey: state.branchBaseRefByThreadKey, diffRenderMode: state.diffRenderMode, }), }, ), ); export function selectThreadDiffPanelSelection( byThreadKey: Record, ref: ScopedThreadRef | null | undefined, hasWorkingTreeChanges = false, ): DiffPanelSelection { if (!ref) return DEFAULT_SELECTION; return ( byThreadKey[scopedThreadKey(ref)] ?? (hasWorkingTreeChanges ? DEFAULT_WORKING_TREE_SELECTION : DEFAULT_SELECTION) ); }