@cerbur/clutch-dsh-worktree 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +290 -433
  2. package/README.zh.md +244 -348
  3. package/assets/screenshots/screenshots-dashboard.png +0 -0
  4. package/assets/screenshots/screenshots-git-reviewer.png +0 -0
  5. package/lib/client/WorktreeSurface.d.ts.map +1 -1
  6. package/lib/client/WorktreeSurface.js +139 -17
  7. package/lib/client/WorktreeSurface.js.map +1 -1
  8. package/lib/client/context/WorktreeHeroContext.d.ts +4 -3
  9. package/lib/client/context/WorktreeHeroContext.d.ts.map +1 -1
  10. package/lib/client/context/WorktreeHeroContext.js +29 -11
  11. package/lib/client/context/WorktreeHeroContext.js.map +1 -1
  12. package/lib/client/dashboard/WorktreeDashboard.d.ts +9 -2
  13. package/lib/client/dashboard/WorktreeDashboard.d.ts.map +1 -1
  14. package/lib/client/dashboard/WorktreeDashboard.js +165 -14
  15. package/lib/client/dashboard/WorktreeDashboard.js.map +1 -1
  16. package/lib/client/dashboard/dashboard-navigation.d.ts +35 -0
  17. package/lib/client/dashboard/dashboard-navigation.d.ts.map +1 -0
  18. package/lib/client/dashboard/dashboard-navigation.js +48 -0
  19. package/lib/client/dashboard/dashboard-navigation.js.map +1 -0
  20. package/lib/client/dashboard/dashboard-overlay.d.ts +7 -2
  21. package/lib/client/dashboard/dashboard-overlay.d.ts.map +1 -1
  22. package/lib/client/dashboard/dashboard-overlay.js +81 -16
  23. package/lib/client/dashboard/dashboard-overlay.js.map +1 -1
  24. package/lib/client/dashboard/dashboard-selection.d.ts +2 -1
  25. package/lib/client/dashboard/dashboard-selection.d.ts.map +1 -1
  26. package/lib/client/dashboard/dashboard-selection.js +1 -1
  27. package/lib/client/dashboard/dashboard-selection.js.map +1 -1
  28. package/lib/client/dashboard/git/GitChangedFiles.d.ts +11 -0
  29. package/lib/client/dashboard/git/GitChangedFiles.d.ts.map +1 -0
  30. package/lib/client/dashboard/git/GitChangedFiles.js +101 -0
  31. package/lib/client/dashboard/git/GitChangedFiles.js.map +1 -0
  32. package/lib/client/dashboard/git/GitCommitList.d.ts +15 -0
  33. package/lib/client/dashboard/git/GitCommitList.d.ts.map +1 -0
  34. package/lib/client/dashboard/git/GitCommitList.js +55 -0
  35. package/lib/client/dashboard/git/GitCommitList.js.map +1 -0
  36. package/lib/client/dashboard/git/GitDiffView.d.ts +10 -0
  37. package/lib/client/dashboard/git/GitDiffView.d.ts.map +1 -0
  38. package/lib/client/dashboard/git/GitDiffView.js +65 -0
  39. package/lib/client/dashboard/git/GitDiffView.js.map +1 -0
  40. package/lib/client/dashboard/git/GitFileTypeIcon.d.ts +8 -0
  41. package/lib/client/dashboard/git/GitFileTypeIcon.d.ts.map +1 -0
  42. package/lib/client/dashboard/git/GitFileTypeIcon.js +24 -0
  43. package/lib/client/dashboard/git/GitFileTypeIcon.js.map +1 -0
  44. package/lib/client/dashboard/git/GitLineStats.d.ts +10 -0
  45. package/lib/client/dashboard/git/GitLineStats.d.ts.map +1 -0
  46. package/lib/client/dashboard/git/GitLineStats.js +9 -0
  47. package/lib/client/dashboard/git/GitLineStats.js.map +1 -0
  48. package/lib/client/dashboard/git/WorktreeGitOverview.d.ts +31 -0
  49. package/lib/client/dashboard/git/WorktreeGitOverview.d.ts.map +1 -0
  50. package/lib/client/dashboard/git/WorktreeGitOverview.js +89 -0
  51. package/lib/client/dashboard/git/WorktreeGitOverview.js.map +1 -0
  52. package/lib/client/dashboard/git/WorktreeGitPanel.d.ts +16 -0
  53. package/lib/client/dashboard/git/WorktreeGitPanel.d.ts.map +1 -0
  54. package/lib/client/dashboard/git/WorktreeGitPanel.js +126 -0
  55. package/lib/client/dashboard/git/WorktreeGitPanel.js.map +1 -0
  56. package/lib/client/dashboard/git/file-address.d.ts +2 -0
  57. package/lib/client/dashboard/git/file-address.d.ts.map +1 -0
  58. package/lib/client/dashboard/git/file-address.js +16 -0
  59. package/lib/client/dashboard/git/file-address.js.map +1 -0
  60. package/lib/client/dashboard/git/git-column-split.d.ts +34 -0
  61. package/lib/client/dashboard/git/git-column-split.d.ts.map +1 -0
  62. package/lib/client/dashboard/git/git-column-split.js +41 -0
  63. package/lib/client/dashboard/git/git-column-split.js.map +1 -0
  64. package/lib/client/dashboard/git/git-diff-parser.d.ts +24 -0
  65. package/lib/client/dashboard/git/git-diff-parser.d.ts.map +1 -0
  66. package/lib/client/dashboard/git/git-diff-parser.js +56 -0
  67. package/lib/client/dashboard/git/git-diff-parser.js.map +1 -0
  68. package/lib/client/dashboard/git/git-facts.d.ts +18 -0
  69. package/lib/client/dashboard/git/git-facts.d.ts.map +1 -0
  70. package/lib/client/dashboard/git/git-facts.js +31 -0
  71. package/lib/client/dashboard/git/git-facts.js.map +1 -0
  72. package/lib/client/dashboard/git/git-file-tree.d.ts +16 -0
  73. package/lib/client/dashboard/git/git-file-tree.d.ts.map +1 -0
  74. package/lib/client/dashboard/git/git-file-tree.js +52 -0
  75. package/lib/client/dashboard/git/git-file-tree.js.map +1 -0
  76. package/lib/client/dashboard/git/git-state-cache.d.ts +49 -0
  77. package/lib/client/dashboard/git/git-state-cache.d.ts.map +1 -0
  78. package/lib/client/dashboard/git/git-state-cache.js +80 -0
  79. package/lib/client/dashboard/git/git-state-cache.js.map +1 -0
  80. package/lib/client/dashboard/git/useGitPaneSplit.d.ts +33 -0
  81. package/lib/client/dashboard/git/useGitPaneSplit.d.ts.map +1 -0
  82. package/lib/client/dashboard/git/useGitPaneSplit.js +119 -0
  83. package/lib/client/dashboard/git/useGitPaneSplit.js.map +1 -0
  84. package/lib/client/dashboard/git/useWorktreeGitState.d.ts +60 -0
  85. package/lib/client/dashboard/git/useWorktreeGitState.d.ts.map +1 -0
  86. package/lib/client/dashboard/git/useWorktreeGitState.js +654 -0
  87. package/lib/client/dashboard/git/useWorktreeGitState.js.map +1 -0
  88. package/lib/client/entry.d.ts.map +1 -1
  89. package/lib/client/entry.js +28 -0
  90. package/lib/client/entry.js.map +1 -1
  91. package/lib/client/locales.d.ts +158 -0
  92. package/lib/client/locales.d.ts.map +1 -1
  93. package/lib/client/locales.js +166 -8
  94. package/lib/client/locales.js.map +1 -1
  95. package/lib/client/session/session-view.d.ts +10 -0
  96. package/lib/client/session/session-view.d.ts.map +1 -1
  97. package/lib/client/session/session-view.js +57 -0
  98. package/lib/client/session/session-view.js.map +1 -1
  99. package/lib/client/session/worktree-session-order.d.ts +2 -5
  100. package/lib/client/session/worktree-session-order.d.ts.map +1 -1
  101. package/lib/client/session/worktree-session-order.js +35 -10
  102. package/lib/client/session/worktree-session-order.js.map +1 -1
  103. package/lib/client/session/worktree-session-position.d.ts +2 -1
  104. package/lib/client/session/worktree-session-position.d.ts.map +1 -1
  105. package/lib/client/session/worktree-session-position.js +10 -3
  106. package/lib/client/session/worktree-session-position.js.map +1 -1
  107. package/lib/client/surface/actions/useDragActions.d.ts +33 -2
  108. package/lib/client/surface/actions/useDragActions.d.ts.map +1 -1
  109. package/lib/client/surface/actions/useDragActions.js +66 -18
  110. package/lib/client/surface/actions/useDragActions.js.map +1 -1
  111. package/lib/client/surface/components/ActiveWorktree.js +2 -2
  112. package/lib/client/surface/components/ActiveWorktree.js.map +1 -1
  113. package/lib/client/surface/components/ArchivedWorktree.js +2 -2
  114. package/lib/client/surface/components/ArchivedWorktree.js.map +1 -1
  115. package/lib/client/surface/components/ArchivedWorktrees.js +2 -2
  116. package/lib/client/surface/components/ArchivedWorktrees.js.map +1 -1
  117. package/lib/client/surface/components/SurfaceContent.d.ts +1 -1
  118. package/lib/client/surface/components/SurfaceContent.d.ts.map +1 -1
  119. package/lib/client/surface/components/SurfaceContent.js +1 -1
  120. package/lib/client/surface/components/SurfaceContent.js.map +1 -1
  121. package/lib/client/surface/components/SurfaceHeader.d.ts +1 -1
  122. package/lib/client/surface/components/SurfaceHeader.d.ts.map +1 -1
  123. package/lib/client/surface/components/SurfaceHeader.js +12 -3
  124. package/lib/client/surface/components/SurfaceHeader.js.map +1 -1
  125. package/lib/client/surface/components/WorkspaceTree.js +4 -4
  126. package/lib/client/surface/components/WorkspaceTree.js.map +1 -1
  127. package/lib/client/surface/components/rows.d.ts +2 -2
  128. package/lib/client/surface/components/rows.d.ts.map +1 -1
  129. package/lib/client/surface/components/rows.js +14 -9
  130. package/lib/client/surface/components/rows.js.map +1 -1
  131. package/lib/client/surface/selectors.d.ts +10 -0
  132. package/lib/client/surface/selectors.d.ts.map +1 -1
  133. package/lib/client/surface/selectors.js +14 -0
  134. package/lib/client/surface/selectors.js.map +1 -1
  135. package/lib/client/surface/state/useSessionExpansion.d.ts +2 -1
  136. package/lib/client/surface/state/useSessionExpansion.d.ts.map +1 -1
  137. package/lib/client/surface/state/useSessionExpansion.js +26 -13
  138. package/lib/client/surface/state/useSessionExpansion.js.map +1 -1
  139. package/lib/client/surface/state/useSessionOrdering.d.ts +1 -1
  140. package/lib/client/surface/state/useSessionOrdering.d.ts.map +1 -1
  141. package/lib/client/surface/state/useSessionOrdering.js +23 -8
  142. package/lib/client/surface/state/useSessionOrdering.js.map +1 -1
  143. package/lib/client/surface/types.d.ts +15 -3
  144. package/lib/client/surface/types.d.ts.map +1 -1
  145. package/lib/client/view/worktree-expand-state.d.ts +1 -1
  146. package/lib/client/view/worktree-expand-state.d.ts.map +1 -1
  147. package/lib/client/view/worktree-expand-state.js +8 -1
  148. package/lib/client/view/worktree-expand-state.js.map +1 -1
  149. package/lib/client/worktree-connection.d.ts +4 -0
  150. package/lib/client/worktree-connection.d.ts.map +1 -1
  151. package/lib/client/worktree-connection.js +10 -0
  152. package/lib/client/worktree-connection.js.map +1 -1
  153. package/lib/client.js +3454 -307
  154. package/lib/client.js.map +1 -1
  155. package/lib/contract/index.contract.js +8 -0
  156. package/lib/contract/index.contract.js.map +1 -1
  157. package/lib/contract/index.d.ts +145 -1
  158. package/lib/contract/index.d.ts.map +1 -1
  159. package/lib/contract/index.js +18 -0
  160. package/lib/contract/index.js.map +1 -1
  161. package/lib/host/remote.d.ts.map +1 -1
  162. package/lib/host/remote.js +4 -0
  163. package/lib/host/remote.js.map +1 -1
  164. package/lib/host/service.d.ts +14 -1
  165. package/lib/host/service.d.ts.map +1 -1
  166. package/lib/host/service.js +24 -0
  167. package/lib/host/service.js.map +1 -1
  168. package/lib/index.d.ts +4 -4
  169. package/lib/index.d.ts.map +1 -1
  170. package/lib/index.js +3 -3
  171. package/lib/index.js.map +1 -1
  172. package/lib/manage/manager-git-history.d.ts +17 -0
  173. package/lib/manage/manager-git-history.d.ts.map +1 -0
  174. package/lib/manage/manager-git-history.js +717 -0
  175. package/lib/manage/manager-git-history.js.map +1 -0
  176. package/lib/manage/manager-worktrees.d.ts.map +1 -1
  177. package/lib/manage/manager-worktrees.js +24 -0
  178. package/lib/manage/manager-worktrees.js.map +1 -1
  179. package/lib/manage/manager.d.ts +14 -1
  180. package/lib/manage/manager.d.ts.map +1 -1
  181. package/lib/manage/manager.js +13 -0
  182. package/lib/manage/manager.js.map +1 -1
  183. package/lib/provider/git/adapter.d.ts +54 -1
  184. package/lib/provider/git/adapter.d.ts.map +1 -1
  185. package/lib/provider/git/adapter.js +820 -1
  186. package/lib/provider/git/adapter.js.map +1 -1
  187. package/lib/provider/index.d.ts +2 -2
  188. package/lib/provider/index.d.ts.map +1 -1
  189. package/lib/provider/index.js +1 -1
  190. package/lib/provider/index.js.map +1 -1
  191. package/lib/provider/mutation-token.d.ts +1 -1
  192. package/lib/provider/mutation-token.d.ts.map +1 -1
  193. package/lib/provider/mutation-token.js +1 -0
  194. package/lib/provider/mutation-token.js.map +1 -1
  195. package/lib/provider/sidecar/repository.d.ts +1 -1
  196. package/lib/provider/sidecar/repository.d.ts.map +1 -1
  197. package/lib/provider/sidecar/repository.js +3 -2
  198. package/lib/provider/sidecar/repository.js.map +1 -1
  199. package/lib/provider/sidecar/sidecar-schema.d.ts +16 -3
  200. package/lib/provider/sidecar/sidecar-schema.d.ts.map +1 -1
  201. package/lib/provider/sidecar/sidecar-schema.js +105 -23
  202. package/lib/provider/sidecar/sidecar-schema.js.map +1 -1
  203. package/lib/provider/transaction/operations/create.d.ts.map +1 -1
  204. package/lib/provider/transaction/operations/create.js +21 -6
  205. package/lib/provider/transaction/operations/create.js.map +1 -1
  206. package/lib/provider/transaction/recovery/failure-recovery.d.ts.map +1 -1
  207. package/lib/provider/transaction/recovery/failure-recovery.js +11 -4
  208. package/lib/provider/transaction/recovery/failure-recovery.js.map +1 -1
  209. package/lib/provider/transaction/recovery/recover.d.ts.map +1 -1
  210. package/lib/provider/transaction/recovery/recover.js +9 -11
  211. package/lib/provider/transaction/recovery/recover.js.map +1 -1
  212. package/lib/provider/transaction/support/inspection.d.ts +0 -2
  213. package/lib/provider/transaction/support/inspection.d.ts.map +1 -1
  214. package/lib/provider/transaction/support/inspection.js +0 -4
  215. package/lib/provider/transaction/support/inspection.js.map +1 -1
  216. package/lib/provider/transaction/support/journal.d.ts.map +1 -1
  217. package/lib/provider/transaction/support/journal.js +2 -0
  218. package/lib/provider/transaction/support/journal.js.map +1 -1
  219. package/lib/provider/transaction/types.d.ts +1 -0
  220. package/lib/provider/transaction/types.d.ts.map +1 -1
  221. package/lib/provider/types.d.ts +53 -5
  222. package/lib/provider/types.d.ts.map +1 -1
  223. package/lib/provider/types.js +1 -1
  224. package/lib/provider/types.js.map +1 -1
  225. package/lib/typert.host.js +295 -34
  226. package/lib/typert.remote-client.d.ts +9 -1
  227. package/lib/typert.remote-client.d.ts.map +1 -1
  228. package/lib/typert.remote-client.js +295 -34
  229. package/package.json +3 -1
  230. package/assets/screenshots/screenshots-dashboard.webp +0 -0
@@ -0,0 +1,654 @@
1
+ import { useEffect, useMemo, useSyncExternalStore } from 'react';
2
+ import { WORKTREE_GIT_WORKING_TREE } from '../../../contract/index.js';
3
+ import { isMainWorktreeId } from './git-facts.js';
4
+ import { MAX_DIFF_CACHE, MAX_FILE_CACHE, canonicalizeCommits, diffCacheKey, fileForPath, gitCacheKey, isLiveCacheKey, isLiveTarget, sameTarget, summarySelection, trimCache, } from './git-state-cache.js';
5
+ function asError(error) {
6
+ return error instanceof Error ? error : new Error(String(error));
7
+ }
8
+ function normalizeBranch(branch) {
9
+ const normalized = branch?.trim();
10
+ return normalized === undefined || normalized.length === 0 ? undefined : normalized;
11
+ }
12
+ function emptyState(defaultBaselineBranch) {
13
+ return {
14
+ branches: { status: 'idle' },
15
+ baselineBranch: normalizeBranch(defaultBaselineBranch),
16
+ history: { status: 'idle' },
17
+ view: 'summary',
18
+ includeWorkingTree: false,
19
+ selectedCommits: [],
20
+ commitMultiSelect: false,
21
+ files: { status: 'idle' },
22
+ diff: { status: 'idle' },
23
+ };
24
+ }
25
+ /**
26
+ * Create the browser-safe Git Dashboard state machine. Construction is inert;
27
+ * the caller explicitly starts history loading on first Git-tab activation.
28
+ */
29
+ export function createWorktreeGitStateController(input) {
30
+ // Main (Local) has no Worktree-relative Git projection, so it never issues a
31
+ // Git read even when a panel mounts for it.
32
+ const main = isMainWorktreeId(input.worktreeId);
33
+ // A Worktree with a captured acquisition commit is readable without naming a
34
+ // baseline branch: the Manager resolves that immutable commit as the implicit
35
+ // boundary. Main never reads Git, and a Worktree with neither a selectable
36
+ // branch nor a captured commit stays unselected until the user picks one.
37
+ const capturedBaseline = input.capturedBaseline === true;
38
+ const canRead = (baselineBranch) => !main && (capturedBaseline || baselineBranch !== undefined);
39
+ let state = emptyState(input.defaultBaselineBranch);
40
+ let disposed = false;
41
+ let branchesLoaded = false;
42
+ let branchesRequest = 0;
43
+ let historyLoaded = false;
44
+ let historyRequest = 0;
45
+ let filesRequest = 0;
46
+ let diffRequest = 0;
47
+ let branchesInFlight;
48
+ let historyInFlight;
49
+ const filesInFlight = new Map();
50
+ const diffInFlight = new Map();
51
+ const listeners = new Set();
52
+ const filesCache = new Map();
53
+ const diffCache = new Map();
54
+ const emit = () => {
55
+ if (disposed)
56
+ return;
57
+ for (const listener of listeners)
58
+ listener();
59
+ };
60
+ const update = (next) => {
61
+ if (disposed)
62
+ return;
63
+ state = next;
64
+ emit();
65
+ };
66
+ const invalidateWorkingTreeCache = () => {
67
+ for (const key of filesCache.keys()) {
68
+ if (isLiveCacheKey(key))
69
+ filesCache.delete(key);
70
+ }
71
+ for (const key of diffCache.keys()) {
72
+ if (isLiveCacheKey(key))
73
+ diffCache.delete(key);
74
+ }
75
+ };
76
+ const retireLiveInFlight = () => {
77
+ let retired = false;
78
+ for (const key of filesInFlight.keys()) {
79
+ if (isLiveCacheKey(key)) {
80
+ filesInFlight.delete(key);
81
+ retired = true;
82
+ }
83
+ }
84
+ for (const key of diffInFlight.keys()) {
85
+ if (isLiveCacheKey(key)) {
86
+ diffInFlight.delete(key);
87
+ retired = true;
88
+ }
89
+ }
90
+ return retired;
91
+ };
92
+ const currentTarget = () => {
93
+ if (state.view === 'summary') {
94
+ return { kind: 'selection', selection: summarySelection(state.includeWorkingTree) };
95
+ }
96
+ if (state.selectedCommit === undefined)
97
+ return undefined;
98
+ if (state.selectedCommit !== WORKTREE_GIT_WORKING_TREE &&
99
+ state.selectedCommits.length > 1) {
100
+ return { kind: 'selection', selection: { kind: 'commits', commits: state.selectedCommits } };
101
+ }
102
+ return { kind: 'commit', commit: state.selectedCommit };
103
+ };
104
+ /**
105
+ * Stale-response guard. A completion only publishes while it is still the
106
+ * newest request for the same baseline, projection, and selected path.
107
+ */
108
+ const isCurrentRequest = (request, latestRequest, baselineBranch, target, selectedPath) => {
109
+ if (disposed || request !== latestRequest || state.baselineBranch !== baselineBranch)
110
+ return false;
111
+ const active = currentTarget();
112
+ if (active === undefined || !sameTarget(active, target))
113
+ return false;
114
+ return selectedPath === undefined || state.selectedPath === selectedPath;
115
+ };
116
+ const requestFiles = (baseBranch, history, target) => {
117
+ const key = gitCacheKey(baseBranch, history, target);
118
+ const existing = filesInFlight.get(key);
119
+ if (existing !== undefined)
120
+ return existing;
121
+ if (input.manager === undefined)
122
+ return Promise.reject(new Error('Git manager unavailable'));
123
+ const request = target.kind === 'commit'
124
+ ? { commit: target.commit }
125
+ : { selection: target.selection };
126
+ let promise;
127
+ try {
128
+ promise = input.manager.listWorktreeCommitFiles({
129
+ workspaceId: input.workspaceId,
130
+ worktreeId: input.worktreeId,
131
+ ...request,
132
+ baseBranch,
133
+ });
134
+ }
135
+ catch (error) {
136
+ promise = Promise.reject(error);
137
+ }
138
+ filesInFlight.set(key, promise);
139
+ void promise.then(() => {
140
+ if (filesInFlight.get(key) === promise)
141
+ filesInFlight.delete(key);
142
+ }, () => {
143
+ if (filesInFlight.get(key) === promise)
144
+ filesInFlight.delete(key);
145
+ });
146
+ return promise;
147
+ };
148
+ const requestDiff = (baseBranch, history, target, diffPath) => {
149
+ const key = diffCacheKey(baseBranch, history, target, diffPath);
150
+ const existing = diffInFlight.get(key);
151
+ if (existing !== undefined)
152
+ return existing;
153
+ if (input.manager === undefined)
154
+ return Promise.reject(new Error('Git manager unavailable'));
155
+ const request = target.kind === 'commit'
156
+ ? { commit: target.commit }
157
+ : { selection: target.selection };
158
+ let promise;
159
+ try {
160
+ promise = input.manager.getWorktreeCommitFileDiff({
161
+ workspaceId: input.workspaceId,
162
+ worktreeId: input.worktreeId,
163
+ ...request,
164
+ path: diffPath,
165
+ baseBranch,
166
+ });
167
+ }
168
+ catch (error) {
169
+ promise = Promise.reject(error);
170
+ }
171
+ diffInFlight.set(key, promise);
172
+ void promise.then(() => {
173
+ if (diffInFlight.get(key) === promise)
174
+ diffInFlight.delete(key);
175
+ }, () => {
176
+ if (diffInFlight.get(key) === promise)
177
+ diffInFlight.delete(key);
178
+ });
179
+ return promise;
180
+ };
181
+ const loadDiff = async (target, selectedPath, diffPath) => {
182
+ const baselineBranch = state.baselineBranch;
183
+ if (!canRead(baselineBranch))
184
+ return;
185
+ const request = ++diffRequest;
186
+ const key = diffCacheKey(baselineBranch, state.history, target, diffPath);
187
+ const cached = isLiveTarget(target) ? undefined : diffCache.get(key);
188
+ if (cached !== undefined) {
189
+ update({ ...state, selectedPath, diff: { status: 'ready', value: cached } });
190
+ return;
191
+ }
192
+ update({ ...state, selectedPath, diff: { status: 'loading' } });
193
+ try {
194
+ const value = await requestDiff(baselineBranch, state.history, target, diffPath);
195
+ if (!isLiveTarget(target)) {
196
+ diffCache.set(key, value);
197
+ trimCache(diffCache, MAX_DIFF_CACHE);
198
+ }
199
+ if (!isCurrentRequest(request, diffRequest, baselineBranch, target, selectedPath))
200
+ return;
201
+ update({ ...state, diff: { status: 'ready', value } });
202
+ }
203
+ catch (error) {
204
+ if (!isCurrentRequest(request, diffRequest, baselineBranch, target, selectedPath))
205
+ return;
206
+ update({ ...state, diff: { status: 'error', error: asError(error) } });
207
+ }
208
+ };
209
+ const applyFiles = (target, value) => {
210
+ const selectedPath = value.files[0]?.path;
211
+ update({
212
+ ...state,
213
+ files: { status: 'ready', value },
214
+ selectedPath,
215
+ diff: { status: 'idle' },
216
+ });
217
+ if (selectedPath !== undefined)
218
+ void loadDiff(target, selectedPath, selectedPath);
219
+ };
220
+ const loadFiles = async (target, preserveReady = false) => {
221
+ const baselineBranch = state.baselineBranch;
222
+ if (!canRead(baselineBranch))
223
+ return;
224
+ const request = ++filesRequest;
225
+ ++diffRequest;
226
+ const key = gitCacheKey(baselineBranch, state.history, target);
227
+ const cached = isLiveTarget(target) ? undefined : filesCache.get(key);
228
+ if (cached !== undefined) {
229
+ applyFiles(target, cached);
230
+ return;
231
+ }
232
+ const previousFiles = state.files.status === 'ready' ? state.files : undefined;
233
+ if (!preserveReady || previousFiles === undefined) {
234
+ update({ ...state, files: { status: 'loading' }, selectedPath: undefined, diff: { status: 'idle' } });
235
+ }
236
+ else {
237
+ update({ ...state, files: { ...previousFiles, refreshing: true } });
238
+ }
239
+ try {
240
+ const value = await requestFiles(baselineBranch, state.history, target);
241
+ if (!isLiveTarget(target)) {
242
+ filesCache.set(key, value);
243
+ trimCache(filesCache, MAX_FILE_CACHE);
244
+ }
245
+ if (!isCurrentRequest(request, filesRequest, baselineBranch, target))
246
+ return;
247
+ applyFiles(target, value);
248
+ }
249
+ catch (error) {
250
+ if (!isCurrentRequest(request, filesRequest, baselineBranch, target))
251
+ return;
252
+ if (preserveReady && previousFiles !== undefined) {
253
+ update({ ...state, files: { status: 'ready', value: previousFiles.value, error: asError(error) } });
254
+ }
255
+ else {
256
+ update({ ...state, files: { status: 'error', error: asError(error) }, selectedPath: undefined, diff: { status: 'idle' } });
257
+ }
258
+ }
259
+ };
260
+ const resetTarget = (next) => {
261
+ retireLiveInFlight();
262
+ ++filesRequest;
263
+ ++diffRequest;
264
+ update({
265
+ ...state,
266
+ ...next,
267
+ files: { status: 'idle' },
268
+ selectedPath: undefined,
269
+ diff: { status: 'idle' },
270
+ });
271
+ };
272
+ const selectBaselineBranch = (branch) => {
273
+ if (disposed)
274
+ return;
275
+ const nextBranch = normalizeBranch(branch);
276
+ if (state.baselineBranch === nextBranch)
277
+ return;
278
+ ++historyRequest;
279
+ ++filesRequest;
280
+ ++diffRequest;
281
+ historyInFlight = undefined;
282
+ historyLoaded = false;
283
+ filesInFlight.clear();
284
+ diffInFlight.clear();
285
+ filesCache.clear();
286
+ diffCache.clear();
287
+ update({
288
+ ...state,
289
+ baselineBranch: nextBranch,
290
+ history: { status: 'idle' },
291
+ view: 'summary',
292
+ selectedCommit: undefined,
293
+ selectedCommits: [],
294
+ files: { status: 'idle' },
295
+ selectedPath: undefined,
296
+ diff: { status: 'idle' },
297
+ });
298
+ // Clearing an explicit selection falls back to the captured baseline, so it
299
+ // must re-read rather than leave the previous branch projection on screen.
300
+ if (nextBranch !== undefined || capturedBaseline)
301
+ void loadHistory();
302
+ };
303
+ const selectSummary = () => {
304
+ if (disposed || !canRead(state.baselineBranch))
305
+ return;
306
+ const target = { kind: 'selection', selection: summarySelection(state.includeWorkingTree) };
307
+ resetTarget({ view: 'summary', selectedCommit: undefined, selectedCommits: [] });
308
+ void loadFiles(target);
309
+ };
310
+ const setIncludeWorkingTree = (includeWorkingTree) => {
311
+ if (disposed || state.includeWorkingTree === includeWorkingTree)
312
+ return;
313
+ if (state.view !== 'summary' || !canRead(state.baselineBranch)) {
314
+ update({ ...state, includeWorkingTree });
315
+ return;
316
+ }
317
+ const target = { kind: 'selection', selection: summarySelection(includeWorkingTree) };
318
+ resetTarget({ includeWorkingTree });
319
+ void loadFiles(target);
320
+ };
321
+ const selectView = (view) => {
322
+ if (view === 'summary') {
323
+ selectSummary();
324
+ return;
325
+ }
326
+ if (disposed || !canRead(state.baselineBranch))
327
+ return;
328
+ const selectedCommit = state.selectedCommit;
329
+ const selectedCommits = state.selectedCommits;
330
+ if (state.view === 'commits' && state.files.status === 'ready')
331
+ return;
332
+ resetTarget({ view: 'commits' });
333
+ if (selectedCommit === undefined)
334
+ return;
335
+ const target = selectedCommit !== WORKTREE_GIT_WORKING_TREE && selectedCommits.length > 1
336
+ ? { kind: 'selection', selection: { kind: 'commits', commits: selectedCommits } }
337
+ : { kind: 'commit', commit: selectedCommit };
338
+ void loadFiles(target);
339
+ };
340
+ const selectCommit = (commit) => {
341
+ if (disposed || !canRead(state.baselineBranch))
342
+ return;
343
+ const history = state.history.status === 'ready' ? state.history.value : undefined;
344
+ if (history !== undefined && !history.commits.some((candidate) => candidate.sha === commit))
345
+ return;
346
+ const target = { kind: 'commit', commit };
347
+ const selectedCommits = commit === WORKTREE_GIT_WORKING_TREE ? [] : [commit];
348
+ if (state.view === 'commits' && state.selectedCommit === commit && state.files.status === 'ready' && state.selectedCommits.length <= 1)
349
+ return;
350
+ resetTarget({ view: 'commits', selectedCommit: commit, selectedCommits });
351
+ void loadFiles(target);
352
+ };
353
+ const toggleCommit = (commit) => {
354
+ if (disposed || !canRead(state.baselineBranch) || commit === WORKTREE_GIT_WORKING_TREE)
355
+ return;
356
+ const history = state.history.status === 'ready' ? state.history.value : undefined;
357
+ if (history === undefined || !history.commits.some((candidate) => candidate.sha === commit && candidate.sha !== WORKTREE_GIT_WORKING_TREE))
358
+ return;
359
+ const selected = state.selectedCommits.includes(commit)
360
+ ? state.selectedCommits.filter((candidate) => candidate !== commit)
361
+ : [...state.selectedCommits, commit];
362
+ const canonical = canonicalizeCommits(history, selected);
363
+ if (canonical.length === 0) {
364
+ resetTarget({ view: 'commits', selectedCommit: undefined, selectedCommits: [] });
365
+ return;
366
+ }
367
+ const selectedCommit = canonical.includes(commit) ? commit : canonical[canonical.length - 1];
368
+ const target = canonical.length > 1
369
+ ? { kind: 'selection', selection: { kind: 'commits', commits: canonical } }
370
+ : { kind: 'commit', commit: selectedCommit };
371
+ resetTarget({ view: 'commits', selectedCommit, selectedCommits: canonical });
372
+ void loadFiles(target);
373
+ };
374
+ /**
375
+ * Switch between replace-on-click and additive selection. Turning multi-select
376
+ * off collapses an aggregate target back to its primary commit so the files and
377
+ * diff panes keep matching the visible selection.
378
+ */
379
+ const setCommitMultiSelect = (enabled) => {
380
+ if (disposed || state.commitMultiSelect === enabled)
381
+ return;
382
+ if (enabled) {
383
+ update({ ...state, commitMultiSelect: true });
384
+ return;
385
+ }
386
+ const primary = state.selectedCommit;
387
+ const aggregated = state.view === 'commits' && primary !== undefined && state.selectedCommits.length > 1;
388
+ if (!aggregated) {
389
+ update({
390
+ ...state,
391
+ commitMultiSelect: false,
392
+ selectedCommits: primary === undefined ? [] : [primary],
393
+ });
394
+ return;
395
+ }
396
+ // selectCommit must still observe the aggregate selection; collapsing it first
397
+ // would trip the "already showing this commit" guard and keep the union files.
398
+ update({ ...state, commitMultiSelect: false });
399
+ selectCommit(primary);
400
+ };
401
+ const clearCommitSelection = () => {
402
+ if (disposed || !canRead(state.baselineBranch))
403
+ return;
404
+ resetTarget({ view: 'commits', selectedCommit: undefined, selectedCommits: [] });
405
+ };
406
+ const selectPath = (selectedPath) => {
407
+ if (disposed || state.files.status !== 'ready')
408
+ return;
409
+ const target = currentTarget();
410
+ if (target === undefined)
411
+ return;
412
+ const changedFile = fileForPath(state.files.value, selectedPath);
413
+ if (changedFile === undefined)
414
+ return;
415
+ ++diffRequest;
416
+ void loadDiff(target, selectedPath, changedFile.path);
417
+ };
418
+ const loadBranches = (refresh = false) => {
419
+ if (disposed || main)
420
+ return Promise.resolve();
421
+ if (branchesInFlight !== undefined)
422
+ return branchesInFlight;
423
+ if (!refresh && branchesLoaded)
424
+ return Promise.resolve();
425
+ const request = ++branchesRequest;
426
+ const previous = state.branches;
427
+ if (refresh && previous.status === 'ready') {
428
+ update({ ...state, branches: { status: 'ready', value: previous.value, refreshing: true } });
429
+ }
430
+ else if (refresh && previous.status === 'error' && previous.previous !== undefined) {
431
+ update({ ...state, branches: { status: 'ready', value: previous.previous, refreshing: true } });
432
+ }
433
+ else {
434
+ update({ ...state, branches: { status: 'loading' } });
435
+ }
436
+ const run = async () => {
437
+ if (input.manager?.listBranches === undefined) {
438
+ if (!disposed && request === branchesRequest) {
439
+ branchesLoaded = true;
440
+ update({ ...state, branches: { status: 'ready', value: [] } });
441
+ }
442
+ return;
443
+ }
444
+ try {
445
+ const value = await input.manager.listBranches({ workspaceId: input.workspaceId });
446
+ if (disposed || request !== branchesRequest)
447
+ return;
448
+ branchesLoaded = true;
449
+ update({ ...state, branches: { status: 'ready', value } });
450
+ }
451
+ catch (error) {
452
+ if (disposed || request !== branchesRequest)
453
+ return;
454
+ branchesLoaded = false;
455
+ const nextError = asError(error);
456
+ if (previous.status === 'ready') {
457
+ update({ ...state, branches: { status: 'ready', value: previous.value, error: nextError } });
458
+ }
459
+ else if (previous.status === 'error' && previous.previous !== undefined) {
460
+ update({ ...state, branches: { status: 'ready', value: previous.previous, error: nextError } });
461
+ }
462
+ else {
463
+ update({ ...state, branches: { status: 'error', error: nextError } });
464
+ }
465
+ }
466
+ };
467
+ const promise = run().finally(() => {
468
+ if (branchesInFlight === promise)
469
+ branchesInFlight = undefined;
470
+ });
471
+ branchesInFlight = promise;
472
+ return promise;
473
+ };
474
+ const loadHistory = (refresh = false) => {
475
+ if (disposed || !canRead(state.baselineBranch))
476
+ return Promise.resolve();
477
+ if (historyInFlight !== undefined)
478
+ return historyInFlight;
479
+ if (!refresh && historyLoaded)
480
+ return Promise.resolve();
481
+ const baselineBranch = state.baselineBranch;
482
+ const request = ++historyRequest;
483
+ const previous = state.history;
484
+ const refreshesWorkingTree = refresh && state.selectedCommit === WORKTREE_GIT_WORKING_TREE;
485
+ if (refresh) {
486
+ const retiredLiveReads = retireLiveInFlight();
487
+ if (retiredLiveReads) {
488
+ ++filesRequest;
489
+ ++diffRequest;
490
+ if (state.files.status === 'loading') {
491
+ update({ ...state, files: { status: 'idle' }, selectedPath: undefined, diff: { status: 'idle' } });
492
+ }
493
+ else if (state.diff.status === 'loading') {
494
+ update({ ...state, diff: { status: 'idle' } });
495
+ }
496
+ }
497
+ }
498
+ if (refreshesWorkingTree)
499
+ invalidateWorkingTreeCache();
500
+ if (refresh && previous.status === 'ready') {
501
+ update({ ...state, history: { status: 'ready', value: previous.value, refreshing: true } });
502
+ }
503
+ else if (refresh && previous.status === 'error' && previous.previous !== undefined) {
504
+ update({ ...state, history: { status: 'ready', value: previous.previous, refreshing: true } });
505
+ }
506
+ else {
507
+ update({ ...state, history: { status: 'loading' } });
508
+ }
509
+ const run = async () => {
510
+ if (input.manager === undefined) {
511
+ const error = new Error('Git manager unavailable');
512
+ if (!disposed && request === historyRequest) {
513
+ historyLoaded = true;
514
+ update({ ...state, history: { status: 'error', error } });
515
+ }
516
+ return;
517
+ }
518
+ try {
519
+ const value = await input.manager.listWorktreeCommits({
520
+ workspaceId: input.workspaceId,
521
+ worktreeId: input.worktreeId,
522
+ baseBranch: baselineBranch,
523
+ });
524
+ if (disposed || request !== historyRequest || state.baselineBranch !== baselineBranch)
525
+ return;
526
+ historyLoaded = true;
527
+ const selectedCommits = state.view === 'summary'
528
+ ? []
529
+ : canonicalizeCommits(value, state.selectedCommits);
530
+ const selectedCommitStillExists = state.view !== 'summary' &&
531
+ state.selectedCommit !== undefined &&
532
+ value.commits.some((commit) => commit.sha === state.selectedCommit);
533
+ const nextSelectedCommit = state.view === 'summary'
534
+ ? undefined
535
+ : selectedCommitStillExists
536
+ ? state.selectedCommit
537
+ : selectedCommits[0] ?? value.commits[0]?.sha;
538
+ const nextSelectedCommits = state.view === 'summary'
539
+ ? []
540
+ : selectedCommits.length > 0
541
+ ? selectedCommits
542
+ : nextSelectedCommit !== undefined && nextSelectedCommit !== WORKTREE_GIT_WORKING_TREE
543
+ ? [nextSelectedCommit]
544
+ : [];
545
+ update({
546
+ ...state,
547
+ history: { status: 'ready', value },
548
+ selectedCommits: nextSelectedCommits,
549
+ selectedCommit: nextSelectedCommit,
550
+ });
551
+ if (state.view === 'summary') {
552
+ void loadFiles({ kind: 'selection', selection: summarySelection(state.includeWorkingTree) }, true);
553
+ }
554
+ else if (nextSelectedCommit === undefined) {
555
+ resetTarget({ selectedCommit: undefined, selectedCommits: [] });
556
+ }
557
+ else if (!selectedCommitStillExists || selectedCommits.length !== state.selectedCommits.length || refreshesWorkingTree) {
558
+ const target = nextSelectedCommit !== WORKTREE_GIT_WORKING_TREE && selectedCommits.length > 1
559
+ ? { kind: 'selection', selection: { kind: 'commits', commits: selectedCommits } }
560
+ : { kind: 'commit', commit: nextSelectedCommit };
561
+ void loadFiles(target, refreshesWorkingTree);
562
+ }
563
+ }
564
+ catch (error) {
565
+ if (disposed || request !== historyRequest || state.baselineBranch !== baselineBranch)
566
+ return;
567
+ historyLoaded = true;
568
+ const nextError = asError(error);
569
+ if (previous.status === 'ready') {
570
+ update({ ...state, history: { status: 'ready', value: previous.value, error: nextError } });
571
+ }
572
+ else if (previous.status === 'error' && previous.previous !== undefined) {
573
+ update({ ...state, history: { status: 'ready', value: previous.previous, error: nextError } });
574
+ }
575
+ else {
576
+ update({ ...state, history: { status: 'error', error: nextError } });
577
+ }
578
+ }
579
+ };
580
+ const promise = run().finally(() => {
581
+ if (historyInFlight === promise)
582
+ historyInFlight = undefined;
583
+ });
584
+ historyInFlight = promise;
585
+ return promise;
586
+ };
587
+ const controller = {
588
+ getSnapshot: () => state,
589
+ subscribe: (listener) => {
590
+ listeners.add(listener);
591
+ return () => listeners.delete(listener);
592
+ },
593
+ loadBranches: () => loadBranches(false),
594
+ loadHistory: () => loadHistory(false),
595
+ refresh: () => {
596
+ const branchesTask = loadBranches(true);
597
+ const historyTask = loadHistory(true);
598
+ return Promise.all([branchesTask, historyTask]).then(() => undefined);
599
+ },
600
+ selectBaselineBranch,
601
+ selectView,
602
+ selectSummary,
603
+ setIncludeWorkingTree,
604
+ selectCommit,
605
+ toggleCommit,
606
+ setCommitMultiSelect,
607
+ clearCommitSelection,
608
+ selectPath,
609
+ dispose: () => {
610
+ if (disposed)
611
+ return;
612
+ disposed = true;
613
+ ++branchesRequest;
614
+ ++historyRequest;
615
+ ++filesRequest;
616
+ ++diffRequest;
617
+ listeners.clear();
618
+ },
619
+ };
620
+ return controller;
621
+ }
622
+ /** React adapter; the first Git history request starts in the mount effect. */
623
+ export function useWorktreeGitState(input) {
624
+ const controller = useMemo(() => createWorktreeGitStateController(input), [
625
+ input.manager,
626
+ input.workspaceId,
627
+ input.worktreeId,
628
+ input.defaultBaselineBranch,
629
+ input.capturedBaseline,
630
+ ]);
631
+ const snapshot = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
632
+ useEffect(() => {
633
+ void controller.loadBranches();
634
+ void controller.loadHistory();
635
+ return controller.dispose;
636
+ }, [controller]);
637
+ return {
638
+ ...snapshot,
639
+ loadBranches: controller.loadBranches,
640
+ loadHistory: controller.loadHistory,
641
+ refresh: controller.refresh,
642
+ selectBaselineBranch: controller.selectBaselineBranch,
643
+ selectView: controller.selectView,
644
+ selectSummary: controller.selectSummary,
645
+ setIncludeWorkingTree: controller.setIncludeWorkingTree,
646
+ selectCommit: controller.selectCommit,
647
+ toggleCommit: controller.toggleCommit,
648
+ setCommitMultiSelect: controller.setCommitMultiSelect,
649
+ clearCommitSelection: controller.clearCommitSelection,
650
+ selectPath: controller.selectPath,
651
+ dispose: controller.dispose,
652
+ };
653
+ }
654
+ //# sourceMappingURL=useWorktreeGitState.js.map