dsh-worktree-switch 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -17
- package/lib/client.js +1269 -489
- package/lib/index.js +473 -103
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -7,6 +7,76 @@ let react = require("react");
|
|
|
7
7
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
10
|
+
// src/client/Badge.tsx
|
|
11
|
+
/**
|
|
12
|
+
* The persistent `⎇ branch` badge in the session header.
|
|
13
|
+
*
|
|
14
|
+
* The switch only exists on the blank-session hero, so once the first prompt is
|
|
15
|
+
* sent nothing on the page said whether this session runs in a copy. The badge
|
|
16
|
+
* fills that seat for non-blank sessions: it is quiet outside a copy, follows
|
|
17
|
+
* the live working directory (changing into or out of a copy), and opens the
|
|
18
|
+
* panel with the current copy highlighted.
|
|
19
|
+
* @module dsh-worktree-switch/client/Badge
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The hover text: exactly where this session runs, and how much is uncommitted.
|
|
23
|
+
* @param state - the current scope answer.
|
|
24
|
+
* @param t - bound translate.
|
|
25
|
+
* @returns tooltip text, empty when there is no copy.
|
|
26
|
+
*/
|
|
27
|
+
function badgeHint(state, t) {
|
|
28
|
+
if (state.worktree === null) return "";
|
|
29
|
+
return t("badge.hint", {
|
|
30
|
+
path: state.worktree.path,
|
|
31
|
+
root: state.repositoryRoot ?? "",
|
|
32
|
+
count: state.changedFiles
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Render the session header badge.
|
|
37
|
+
* @param props - slot runtime props, the badge face, and the locale seat.
|
|
38
|
+
* @returns the badge, or null while the host is checking or outside a copy.
|
|
39
|
+
*/
|
|
40
|
+
function WorktreeBadge(props) {
|
|
41
|
+
const { useSession, useProjection, useScope, query, open, t } = props;
|
|
42
|
+
const blank = useSession((session) => session.blank);
|
|
43
|
+
const workingDirectory = useProjection("workingDirectory");
|
|
44
|
+
const state = useScope((current) => current);
|
|
45
|
+
(0, react.useEffect)(() => {
|
|
46
|
+
if (blank) return;
|
|
47
|
+
query();
|
|
48
|
+
}, [
|
|
49
|
+
blank,
|
|
50
|
+
query,
|
|
51
|
+
workingDirectory
|
|
52
|
+
]);
|
|
53
|
+
if (blank || state.phase !== "in" || state.worktree === null) return null;
|
|
54
|
+
const label = state.worktree.branch === null ? t("badge.detached") : t("badge.label", { branch: state.worktree.branch });
|
|
55
|
+
const hint = badgeHint(state, t);
|
|
56
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
57
|
+
className: "dsh-wt-badge",
|
|
58
|
+
"data-phase": state.phase,
|
|
59
|
+
"data-branch": state.worktree.branch ?? "",
|
|
60
|
+
"data-hint": hint,
|
|
61
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
62
|
+
label: hint,
|
|
63
|
+
side: "top",
|
|
64
|
+
delayMs: 300,
|
|
65
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
66
|
+
className: "dsh-wt-anchor",
|
|
67
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
68
|
+
type: "button",
|
|
69
|
+
className: "dsh-wt-badge-button",
|
|
70
|
+
"aria-label": label,
|
|
71
|
+
onClick: () => {
|
|
72
|
+
open();
|
|
73
|
+
},
|
|
74
|
+
children: label
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
10
80
|
// src/client/Chip.tsx
|
|
11
81
|
/**
|
|
12
82
|
* The New Session switch.
|
|
@@ -45,13 +115,19 @@ function chipLabel(state, t) {
|
|
|
45
115
|
function chipHint(state, t) {
|
|
46
116
|
switch (state.phase) {
|
|
47
117
|
case "checking": return t("chip.hint.checking");
|
|
48
|
-
case "off":
|
|
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
|
+
}
|
|
49
124
|
case "creating": return t("chip.hint.creating");
|
|
50
125
|
case "reverting": return t("chip.hint.reverting");
|
|
51
126
|
case "ready": {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
55
131
|
})];
|
|
56
132
|
if (state.base?.kind === "fallback") parts.push(t("chip.hint.baseFallback"));
|
|
57
133
|
if (!state.intent) parts.push(t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }));
|
|
@@ -102,6 +178,7 @@ function WorktreeChip(props) {
|
|
|
102
178
|
className: "dsh-wt-chip",
|
|
103
179
|
"data-phase": state.phase,
|
|
104
180
|
"data-intent": state.intent ? "on" : "off",
|
|
181
|
+
"data-kept": state.kept === null ? "false" : state.kept.reason,
|
|
105
182
|
"data-hint": hint,
|
|
106
183
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
107
184
|
label: hint,
|
|
@@ -139,372 +216,58 @@ function WorktreeChip(props) {
|
|
|
139
216
|
}) : null]
|
|
140
217
|
});
|
|
141
218
|
}
|
|
142
|
-
// src/client/
|
|
219
|
+
// src/client/Notice.tsx
|
|
143
220
|
/**
|
|
144
|
-
* The
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* offers here: nothing merges, nothing is reclaimed automatically, and a copy
|
|
149
|
-
* with uncommitted changes is refused by the host, not hidden by a disabled
|
|
150
|
-
* button alone.
|
|
151
|
-
* @module dsh-worktree-switch/client/Panel
|
|
221
|
+
* The sentence a notice shows.
|
|
222
|
+
* @param notice - what the leave reported.
|
|
223
|
+
* @param t - bound translate.
|
|
224
|
+
* @returns the localized explanation.
|
|
152
225
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (milliseconds <= 0) return "—";
|
|
158
|
-
return new Date(milliseconds).toISOString().slice(0, 16).replace("T", " ");
|
|
159
|
-
}
|
|
160
|
-
/** The key that identifies one discard row. */
|
|
161
|
-
function rowKey(group, item) {
|
|
162
|
-
return `${group.path}\u0000${item.name}`;
|
|
163
|
-
}
|
|
164
|
-
/** One project's copies. */
|
|
165
|
-
function ProjectSection(props) {
|
|
166
|
-
const { group, state, armed, t, onArm, onDiscard } = props;
|
|
167
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
168
|
-
className: "dsh-wt-project",
|
|
169
|
-
"data-project": group.path,
|
|
170
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
171
|
-
className: "dsh-wt-project-head",
|
|
172
|
-
children: [
|
|
173
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: group.title }),
|
|
174
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
175
|
-
className: "dsh-wt-path",
|
|
176
|
-
children: group.path
|
|
177
|
-
}),
|
|
178
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
179
|
-
className: "dsh-wt-count",
|
|
180
|
-
children: t("panel.count", { count: group.items.length })
|
|
181
|
-
})
|
|
182
|
-
]
|
|
183
|
-
}), group.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
184
|
-
className: "dsh-wt-error",
|
|
185
|
-
role: "alert",
|
|
186
|
-
children: group.error
|
|
187
|
-
}) : !group.supported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
188
|
-
className: "dsh-wt-muted",
|
|
189
|
-
children: t("panel.notRepository")
|
|
190
|
-
}) : group.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
191
|
-
className: "dsh-wt-muted",
|
|
192
|
-
children: t("panel.empty")
|
|
193
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
194
|
-
className: "dsh-wt-rows",
|
|
195
|
-
children: group.items.map((item) => {
|
|
196
|
-
const key = rowKey(group, item);
|
|
197
|
-
const isArmed = armed === key;
|
|
198
|
-
const busy = state.busy === key;
|
|
199
|
-
const dirty = item.changedFiles > 0;
|
|
200
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
201
|
-
className: "dsh-wt-row",
|
|
202
|
-
"data-worktree": item.name,
|
|
203
|
-
"data-dirty": dirty ? "true" : "false",
|
|
204
|
-
children: [
|
|
205
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
206
|
-
className: "dsh-wt-name",
|
|
207
|
-
children: item.name
|
|
208
|
-
}),
|
|
209
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
210
|
-
className: "dsh-wt-branch",
|
|
211
|
-
children: [
|
|
212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
213
|
-
className: "dsh-wt-muted",
|
|
214
|
-
children: t("panel.branch")
|
|
215
|
-
}),
|
|
216
|
-
" ",
|
|
217
|
-
item.branch
|
|
218
|
-
]
|
|
219
|
-
}),
|
|
220
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
221
|
-
className: "dsh-wt-row-path",
|
|
222
|
-
children: item.path
|
|
223
|
-
}),
|
|
224
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
225
|
-
className: "dsh-wt-created",
|
|
226
|
-
children: [
|
|
227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
228
|
-
className: "dsh-wt-muted",
|
|
229
|
-
children: t("panel.created")
|
|
230
|
-
}),
|
|
231
|
-
" ",
|
|
232
|
-
formatTime(item.createdAt)
|
|
233
|
-
]
|
|
234
|
-
}),
|
|
235
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
236
|
-
className: "dsh-wt-changes",
|
|
237
|
-
"data-changes": item.changedFiles,
|
|
238
|
-
children: dirty ? t("panel.changesCount", { count: item.changedFiles }) : t("panel.clean")
|
|
239
|
-
}),
|
|
240
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
241
|
-
label: dirty ? t("panel.discardDirty") : t("panel.discardHint"),
|
|
242
|
-
side: "top",
|
|
243
|
-
delayMs: 200,
|
|
244
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
245
|
-
className: "dsh-wt-anchor",
|
|
246
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
247
|
-
type: "button",
|
|
248
|
-
className: "dsh-wt-discard",
|
|
249
|
-
"data-armed": isArmed ? "true" : "false",
|
|
250
|
-
disabled: dirty || busy || !group.supported,
|
|
251
|
-
onClick: () => {
|
|
252
|
-
if (isArmed) {
|
|
253
|
-
onDiscard(group, item);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
onArm(key);
|
|
257
|
-
},
|
|
258
|
-
children: busy ? "…" : isArmed ? t("panel.discardAgain") : t("panel.discard")
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
]
|
|
263
|
-
}, key);
|
|
264
|
-
})
|
|
265
|
-
})]
|
|
266
|
-
});
|
|
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 });
|
|
267
230
|
}
|
|
268
231
|
/**
|
|
269
|
-
* Render the
|
|
270
|
-
* @param props - runtime props, the
|
|
271
|
-
* @returns the
|
|
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.
|
|
272
235
|
*/
|
|
273
|
-
function
|
|
274
|
-
const {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const timer = (0, react.useRef)(null);
|
|
278
|
-
(0, react.useEffect)(() => {
|
|
279
|
-
refresh();
|
|
280
|
-
}, [refresh]);
|
|
281
|
-
(0, react.useEffect)(() => () => {
|
|
282
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
283
|
-
}, []);
|
|
284
|
-
const arm = (key) => {
|
|
285
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
286
|
-
setArmed(key);
|
|
287
|
-
timer.current = window.setTimeout(() => {
|
|
288
|
-
setArmed(null);
|
|
289
|
-
}, ARM_TIMEOUT_MS);
|
|
290
|
-
};
|
|
291
|
-
const onDiscard = (group, item) => {
|
|
292
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
293
|
-
setArmed(null);
|
|
294
|
-
discard(group.path, item.name);
|
|
295
|
-
};
|
|
296
|
-
const total = state.groups.reduce((sum, group) => sum + group.items.length, 0);
|
|
236
|
+
function WorktreeNotice(props) {
|
|
237
|
+
const { useNotice, dismiss, openPanel, t } = props;
|
|
238
|
+
const notice = useNotice((current) => current);
|
|
239
|
+
if (notice === null) return null;
|
|
297
240
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
298
|
-
className: "dsh-wt-
|
|
299
|
-
"data-
|
|
241
|
+
className: "dsh-wt-notice",
|
|
242
|
+
"data-kind": notice.kind,
|
|
243
|
+
"data-seq": notice.seq,
|
|
244
|
+
role: "status",
|
|
300
245
|
children: [
|
|
301
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
302
|
-
className: "dsh-wt-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
type: "button",
|
|
306
|
-
className: "dsh-wt-back",
|
|
307
|
-
onClick: () => {
|
|
308
|
-
close();
|
|
309
|
-
},
|
|
310
|
-
children: t("panel.back")
|
|
311
|
-
}),
|
|
312
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
313
|
-
className: "dsh-wt-titles",
|
|
314
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("panel.title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("panel.subtitle") })]
|
|
315
|
-
}),
|
|
316
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
317
|
-
className: "dsh-wt-count",
|
|
318
|
-
children: t("panel.count", { count: total })
|
|
319
|
-
}),
|
|
320
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
321
|
-
type: "button",
|
|
322
|
-
className: "dsh-wt-refresh",
|
|
323
|
-
onClick: () => {
|
|
324
|
-
refresh();
|
|
325
|
-
},
|
|
326
|
-
children: t("panel.refresh")
|
|
327
|
-
})
|
|
328
|
-
]
|
|
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)
|
|
329
250
|
}),
|
|
330
|
-
|
|
331
|
-
className: "dsh-wt-error",
|
|
332
|
-
role: "alert",
|
|
333
|
-
children: t("panel.error", { message: state.error })
|
|
334
|
-
}) : null,
|
|
335
|
-
state.loading && state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
336
|
-
className: "dsh-wt-muted",
|
|
337
|
-
role: "status",
|
|
338
|
-
children: t("panel.loading")
|
|
339
|
-
}) : state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
340
|
-
className: "dsh-wt-muted",
|
|
341
|
-
children: t("panel.workspaceEmpty")
|
|
342
|
-
}) : state.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectSection, {
|
|
343
|
-
group,
|
|
344
|
-
state,
|
|
345
|
-
armed,
|
|
346
|
-
t,
|
|
347
|
-
onArm: arm,
|
|
348
|
-
onDiscard
|
|
349
|
-
}, group.path))
|
|
350
|
-
]
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Render the sidebar entry that opens the panel.
|
|
355
|
-
* @param props - sidebar owner props, the open action, and the locale seat.
|
|
356
|
-
* @returns the icon button.
|
|
357
|
-
*/
|
|
358
|
-
function WorktreePanelIcon(props) {
|
|
359
|
-
const { size, active, open, t } = props;
|
|
360
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
361
|
-
label: t("sidebar.label"),
|
|
362
|
-
side: "right",
|
|
363
|
-
delayMs: 300,
|
|
364
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
365
|
-
className: "dsh-wt-anchor",
|
|
366
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
367
252
|
type: "button",
|
|
368
|
-
className: "dsh-wt-
|
|
369
|
-
"data-active": active ? "true" : "false",
|
|
370
|
-
"aria-label": t("sidebar.label"),
|
|
371
|
-
"aria-pressed": active,
|
|
253
|
+
className: "dsh-wt-notice-open",
|
|
372
254
|
onClick: () => {
|
|
373
|
-
|
|
255
|
+
openPanel();
|
|
374
256
|
},
|
|
375
|
-
children:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 2.5v7A2.5 2.5 0 0 0 6.5 12H11" }),
|
|
386
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
387
|
-
cx: "4",
|
|
388
|
-
cy: "2.5",
|
|
389
|
-
r: "1.5"
|
|
390
|
-
}),
|
|
391
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
392
|
-
cx: "4",
|
|
393
|
-
cy: "12.5",
|
|
394
|
-
r: "1.5"
|
|
395
|
-
}),
|
|
396
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
397
|
-
cx: "11",
|
|
398
|
-
cy: "10.5",
|
|
399
|
-
r: "1.5"
|
|
400
|
-
}),
|
|
401
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
402
|
-
d: "M8.5 6.5h2.5v2",
|
|
403
|
-
strokeDasharray: "1.6 2"
|
|
404
|
-
})
|
|
405
|
-
]
|
|
406
|
-
})
|
|
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: "×"
|
|
407
267
|
})
|
|
408
|
-
|
|
409
|
-
});
|
|
268
|
+
]
|
|
269
|
+
}, notice.seq);
|
|
410
270
|
}
|
|
411
|
-
// src/client/locales.ts
|
|
412
|
-
/** English dictionary; its keys are the namespace's key domain. */
|
|
413
|
-
const en = {
|
|
414
|
-
"chip.label": "Worktree copy for this session",
|
|
415
|
-
"chip.checking": "Worktree…",
|
|
416
|
-
"chip.off": "Worktree off",
|
|
417
|
-
"chip.creating": "Creating worktree…",
|
|
418
|
-
"chip.reverting": "Leaving worktree…",
|
|
419
|
-
"chip.ready": "Worktree: {name}",
|
|
420
|
-
"chip.failed": "Worktree failed",
|
|
421
|
-
"chip.retry": "Retry",
|
|
422
|
-
"chip.disabled": "Worktree unavailable",
|
|
423
|
-
"chip.hint.checking": "Checking whether this project can be isolated…",
|
|
424
|
-
"chip.hint.off": "New sessions in this project start in the project directory.",
|
|
425
|
-
"chip.hint.on": "This session runs in a copy of the project; the source checkout is untouched.",
|
|
426
|
-
"chip.hint.creating": "Creating a checkout inside the project…",
|
|
427
|
-
"chip.hint.reverting": "Moving this session back to the project directory…",
|
|
428
|
-
"chip.hint.ready": "Branch {branch} at {path}",
|
|
429
|
-
"chip.hint.baseFallback": "Base: local HEAD (the remote default branch could not be fetched).",
|
|
430
|
-
"chip.hint.failed": "The copy was not created, so this session is still in the project directory.",
|
|
431
|
-
"chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
|
|
432
|
-
"chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
|
|
433
|
-
"chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
|
|
434
|
-
"chip.blocked.checking": "Checking whether this session gets a worktree…",
|
|
435
|
-
"chip.blocked.creating": "Creating the worktree before the first prompt…",
|
|
436
|
-
"chip.blocked.reverting": "Leaving the worktree before the first prompt…",
|
|
437
|
-
"chip.blocked.failed": "The worktree could not be created. Retry, turn the switch off, or discard the failed copy first.",
|
|
438
|
-
"panel.title": "Worktrees",
|
|
439
|
-
"panel.subtitle": "Copies created by the Worktree switch: one checkout and one branch each. Discard removes both.",
|
|
440
|
-
"panel.refresh": "Refresh",
|
|
441
|
-
"panel.back": "Back",
|
|
442
|
-
"panel.loading": "Reading worktrees…",
|
|
443
|
-
"panel.empty": "No worktrees here yet.",
|
|
444
|
-
"panel.workspaceEmpty": "No projects yet. Open a session to give this panel something to read.",
|
|
445
|
-
"panel.error": "Could not read worktrees: {message}",
|
|
446
|
-
"panel.notRepository": "Not a Git repository.",
|
|
447
|
-
"panel.name": "Name",
|
|
448
|
-
"panel.branch": "Branch",
|
|
449
|
-
"panel.created": "Created",
|
|
450
|
-
"panel.changes": "Changes",
|
|
451
|
-
"panel.changesCount": "{count} uncommitted",
|
|
452
|
-
"panel.clean": "Clean",
|
|
453
|
-
"panel.discard": "Discard",
|
|
454
|
-
"panel.discardAgain": "Discard now",
|
|
455
|
-
"panel.discardHint": "Deletes the checkout and its branch.",
|
|
456
|
-
"panel.discardDirty": "Commit or discard its changes first.",
|
|
457
|
-
"panel.count": "{count} copies",
|
|
458
|
-
"sidebar.label": "Worktrees"
|
|
459
|
-
};
|
|
460
|
-
/** Chinese dictionary: every key of {@link en}. */
|
|
461
|
-
const zh = {
|
|
462
|
-
"chip.label": "本会话使用 Worktree 副本",
|
|
463
|
-
"chip.checking": "Worktree…",
|
|
464
|
-
"chip.off": "Worktree 已关",
|
|
465
|
-
"chip.creating": "正在创建 Worktree…",
|
|
466
|
-
"chip.reverting": "正在离开 Worktree…",
|
|
467
|
-
"chip.ready": "Worktree:{name}",
|
|
468
|
-
"chip.failed": "Worktree 创建失败",
|
|
469
|
-
"chip.retry": "重试",
|
|
470
|
-
"chip.disabled": "Worktree 不可用",
|
|
471
|
-
"chip.hint.checking": "正在检查这个项目能否隔离…",
|
|
472
|
-
"chip.hint.off": "这个项目的新会话直接在项目目录里开始。",
|
|
473
|
-
"chip.hint.on": "本会话在项目的一份副本里运行,源仓库不受影响。",
|
|
474
|
-
"chip.hint.creating": "正在项目里创建检出…",
|
|
475
|
-
"chip.hint.reverting": "正在把本会话切回项目目录…",
|
|
476
|
-
"chip.hint.ready": "分支 {branch},位于 {path}",
|
|
477
|
-
"chip.hint.baseFallback": "基点:本地 HEAD(远端未取到)。",
|
|
478
|
-
"chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
|
|
479
|
-
"chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
|
|
480
|
-
"chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
|
|
481
|
-
"chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
|
|
482
|
-
"chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
|
|
483
|
-
"chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
|
|
484
|
-
"chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
|
|
485
|
-
"chip.blocked.failed": "Worktree 创建失败。请重试、关闭开关,或先丢弃失败的副本。",
|
|
486
|
-
"panel.title": "Worktree",
|
|
487
|
-
"panel.subtitle": "Worktree 开关创建的副本:每份副本是一个检出加一个分支,丢弃会同时删掉两者。",
|
|
488
|
-
"panel.refresh": "刷新",
|
|
489
|
-
"panel.back": "返回",
|
|
490
|
-
"panel.loading": "正在读取 Worktree…",
|
|
491
|
-
"panel.empty": "这里还没有 Worktree。",
|
|
492
|
-
"panel.workspaceEmpty": "还没有项目。打开一个会话,这里才有可读的内容。",
|
|
493
|
-
"panel.error": "读取 Worktree 失败:{message}",
|
|
494
|
-
"panel.notRepository": "不是 Git 仓库。",
|
|
495
|
-
"panel.name": "名称",
|
|
496
|
-
"panel.branch": "分支",
|
|
497
|
-
"panel.created": "创建时间",
|
|
498
|
-
"panel.changes": "改动",
|
|
499
|
-
"panel.changesCount": "{count} 处未提交",
|
|
500
|
-
"panel.clean": "干净",
|
|
501
|
-
"panel.discard": "丢弃",
|
|
502
|
-
"panel.discardAgain": "确认丢弃",
|
|
503
|
-
"panel.discardHint": "删除检出目录和它的分支。",
|
|
504
|
-
"panel.discardDirty": "请先提交或丢弃其中的改动。",
|
|
505
|
-
"panel.count": "{count} 份副本",
|
|
506
|
-
"sidebar.label": "Worktree"
|
|
507
|
-
};
|
|
508
271
|
// src/client/intent.ts
|
|
509
272
|
/**
|
|
510
273
|
* The remembered per-project choice: "this project always isolates" and
|
|
@@ -585,7 +348,8 @@ const INITIAL = {
|
|
|
585
348
|
repositoryRoot: null,
|
|
586
349
|
sourceDirty: 0,
|
|
587
350
|
base: null,
|
|
588
|
-
error: null
|
|
351
|
+
error: null,
|
|
352
|
+
kept: null
|
|
589
353
|
};
|
|
590
354
|
/** A thrown value's message, without leaking `undefined`. */
|
|
591
355
|
function messageOf(error) {
|
|
@@ -594,18 +358,174 @@ function messageOf(error) {
|
|
|
594
358
|
return text === "" ? "unknown error" : text;
|
|
595
359
|
}
|
|
596
360
|
/**
|
|
361
|
+
* Owns the one-shot notice a leave raises.
|
|
362
|
+
*
|
|
363
|
+
* Kept apart from the switch controller because it is frame-scoped and lives
|
|
364
|
+
* outside every session: the notice survives the session header, and unloading
|
|
365
|
+
* the plugin must drop it without touching any session state.
|
|
366
|
+
*/
|
|
367
|
+
var NoticeController = class {
|
|
368
|
+
openPanel;
|
|
369
|
+
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
|
|
370
|
+
seq = 0;
|
|
371
|
+
/**
|
|
372
|
+
* @param openPanel - opens this plugin's panel.
|
|
373
|
+
*/
|
|
374
|
+
constructor(openPanel) {
|
|
375
|
+
this.openPanel = openPanel;
|
|
376
|
+
}
|
|
377
|
+
/** The stable face handed to the overlay component. */
|
|
378
|
+
face = {
|
|
379
|
+
hooks: { notice: this.store },
|
|
380
|
+
dismiss: () => {
|
|
381
|
+
this.store.set(null);
|
|
382
|
+
},
|
|
383
|
+
openPanel: () => {
|
|
384
|
+
this.store.set(null);
|
|
385
|
+
this.openPanel();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Show one notice, replacing whatever was showing.
|
|
390
|
+
* @param notice - what happened, without its sequence.
|
|
391
|
+
*/
|
|
392
|
+
raise(notice) {
|
|
393
|
+
this.seq += 1;
|
|
394
|
+
this.store.set({
|
|
395
|
+
seq: this.seq,
|
|
396
|
+
...notice
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
/** The first answer before the host has said anything. */
|
|
401
|
+
const EMPTY_SCOPE = {
|
|
402
|
+
phase: "checking",
|
|
403
|
+
worktree: null,
|
|
404
|
+
repositoryRoot: null,
|
|
405
|
+
changedFiles: 0
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Answers "does this session run in a copy?" per session, once per directory.
|
|
409
|
+
*
|
|
410
|
+
* Deliberately not polled: the badge subscribes to the `workingDirectory`
|
|
411
|
+
* projection and asks again only when that value changes, which is also what
|
|
412
|
+
* makes it appear right after the switch creates a copy.
|
|
413
|
+
*/
|
|
414
|
+
var BadgeController = class {
|
|
415
|
+
ctx;
|
|
416
|
+
openCopy;
|
|
417
|
+
bindings = /* @__PURE__ */ new Map();
|
|
418
|
+
lifetime = new AbortController();
|
|
419
|
+
/**
|
|
420
|
+
* @param ctx - browser Cordis context.
|
|
421
|
+
* @param openCopy - opens the panel with one copy path highlighted.
|
|
422
|
+
*/
|
|
423
|
+
constructor(ctx, openCopy) {
|
|
424
|
+
this.ctx = ctx;
|
|
425
|
+
this.openCopy = openCopy;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The binding for one session, created on first use.
|
|
429
|
+
* @param sessionId - the session whose header badge is rendering.
|
|
430
|
+
* @returns the stable binding.
|
|
431
|
+
*/
|
|
432
|
+
binding(sessionId) {
|
|
433
|
+
const key = String(sessionId);
|
|
434
|
+
const existing = this.bindings.get(key);
|
|
435
|
+
if (existing !== void 0) return existing.face;
|
|
436
|
+
const binding = new InternalScope(this.ctx, key, this.lifetime.signal, this.openCopy);
|
|
437
|
+
this.bindings.set(key, binding);
|
|
438
|
+
return binding.face;
|
|
439
|
+
}
|
|
440
|
+
/** Abort in-flight reads when the plugin unloads. */
|
|
441
|
+
dispose() {
|
|
442
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("worktree switch unloaded"));
|
|
443
|
+
this.bindings.clear();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
/** The concrete per-session scope reader behind one {@link BadgeBinding}. */
|
|
447
|
+
var InternalScope = class {
|
|
448
|
+
ctx;
|
|
449
|
+
sessionId;
|
|
450
|
+
signal;
|
|
451
|
+
openCopy;
|
|
452
|
+
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(EMPTY_SCOPE);
|
|
453
|
+
/** Last answer generation; a stale reply to an older directory must not win. */
|
|
454
|
+
generation = 0;
|
|
455
|
+
face;
|
|
456
|
+
/**
|
|
457
|
+
* @param ctx - browser Cordis context.
|
|
458
|
+
* @param sessionId - wire session identity.
|
|
459
|
+
* @param signal - plugin lifetime cancellation.
|
|
460
|
+
* @param openCopy - opens the panel with one copy path highlighted.
|
|
461
|
+
*/
|
|
462
|
+
constructor(ctx, sessionId, signal, openCopy) {
|
|
463
|
+
this.ctx = ctx;
|
|
464
|
+
this.sessionId = sessionId;
|
|
465
|
+
this.signal = signal;
|
|
466
|
+
this.openCopy = openCopy;
|
|
467
|
+
this.face = {
|
|
468
|
+
hooks: { scope: this.store },
|
|
469
|
+
query: () => {
|
|
470
|
+
this.query();
|
|
471
|
+
},
|
|
472
|
+
open: () => {
|
|
473
|
+
const state = this.store.getSnapshot();
|
|
474
|
+
if (state.worktree !== null) this.openCopy(state.worktree.path);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/** Ask the host where this session is; a superseding call wins. */
|
|
479
|
+
query() {
|
|
480
|
+
this.run();
|
|
481
|
+
}
|
|
482
|
+
async run() {
|
|
483
|
+
this.generation += 1;
|
|
484
|
+
const generation = this.generation;
|
|
485
|
+
try {
|
|
486
|
+
const value = await callHost(this.ctx, "scope", { sessionId: this.sessionId }, this.signal);
|
|
487
|
+
if (this.signal.aborted || generation !== this.generation) return;
|
|
488
|
+
this.store.set(value.inWorktree ? {
|
|
489
|
+
phase: "in",
|
|
490
|
+
worktree: value.worktree,
|
|
491
|
+
repositoryRoot: value.repositoryRoot,
|
|
492
|
+
changedFiles: value.changedFiles
|
|
493
|
+
} : {
|
|
494
|
+
phase: "out",
|
|
495
|
+
worktree: null,
|
|
496
|
+
repositoryRoot: null,
|
|
497
|
+
changedFiles: 0
|
|
498
|
+
});
|
|
499
|
+
} catch {
|
|
500
|
+
if (this.signal.aborted || generation !== this.generation) return;
|
|
501
|
+
this.store.set({
|
|
502
|
+
phase: "failed",
|
|
503
|
+
worktree: null,
|
|
504
|
+
repositoryRoot: null,
|
|
505
|
+
changedFiles: 0
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
/**
|
|
597
511
|
* One controller per plugin instance: it caches the state of every session the
|
|
598
512
|
* switch has rendered for, and owns their composer blocks.
|
|
599
513
|
*/
|
|
600
514
|
var ToggleController = class {
|
|
601
515
|
ctx;
|
|
516
|
+
notices;
|
|
517
|
+
onLeftover;
|
|
602
518
|
bindings = /* @__PURE__ */ new Map();
|
|
603
519
|
lifetime = new AbortController();
|
|
604
520
|
/**
|
|
605
521
|
* @param ctx - browser Cordis context.
|
|
522
|
+
* @param notices - frame-level notice the switch raises when a leave keeps a copy or fails.
|
|
523
|
+
* @param onLeftover - asks the panel to re-count leftovers after a leave kept one.
|
|
606
524
|
*/
|
|
607
|
-
constructor(ctx) {
|
|
525
|
+
constructor(ctx, notices, onLeftover) {
|
|
608
526
|
this.ctx = ctx;
|
|
527
|
+
this.notices = notices;
|
|
528
|
+
this.onLeftover = onLeftover;
|
|
609
529
|
}
|
|
610
530
|
/**
|
|
611
531
|
* The binding for one session, created on first use.
|
|
@@ -616,7 +536,7 @@ var ToggleController = class {
|
|
|
616
536
|
const key = String(sessionId);
|
|
617
537
|
const existing = this.bindings.get(key);
|
|
618
538
|
if (existing !== void 0) return existing;
|
|
619
|
-
const binding = new InternalBinding(this.ctx, key, this.lifetime.signal);
|
|
539
|
+
const binding = new InternalBinding(this.ctx, key, this.lifetime.signal, this.notices, this.onLeftover);
|
|
620
540
|
this.bindings.set(key, binding.face);
|
|
621
541
|
return binding.face;
|
|
622
542
|
}
|
|
@@ -632,6 +552,8 @@ var InternalBinding = class {
|
|
|
632
552
|
ctx;
|
|
633
553
|
sessionId;
|
|
634
554
|
signal;
|
|
555
|
+
notices;
|
|
556
|
+
onLeftover;
|
|
635
557
|
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL);
|
|
636
558
|
started = false;
|
|
637
559
|
/** The project whose remembered choice is in play; null until status answers. */
|
|
@@ -646,10 +568,12 @@ var InternalBinding = class {
|
|
|
646
568
|
* @param sessionId - wire session identity.
|
|
647
569
|
* @param signal - plugin lifetime cancellation.
|
|
648
570
|
*/
|
|
649
|
-
constructor(ctx, sessionId, signal) {
|
|
571
|
+
constructor(ctx, sessionId, signal, notices, onLeftover) {
|
|
650
572
|
this.ctx = ctx;
|
|
651
573
|
this.sessionId = sessionId;
|
|
652
574
|
this.signal = signal;
|
|
575
|
+
this.notices = notices;
|
|
576
|
+
this.onLeftover = onLeftover;
|
|
653
577
|
this.face = {
|
|
654
578
|
hooks: { toggle: this.store },
|
|
655
579
|
ensure: () => {
|
|
@@ -694,7 +618,8 @@ var InternalBinding = class {
|
|
|
694
618
|
repositoryRoot: status.repositoryRoot,
|
|
695
619
|
sourceDirty: status.sourceDirty,
|
|
696
620
|
base: null,
|
|
697
|
-
error: null
|
|
621
|
+
error: null,
|
|
622
|
+
kept: null
|
|
698
623
|
};
|
|
699
624
|
if (status.sessionWorktree !== null) {
|
|
700
625
|
this.store.set({
|
|
@@ -744,7 +669,8 @@ var InternalBinding = class {
|
|
|
744
669
|
repositoryRoot: prepared.repositoryRoot,
|
|
745
670
|
sourceDirty: prepared.sourceDirty,
|
|
746
671
|
base: prepared.state === "created" ? prepared.base : null,
|
|
747
|
-
error: null
|
|
672
|
+
error: null,
|
|
673
|
+
kept: null
|
|
748
674
|
});
|
|
749
675
|
} catch (error) {
|
|
750
676
|
if (this.signal.aborted) return;
|
|
@@ -782,31 +708,79 @@ var InternalBinding = class {
|
|
|
782
708
|
this.store.set({
|
|
783
709
|
...before,
|
|
784
710
|
phase: "reverting",
|
|
785
|
-
error: null
|
|
711
|
+
error: null,
|
|
712
|
+
kept: null
|
|
786
713
|
});
|
|
787
714
|
try {
|
|
788
|
-
await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
|
|
715
|
+
const value = await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
|
|
789
716
|
if (this.signal.aborted) return;
|
|
790
717
|
const now = this.store.getSnapshot();
|
|
718
|
+
const kept = value.kept !== null && value.keptReason !== null ? {
|
|
719
|
+
worktree: value.kept,
|
|
720
|
+
reason: value.keptReason,
|
|
721
|
+
...value.error === void 0 ? {} : { error: value.error }
|
|
722
|
+
} : null;
|
|
791
723
|
this.store.set({
|
|
792
724
|
...now,
|
|
793
725
|
phase: "off",
|
|
794
726
|
intent: false,
|
|
795
727
|
worktree: null,
|
|
796
728
|
base: null,
|
|
797
|
-
error: null
|
|
729
|
+
error: null,
|
|
730
|
+
kept
|
|
798
731
|
});
|
|
732
|
+
if (kept !== null) {
|
|
733
|
+
this.notices.raise({
|
|
734
|
+
kind: "kept",
|
|
735
|
+
worktree: kept.worktree,
|
|
736
|
+
reason: kept.reason,
|
|
737
|
+
message: kept.error ?? null
|
|
738
|
+
});
|
|
739
|
+
this.onLeftover();
|
|
740
|
+
}
|
|
799
741
|
} catch (error) {
|
|
800
742
|
if (this.signal.aborted) return;
|
|
801
743
|
const now = this.store.getSnapshot();
|
|
744
|
+
const message = messageOf(error);
|
|
802
745
|
this.store.set({
|
|
803
746
|
...now,
|
|
804
747
|
phase: now.worktree === null ? "off" : "ready",
|
|
805
748
|
intent: false,
|
|
806
|
-
error:
|
|
749
|
+
error: message,
|
|
750
|
+
kept: null
|
|
751
|
+
});
|
|
752
|
+
this.notices.raise({
|
|
753
|
+
kind: "failed",
|
|
754
|
+
worktree: now.worktree,
|
|
755
|
+
reason: null,
|
|
756
|
+
message
|
|
807
757
|
});
|
|
758
|
+
this.onLeftover();
|
|
759
|
+
this.recheck();
|
|
808
760
|
}
|
|
809
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Re-read the session's projected state after a failure.
|
|
764
|
+
*
|
|
765
|
+
* The failing verb may still have moved the session before the delete it
|
|
766
|
+
* attempted failed, so the cached "ready in <copy>" face can be stale. This
|
|
767
|
+
* asks once which directory the host now records and shows that; it never
|
|
768
|
+
* creates, releases, or retries anything.
|
|
769
|
+
*/
|
|
770
|
+
async recheck() {
|
|
771
|
+
try {
|
|
772
|
+
const status = await callHost(this.ctx, "status", { sessionId: this.sessionId }, this.signal);
|
|
773
|
+
if (this.signal.aborted || !status.supported) return;
|
|
774
|
+
const now = this.store.getSnapshot();
|
|
775
|
+
this.store.set({
|
|
776
|
+
...now,
|
|
777
|
+
phase: status.sessionWorktree === null ? "off" : "ready",
|
|
778
|
+
worktree: status.sessionWorktree,
|
|
779
|
+
repositoryRoot: status.repositoryRoot,
|
|
780
|
+
sourceDirty: status.sourceDirty
|
|
781
|
+
});
|
|
782
|
+
} catch {}
|
|
783
|
+
}
|
|
810
784
|
/** Persist and apply one switch move. */
|
|
811
785
|
setIntent(next) {
|
|
812
786
|
const state = this.store.getSnapshot();
|
|
@@ -817,7 +791,8 @@ var InternalBinding = class {
|
|
|
817
791
|
this.store.set({
|
|
818
792
|
...state,
|
|
819
793
|
intent: false,
|
|
820
|
-
error: null
|
|
794
|
+
error: null,
|
|
795
|
+
kept: null
|
|
821
796
|
});
|
|
822
797
|
this.release();
|
|
823
798
|
return;
|
|
@@ -826,7 +801,8 @@ var InternalBinding = class {
|
|
|
826
801
|
this.store.set({
|
|
827
802
|
...state,
|
|
828
803
|
intent: true,
|
|
829
|
-
error: null
|
|
804
|
+
error: null,
|
|
805
|
+
kept: null
|
|
830
806
|
});
|
|
831
807
|
if (state.phase === "off" || state.phase === "failed") this.create();
|
|
832
808
|
}
|
|
@@ -839,7 +815,8 @@ var InternalBinding = class {
|
|
|
839
815
|
...state,
|
|
840
816
|
intent: true,
|
|
841
817
|
phase: "off",
|
|
842
|
-
error: null
|
|
818
|
+
error: null,
|
|
819
|
+
kept: null
|
|
843
820
|
});
|
|
844
821
|
this.create();
|
|
845
822
|
}
|
|
@@ -851,6 +828,27 @@ var InternalBinding = class {
|
|
|
851
828
|
}
|
|
852
829
|
};
|
|
853
830
|
/**
|
|
831
|
+
* The row key that identifies one copy's row in the panel.
|
|
832
|
+
*
|
|
833
|
+
* The panel's armed/busy state and this controller's in-flight key must agree
|
|
834
|
+
* character for character, so both sides build the key here.
|
|
835
|
+
* @param project - workspace directory the row belongs to.
|
|
836
|
+
* @param name - copy name.
|
|
837
|
+
* @returns the stable row key.
|
|
838
|
+
*/
|
|
839
|
+
function copyRowKey(project, name) {
|
|
840
|
+
return `${project}\u0000${name}`;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* The row key that identifies one leftover branch's row in the panel.
|
|
844
|
+
* @param project - workspace directory the row belongs to.
|
|
845
|
+
* @param branch - branch name.
|
|
846
|
+
* @returns the stable row key.
|
|
847
|
+
*/
|
|
848
|
+
function branchRowKey(project, branch) {
|
|
849
|
+
return `${project}\u0000branch:${branch}`;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
854
852
|
* Read every workspace's pool and discard copies from the panel.
|
|
855
853
|
*/
|
|
856
854
|
var PanelController = class {
|
|
@@ -860,115 +858,787 @@ var PanelController = class {
|
|
|
860
858
|
loading: true,
|
|
861
859
|
groups: [],
|
|
862
860
|
error: null,
|
|
863
|
-
busy: null
|
|
861
|
+
busy: null,
|
|
862
|
+
cleanup: 0,
|
|
863
|
+
current: []
|
|
864
|
+
});
|
|
865
|
+
lifetime = new AbortController();
|
|
866
|
+
/** The read in flight, so overlapping refreshes coalesce into one more read. */
|
|
867
|
+
running = null;
|
|
868
|
+
/** A refresh arrived while one was in flight. */
|
|
869
|
+
again = false;
|
|
870
|
+
/** A copy path the badge click asked to highlight; wins over the derived ones. */
|
|
871
|
+
highlighted = null;
|
|
872
|
+
/** The failure a completed read must publish; a fresh read clears it. */
|
|
873
|
+
failure = null;
|
|
874
|
+
/**
|
|
875
|
+
* @param ctx - browser Cordis context.
|
|
876
|
+
* @param leave - called by the panel's Back action.
|
|
877
|
+
*/
|
|
878
|
+
constructor(ctx, leave) {
|
|
879
|
+
this.ctx = ctx;
|
|
880
|
+
this.leave = leave;
|
|
881
|
+
}
|
|
882
|
+
/** The stable face handed to the panel component. */
|
|
883
|
+
face = {
|
|
884
|
+
hooks: { worktrees: this.store },
|
|
885
|
+
refresh: () => {
|
|
886
|
+
this.refresh();
|
|
887
|
+
},
|
|
888
|
+
discard: (project, name) => {
|
|
889
|
+
this.discard(project, name);
|
|
890
|
+
},
|
|
891
|
+
discardBranch: (project, branch) => {
|
|
892
|
+
this.discardBranch(project, branch);
|
|
893
|
+
},
|
|
894
|
+
highlight: (path) => {
|
|
895
|
+
this.highlight(path);
|
|
896
|
+
},
|
|
897
|
+
close: () => {
|
|
898
|
+
this.highlighted = null;
|
|
899
|
+
this.leave();
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
/**
|
|
903
|
+
* Start the count the sidebar entry shows.
|
|
904
|
+
*
|
|
905
|
+
* Runs without the panel being open, so a leftover is visible from the
|
|
906
|
+
* sidebar, and follows the workspace list because a project added later has
|
|
907
|
+
* a pool this page has never read.
|
|
908
|
+
* @returns disposer for the workspace subscription.
|
|
909
|
+
*/
|
|
910
|
+
watch() {
|
|
911
|
+
this.refresh();
|
|
912
|
+
const workspaces = this.ctx.get("workspaces");
|
|
913
|
+
if (workspaces === void 0) return () => {};
|
|
914
|
+
return workspaces.list.subscribe(() => {
|
|
915
|
+
this.refresh();
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
/** Read every workspace's pool; projects that fail stay visible with their error. */
|
|
919
|
+
refresh() {
|
|
920
|
+
this.failure = null;
|
|
921
|
+
this.start();
|
|
922
|
+
}
|
|
923
|
+
/** Run one read, or queue exactly one more behind the read in flight. */
|
|
924
|
+
start() {
|
|
925
|
+
if (this.running !== null) {
|
|
926
|
+
this.again = true;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.running = this.run().finally(() => {
|
|
930
|
+
this.running = null;
|
|
931
|
+
if (this.again) {
|
|
932
|
+
this.again = false;
|
|
933
|
+
this.start();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/** Mark one copy current without asking the host: the badge already knows it. */
|
|
938
|
+
highlight(path) {
|
|
939
|
+
this.highlighted = path;
|
|
940
|
+
this.store.set({
|
|
941
|
+
...this.store.getSnapshot(),
|
|
942
|
+
current: [path]
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async run() {
|
|
946
|
+
const workspaces = this.ctx.get("workspaces");
|
|
947
|
+
if (workspaces === void 0) {
|
|
948
|
+
this.store.set({
|
|
949
|
+
loading: false,
|
|
950
|
+
groups: [],
|
|
951
|
+
error: "the workspace service is not mounted",
|
|
952
|
+
busy: null,
|
|
953
|
+
cleanup: 0,
|
|
954
|
+
current: []
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const items = workspaces.list.getSnapshot().items;
|
|
959
|
+
this.store.set({
|
|
960
|
+
...this.store.getSnapshot(),
|
|
961
|
+
loading: true
|
|
962
|
+
});
|
|
963
|
+
const groups = [];
|
|
964
|
+
for (const workspace of items) try {
|
|
965
|
+
const listed = await callHost(this.ctx, "list", { path: workspace.path }, this.lifetime.signal);
|
|
966
|
+
groups.push(listed.supported ? {
|
|
967
|
+
path: workspace.path,
|
|
968
|
+
title: workspace.title,
|
|
969
|
+
supported: true,
|
|
970
|
+
items: listed.items,
|
|
971
|
+
orphans: listed.orphans,
|
|
972
|
+
keptCopies: listed.keptCopies,
|
|
973
|
+
error: null
|
|
974
|
+
} : {
|
|
975
|
+
path: workspace.path,
|
|
976
|
+
title: workspace.title,
|
|
977
|
+
supported: false,
|
|
978
|
+
items: [],
|
|
979
|
+
orphans: [],
|
|
980
|
+
keptCopies: 0,
|
|
981
|
+
error: null
|
|
982
|
+
});
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (this.lifetime.signal.aborted) return;
|
|
985
|
+
groups.push({
|
|
986
|
+
path: workspace.path,
|
|
987
|
+
title: workspace.title,
|
|
988
|
+
supported: true,
|
|
989
|
+
items: [],
|
|
990
|
+
orphans: [],
|
|
991
|
+
keptCopies: 0,
|
|
992
|
+
error: messageOf(error)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (this.lifetime.signal.aborted) return;
|
|
996
|
+
this.store.set({
|
|
997
|
+
loading: false,
|
|
998
|
+
groups,
|
|
999
|
+
error: this.failure,
|
|
1000
|
+
busy: null,
|
|
1001
|
+
cleanup: groups.reduce((sum, group) => sum + group.orphans.length + group.keptCopies, 0),
|
|
1002
|
+
current: this.highlighted !== null ? [this.highlighted] : this.currentCopies(groups)
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* The copies a session is currently inside.
|
|
1007
|
+
*
|
|
1008
|
+
* Read from the Session Controller instead of the host: it is already on the
|
|
1009
|
+
* page, and a new host event would have to be threaded through the wire for
|
|
1010
|
+
* one highlight. The main-view retention count tells which session is the
|
|
1011
|
+
* one on screen, so archived rows never mark a copy.
|
|
1012
|
+
* @param groups - the groups just read.
|
|
1013
|
+
* @returns copy paths that session's working directory falls inside.
|
|
1014
|
+
*/
|
|
1015
|
+
currentCopies(groups) {
|
|
1016
|
+
const sessions = this.ctx.get("sessions");
|
|
1017
|
+
if (sessions === void 0) return [];
|
|
1018
|
+
const list = sessions.list.getSnapshot();
|
|
1019
|
+
const found = /* @__PURE__ */ new Set();
|
|
1020
|
+
for (const row of Object.values(list.byId)) {
|
|
1021
|
+
if ((row.retainedBy.mainView ?? 0) <= 0) continue;
|
|
1022
|
+
const binding = sessions.binding(row.id);
|
|
1023
|
+
if (binding === void 0) continue;
|
|
1024
|
+
const directory = binding.session.projections.faceOf("workingDirectory").getSnapshot();
|
|
1025
|
+
if (typeof directory !== "string" || directory === "") continue;
|
|
1026
|
+
for (const group of groups) for (const item of group.items) if (directory === item.path || directory.startsWith(`${item.path}/`)) found.add(item.path);
|
|
1027
|
+
}
|
|
1028
|
+
return [...found];
|
|
1029
|
+
}
|
|
1030
|
+
async discard(project, name) {
|
|
1031
|
+
const key = copyRowKey(project, name);
|
|
1032
|
+
this.store.set({
|
|
1033
|
+
...this.store.getSnapshot(),
|
|
1034
|
+
busy: key,
|
|
1035
|
+
error: null
|
|
1036
|
+
});
|
|
1037
|
+
try {
|
|
1038
|
+
await callHost(this.ctx, "discard", {
|
|
1039
|
+
path: project,
|
|
1040
|
+
name
|
|
1041
|
+
}, this.lifetime.signal);
|
|
1042
|
+
this.store.set({
|
|
1043
|
+
...this.store.getSnapshot(),
|
|
1044
|
+
busy: null
|
|
1045
|
+
});
|
|
1046
|
+
this.refresh();
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (this.lifetime.signal.aborted) return;
|
|
1049
|
+
this.store.set({
|
|
1050
|
+
...this.store.getSnapshot(),
|
|
1051
|
+
busy: null,
|
|
1052
|
+
error: messageOf(error)
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Delete one leftover branch.
|
|
1058
|
+
*
|
|
1059
|
+
* A host refusal (the branch is gone already, or a copy has it checked out)
|
|
1060
|
+
* still re-reads the pool: the row the user clicked may be stale, and the
|
|
1061
|
+
* refreshed list is the honest answer either way.
|
|
1062
|
+
* @param project - workspace directory the branch's pool belongs to.
|
|
1063
|
+
* @param branch - branch to delete.
|
|
1064
|
+
*/
|
|
1065
|
+
async discardBranch(project, branch) {
|
|
1066
|
+
const key = branchRowKey(project, branch);
|
|
1067
|
+
this.store.set({
|
|
1068
|
+
...this.store.getSnapshot(),
|
|
1069
|
+
busy: key,
|
|
1070
|
+
error: null
|
|
1071
|
+
});
|
|
1072
|
+
try {
|
|
1073
|
+
await callHost(this.ctx, "discardBranch", {
|
|
1074
|
+
path: project,
|
|
1075
|
+
branch
|
|
1076
|
+
}, this.lifetime.signal);
|
|
1077
|
+
this.store.set({
|
|
1078
|
+
...this.store.getSnapshot(),
|
|
1079
|
+
busy: null
|
|
1080
|
+
});
|
|
1081
|
+
this.refresh();
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (this.lifetime.signal.aborted) return;
|
|
1084
|
+
const message = messageOf(error);
|
|
1085
|
+
this.failure = message;
|
|
1086
|
+
this.store.set({
|
|
1087
|
+
...this.store.getSnapshot(),
|
|
1088
|
+
busy: null,
|
|
1089
|
+
error: message
|
|
1090
|
+
});
|
|
1091
|
+
this.start();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/** Abort in-flight reads when the plugin unloads. */
|
|
1095
|
+
dispose() {
|
|
1096
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("worktree switch unloaded"));
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
// src/client/Panel.tsx
|
|
1100
|
+
/**
|
|
1101
|
+
* The Worktrees panel and its sidebar entry.
|
|
1102
|
+
*
|
|
1103
|
+
* The page is a plain read of every workspace's pool plus explicit,
|
|
1104
|
+
* twice-confirmed destructive actions. Cleanup is the only write the plugin
|
|
1105
|
+
* offers here, and it is always explicit: a copy with uncommitted changes is
|
|
1106
|
+
* refused by the host, a leftover branch needs a second click, and a copy
|
|
1107
|
+
* whose directory was deleted by hand stays listed so its Git registration can
|
|
1108
|
+
* still be pruned. The current copy is marked, so the list answers "where am I"
|
|
1109
|
+
* as directly as possible.
|
|
1110
|
+
* @module dsh-worktree-switch/client/Panel
|
|
1111
|
+
*/
|
|
1112
|
+
/** How long an armed destructive button stays armed, in milliseconds. */
|
|
1113
|
+
const ARM_TIMEOUT_MS = 5e3;
|
|
1114
|
+
/** Render one copy's creation time without depending on the test machine's locale. */
|
|
1115
|
+
function formatTime(milliseconds) {
|
|
1116
|
+
if (milliseconds <= 0) return "—";
|
|
1117
|
+
return new Date(milliseconds).toISOString().slice(0, 16).replace("T", " ");
|
|
1118
|
+
}
|
|
1119
|
+
/** One project's leftover branches, rendered only when there are any. */
|
|
1120
|
+
function OrphanSection(props) {
|
|
1121
|
+
const { group, state, armed, t, onArm, onDelete } = props;
|
|
1122
|
+
if (group.orphans.length === 0) return null;
|
|
1123
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1124
|
+
className: "dsh-wt-orphans",
|
|
1125
|
+
"data-project": group.path,
|
|
1126
|
+
"data-orphans": group.orphans.length,
|
|
1127
|
+
children: [
|
|
1128
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1129
|
+
className: "dsh-wt-orphans-head",
|
|
1130
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("panel.orphans") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1131
|
+
className: "dsh-wt-count",
|
|
1132
|
+
"data-count": group.orphans.length,
|
|
1133
|
+
children: group.orphans.length
|
|
1134
|
+
})]
|
|
1135
|
+
}),
|
|
1136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1137
|
+
className: "dsh-wt-muted",
|
|
1138
|
+
children: t("panel.orphansHint")
|
|
1139
|
+
}),
|
|
1140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1141
|
+
className: "dsh-wt-orphan-rows",
|
|
1142
|
+
children: group.orphans.map((orphan) => {
|
|
1143
|
+
const key = branchRowKey(group.path, orphan.branch);
|
|
1144
|
+
const isArmed = armed === key;
|
|
1145
|
+
const busy = state.busy === key;
|
|
1146
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1147
|
+
className: "dsh-wt-orphan",
|
|
1148
|
+
"data-branch": orphan.branch,
|
|
1149
|
+
"data-commits": orphan.commits,
|
|
1150
|
+
children: [
|
|
1151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1152
|
+
className: "dsh-wt-orphan-branch",
|
|
1153
|
+
children: orphan.branch
|
|
1154
|
+
}),
|
|
1155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1156
|
+
className: "dsh-wt-orphan-commits",
|
|
1157
|
+
children: t("panel.orphanCommits", { count: orphan.commits })
|
|
1158
|
+
}),
|
|
1159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1160
|
+
className: "dsh-wt-orphan-subject",
|
|
1161
|
+
title: orphan.subject,
|
|
1162
|
+
children: orphan.subject
|
|
1163
|
+
}),
|
|
1164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1165
|
+
className: "dsh-wt-orphan-time",
|
|
1166
|
+
children: [
|
|
1167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1168
|
+
className: "dsh-wt-muted",
|
|
1169
|
+
children: t("panel.orphanLatest")
|
|
1170
|
+
}),
|
|
1171
|
+
" ",
|
|
1172
|
+
formatTime(orphan.committedAt)
|
|
1173
|
+
]
|
|
1174
|
+
}),
|
|
1175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1176
|
+
label: t("panel.orphanDeleteHint"),
|
|
1177
|
+
side: "top",
|
|
1178
|
+
delayMs: 200,
|
|
1179
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1180
|
+
className: "dsh-wt-anchor",
|
|
1181
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1182
|
+
type: "button",
|
|
1183
|
+
className: "dsh-wt-orphan-delete",
|
|
1184
|
+
"data-armed": isArmed ? "true" : "false",
|
|
1185
|
+
disabled: busy,
|
|
1186
|
+
onClick: () => {
|
|
1187
|
+
if (isArmed) {
|
|
1188
|
+
onDelete(group, orphan);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
onArm(key);
|
|
1192
|
+
},
|
|
1193
|
+
children: busy ? "…" : isArmed ? t("panel.orphanDeleteAgain") : t("panel.orphanDelete")
|
|
1194
|
+
})
|
|
1195
|
+
})
|
|
1196
|
+
})
|
|
1197
|
+
]
|
|
1198
|
+
}, key);
|
|
1199
|
+
})
|
|
1200
|
+
})
|
|
1201
|
+
]
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
/** One project's copies. */
|
|
1205
|
+
function ProjectSection(props) {
|
|
1206
|
+
const { group, state, armed, t, onArm, onDiscard, onDeleteBranch } = props;
|
|
1207
|
+
const current = state.current;
|
|
1208
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1209
|
+
className: "dsh-wt-project",
|
|
1210
|
+
"data-project": group.path,
|
|
1211
|
+
children: [
|
|
1212
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1213
|
+
className: "dsh-wt-project-head",
|
|
1214
|
+
children: [
|
|
1215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: group.title }),
|
|
1216
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1217
|
+
className: "dsh-wt-path",
|
|
1218
|
+
children: group.path
|
|
1219
|
+
}),
|
|
1220
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1221
|
+
className: "dsh-wt-count",
|
|
1222
|
+
children: t("panel.count", { count: group.items.length })
|
|
1223
|
+
})
|
|
1224
|
+
]
|
|
1225
|
+
}),
|
|
1226
|
+
group.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1227
|
+
className: "dsh-wt-error",
|
|
1228
|
+
role: "alert",
|
|
1229
|
+
children: group.error
|
|
1230
|
+
}) : !group.supported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1231
|
+
className: "dsh-wt-muted",
|
|
1232
|
+
children: t("panel.notRepository")
|
|
1233
|
+
}) : group.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1234
|
+
className: "dsh-wt-muted",
|
|
1235
|
+
children: t("panel.empty")
|
|
1236
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1237
|
+
className: "dsh-wt-rows",
|
|
1238
|
+
children: group.items.map((item) => {
|
|
1239
|
+
const key = copyRowKey(group.path, item.name);
|
|
1240
|
+
const isArmed = armed === key;
|
|
1241
|
+
const busy = state.busy === key;
|
|
1242
|
+
const dirty = item.changedFiles > 0;
|
|
1243
|
+
const isCurrent = current.includes(item.path);
|
|
1244
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1245
|
+
className: "dsh-wt-row",
|
|
1246
|
+
"data-worktree": item.name,
|
|
1247
|
+
"data-dirty": dirty ? "true" : "false",
|
|
1248
|
+
"data-prunable": item.prunable ? "true" : "false",
|
|
1249
|
+
"data-current": isCurrent ? "true" : "false",
|
|
1250
|
+
"aria-current": isCurrent ? "true" : void 0,
|
|
1251
|
+
children: [
|
|
1252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1253
|
+
className: "dsh-wt-name",
|
|
1254
|
+
children: item.name
|
|
1255
|
+
}),
|
|
1256
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1257
|
+
className: "dsh-wt-branch",
|
|
1258
|
+
children: [
|
|
1259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1260
|
+
className: "dsh-wt-muted",
|
|
1261
|
+
children: t("panel.branch")
|
|
1262
|
+
}),
|
|
1263
|
+
" ",
|
|
1264
|
+
item.branch ?? t("panel.detached")
|
|
1265
|
+
]
|
|
1266
|
+
}),
|
|
1267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1268
|
+
className: "dsh-wt-row-path",
|
|
1269
|
+
children: item.path
|
|
1270
|
+
}),
|
|
1271
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1272
|
+
className: "dsh-wt-created",
|
|
1273
|
+
children: [
|
|
1274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1275
|
+
className: "dsh-wt-muted",
|
|
1276
|
+
children: t("panel.created")
|
|
1277
|
+
}),
|
|
1278
|
+
" ",
|
|
1279
|
+
formatTime(item.createdAt)
|
|
1280
|
+
]
|
|
1281
|
+
}),
|
|
1282
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1283
|
+
className: "dsh-wt-changes",
|
|
1284
|
+
"data-changes": item.changedFiles,
|
|
1285
|
+
children: dirty ? t("panel.changesCount", { count: item.changedFiles }) : t("panel.clean")
|
|
1286
|
+
}),
|
|
1287
|
+
item.prunable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1288
|
+
className: "dsh-wt-prunable",
|
|
1289
|
+
"data-prunable": "true",
|
|
1290
|
+
children: t("panel.prunable")
|
|
1291
|
+
}) : null,
|
|
1292
|
+
isCurrent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1293
|
+
className: "dsh-wt-current",
|
|
1294
|
+
children: t("panel.current")
|
|
1295
|
+
}) : null,
|
|
1296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1297
|
+
label: dirty ? t("panel.discardDirty") : item.prunable ? t("panel.prunableHint") : t("panel.discardHint"),
|
|
1298
|
+
side: "top",
|
|
1299
|
+
delayMs: 200,
|
|
1300
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1301
|
+
className: "dsh-wt-anchor",
|
|
1302
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1303
|
+
type: "button",
|
|
1304
|
+
className: "dsh-wt-discard",
|
|
1305
|
+
"data-armed": isArmed ? "true" : "false",
|
|
1306
|
+
disabled: dirty || busy || !group.supported,
|
|
1307
|
+
onClick: () => {
|
|
1308
|
+
if (isArmed) {
|
|
1309
|
+
onDiscard(group, item);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
onArm(key);
|
|
1313
|
+
},
|
|
1314
|
+
children: busy ? "…" : isArmed ? t("panel.discardAgain") : t("panel.discard")
|
|
1315
|
+
})
|
|
1316
|
+
})
|
|
1317
|
+
})
|
|
1318
|
+
]
|
|
1319
|
+
}, key);
|
|
1320
|
+
})
|
|
1321
|
+
}),
|
|
1322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OrphanSection, {
|
|
1323
|
+
group,
|
|
1324
|
+
state,
|
|
1325
|
+
armed,
|
|
1326
|
+
t,
|
|
1327
|
+
onArm,
|
|
1328
|
+
onDelete: onDeleteBranch
|
|
1329
|
+
})
|
|
1330
|
+
]
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Render the Worktrees panel.
|
|
1335
|
+
* @param props - runtime props, the panel face, and the locale seat.
|
|
1336
|
+
* @returns the page.
|
|
1337
|
+
*/
|
|
1338
|
+
function WorktreePanel(props) {
|
|
1339
|
+
const { useWorktrees, refresh, discard, discardBranch, close, t } = props;
|
|
1340
|
+
const state = useWorktrees((current) => current);
|
|
1341
|
+
const [armed, setArmed] = (0, react.useState)(null);
|
|
1342
|
+
const timer = (0, react.useRef)(null);
|
|
1343
|
+
(0, react.useEffect)(() => {
|
|
1344
|
+
refresh();
|
|
1345
|
+
}, [refresh]);
|
|
1346
|
+
(0, react.useEffect)(() => () => {
|
|
1347
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
1348
|
+
}, []);
|
|
1349
|
+
const arm = (key) => {
|
|
1350
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
1351
|
+
setArmed(key);
|
|
1352
|
+
timer.current = window.setTimeout(() => {
|
|
1353
|
+
setArmed(null);
|
|
1354
|
+
}, ARM_TIMEOUT_MS);
|
|
1355
|
+
};
|
|
1356
|
+
const onDiscard = (group, item) => {
|
|
1357
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
1358
|
+
setArmed(null);
|
|
1359
|
+
discard(group.path, item.name);
|
|
1360
|
+
};
|
|
1361
|
+
const onDeleteBranch = (group, orphan) => {
|
|
1362
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
1363
|
+
setArmed(null);
|
|
1364
|
+
discardBranch(group.path, orphan.branch);
|
|
1365
|
+
};
|
|
1366
|
+
const total = state.groups.reduce((sum, group) => sum + group.items.length, 0);
|
|
1367
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1368
|
+
className: "dsh-wt-panel",
|
|
1369
|
+
"data-loading": state.loading ? "true" : "false",
|
|
1370
|
+
children: [
|
|
1371
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1372
|
+
className: "dsh-wt-panel-head",
|
|
1373
|
+
children: [
|
|
1374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1375
|
+
type: "button",
|
|
1376
|
+
className: "dsh-wt-back",
|
|
1377
|
+
onClick: () => {
|
|
1378
|
+
close();
|
|
1379
|
+
},
|
|
1380
|
+
children: t("panel.back")
|
|
1381
|
+
}),
|
|
1382
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1383
|
+
className: "dsh-wt-titles",
|
|
1384
|
+
children: [
|
|
1385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("panel.title") }),
|
|
1386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("panel.subtitle") }),
|
|
1387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1388
|
+
className: "dsh-wt-muted",
|
|
1389
|
+
"data-cleanup-hint": true,
|
|
1390
|
+
children: t("panel.cleanupHint")
|
|
1391
|
+
})
|
|
1392
|
+
]
|
|
1393
|
+
}),
|
|
1394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1395
|
+
className: "dsh-wt-count",
|
|
1396
|
+
children: t("panel.count", { count: total })
|
|
1397
|
+
}),
|
|
1398
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1399
|
+
type: "button",
|
|
1400
|
+
className: "dsh-wt-refresh",
|
|
1401
|
+
onClick: () => {
|
|
1402
|
+
refresh();
|
|
1403
|
+
},
|
|
1404
|
+
children: t("panel.refresh")
|
|
1405
|
+
})
|
|
1406
|
+
]
|
|
1407
|
+
}),
|
|
1408
|
+
state.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1409
|
+
className: "dsh-wt-error",
|
|
1410
|
+
role: "alert",
|
|
1411
|
+
children: t("panel.error", { message: state.error })
|
|
1412
|
+
}) : null,
|
|
1413
|
+
state.loading && state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1414
|
+
className: "dsh-wt-muted",
|
|
1415
|
+
role: "status",
|
|
1416
|
+
children: t("panel.loading")
|
|
1417
|
+
}) : state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1418
|
+
className: "dsh-wt-muted",
|
|
1419
|
+
children: t("panel.workspaceEmpty")
|
|
1420
|
+
}) : state.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectSection, {
|
|
1421
|
+
group,
|
|
1422
|
+
state,
|
|
1423
|
+
armed,
|
|
1424
|
+
t,
|
|
1425
|
+
onArm: arm,
|
|
1426
|
+
onDiscard,
|
|
1427
|
+
onDeleteBranch
|
|
1428
|
+
}, group.path))
|
|
1429
|
+
]
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Render the sidebar entry that opens the panel.
|
|
1434
|
+
*
|
|
1435
|
+
* The numeric count is the plugin's only ambient signal outside the header: it
|
|
1436
|
+
* appears when a pool still holds a leftover branch or a kept copy, and stays
|
|
1437
|
+
* silent otherwise.
|
|
1438
|
+
* @param props - sidebar owner props, the panel snapshot, the open action, and the locale seat.
|
|
1439
|
+
* @returns the icon button.
|
|
1440
|
+
*/
|
|
1441
|
+
function WorktreePanelIcon(props) {
|
|
1442
|
+
const { size, active, useWorktrees, open, t } = props;
|
|
1443
|
+
const cleanup = useWorktrees((state) => state.cleanup);
|
|
1444
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1445
|
+
label: t("sidebar.label"),
|
|
1446
|
+
side: "right",
|
|
1447
|
+
delayMs: 300,
|
|
1448
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1449
|
+
className: "dsh-wt-anchor",
|
|
1450
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1451
|
+
type: "button",
|
|
1452
|
+
className: "dsh-wt-panel-icon",
|
|
1453
|
+
"data-active": active ? "true" : "false",
|
|
1454
|
+
"data-cleanup": cleanup,
|
|
1455
|
+
"aria-label": t("sidebar.label"),
|
|
1456
|
+
"aria-pressed": active,
|
|
1457
|
+
onClick: () => {
|
|
1458
|
+
open();
|
|
1459
|
+
},
|
|
1460
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1461
|
+
width: Math.max(12, Math.round(size * .6)),
|
|
1462
|
+
height: Math.max(12, Math.round(size * .6)),
|
|
1463
|
+
viewBox: "0 0 16 16",
|
|
1464
|
+
"aria-hidden": "true",
|
|
1465
|
+
fill: "none",
|
|
1466
|
+
stroke: "currentColor",
|
|
1467
|
+
strokeWidth: "1.4",
|
|
1468
|
+
strokeLinecap: "round",
|
|
1469
|
+
children: [
|
|
1470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 2.5v7A2.5 2.5 0 0 0 6.5 12H11" }),
|
|
1471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1472
|
+
cx: "4",
|
|
1473
|
+
cy: "2.5",
|
|
1474
|
+
r: "1.5"
|
|
1475
|
+
}),
|
|
1476
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1477
|
+
cx: "4",
|
|
1478
|
+
cy: "12.5",
|
|
1479
|
+
r: "1.5"
|
|
1480
|
+
}),
|
|
1481
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1482
|
+
cx: "11",
|
|
1483
|
+
cy: "10.5",
|
|
1484
|
+
r: "1.5"
|
|
1485
|
+
}),
|
|
1486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1487
|
+
d: "M8.5 6.5h2.5v2",
|
|
1488
|
+
strokeDasharray: "1.6 2"
|
|
1489
|
+
})
|
|
1490
|
+
]
|
|
1491
|
+
}), cleanup > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1492
|
+
className: "dsh-wt-panel-icon-count",
|
|
1493
|
+
"aria-hidden": "true",
|
|
1494
|
+
children: cleanup > 99 ? "99+" : cleanup
|
|
1495
|
+
}) : null]
|
|
1496
|
+
})
|
|
1497
|
+
})
|
|
864
1498
|
});
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1499
|
+
}
|
|
1500
|
+
// src/client/locales.ts
|
|
1501
|
+
/** English dictionary; its keys are the namespace's key domain. */
|
|
1502
|
+
const en = {
|
|
1503
|
+
"chip.label": "Worktree copy for this session",
|
|
1504
|
+
"chip.checking": "Worktree…",
|
|
1505
|
+
"chip.off": "Worktree off",
|
|
1506
|
+
"chip.creating": "Creating worktree…",
|
|
1507
|
+
"chip.reverting": "Leaving worktree…",
|
|
1508
|
+
"chip.ready": "Worktree: {name}",
|
|
1509
|
+
"chip.failed": "Worktree failed",
|
|
1510
|
+
"chip.retry": "Retry",
|
|
1511
|
+
"chip.disabled": "Worktree unavailable",
|
|
1512
|
+
"chip.hint.checking": "Checking whether this project can be isolated…",
|
|
1513
|
+
"chip.hint.off": "New sessions in this project start in the project directory.",
|
|
1514
|
+
"chip.hint.on": "This session runs in a copy of the project; the source checkout is untouched.",
|
|
1515
|
+
"chip.hint.creating": "Creating a checkout inside the project…",
|
|
1516
|
+
"chip.hint.reverting": "Moving this session back to the project directory…",
|
|
1517
|
+
"chip.hint.ready": "Branch {branch} at {path}",
|
|
1518
|
+
"chip.hint.detached": "A detached copy at {path}; it has no branch.",
|
|
1519
|
+
"chip.hint.baseFallback": "Base: local HEAD (the remote default branch could not be fetched).",
|
|
1520
|
+
"chip.hint.failed": "The copy was not created, so this session is still in the project directory.",
|
|
1521
|
+
"chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
|
|
1522
|
+
"chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
|
|
1523
|
+
"chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
|
|
1524
|
+
"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.",
|
|
1526
|
+
"chip.blocked.checking": "Checking whether this session gets a worktree…",
|
|
1527
|
+
"chip.blocked.creating": "Creating the worktree before the first prompt…",
|
|
1528
|
+
"chip.blocked.reverting": "Leaving the worktree before the first prompt…",
|
|
1529
|
+
"chip.blocked.failed": "The worktree could not be created. Retry, turn the switch off, or discard the failed copy first.",
|
|
1530
|
+
"panel.title": "Worktrees",
|
|
1531
|
+
"panel.subtitle": "Copies created by the Worktree switch, plus leftover branches no copy checks out. Both are removed only by an explicit action here.",
|
|
1532
|
+
"panel.refresh": "Refresh",
|
|
1533
|
+
"panel.back": "Back",
|
|
1534
|
+
"panel.loading": "Reading worktrees…",
|
|
1535
|
+
"panel.empty": "No worktrees here yet.",
|
|
1536
|
+
"panel.workspaceEmpty": "No projects yet. Open a session to give this panel something to read.",
|
|
1537
|
+
"panel.error": "Could not read worktrees: {message}",
|
|
1538
|
+
"panel.notRepository": "Not a Git repository.",
|
|
1539
|
+
"panel.name": "Name",
|
|
1540
|
+
"panel.branch": "Branch",
|
|
1541
|
+
"panel.detached": "detached",
|
|
1542
|
+
"panel.created": "Created",
|
|
1543
|
+
"panel.changes": "Changes",
|
|
1544
|
+
"panel.changesCount": "{count} uncommitted",
|
|
1545
|
+
"panel.clean": "Clean",
|
|
1546
|
+
"panel.discard": "Discard",
|
|
1547
|
+
"panel.discardAgain": "Discard now",
|
|
1548
|
+
"panel.discardHint": "Deletes the checkout and its branch.",
|
|
1549
|
+
"panel.discardDirty": "Commit or discard its changes first.",
|
|
1550
|
+
"panel.cleanupHint": "Cleanup count: leftover branches plus copies this host kept when a switch was turned off. A host restart forgets the latter, so the count then covers leftovers only.",
|
|
1551
|
+
"panel.prunable": "Directory missing",
|
|
1552
|
+
"panel.prunableHint": "The directory is gone; discarding removes the leftover Git registration and its branch.",
|
|
1553
|
+
"panel.current": "current copy",
|
|
1554
|
+
"panel.orphans": "Leftover branches",
|
|
1555
|
+
"panel.orphansHint": "Branches from a finished isolation that no copy checks out. Deleting one removes only the branch.",
|
|
1556
|
+
"panel.orphanCommits": "{count} commit(s) the source checkout does not have",
|
|
1557
|
+
"panel.orphanLatest": "Latest",
|
|
1558
|
+
"panel.orphanDelete": "Delete branch",
|
|
1559
|
+
"panel.orphanDeleteAgain": "Delete branch now",
|
|
1560
|
+
"panel.orphanDeleteHint": "Deletes this branch. No copy checks it out.",
|
|
1561
|
+
"panel.count": "{count} copies",
|
|
1562
|
+
"badge.label": "⎇ {branch}",
|
|
1563
|
+
"badge.detached": "⎇ detached copy",
|
|
1564
|
+
"badge.hint": "This session runs in a copy of the project. Directory: {path}. Repository: {root}. Uncommitted files: {count}.",
|
|
1565
|
+
"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
|
+
"notice.failed": "The Worktree switch is off, but leaving the copy failed: {message}",
|
|
1568
|
+
"notice.openPanel": "Open panel",
|
|
1569
|
+
"notice.dismiss": "Dismiss",
|
|
1570
|
+
"sidebar.label": "Worktrees"
|
|
1571
|
+
};
|
|
1572
|
+
/** Chinese dictionary: every key of {@link en}. */
|
|
1573
|
+
const zh = {
|
|
1574
|
+
"chip.label": "本会话使用 Worktree 副本",
|
|
1575
|
+
"chip.checking": "Worktree…",
|
|
1576
|
+
"chip.off": "Worktree 已关",
|
|
1577
|
+
"chip.creating": "正在创建 Worktree…",
|
|
1578
|
+
"chip.reverting": "正在离开 Worktree…",
|
|
1579
|
+
"chip.ready": "Worktree:{name}",
|
|
1580
|
+
"chip.failed": "Worktree 创建失败",
|
|
1581
|
+
"chip.retry": "重试",
|
|
1582
|
+
"chip.disabled": "Worktree 不可用",
|
|
1583
|
+
"chip.hint.checking": "正在检查这个项目能否隔离…",
|
|
1584
|
+
"chip.hint.off": "这个项目的新会话直接在项目目录里开始。",
|
|
1585
|
+
"chip.hint.on": "本会话在项目的一份副本里运行,源仓库不受影响。",
|
|
1586
|
+
"chip.hint.creating": "正在项目里创建检出…",
|
|
1587
|
+
"chip.hint.reverting": "正在把本会话切回项目目录…",
|
|
1588
|
+
"chip.hint.ready": "分支 {branch},位于 {path}",
|
|
1589
|
+
"chip.hint.detached": "{path} 处是一份分离 HEAD 的副本,没有分支。",
|
|
1590
|
+
"chip.hint.baseFallback": "基点:本地 HEAD(远端未取到)。",
|
|
1591
|
+
"chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
|
|
1592
|
+
"chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
|
|
1593
|
+
"chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
|
|
1594
|
+
"chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
|
|
1595
|
+
"chip.hint.keptDirty": "关闭开关时保留了副本 {name}:它有自己的提交或未提交改动。",
|
|
1596
|
+
"chip.hint.keptNotOwned": "关闭开关时保留了副本 {name}:无法证明这份副本是开关创建的,所以不会删除它。",
|
|
1597
|
+
"chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
|
|
1598
|
+
"chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
|
|
1599
|
+
"chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
|
|
1600
|
+
"chip.blocked.failed": "Worktree 创建失败。请重试、关闭开关,或先丢弃失败的副本。",
|
|
1601
|
+
"panel.title": "Worktree",
|
|
1602
|
+
"panel.subtitle": "Worktree 开关创建的副本,以及没有副本检出的遗留分支。两者都只在这里显式操作时才会被删除。",
|
|
1603
|
+
"panel.refresh": "刷新",
|
|
1604
|
+
"panel.back": "返回",
|
|
1605
|
+
"panel.loading": "正在读取 Worktree…",
|
|
1606
|
+
"panel.empty": "这里还没有 Worktree。",
|
|
1607
|
+
"panel.workspaceEmpty": "还没有项目。打开一个会话,这里才有可读的内容。",
|
|
1608
|
+
"panel.error": "读取 Worktree 失败:{message}",
|
|
1609
|
+
"panel.notRepository": "不是 Git 仓库。",
|
|
1610
|
+
"panel.name": "名称",
|
|
1611
|
+
"panel.branch": "分支",
|
|
1612
|
+
"panel.detached": "分离 HEAD",
|
|
1613
|
+
"panel.created": "创建时间",
|
|
1614
|
+
"panel.changes": "改动",
|
|
1615
|
+
"panel.changesCount": "{count} 处未提交",
|
|
1616
|
+
"panel.clean": "干净",
|
|
1617
|
+
"panel.discard": "丢弃",
|
|
1618
|
+
"panel.discardAgain": "确认丢弃",
|
|
1619
|
+
"panel.discardHint": "删除检出目录和它的分支。",
|
|
1620
|
+
"panel.discardDirty": "请先提交或丢弃其中的改动。",
|
|
1621
|
+
"panel.cleanupHint": "清理计数 = 遗留分支 + 本次宿主运行时开关保留的副本;宿主重启后不可知,只剩遗留分支可数。",
|
|
1622
|
+
"panel.prunable": "目录已丢失",
|
|
1623
|
+
"panel.prunableHint": "目录已经不在了;丢弃会清掉残留的 Git 注册和它的分支。",
|
|
1624
|
+
"panel.current": "当前副本",
|
|
1625
|
+
"panel.orphans": "遗留分支",
|
|
1626
|
+
"panel.orphansHint": "一次隔离结束后没有删掉、也没有副本检出的分支。删除只移除这个分支。",
|
|
1627
|
+
"panel.orphanCommits": "源检出没有的 {count} 个提交",
|
|
1628
|
+
"panel.orphanLatest": "最近提交",
|
|
1629
|
+
"panel.orphanDelete": "删除分支",
|
|
1630
|
+
"panel.orphanDeleteAgain": "确认删除分支",
|
|
1631
|
+
"panel.orphanDeleteHint": "删除这个分支。没有副本检出它。",
|
|
1632
|
+
"panel.count": "{count} 份副本",
|
|
1633
|
+
"badge.label": "⎇ {branch}",
|
|
1634
|
+
"badge.detached": "⎇ 分离副本",
|
|
1635
|
+
"badge.hint": "本会话在项目的一份副本里运行。目录:{path}。仓库:{root}。未提交文件:{count}。",
|
|
1636
|
+
"notice.keptDirty": "Worktree 开关已关闭。副本 {name} 被保留,因为它有自己的提交或未提交改动;用完请在 Worktree 面板里删除。",
|
|
1637
|
+
"notice.keptNotOwned": "Worktree 开关已关闭。副本 {name} 被保留,因为无法证明它是开关创建的,插件不会删除它。",
|
|
1638
|
+
"notice.failed": "Worktree 开关已关闭,但离开副本失败:{message}",
|
|
1639
|
+
"notice.openPanel": "打开面板",
|
|
1640
|
+
"notice.dismiss": "关闭提示",
|
|
1641
|
+
"sidebar.label": "Worktree"
|
|
972
1642
|
};
|
|
973
1643
|
// src/client/styles.ts
|
|
974
1644
|
/**
|
|
@@ -1036,12 +1706,14 @@ const CSS = `
|
|
|
1036
1706
|
.dsh-wt-path, .dsh-wt-row-path { font-size: 11px; opacity: .64; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1037
1707
|
.dsh-wt-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1038
1708
|
.dsh-wt-row {
|
|
1039
|
-
display:
|
|
1040
|
-
grid-template-columns: minmax(120px, 1.2fr) minmax(120px, 1fr) minmax(160px, 1.6fr) auto auto auto;
|
|
1709
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
|
|
1041
1710
|
padding: 8px 12px; border-radius: 8px;
|
|
1042
1711
|
border: 1px solid color-mix(in oklab, currentColor 12%, transparent);
|
|
1043
1712
|
}
|
|
1044
|
-
.dsh-wt-name { font-weight: 600; }
|
|
1713
|
+
.dsh-wt-name { font-weight: 600; min-width: 120px; }
|
|
1714
|
+
.dsh-wt-branch { min-width: 120px; }
|
|
1715
|
+
.dsh-wt-row-path { flex: 1 1 180px; min-width: 140px; }
|
|
1716
|
+
.dsh-wt-row .dsh-wt-anchor { margin-left: auto; }
|
|
1045
1717
|
.dsh-wt-branch, .dsh-wt-created, .dsh-wt-changes { font-size: 12px; }
|
|
1046
1718
|
.dsh-wt-changes[data-changes]:not([data-changes="0"]) { color: #bf8700; }
|
|
1047
1719
|
.dsh-wt-muted { opacity: .64; font-size: 12px; }
|
|
@@ -1053,6 +1725,81 @@ const CSS = `
|
|
|
1053
1725
|
}
|
|
1054
1726
|
.dsh-wt-panel-icon:hover { opacity: 1; background: color-mix(in oklab, currentColor 8%, transparent); }
|
|
1055
1727
|
.dsh-wt-panel-icon[data-active="true"] { opacity: 1; background: color-mix(in oklab, currentColor 12%, transparent); }
|
|
1728
|
+
.dsh-wt-panel-icon { position: relative; }
|
|
1729
|
+
.dsh-wt-panel-icon-count {
|
|
1730
|
+
position: absolute; top: -2px; right: -2px; min-width: 14px; height: 14px; padding: 0 3px;
|
|
1731
|
+
border-radius: 999px; background: #bf8700; color: #fff; font-size: 9px; line-height: 14px;
|
|
1732
|
+
font-weight: 600; text-align: center; pointer-events: none;
|
|
1733
|
+
}
|
|
1734
|
+
.dsh-wt-badge { display: inline-flex; align-items: center; }
|
|
1735
|
+
.dsh-wt-badge-button {
|
|
1736
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1737
|
+
padding: 1px 8px; border-radius: 999px;
|
|
1738
|
+
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
|
|
1739
|
+
background: transparent; color: inherit; font: inherit; font-size: 12px;
|
|
1740
|
+
line-height: 18px; cursor: pointer; white-space: nowrap;
|
|
1741
|
+
max-width: 220px; overflow: hidden; text-overflow: ellipsis;
|
|
1742
|
+
}
|
|
1743
|
+
.dsh-wt-badge-button:hover { border-color: color-mix(in oklab, currentColor 42%, transparent); }
|
|
1744
|
+
.dsh-wt-badge-button:focus-visible { outline: 2px solid color-mix(in oklab, currentColor 45%, transparent); outline-offset: 1px; }
|
|
1745
|
+
.dsh-wt-notice {
|
|
1746
|
+
position: absolute; left: 50%; bottom: 28px; transform: translateX(-50%);
|
|
1747
|
+
display: flex; align-items: center; gap: 12px; max-width: min(720px, calc(100vw - 40px));
|
|
1748
|
+
padding: 10px 14px; border-radius: 10px; pointer-events: auto;
|
|
1749
|
+
border: 1px solid color-mix(in oklab, #bf8700 55%, transparent);
|
|
1750
|
+
background: color-mix(in oklab, #1f2328 88%, transparent); color: #f0f6fc;
|
|
1751
|
+
font: inherit; font-size: 12px; line-height: 18px;
|
|
1752
|
+
box-shadow: 0 6px 20px color-mix(in oklab, #000 32%, transparent);
|
|
1753
|
+
}
|
|
1754
|
+
.dsh-wt-notice[data-kind="failed"] { border-color: color-mix(in oklab, #e5534b 60%, transparent); }
|
|
1755
|
+
.dsh-wt-notice-text { flex: 1; min-width: 0; }
|
|
1756
|
+
.dsh-wt-notice-open {
|
|
1757
|
+
border: 1px solid color-mix(in oklab, currentColor 40%, transparent);
|
|
1758
|
+
background: transparent; color: inherit; font: inherit; font-size: 12px;
|
|
1759
|
+
border-radius: 6px; padding: 3px 10px; cursor: pointer; white-space: nowrap;
|
|
1760
|
+
}
|
|
1761
|
+
.dsh-wt-notice-open:hover { border-color: currentColor; }
|
|
1762
|
+
.dsh-wt-notice-dismiss {
|
|
1763
|
+
border: none; background: transparent; color: inherit; font: inherit; font-size: 15px;
|
|
1764
|
+
line-height: 1; cursor: pointer; padding: 2px 4px; opacity: .72;
|
|
1765
|
+
}
|
|
1766
|
+
.dsh-wt-notice-dismiss:hover { opacity: 1; }
|
|
1767
|
+
.dsh-wt-row[data-current="true"] {
|
|
1768
|
+
border-color: color-mix(in oklab, #2da44e 55%, transparent);
|
|
1769
|
+
background: color-mix(in oklab, #2da44e 8%, transparent);
|
|
1770
|
+
}
|
|
1771
|
+
.dsh-wt-current {
|
|
1772
|
+
font-size: 11px; font-weight: 600; color: #2da44e;
|
|
1773
|
+
border: 1px solid color-mix(in oklab, #2da44e 45%, transparent);
|
|
1774
|
+
border-radius: 999px; padding: 0 8px; white-space: nowrap;
|
|
1775
|
+
}
|
|
1776
|
+
.dsh-wt-prunable {
|
|
1777
|
+
font-size: 11px; color: #bf8700; white-space: nowrap;
|
|
1778
|
+
}
|
|
1779
|
+
.dsh-wt-orphans { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
1780
|
+
.dsh-wt-orphans-head { display: flex; align-items: baseline; gap: 8px; }
|
|
1781
|
+
.dsh-wt-orphans-head h4 { margin: 0; font-size: 12px; font-weight: 600; opacity: .86; }
|
|
1782
|
+
.dsh-wt-orphan-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1783
|
+
.dsh-wt-orphan {
|
|
1784
|
+
display: grid; align-items: center; gap: 12px;
|
|
1785
|
+
grid-template-columns: minmax(120px, 1fr) minmax(150px, 1fr) minmax(160px, 1.6fr) auto auto;
|
|
1786
|
+
padding: 6px 12px; border-radius: 8px;
|
|
1787
|
+
border: 1px dashed color-mix(in oklab, currentColor 18%, transparent);
|
|
1788
|
+
}
|
|
1789
|
+
.dsh-wt-orphan-branch { font-size: 12px; font-weight: 600; }
|
|
1790
|
+
.dsh-wt-orphan-commits, .dsh-wt-orphan-time { font-size: 12px; opacity: .78; }
|
|
1791
|
+
.dsh-wt-orphan-subject { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1792
|
+
.dsh-wt-orphan-delete {
|
|
1793
|
+
border: 1px solid color-mix(in oklab, currentColor 24%, transparent);
|
|
1794
|
+
background: transparent; color: inherit; font: inherit; font-size: 12px;
|
|
1795
|
+
border-radius: 6px; padding: 4px 10px; cursor: pointer; white-space: nowrap;
|
|
1796
|
+
}
|
|
1797
|
+
.dsh-wt-orphan-delete:hover:not(:disabled) { border-color: color-mix(in oklab, currentColor 44%, transparent); }
|
|
1798
|
+
.dsh-wt-orphan-delete:disabled { opacity: .6; cursor: default; }
|
|
1799
|
+
.dsh-wt-orphan-delete[data-armed="true"] {
|
|
1800
|
+
border-color: color-mix(in oklab, #e5534b 60%, transparent);
|
|
1801
|
+
color: #e5534b;
|
|
1802
|
+
}
|
|
1056
1803
|
`;
|
|
1057
1804
|
/**
|
|
1058
1805
|
* Install the stylesheet if this page has not installed it yet.
|
|
@@ -1080,13 +1827,15 @@ const PANEL_ID = "dsh-worktree-switch";
|
|
|
1080
1827
|
* Client services this plugin cannot work without.
|
|
1081
1828
|
*
|
|
1082
1829
|
* `conversation` is how the switch stops a prompt while a copy it promised is
|
|
1083
|
-
* not ready; `workspaces` is how the panel finds the projects to list
|
|
1830
|
+
* not ready; `workspaces` is how the panel finds the projects to list;
|
|
1831
|
+
* `sessions` is how the panel derives which copy the visible session is in.
|
|
1084
1832
|
*/
|
|
1085
1833
|
const inject = [
|
|
1086
1834
|
"conversation",
|
|
1087
1835
|
"connection",
|
|
1088
1836
|
"layout",
|
|
1089
1837
|
"locale",
|
|
1838
|
+
"sessions",
|
|
1090
1839
|
"slots",
|
|
1091
1840
|
"workspaces"
|
|
1092
1841
|
];
|
|
@@ -1101,13 +1850,27 @@ function apply(ctx) {
|
|
|
1101
1850
|
}), "dsh-worktree-switch: dictionaries");
|
|
1102
1851
|
ctx.effect(installStyles, "dsh-worktree-switch: styles");
|
|
1103
1852
|
const t = ctx.locale.bind(NS);
|
|
1104
|
-
const
|
|
1853
|
+
const notices = new NoticeController(() => {
|
|
1854
|
+
ctx.layout.selectPanel(PANEL_ID);
|
|
1855
|
+
});
|
|
1105
1856
|
const panels = new PanelController(ctx, () => {
|
|
1106
1857
|
ctx.layout.selectPanel(null);
|
|
1107
1858
|
});
|
|
1108
|
-
ctx
|
|
1109
|
-
|
|
1110
|
-
|
|
1859
|
+
const toggles = new ToggleController(ctx, notices, () => {
|
|
1860
|
+
panels.refresh();
|
|
1861
|
+
});
|
|
1862
|
+
const badges = new BadgeController(ctx, (path) => {
|
|
1863
|
+
panels.highlight(path);
|
|
1864
|
+
ctx.layout.selectPanel(PANEL_ID);
|
|
1865
|
+
});
|
|
1866
|
+
ctx.effect(() => {
|
|
1867
|
+
const unwatch = panels.watch();
|
|
1868
|
+
return () => {
|
|
1869
|
+
unwatch();
|
|
1870
|
+
toggles.dispose();
|
|
1871
|
+
panels.dispose();
|
|
1872
|
+
badges.dispose();
|
|
1873
|
+
};
|
|
1111
1874
|
}, "dsh-worktree-switch: controllers");
|
|
1112
1875
|
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
1113
1876
|
name: "conversation.input.left",
|
|
@@ -1128,10 +1891,27 @@ function apply(ctx) {
|
|
|
1128
1891
|
order: 30,
|
|
1129
1892
|
locale: NS,
|
|
1130
1893
|
label: () => t("sidebar.label"),
|
|
1131
|
-
inject: () => ({
|
|
1132
|
-
|
|
1133
|
-
|
|
1894
|
+
inject: () => ({
|
|
1895
|
+
hooks: panels.face.hooks,
|
|
1896
|
+
open: () => {
|
|
1897
|
+
ctx.layout.selectPanel(PANEL_ID);
|
|
1898
|
+
}
|
|
1899
|
+
})
|
|
1134
1900
|
}, WorktreePanelIcon));
|
|
1901
|
+
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
1902
|
+
name: "conversation.session.header.utilities",
|
|
1903
|
+
id: "dsh-worktree-switch.badge",
|
|
1904
|
+
order: 60,
|
|
1905
|
+
locale: NS,
|
|
1906
|
+
inject: (sessionId) => badges.binding(sessionId)
|
|
1907
|
+
}, WorktreeBadge));
|
|
1908
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1909
|
+
name: "shell.overlay",
|
|
1910
|
+
id: "dsh-worktree-switch.notice",
|
|
1911
|
+
order: 20,
|
|
1912
|
+
locale: NS,
|
|
1913
|
+
inject: () => notices.face
|
|
1914
|
+
}, WorktreeNotice));
|
|
1135
1915
|
}
|
|
1136
1916
|
exports.NS = NS;
|
|
1137
1917
|
exports.PANEL_ID = PANEL_ID;
|