1import { scopedThreadKey } from "@t3tools/client-runtime/environment";
2import type { RunId, ScopedThreadRef } from "@t3tools/contracts";
3import { create } from "zustand";
4import { createJSONStorage, persist } from "zustand/middleware";
5
6import { resolveStorage } from "./lib/storage";
7
8export type DiffPanelSelection =
9 | { kind: "branch"; baseRef: string | null }
10 | { kind: "unstaged" }
11 | { kind: "turn"; turnId: RunId; filePath: string | null; revealRequestId: number };
12
13// "branch" is the Changes view: everything this checkout changed since its base.
14const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null };
15
16interface DiffPanelStoreState {
17 byThreadKey: Record<string, DiffPanelSelection>;
18 branchBaseRefByThreadKey: Record<string, string | null>;
19 selectGitScope: (ref: ScopedThreadRef, scope: "branch" | "unstaged") => void;
20 selectBranchBaseRef: (ref: ScopedThreadRef, baseRef: string | null) => void;
21 selectTurn: (ref: ScopedThreadRef, turnId: RunId, filePath?: string) => void;
22 reconcileTurnSelection: (ref: ScopedThreadRef, availableTurnIds: ReadonlyArray<RunId>) => void;
23 removeThread: (ref: ScopedThreadRef) => void;
24}
25
26function normalizeBaseRef(baseRef: string | null): string | null {
27 const normalized = baseRef?.trim();
28 return normalized ? normalized : null;
29}
30
31export const useDiffPanelStore = create<DiffPanelStoreState>()(
32 persist(
33 (set) => ({
34 byThreadKey: {},
35 branchBaseRefByThreadKey: {},
36 selectGitScope: (ref, scope) =>
37 set((state) => {
38 const threadKey = scopedThreadKey(ref);
39 const previous = state.byThreadKey[threadKey];
40 const previousBaseRef =
41 previous?.kind === "branch"
42 ? previous.baseRef
43 : (state.branchBaseRefByThreadKey[threadKey] ?? null);
44 return {
45 byThreadKey: {
46 ...state.byThreadKey,
47 [threadKey]:
48 scope === "branch"
49 ? { kind: "branch", baseRef: previousBaseRef }
50 : { kind: "unstaged" },
51 },
52 branchBaseRefByThreadKey:
53 previous?.kind === "branch"
54 ? { ...state.branchBaseRefByThreadKey, [threadKey]: previous.baseRef }
55 : state.branchBaseRefByThreadKey,
56 };
57 }),
58 selectBranchBaseRef: (ref, baseRef) =>
59 set((state) => {
60 const threadKey = scopedThreadKey(ref);
61 const normalizedBaseRef = normalizeBaseRef(baseRef);
62 return {
63 byThreadKey: {
64 ...state.byThreadKey,
65 [threadKey]: { kind: "branch", baseRef: normalizedBaseRef },
66 },
67 branchBaseRefByThreadKey: {
68 ...state.branchBaseRefByThreadKey,
69 [threadKey]: normalizedBaseRef,
70 },
71 };
72 }),
73 selectTurn: (ref, turnId, filePath) =>
74 set((state) => {
75 const threadKey = scopedThreadKey(ref);
76 const previous = state.byThreadKey[threadKey];
77 return {
78 byThreadKey: {
79 ...state.byThreadKey,
80 [threadKey]: {
81 kind: "turn",
82 turnId,
83 filePath: filePath?.trim() || null,
84 revealRequestId: previous?.kind === "turn" ? previous.revealRequestId + 1 : 1,
85 },
86 },
87 };
88 }),
89 reconcileTurnSelection: (ref, availableTurnIds) =>
90 set((state) => {
91 const threadKey = scopedThreadKey(ref);
92 const previous = state.byThreadKey[threadKey];
93 const latestTurnId = availableTurnIds[0];
94 if (
95 previous?.kind !== "turn" ||
96 latestTurnId === undefined ||
97 availableTurnIds.includes(previous.turnId)
98 ) {
99 return state;
100 }
101 return {
102 byThreadKey: {
103 ...state.byThreadKey,
104 [threadKey]: { ...previous, turnId: latestTurnId },
105 },
106 };
107 }),
108 removeThread: (ref) =>
109 set((state) => {
110 const threadKey = scopedThreadKey(ref);
111 if (!(threadKey in state.byThreadKey) && !(threadKey in state.branchBaseRefByThreadKey)) {
112 return state;
113 }
114 const { [threadKey]: _removed, ...byThreadKey } = state.byThreadKey;
115 const { [threadKey]: _removedBaseRef, ...branchBaseRefByThreadKey } =
116 state.branchBaseRefByThreadKey;
117 return { byThreadKey, branchBaseRefByThreadKey };
118 }),
119 }),
120 {
121 name: "t3code:diff-panel-state:v1",
122 version: 2,
123 storage: createJSONStorage(() =>
124 resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined),
125 ),
126 partialize: (state) => ({
127 byThreadKey: state.byThreadKey,
128 branchBaseRefByThreadKey: state.branchBaseRefByThreadKey,
129 }),
130 },
131 ),
132);
133
134export function selectThreadDiffPanelSelection(
135 byThreadKey: Record<string, DiffPanelSelection>,
136 ref: ScopedThreadRef | null | undefined,
137): DiffPanelSelection {
138 if (!ref) return DEFAULT_SELECTION;
139 return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;
140}