dsh-worktree-switch 0.3.1 → 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 +104 -21
- package/lib/client.js +1274 -487
- package/lib/index.d.ts +13 -0
- package/lib/index.js +601 -106
- 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,14 +115,21 @@ 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
|
})];
|
|
132
|
+
if (state.base?.kind === "fallback") parts.push(t("chip.hint.baseFallback"));
|
|
56
133
|
if (!state.intent) parts.push(t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }));
|
|
57
134
|
if (state.sourceDirty > 0) parts.push(t("chip.hint.dirty", { count: state.sourceDirty }));
|
|
58
135
|
if (state.error !== null) parts.push(state.error);
|
|
@@ -101,6 +178,7 @@ function WorktreeChip(props) {
|
|
|
101
178
|
className: "dsh-wt-chip",
|
|
102
179
|
"data-phase": state.phase,
|
|
103
180
|
"data-intent": state.intent ? "on" : "off",
|
|
181
|
+
"data-kept": state.kept === null ? "false" : state.kept.reason,
|
|
104
182
|
"data-hint": hint,
|
|
105
183
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
106
184
|
label: hint,
|
|
@@ -138,370 +216,58 @@ function WorktreeChip(props) {
|
|
|
138
216
|
}) : null]
|
|
139
217
|
});
|
|
140
218
|
}
|
|
141
|
-
// src/client/
|
|
219
|
+
// src/client/Notice.tsx
|
|
142
220
|
/**
|
|
143
|
-
* The
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* offers here: nothing merges, nothing is reclaimed automatically, and a copy
|
|
148
|
-
* with uncommitted changes is refused by the host, not hidden by a disabled
|
|
149
|
-
* button alone.
|
|
150
|
-
* @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.
|
|
151
225
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (milliseconds <= 0) return "—";
|
|
157
|
-
return new Date(milliseconds).toISOString().slice(0, 16).replace("T", " ");
|
|
158
|
-
}
|
|
159
|
-
/** The key that identifies one discard row. */
|
|
160
|
-
function rowKey(group, item) {
|
|
161
|
-
return `${group.path}\u0000${item.name}`;
|
|
162
|
-
}
|
|
163
|
-
/** One project's copies. */
|
|
164
|
-
function ProjectSection(props) {
|
|
165
|
-
const { group, state, armed, t, onArm, onDiscard } = props;
|
|
166
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
167
|
-
className: "dsh-wt-project",
|
|
168
|
-
"data-project": group.path,
|
|
169
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
170
|
-
className: "dsh-wt-project-head",
|
|
171
|
-
children: [
|
|
172
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: group.title }),
|
|
173
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
174
|
-
className: "dsh-wt-path",
|
|
175
|
-
children: group.path
|
|
176
|
-
}),
|
|
177
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
178
|
-
className: "dsh-wt-count",
|
|
179
|
-
children: t("panel.count", { count: group.items.length })
|
|
180
|
-
})
|
|
181
|
-
]
|
|
182
|
-
}), group.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
183
|
-
className: "dsh-wt-error",
|
|
184
|
-
role: "alert",
|
|
185
|
-
children: group.error
|
|
186
|
-
}) : !group.supported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
187
|
-
className: "dsh-wt-muted",
|
|
188
|
-
children: t("panel.notRepository")
|
|
189
|
-
}) : group.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
190
|
-
className: "dsh-wt-muted",
|
|
191
|
-
children: t("panel.empty")
|
|
192
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
193
|
-
className: "dsh-wt-rows",
|
|
194
|
-
children: group.items.map((item) => {
|
|
195
|
-
const key = rowKey(group, item);
|
|
196
|
-
const isArmed = armed === key;
|
|
197
|
-
const busy = state.busy === key;
|
|
198
|
-
const dirty = item.changedFiles > 0;
|
|
199
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
200
|
-
className: "dsh-wt-row",
|
|
201
|
-
"data-worktree": item.name,
|
|
202
|
-
"data-dirty": dirty ? "true" : "false",
|
|
203
|
-
children: [
|
|
204
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
205
|
-
className: "dsh-wt-name",
|
|
206
|
-
children: item.name
|
|
207
|
-
}),
|
|
208
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
209
|
-
className: "dsh-wt-branch",
|
|
210
|
-
children: [
|
|
211
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
212
|
-
className: "dsh-wt-muted",
|
|
213
|
-
children: t("panel.branch")
|
|
214
|
-
}),
|
|
215
|
-
" ",
|
|
216
|
-
item.branch
|
|
217
|
-
]
|
|
218
|
-
}),
|
|
219
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
220
|
-
className: "dsh-wt-row-path",
|
|
221
|
-
children: item.path
|
|
222
|
-
}),
|
|
223
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
224
|
-
className: "dsh-wt-created",
|
|
225
|
-
children: [
|
|
226
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
227
|
-
className: "dsh-wt-muted",
|
|
228
|
-
children: t("panel.created")
|
|
229
|
-
}),
|
|
230
|
-
" ",
|
|
231
|
-
formatTime(item.createdAt)
|
|
232
|
-
]
|
|
233
|
-
}),
|
|
234
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
235
|
-
className: "dsh-wt-changes",
|
|
236
|
-
"data-changes": item.changedFiles,
|
|
237
|
-
children: dirty ? t("panel.changesCount", { count: item.changedFiles }) : t("panel.clean")
|
|
238
|
-
}),
|
|
239
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
240
|
-
label: dirty ? t("panel.discardDirty") : t("panel.discardHint"),
|
|
241
|
-
side: "top",
|
|
242
|
-
delayMs: 200,
|
|
243
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
244
|
-
className: "dsh-wt-anchor",
|
|
245
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
246
|
-
type: "button",
|
|
247
|
-
className: "dsh-wt-discard",
|
|
248
|
-
"data-armed": isArmed ? "true" : "false",
|
|
249
|
-
disabled: dirty || busy || !group.supported,
|
|
250
|
-
onClick: () => {
|
|
251
|
-
if (isArmed) {
|
|
252
|
-
onDiscard(group, item);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
onArm(key);
|
|
256
|
-
},
|
|
257
|
-
children: busy ? "…" : isArmed ? t("panel.discardAgain") : t("panel.discard")
|
|
258
|
-
})
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
]
|
|
262
|
-
}, key);
|
|
263
|
-
})
|
|
264
|
-
})]
|
|
265
|
-
});
|
|
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 });
|
|
266
230
|
}
|
|
267
231
|
/**
|
|
268
|
-
* Render the
|
|
269
|
-
* @param props - runtime props, the
|
|
270
|
-
* @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.
|
|
271
235
|
*/
|
|
272
|
-
function
|
|
273
|
-
const {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const timer = (0, react.useRef)(null);
|
|
277
|
-
(0, react.useEffect)(() => {
|
|
278
|
-
refresh();
|
|
279
|
-
}, [refresh]);
|
|
280
|
-
(0, react.useEffect)(() => () => {
|
|
281
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
282
|
-
}, []);
|
|
283
|
-
const arm = (key) => {
|
|
284
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
285
|
-
setArmed(key);
|
|
286
|
-
timer.current = window.setTimeout(() => {
|
|
287
|
-
setArmed(null);
|
|
288
|
-
}, ARM_TIMEOUT_MS);
|
|
289
|
-
};
|
|
290
|
-
const onDiscard = (group, item) => {
|
|
291
|
-
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
292
|
-
setArmed(null);
|
|
293
|
-
discard(group.path, item.name);
|
|
294
|
-
};
|
|
295
|
-
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;
|
|
296
240
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
297
|
-
className: "dsh-wt-
|
|
298
|
-
"data-
|
|
241
|
+
className: "dsh-wt-notice",
|
|
242
|
+
"data-kind": notice.kind,
|
|
243
|
+
"data-seq": notice.seq,
|
|
244
|
+
role: "status",
|
|
299
245
|
children: [
|
|
300
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
301
|
-
className: "dsh-wt-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
type: "button",
|
|
305
|
-
className: "dsh-wt-back",
|
|
306
|
-
onClick: () => {
|
|
307
|
-
close();
|
|
308
|
-
},
|
|
309
|
-
children: t("panel.back")
|
|
310
|
-
}),
|
|
311
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
312
|
-
className: "dsh-wt-titles",
|
|
313
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("panel.title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("panel.subtitle") })]
|
|
314
|
-
}),
|
|
315
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
316
|
-
className: "dsh-wt-count",
|
|
317
|
-
children: t("panel.count", { count: total })
|
|
318
|
-
}),
|
|
319
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
320
|
-
type: "button",
|
|
321
|
-
className: "dsh-wt-refresh",
|
|
322
|
-
onClick: () => {
|
|
323
|
-
refresh();
|
|
324
|
-
},
|
|
325
|
-
children: t("panel.refresh")
|
|
326
|
-
})
|
|
327
|
-
]
|
|
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)
|
|
328
250
|
}),
|
|
329
|
-
|
|
330
|
-
className: "dsh-wt-error",
|
|
331
|
-
role: "alert",
|
|
332
|
-
children: t("panel.error", { message: state.error })
|
|
333
|
-
}) : null,
|
|
334
|
-
state.loading && state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
335
|
-
className: "dsh-wt-muted",
|
|
336
|
-
role: "status",
|
|
337
|
-
children: t("panel.loading")
|
|
338
|
-
}) : state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
339
|
-
className: "dsh-wt-muted",
|
|
340
|
-
children: t("panel.workspaceEmpty")
|
|
341
|
-
}) : state.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectSection, {
|
|
342
|
-
group,
|
|
343
|
-
state,
|
|
344
|
-
armed,
|
|
345
|
-
t,
|
|
346
|
-
onArm: arm,
|
|
347
|
-
onDiscard
|
|
348
|
-
}, group.path))
|
|
349
|
-
]
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Render the sidebar entry that opens the panel.
|
|
354
|
-
* @param props - sidebar owner props, the open action, and the locale seat.
|
|
355
|
-
* @returns the icon button.
|
|
356
|
-
*/
|
|
357
|
-
function WorktreePanelIcon(props) {
|
|
358
|
-
const { size, active, open, t } = props;
|
|
359
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
360
|
-
label: t("sidebar.label"),
|
|
361
|
-
side: "right",
|
|
362
|
-
delayMs: 300,
|
|
363
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
364
|
-
className: "dsh-wt-anchor",
|
|
365
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
366
252
|
type: "button",
|
|
367
|
-
className: "dsh-wt-
|
|
368
|
-
"data-active": active ? "true" : "false",
|
|
369
|
-
"aria-label": t("sidebar.label"),
|
|
370
|
-
"aria-pressed": active,
|
|
253
|
+
className: "dsh-wt-notice-open",
|
|
371
254
|
onClick: () => {
|
|
372
|
-
|
|
255
|
+
openPanel();
|
|
373
256
|
},
|
|
374
|
-
children:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 2.5v7A2.5 2.5 0 0 0 6.5 12H11" }),
|
|
385
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
386
|
-
cx: "4",
|
|
387
|
-
cy: "2.5",
|
|
388
|
-
r: "1.5"
|
|
389
|
-
}),
|
|
390
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
391
|
-
cx: "4",
|
|
392
|
-
cy: "12.5",
|
|
393
|
-
r: "1.5"
|
|
394
|
-
}),
|
|
395
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
396
|
-
cx: "11",
|
|
397
|
-
cy: "10.5",
|
|
398
|
-
r: "1.5"
|
|
399
|
-
}),
|
|
400
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
401
|
-
d: "M8.5 6.5h2.5v2",
|
|
402
|
-
strokeDasharray: "1.6 2"
|
|
403
|
-
})
|
|
404
|
-
]
|
|
405
|
-
})
|
|
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: "×"
|
|
406
267
|
})
|
|
407
|
-
|
|
408
|
-
});
|
|
268
|
+
]
|
|
269
|
+
}, notice.seq);
|
|
409
270
|
}
|
|
410
|
-
// src/client/locales.ts
|
|
411
|
-
/** English dictionary; its keys are the namespace's key domain. */
|
|
412
|
-
const en = {
|
|
413
|
-
"chip.label": "Worktree copy for this session",
|
|
414
|
-
"chip.checking": "Worktree…",
|
|
415
|
-
"chip.off": "Worktree off",
|
|
416
|
-
"chip.creating": "Creating worktree…",
|
|
417
|
-
"chip.reverting": "Leaving worktree…",
|
|
418
|
-
"chip.ready": "Worktree: {name}",
|
|
419
|
-
"chip.failed": "Worktree failed",
|
|
420
|
-
"chip.retry": "Retry",
|
|
421
|
-
"chip.disabled": "Worktree unavailable",
|
|
422
|
-
"chip.hint.checking": "Checking whether this project can be isolated…",
|
|
423
|
-
"chip.hint.off": "New sessions in this project start in the project directory.",
|
|
424
|
-
"chip.hint.on": "This session runs in a copy of the project; the source checkout is untouched.",
|
|
425
|
-
"chip.hint.creating": "Creating a checkout inside the project…",
|
|
426
|
-
"chip.hint.reverting": "Moving this session back to the project directory…",
|
|
427
|
-
"chip.hint.ready": "Branch {branch} at {path}",
|
|
428
|
-
"chip.hint.failed": "The copy was not created, so this session is still in the project directory.",
|
|
429
|
-
"chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
|
|
430
|
-
"chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
|
|
431
|
-
"chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
|
|
432
|
-
"chip.blocked.checking": "Checking whether this session gets a worktree…",
|
|
433
|
-
"chip.blocked.creating": "Creating the worktree before the first prompt…",
|
|
434
|
-
"chip.blocked.reverting": "Leaving the worktree before the first prompt…",
|
|
435
|
-
"chip.blocked.failed": "The worktree could not be created. Retry, turn the switch off, or discard the failed copy first.",
|
|
436
|
-
"panel.title": "Worktrees",
|
|
437
|
-
"panel.subtitle": "Copies created by the Worktree switch: one checkout and one branch each. Discard removes both.",
|
|
438
|
-
"panel.refresh": "Refresh",
|
|
439
|
-
"panel.back": "Back",
|
|
440
|
-
"panel.loading": "Reading worktrees…",
|
|
441
|
-
"panel.empty": "No worktrees here yet.",
|
|
442
|
-
"panel.workspaceEmpty": "No projects yet. Open a session to give this panel something to read.",
|
|
443
|
-
"panel.error": "Could not read worktrees: {message}",
|
|
444
|
-
"panel.notRepository": "Not a Git repository.",
|
|
445
|
-
"panel.name": "Name",
|
|
446
|
-
"panel.branch": "Branch",
|
|
447
|
-
"panel.created": "Created",
|
|
448
|
-
"panel.changes": "Changes",
|
|
449
|
-
"panel.changesCount": "{count} uncommitted",
|
|
450
|
-
"panel.clean": "Clean",
|
|
451
|
-
"panel.discard": "Discard",
|
|
452
|
-
"panel.discardAgain": "Discard now",
|
|
453
|
-
"panel.discardHint": "Deletes the checkout and its branch.",
|
|
454
|
-
"panel.discardDirty": "Commit or discard its changes first.",
|
|
455
|
-
"panel.count": "{count} copies",
|
|
456
|
-
"sidebar.label": "Worktrees"
|
|
457
|
-
};
|
|
458
|
-
/** Chinese dictionary: every key of {@link en}. */
|
|
459
|
-
const zh = {
|
|
460
|
-
"chip.label": "本会话使用 Worktree 副本",
|
|
461
|
-
"chip.checking": "Worktree…",
|
|
462
|
-
"chip.off": "Worktree 已关",
|
|
463
|
-
"chip.creating": "正在创建 Worktree…",
|
|
464
|
-
"chip.reverting": "正在离开 Worktree…",
|
|
465
|
-
"chip.ready": "Worktree:{name}",
|
|
466
|
-
"chip.failed": "Worktree 创建失败",
|
|
467
|
-
"chip.retry": "重试",
|
|
468
|
-
"chip.disabled": "Worktree 不可用",
|
|
469
|
-
"chip.hint.checking": "正在检查这个项目能否隔离…",
|
|
470
|
-
"chip.hint.off": "这个项目的新会话直接在项目目录里开始。",
|
|
471
|
-
"chip.hint.on": "本会话在项目的一份副本里运行,源仓库不受影响。",
|
|
472
|
-
"chip.hint.creating": "正在项目里创建检出…",
|
|
473
|
-
"chip.hint.reverting": "正在把本会话切回项目目录…",
|
|
474
|
-
"chip.hint.ready": "分支 {branch},位于 {path}",
|
|
475
|
-
"chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
|
|
476
|
-
"chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
|
|
477
|
-
"chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
|
|
478
|
-
"chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
|
|
479
|
-
"chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
|
|
480
|
-
"chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
|
|
481
|
-
"chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
|
|
482
|
-
"chip.blocked.failed": "Worktree 创建失败。请重试、关闭开关,或先丢弃失败的副本。",
|
|
483
|
-
"panel.title": "Worktree",
|
|
484
|
-
"panel.subtitle": "Worktree 开关创建的副本:每份副本是一个检出加一个分支,丢弃会同时删掉两者。",
|
|
485
|
-
"panel.refresh": "刷新",
|
|
486
|
-
"panel.back": "返回",
|
|
487
|
-
"panel.loading": "正在读取 Worktree…",
|
|
488
|
-
"panel.empty": "这里还没有 Worktree。",
|
|
489
|
-
"panel.workspaceEmpty": "还没有项目。打开一个会话,这里才有可读的内容。",
|
|
490
|
-
"panel.error": "读取 Worktree 失败:{message}",
|
|
491
|
-
"panel.notRepository": "不是 Git 仓库。",
|
|
492
|
-
"panel.name": "名称",
|
|
493
|
-
"panel.branch": "分支",
|
|
494
|
-
"panel.created": "创建时间",
|
|
495
|
-
"panel.changes": "改动",
|
|
496
|
-
"panel.changesCount": "{count} 处未提交",
|
|
497
|
-
"panel.clean": "干净",
|
|
498
|
-
"panel.discard": "丢弃",
|
|
499
|
-
"panel.discardAgain": "确认丢弃",
|
|
500
|
-
"panel.discardHint": "删除检出目录和它的分支。",
|
|
501
|
-
"panel.discardDirty": "请先提交或丢弃其中的改动。",
|
|
502
|
-
"panel.count": "{count} 份副本",
|
|
503
|
-
"sidebar.label": "Worktree"
|
|
504
|
-
};
|
|
505
271
|
// src/client/intent.ts
|
|
506
272
|
/**
|
|
507
273
|
* The remembered per-project choice: "this project always isolates" and
|
|
@@ -581,7 +347,9 @@ const INITIAL = {
|
|
|
581
347
|
worktree: null,
|
|
582
348
|
repositoryRoot: null,
|
|
583
349
|
sourceDirty: 0,
|
|
584
|
-
|
|
350
|
+
base: null,
|
|
351
|
+
error: null,
|
|
352
|
+
kept: null
|
|
585
353
|
};
|
|
586
354
|
/** A thrown value's message, without leaking `undefined`. */
|
|
587
355
|
function messageOf(error) {
|
|
@@ -590,18 +358,174 @@ function messageOf(error) {
|
|
|
590
358
|
return text === "" ? "unknown error" : text;
|
|
591
359
|
}
|
|
592
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
|
+
/**
|
|
593
511
|
* One controller per plugin instance: it caches the state of every session the
|
|
594
512
|
* switch has rendered for, and owns their composer blocks.
|
|
595
513
|
*/
|
|
596
514
|
var ToggleController = class {
|
|
597
515
|
ctx;
|
|
516
|
+
notices;
|
|
517
|
+
onLeftover;
|
|
598
518
|
bindings = /* @__PURE__ */ new Map();
|
|
599
519
|
lifetime = new AbortController();
|
|
600
520
|
/**
|
|
601
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.
|
|
602
524
|
*/
|
|
603
|
-
constructor(ctx) {
|
|
525
|
+
constructor(ctx, notices, onLeftover) {
|
|
604
526
|
this.ctx = ctx;
|
|
527
|
+
this.notices = notices;
|
|
528
|
+
this.onLeftover = onLeftover;
|
|
605
529
|
}
|
|
606
530
|
/**
|
|
607
531
|
* The binding for one session, created on first use.
|
|
@@ -612,7 +536,7 @@ var ToggleController = class {
|
|
|
612
536
|
const key = String(sessionId);
|
|
613
537
|
const existing = this.bindings.get(key);
|
|
614
538
|
if (existing !== void 0) return existing;
|
|
615
|
-
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);
|
|
616
540
|
this.bindings.set(key, binding.face);
|
|
617
541
|
return binding.face;
|
|
618
542
|
}
|
|
@@ -628,6 +552,8 @@ var InternalBinding = class {
|
|
|
628
552
|
ctx;
|
|
629
553
|
sessionId;
|
|
630
554
|
signal;
|
|
555
|
+
notices;
|
|
556
|
+
onLeftover;
|
|
631
557
|
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL);
|
|
632
558
|
started = false;
|
|
633
559
|
/** The project whose remembered choice is in play; null until status answers. */
|
|
@@ -642,10 +568,12 @@ var InternalBinding = class {
|
|
|
642
568
|
* @param sessionId - wire session identity.
|
|
643
569
|
* @param signal - plugin lifetime cancellation.
|
|
644
570
|
*/
|
|
645
|
-
constructor(ctx, sessionId, signal) {
|
|
571
|
+
constructor(ctx, sessionId, signal, notices, onLeftover) {
|
|
646
572
|
this.ctx = ctx;
|
|
647
573
|
this.sessionId = sessionId;
|
|
648
574
|
this.signal = signal;
|
|
575
|
+
this.notices = notices;
|
|
576
|
+
this.onLeftover = onLeftover;
|
|
649
577
|
this.face = {
|
|
650
578
|
hooks: { toggle: this.store },
|
|
651
579
|
ensure: () => {
|
|
@@ -689,7 +617,9 @@ var InternalBinding = class {
|
|
|
689
617
|
worktree: status.sessionWorktree,
|
|
690
618
|
repositoryRoot: status.repositoryRoot,
|
|
691
619
|
sourceDirty: status.sourceDirty,
|
|
692
|
-
|
|
620
|
+
base: null,
|
|
621
|
+
error: null,
|
|
622
|
+
kept: null
|
|
693
623
|
};
|
|
694
624
|
if (status.sessionWorktree !== null) {
|
|
695
625
|
this.store.set({
|
|
@@ -738,7 +668,9 @@ var InternalBinding = class {
|
|
|
738
668
|
worktree: prepared.worktree,
|
|
739
669
|
repositoryRoot: prepared.repositoryRoot,
|
|
740
670
|
sourceDirty: prepared.sourceDirty,
|
|
741
|
-
|
|
671
|
+
base: prepared.state === "created" ? prepared.base : null,
|
|
672
|
+
error: null,
|
|
673
|
+
kept: null
|
|
742
674
|
});
|
|
743
675
|
} catch (error) {
|
|
744
676
|
if (this.signal.aborted) return;
|
|
@@ -776,30 +708,79 @@ var InternalBinding = class {
|
|
|
776
708
|
this.store.set({
|
|
777
709
|
...before,
|
|
778
710
|
phase: "reverting",
|
|
779
|
-
error: null
|
|
711
|
+
error: null,
|
|
712
|
+
kept: null
|
|
780
713
|
});
|
|
781
714
|
try {
|
|
782
|
-
await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
|
|
715
|
+
const value = await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
|
|
783
716
|
if (this.signal.aborted) return;
|
|
784
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;
|
|
785
723
|
this.store.set({
|
|
786
724
|
...now,
|
|
787
725
|
phase: "off",
|
|
788
726
|
intent: false,
|
|
789
727
|
worktree: null,
|
|
790
|
-
|
|
728
|
+
base: null,
|
|
729
|
+
error: null,
|
|
730
|
+
kept
|
|
791
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
|
+
}
|
|
792
741
|
} catch (error) {
|
|
793
742
|
if (this.signal.aborted) return;
|
|
794
743
|
const now = this.store.getSnapshot();
|
|
744
|
+
const message = messageOf(error);
|
|
795
745
|
this.store.set({
|
|
796
746
|
...now,
|
|
797
747
|
phase: now.worktree === null ? "off" : "ready",
|
|
798
748
|
intent: false,
|
|
799
|
-
error:
|
|
749
|
+
error: message,
|
|
750
|
+
kept: null
|
|
800
751
|
});
|
|
752
|
+
this.notices.raise({
|
|
753
|
+
kind: "failed",
|
|
754
|
+
worktree: now.worktree,
|
|
755
|
+
reason: null,
|
|
756
|
+
message
|
|
757
|
+
});
|
|
758
|
+
this.onLeftover();
|
|
759
|
+
this.recheck();
|
|
801
760
|
}
|
|
802
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
|
+
}
|
|
803
784
|
/** Persist and apply one switch move. */
|
|
804
785
|
setIntent(next) {
|
|
805
786
|
const state = this.store.getSnapshot();
|
|
@@ -810,7 +791,8 @@ var InternalBinding = class {
|
|
|
810
791
|
this.store.set({
|
|
811
792
|
...state,
|
|
812
793
|
intent: false,
|
|
813
|
-
error: null
|
|
794
|
+
error: null,
|
|
795
|
+
kept: null
|
|
814
796
|
});
|
|
815
797
|
this.release();
|
|
816
798
|
return;
|
|
@@ -819,7 +801,8 @@ var InternalBinding = class {
|
|
|
819
801
|
this.store.set({
|
|
820
802
|
...state,
|
|
821
803
|
intent: true,
|
|
822
|
-
error: null
|
|
804
|
+
error: null,
|
|
805
|
+
kept: null
|
|
823
806
|
});
|
|
824
807
|
if (state.phase === "off" || state.phase === "failed") this.create();
|
|
825
808
|
}
|
|
@@ -832,7 +815,8 @@ var InternalBinding = class {
|
|
|
832
815
|
...state,
|
|
833
816
|
intent: true,
|
|
834
817
|
phase: "off",
|
|
835
|
-
error: null
|
|
818
|
+
error: null,
|
|
819
|
+
kept: null
|
|
836
820
|
});
|
|
837
821
|
this.create();
|
|
838
822
|
}
|
|
@@ -844,6 +828,27 @@ var InternalBinding = class {
|
|
|
844
828
|
}
|
|
845
829
|
};
|
|
846
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
|
+
/**
|
|
847
852
|
* Read every workspace's pool and discard copies from the panel.
|
|
848
853
|
*/
|
|
849
854
|
var PanelController = class {
|
|
@@ -853,115 +858,787 @@ var PanelController = class {
|
|
|
853
858
|
loading: true,
|
|
854
859
|
groups: [],
|
|
855
860
|
error: null,
|
|
856
|
-
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
|
+
})
|
|
857
1498
|
});
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
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"
|
|
965
1642
|
};
|
|
966
1643
|
// src/client/styles.ts
|
|
967
1644
|
/**
|
|
@@ -1029,12 +1706,14 @@ const CSS = `
|
|
|
1029
1706
|
.dsh-wt-path, .dsh-wt-row-path { font-size: 11px; opacity: .64; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1030
1707
|
.dsh-wt-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1031
1708
|
.dsh-wt-row {
|
|
1032
|
-
display:
|
|
1033
|
-
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;
|
|
1034
1710
|
padding: 8px 12px; border-radius: 8px;
|
|
1035
1711
|
border: 1px solid color-mix(in oklab, currentColor 12%, transparent);
|
|
1036
1712
|
}
|
|
1037
|
-
.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; }
|
|
1038
1717
|
.dsh-wt-branch, .dsh-wt-created, .dsh-wt-changes { font-size: 12px; }
|
|
1039
1718
|
.dsh-wt-changes[data-changes]:not([data-changes="0"]) { color: #bf8700; }
|
|
1040
1719
|
.dsh-wt-muted { opacity: .64; font-size: 12px; }
|
|
@@ -1046,6 +1725,81 @@ const CSS = `
|
|
|
1046
1725
|
}
|
|
1047
1726
|
.dsh-wt-panel-icon:hover { opacity: 1; background: color-mix(in oklab, currentColor 8%, transparent); }
|
|
1048
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
|
+
}
|
|
1049
1803
|
`;
|
|
1050
1804
|
/**
|
|
1051
1805
|
* Install the stylesheet if this page has not installed it yet.
|
|
@@ -1073,13 +1827,15 @@ const PANEL_ID = "dsh-worktree-switch";
|
|
|
1073
1827
|
* Client services this plugin cannot work without.
|
|
1074
1828
|
*
|
|
1075
1829
|
* `conversation` is how the switch stops a prompt while a copy it promised is
|
|
1076
|
-
* 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.
|
|
1077
1832
|
*/
|
|
1078
1833
|
const inject = [
|
|
1079
1834
|
"conversation",
|
|
1080
1835
|
"connection",
|
|
1081
1836
|
"layout",
|
|
1082
1837
|
"locale",
|
|
1838
|
+
"sessions",
|
|
1083
1839
|
"slots",
|
|
1084
1840
|
"workspaces"
|
|
1085
1841
|
];
|
|
@@ -1094,13 +1850,27 @@ function apply(ctx) {
|
|
|
1094
1850
|
}), "dsh-worktree-switch: dictionaries");
|
|
1095
1851
|
ctx.effect(installStyles, "dsh-worktree-switch: styles");
|
|
1096
1852
|
const t = ctx.locale.bind(NS);
|
|
1097
|
-
const
|
|
1853
|
+
const notices = new NoticeController(() => {
|
|
1854
|
+
ctx.layout.selectPanel(PANEL_ID);
|
|
1855
|
+
});
|
|
1098
1856
|
const panels = new PanelController(ctx, () => {
|
|
1099
1857
|
ctx.layout.selectPanel(null);
|
|
1100
1858
|
});
|
|
1101
|
-
ctx
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
+
};
|
|
1104
1874
|
}, "dsh-worktree-switch: controllers");
|
|
1105
1875
|
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
1106
1876
|
name: "conversation.input.left",
|
|
@@ -1121,10 +1891,27 @@ function apply(ctx) {
|
|
|
1121
1891
|
order: 30,
|
|
1122
1892
|
locale: NS,
|
|
1123
1893
|
label: () => t("sidebar.label"),
|
|
1124
|
-
inject: () => ({
|
|
1125
|
-
|
|
1126
|
-
|
|
1894
|
+
inject: () => ({
|
|
1895
|
+
hooks: panels.face.hooks,
|
|
1896
|
+
open: () => {
|
|
1897
|
+
ctx.layout.selectPanel(PANEL_ID);
|
|
1898
|
+
}
|
|
1899
|
+
})
|
|
1127
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));
|
|
1128
1915
|
}
|
|
1129
1916
|
exports.NS = NS;
|
|
1130
1917
|
exports.PANEL_ID = PANEL_ID;
|