dsh-git-worktree 0.4.0 → 0.7.1

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 (95) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/NOTICE +14 -0
  3. package/README.en.md +74 -266
  4. package/README.md +78 -286
  5. package/cordis.patch.yml +7 -3
  6. package/lib/adapters/git.js +8 -0
  7. package/lib/adapters/git.js.map +1 -1
  8. package/lib/adapters/registry.js +100 -1
  9. package/lib/adapters/registry.js.map +1 -1
  10. package/lib/checkpoint.d.ts +3 -0
  11. package/lib/checkpoint.js +29 -0
  12. package/lib/checkpoint.js.map +1 -0
  13. package/lib/client/WorktreeReviewRow.d.ts +1 -1
  14. package/lib/client/WorktreeReviewRow.js +32 -4
  15. package/lib/client/WorktreeReviewRow.js.map +1 -1
  16. package/lib/client/actions.d.ts +25 -1
  17. package/lib/client/actions.js.map +1 -1
  18. package/lib/client/console-remote/adapter.js +11 -3
  19. package/lib/client/console-remote/adapter.js.map +1 -1
  20. package/lib/client/index.d.ts +1 -1
  21. package/lib/client/index.js +3 -1
  22. package/lib/client/index.js.map +1 -1
  23. package/lib/client/pre-session/controller.js +9 -0
  24. package/lib/client/pre-session/controller.js.map +1 -1
  25. package/lib/client/review-console/PreflightStatus.d.ts +3 -2
  26. package/lib/client/review-console/PreflightStatus.js +3 -3
  27. package/lib/client/review-console/PreflightStatus.js.map +1 -1
  28. package/lib/client/review-console/ReviewActions.js +566 -27
  29. package/lib/client/review-console/ReviewActions.js.map +1 -1
  30. package/lib/client/review-console/WorktreeReviewPanel.js +18 -3
  31. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -1
  32. package/lib/client/review-console/preflight-cache.d.ts +2 -0
  33. package/lib/client/review-console/preflight-cache.js +18 -5
  34. package/lib/client/review-console/preflight-cache.js.map +1 -1
  35. package/lib/client/review-console/preview-recovery-cache.d.ts +20 -0
  36. package/lib/client/review-console/preview-recovery-cache.js +103 -0
  37. package/lib/client/review-console/preview-recovery-cache.js.map +1 -0
  38. package/lib/client/review-console/recovery-continuation.d.ts +53 -0
  39. package/lib/client/review-console/recovery-continuation.js +483 -0
  40. package/lib/client/review-console/recovery-continuation.js.map +1 -0
  41. package/lib/client/review-console/review-console.styles.js +9 -1
  42. package/lib/client/review-console/review-console.styles.js.map +1 -1
  43. package/lib/client/styles.js +61 -0
  44. package/lib/client/styles.js.map +1 -1
  45. package/lib/client/target-console/TargetStatusAction.js +2 -2
  46. package/lib/client/target-console/TargetStatusAction.js.map +1 -1
  47. package/lib/client/target-console/WorktreeConsoleView.js +1 -1
  48. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -1
  49. package/lib/client/target-console/WorktreeReviewStatus.js +19 -6
  50. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -1
  51. package/lib/client/workspace-sidebar/index.d.ts +33 -0
  52. package/lib/client/workspace-sidebar/index.js +127 -0
  53. package/lib/client/workspace-sidebar/index.js.map +1 -0
  54. package/lib/client/workspace-sidebar/model.d.ts +37 -0
  55. package/lib/client/workspace-sidebar/model.js +88 -0
  56. package/lib/client/workspace-sidebar/model.js.map +1 -0
  57. package/lib/client.js +4511 -106
  58. package/lib/console-contract.d.ts +124 -1
  59. package/lib/console-contract.js.map +1 -1
  60. package/lib/console-host/control-plane.d.ts +7 -1
  61. package/lib/console-host/control-plane.js +264 -9
  62. package/lib/console-host/control-plane.js.map +1 -1
  63. package/lib/console-host/errors.d.ts +2 -2
  64. package/lib/console-host/errors.js +21 -5
  65. package/lib/console-host/errors.js.map +1 -1
  66. package/lib/console-host/projection.js +60 -1
  67. package/lib/console-host/projection.js.map +1 -1
  68. package/lib/console-host/service.d.ts +11 -5
  69. package/lib/console-host/service.js +54 -6
  70. package/lib/console-host/service.js.map +1 -1
  71. package/lib/console-remote/descriptors.js +40 -3
  72. package/lib/console-remote/descriptors.js.map +1 -1
  73. package/lib/console-remote/remote.d.ts +11 -5
  74. package/lib/console-remote/remote.js.map +1 -1
  75. package/lib/console-remote/schemas.d.ts +104 -1
  76. package/lib/console-remote/schemas.js +161 -5
  77. package/lib/console-remote/schemas.js.map +1 -1
  78. package/lib/index.d.ts +13 -2
  79. package/lib/index.js.map +1 -1
  80. package/lib/ports.d.ts +76 -1
  81. package/lib/session-checkout-apply.d.ts +76 -0
  82. package/lib/session-checkout-apply.js +608 -143
  83. package/lib/session-checkout-apply.js.map +1 -1
  84. package/lib/session-checkout-module.js +759 -124
  85. package/lib/session-checkout-module.js.map +1 -1
  86. package/lib/tools.js +1 -1
  87. package/lib/tools.js.map +1 -1
  88. package/lib/types.d.ts +93 -2
  89. package/lib/types.js.map +1 -1
  90. package/package.json +22 -14
  91. package/scripts/check-publish.mjs +17 -2
  92. package/scripts/check-workspace-sidebar-version.mjs +14 -0
  93. package/scripts/dev-dsh-lib.mjs +1 -1
  94. package/scripts/workspace-sidebar-upstream.d.mts +8 -0
  95. package/scripts/workspace-sidebar-upstream.mjs +160 -0
@@ -2,14 +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, prefillSessionDraft, } from '../actions.js';
5
+ import { openAuthorizedWorktreeTarget, openIsolatedTarget, prefillSessionDraft, } from '../actions.js';
6
6
  import { DeliveryProof } from './DeliveryProof.js';
7
7
  import { PreflightStatus } from './PreflightStatus.js';
8
- import { readReviewPreflight, useReviewPreflight } from './preflight-cache.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';
9
11
  import { requestWorktreeReviewRefresh } from './status-events.js';
12
+ function checkpointRequestIdForGeneration(generation) {
13
+ return `checkpoint:${generation}`;
14
+ }
10
15
  function isStale(error) {
11
16
  return error.code === 'stale_target' || error.code === 'stale_isolated' || error.code === 'stale_local';
12
17
  }
18
+ function sameStrings(left, right) {
19
+ return left.length === right.length && left.every((value, index) => value === right[index]);
20
+ }
13
21
  function preflightMessage(preflight) {
14
22
  if (preflight.status === 'ready')
15
23
  return '同步预检通过,正在创建可撤回的 Local Preview。';
@@ -25,16 +33,23 @@ function preflightMessage(preflight) {
25
33
  }
26
34
  export function ReviewActions({ review, adapter, services, identity, target, disabled, unavailableMessage, focusReview, isActive = () => true, onStale, onTargetChange, }) {
27
35
  const formId = useId();
36
+ const moreMenuRef = useRef(null);
28
37
  const [submitting, setSubmitting] = useState(null);
29
38
  const [commitMode, setCommitMode] = useState(null);
39
+ const [checkpointOpen, setCheckpointOpen] = useState(false);
30
40
  const [discardOpen, setDiscardOpen] = useState(false);
31
41
  const [commitMessage, setCommitMessage] = useState(review.suggestedCommitMessage);
42
+ const [checkpointMessage, setCheckpointMessage] = useState(review.suggestedCommitMessage);
43
+ const checkpointRequestId = useRef(null);
32
44
  const [retainEnvironment, setRetainEnvironment] = useState(false);
33
45
  const [retention, setRetention] = useState('retain_24h');
34
46
  const [message, setMessage] = useState(null);
35
47
  const [error, setError] = useState(null);
48
+ const [runtimeConflict, setRuntimeConflict] = useState(null);
36
49
  const mutationLock = useRef(false);
37
50
  const observedTargetRevision = useRef(target?.revision);
51
+ const observedReviewSlot = useRef(target?.reviewSlot);
52
+ const recoverySnapshot = useWorktreeRecoverySnapshot(identity?.sessionId);
38
53
  const autoPreflightEnabled = Boolean(adapter
39
54
  && identity
40
55
  && !disabled
@@ -44,11 +59,59 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
44
59
  const safeReadyPreflight = preflightSnapshot.status === 'success'
45
60
  && preflightSnapshot.preflight.status !== 'blocked'
46
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]);
47
84
  useEffect(() => {
48
85
  setCommitMessage(review.suggestedCommitMessage);
86
+ setCheckpointMessage(review.suggestedCommitMessage);
87
+ setCheckpointOpen(false);
88
+ if (moreMenuRef.current)
89
+ moreMenuRef.current.open = false;
90
+ checkpointRequestId.current = null;
49
91
  setRetainEnvironment(false);
50
92
  setRetention('retain_24h');
51
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
+ }, []);
52
115
  useEffect(() => {
53
116
  const revision = target?.revision;
54
117
  if (revision === undefined || revision === observedTargetRevision.current)
@@ -58,7 +121,19 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
58
121
  return;
59
122
  setMessage(null);
60
123
  setError(null);
124
+ setRuntimeConflict(null);
61
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]);
62
137
  const begin = (mutation) => {
63
138
  if (disabled || !adapter || !identity || !target || mutationLock.current)
64
139
  return false;
@@ -72,7 +147,19 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
72
147
  mutationLock.current = false;
73
148
  setSubmitting(null);
74
149
  };
150
+ const closeMoreMenu = () => {
151
+ if (moreMenuRef.current)
152
+ moreMenuRef.current.open = false;
153
+ };
75
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
+ }
76
163
  setError(nextError);
77
164
  if (nextError.code === 'stale_target')
78
165
  onStale(nextError);
@@ -90,6 +177,183 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
90
177
  if (identity)
91
178
  requestWorktreeReviewRefresh(identity.sessionId);
92
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
+ };
93
357
  const previewLocal = async () => {
94
358
  if (!begin('preview') || !adapter || !identity)
95
359
  return;
@@ -124,6 +388,53 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
124
388
  setMessage('已同步为可撤回的 Local Preview;请在 Local 中验收。');
125
389
  finish();
126
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
+ };
127
438
  const resumeRevision = async () => {
128
439
  if (!begin('resume_revision') || !adapter || !identity)
129
440
  return;
@@ -148,7 +459,41 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
148
459
  const rollbackPreview = async (resumeRevision = false) => {
149
460
  if (!begin('rollback') || !adapter || !identity)
150
461
  return;
151
- 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
+ });
152
497
  if (!isActive()) {
153
498
  finish();
154
499
  return;
@@ -159,9 +504,7 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
159
504
  }
160
505
  applyTarget(outcome.value.target);
161
506
  setMessage(outcome.value.target.state === 'preview_detached'
162
- ? outcome.value.target.previewRecovery?.reason === 'stale_local'
163
- ? 'Local branch/HEAD 已变化;同分支安全快进可重试撤回,其他历史变化不会被覆盖。'
164
- : 'Preview 与 Local 存在冲突;恢复证据和 Worktree 已保留。'
507
+ ? '恢复条件在写入前发生变化;Preview 证据和 Worktree 已保留,请重新检查。'
165
508
  : resumeRevision ? '已撤回 Local Preview,可以继续修改 Worktree。' : '已撤回 Local Preview,验收卡仍可再次同步。');
166
509
  finish();
167
510
  };
@@ -188,8 +531,38 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
188
531
  return;
189
532
  }
190
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
+ }
191
564
  const outcome = mode === 'finalize_preview'
192
- ? await adapter.finalizePreview(request)
565
+ ? await adapter.finalizePreview({ ...request, ...(previewProof ? { recoveryProof: previewProof } : {}) })
193
566
  : await adapter.finalize(request);
194
567
  if (!isActive()) {
195
568
  finish();
@@ -234,6 +607,143 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
234
607
  setDiscardOpen(false);
235
608
  finish();
236
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
+ };
237
747
  const retryCleanup = async () => {
238
748
  if (!begin('retry_cleanup') || !adapter || !identity)
239
749
  return;
@@ -259,7 +769,7 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
259
769
  && preflightSnapshot.preflight.status === 'blocked'
260
770
  && preflightSnapshot.preflight.reason === 'project_acceptance_busy'
261
771
  ? preflightSnapshot.preflight.blocker ?? target?.reviewSlotHolder
262
- : target?.reviewSlotHolder;
772
+ : recoveryProof?.blocker ?? target?.reviewSlotHolder;
263
773
  if (!holder || !adapter || !identity || !services || mutationLock.current)
264
774
  return;
265
775
  mutationLock.current = true;
@@ -281,8 +791,10 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
281
791
  const previewActive = target?.state === 'preview_active';
282
792
  const previewRecovery = target?.state === 'preview_detached'
283
793
  || target?.state === 'recovery_required' && target.capabilities.rollbackPreview;
284
- const safeFastForwardRecovery = target?.state === 'preview_detached'
285
- && 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';
286
798
  const cleanupPending = target?.state === 'cleanup_pending';
287
799
  const terminal = target?.state === 'retained' || target?.state === 'delivered';
288
800
  const canDiscard = Boolean(target?.capabilities.discard);
@@ -290,34 +802,61 @@ export function ReviewActions({ review, adapter, services, identity, target, dis
290
802
  if (submitting === null)
291
803
  setCommitMode(null);
292
804
  };
805
+ const closeCheckpoint = () => {
806
+ if (submitting !== null)
807
+ return;
808
+ setCheckpointOpen(false);
809
+ checkpointRequestId.current = null;
810
+ };
293
811
  const closeDiscard = () => {
294
812
  if (submitting === null)
295
813
  setDiscardOpen(false);
296
814
  };
297
- 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' ? '同步中…' : '同步到 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
+ : '预览修改' })) : previewActive ? (_jsx("button", { type: "button", className: "dsh-wt-button dsh-wt-primary", disabled: allDisabled || !target.capabilities.finalizePreview, onClick: () => setCommitMode('finalize_preview'), children: "\u786E\u8BA4\u5E76\u4FDD\u5B58" })) : previewRecovery ? (rollbackRecoverySafe ? (_jsx("button", { type: "button", className: "dsh-wt-button", disabled: allDisabled || !target?.capabilities.rollbackPreview, onClick: () => { void rollbackPreview(true); }, children: submitting === 'rollback' ? '处理中…' : '恢复并撤回预览' })) : (_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;
298
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'
299
821
  ? `本轮已提交,运行环境暂时保留${target.commitOid ? ` · ${target.commitOid.slice(0, 8)}` : ''}`
300
- : `本轮已交付${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: () => {
301
823
  focusReview();
302
- event.currentTarget.closest('details')?.removeAttribute('open');
303
- }, 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: () => {
304
829
  void resumeRevision();
305
- event.currentTarget.closest('details')?.removeAttribute('open');
306
- }, children: submitting === 'resume_revision' ? '恢复中…' : '继续修改' }), _jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item", disabled: allDisabled || !target.capabilities.finalize || !safeReadyPreflight, 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: () => {
307
832
  setCommitMode('finish');
308
- event.currentTarget.closest('details')?.removeAttribute('open');
309
- }, 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\u9884\u89C8\u5E76\u4FDD\u5B58" })] })) : 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: "\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: () => {
310
838
  void rollbackPreview(true);
311
- event.currentTarget.closest('details')?.removeAttribute('open');
312
- }, 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 ? '撤回本次预览' : '重新尝试撤回' })) : 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: "\u4FDD\u5B58\u4FEE\u6539" })) : null, canDiscard ? (_jsx("button", { type: "button", role: "menuitem", className: "dsh-wt-more-item dsh-wt-danger-text", disabled: allDisabled, onClick: () => {
313
844
  setDiscardOpen(true);
314
- event.currentTarget.closest('details')?.removeAttribute('open');
315
- }, 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'
316
- ? '只会提交当前可撤回 Preview 对应的任务增量;Local 中无关修改不会进入该 Commit。'
317
- : '将跳过 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'
318
- ? '正在提交…'
319
- : 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
845
+ closeMoreMenu();
846
+ }, children: "\u653E\u5F03\u4EFB\u52A1" })) : null] })] })] })), _jsx(Modal, { open: checkpointOpen, onClose: closeCheckpoint, title: "\u4FDD\u5B58\u5F53\u524D\u8FDB\u5EA6\u5E76\u7EE7\u7EED\uFF1F", closeLabel: "\u5173\u95ED\u4FDD\u5B58\u8FDB\u5EA6\u786E\u8BA4", description: previewActive
847
+ ? '会先安全撤回当前预览,再保存本轮任务进度并继续开发;无法证明可以安全撤回时会停止。当前项目不会立即更新。'
848
+ : '保存当前任务进度并继续下一阶段;当前项目不会立即更新,最终确认时仍只会生成一次交付。', 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'
849
+ ? '确认并保存本次修改?'
850
+ : target?.state === 'preview_detached' ? '保存本次修改?' : '跳过预览并直接保存?', closeLabel: "\u5173\u95ED\u4FDD\u5B58\u786E\u8BA4", description: commitMode === 'finalize_preview'
851
+ ? '只会保存当前预览对应的本轮任务内容;Local 中已有或之后新增的无关修改不会进入该 Commit。'
852
+ : '将跳过 Local Preview,直接把本轮 Worktree 增量保存为一个 Commit。', 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'
853
+ ? '正在保存…'
854
+ : 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
320
855
  ? '会先安全撤回本次 Local Preview,再清理 Worktree;无法无损撤回时会停止,不会覆盖 Local 修改。'
321
- : '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(); }, onResume: () => { void resumeRevision(); }, onOpenHolder: () => { void openHolder(); }, busy: submitting !== 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] }));
856
+ : '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'
857
+ ? activeRecovery.request.kind === 'worktree_apply_conflict'
858
+ ? '已交给 Agent 解决冲突;完成后必须生成新的验收卡。'
859
+ : '已交给 Agent 只读重新生成验收结果;不会修改 Worktree。'
860
+ : 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] }));
322
861
  }
323
862
  //# sourceMappingURL=ReviewActions.js.map