@cerbur/clutch-dsh-worktree 0.1.17 → 0.1.18

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 (177) hide show
  1. package/README.md +105 -33
  2. package/README.zh.md +92 -29
  3. package/lib/client/WorktreeSurface.d.ts.map +1 -1
  4. package/lib/client/WorktreeSurface.js +37 -10
  5. package/lib/client/WorktreeSurface.js.map +1 -1
  6. package/lib/client/branch/GitBranchSelector.d.ts +23 -0
  7. package/lib/client/branch/GitBranchSelector.d.ts.map +1 -0
  8. package/lib/client/branch/GitBranchSelector.js +142 -0
  9. package/lib/client/branch/GitBranchSelector.js.map +1 -0
  10. package/lib/client/context/worktree-context-store.d.ts.map +1 -1
  11. package/lib/client/context/worktree-context-store.js +1 -0
  12. package/lib/client/context/worktree-context-store.js.map +1 -1
  13. package/lib/client/context/worktree-context.d.ts +3 -2
  14. package/lib/client/context/worktree-context.d.ts.map +1 -1
  15. package/lib/client/context/worktree-context.js +5 -3
  16. package/lib/client/context/worktree-context.js.map +1 -1
  17. package/lib/client/dashboard/WorktreeDashboard.d.ts +4 -1
  18. package/lib/client/dashboard/WorktreeDashboard.d.ts.map +1 -1
  19. package/lib/client/dashboard/WorktreeDashboard.js +67 -69
  20. package/lib/client/dashboard/WorktreeDashboard.js.map +1 -1
  21. package/lib/client/dashboard/dashboard-selection.d.ts +4 -3
  22. package/lib/client/dashboard/dashboard-selection.d.ts.map +1 -1
  23. package/lib/client/dashboard/dashboard-selection.js +6 -4
  24. package/lib/client/dashboard/dashboard-selection.js.map +1 -1
  25. package/lib/client/dashboard/git/GitChangedFiles.d.ts +2 -1
  26. package/lib/client/dashboard/git/GitChangedFiles.d.ts.map +1 -1
  27. package/lib/client/dashboard/git/GitChangedFiles.js +8 -6
  28. package/lib/client/dashboard/git/GitChangedFiles.js.map +1 -1
  29. package/lib/client/dashboard/git/GitCommitList.d.ts +6 -1
  30. package/lib/client/dashboard/git/GitCommitList.d.ts.map +1 -1
  31. package/lib/client/dashboard/git/GitCommitList.js +34 -29
  32. package/lib/client/dashboard/git/GitCommitList.js.map +1 -1
  33. package/lib/client/dashboard/git/GitCommitStats.d.ts +8 -0
  34. package/lib/client/dashboard/git/GitCommitStats.d.ts.map +1 -0
  35. package/lib/client/dashboard/git/GitCommitStats.js +7 -0
  36. package/lib/client/dashboard/git/GitCommitStats.js.map +1 -0
  37. package/lib/client/dashboard/git/WorktreeGitOverview.d.ts.map +1 -1
  38. package/lib/client/dashboard/git/WorktreeGitOverview.js +2 -1
  39. package/lib/client/dashboard/git/WorktreeGitOverview.js.map +1 -1
  40. package/lib/client/dashboard/git/WorktreeGitPanel.d.ts +1 -1
  41. package/lib/client/dashboard/git/WorktreeGitPanel.d.ts.map +1 -1
  42. package/lib/client/dashboard/git/WorktreeGitPanel.js +28 -9
  43. package/lib/client/dashboard/git/WorktreeGitPanel.js.map +1 -1
  44. package/lib/client/dashboard/git/file-address.d.ts +5 -2
  45. package/lib/client/dashboard/git/file-address.d.ts.map +1 -1
  46. package/lib/client/dashboard/git/file-address.js +50 -7
  47. package/lib/client/dashboard/git/file-address.js.map +1 -1
  48. package/lib/client/dashboard/git/git-facts.js +1 -1
  49. package/lib/client/dashboard/git/git-facts.js.map +1 -1
  50. package/lib/client/dashboard/git/useWorktreeGitState.d.ts +7 -2
  51. package/lib/client/dashboard/git/useWorktreeGitState.d.ts.map +1 -1
  52. package/lib/client/dashboard/git/useWorktreeGitState.js +84 -21
  53. package/lib/client/dashboard/git/useWorktreeGitState.js.map +1 -1
  54. package/lib/client/dsh-icons.d.ts +2 -0
  55. package/lib/client/dsh-icons.d.ts.map +1 -1
  56. package/lib/client/dsh-icons.js +2 -0
  57. package/lib/client/dsh-icons.js.map +1 -1
  58. package/lib/client/locales.d.ts +40 -2
  59. package/lib/client/locales.d.ts.map +1 -1
  60. package/lib/client/locales.js +46 -8
  61. package/lib/client/locales.js.map +1 -1
  62. package/lib/client/select/SearchableSingleSelect.d.ts +44 -0
  63. package/lib/client/select/SearchableSingleSelect.d.ts.map +1 -0
  64. package/lib/client/select/SearchableSingleSelect.js +180 -0
  65. package/lib/client/select/SearchableSingleSelect.js.map +1 -0
  66. package/lib/client/select/WorktreeSelector.d.ts +14 -0
  67. package/lib/client/select/WorktreeSelector.d.ts.map +1 -0
  68. package/lib/client/select/WorktreeSelector.js +11 -0
  69. package/lib/client/select/WorktreeSelector.js.map +1 -0
  70. package/lib/client/surface/actions/useLifecycleActions.js +1 -1
  71. package/lib/client/surface/actions/useLifecycleActions.js.map +1 -1
  72. package/lib/client/surface/actions/useWorktreeRegistration.d.ts +1 -1
  73. package/lib/client/surface/actions/useWorktreeRegistration.d.ts.map +1 -1
  74. package/lib/client/surface/actions/useWorktreeRegistration.js +32 -11
  75. package/lib/client/surface/actions/useWorktreeRegistration.js.map +1 -1
  76. package/lib/client/surface/components/RegistrationDialog.d.ts +3 -3
  77. package/lib/client/surface/components/RegistrationDialog.d.ts.map +1 -1
  78. package/lib/client/surface/components/RegistrationDialog.js +70 -5
  79. package/lib/client/surface/components/RegistrationDialog.js.map +1 -1
  80. package/lib/client/surface/components/WorkspaceTree.d.ts.map +1 -1
  81. package/lib/client/surface/components/WorkspaceTree.js +11 -8
  82. package/lib/client/surface/components/WorkspaceTree.js.map +1 -1
  83. package/lib/client/surface/components/dialogs.d.ts +1 -1
  84. package/lib/client/surface/components/dialogs.d.ts.map +1 -1
  85. package/lib/client/surface/components/dialogs.js +24 -15
  86. package/lib/client/surface/components/dialogs.js.map +1 -1
  87. package/lib/client/surface/state/useRegistrationState.d.ts +6 -1
  88. package/lib/client/surface/state/useRegistrationState.d.ts.map +1 -1
  89. package/lib/client/surface/state/useRegistrationState.js +7 -0
  90. package/lib/client/surface/state/useRegistrationState.js.map +1 -1
  91. package/lib/client/surface/types.d.ts +5 -1
  92. package/lib/client/surface/types.d.ts.map +1 -1
  93. package/lib/client/view/worktree-error-copy.d.ts.map +1 -1
  94. package/lib/client/view/worktree-error-copy.js +4 -0
  95. package/lib/client/view/worktree-error-copy.js.map +1 -1
  96. package/lib/client/view/worktree-view-read.d.ts +4 -2
  97. package/lib/client/view/worktree-view-read.d.ts.map +1 -1
  98. package/lib/client/view/worktree-view-read.js +34 -17
  99. package/lib/client/view/worktree-view-read.js.map +1 -1
  100. package/lib/client/worktree-connection.d.ts +3 -0
  101. package/lib/client/worktree-connection.d.ts.map +1 -1
  102. package/lib/client/worktree-connection.js +18 -1
  103. package/lib/client/worktree-connection.js.map +1 -1
  104. package/lib/client.js +1386 -434
  105. package/lib/client.js.map +1 -1
  106. package/lib/contract/index.contract.js +15 -0
  107. package/lib/contract/index.contract.js.map +1 -1
  108. package/lib/contract/index.d.ts +69 -12
  109. package/lib/contract/index.d.ts.map +1 -1
  110. package/lib/contract/index.js +3 -0
  111. package/lib/contract/index.js.map +1 -1
  112. package/lib/host/remote.d.ts.map +1 -1
  113. package/lib/host/remote.js +7 -0
  114. package/lib/host/remote.js.map +1 -1
  115. package/lib/host/service.d.ts +13 -6
  116. package/lib/host/service.d.ts.map +1 -1
  117. package/lib/host/service.js +18 -0
  118. package/lib/host/service.js.map +1 -1
  119. package/lib/manage/manager-git-history.d.ts +5 -7
  120. package/lib/manage/manager-git-history.d.ts.map +1 -1
  121. package/lib/manage/manager-git-history.js +158 -65
  122. package/lib/manage/manager-git-history.js.map +1 -1
  123. package/lib/manage/manager-git-paths.d.ts +7 -0
  124. package/lib/manage/manager-git-paths.d.ts.map +1 -0
  125. package/lib/manage/manager-git-paths.js +62 -0
  126. package/lib/manage/manager-git-paths.js.map +1 -0
  127. package/lib/manage/manager-worktrees.d.ts +9 -1
  128. package/lib/manage/manager-worktrees.d.ts.map +1 -1
  129. package/lib/manage/manager-worktrees.js +69 -14
  130. package/lib/manage/manager-worktrees.js.map +1 -1
  131. package/lib/manage/manager.d.ts +12 -5
  132. package/lib/manage/manager.d.ts.map +1 -1
  133. package/lib/manage/manager.js +64 -3
  134. package/lib/manage/manager.js.map +1 -1
  135. package/lib/provider/git/adapter.d.ts +13 -12
  136. package/lib/provider/git/adapter.d.ts.map +1 -1
  137. package/lib/provider/git/adapter.js +152 -29
  138. package/lib/provider/git/adapter.js.map +1 -1
  139. package/lib/provider/git/branch-name.d.ts +5 -0
  140. package/lib/provider/git/branch-name.d.ts.map +1 -0
  141. package/lib/provider/git/branch-name.js +16 -0
  142. package/lib/provider/git/branch-name.js.map +1 -0
  143. package/lib/provider/git/branches.d.ts +11 -0
  144. package/lib/provider/git/branches.d.ts.map +1 -0
  145. package/lib/provider/git/branches.js +37 -0
  146. package/lib/provider/git/branches.js.map +1 -0
  147. package/lib/provider/sidecar/repository.d.ts.map +1 -1
  148. package/lib/provider/sidecar/repository.js +2 -1
  149. package/lib/provider/sidecar/repository.js.map +1 -1
  150. package/lib/provider/sidecar/sidecar-schema.d.ts.map +1 -1
  151. package/lib/provider/sidecar/sidecar-schema.js +2 -0
  152. package/lib/provider/sidecar/sidecar-schema.js.map +1 -1
  153. package/lib/provider/transaction/operations/create.d.ts.map +1 -1
  154. package/lib/provider/transaction/operations/create.js +18 -6
  155. package/lib/provider/transaction/operations/create.js.map +1 -1
  156. package/lib/provider/transaction/recovery/failure-recovery.d.ts.map +1 -1
  157. package/lib/provider/transaction/recovery/failure-recovery.js +3 -2
  158. package/lib/provider/transaction/recovery/failure-recovery.js.map +1 -1
  159. package/lib/provider/transaction/recovery/recover.d.ts.map +1 -1
  160. package/lib/provider/transaction/recovery/recover.js +8 -5
  161. package/lib/provider/transaction/recovery/recover.js.map +1 -1
  162. package/lib/provider/transaction/support/journal.d.ts.map +1 -1
  163. package/lib/provider/transaction/support/journal.js +1 -0
  164. package/lib/provider/transaction/support/journal.js.map +1 -1
  165. package/lib/provider/types.d.ts +24 -4
  166. package/lib/provider/types.d.ts.map +1 -1
  167. package/lib/provider/types.js.map +1 -1
  168. package/lib/tool/list-worktrees.d.ts.map +1 -1
  169. package/lib/tool/list-worktrees.js +9 -2
  170. package/lib/tool/list-worktrees.js.map +1 -1
  171. package/lib/tool/new-worktree.js +2 -2
  172. package/lib/tool/new-worktree.js.map +1 -1
  173. package/lib/typert.host.js +216 -46
  174. package/lib/typert.remote-client.d.ts +11 -5
  175. package/lib/typert.remote-client.d.ts.map +1 -1
  176. package/lib/typert.remote-client.js +216 -46
  177. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"manager-git-history.d.ts","sourceRoot":"","sources":["../../src/manage/manager-git-history.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAElC,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EAExB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AA4RnE,iFAAiF;AACjF,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IACL,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC,GACA,OAAO,CAAC,MAAM,CAAC,CA4EjB;AAwRD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACjG,OAAO,CAAC,kBAAkB,CAAC,CA8C7B;AAuYD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAiEjC;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,mBAAmB,CAAC,CA0I9B"}
1
+ {"version":3,"file":"manager-git-history.d.ts","sourceRoot":"","sources":["../../src/manage/manager-git-history.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAElC,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAE/B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAuSnE,wFAAwF;AACxF,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IACL,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC,GACA,OAAO,CAAC,MAAM,CAAC,CAkFjB;AA4TD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAoE7B;AAwnBD,sGAAsG;AACtG,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAMjC;AAED,yGAAyG;AACzG,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAM9B"}
@@ -1,7 +1,11 @@
1
+ import { isSafeBranchIdentity } from '../provider/git/branch-name.js';
2
+ import { readBranchFacts, findBranchIdentity, canonicalBranchIdentity } from '../provider/git/branches.js';
1
3
  import { gitWorktreeRoot } from '../contract/index.js';
2
4
  import { WORKTREE_GIT_COMMIT_PATTERN, WORKTREE_GIT_SUMMARY, WORKTREE_GIT_WORKING_TREE, } from '../contract/index.js';
3
5
  import { providerError } from '../provider/types.js';
4
6
  import { isDirectory, requireWorkspace, samePhysicalPath } from './manager-support.js';
7
+ import { projectWorktreePath } from '../provider/worktree-path.js';
8
+ import { workspaceGitPaths } from './manager-git-paths.js';
5
9
  /** Shared with Contract/Provider/Client so commit-SHA validation cannot drift. */
6
10
  const COMMIT_PATTERN = WORKTREE_GIT_COMMIT_PATTERN;
7
11
  /**
@@ -57,14 +61,32 @@ async function findLiveWorktree(worktrees, absolutePath) {
57
61
  }
58
62
  return undefined;
59
63
  }
60
- async function resolveWorktree(context, input) {
64
+ async function resolveWorktree(context, input, compareMain = true) {
61
65
  const workspace = await requireWorkspace(context, input.workspaceId);
62
66
  if (isMainWorktreeId(input.worktreeId, input.workspaceId)) {
63
67
  // The imported Workspace can be nested, while committed Git paths are root-relative.
64
68
  const repositoryRoot = context.git.resolveRepositoryRoot
65
69
  ? await context.git.resolveRepositoryRoot(workspace.rootPath, { signal: context.signal })
66
70
  : workspace.rootPath;
67
- return { main: true, workspaceRoot: workspace.rootPath, repositoryRoot };
71
+ if (compareMain && input.baseBranch !== undefined) {
72
+ // A request-local checkout context reuses comparison and path authorization.
73
+ // It is never read from or written to the sidecar.
74
+ const headCommit = await gitRead(context, 'resolveCommit', input.worktreeId)(repositoryRoot, 'HEAD', { signal: context.signal });
75
+ return { main: false, value: {
76
+ workspaceRoot: workspace.rootPath,
77
+ repositoryRoot,
78
+ record: {
79
+ workspaceId: input.workspaceId, worktreeId: input.worktreeId,
80
+ absolutePath: workspace.rootPath, gitRootPath: repositoryRoot,
81
+ branch: 'HEAD', source: 'external', status: 'active',
82
+ },
83
+ live: { absolutePath: repositoryRoot, headCommit },
84
+ } };
85
+ }
86
+ const headCommit = compareMain && context.git.resolveCommit
87
+ ? await context.git.resolveCommit(repositoryRoot, 'HEAD', { signal: context.signal })
88
+ : undefined;
89
+ return { main: true, workspaceRoot: workspace.rootPath, repositoryRoot, headCommit };
68
90
  }
69
91
  const snapshot = await context.sidecar.read(input.workspaceId);
70
92
  if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
@@ -110,31 +132,16 @@ async function resolveWorktree(context, input) {
110
132
  workspaceRoot: workspace.rootPath,
111
133
  repositoryRoot,
112
134
  record,
113
- live,
135
+ live: live.headCommit === undefined && context.git.resolveCommit
136
+ ? { ...live, headCommit: await context.git.resolveCommit(gitWorktreeRoot(record), 'HEAD', { signal: context.signal }) }
137
+ : live,
114
138
  },
115
139
  };
116
140
  }
117
- /** True when a branch name can be safely handed to Git as a ref argument. */
118
- function isSafeBranchName(branch) {
119
- if (branch.length === 0 || branch.startsWith('-'))
120
- return false;
121
- // Only a local branch short name may be selected. A full ref path is an
122
- // arbitrary-ref attempt and is rejected instead of degrading to unavailable.
123
- if (branch.startsWith('refs/'))
124
- return false;
125
- for (const character of branch) {
126
- const code = character.codePointAt(0) ?? 0;
127
- if (code < 0x20 || code === 0x7f)
128
- return false;
129
- }
130
- return true;
131
- }
132
141
  /**
133
- * Resolve one local branch tip for the Dashboard baseline. The branch must exist
134
- * under `refs/heads/`, so tags, remote-tracking branches, and arbitrary refs are
135
- * rejected even though the browser may send any string.
142
+ * Resolve only enumerated branch identities. Tags and arbitrary revision expressions are rejected.
136
143
  */
137
- async function resolveLocalBranchCommit(context, resolved, baseBranch, options = {}) {
144
+ async function resolveBranchCommit(context, resolved, baseBranch, options = {}) {
138
145
  const unavailable = () => {
139
146
  throw providerError('WORKTREE_STATE_CONFLICT', 'Selected baseline branch is unavailable: ' + baseBranch, {
140
147
  worktreeId: resolved.record.worktreeId,
@@ -143,20 +150,20 @@ async function resolveLocalBranchCommit(context, resolved, baseBranch, options =
143
150
  };
144
151
  // An unsafe ref is always a hard failure: arbitrary refs must never resolve,
145
152
  // even for a read that is allowed to degrade to an unavailable projection.
146
- if (!isSafeBranchName(baseBranch))
153
+ if (!isSafeBranchIdentity(baseBranch))
147
154
  unavailable();
148
- const listBranches = gitRead(context, 'listBranches', resolved.record.worktreeId);
149
155
  const resolveCommit = gitRead(context, 'resolveCommit', resolved.record.worktreeId);
150
- const branches = await listBranches(resolved.repositoryRoot, { signal: context.signal });
156
+ const branches = await readBranchFacts(context.git, resolved.repositoryRoot, { signal: context.signal });
157
+ const branch = findBranchIdentity(branches, baseBranch);
151
158
  // A baseline branch that no longer exists is an expected unavailable state for
152
159
  // reads, so the Dashboard can show an honest empty projection instead of a
153
160
  // generic Git failure. Writes keep the strict behaviour.
154
- if (!branches.includes(baseBranch)) {
161
+ if (branch === undefined) {
155
162
  if (options.allowUnavailable === true)
156
163
  return undefined;
157
164
  unavailable();
158
165
  }
159
- const commit = await resolveCommit(gitWorktreeRoot(resolved.record), baseBranch, {
166
+ const commit = await resolveCommit(gitWorktreeRoot(resolved.record), branch?.ref ?? baseBranch, {
160
167
  signal: context.signal,
161
168
  });
162
169
  if (!COMMIT_PATTERN.test(commit)) {
@@ -165,18 +172,19 @@ async function resolveLocalBranchCommit(context, resolved, baseBranch, options =
165
172
  baseBranch,
166
173
  });
167
174
  }
168
- return commit;
175
+ return { commit, branch: branch };
169
176
  }
170
177
  async function resolveSelectedBaseline(context, resolved, rawBranch) {
171
178
  const baseBranch = rawBranch.trim();
172
179
  if (baseBranch.length === 0)
173
180
  return undefined;
174
181
  // A vanished baseline branch degrades to an unavailable projection for reads.
175
- const commit = await resolveLocalBranchCommit(context, resolved, baseBranch, {
182
+ const selected = await resolveBranchCommit(context, resolved, baseBranch, {
176
183
  allowUnavailable: true,
177
184
  });
178
- if (commit === undefined)
185
+ if (selected === undefined)
179
186
  return undefined;
187
+ const { commit } = selected;
180
188
  // A selected branch can move after the Worktree was created. Match GitHub's
181
189
  // pull-request range by diffing from the two heads' merge base to Worktree
182
190
  // HEAD, while retaining the selected branch tip for ahead/behind counts.
@@ -201,14 +209,14 @@ async function resolveSelectedBaseline(context, resolved, rawBranch) {
201
209
  treeBaseCommit: mergeBase ?? commit,
202
210
  };
203
211
  }
204
- /** Persist a user-selected local branch as the Worktree's Dashboard baseline. */
212
+ /** Persist a user-selected local or fetched remote branch as the Dashboard baseline. */
205
213
  export async function updateWorktreeBaseBranch(context, input) {
206
214
  if (typeof input.baseBranch !== 'string') {
207
215
  throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch must be a string', {
208
216
  worktreeId: input.worktreeId,
209
217
  });
210
218
  }
211
- const baseBranch = input.baseBranch.trim();
219
+ let baseBranch = input.baseBranch.trim();
212
220
  if (baseBranch.length === 0) {
213
221
  throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be empty', {
214
222
  worktreeId: input.worktreeId,
@@ -220,7 +228,7 @@ export async function updateWorktreeBaseBranch(context, input) {
220
228
  });
221
229
  }
222
230
  const expectedBaseBranch = input.expectedBaseBranch?.trim() || undefined;
223
- const resolved = await resolveWorktree(context, input);
231
+ const resolved = await resolveWorktree(context, input, false);
224
232
  if (resolved.main) {
225
233
  throw providerError('WORKTREE_STATE_CONFLICT', 'The local Workspace has no editable baseline', {
226
234
  workspaceId: input.workspaceId,
@@ -235,7 +243,13 @@ export async function updateWorktreeBaseBranch(context, input) {
235
243
  }
236
244
  // Resolve the replacement branch against the pinned repository root before the
237
245
  // sidecar mutation, so an unavailable branch fails closed without a write.
238
- await resolveLocalBranchCommit(context, resolved.value, baseBranch);
246
+ const selected = await resolveBranchCommit(context, resolved.value, baseBranch);
247
+ if (selected.branch.kind !== 'remote' && selected.branch.name === currentBranch) {
248
+ throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be the current Worktree branch', {
249
+ worktreeId: input.worktreeId, baseBranch,
250
+ });
251
+ }
252
+ baseBranch = canonicalBranchIdentity(selected.branch, baseBranch);
239
253
  return context.sidecar.mutate(input.workspaceId, (snapshot) => {
240
254
  if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
241
255
  throw providerError('WORKTREE_RECOVERY_REQUIRED', 'Workspace Worktree state needs recovery: ' + input.workspaceId, { workspaceId: input.workspaceId });
@@ -327,7 +341,7 @@ async function resolveBaseline(context, resolved, requestedBaseBranch) {
327
341
  async function requireBaseline(context, resolved, requestedBaseBranch) {
328
342
  const baseline = await resolveBaseline(context, resolved, requestedBaseBranch);
329
343
  if (baseline === undefined) {
330
- throw providerError('WORKTREE_STATE_CONFLICT', 'Select a local baseline branch before reading Git changes', { worktreeId: resolved.record.worktreeId });
344
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Select a baseline branch before reading Git changes', { worktreeId: resolved.record.worktreeId });
331
345
  }
332
346
  return baseline;
333
347
  }
@@ -351,13 +365,9 @@ async function readCommitDivergence(context, resolved, comparison, head) {
351
365
  /**
352
366
  * Authorize commit SHAs against one pinned comparison projection.
353
367
  *
354
- * One bounded `listCommits` read yields exactly the authorized set: commits
355
- * reachable from the Worktree HEAD and not reachable from the resolved baseline
356
- * head. That is the same membership the previous per-SHA ancestry walks proved,
357
- * but a wide multi-commit selection now costs one projection read instead of
358
- * several Git processes per candidate SHA. The visible projection is therefore
359
- * the authorization boundary, matching the truncated 200-commit history the
360
- * browser can actually select from.
368
+ * Candidates must be reachable from HEAD and excluded from baseline ancestry,
369
+ * independently of which 50-commit page the browser has loaded. Legacy adapters
370
+ * without the membership capability retain bounded projection authorization.
361
371
  */
362
372
  async function authorizeCommits(context, resolved, comparison, candidates) {
363
373
  const worktreeId = resolved.record.worktreeId;
@@ -369,6 +379,11 @@ async function authorizeCommits(context, resolved, comparison, candidates) {
369
379
  }
370
380
  return candidate;
371
381
  });
382
+ const headCommit = resolved.live.headCommit;
383
+ if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
384
+ await authorizeMembership(context, gitWorktreeRoot(resolved.record), worktreeId, requested, headCommit, comparison.baseHeadCommit);
385
+ return requested;
386
+ }
372
387
  const listCommits = gitRead(context, 'listCommits', worktreeId);
373
388
  const history = await listCommits(gitWorktreeRoot(resolved.record), comparison.baseHeadCommit, {
374
389
  signal: context.signal,
@@ -384,6 +399,22 @@ async function authorizeCommits(context, resolved, comparison, candidates) {
384
399
  }
385
400
  return requested;
386
401
  }
402
+ /** Share a fixed checkout identity across all membership checks in a request. */
403
+ async function authorizeMembership(context, root, worktreeId, commits, headCommit, baseCommit) {
404
+ const options = { signal: context.signal };
405
+ const allowed = context.git.containsCommits
406
+ ? await context.git.containsCommits(root, commits, headCommit, baseCommit, options)
407
+ : await (async () => {
408
+ for (const commit of commits) {
409
+ if (!await context.git.containsCommit(root, commit, headCommit, baseCommit, options))
410
+ return false;
411
+ }
412
+ return true;
413
+ })();
414
+ if (!allowed) {
415
+ throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commits are outside this checkout history', { worktreeId });
416
+ }
417
+ }
387
418
  async function authorizeCommit(context, resolved, commitInput, requestedBaseBranch) {
388
419
  const comparison = await requireBaseline(context, resolved, requestedBaseBranch);
389
420
  const [commit] = await authorizeCommits(context, resolved, comparison, [commitInput]);
@@ -396,13 +427,13 @@ async function authorizeWorkingTree(context, resolved, requestedBaseBranch) {
396
427
  await requireBaseline(context, resolved, requestedBaseBranch);
397
428
  }
398
429
  /** Read Main's bounded HEAD history and expose live Workspace-root changes first. */
399
- async function listMainCommits(context, workspaceRoot, worktreeId) {
430
+ async function listMainCommits(context, workspaceRoot, worktreeId, offset = 0) {
400
431
  const listCommits = gitRead(context, 'listCommits', worktreeId);
401
432
  const readWorkingTreePaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
402
433
  ?? gitReadOptional(context, 'listWorkingTreeFiles');
403
434
  const [history, workingTreeFiles] = await Promise.all([
404
- listCommits(workspaceRoot, undefined, { signal: context.signal }),
405
- readWorkingTreePaths === undefined
435
+ listCommits(workspaceRoot, undefined, { signal: context.signal, offset }),
436
+ offset > 0 || readWorkingTreePaths === undefined
406
437
  ? Promise.resolve({ files: [], truncated: false })
407
438
  : readWorkingTreePaths(workspaceRoot, { signal: context.signal }),
408
439
  ]);
@@ -414,12 +445,16 @@ async function listMainCommits(context, workspaceRoot, worktreeId) {
414
445
  : history.commits,
415
446
  };
416
447
  }
417
- async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInput) {
448
+ async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInput, headCommit) {
418
449
  if (typeof commitInput !== 'string' || !COMMIT_PATTERN.test(commitInput)) {
419
450
  throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is not a valid commit SHA', {
420
451
  worktreeId,
421
452
  });
422
453
  }
454
+ if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
455
+ await authorizeMembership(context, workspaceRoot, worktreeId, [commitInput], headCommit);
456
+ return commitInput;
457
+ }
423
458
  const history = await listMainCommits(context, workspaceRoot, worktreeId);
424
459
  if (!history.commits.some((commit) => commit.sha === commitInput)) {
425
460
  throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is outside the visible Main history', {
@@ -430,7 +465,7 @@ async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInp
430
465
  return commitInput;
431
466
  }
432
467
  const MAX_AGGREGATE_COMMITS = 200;
433
- async function authorizeMainAggregate(context, workspaceRoot, worktreeId, selection) {
468
+ async function authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit) {
434
469
  if (selection.kind !== 'commits') {
435
470
  throw providerError('WORKTREE_STATE_CONFLICT', 'Main Git supports committed selections only', {
436
471
  worktreeId,
@@ -454,6 +489,10 @@ async function authorizeMainAggregate(context, workspaceRoot, worktreeId, select
454
489
  worktreeId,
455
490
  });
456
491
  }
492
+ if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
493
+ await authorizeMembership(context, workspaceRoot, worktreeId, requested, headCommit);
494
+ return { selection: { kind: 'commits', commits: requested }, commits: requested };
495
+ }
457
496
  const history = await listMainCommits(context, workspaceRoot, worktreeId);
458
497
  const members = new Set(history.commits
459
498
  .filter((commit) => commit.kind !== 'working-tree')
@@ -472,10 +511,26 @@ async function authorizeMainAggregate(context, workspaceRoot, worktreeId, select
472
511
  };
473
512
  }
474
513
  export async function listWorktreeCommits(context, input) {
514
+ const offset = input.offset ?? 0;
515
+ if (!Number.isSafeInteger(offset) || offset < 0 ||
516
+ (offset > 0 && (input.expectedHeadCommit === undefined ||
517
+ (input.baseBranch !== undefined && input.expectedBaselineCommit === undefined)))) {
518
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Invalid history page', { worktreeId: input.worktreeId });
519
+ }
520
+ const checkProjection = (head, baseline) => {
521
+ if ((input.expectedHeadCommit !== undefined && input.expectedHeadCommit !== head) ||
522
+ (input.expectedBaselineCommit !== undefined && input.expectedBaselineCommit !== baseline)) {
523
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Git history changed; refresh before loading more', {
524
+ worktreeId: input.worktreeId,
525
+ });
526
+ }
527
+ };
475
528
  const resolved = await resolveWorktree(context, input);
476
529
  if (resolved.main) {
477
- const history = await listMainCommits(context, resolved.workspaceRoot, input.worktreeId);
530
+ const history = await listMainCommits(context, resolved.repositoryRoot, input.worktreeId, offset);
531
+ checkProjection(history.headCommit);
478
532
  return {
533
+ ...(history.nextOffset === undefined ? {} : { nextOffset: history.nextOffset }),
479
534
  headCommit: history.headCommit,
480
535
  commits: history.commits,
481
536
  truncated: history.truncated,
@@ -488,17 +543,23 @@ export async function listWorktreeCommits(context, input) {
488
543
  ? 'baseline-unselected'
489
544
  : 'baseline-unknown');
490
545
  }
546
+ if (offset > 0 && input.expectedBaselineCommit === undefined) {
547
+ throw providerError('WORKTREE_STATE_CONFLICT', 'A comparison page requires its baseline commit', {
548
+ worktreeId: input.worktreeId,
549
+ });
550
+ }
491
551
  const listCommits = gitRead(context, 'listCommits', input.worktreeId);
492
552
  // The working-tree entry only needs the changed-path projection here; its file
493
553
  // list and line counts are read on demand when the entry is selected.
494
554
  const readWorkingTreePaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
495
555
  ?? gitReadOptional(context, 'listWorkingTreeFiles');
496
556
  const [history, workingTreeFiles] = await Promise.all([
497
- listCommits(gitWorktreeRoot(resolved.value.record), baseline.baseHeadCommit, { signal: context.signal }),
498
- readWorkingTreePaths === undefined
557
+ listCommits(gitWorktreeRoot(resolved.value.record), baseline.baseHeadCommit, { signal: context.signal, offset }),
558
+ offset > 0 || readWorkingTreePaths === undefined
499
559
  ? Promise.resolve({ files: [], truncated: false })
500
560
  : readWorkingTreePaths(gitWorktreeRoot(resolved.value.record), { signal: context.signal }),
501
561
  ]);
562
+ checkProjection(history.headCommit, baseline.baseline.commit);
502
563
  const divergence = await readCommitDivergence(context, resolved.value, baseline, history.headCommit);
503
564
  return {
504
565
  ...history,
@@ -672,17 +733,17 @@ async function selectedAggregateFileDiff(context, absolutePath, worktreeId, sele
672
733
  segments,
673
734
  };
674
735
  }
675
- async function aggregateMainFileDiff(context, workspaceRoot, repositoryRoot, worktreeId, selection, requestedPath) {
736
+ async function aggregateMainFileDiff(context, workspaceRoot, repositoryRoot, worktreeId, selection, requestedPath, headCommit) {
676
737
  if (typeof requestedPath !== 'string' || requestedPath.length === 0) {
677
738
  throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
678
739
  worktreeId,
679
740
  });
680
741
  }
681
- const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection);
742
+ const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit);
682
743
  return selectedAggregateFileDiff(context, repositoryRoot, worktreeId, authorized.selection, authorized.commits, requestedPath);
683
744
  }
684
- async function listMainAggregateFiles(context, workspaceRoot, repositoryRoot, worktreeId, selection) {
685
- const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection);
745
+ async function listMainAggregateFiles(context, workspaceRoot, repositoryRoot, worktreeId, selection, headCommit) {
746
+ const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit);
686
747
  const groups = await selectedCommitFiles(context, repositoryRoot, worktreeId, authorized.commits);
687
748
  return {
688
749
  commit: authorized.commits[0],
@@ -736,23 +797,22 @@ async function aggregateFileDiff(context, resolved, selection, requestedPath, re
736
797
  }
737
798
  return selectedAggregateFileDiff(context, gitWorktreeRoot(resolved.record), resolved.record.worktreeId, authorized.selection, authorized.commits, requestedPath);
738
799
  }
739
- export async function listWorktreeCommitFiles(context, input) {
740
- const resolved = await resolveWorktree(context, input);
800
+ async function listCheckoutCommitFiles(context, input, resolved) {
741
801
  const requested = requestedDiff(input, input.worktreeId);
742
802
  if (resolved.main) {
743
803
  if (requested.kind === 'aggregate') {
744
- return listMainAggregateFiles(context, resolved.workspaceRoot, resolved.repositoryRoot, input.worktreeId, requested.selection);
804
+ return listMainAggregateFiles(context, resolved.repositoryRoot, resolved.repositoryRoot, input.worktreeId, requested.selection, resolved.headCommit);
745
805
  }
746
806
  if (requested.commit === WORKTREE_GIT_WORKING_TREE) {
747
807
  const listWorkingTreeFiles = gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
748
- const read = await listWorkingTreeFiles(resolved.workspaceRoot, { signal: context.signal });
808
+ const read = await listWorkingTreeFiles(resolved.repositoryRoot, { signal: context.signal });
749
809
  return {
750
810
  commit: WORKTREE_GIT_WORKING_TREE,
751
811
  files: read.files,
752
812
  ...(read.truncated ? { truncated: true } : {}),
753
813
  };
754
814
  }
755
- const commit = await authorizeMainCommit(context, resolved.workspaceRoot, input.worktreeId, requested.commit);
815
+ const commit = await authorizeMainCommit(context, resolved.repositoryRoot, input.worktreeId, requested.commit, resolved.headCommit);
756
816
  const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
757
817
  const read = await listCommitFiles(resolved.repositoryRoot, commit, { signal: context.signal });
758
818
  return {
@@ -784,8 +844,7 @@ export async function listWorktreeCommitFiles(context, input) {
784
844
  ...(read.truncated ? { truncated: true } : {}),
785
845
  };
786
846
  }
787
- export async function getWorktreeCommitFileDiff(context, input) {
788
- const resolved = await resolveWorktree(context, input);
847
+ async function getCheckoutCommitFileDiff(context, input, resolved) {
789
848
  const requested = requestedDiff(input, input.worktreeId);
790
849
  if (resolved.main) {
791
850
  if (requested.kind === 'aggregate') {
@@ -794,7 +853,7 @@ export async function getWorktreeCommitFileDiff(context, input) {
794
853
  worktreeId: input.worktreeId,
795
854
  });
796
855
  }
797
- return aggregateMainFileDiff(context, resolved.workspaceRoot, resolved.repositoryRoot, input.worktreeId, requested.selection, input.path);
856
+ return aggregateMainFileDiff(context, resolved.repositoryRoot, resolved.repositoryRoot, input.worktreeId, requested.selection, input.path, resolved.headCommit);
798
857
  }
799
858
  if (typeof input.path !== 'string' || input.path.length === 0) {
800
859
  throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
@@ -805,7 +864,7 @@ export async function getWorktreeCommitFileDiff(context, input) {
805
864
  const readWorkingTreeFileDiff = gitRead(context, 'readWorkingTreeFileDiff', input.worktreeId);
806
865
  const readChangedPaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
807
866
  ?? gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
808
- const readPaths = await readChangedPaths(resolved.workspaceRoot, { signal: context.signal });
867
+ const readPaths = await readChangedPaths(resolved.repositoryRoot, { signal: context.signal });
809
868
  const changedFile = findChangedFile(readPaths.files, input.path);
810
869
  if (changedFile === undefined) {
811
870
  throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed in the Main working tree', {
@@ -814,10 +873,10 @@ export async function getWorktreeCommitFileDiff(context, input) {
814
873
  path: input.path,
815
874
  });
816
875
  }
817
- const diff = await readWorkingTreeFileDiff(resolved.workspaceRoot, changedFile.path, { signal: context.signal });
876
+ const diff = await readWorkingTreeFileDiff(resolved.repositoryRoot, changedFile.path, { signal: context.signal });
818
877
  return { ...diff, commit: WORKTREE_GIT_WORKING_TREE, path: input.path };
819
878
  }
820
- const commit = await authorizeMainCommit(context, resolved.workspaceRoot, input.worktreeId, requested.commit);
879
+ const commit = await authorizeMainCommit(context, resolved.repositoryRoot, input.worktreeId, requested.commit, resolved.headCommit);
821
880
  const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
822
881
  const filesRead = await listCommitFiles(resolved.repositoryRoot, commit, { signal: context.signal });
823
882
  const changedFile = findChangedFile(filesRead.files, input.path);
@@ -886,4 +945,38 @@ export async function getWorktreeCommitFileDiff(context, input) {
886
945
  path: input.path,
887
946
  };
888
947
  }
948
+ async function pathsFor(resolved) {
949
+ if (resolved.main)
950
+ return workspaceGitPaths(resolved.repositoryRoot, resolved.workspaceRoot);
951
+ const { record, workspaceRoot, repositoryRoot } = resolved.value;
952
+ const projected = await projectWorktreePath(record, workspaceRoot, repositoryRoot);
953
+ return workspaceGitPaths(gitWorktreeRoot(projected), projected.absolutePath);
954
+ }
955
+ /** Reject a projection if checkout HEAD moved during authorization or file reads. */
956
+ async function assertCheckoutHead(context, resolved, worktreeId) {
957
+ const headCommit = resolved.main ? resolved.headCommit : resolved.value.live.headCommit;
958
+ if (headCommit === undefined || context.git.resolveCommit === undefined)
959
+ return;
960
+ const root = resolved.main ? resolved.repositoryRoot : gitWorktreeRoot(resolved.value.record);
961
+ const current = await context.git.resolveCommit(root, 'HEAD', { signal: context.signal });
962
+ if (current !== headCommit) {
963
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Checkout HEAD changed; refresh Git data', { worktreeId });
964
+ }
965
+ }
966
+ /** Public changed-file identities always start at the Workspace directory, including ../ siblings. */
967
+ export async function listWorktreeCommitFiles(context, input) {
968
+ const resolved = await resolveWorktree(context, input);
969
+ const paths = await pathsFor(resolved);
970
+ const read = await listCheckoutCommitFiles(context, input, resolved);
971
+ await assertCheckoutHead(context, resolved, input.worktreeId);
972
+ return paths.projectFiles(read);
973
+ }
974
+ /** Translate exactly once, then keep the existing changed-file authorization in checkout coordinates. */
975
+ export async function getWorktreeCommitFileDiff(context, input) {
976
+ const resolved = await resolveWorktree(context, input);
977
+ const paths = await pathsFor(resolved);
978
+ const diff = await getCheckoutCommitFileDiff(context, { ...input, path: paths.toCheckout(input.path) }, resolved);
979
+ await assertCheckoutHead(context, resolved, input.worktreeId);
980
+ return { ...diff, path: input.path };
981
+ }
889
982
  //# sourceMappingURL=manager-git-history.js.map