dsh-git-worktree 0.3.2 → 0.6.0

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 (92) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.en.md +48 -18
  3. package/README.md +49 -18
  4. package/lib/adapters/git.js +8 -0
  5. package/lib/adapters/git.js.map +1 -1
  6. package/lib/adapters/registry.js +107 -2
  7. package/lib/adapters/registry.js.map +1 -1
  8. package/lib/checkpoint.d.ts +3 -0
  9. package/lib/checkpoint.js +29 -0
  10. package/lib/checkpoint.js.map +1 -0
  11. package/lib/client/WorktreeReviewRow.d.ts +1 -1
  12. package/lib/client/WorktreeReviewRow.js +26 -6
  13. package/lib/client/WorktreeReviewRow.js.map +1 -1
  14. package/lib/client/actions.d.ts +49 -4
  15. package/lib/client/actions.js +104 -0
  16. package/lib/client/actions.js.map +1 -1
  17. package/lib/client/console-remote/adapter.js +10 -3
  18. package/lib/client/console-remote/adapter.js.map +1 -1
  19. package/lib/client/review-console/DeliveryProof.d.ts +5 -0
  20. package/lib/client/review-console/DeliveryProof.js +32 -0
  21. package/lib/client/review-console/DeliveryProof.js.map +1 -0
  22. package/lib/client/review-console/PreflightStatus.d.ts +12 -0
  23. package/lib/client/review-console/PreflightStatus.js +31 -0
  24. package/lib/client/review-console/PreflightStatus.js.map +1 -0
  25. package/lib/client/review-console/ReviewActions.d.ts +4 -1
  26. package/lib/client/review-console/ReviewActions.js +657 -31
  27. package/lib/client/review-console/ReviewActions.js.map +1 -1
  28. package/lib/client/review-console/WorktreeReviewPanel.d.ts +3 -1
  29. package/lib/client/review-console/WorktreeReviewPanel.js +35 -5
  30. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -1
  31. package/lib/client/review-console/preflight-cache.d.ts +21 -0
  32. package/lib/client/review-console/preflight-cache.js +98 -0
  33. package/lib/client/review-console/preflight-cache.js.map +1 -0
  34. package/lib/client/review-console/preview-recovery-cache.d.ts +20 -0
  35. package/lib/client/review-console/preview-recovery-cache.js +103 -0
  36. package/lib/client/review-console/preview-recovery-cache.js.map +1 -0
  37. package/lib/client/review-console/recovery-continuation.d.ts +53 -0
  38. package/lib/client/review-console/recovery-continuation.js +483 -0
  39. package/lib/client/review-console/recovery-continuation.js.map +1 -0
  40. package/lib/client/review-console/review-console.styles.js +36 -4
  41. package/lib/client/review-console/review-console.styles.js.map +1 -1
  42. package/lib/client/target-console/TargetStatusAction.d.ts +4 -2
  43. package/lib/client/target-console/TargetStatusAction.js +196 -23
  44. package/lib/client/target-console/TargetStatusAction.js.map +1 -1
  45. package/lib/client/target-console/WorktreeConsoleView.d.ts +4 -2
  46. package/lib/client/target-console/WorktreeConsoleView.js +33 -73
  47. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -1
  48. package/lib/client/target-console/WorktreeManagerModal.d.ts +13 -0
  49. package/lib/client/target-console/WorktreeManagerModal.js +8 -0
  50. package/lib/client/target-console/WorktreeManagerModal.js.map +1 -0
  51. package/lib/client/target-console/WorktreeReviewStatus.d.ts +3 -1
  52. package/lib/client/target-console/WorktreeReviewStatus.js +35 -15
  53. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -1
  54. package/lib/client/target-console/index.d.ts +2 -1
  55. package/lib/client/target-console/index.js +4 -3
  56. package/lib/client/target-console/index.js.map +1 -1
  57. package/lib/client/target-console/target-console.styles.js +22 -3
  58. package/lib/client/target-console/target-console.styles.js.map +1 -1
  59. package/lib/client.js +2976 -211
  60. package/lib/console-contract.d.ts +110 -1
  61. package/lib/console-contract.js.map +1 -1
  62. package/lib/console-host/control-plane.d.ts +6 -1
  63. package/lib/console-host/control-plane.js +363 -44
  64. package/lib/console-host/control-plane.js.map +1 -1
  65. package/lib/console-host/errors.d.ts +2 -2
  66. package/lib/console-host/errors.js +21 -5
  67. package/lib/console-host/errors.js.map +1 -1
  68. package/lib/console-host/projection.d.ts +5 -3
  69. package/lib/console-host/projection.js +103 -11
  70. package/lib/console-host/projection.js.map +1 -1
  71. package/lib/console-host/service.d.ts +10 -5
  72. package/lib/console-host/service.js +48 -6
  73. package/lib/console-host/service.js.map +1 -1
  74. package/lib/console-remote/descriptors.js +37 -1
  75. package/lib/console-remote/descriptors.js.map +1 -1
  76. package/lib/console-remote/remote.d.ts +10 -5
  77. package/lib/console-remote/remote.js.map +1 -1
  78. package/lib/console-remote/schemas.d.ts +103 -1
  79. package/lib/console-remote/schemas.js +166 -6
  80. package/lib/console-remote/schemas.js.map +1 -1
  81. package/lib/index.d.ts +13 -2
  82. package/lib/index.js.map +1 -1
  83. package/lib/ports.d.ts +79 -1
  84. package/lib/session-checkout-apply.d.ts +76 -0
  85. package/lib/session-checkout-apply.js +608 -143
  86. package/lib/session-checkout-apply.js.map +1 -1
  87. package/lib/session-checkout-module.js +775 -117
  88. package/lib/session-checkout-module.js.map +1 -1
  89. package/lib/types.d.ts +104 -3
  90. package/lib/types.js.map +1 -1
  91. package/package.json +14 -14
  92. package/scripts/check-publish.mjs +2 -2
@@ -2,10 +2,22 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useEffect, useId, useRef, useState } from 'react';
3
3
  import { Modal } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { worktreeConsoleErrorMeta } from '../../console-contract.js';
5
+ import { openAuthorizedWorktreeTarget, openIsolatedTarget, prefillSessionDraft, } from '../actions.js';
6
+ import { DeliveryProof } from './DeliveryProof.js';
7
+ import { PreflightStatus } from './PreflightStatus.js';
8
+ import { invalidateReviewPreflight, readReviewPreflight, useReviewPreflight } from './preflight-cache.js';
9
+ import { readPreviewRecoveryPreflight, usePreviewRecoveryPreflight } from './preview-recovery-cache.js';
10
+ import { enqueueWorktreeRecovery, restoreWorktreeRecovery, retryWorktreeRecovery, useWorktreeRecoverySnapshot, } from './recovery-continuation.js';
5
11
  import { requestWorktreeReviewRefresh } from './status-events.js';
12
+ function checkpointRequestIdForGeneration(generation) {
13
+ return `checkpoint:${generation}`;
14
+ }
6
15
  function isStale(error) {
7
16
  return error.code === 'stale_target' || error.code === 'stale_isolated' || error.code === 'stale_local';
8
17
  }
18
+ function sameStrings(left, right) {
19
+ return left.length === right.length && left.every((value, index) => value === right[index]);
20
+ }
9
21
  function preflightMessage(preflight) {
10
22
  if (preflight.status === 'ready')
11
23
  return '同步预检通过,正在创建可撤回的 Local Preview。';
@@ -19,25 +31,87 @@ function preflightMessage(preflight) {
19
31
  return `同步暂时阻塞:${preflight.message}`;
20
32
  return '同步预检完成;Local 未修改。';
21
33
  }
22
- export function ReviewActions({ review, adapter, identity, target, disabled, unavailableMessage, focusReview, onStale, onTargetChange, }) {
34
+ export function ReviewActions({ review, adapter, services, identity, target, disabled, unavailableMessage, focusReview, isActive = () => true, onStale, onTargetChange, }) {
23
35
  const formId = useId();
36
+ const moreMenuRef = useRef(null);
24
37
  const [submitting, setSubmitting] = useState(null);
25
38
  const [commitMode, setCommitMode] = useState(null);
39
+ const [checkpointOpen, setCheckpointOpen] = useState(false);
26
40
  const [discardOpen, setDiscardOpen] = useState(false);
27
41
  const [commitMessage, setCommitMessage] = useState(review.suggestedCommitMessage);
42
+ const [checkpointMessage, setCheckpointMessage] = useState(review.suggestedCommitMessage);
43
+ const checkpointRequestId = useRef(null);
28
44
  const [retainEnvironment, setRetainEnvironment] = useState(false);
29
45
  const [retention, setRetention] = useState('retain_24h');
30
46
  const [message, setMessage] = useState(null);
31
47
  const [error, setError] = useState(null);
32
- const [preflight, setPreflight] = useState(null);
48
+ const [runtimeConflict, setRuntimeConflict] = useState(null);
33
49
  const mutationLock = useRef(false);
34
50
  const observedTargetRevision = useRef(target?.revision);
51
+ const observedReviewSlot = useRef(target?.reviewSlot);
52
+ const recoverySnapshot = useWorktreeRecoverySnapshot(identity?.sessionId);
53
+ const autoPreflightEnabled = Boolean(adapter
54
+ && identity
55
+ && !disabled
56
+ && target?.state === 'ready_for_review'
57
+ && target.capabilities.preflight);
58
+ const { snapshot: preflightSnapshot, refresh: refreshPreflight } = useReviewPreflight(adapter, identity, autoPreflightEnabled);
59
+ const safeReadyPreflight = preflightSnapshot.status === 'success'
60
+ && preflightSnapshot.preflight.status !== 'blocked'
61
+ && preflightSnapshot.preflight.status !== 'conflict';
62
+ const recoveryIdentity = identity && target?.state === 'preview_detached' && target.previewRecovery
63
+ ? {
64
+ ...identity,
65
+ expectedPreviewId: target.previewRecovery.previewId,
66
+ }
67
+ : undefined;
68
+ const { snapshot: recoveryPreflight, refresh: refreshRecoveryPreflight } = usePreviewRecoveryPreflight(adapter, recoveryIdentity, Boolean(adapter && recoveryIdentity && !disabled));
69
+ const recoveryProof = recoveryPreflight.status === 'success'
70
+ && recoveryPreflight.preflight.status === 'assessed'
71
+ ? recoveryPreflight.preflight.proof
72
+ : undefined;
73
+ const activeRecovery = recoverySnapshot
74
+ && identity
75
+ && recoverySnapshot.request.checkoutId === identity.checkoutId
76
+ && recoverySnapshot.request.reviewId === identity.expectedReviewId
77
+ ? recoverySnapshot
78
+ : null;
79
+ useEffect(() => {
80
+ if (!adapter || !services || !identity)
81
+ return;
82
+ restoreWorktreeRecovery({ sessionId: identity.sessionId, adapter, services, isActive });
83
+ }, [adapter, services, identity?.sessionId]);
35
84
  useEffect(() => {
36
85
  setCommitMessage(review.suggestedCommitMessage);
86
+ setCheckpointMessage(review.suggestedCommitMessage);
87
+ setCheckpointOpen(false);
88
+ if (moreMenuRef.current)
89
+ moreMenuRef.current.open = false;
90
+ checkpointRequestId.current = null;
37
91
  setRetainEnvironment(false);
38
92
  setRetention('retain_24h');
39
- setPreflight(null);
40
93
  }, [review.reviewId, review.suggestedCommitMessage]);
94
+ useEffect(() => {
95
+ const closeFromOutside = (event) => {
96
+ const menu = moreMenuRef.current;
97
+ if (menu?.open && !menu.contains(event.target))
98
+ menu.open = false;
99
+ };
100
+ const closeFromKeyboard = (event) => {
101
+ const menu = moreMenuRef.current;
102
+ if (event.key !== 'Escape' || !menu?.open)
103
+ return;
104
+ event.preventDefault();
105
+ menu.open = false;
106
+ menu.querySelector('summary')?.focus();
107
+ };
108
+ document.addEventListener('mousedown', closeFromOutside);
109
+ document.addEventListener('keydown', closeFromKeyboard);
110
+ return () => {
111
+ document.removeEventListener('mousedown', closeFromOutside);
112
+ document.removeEventListener('keydown', closeFromKeyboard);
113
+ };
114
+ }, []);
41
115
  useEffect(() => {
42
116
  const revision = target?.revision;
43
117
  if (revision === undefined || revision === observedTargetRevision.current)
@@ -47,8 +121,19 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
47
121
  return;
48
122
  setMessage(null);
49
123
  setError(null);
50
- setPreflight(null);
124
+ setRuntimeConflict(null);
51
125
  }, [submitting, target?.revision]);
126
+ useEffect(() => {
127
+ const previous = observedReviewSlot.current;
128
+ observedReviewSlot.current = target?.reviewSlot;
129
+ if (previous === 'waiting'
130
+ && target?.reviewSlot === 'available'
131
+ && adapter
132
+ && identity) {
133
+ invalidateReviewPreflight(adapter, identity);
134
+ void readReviewPreflight(adapter, identity);
135
+ }
136
+ }, [adapter, identity?.checkoutId, identity?.expectedReviewId, identity?.expectedRevision, target?.reviewSlot]);
52
137
  const begin = (mutation) => {
53
138
  if (disabled || !adapter || !identity || !target || mutationLock.current)
54
139
  return false;
@@ -62,33 +147,239 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
62
147
  mutationLock.current = false;
63
148
  setSubmitting(null);
64
149
  };
150
+ const closeMoreMenu = () => {
151
+ if (moreMenuRef.current)
152
+ moreMenuRef.current.open = false;
153
+ };
65
154
  const finishError = (nextError) => {
155
+ if (nextError.code === 'apply_conflict' && nextError.continuation?.kind === 'worktree_apply_conflict') {
156
+ setRuntimeConflict(nextError.continuation);
157
+ setCommitMode(null);
158
+ setError(null);
159
+ setMessage('实时写入校验发现冲突;Local 未修改。请明确让 Agent 在 managed Worktree 中解决。');
160
+ finish();
161
+ return;
162
+ }
66
163
  setError(nextError);
67
- if (isStale(nextError))
164
+ if (nextError.code === 'stale_target')
68
165
  onStale(nextError);
166
+ if (nextError.code === 'stale_local' || nextError.code === 'stale_isolated') {
167
+ setMessage('状态在写入前发生变化;已停止操作,请按最新只读预检恢复。');
168
+ void refreshPreflight();
169
+ }
69
170
  finish();
70
171
  };
71
172
  const applyTarget = (nextTarget) => {
173
+ if (!isActive())
174
+ return;
72
175
  observedTargetRevision.current = nextTarget.revision;
73
176
  onTargetChange(nextTarget);
74
177
  if (identity)
75
178
  requestWorktreeReviewRefresh(identity.sessionId);
76
179
  };
180
+ const enqueueRecovery = (request) => {
181
+ if (!adapter || !services)
182
+ return;
183
+ enqueueWorktreeRecovery({ adapter, services, request, isActive });
184
+ };
185
+ const startConflictRecovery = async (preflight, requestId = `preflight-conflict:${preflight.checkoutId}:${preflight.reviewId}:${preflight.revision}:${preflight.localHeadOid}`) => {
186
+ if (!services || !begin('recovery') || !adapter || !identity)
187
+ return;
188
+ const resumeIdentity = {
189
+ sessionId: identity.sessionId,
190
+ checkoutId: preflight.checkoutId,
191
+ expectedRevision: preflight.revision,
192
+ expectedReviewId: preflight.reviewId,
193
+ };
194
+ const latest = await readReviewPreflight(adapter, resumeIdentity, true);
195
+ if (!isActive()) {
196
+ finish();
197
+ return;
198
+ }
199
+ if (latest.status === 'error') {
200
+ finishError(latest.error);
201
+ return;
202
+ }
203
+ if (latest.status !== 'success' || latest.preflight.status !== 'conflict') {
204
+ setMessage(latest.status === 'success' ? preflightMessage(latest.preflight) : '冲突预检未完成。');
205
+ finish();
206
+ return;
207
+ }
208
+ if (latest.preflight.checkoutId !== preflight.checkoutId
209
+ || latest.preflight.reviewId !== preflight.reviewId
210
+ || latest.preflight.revision !== preflight.revision
211
+ || latest.preflight.localHeadOid !== preflight.localHeadOid
212
+ || !sameStrings(latest.preflight.conflictingFiles, preflight.conflictingFiles)) {
213
+ setError({ code: 'stale_target', message: '冲突身份在恢复前已变化,请按最新预检重试。' });
214
+ finish();
215
+ return;
216
+ }
217
+ const outcome = await adapter.resumeRevision({ ...resumeIdentity, conflictContinuation: {
218
+ kind: 'worktree_apply_conflict',
219
+ requestId,
220
+ checkoutId: preflight.checkoutId,
221
+ reviewId: preflight.reviewId,
222
+ revision: preflight.revision,
223
+ localHeadOid: preflight.localHeadOid,
224
+ conflictingFiles: [...preflight.conflictingFiles],
225
+ } });
226
+ if (!isActive()) {
227
+ finish();
228
+ return;
229
+ }
230
+ if (!outcome.ok) {
231
+ finishError(outcome.error);
232
+ return;
233
+ }
234
+ const nextTarget = outcome.value.target;
235
+ if (nextTarget.checkoutId !== preflight.checkoutId
236
+ || nextTarget.ownerSessionId !== identity.sessionId
237
+ || nextTarget.targetSessionId !== identity.sessionId
238
+ || nextTarget.state !== 'working'
239
+ || nextTarget.revision !== preflight.revision + 1) {
240
+ setError({ code: 'checkout_mismatch', message: '恢复编辑后 Host 返回的 Working 身份不一致。' });
241
+ finish();
242
+ return;
243
+ }
244
+ const recovery = outcome.value.recoveryContinuation;
245
+ if (!recovery
246
+ || recovery.kind !== 'worktree_apply_conflict'
247
+ || recovery.checkoutId !== preflight.checkoutId
248
+ || recovery.reviewId !== preflight.reviewId
249
+ || recovery.revision !== nextTarget.revision
250
+ || recovery.localHeadOid !== preflight.localHeadOid
251
+ || !sameStrings(recovery.conflictingFiles, preflight.conflictingFiles)) {
252
+ setError({ code: 'checkout_mismatch', message: 'Host 未返回精确的冲突恢复凭证。' });
253
+ finish();
254
+ return;
255
+ }
256
+ applyTarget(nextTarget);
257
+ setRuntimeConflict(null);
258
+ enqueueRecovery({
259
+ kind: recovery.kind,
260
+ sessionId: identity.sessionId,
261
+ requestId: recovery.requestId,
262
+ checkoutId: recovery.checkoutId,
263
+ reviewId: recovery.reviewId,
264
+ revision: recovery.revision,
265
+ localHeadOid: recovery.localHeadOid,
266
+ conflictingFiles: [...recovery.conflictingFiles],
267
+ });
268
+ setMessage('已安全恢复 Working,冲突解决请求正在等待精确 owner Session 空闲。Local 未修改。');
269
+ finish();
270
+ };
271
+ const startReviewRegeneration = async (preflight) => {
272
+ if (!services || !begin('recovery') || !adapter || !identity)
273
+ return;
274
+ const regenerationIdentity = {
275
+ sessionId: identity.sessionId,
276
+ checkoutId: preflight.checkoutId,
277
+ expectedRevision: preflight.revision,
278
+ expectedReviewId: preflight.reviewId,
279
+ };
280
+ const latest = await readReviewPreflight(adapter, regenerationIdentity, true);
281
+ if (!isActive()) {
282
+ finish();
283
+ return;
284
+ }
285
+ if (latest.status === 'error') {
286
+ finishError(latest.error);
287
+ return;
288
+ }
289
+ if (latest.status !== 'success'
290
+ || latest.preflight.status !== 'blocked'
291
+ || latest.preflight.reason !== 'stale_isolated'
292
+ || latest.preflight.checkoutId !== preflight.checkoutId
293
+ || latest.preflight.reviewId !== preflight.reviewId
294
+ || latest.preflight.revision !== preflight.revision) {
295
+ setMessage(latest.status === 'success' ? preflightMessage(latest.preflight) : '只读复核未完成。');
296
+ finish();
297
+ return;
298
+ }
299
+ const prepared = await adapter.prepareReviewRegeneration(regenerationIdentity);
300
+ if (!isActive()) {
301
+ finish();
302
+ return;
303
+ }
304
+ if (!prepared.ok) {
305
+ finishError(prepared.error);
306
+ return;
307
+ }
308
+ const recovery = prepared.value.recoveryContinuation;
309
+ if (!recovery
310
+ || recovery.kind !== 'worktree_review_regeneration'
311
+ || recovery.checkoutId !== preflight.checkoutId
312
+ || recovery.reviewId !== preflight.reviewId
313
+ || recovery.revision !== preflight.revision) {
314
+ setError({ code: 'checkout_mismatch', message: 'Host 未返回精确的只读验收再生成凭证。' });
315
+ finish();
316
+ return;
317
+ }
318
+ enqueueRecovery({
319
+ kind: recovery.kind,
320
+ sessionId: identity.sessionId,
321
+ requestId: recovery.requestId,
322
+ checkoutId: recovery.checkoutId,
323
+ reviewId: recovery.reviewId,
324
+ revision: recovery.revision,
325
+ });
326
+ setMessage('只读验收再生成请求正在等待精确 owner Session 空闲;不会恢复 Working 或修改文件。');
327
+ finish();
328
+ };
329
+ const recoverPreflight = (preflight) => {
330
+ if (preflight.status === 'conflict') {
331
+ void startConflictRecovery(preflight);
332
+ return;
333
+ }
334
+ if (preflight.status === 'blocked' && preflight.reason === 'stale_isolated' && preflight.reviewId !== null) {
335
+ void startReviewRegeneration({ ...preflight, reason: 'stale_isolated', reviewId: preflight.reviewId });
336
+ }
337
+ };
338
+ const recoverRuntimeConflict = () => {
339
+ if (!runtimeConflict)
340
+ return;
341
+ void startConflictRecovery({
342
+ status: 'conflict',
343
+ localModified: false,
344
+ checkoutId: runtimeConflict.checkoutId,
345
+ reviewId: runtimeConflict.reviewId,
346
+ revision: runtimeConflict.revision,
347
+ configuredBaseOid: runtimeConflict.localHeadOid,
348
+ effectiveBaseOid: runtimeConflict.localHeadOid,
349
+ baseStrategy: 'recorded_base',
350
+ localBranch: null,
351
+ localHeadOid: runtimeConflict.localHeadOid,
352
+ isolatedHeadOid: target?.currentOid ?? runtimeConflict.localHeadOid,
353
+ changedFiles: [],
354
+ conflictingFiles: runtimeConflict.conflictingFiles,
355
+ }, runtimeConflict.requestId);
356
+ };
77
357
  const previewLocal = async () => {
78
358
  if (!begin('preview') || !adapter || !identity)
79
359
  return;
80
- const inspected = await adapter.preflight(identity);
81
- if (!inspected.ok) {
360
+ const inspected = await readReviewPreflight(adapter, identity, true);
361
+ if (!isActive()) {
362
+ finish();
363
+ return;
364
+ }
365
+ if (inspected.status === 'error') {
82
366
  finishError(inspected.error);
83
367
  return;
84
368
  }
85
- setPreflight(inspected.value.preflight);
86
- setMessage(preflightMessage(inspected.value.preflight));
87
- if (inspected.value.preflight.status === 'conflict' || inspected.value.preflight.status === 'blocked') {
369
+ if (inspected.status !== 'success') {
370
+ finish();
371
+ return;
372
+ }
373
+ setMessage(preflightMessage(inspected.preflight));
374
+ if (inspected.preflight.status === 'conflict' || inspected.preflight.status === 'blocked') {
88
375
  finish();
89
376
  return;
90
377
  }
91
378
  const outcome = await adapter.preview(identity);
379
+ if (!isActive()) {
380
+ finish();
381
+ return;
382
+ }
92
383
  if (!outcome.ok) {
93
384
  finishError(outcome.error);
94
385
  return;
@@ -97,31 +388,123 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
97
388
  setMessage('已同步为可撤回的 Local Preview;请在 Local 中验收。');
98
389
  finish();
99
390
  };
391
+ const openCheckpoint = () => {
392
+ if (!target?.checkpointGeneration || !target.capabilities.checkpoint || disabled || submitting !== null)
393
+ return;
394
+ checkpointRequestId.current = checkpointRequestIdForGeneration(target.checkpointGeneration);
395
+ setCheckpointMessage(review.suggestedCommitMessage);
396
+ setCheckpointOpen(true);
397
+ };
398
+ const checkpoint = async () => {
399
+ const value = checkpointMessage.trim();
400
+ const requestId = checkpointRequestId.current;
401
+ if (!value || value.length > 500 || !requestId || !begin('checkpoint') || !adapter || !identity || !target?.checkpointGeneration)
402
+ return;
403
+ const expectedCount = target.checkpoints?.length ?? 0;
404
+ const outcome = await adapter.checkpoint({
405
+ ...identity,
406
+ expectedGeneration: target.checkpointGeneration,
407
+ requestId,
408
+ commitMessage: value,
409
+ });
410
+ if (!isActive()) {
411
+ finish();
412
+ return;
413
+ }
414
+ if (!outcome.ok) {
415
+ finishError(outcome.error);
416
+ return;
417
+ }
418
+ const saved = outcome.value.checkpoint;
419
+ const nextTarget = outcome.value.target;
420
+ if (!saved
421
+ || saved.reviewId !== identity.expectedReviewId
422
+ || saved.sequence !== expectedCount + 1
423
+ || nextTarget.checkoutId !== identity.checkoutId
424
+ || nextTarget.ownerSessionId !== identity.sessionId
425
+ || nextTarget.state !== 'working'
426
+ || nextTarget.review !== undefined
427
+ || nextTarget.checkpoints?.at(-1)?.checkpointId !== saved.checkpointId) {
428
+ setError({ code: 'checkout_mismatch', message: 'Host 返回的 Checkpoint 身份或 Working 状态不一致。' });
429
+ finish();
430
+ return;
431
+ }
432
+ applyTarget(nextTarget);
433
+ setCheckpointOpen(false);
434
+ checkpointRequestId.current = null;
435
+ setMessage(`已保存第 ${saved.sequence} 个 Worktree 阶段并继续修改;阶段尚未发布到 Local。`);
436
+ finish();
437
+ };
100
438
  const resumeRevision = async () => {
101
439
  if (!begin('resume_revision') || !adapter || !identity)
102
440
  return;
103
441
  const outcome = await adapter.resumeRevision(identity);
442
+ if (!isActive()) {
443
+ finish();
444
+ return;
445
+ }
104
446
  if (!outcome.ok) {
105
447
  finishError(outcome.error);
106
448
  return;
107
449
  }
108
450
  applyTarget(outcome.value.target);
109
- setMessage('已恢复编辑;继续发送修改要求即可。Local 未受影响。');
451
+ const drafted = services
452
+ ? prefillSessionDraft(services, identity.sessionId, '请重新检查当前 Worktree 的修改,重新运行必要验证,并重新生成验收稿。')
453
+ : false;
454
+ setMessage(drafted
455
+ ? '已恢复编辑并预填重新验收请求;Local 未受影响。'
456
+ : '已恢复编辑;请重新检查、验证并生成新的验收稿。Local 未受影响。');
110
457
  finish();
111
458
  };
112
459
  const rollbackPreview = async (resumeRevision = false) => {
113
460
  if (!begin('rollback') || !adapter || !identity)
114
461
  return;
115
- const outcome = await adapter.rollbackPreview({ ...identity, resumeRevision });
462
+ let freshProof;
463
+ if (target?.state === 'preview_detached') {
464
+ if (!recoveryIdentity) {
465
+ setError({ code: 'stale_target', message: 'Detached Preview 身份不完整,请刷新。' });
466
+ finish();
467
+ return;
468
+ }
469
+ const inspected = await readPreviewRecoveryPreflight(adapter, recoveryIdentity, true);
470
+ if (!isActive()) {
471
+ finish();
472
+ return;
473
+ }
474
+ if (inspected.status === 'error') {
475
+ finishError(inspected.error);
476
+ return;
477
+ }
478
+ if (inspected.status !== 'success' || inspected.preflight.status !== 'assessed') {
479
+ setMessage(inspected.status === 'success' && inspected.preflight.status === 'blocked'
480
+ ? inspected.preflight.message
481
+ : 'Preview Recovery 预检未完成。');
482
+ finish();
483
+ return;
484
+ }
485
+ if (inspected.preflight.proof.rollback.status !== 'safe') {
486
+ setMessage(inspected.preflight.proof.rollback.message);
487
+ finish();
488
+ return;
489
+ }
490
+ freshProof = inspected.preflight.proof;
491
+ }
492
+ const outcome = await adapter.rollbackPreview({
493
+ ...identity,
494
+ resumeRevision,
495
+ ...(freshProof ? { recoveryProof: freshProof } : {}),
496
+ });
497
+ if (!isActive()) {
498
+ finish();
499
+ return;
500
+ }
116
501
  if (!outcome.ok) {
117
502
  finishError(outcome.error);
118
503
  return;
119
504
  }
120
505
  applyTarget(outcome.value.target);
121
506
  setMessage(outcome.value.target.state === 'preview_detached'
122
- ? outcome.value.target.previewRecovery?.reason === 'stale_local'
123
- ? 'Local branch/HEAD 已变化;同分支安全快进可重试撤回,其他历史变化不会被覆盖。'
124
- : 'Preview 与 Local 存在冲突;恢复证据和 Worktree 已保留。'
507
+ ? '恢复条件在写入前发生变化;Preview 证据和 Worktree 已保留,请重新检查。'
125
508
  : resumeRevision ? '已撤回 Local Preview,可以继续修改 Worktree。' : '已撤回 Local Preview,验收卡仍可再次同步。');
126
509
  finish();
127
510
  };
@@ -132,9 +515,59 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
132
515
  return;
133
516
  const selectedRetention = retainEnvironment ? retention : 'cleanup';
134
517
  const request = { ...identity, commitMessage: value, retention: selectedRetention };
518
+ if (mode === 'finish') {
519
+ const inspected = await readReviewPreflight(adapter, identity, true);
520
+ if (!isActive()) {
521
+ finish();
522
+ return;
523
+ }
524
+ if (inspected.status === 'error') {
525
+ finishError(inspected.error);
526
+ return;
527
+ }
528
+ if (inspected.status !== 'success' || inspected.preflight.status === 'blocked' || inspected.preflight.status === 'conflict') {
529
+ setMessage(inspected.status === 'success' ? preflightMessage(inspected.preflight) : '同步预检未完成。');
530
+ finish();
531
+ return;
532
+ }
533
+ }
534
+ let previewProof;
535
+ if (mode === 'finalize_preview' && target?.state === 'preview_detached') {
536
+ if (!recoveryIdentity) {
537
+ setError({ code: 'stale_target', message: 'Detached Preview 身份不完整,请刷新。' });
538
+ finish();
539
+ return;
540
+ }
541
+ const inspected = await readPreviewRecoveryPreflight(adapter, recoveryIdentity, true);
542
+ if (!isActive()) {
543
+ finish();
544
+ return;
545
+ }
546
+ if (inspected.status === 'error') {
547
+ finishError(inspected.error);
548
+ return;
549
+ }
550
+ if (inspected.status !== 'success' || inspected.preflight.status !== 'assessed') {
551
+ setMessage(inspected.status === 'success' && inspected.preflight.status === 'blocked'
552
+ ? inspected.preflight.message
553
+ : 'Preview Recovery 预检未完成。');
554
+ finish();
555
+ return;
556
+ }
557
+ if (inspected.preflight.proof.finalize.status !== 'safe') {
558
+ setMessage(inspected.preflight.proof.finalize.message);
559
+ finish();
560
+ return;
561
+ }
562
+ previewProof = inspected.preflight.proof;
563
+ }
135
564
  const outcome = mode === 'finalize_preview'
136
- ? await adapter.finalizePreview(request)
565
+ ? await adapter.finalizePreview({ ...request, ...(previewProof ? { recoveryProof: previewProof } : {}) })
137
566
  : await adapter.finalize(request);
567
+ if (!isActive()) {
568
+ finish();
569
+ return;
570
+ }
138
571
  if (!outcome.ok) {
139
572
  finishError(outcome.error);
140
573
  return;
@@ -159,6 +592,10 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
159
592
  confirmDirty: true,
160
593
  ...(target.state === 'preview_active' || target.capabilities.rollbackPreview ? { rollbackPreview: true } : {}),
161
594
  });
595
+ if (!isActive()) {
596
+ finish();
597
+ return;
598
+ }
162
599
  if (!outcome.ok) {
163
600
  finishError(outcome.error);
164
601
  return;
@@ -170,6 +607,143 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
170
607
  setDiscardOpen(false);
171
608
  finish();
172
609
  };
610
+ const freshRecoveryProof = async () => {
611
+ if (!adapter || !recoveryIdentity)
612
+ return null;
613
+ const inspected = await readPreviewRecoveryPreflight(adapter, recoveryIdentity, true);
614
+ if (!isActive())
615
+ return null;
616
+ if (inspected.status === 'error') {
617
+ finishError(inspected.error);
618
+ return null;
619
+ }
620
+ if (inspected.status !== 'success' || inspected.preflight.status !== 'assessed') {
621
+ setMessage(inspected.status === 'success' && inspected.preflight.status === 'blocked'
622
+ ? inspected.preflight.message
623
+ : 'Preview Recovery 预检未完成。');
624
+ return null;
625
+ }
626
+ return inspected.preflight.proof;
627
+ };
628
+ const analyzeRecovery = async () => {
629
+ if (!services || !begin('recovery_analysis') || !adapter || !identity || !recoveryIdentity)
630
+ return;
631
+ const proof = await freshRecoveryProof();
632
+ if (!proof) {
633
+ finish();
634
+ return;
635
+ }
636
+ const prepared = await adapter.preparePreviewRecoveryAnalysis({ ...recoveryIdentity, recoveryProof: proof });
637
+ if (!isActive()) {
638
+ finish();
639
+ return;
640
+ }
641
+ if (!prepared.ok) {
642
+ finishError(prepared.error);
643
+ return;
644
+ }
645
+ const continuation = prepared.value.recoveryContinuation;
646
+ if (continuation?.kind !== 'worktree_preview_recovery_analysis'
647
+ || continuation.checkoutId !== identity.checkoutId
648
+ || continuation.reviewId !== identity.expectedReviewId
649
+ || continuation.previewId !== recoveryIdentity.expectedPreviewId
650
+ || continuation.revision !== identity.expectedRevision
651
+ || continuation.generation !== proof.generation) {
652
+ setError({ code: 'checkout_mismatch', message: 'Host 未返回精确的 detached Recovery 分析凭证。' });
653
+ finish();
654
+ return;
655
+ }
656
+ try {
657
+ await openAuthorizedWorktreeTarget(adapter, services, identity.sessionId, {
658
+ checkoutId: identity.checkoutId,
659
+ ownerSessionId: identity.sessionId,
660
+ }, isActive);
661
+ const verified = await adapter.inspect({ sessionId: identity.sessionId, checkoutId: identity.checkoutId });
662
+ if (!verified.ok || verified.value.target.recoveryContinuation?.kind !== continuation.kind
663
+ || verified.value.target.recoveryContinuation.requestId !== continuation.requestId
664
+ || verified.value.target.recoveryContinuation.generation !== continuation.generation) {
665
+ throw new Error('二次 Host 检查未确认 detached Recovery 分析凭证。');
666
+ }
667
+ const binding = services.sessions.binding(identity.sessionId);
668
+ const prompt = binding?.session.prompt;
669
+ if (!binding || !prompt)
670
+ throw new Error('Owner Session 尚未提供 Harness prompt API。');
671
+ const result = await prompt.call(binding.session, [{ type: 'text', text: [
672
+ `请只读分析 detached Preview Recovery:checkout ${identity.checkoutId},review ${identity.expectedReviewId},preview ${recoveryIdentity.expectedPreviewId},generation ${proof.generation}。`,
673
+ '禁止修改旧 managed Worktree、Local、Git refs、index、receipt 或 retained artifacts;不要运行 reset/rebase/force checkout/clean。',
674
+ '请解释 rollback/finalize blocker、仍缺失的任务增量与最安全的人工下一步。',
675
+ ].join('\n') }], 'queue');
676
+ if (!result.ok)
677
+ throw new Error(`${result.error.code}: ${result.error.message}`);
678
+ setMessage('已向精确 owner Session 发送只读 Recovery 分析请求。');
679
+ }
680
+ catch (reason) {
681
+ setError({ code: 'checkout_mismatch', message: reason instanceof Error ? reason.message : String(reason) });
682
+ }
683
+ finish();
684
+ };
685
+ const handoffRecovery = async () => {
686
+ if (!services || !begin('recovery_handoff') || !adapter || !identity || !recoveryIdentity)
687
+ return;
688
+ const proof = await freshRecoveryProof();
689
+ if (!proof) {
690
+ finish();
691
+ return;
692
+ }
693
+ const created = await adapter.createPreviewRecoveryHandoff({ ...recoveryIdentity, recoveryProof: proof });
694
+ if (!isActive()) {
695
+ finish();
696
+ return;
697
+ }
698
+ if (!created.ok) {
699
+ finishError(created.error);
700
+ return;
701
+ }
702
+ const continuation = created.value.recoveryContinuation;
703
+ if (continuation.kind !== 'worktree_preview_recovery_handoff'
704
+ || continuation.sourceCheckoutId !== identity.checkoutId
705
+ || continuation.reviewId !== identity.expectedReviewId
706
+ || continuation.previewId !== recoveryIdentity.expectedPreviewId
707
+ || continuation.revision !== identity.expectedRevision
708
+ || continuation.generation !== proof.generation
709
+ || continuation.checkoutId !== created.value.target.checkoutId
710
+ || continuation.checkoutId === identity.checkoutId) {
711
+ setError({ code: 'checkout_mismatch', message: 'Host 返回的 Recovery handoff 身份不一致。' });
712
+ finish();
713
+ return;
714
+ }
715
+ try {
716
+ await openIsolatedTarget(services, {
717
+ targetSessionId: created.value.targetSessionId,
718
+ managedRoot: created.value.managedRoot,
719
+ }, isActive);
720
+ const verified = await adapter.inspect({
721
+ sessionId: created.value.targetSessionId,
722
+ checkoutId: continuation.checkoutId,
723
+ });
724
+ if (!verified.ok || verified.value.target.managedRoot !== created.value.managedRoot
725
+ || verified.value.target.recoveryContinuation?.kind !== continuation.kind
726
+ || verified.value.target.recoveryContinuation.requestId !== continuation.requestId) {
727
+ throw new Error('新 Worktree 的二次 Host/cwd 检查未通过。');
728
+ }
729
+ const binding = services.sessions.binding(created.value.targetSessionId);
730
+ const prompt = binding?.session.prompt;
731
+ if (!binding || !prompt)
732
+ throw new Error('新 owner Session 尚未提供 Harness prompt API。');
733
+ const result = await prompt.call(binding.session, [{ type: 'text', text: [
734
+ `这是 detached Preview Recovery handoff。旧 checkout ${identity.checkoutId} / review ${identity.expectedReviewId} / preview ${recoveryIdentity.expectedPreviewId} / generation ${proof.generation} 只读。`,
735
+ '当前新 Worktree 基于最新 Local HEAD;只恢复仍缺失的任务增量。禁止修改 Local、旧 Worktree、旧 receipt/refs,禁止 reset/rebase/force checkout/clean。',
736
+ '完成后运行必要验证并生成新的 Ready for Review。',
737
+ ].join('\n') }], 'queue');
738
+ if (!result.ok)
739
+ throw new Error(`${result.error.code}: ${result.error.message}`);
740
+ setMessage('已创建并打开基于最新 Local HEAD 的 fresh Worktree,Recovery handoff 请求已发送。');
741
+ }
742
+ catch (reason) {
743
+ setError({ code: 'checkout_mismatch', message: reason instanceof Error ? reason.message : String(reason) });
744
+ }
745
+ finish();
746
+ };
173
747
  const retryCleanup = async () => {
174
748
  if (!begin('retry_cleanup') || !adapter || !identity)
175
749
  return;
@@ -178,6 +752,10 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
178
752
  checkoutId: identity.checkoutId,
179
753
  expectedRevision: identity.expectedRevision,
180
754
  });
755
+ if (!isActive()) {
756
+ finish();
757
+ return;
758
+ }
181
759
  if (!outcome.ok) {
182
760
  finishError(outcome.error);
183
761
  return;
@@ -186,14 +764,37 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
186
764
  setMessage(outcome.value.target.state === 'delivered' ? 'Worktree 环境已清理。' : '清理仍未完成,已保留恢复信息。');
187
765
  finish();
188
766
  };
767
+ const openHolder = async () => {
768
+ const holder = preflightSnapshot.status === 'success'
769
+ && preflightSnapshot.preflight.status === 'blocked'
770
+ && preflightSnapshot.preflight.reason === 'project_acceptance_busy'
771
+ ? preflightSnapshot.preflight.blocker ?? target?.reviewSlotHolder
772
+ : recoveryProof?.blocker ?? target?.reviewSlotHolder;
773
+ if (!holder || !adapter || !identity || !services || mutationLock.current)
774
+ return;
775
+ mutationLock.current = true;
776
+ setSubmitting('open_holder');
777
+ setError(null);
778
+ try {
779
+ await openAuthorizedWorktreeTarget(adapter, services, identity.sessionId, holder, isActive);
780
+ }
781
+ catch (reason) {
782
+ setError({ code: 'checkout_mismatch', message: reason instanceof Error ? reason.message : String(reason) });
783
+ }
784
+ finally {
785
+ finish();
786
+ }
787
+ };
189
788
  const live = Boolean(adapter && identity && target);
190
789
  const allDisabled = disabled || submitting !== null || !live;
191
790
  const ready = target?.state === 'ready_for_review';
192
791
  const previewActive = target?.state === 'preview_active';
193
792
  const previewRecovery = target?.state === 'preview_detached'
194
793
  || target?.state === 'recovery_required' && target.capabilities.rollbackPreview;
195
- const safeFastForwardRecovery = target?.state === 'preview_detached'
196
- && target.previewRecovery?.reason === 'stale_local';
794
+ const rollbackRecoverySafe = target?.state === 'preview_detached'
795
+ && recoveryProof?.rollback.status === 'safe';
796
+ const finalizeRecoverySafe = target?.state === 'preview_detached'
797
+ && recoveryProof?.finalize.status === 'safe';
197
798
  const cleanupPending = target?.state === 'cleanup_pending';
198
799
  const terminal = target?.state === 'retained' || target?.state === 'delivered';
199
800
  const canDiscard = Boolean(target?.capabilities.discard);
@@ -201,34 +802,59 @@ export function ReviewActions({ review, adapter, identity, target, disabled, una
201
802
  if (submitting === null)
202
803
  setCommitMode(null);
203
804
  };
805
+ const closeCheckpoint = () => {
806
+ if (submitting !== null)
807
+ return;
808
+ setCheckpointOpen(false);
809
+ checkpointRequestId.current = null;
810
+ };
204
811
  const closeDiscard = () => {
205
812
  if (submitting === null)
206
813
  setDiscardOpen(false);
207
814
  };
208
- const primary = ready ? (_jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: allDisabled || !target.capabilities.preview, onClick: () => { void previewLocal(); }, children: submitting === 'preview' ? '同步中…' : '同步到 Local 验收' })) : previewActive ? (_jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: allDisabled || !target.capabilities.finalizePreview, onClick: () => setCommitMode('finalize_preview'), children: "\u9A8C\u6536\u901A\u8FC7\u5E76\u63D0\u4EA4" })) : previewRecovery ? (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || !target?.capabilities.rollbackPreview, onClick: () => { void rollbackPreview(true); }, children: submitting === 'rollback' ? '处理中…' : safeFastForwardRecovery ? '安全重试撤回' : target?.state === 'preview_detached' ? '重新检查撤回' : '重新尝试撤回' })) : cleanupPending ? (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || !target?.capabilities.retryCleanup, onClick: () => { void retryCleanup(); }, children: submitting === 'retry_cleanup' ? '清理中…' : '重试清理环境' })) : null;
815
+ const primary = ready ? (_jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: allDisabled || !target.capabilities.preview || !safeReadyPreflight, onClick: () => { void previewLocal(); }, children: submitting === 'preview'
816
+ ? '同步中…'
817
+ : autoPreflightEnabled && (preflightSnapshot.status === 'idle' || preflightSnapshot.status === 'loading')
818
+ ? '检查中…'
819
+ : '同步到 Local 验收' })) : previewActive ? (_jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: allDisabled || !target.capabilities.finalizePreview, onClick: () => setCommitMode('finalize_preview'), children: "\u9A8C\u6536\u901A\u8FC7\u5E76\u63D0\u4EA4" })) : previewRecovery ? (rollbackRecoverySafe ? (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || !target?.capabilities.rollbackPreview, onClick: () => { void rollbackPreview(true); }, children: submitting === 'rollback' ? '处理中…' : '安全撤回 Preview' })) : (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || target?.state !== 'preview_detached', onClick: () => { void refreshRecoveryPreflight(); }, children: recoveryPreflight.status === 'loading' ? '检查中…' : '重新检查' }))) : cleanupPending ? (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || !target?.capabilities.retryCleanup, onClick: () => { void retryCleanup(); }, children: submitting === 'retry_cleanup' ? '清理中…' : '重试清理环境' })) : null;
209
820
  return (_jsxs("section", { className: "dsh-wt-review-actions", "aria-label": "\u9A8C\u6536\u64CD\u4F5C", children: [!live ? _jsx("p", { className: "dsh-wt-status", children: unavailableMessage }) : null, terminal ? (_jsx("p", { className: "dsh-wt-status", children: target.state === 'retained'
210
821
  ? `本轮已提交,运行环境暂时保留${target.commitOid ? ` · ${target.commitOid.slice(0, 8)}` : ''}`
211
- : `本轮已交付${target.commitOid ? ` · ${target.commitOid.slice(0, 8)}` : ''}` })) : (_jsxs("div", { className: "dsh-wt-actions", children: [primary, _jsxs("details", { className: "dsh-wt-more-menu", children: [_jsx("summary", { className: "dsh-wt-more-trigger", "aria-label": "\u66F4\u591A\u4EA4\u4ED8\u64CD\u4F5C", children: "\u2022\u2022\u2022" }), _jsxs("div", { className: "dsh-wt-more-content", role: "menu", children: [focusReview ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", onClick: (event) => {
822
+ : `本轮已交付${target.commitOid ? ` · ${target.commitOid.slice(0, 8)}` : ''}` })) : (_jsxs("div", { className: "dsh-wt-actions", children: [primary, _jsxs("details", { ref: moreMenuRef, className: "dsh-wt-more-menu", children: [_jsx("summary", { className: "dsh-wt-more-trigger", "aria-label": "\u66F4\u591A\u4EA4\u4ED8\u64CD\u4F5C", children: "\u2022\u2022\u2022" }), _jsxs("div", { className: "dsh-wt-more-content", role: "menu", children: [focusReview ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", onClick: () => {
212
823
  focusReview();
213
- event.currentTarget.closest('details')?.removeAttribute('open');
214
- }, children: "\u67E5\u770B\u9A8C\u6536\u5361" })) : null, ready ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.resumeRevision, onClick: (event) => {
824
+ closeMoreMenu();
825
+ }, children: "\u67E5\u770B\u9A8C\u6536\u5361" })) : null, ready ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.checkpoint || !target.checkpointGeneration, onClick: () => {
826
+ openCheckpoint();
827
+ closeMoreMenu();
828
+ }, children: "\u4FDD\u5B58\u9636\u6BB5\u5E76\u7EE7\u7EED" }), _jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.resumeRevision, onClick: () => {
215
829
  void resumeRevision();
216
- event.currentTarget.closest('details')?.removeAttribute('open');
217
- }, children: submitting === 'resume_revision' ? '恢复中…' : '继续修改' }), _jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.finalize, onClick: (event) => {
830
+ closeMoreMenu();
831
+ }, children: submitting === 'resume_revision' ? '恢复中…' : '继续修改' }), _jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.finalize || !safeReadyPreflight, onClick: () => {
218
832
  setCommitMode('finish');
219
- event.currentTarget.closest('details')?.removeAttribute('open');
220
- }, children: "\u8DF3\u8FC7\u9A8C\u6536\uFF0C\u76F4\u63A5\u63D0\u4EA4" })] })) : null, previewActive || previewRecovery ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.rollbackPreview, onClick: (event) => {
833
+ closeMoreMenu();
834
+ }, children: "\u8DF3\u8FC7\u9A8C\u6536\uFF0C\u76F4\u63A5\u63D0\u4EA4" })] })) : null, previewActive && target.capabilities.checkpoint && target.checkpointGeneration ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled, onClick: () => {
835
+ openCheckpoint();
836
+ closeMoreMenu();
837
+ }, children: "\u64A4\u56DE Preview\uFF0C\u4FDD\u5B58\u9636\u6BB5\u5E76\u7EE7\u7EED" })) : null, previewActive || rollbackRecoverySafe ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.rollbackPreview, onClick: () => {
221
838
  void rollbackPreview(true);
222
- event.currentTarget.closest('details')?.removeAttribute('open');
223
- }, children: "\u64A4\u56DE\u672C\u6B21\u9884\u89C8" })) : null, canDiscard ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item dsh-wt-danger-text", disabled: allDisabled, onClick: (event) => {
839
+ closeMoreMenu();
840
+ }, children: previewActive ? '撤回本次预览' : '安全撤回 Preview' })) : null, finalizeRecoverySafe ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.finalizePreview, onClick: () => {
841
+ setCommitMode('finalize_preview');
842
+ closeMoreMenu();
843
+ }, children: "\u76F4\u63A5\u63D0\u4EA4\u5F53\u524D\u4EFB\u52A1" })) : null, canDiscard ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item dsh-wt-danger-text", disabled: allDisabled, onClick: () => {
224
844
  setDiscardOpen(true);
225
- event.currentTarget.closest('details')?.removeAttribute('open');
226
- }, children: "\u653E\u5F03\u4EFB\u52A1" })) : null] })] })] })), _jsx(Modal, { open: commitMode !== null, onClose: closeCommit, title: commitMode === 'finalize_preview' ? '验收通过并提交?' : '跳过 Local 验收,直接提交?', closeLabel: "\u5173\u95ED\u63D0\u4EA4\u786E\u8BA4", description: commitMode === 'finalize_preview'
845
+ closeMoreMenu();
846
+ }, children: "\u653E\u5F03\u4EFB\u52A1" })) : null] })] })] })), _jsx(Modal, { open: checkpointOpen, onClose: closeCheckpoint, title: "\u4FDD\u5B58 Worktree \u9636\u6BB5\u5E76\u7EE7\u7EED\uFF1F", closeLabel: "\u5173\u95ED\u4FDD\u5B58\u9636\u6BB5\u786E\u8BA4", description: previewActive
847
+ ? '会先按现有 receipt 安全撤回 Local Preview,再把验收快照保存为仅存在于 managed Worktree 的阶段提交。撤回失败或 Local 漂移时会停止。'
848
+ : '把当前验收快照保存为仅存在于 managed Worktree 的阶段提交,然后在同一 Session 继续开发;Local 不会收到该提交。', footer: (_jsxs("div", { className: "dsh-wt-modal-footer", children: [_jsx("button", { type: "button", className: "dsh-wt-button", disabled: submitting !== null, onClick: closeCheckpoint, children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: submitting !== null || !checkpointMessage.trim() || checkpointMessage.trim().length > 500, onClick: () => { void checkpoint(); }, children: submitting === 'checkpoint' ? '正在保存…' : '确认保存并继续' })] })), children: _jsxs("div", { className: "dsh-wt-commit-dialog", children: [_jsxs("div", { className: "dsh-wt-checkpoint-note", children: [_jsx("strong", { children: "\u9636\u6BB5\u4E0D\u4F1A\u53D1\u5E03\u5230 Local" }), _jsxs("span", { children: ["\u5DF2\u6709 ", target?.checkpoints?.length ?? 0, " \u4E2A\u9636\u6BB5\uFF1B\u6700\u7EC8\u4EA4\u4ED8\u4ECD\u4F1A\u4ECE\u539F\u59CB\u4EFB\u52A1\u57FA\u7EBF\u6C47\u603B\u4E3A\u4E00\u4E2A Local Commit\u3002"] })] }), _jsx("label", { htmlFor: `${formId}-checkpoint-message`, children: "Checkpoint Commit Message" }), _jsx("textarea", { id: `${formId}-checkpoint-message`, "aria-label": "Checkpoint Commit Message", rows: 6, maxLength: 500, value: checkpointMessage, onChange: event => setCheckpointMessage(event.target.value) }), _jsxs("div", { className: "dsh-wt-character-count", children: [checkpointMessage.length, "/500"] })] }) }), _jsx(Modal, { open: commitMode !== null, onClose: closeCommit, title: commitMode === 'finalize_preview' ? '验收通过并提交?' : '跳过 Local 验收,直接提交?', closeLabel: "\u5173\u95ED\u63D0\u4EA4\u786E\u8BA4", description: commitMode === 'finalize_preview'
227
849
  ? '只会提交当前可撤回 Preview 对应的任务增量;Local 中无关修改不会进入该 Commit。'
228
850
  : '将跳过 Local Preview 验收并直接提交本轮 Worktree 增量。', footer: (_jsxs("div", { className: "dsh-wt-modal-footer", children: [_jsx("button", { type: "button", className: "dsh-wt-button", disabled: submitting !== null, onClick: closeCommit, children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: submitting !== null || !commitMessage.trim() || commitMessage.trim().length > 500, onClick: () => { void submitCommit(); }, children: submitting === 'finish' || submitting === 'finalize_preview'
229
851
  ? '正在提交…'
230
852
  : retainEnvironment ? '确认提交并保留环境' : '确认提交并清理' })] })), children: _jsxs("div", { className: "dsh-wt-commit-dialog", children: [_jsx("label", { htmlFor: `${formId}-message`, children: "Commit Message" }), _jsx("textarea", { id: `${formId}-message`, "aria-label": "Commit Message", rows: 6, maxLength: 500, value: commitMessage, onChange: event => setCommitMessage(event.target.value) }), _jsxs("div", { className: "dsh-wt-character-count", children: [commitMessage.length, "/500"] }), _jsxs("label", { className: "dsh-wt-retention-check", children: [_jsx("input", { type: "checkbox", checked: retainEnvironment, onChange: event => setRetainEnvironment(event.target.checked) }), _jsx("span", { children: "\u63D0\u4EA4\u540E\u6682\u65F6\u4FDD\u7559\u5F53\u524D\u8FD0\u884C\u73AF\u5883" })] }), retainEnvironment ? (_jsxs("label", { className: "dsh-wt-retention-select", children: [_jsx("span", { children: "\u4FDD\u7559\u65F6\u957F" }), _jsxs("select", { "aria-label": "\u4FDD\u7559\u65F6\u957F", value: retention, onChange: event => setRetention(event.target.value), children: [_jsx("option", { value: "retain_24h", children: "\u4FDD\u7559 24 \u5C0F\u65F6" }), _jsx("option", { value: "retain_3d", children: "\u4FDD\u7559 3 \u5929" }), _jsx("option", { value: "retain_manual", children: "\u624B\u52A8\u6E05\u7406" })] })] })) : null] }) }), _jsx(Modal, { open: discardOpen, onClose: closeDiscard, title: "\u653E\u5F03\u672C\u8F6E\u4EFB\u52A1\uFF1F", closeLabel: "\u5173\u95ED\u653E\u5F03\u786E\u8BA4", description: previewActive
231
853
  ? '会先安全撤回本次 Local Preview,再清理 Worktree;无法无损撤回时会停止,不会覆盖 Local 修改。'
232
- : 'Worktree 中尚未交付的修改将被永久丢弃,Local 不受影响。', footer: (_jsxs("div", { className: "dsh-wt-modal-footer", children: [_jsx("button", { type: "button", className: "dsh-wt-button", disabled: submitting !== null, onClick: closeDiscard, children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-danger", disabled: submitting !== null, onClick: () => { void discard(); }, children: submitting === 'discard' ? '正在放弃…' : '确认放弃任务' })] })) }), preflight ? _jsx("div", { className: "dsh-wt-preflight", "data-preflight": preflight.status, children: preflightMessage(preflight) }) : null, _jsx("div", { className: "dsh-wt-action-status", "aria-live": "polite", children: submitting ? '正在处理 Worktree,请稍候…' : message }), error && !isStale(error) ? (_jsxs("div", { className: "dsh-wt-error", role: "alert", children: [error.message, "\uFF08", worktreeConsoleErrorMeta(error.code).category, "\uFF1B\u6062\u590D\u65B9\u5F0F\uFF1A", worktreeConsoleErrorMeta(error.code).recovery, "\uFF09"] })) : null] }));
854
+ : 'Worktree 中尚未交付的修改将被永久丢弃,Local 不受影响。', footer: (_jsxs("div", { className: "dsh-wt-modal-footer", children: [_jsx("button", { type: "button", className: "dsh-wt-button", disabled: submitting !== null, onClick: closeDiscard, children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-danger", disabled: submitting !== null, onClick: () => { void discard(); }, children: submitting === 'discard' ? '正在放弃…' : '确认放弃任务' })] })) }), ready ? (_jsx(PreflightStatus, { snapshot: preflightSnapshot, target: target, onRefresh: () => { void refreshPreflight(); }, onRecovery: recoverPreflight, onOpenHolder: () => { void openHolder(); }, busy: submitting !== null || activeRecovery?.status === 'queued' || activeRecovery?.status === 'sending' || activeRecovery?.status === 'sent' })) : null, target?.state === 'preview_detached' ? (_jsxs("div", { className: "dsh-wt-preflight", "data-status": recoveryPreflight.status, children: [_jsxs("div", { className: "dsh-wt-preflight-head", children: [_jsx("strong", { children: "Detached Preview Recovery" }), _jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null || recoveryPreflight.status === 'loading', onClick: () => { void refreshRecoveryPreflight(); }, children: "\u91CD\u65B0\u68C0\u67E5" })] }), _jsx("p", { children: "Detached \u662F\u4EA4\u4ED8\u6062\u590D\u72B6\u6001\uFF0C\u4E0D\u662F Git detached HEAD\u3002\u68C0\u67E5\u4E25\u683C\u53EA\u8BFB\uFF0C\u5199\u64CD\u4F5C\u4F1A\u5728 Host \u9501\u5185\u518D\u6B21\u9A8C\u8BC1\u3002" }), recoveryPreflight.status === 'loading' ? _jsx("p", { children: "\u6B63\u5728\u68C0\u67E5 Local HEAD\u3001index\u3001working tree\u3001retained artifacts \u4E0E\u9A8C\u6536\u69FD\u4F4D\u2026" }) : null, recoveryPreflight.status === 'error' ? _jsx("p", { className: "dsh-wt-error", children: recoveryPreflight.error.message }) : null, recoveryPreflight.status === 'success' && recoveryPreflight.preflight.status === 'blocked' ? (_jsx("p", { className: "dsh-wt-error", children: recoveryPreflight.preflight.message })) : null, recoveryProof ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "dsh-wt-code", children: ["generation ", recoveryProof.generation.slice(0, 12), " \u00B7 HEAD ", recoveryProof.localHeadOid.slice(0, 12), " \u00B7 ", recoveryProof.localHeadRef ?? 'detached HEAD'] }), _jsxs("ul", { className: "dsh-wt-test-list", "aria-label": "Preview Recovery \u7ED3\u8BBA", children: [_jsxs("li", { children: ["\u64A4\u56DE\uFF1A", recoveryProof.rollback.status === 'safe' ? '可证明安全' : recoveryProof.rollback.message] }), _jsxs("li", { children: ["\u63D0\u4EA4\uFF1A", recoveryProof.finalize.status === 'safe' ? '可证明安全' : recoveryProof.finalize.message] })] }), recoveryProof.rollback.status === 'blocked' && recoveryProof.rollback.conflictingFiles?.length ? (_jsxs("p", { children: ["\u64A4\u56DE\u51B2\u7A81\uFF1A", recoveryProof.rollback.conflictingFiles.join('、')] })) : null, recoveryProof.finalize.status === 'blocked' && recoveryProof.finalize.conflictingFiles?.length ? (_jsxs("p", { children: ["\u63D0\u4EA4\u51B2\u7A81\uFF1A", recoveryProof.finalize.conflictingFiles.join('、')] })) : null, recoveryProof.blocker ? (_jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null || !services, onClick: () => { void openHolder(); }, children: "\u6253\u5F00\u5360\u7528 Local \u9A8C\u6536\u69FD\u4F4D\u7684 Worktree" })) : null, _jsxs("div", { className: "dsh-wt-recovery-actions", children: [_jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null || !services, onClick: () => { void analyzeRecovery(); }, children: "\u8BA9 Agent \u53EA\u8BFB\u5206\u6790" }), _jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null || !services, onClick: () => { void handoffRecovery(); }, children: "\u4EA4\u63A5\u5230\u65B0 Worktree" })] })] })) : null] })) : null, runtimeConflict ? (_jsx("div", { className: "dsh-wt-recovery-actions", children: _jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null || !services, onClick: recoverRuntimeConflict, children: "\u8BA9 Agent \u89E3\u51B3\u51B2\u7A81" }) })) : null, activeRecovery ? (_jsxs("div", { className: "dsh-wt-action-status", "data-recovery-status": activeRecovery.status, children: [activeRecovery.status === 'queued' ? '恢复请求已排队,等待 owner Session 加载完成且停止 streaming。' : null, activeRecovery.status === 'sending' ? '正在通过 Harness 官方 Session API 发送恢复请求…' : null, activeRecovery.status === 'sent'
855
+ ? activeRecovery.request.kind === 'worktree_apply_conflict'
856
+ ? '已交给 Agent 解决冲突;完成后必须生成新的验收卡。'
857
+ : '已交给 Agent 只读重新生成验收结果;不会修改 Worktree。'
858
+ : null, activeRecovery.status === 'cancelled' ? 'Session/checkout 已切换,旧恢复请求已取消。' : null, activeRecovery.status === 'failed' ? (_jsxs(_Fragment, { children: [_jsxs("span", { children: ["\u6062\u590D\u8BF7\u6C42\u53D1\u9001\u5931\u8D25\uFF1A", activeRecovery.error] }), _jsx("button", { type: "button", className: "dsh-wt-inline-action", disabled: submitting !== null, onClick: () => retryWorktreeRecovery(activeRecovery.request.sessionId), children: "\u91CD\u65B0\u53D1\u9001" })] })) : null] })) : null, target && (target.state === 'cleanup_pending' || terminal) ? _jsx(DeliveryProof, { target: target }) : null, _jsx("div", { className: "dsh-wt-action-status", "aria-live": "polite", children: submitting ? '正在处理 Worktree,请稍候…' : message }), error && !isStale(error) ? (_jsxs("div", { className: "dsh-wt-error", role: "alert", children: [error.message, "\uFF08", worktreeConsoleErrorMeta(error.code).category, "\uFF1B\u6062\u590D\u65B9\u5F0F\uFF1A", worktreeConsoleErrorMeta(error.code).recovery, "\uFF09"] })) : null] }));
233
859
  }
234
860
  //# sourceMappingURL=ReviewActions.js.map