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/README.md +46 -20
- package/lib/client.js +319 -206
- package/lib/index.js +23 -4
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -77,196 +77,21 @@ function WorktreeBadge(props) {
|
|
|
77
77
|
})
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
-
// src/
|
|
80
|
+
// src/types.ts
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Whether a leave that kept a copy left a 保留副本 behind.
|
|
83
83
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @
|
|
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
|
|
227
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
...
|
|
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.
|
|
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
|
|
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.
|
|
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}
|
|
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": "
|
|
1707
|
+
"chip.hint.intentOff": "拨回后没能离开 {name};本会话仍在副本里。",
|
|
1708
|
+
"chip.hint.isolationOff": "本会话在副本里运行;这个项目的新会话从源检出开始。",
|
|
1595
1709
|
"chip.hint.keptDirty": "关闭开关时保留了副本 {name}:它有自己的提交或未提交改动。",
|
|
1596
|
-
"chip.hint.keptNotOwned": "
|
|
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-
|
|
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; }
|