dsh-worktree-switch 0.5.0 → 0.5.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.
package/lib/client.js CHANGED
@@ -77,196 +77,21 @@ function WorktreeBadge(props) {
77
77
  })
78
78
  });
79
79
  }
80
- // src/client/Chip.tsx
80
+ // src/types.ts
81
81
  /**
82
- * The New Session switch.
82
+ * Whether a leave that kept a copy left a 保留副本 behind.
83
83
  *
84
- * The slot it occupies (`conversation.input.left`) renders in the composer of
85
- * every open session, and gives its occupant no `variant`; the blank-session
86
- * hero is instead recognised from the session itself, exactly as the shell
87
- * recognises it: a session whose list summary says blank and whose history is
88
- * open. Outside that state this component renders nothing at all — the spec's
89
- * "no greyed remnant" — and it releases the composer block it raised.
90
- * @module dsh-worktree-switch/client/Chip
91
- */
92
- /**
93
- * The line the switch shows.
94
- * @param state - current state.
95
- * @param t - bound translate.
96
- * @returns visible label.
97
- */
98
- function chipLabel(state, t) {
99
- switch (state.phase) {
100
- case "checking": return t("chip.checking");
101
- case "off": return t("chip.off");
102
- case "creating": return t("chip.creating");
103
- case "reverting": return t("chip.reverting");
104
- case "ready": return t("chip.ready", { name: state.worktree?.name ?? "" });
105
- case "failed": return t("chip.failed");
106
- case "disabled": return t("chip.disabled");
107
- }
108
- }
109
- /**
110
- * The hover text: why the switch is where it is, and what the copy is.
111
- * @param state - current state.
112
- * @param t - bound translate.
113
- * @returns tooltip text.
114
- */
115
- function chipHint(state, t) {
116
- switch (state.phase) {
117
- case "checking": return t("chip.hint.checking");
118
- case "off": {
119
- const parts = [t("chip.hint.off")];
120
- if (state.kept !== null) parts.push(state.kept.reason === "dirty" ? t("chip.hint.keptDirty", { name: state.kept.worktree.name }) : t("chip.hint.keptNotOwned", { name: state.kept.worktree.name }));
121
- if (state.error !== null) parts.push(state.error);
122
- return parts.join(" ");
123
- }
124
- case "creating": return t("chip.hint.creating");
125
- case "reverting": return t("chip.hint.reverting");
126
- case "ready": {
127
- const copy = state.worktree;
128
- const parts = [copy === null || copy.branch === null ? t("chip.hint.detached", { path: copy?.path ?? "" }) : t("chip.hint.ready", {
129
- branch: copy.branch,
130
- path: copy.path
131
- })];
132
- if (state.base?.kind === "fallback") parts.push(t("chip.hint.baseFallback"));
133
- if (!state.intent) parts.push(t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }));
134
- if (state.sourceDirty > 0) parts.push(t("chip.hint.dirty", { count: state.sourceDirty }));
135
- if (state.error !== null) parts.push(state.error);
136
- return parts.join(" ");
137
- }
138
- case "failed": {
139
- const base = t("chip.hint.failed");
140
- return state.error === null ? base : `${base} ${state.error}`;
141
- }
142
- case "disabled": return t("chip.hint.disabled");
143
- }
144
- }
145
- /**
146
- * Render the Worktree switch.
147
- * @param props - slot runtime props, the binding face, and the locale seat.
148
- * @returns the switch, or null outside the blank-session hero.
149
- */
150
- function WorktreeChip(props) {
151
- const { useSession, useToggle, ensure, setIntent, retry, block, t } = props;
152
- const hero = useSession((session) => session.blank && session.openState === "open");
153
- const state = useToggle((current) => current);
154
- (0, react.useEffect)(() => {
155
- if (hero) ensure();
156
- }, [ensure, hero]);
157
- (0, react.useEffect)(() => {
158
- if (!hero) {
159
- block(null);
160
- return;
161
- }
162
- const reason = state.phase === "checking" ? t("chip.blocked.checking") : state.phase === "creating" ? t("chip.blocked.creating") : state.phase === "reverting" ? t("chip.blocked.reverting") : state.phase === "failed" && state.intent ? t("chip.blocked.failed") : null;
163
- block(reason);
164
- }, [
165
- block,
166
- hero,
167
- state.intent,
168
- state.phase,
169
- t
170
- ]);
171
- (0, react.useEffect)(() => () => {
172
- block(null);
173
- }, [block]);
174
- if (!hero) return null;
175
- const hint = chipHint(state, t);
176
- const locked = state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting";
177
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
178
- className: "dsh-wt-chip",
179
- "data-phase": state.phase,
180
- "data-intent": state.intent ? "on" : "off",
181
- "data-kept": state.kept === null ? "false" : state.kept.reason,
182
- "data-hint": hint,
183
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
184
- label: hint,
185
- side: "top",
186
- delayMs: 200,
187
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
188
- className: "dsh-wt-anchor",
189
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
190
- type: "button",
191
- role: "switch",
192
- "aria-checked": state.intent,
193
- "aria-label": t("chip.label"),
194
- className: "dsh-wt-switch",
195
- "data-phase": state.phase,
196
- disabled: locked,
197
- onClick: () => {
198
- setIntent(!state.intent);
199
- },
200
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
201
- className: "dsh-wt-dot",
202
- "aria-hidden": "true"
203
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
204
- className: "dsh-wt-label",
205
- children: chipLabel(state, t)
206
- })]
207
- })
208
- })
209
- }), state.phase === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
210
- type: "button",
211
- className: "dsh-wt-retry",
212
- onClick: () => {
213
- retry();
214
- },
215
- children: t("chip.retry")
216
- }) : null]
217
- });
218
- }
219
- // src/client/Notice.tsx
220
- /**
221
- * The sentence a notice shows.
222
- * @param notice - what the leave reported.
223
- * @param t - bound translate.
224
- * @returns the localized explanation.
84
+ * Not every copy a leave leaves on disk is a leftover: only `dirty` is. A copy
85
+ * kept because this session never owned it (`not-owned`) is the ordinary result
86
+ * of leaving someone else's copy — explained in the switch's hover, never
87
+ * reported as cleanup. The host registers a kept copy for the panel count and
88
+ * the browser half raises the one-shot notice only when this returns true, so
89
+ * the two halves cannot drift apart on the rule.
90
+ * @param reason - why the host kept the copy, or null when it kept nothing.
91
+ * @returns whether the kept copy is a leftover this host has to clean up.
225
92
  */
226
- function noticeText(notice, t) {
227
- if (notice.kind === "failed") return t("notice.failed", { message: notice.message ?? "" });
228
- const name = notice.worktree?.name ?? "";
229
- return notice.reason === "dirty" ? t("notice.keptDirty", { name }) : t("notice.keptNotOwned", { name });
230
- }
231
- /**
232
- * Render the one-shot notice.
233
- * @param props - overlay runtime props, the notice face, and the locale seat.
234
- * @returns the banner, or null while nothing happened.
235
- */
236
- function WorktreeNotice(props) {
237
- const { useNotice, dismiss, openPanel, t } = props;
238
- const notice = useNotice((current) => current);
239
- if (notice === null) return null;
240
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
241
- className: "dsh-wt-notice",
242
- "data-kind": notice.kind,
243
- "data-seq": notice.seq,
244
- role: "status",
245
- children: [
246
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
247
- className: "dsh-wt-notice-text",
248
- title: notice.kind === "kept" && notice.message !== null ? notice.message : void 0,
249
- children: noticeText(notice, t)
250
- }),
251
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
252
- type: "button",
253
- className: "dsh-wt-notice-open",
254
- onClick: () => {
255
- openPanel();
256
- },
257
- children: t("notice.openPanel")
258
- }),
259
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
260
- type: "button",
261
- className: "dsh-wt-notice-dismiss",
262
- "aria-label": t("notice.dismiss"),
263
- onClick: () => {
264
- dismiss();
265
- },
266
- children: "×"
267
- })
268
- ]
269
- }, notice.seq);
93
+ function isKeptLeftover(reason) {
94
+ return reason === "dirty";
270
95
  }
271
96
  // src/client/intent.ts
272
97
  /**
@@ -340,6 +165,21 @@ async function callHost(ctx, endpoint, payload, signal) {
340
165
  return result.value;
341
166
  }
342
167
  // src/client/state.ts
168
+ /**
169
+ * The switch position the page shows: on exactly while the session runs in a
170
+ * copy.
171
+ *
172
+ * The position is derived, not stored: `ready` means the host confirmed a copy
173
+ * for this session, and every other face falls back to the project's isolation
174
+ * default. Keeping the two apart is what lets `/new` inherit a copy in a
175
+ * project whose default is off without the control claiming the session runs
176
+ * in the source checkout.
177
+ * @param state - the current switch state.
178
+ * @returns whether the switch reads as on.
179
+ */
180
+ function switchPosition(state) {
181
+ return state.phase === "ready" ? true : state.intent;
182
+ }
343
183
  /** The initial state: nothing is known until the host answers. */
344
184
  const INITIAL = {
345
185
  phase: "checking",
@@ -349,6 +189,7 @@ const INITIAL = {
349
189
  sourceDirty: 0,
350
190
  base: null,
351
191
  error: null,
192
+ leaveFailed: false,
352
193
  kept: null
353
194
  };
354
195
  /** A thrown value's message, without leaking `undefined`. */
@@ -562,6 +403,16 @@ var InternalBinding = class {
562
403
  creating = false;
563
404
  /** The release in flight, shared by callers that ask to leave at once. */
564
405
  releasing = null;
406
+ /** A check is in flight; a recheck that arrives meanwhile must not double it. */
407
+ checking = false;
408
+ /** A recheck arrived while one was running: run exactly one more afterwards. */
409
+ checkAgain = false;
410
+ /**
411
+ * Whether the chip is still in the blank-session hero. A session that has
412
+ * history must never be moved, even by a check that was in flight when its
413
+ * first prompt landed.
414
+ */
415
+ following = false;
565
416
  face;
566
417
  /**
567
418
  * @param ctx - browser Cordis context.
@@ -579,6 +430,12 @@ var InternalBinding = class {
579
430
  ensure: () => {
580
431
  this.ensure();
581
432
  },
433
+ recheck: () => {
434
+ this.recheck();
435
+ },
436
+ follow: (active) => {
437
+ this.following = active;
438
+ },
582
439
  setIntent: (next) => {
583
440
  this.setIntent(next);
584
441
  },
@@ -594,7 +451,40 @@ var InternalBinding = class {
594
451
  ensure() {
595
452
  if (this.started) return;
596
453
  this.started = true;
597
- this.check();
454
+ this.run();
455
+ }
456
+ /**
457
+ * Re-run the whole check after the session's working directory moved.
458
+ *
459
+ * The working-directory projection is the only announcement of a `/new`
460
+ * inheritance, a host self-heal, or a completed leave, so the switch follows
461
+ * it with the same round trip as the first check. That allows one more
462
+ * `prepare` — only for a session the host reports without a copy while the
463
+ * isolation default is on. A create or leave in flight owns the switch: the
464
+ * change it caused is already covered by its own answer, and a second
465
+ * prepare/release must never race it.
466
+ */
467
+ recheck() {
468
+ if (this.creating || this.releasing !== null) return;
469
+ if (!this.started) {
470
+ this.ensure();
471
+ return;
472
+ }
473
+ this.run();
474
+ }
475
+ /** Run one check, coalescing a recheck that arrives while it is in flight. */
476
+ run() {
477
+ if (this.checking) {
478
+ this.checkAgain = true;
479
+ return;
480
+ }
481
+ this.checking = true;
482
+ this.check().finally(() => {
483
+ this.checking = false;
484
+ if (!this.checkAgain) return;
485
+ this.checkAgain = false;
486
+ this.recheck();
487
+ });
598
488
  }
599
489
  /** Ask the host for the project's facts and act on the remembered choice. */
600
490
  async check() {
@@ -605,28 +495,32 @@ var InternalBinding = class {
605
495
  ...this.store.getSnapshot(),
606
496
  phase: "disabled",
607
497
  intent: status.defaultEnabled,
608
- error: null
498
+ error: null,
499
+ leaveFailed: false
609
500
  });
610
501
  return;
611
502
  }
612
503
  this.project = status.repositoryRoot;
504
+ const now = this.store.getSnapshot();
613
505
  const intent = readIntent(status.repositoryRoot) ?? status.defaultEnabled;
506
+ const sameCopy = now.worktree !== null && status.sessionWorktree !== null && now.worktree.path === status.sessionWorktree.path;
507
+ const kept = !intent && now.phase === "off" && now.kept !== null && status.sessionWorktree === null ? now.kept : null;
614
508
  const base = {
615
509
  phase: "off",
616
510
  intent,
617
511
  worktree: status.sessionWorktree,
618
512
  repositoryRoot: status.repositoryRoot,
619
513
  sourceDirty: status.sourceDirty,
620
- base: null,
514
+ base: sameCopy ? now.base : null,
621
515
  error: null,
622
- kept: null
516
+ leaveFailed: false,
517
+ kept
623
518
  };
624
519
  if (status.sessionWorktree !== null) {
625
520
  this.store.set({
626
521
  ...base,
627
522
  phase: "ready"
628
523
  });
629
- if (!intent) this.release();
630
524
  return;
631
525
  }
632
526
  if (!intent) {
@@ -637,8 +531,16 @@ var InternalBinding = class {
637
531
  await this.create();
638
532
  } catch (error) {
639
533
  if (this.signal.aborted) return;
534
+ const now = this.store.getSnapshot();
535
+ if (now.worktree !== null) {
536
+ this.store.set({
537
+ ...now,
538
+ error: messageOf(error)
539
+ });
540
+ return;
541
+ }
640
542
  this.store.set({
641
- ...this.store.getSnapshot(),
543
+ ...now,
642
544
  phase: "failed",
643
545
  error: messageOf(error)
644
546
  });
@@ -646,7 +548,7 @@ var InternalBinding = class {
646
548
  }
647
549
  /** Create the copy for a session the switch says should have one. */
648
550
  async create() {
649
- if (this.creating) return;
551
+ if (!this.following || this.creating) return;
650
552
  const before = this.store.getSnapshot();
651
553
  if (before.repositoryRoot === null || !before.intent) return;
652
554
  this.creating = true;
@@ -670,6 +572,7 @@ var InternalBinding = class {
670
572
  sourceDirty: prepared.sourceDirty,
671
573
  base: prepared.state === "created" ? prepared.base : null,
672
574
  error: null,
575
+ leaveFailed: false,
673
576
  kept: null
674
577
  });
675
578
  } catch (error) {
@@ -727,13 +630,13 @@ var InternalBinding = class {
727
630
  worktree: null,
728
631
  base: null,
729
632
  error: null,
633
+ leaveFailed: false,
730
634
  kept
731
635
  });
732
- if (kept !== null) {
636
+ if (kept !== null && isKeptLeftover(kept.reason)) {
733
637
  this.notices.raise({
734
638
  kind: "kept",
735
639
  worktree: kept.worktree,
736
- reason: kept.reason,
737
640
  message: kept.error ?? null
738
641
  });
739
642
  this.onLeftover();
@@ -747,16 +650,16 @@ var InternalBinding = class {
747
650
  phase: now.worktree === null ? "off" : "ready",
748
651
  intent: false,
749
652
  error: message,
653
+ leaveFailed: true,
750
654
  kept: null
751
655
  });
752
656
  this.notices.raise({
753
657
  kind: "failed",
754
658
  worktree: now.worktree,
755
- reason: null,
756
659
  message
757
660
  });
758
661
  this.onLeftover();
759
- this.recheck();
662
+ this.reread();
760
663
  }
761
664
  }
762
665
  /**
@@ -767,7 +670,7 @@ var InternalBinding = class {
767
670
  * asks once which directory the host now records and shows that; it never
768
671
  * creates, releases, or retries anything.
769
672
  */
770
- async recheck() {
673
+ async reread() {
771
674
  try {
772
675
  const status = await callHost(this.ctx, "status", { sessionId: this.sessionId }, this.signal);
773
676
  if (this.signal.aborted || !status.supported) return;
@@ -787,7 +690,7 @@ var InternalBinding = class {
787
690
  if (state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting") return;
788
691
  if (this.project !== null) writeIntent(this.project, next);
789
692
  if (!next) {
790
- if (state.intent === next) return;
693
+ if (state.phase === "off" && state.worktree === null && !state.intent) return;
791
694
  this.store.set({
792
695
  ...state,
793
696
  intent: false,
@@ -1096,6 +999,216 @@ var PanelController = class {
1096
999
  this.lifetime.abort(/* @__PURE__ */ new Error("worktree switch unloaded"));
1097
1000
  }
1098
1001
  };
1002
+ // src/client/Chip.tsx
1003
+ /**
1004
+ * The New Session switch.
1005
+ *
1006
+ * The slot it occupies (`conversation.input.left`) renders in the composer of
1007
+ * every open session, and gives its occupant no `variant`; the blank-session
1008
+ * hero is instead recognised from the session itself, exactly as the shell
1009
+ * recognises it: a session whose list summary says blank and whose history is
1010
+ * open. Outside that state this component renders nothing at all — the spec's
1011
+ * "no greyed remnant" — and it releases the composer block it raised.
1012
+ * @module dsh-worktree-switch/client/Chip
1013
+ */
1014
+ /**
1015
+ * The line the switch shows.
1016
+ * @param state - current state.
1017
+ * @param t - bound translate.
1018
+ * @returns visible label.
1019
+ */
1020
+ function chipLabel(state, t) {
1021
+ switch (state.phase) {
1022
+ case "checking": return t("chip.checking");
1023
+ case "off": return t("chip.off");
1024
+ case "creating": return t("chip.creating");
1025
+ case "reverting": return t("chip.reverting");
1026
+ case "ready": return t("chip.ready", { name: state.worktree?.name ?? "" });
1027
+ case "failed": return t("chip.failed");
1028
+ case "disabled": return t("chip.disabled");
1029
+ }
1030
+ }
1031
+ /**
1032
+ * The hover text: why the switch is where it is, and what the copy is.
1033
+ * @param state - current state.
1034
+ * @param t - bound translate.
1035
+ * @returns tooltip text.
1036
+ */
1037
+ function chipHint(state, t) {
1038
+ switch (state.phase) {
1039
+ case "checking": return t("chip.hint.checking");
1040
+ case "off": {
1041
+ const parts = [t("chip.hint.off")];
1042
+ if (state.kept !== null) parts.push(state.kept.reason === "dirty" ? t("chip.hint.keptDirty", { name: state.kept.worktree.name }) : t("chip.hint.keptNotOwned", { name: state.kept.worktree.name }));
1043
+ if (state.error !== null) parts.push(state.error);
1044
+ return parts.join(" ");
1045
+ }
1046
+ case "creating": return t("chip.hint.creating");
1047
+ case "reverting": return t("chip.hint.reverting");
1048
+ case "ready": {
1049
+ const copy = state.worktree;
1050
+ const parts = [copy === null || copy.branch === null ? t("chip.hint.detached", { path: copy?.path ?? "" }) : t("chip.hint.ready", {
1051
+ branch: copy.branch,
1052
+ path: copy.path
1053
+ })];
1054
+ if (state.base?.kind === "fallback") parts.push(t("chip.hint.baseFallback"));
1055
+ if (!state.intent) parts.push(state.leaveFailed ? t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }) : t("chip.hint.isolationOff"));
1056
+ if (state.sourceDirty > 0) parts.push(t("chip.hint.dirty", { count: state.sourceDirty }));
1057
+ if (state.error !== null) parts.push(state.error);
1058
+ return parts.join(" ");
1059
+ }
1060
+ case "failed": {
1061
+ const base = t("chip.hint.failed");
1062
+ return state.error === null ? base : `${base} ${state.error}`;
1063
+ }
1064
+ case "disabled": return t("chip.hint.disabled");
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Render the Worktree switch.
1069
+ * @param props - slot runtime props, the binding face, and the locale seat.
1070
+ * @returns the switch, or null outside the blank-session hero.
1071
+ */
1072
+ function WorktreeChip(props) {
1073
+ const { useSession, useProjection, useToggle, ensure, recheck, follow, setIntent, retry, block, t } = props;
1074
+ const hero = useSession((session) => session.blank && session.openState === "open");
1075
+ const workingDirectory = useProjection("workingDirectory");
1076
+ const state = useToggle((current) => current);
1077
+ const position = switchPosition(state);
1078
+ (0, react.useEffect)(() => {
1079
+ follow(hero);
1080
+ return () => {
1081
+ follow(false);
1082
+ };
1083
+ }, [follow, hero]);
1084
+ const followed = (0, react.useRef)(false);
1085
+ (0, react.useEffect)(() => {
1086
+ if (!hero) return;
1087
+ if (!followed.current) {
1088
+ followed.current = true;
1089
+ ensure();
1090
+ return;
1091
+ }
1092
+ recheck();
1093
+ }, [
1094
+ ensure,
1095
+ hero,
1096
+ recheck,
1097
+ workingDirectory
1098
+ ]);
1099
+ (0, react.useEffect)(() => {
1100
+ if (!hero) {
1101
+ block(null);
1102
+ return;
1103
+ }
1104
+ const reason = state.phase === "checking" ? t("chip.blocked.checking") : state.phase === "creating" ? t("chip.blocked.creating") : state.phase === "reverting" ? t("chip.blocked.reverting") : state.phase === "failed" && state.intent ? t("chip.blocked.failed") : null;
1105
+ block(reason);
1106
+ }, [
1107
+ block,
1108
+ hero,
1109
+ state.intent,
1110
+ state.phase,
1111
+ t
1112
+ ]);
1113
+ (0, react.useEffect)(() => () => {
1114
+ block(null);
1115
+ }, [block]);
1116
+ if (!hero) return null;
1117
+ const hint = chipHint(state, t);
1118
+ const locked = state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting";
1119
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1120
+ className: "dsh-wt-chip",
1121
+ "data-phase": state.phase,
1122
+ "data-position": position ? "on" : "off",
1123
+ "data-kept": state.kept === null ? "false" : state.kept.reason,
1124
+ "data-hint": hint,
1125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1126
+ label: hint,
1127
+ side: "top",
1128
+ delayMs: 200,
1129
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1130
+ className: "dsh-wt-anchor",
1131
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1132
+ type: "button",
1133
+ role: "switch",
1134
+ "aria-checked": position,
1135
+ "aria-label": t("chip.label"),
1136
+ className: "dsh-wt-switch",
1137
+ "data-phase": state.phase,
1138
+ disabled: locked,
1139
+ onClick: () => {
1140
+ setIntent(!position);
1141
+ },
1142
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1143
+ className: "dsh-wt-dot",
1144
+ "aria-hidden": "true"
1145
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1146
+ className: "dsh-wt-label",
1147
+ children: chipLabel(state, t)
1148
+ })]
1149
+ })
1150
+ })
1151
+ }), state.phase === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1152
+ type: "button",
1153
+ className: "dsh-wt-retry",
1154
+ onClick: () => {
1155
+ retry();
1156
+ },
1157
+ children: t("chip.retry")
1158
+ }) : null]
1159
+ });
1160
+ }
1161
+ // src/client/Notice.tsx
1162
+ /**
1163
+ * The sentence a notice shows.
1164
+ * @param notice - what the leave reported.
1165
+ * @param t - bound translate.
1166
+ * @returns the localized explanation.
1167
+ */
1168
+ function noticeText(notice, t) {
1169
+ if (notice.kind === "failed") return t("notice.failed", { message: notice.message ?? "" });
1170
+ return t("notice.keptDirty", { name: notice.worktree?.name ?? "" });
1171
+ }
1172
+ /**
1173
+ * Render the one-shot notice.
1174
+ * @param props - overlay runtime props, the notice face, and the locale seat.
1175
+ * @returns the banner, or null while nothing happened.
1176
+ */
1177
+ function WorktreeNotice(props) {
1178
+ const { useNotice, dismiss, openPanel, t } = props;
1179
+ const notice = useNotice((current) => current);
1180
+ if (notice === null) return null;
1181
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1182
+ className: "dsh-wt-notice",
1183
+ "data-kind": notice.kind,
1184
+ "data-seq": notice.seq,
1185
+ role: "status",
1186
+ children: [
1187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1188
+ className: "dsh-wt-notice-text",
1189
+ title: notice.kind === "kept" && notice.message !== null ? notice.message : void 0,
1190
+ children: noticeText(notice, t)
1191
+ }),
1192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1193
+ type: "button",
1194
+ className: "dsh-wt-notice-open",
1195
+ onClick: () => {
1196
+ openPanel();
1197
+ },
1198
+ children: t("notice.openPanel")
1199
+ }),
1200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1201
+ type: "button",
1202
+ className: "dsh-wt-notice-dismiss",
1203
+ "aria-label": t("notice.dismiss"),
1204
+ onClick: () => {
1205
+ dismiss();
1206
+ },
1207
+ children: "×"
1208
+ })
1209
+ ]
1210
+ }, notice.seq);
1211
+ }
1099
1212
  // src/client/Panel.tsx
1100
1213
  /**
1101
1214
  * The Worktrees panel and its sidebar entry.
@@ -1521,8 +1634,9 @@ const en = {
1521
1634
  "chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
1522
1635
  "chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
1523
1636
  "chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
1637
+ "chip.hint.isolationOff": "This session runs in the copy; new sessions in this project start in the source checkout.",
1524
1638
  "chip.hint.keptDirty": "The copy {name} was kept when the switch turned off: it has uncommitted changes or commits of its own.",
1525
- "chip.hint.keptNotOwned": "The copy {name} was kept when the switch turned off: the switch cannot prove it created this copy, so it never deletes it.",
1639
+ "chip.hint.keptNotOwned": "The copy {name} is not deleted by the switch: it was not created for this session.",
1526
1640
  "chip.blocked.checking": "Checking whether this session gets a worktree…",
1527
1641
  "chip.blocked.creating": "Creating the worktree before the first prompt…",
1528
1642
  "chip.blocked.reverting": "Leaving the worktree before the first prompt…",
@@ -1563,7 +1677,6 @@ const en = {
1563
1677
  "badge.detached": "⎇ detached copy",
1564
1678
  "badge.hint": "This session runs in a copy of the project. Directory: {path}. Repository: {root}. Uncommitted files: {count}.",
1565
1679
  "notice.keptDirty": "The Worktree switch is off. The copy {name} was kept because it has uncommitted changes or commits of its own; delete it from the Worktrees panel when you are done.",
1566
- "notice.keptNotOwned": "The Worktree switch is off. The copy {name} was kept because the switch cannot prove it created it, so it never deletes it.",
1567
1680
  "notice.failed": "The Worktree switch is off, but leaving the copy failed: {message}",
1568
1681
  "notice.openPanel": "Open panel",
1569
1682
  "notice.dismiss": "Dismiss",
@@ -1591,9 +1704,10 @@ const zh = {
1591
1704
  "chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
1592
1705
  "chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
1593
1706
  "chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
1594
- "chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
1707
+ "chip.hint.intentOff": "拨回后没能离开 {name};本会话仍在副本里。",
1708
+ "chip.hint.isolationOff": "本会话在副本里运行;这个项目的新会话从源检出开始。",
1595
1709
  "chip.hint.keptDirty": "关闭开关时保留了副本 {name}:它有自己的提交或未提交改动。",
1596
- "chip.hint.keptNotOwned": "关闭开关时保留了副本 {name}:无法证明这份副本是开关创建的,所以不会删除它。",
1710
+ "chip.hint.keptNotOwned": "开关不删这份副本 {name}:它不是为这条会话建的。",
1597
1711
  "chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
1598
1712
  "chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
1599
1713
  "chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
@@ -1634,7 +1748,6 @@ const zh = {
1634
1748
  "badge.detached": "⎇ 分离副本",
1635
1749
  "badge.hint": "本会话在项目的一份副本里运行。目录:{path}。仓库:{root}。未提交文件:{count}。",
1636
1750
  "notice.keptDirty": "Worktree 开关已关闭。副本 {name} 被保留,因为它有自己的提交或未提交改动;用完请在 Worktree 面板里删除。",
1637
- "notice.keptNotOwned": "Worktree 开关已关闭。副本 {name} 被保留,因为无法证明它是开关创建的,插件不会删除它。",
1638
1751
  "notice.failed": "Worktree 开关已关闭,但离开副本失败:{message}",
1639
1752
  "notice.openPanel": "打开面板",
1640
1753
  "notice.dismiss": "关闭提示",
@@ -1670,7 +1783,7 @@ const CSS = `
1670
1783
  width: 6px; height: 6px; border-radius: 999px;
1671
1784
  background: color-mix(in oklab, currentColor 40%, transparent);
1672
1785
  }
1673
- .dsh-wt-chip[data-intent="on"] .dsh-wt-dot { background: #2da44e; }
1786
+ .dsh-wt-chip[data-position="on"] .dsh-wt-dot { background: #2da44e; }
1674
1787
  .dsh-wt-chip[data-phase="failed"] .dsh-wt-dot { background: #e5534b; }
1675
1788
  .dsh-wt-chip[data-phase="creating"] .dsh-wt-dot { background: #bf8700; }
1676
1789
  .dsh-wt-label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; }