@hunan36/dsh-git-flow 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +253 -2
- package/README.zh.md +262 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2138 -0
- package/lib/index.js +1347 -0
- package/lib/types/client/BranchChip.d.ts +8 -0
- package/lib/types/client/BranchMenu.d.ts +27 -0
- package/lib/types/client/CommitDialog.d.ts +45 -0
- package/lib/types/client/GitFlowDialog.d.ts +37 -0
- package/lib/types/client/api.d.ts +42 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/locales.d.ts +236 -0
- package/lib/types/client/styles.d.ts +12 -0
- package/lib/types/contract.d.ts +207 -0
- package/lib/types/git.d.ts +86 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/message.d.ts +27 -0
- package/lib/types/routes.d.ts +14 -0
- package/lib/types/service.d.ts +178 -0
- package/package.json +92 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,2138 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@hunan36/dsh-git-flow",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_dom = require("react-dom");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#region src/client/BranchMenu.tsx
|
|
12
|
+
/**
|
|
13
|
+
* The composer-tool-row branch chip and its anchored popover. Data and
|
|
14
|
+
* mutations belong to the parent; this component renders rows and reports intent.
|
|
15
|
+
*
|
|
16
|
+
* The popover is assembled from the overlay primitives rather than the `Menu`
|
|
17
|
+
* component: a menu renders every entry into its scrolling viewport, so a
|
|
18
|
+
* search box there would scroll away with the rows — and would sit inside a
|
|
19
|
+
* `role="menuitem"` button, where a click selects the row instead of typing.
|
|
20
|
+
*/
|
|
21
|
+
/** Pill plus anchored branch picker: pinned search, scrolling rows, pinned actions. */
|
|
22
|
+
function BranchMenu(props) {
|
|
23
|
+
const { t, status, branches, busy, open } = props;
|
|
24
|
+
const anchorRef = (0, react.useRef)(null);
|
|
25
|
+
const panelRef = (0, react.useRef)(null);
|
|
26
|
+
const [filter, setFilter] = (0, react.useState)("");
|
|
27
|
+
const [placement, setPlacement] = (0, react.useState)(void 0);
|
|
28
|
+
(0, react.useLayoutEffect)(() => {
|
|
29
|
+
if (!open) {
|
|
30
|
+
setPlacement(void 0);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const place = () => {
|
|
34
|
+
const rect = anchorRef.current?.getBoundingClientRect();
|
|
35
|
+
if (rect === void 0) return;
|
|
36
|
+
const width = panelRef.current?.offsetWidth ?? PANEL_WIDTH;
|
|
37
|
+
const left = Math.min(Math.max(rect.left, MARGIN), Math.max(MARGIN, window.innerWidth - width - MARGIN));
|
|
38
|
+
const room = rect.top - GAP - MARGIN;
|
|
39
|
+
setPlacement({
|
|
40
|
+
left,
|
|
41
|
+
bottom: window.innerHeight - rect.top + GAP,
|
|
42
|
+
maxHeight: Math.max(PANEL_MIN_HEIGHT, Math.min(PANEL_MAX_HEIGHT, room))
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
place();
|
|
46
|
+
window.addEventListener("scroll", place, true);
|
|
47
|
+
window.addEventListener("resize", place);
|
|
48
|
+
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(place);
|
|
49
|
+
if (panelRef.current !== null) observer?.observe(panelRef.current);
|
|
50
|
+
return () => {
|
|
51
|
+
observer?.disconnect();
|
|
52
|
+
window.removeEventListener("scroll", place, true);
|
|
53
|
+
window.removeEventListener("resize", place);
|
|
54
|
+
};
|
|
55
|
+
}, [open]);
|
|
56
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(anchorRef, open, (next) => {
|
|
57
|
+
props.onOpenChange(next);
|
|
58
|
+
}, panelRef);
|
|
59
|
+
(0, react.useEffect)(() => {
|
|
60
|
+
if (open) setFilter("");
|
|
61
|
+
}, [open]);
|
|
62
|
+
const rows = (0, react.useMemo)(() => buildRows(t, branches, filter), [
|
|
63
|
+
t,
|
|
64
|
+
branches,
|
|
65
|
+
filter
|
|
66
|
+
]);
|
|
67
|
+
const firstBranch = rows.find((row) => row.kind === "branch" && row.name !== status.head);
|
|
68
|
+
const tooltip = status.files.length === 0 ? t("chip.tooltip.clean", { branch: status.head }) : t("chip.tooltip", {
|
|
69
|
+
branch: status.head,
|
|
70
|
+
count: status.files.length
|
|
71
|
+
});
|
|
72
|
+
const unpushed = status.ahead > 0 ? t("chip.unpushed", { count: status.ahead }) : void 0;
|
|
73
|
+
const noUpstream = status.upstream === void 0 ? t("commit.noUpstream") : void 0;
|
|
74
|
+
const hints = [
|
|
75
|
+
status.upstream !== void 0 && status.behind > 0 ? t("chip.aheadBehind", {
|
|
76
|
+
ahead: status.ahead,
|
|
77
|
+
behind: status.behind
|
|
78
|
+
}) : void 0,
|
|
79
|
+
unpushed,
|
|
80
|
+
noUpstream
|
|
81
|
+
].filter((part) => part !== void 0);
|
|
82
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
83
|
+
ref: anchorRef,
|
|
84
|
+
style: anchorStyle,
|
|
85
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
86
|
+
label: [tooltip, ...hints].join(" · "),
|
|
87
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
88
|
+
onClick: () => {
|
|
89
|
+
props.onOpenChange(!open);
|
|
90
|
+
},
|
|
91
|
+
"aria-label": [tooltip, ...hints].join(" · "),
|
|
92
|
+
"aria-expanded": open,
|
|
93
|
+
style: pillStyle,
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { size: 14 }),
|
|
96
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
97
|
+
style: nameStyle,
|
|
98
|
+
children: status.head
|
|
99
|
+
}),
|
|
100
|
+
props.flow?.kind !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
101
|
+
style: flowBadgeStyle,
|
|
102
|
+
children: t(`flow.${props.flow.kind}`)
|
|
103
|
+
}),
|
|
104
|
+
status.behind > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
105
|
+
style: {
|
|
106
|
+
...badgeBase,
|
|
107
|
+
background: "var(--dsw-alias-state-business-primary, #4176e6)"
|
|
108
|
+
},
|
|
109
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 10 }), status.behind]
|
|
110
|
+
}),
|
|
111
|
+
status.ahead > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
112
|
+
style: {
|
|
113
|
+
...badgeBase,
|
|
114
|
+
background: "var(--dsw-alias-state-success-primary, #22c55e)"
|
|
115
|
+
},
|
|
116
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, { size: 10 }), status.ahead]
|
|
117
|
+
}),
|
|
118
|
+
status.upstream === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
119
|
+
style: aheadStyle,
|
|
120
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, { size: 11 })
|
|
121
|
+
}),
|
|
122
|
+
status.files.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
123
|
+
style: {
|
|
124
|
+
...badgeBase,
|
|
125
|
+
background: "var(--dsw-alias-state-warn-primary, #f59e0b)"
|
|
126
|
+
},
|
|
127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, { size: 10 }), status.files.length]
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
133
|
+
ref: panelRef,
|
|
134
|
+
role: "dialog",
|
|
135
|
+
"aria-label": tooltip,
|
|
136
|
+
style: {
|
|
137
|
+
...placement ?? hidingStyle,
|
|
138
|
+
...panelStyle
|
|
139
|
+
},
|
|
140
|
+
onKeyDown: (event) => {
|
|
141
|
+
if (event.key !== "Escape") return;
|
|
142
|
+
event.stopPropagation();
|
|
143
|
+
props.onOpenChange(false);
|
|
144
|
+
},
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
147
|
+
style: searchRowStyle,
|
|
148
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
149
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }),
|
|
150
|
+
value: filter,
|
|
151
|
+
placeholder: t("menu.search"),
|
|
152
|
+
autoFocus: true,
|
|
153
|
+
style: searchInputStyle,
|
|
154
|
+
onChange: (event) => {
|
|
155
|
+
setFilter(event.target.value);
|
|
156
|
+
},
|
|
157
|
+
onKeyDown: (event) => {
|
|
158
|
+
if (event.key === "Enter" && firstBranch !== void 0) {
|
|
159
|
+
props.onOpenChange(false);
|
|
160
|
+
props.onSelectBranch(firstBranch.name ?? "");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
166
|
+
style: listStyle$1,
|
|
167
|
+
role: "menu",
|
|
168
|
+
children: [rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
169
|
+
style: emptyStyle,
|
|
170
|
+
children: t("menu.empty")
|
|
171
|
+
}), rows.map((row) => row.kind === "group" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
172
|
+
role: "presentation",
|
|
173
|
+
style: groupStyle,
|
|
174
|
+
children: row.label
|
|
175
|
+
}, row.key) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchRow, {
|
|
176
|
+
label: row.label,
|
|
177
|
+
title: row.name ?? "",
|
|
178
|
+
current: row.name === status.head,
|
|
179
|
+
disabled: busy,
|
|
180
|
+
onSelect: () => {
|
|
181
|
+
props.onOpenChange(false);
|
|
182
|
+
props.onSelectBranch(row.name ?? "");
|
|
183
|
+
}
|
|
184
|
+
}, row.key))]
|
|
185
|
+
}),
|
|
186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
187
|
+
style: actionsStyle$2,
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
190
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 14 }),
|
|
191
|
+
label: t("menu.newBranch"),
|
|
192
|
+
onClick: () => {
|
|
193
|
+
props.onOpenChange(false);
|
|
194
|
+
props.onNewBranch();
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
198
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkleRegular, { size: 14 }),
|
|
199
|
+
label: t("menu.commit"),
|
|
200
|
+
disabled: status.files.length === 0,
|
|
201
|
+
onClick: () => {
|
|
202
|
+
props.onOpenChange(false);
|
|
203
|
+
props.onCommit();
|
|
204
|
+
}
|
|
205
|
+
}),
|
|
206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
207
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, { size: 14 }),
|
|
208
|
+
label: t("menu.push"),
|
|
209
|
+
disabled: status.ahead === 0 && status.upstream !== void 0,
|
|
210
|
+
onClick: () => {
|
|
211
|
+
props.onOpenChange(false);
|
|
212
|
+
props.onPush();
|
|
213
|
+
}
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
216
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineRegular, { size: 14 }),
|
|
217
|
+
label: t("menu.pull"),
|
|
218
|
+
disabled: status.upstream === void 0,
|
|
219
|
+
onClick: () => {
|
|
220
|
+
props.onOpenChange(false);
|
|
221
|
+
props.onPull();
|
|
222
|
+
}
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
225
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }),
|
|
226
|
+
label: t("menu.refresh"),
|
|
227
|
+
onClick: () => {
|
|
228
|
+
props.onOpenChange(false);
|
|
229
|
+
props.onRefresh();
|
|
230
|
+
}
|
|
231
|
+
}),
|
|
232
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
233
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { size: 14 }),
|
|
234
|
+
label: t("menu.startFlow"),
|
|
235
|
+
disabled: status.detached,
|
|
236
|
+
onClick: () => {
|
|
237
|
+
props.onOpenChange(false);
|
|
238
|
+
props.onStartFlow();
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
|
|
242
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }),
|
|
243
|
+
label: t("menu.finishFlow"),
|
|
244
|
+
disabled: props.flow?.kind === void 0 || status.files.length > 0,
|
|
245
|
+
onClick: () => {
|
|
246
|
+
props.onOpenChange(false);
|
|
247
|
+
props.onFinishFlow();
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
]
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
}), document.body)] });
|
|
254
|
+
}
|
|
255
|
+
/** One branch row: the name, a check for the current branch, hover fill. */
|
|
256
|
+
function BranchRow(props) {
|
|
257
|
+
const [hover, setHover] = (0, react.useState)(false);
|
|
258
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
259
|
+
type: "button",
|
|
260
|
+
role: "menuitem",
|
|
261
|
+
disabled: props.disabled,
|
|
262
|
+
title: props.title,
|
|
263
|
+
onClick: props.onSelect,
|
|
264
|
+
onPointerEnter: () => {
|
|
265
|
+
setHover(true);
|
|
266
|
+
},
|
|
267
|
+
onPointerLeave: () => {
|
|
268
|
+
setHover(false);
|
|
269
|
+
},
|
|
270
|
+
style: {
|
|
271
|
+
...rowStyle,
|
|
272
|
+
...hover && !props.disabled ? rowHoverStyle$1 : null
|
|
273
|
+
},
|
|
274
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
275
|
+
style: rowNameStyle,
|
|
276
|
+
children: props.label
|
|
277
|
+
}), props.current && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 })]
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** One pinned action below the scrolling list. */
|
|
281
|
+
function ActionRow(props) {
|
|
282
|
+
const [hover, setHover] = (0, react.useState)(false);
|
|
283
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
284
|
+
type: "button",
|
|
285
|
+
disabled: props.disabled,
|
|
286
|
+
onClick: props.onClick,
|
|
287
|
+
onPointerEnter: () => {
|
|
288
|
+
setHover(true);
|
|
289
|
+
},
|
|
290
|
+
onPointerLeave: () => {
|
|
291
|
+
setHover(false);
|
|
292
|
+
},
|
|
293
|
+
style: {
|
|
294
|
+
...actionStyle,
|
|
295
|
+
...hover && props.disabled !== true ? rowHoverStyle$1 : null,
|
|
296
|
+
...props.disabled === true ? disabledStyle : null
|
|
297
|
+
},
|
|
298
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
299
|
+
style: actionIconStyle,
|
|
300
|
+
children: props.icon
|
|
301
|
+
}), props.label]
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** Local rows, then the remote-only ones, filtered by the search text. */
|
|
305
|
+
function buildRows(t, branches, filter) {
|
|
306
|
+
if (branches === void 0) return [{
|
|
307
|
+
kind: "group",
|
|
308
|
+
key: "loading",
|
|
309
|
+
label: t("menu.loading")
|
|
310
|
+
}];
|
|
311
|
+
const needle = filter.trim().toLowerCase();
|
|
312
|
+
const named = (name) => name.toLowerCase().includes(needle);
|
|
313
|
+
const rows = [];
|
|
314
|
+
const group = (label, list) => {
|
|
315
|
+
if (list.length === 0) return;
|
|
316
|
+
rows.push({
|
|
317
|
+
kind: "group",
|
|
318
|
+
key: `group:${label}`,
|
|
319
|
+
label: `${label} · ${list.length}`
|
|
320
|
+
});
|
|
321
|
+
rows.push(...list);
|
|
322
|
+
};
|
|
323
|
+
group(t("menu.local"), branches.local.filter((branch) => !branch.remoteOnly && named(branch.name)).map((branch) => ({
|
|
324
|
+
kind: "branch",
|
|
325
|
+
key: `local:${branch.name}`,
|
|
326
|
+
label: branch.name,
|
|
327
|
+
name: branch.name
|
|
328
|
+
})));
|
|
329
|
+
group(t("menu.remote"), branches.local.filter((branch) => branch.remoteOnly && named(branch.name)).map((branch) => ({
|
|
330
|
+
kind: "branch",
|
|
331
|
+
key: `remote:${branch.name}`,
|
|
332
|
+
label: branch.name,
|
|
333
|
+
name: branch.name
|
|
334
|
+
})));
|
|
335
|
+
return rows;
|
|
336
|
+
}
|
|
337
|
+
/** Fixed-position placeholder for the frame before the anchor is measured. */
|
|
338
|
+
const hidingStyle = {
|
|
339
|
+
position: "fixed",
|
|
340
|
+
left: 0,
|
|
341
|
+
bottom: 0,
|
|
342
|
+
visibility: "hidden"
|
|
343
|
+
};
|
|
344
|
+
/** Design width of the popover card. */
|
|
345
|
+
const PANEL_WIDTH = 268;
|
|
346
|
+
/** Distance kept between the chip's top edge and the popover. */
|
|
347
|
+
const GAP = 8;
|
|
348
|
+
/** Distance kept between the popover and each viewport edge. */
|
|
349
|
+
const MARGIN = 12;
|
|
350
|
+
/** Height cap on a tall window, and the floor that keeps the card usable. */
|
|
351
|
+
const PANEL_MAX_HEIGHT = 420;
|
|
352
|
+
const PANEL_MIN_HEIGHT = 180;
|
|
353
|
+
const anchorStyle = { display: "inline-flex" };
|
|
354
|
+
const pillStyle = {
|
|
355
|
+
gap: 6,
|
|
356
|
+
maxWidth: 240
|
|
357
|
+
};
|
|
358
|
+
const nameStyle = {
|
|
359
|
+
overflow: "hidden",
|
|
360
|
+
textOverflow: "ellipsis",
|
|
361
|
+
whiteSpace: "nowrap"
|
|
362
|
+
};
|
|
363
|
+
const panelStyle = {
|
|
364
|
+
position: "fixed",
|
|
365
|
+
zIndex: 1100,
|
|
366
|
+
display: "flex",
|
|
367
|
+
flexDirection: "column",
|
|
368
|
+
width: PANEL_WIDTH,
|
|
369
|
+
background: "var(--dsw-specific-menu, var(--dsw-alias-bg-layer-1, #ffffff))",
|
|
370
|
+
backdropFilter: "var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%))",
|
|
371
|
+
WebkitBackdropFilter: "var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%))",
|
|
372
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.25))",
|
|
373
|
+
borderRadius: 16,
|
|
374
|
+
boxShadow: "var(--dsw-elevation-prominent, 0 12px 32px rgba(0,0,0,0.16))",
|
|
375
|
+
overflow: "hidden"
|
|
376
|
+
};
|
|
377
|
+
const searchRowStyle = {
|
|
378
|
+
flex: "0 0 auto",
|
|
379
|
+
padding: "8px 8px 6px",
|
|
380
|
+
borderBottom: "1px solid var(--dsw-alias-border-l3, rgba(127,127,127,0.16))"
|
|
381
|
+
};
|
|
382
|
+
const searchInputStyle = { width: "100%" };
|
|
383
|
+
const listStyle$1 = {
|
|
384
|
+
flex: "1 1 auto",
|
|
385
|
+
minHeight: 0,
|
|
386
|
+
overflowY: "auto",
|
|
387
|
+
padding: 4
|
|
388
|
+
};
|
|
389
|
+
const actionsStyle$2 = {
|
|
390
|
+
flex: "0 0 auto",
|
|
391
|
+
padding: 4,
|
|
392
|
+
borderTop: "1px solid var(--dsw-alias-border-l3, rgba(127,127,127,0.16))"
|
|
393
|
+
};
|
|
394
|
+
const rowStyle = {
|
|
395
|
+
display: "flex",
|
|
396
|
+
alignItems: "center",
|
|
397
|
+
justifyContent: "space-between",
|
|
398
|
+
gap: 8,
|
|
399
|
+
width: "100%",
|
|
400
|
+
padding: "6px 8px",
|
|
401
|
+
border: "none",
|
|
402
|
+
borderRadius: 8,
|
|
403
|
+
background: "transparent",
|
|
404
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
405
|
+
font: "inherit",
|
|
406
|
+
fontSize: 13,
|
|
407
|
+
textAlign: "left",
|
|
408
|
+
cursor: "pointer"
|
|
409
|
+
};
|
|
410
|
+
const rowHoverStyle$1 = { background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.10))" };
|
|
411
|
+
const rowNameStyle = {
|
|
412
|
+
overflow: "hidden",
|
|
413
|
+
textOverflow: "ellipsis",
|
|
414
|
+
whiteSpace: "nowrap"
|
|
415
|
+
};
|
|
416
|
+
const groupStyle = {
|
|
417
|
+
padding: "6px 8px 2px",
|
|
418
|
+
fontSize: 11,
|
|
419
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)"
|
|
420
|
+
};
|
|
421
|
+
const emptyStyle = {
|
|
422
|
+
padding: "10px 8px",
|
|
423
|
+
fontSize: 12,
|
|
424
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)"
|
|
425
|
+
};
|
|
426
|
+
const actionStyle = {
|
|
427
|
+
display: "flex",
|
|
428
|
+
alignItems: "center",
|
|
429
|
+
gap: 8,
|
|
430
|
+
width: "100%",
|
|
431
|
+
padding: "6px 8px",
|
|
432
|
+
border: "none",
|
|
433
|
+
borderRadius: 8,
|
|
434
|
+
background: "transparent",
|
|
435
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
436
|
+
font: "inherit",
|
|
437
|
+
fontSize: 13,
|
|
438
|
+
textAlign: "left",
|
|
439
|
+
cursor: "pointer"
|
|
440
|
+
};
|
|
441
|
+
const actionIconStyle = {
|
|
442
|
+
display: "inline-flex",
|
|
443
|
+
flex: "0 0 auto"
|
|
444
|
+
};
|
|
445
|
+
const disabledStyle = {
|
|
446
|
+
opacity: .45,
|
|
447
|
+
cursor: "default"
|
|
448
|
+
};
|
|
449
|
+
const aheadStyle = {
|
|
450
|
+
display: "inline-flex",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 1,
|
|
453
|
+
flex: "0 0 auto",
|
|
454
|
+
fontSize: 11,
|
|
455
|
+
color: "var(--dsw-alias-label-secondary, currentColor)"
|
|
456
|
+
};
|
|
457
|
+
/** Chip count pill: white glyphs on a state color; the color names the fact. */
|
|
458
|
+
const badgeBase = {
|
|
459
|
+
display: "inline-flex",
|
|
460
|
+
alignItems: "center",
|
|
461
|
+
gap: 2,
|
|
462
|
+
minWidth: 16,
|
|
463
|
+
padding: "0 5px",
|
|
464
|
+
borderRadius: 8,
|
|
465
|
+
fontSize: 11,
|
|
466
|
+
lineHeight: "16px",
|
|
467
|
+
color: "var(--dsw-alias-label-primary-inverted, #ffffff)"
|
|
468
|
+
};
|
|
469
|
+
/** Flow-family tag next to the chip name; neutral so it never competes with the counts. */
|
|
470
|
+
const flowBadgeStyle = {
|
|
471
|
+
flex: "0 0 auto",
|
|
472
|
+
padding: "0 5px",
|
|
473
|
+
borderRadius: 6,
|
|
474
|
+
fontSize: 10,
|
|
475
|
+
lineHeight: "15px",
|
|
476
|
+
color: "var(--dsw-alias-label-secondary, currentColor)",
|
|
477
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.14))"
|
|
478
|
+
};
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/client/api.ts
|
|
481
|
+
const BASE = "/api/dsh-git-flow";
|
|
482
|
+
/** A git operation refused by the host, carrying its structured code. */
|
|
483
|
+
var GitApiError = class extends Error {
|
|
484
|
+
code;
|
|
485
|
+
constructor(code, message) {
|
|
486
|
+
super(message);
|
|
487
|
+
this.code = code;
|
|
488
|
+
this.name = "GitApiError";
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
/** Codes that mean "this session has no git surface"; the chip stays hidden. */
|
|
492
|
+
const SILENT = ["git/not-a-repository", "git/not-installed"];
|
|
493
|
+
/** Whether an error should hide the branch chip instead of reporting itself. */
|
|
494
|
+
function isSilentError(error) {
|
|
495
|
+
return error instanceof GitApiError && SILENT.includes(error.code);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Codes that mean the panel's selection no longer matches the repository: a
|
|
499
|
+
* commit landed, or files were written while the panel was open. The owner
|
|
500
|
+
* re-reads status so the list catches up instead of failing the same way again.
|
|
501
|
+
*/
|
|
502
|
+
const STALE_SELECTION = [
|
|
503
|
+
"git/invalid-input",
|
|
504
|
+
"git/no-files-selected",
|
|
505
|
+
"git/tag-exists",
|
|
506
|
+
"git/gitflow-branch-required"
|
|
507
|
+
];
|
|
508
|
+
/** Whether an error means the checked paths are out of date. */
|
|
509
|
+
function isStaleSelection(error) {
|
|
510
|
+
return error instanceof GitApiError && STALE_SELECTION.includes(error.code);
|
|
511
|
+
}
|
|
512
|
+
/** Codes with dedicated copy; anything else falls back to git's message. */
|
|
513
|
+
const CODED = {
|
|
514
|
+
"git/not-a-repository": "error.notRepository",
|
|
515
|
+
"git/not-installed": "error.notInstalled",
|
|
516
|
+
"git/timeout": "error.timeout",
|
|
517
|
+
"git/dirty-worktree": "checkout.dirtyTitle",
|
|
518
|
+
"git/detached-head": "error.detached",
|
|
519
|
+
"git/invalid-input": "error.invalid",
|
|
520
|
+
"git/no-files-selected": "error.noFiles",
|
|
521
|
+
"git/no-route": "error.noRoute",
|
|
522
|
+
"git/develop-missing": "error.developMissing",
|
|
523
|
+
"git/merge-conflict": "error.mergeConflict",
|
|
524
|
+
"git/tag-exists": "error.tagExists",
|
|
525
|
+
"git/gitflow-branch-required": "error.gitflowBranchRequired"
|
|
526
|
+
};
|
|
527
|
+
/** Human-readable failure text for any thrown value. */
|
|
528
|
+
function errorText(t, error) {
|
|
529
|
+
if (!(error instanceof GitApiError)) return t("error.failed", { detail: String(error) });
|
|
530
|
+
const key = CODED[error.code];
|
|
531
|
+
return key === void 0 ? t("error.failed", { detail: error.message }) : t(key, { detail: error.message });
|
|
532
|
+
}
|
|
533
|
+
async function request(path, init) {
|
|
534
|
+
let response;
|
|
535
|
+
try {
|
|
536
|
+
response = await fetch(`${BASE}${path}`, init);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
throw new GitApiError("git/failed", String(error));
|
|
539
|
+
}
|
|
540
|
+
let payload;
|
|
541
|
+
try {
|
|
542
|
+
payload = await response.json();
|
|
543
|
+
} catch {
|
|
544
|
+
throw new GitApiError("git/failed", `${response.status} ${response.statusText}`);
|
|
545
|
+
}
|
|
546
|
+
if (!payload.ok) throw new GitApiError(payload.code, payload.message);
|
|
547
|
+
return payload.data;
|
|
548
|
+
}
|
|
549
|
+
function post(path, body) {
|
|
550
|
+
return request(path, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
headers: { "content-type": "application/json" },
|
|
553
|
+
body: JSON.stringify(body)
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/** One session-scoped query string. */
|
|
557
|
+
function query(sessionId) {
|
|
558
|
+
return `sessionId=${encodeURIComponent(sessionId)}`;
|
|
559
|
+
}
|
|
560
|
+
/** The browser-side git API. */
|
|
561
|
+
const gitApi = {
|
|
562
|
+
status: (sessionId) => request(`/status?${query(sessionId)}`),
|
|
563
|
+
branches: (sessionId) => request(`/branches?${query(sessionId)}`),
|
|
564
|
+
checkout: (body) => post("/checkout", body),
|
|
565
|
+
createBranch: (body) => post("/branch", body),
|
|
566
|
+
commit: (body) => post("/commit", body),
|
|
567
|
+
discard: (body) => post("/discard", body),
|
|
568
|
+
push: (body) => post("/push", body),
|
|
569
|
+
pull: (body) => post("/pull", body),
|
|
570
|
+
generateMessage: (body) => post("/generate-message", body),
|
|
571
|
+
gitflowState: (sessionId) => request(`/gitflow?${query(sessionId)}`),
|
|
572
|
+
startFlow: (body) => post("/gitflow/start", body),
|
|
573
|
+
initDevelop: (body) => post("/gitflow/init-develop", body),
|
|
574
|
+
finishFlow: (body) => post("/gitflow/finish", body)
|
|
575
|
+
};
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/client/CommitDialog.tsx
|
|
578
|
+
/**
|
|
579
|
+
* Small modal surfaces: the commit composer, the new-branch prompt, and the
|
|
580
|
+
* second confirmation a dirty worktree needs before a forced switch.
|
|
581
|
+
* Every mutation goes through `/api/dsh-git-flow/*`, which names only the session.
|
|
582
|
+
*/
|
|
583
|
+
/** Custom property and storage key behind the drag-to-resize width. */
|
|
584
|
+
const DIALOG_WIDTH_VAR = "--dsh-git-flow-dialog-width";
|
|
585
|
+
const DIALOG_WIDTH_KEY = "dsh-git-flow:dialog-width";
|
|
586
|
+
/** Bounds for the dragged width, in px. */
|
|
587
|
+
const WIDTH_MIN = 380;
|
|
588
|
+
const WIDTH_MAX = 1100;
|
|
589
|
+
/** Languages the panel offers, in display order. */
|
|
590
|
+
const LANGUAGES = ["en", "zh"];
|
|
591
|
+
/** Endonyms: each option reads correctly in either UI language. */
|
|
592
|
+
const LANGUAGE_LABEL = {
|
|
593
|
+
en: "English",
|
|
594
|
+
zh: "中文"
|
|
595
|
+
};
|
|
596
|
+
/** Where the panel remembers the choice; the host config remains the default. */
|
|
597
|
+
const LANGUAGE_KEY = "dsh-git-flow:message-language";
|
|
598
|
+
/** Stored preference, falling back to the configured default (English). */
|
|
599
|
+
function readLanguage() {
|
|
600
|
+
try {
|
|
601
|
+
const stored = window.localStorage.getItem(LANGUAGE_KEY);
|
|
602
|
+
if (stored === "zh" || stored === "en") return stored;
|
|
603
|
+
} catch {}
|
|
604
|
+
return "en";
|
|
605
|
+
}
|
|
606
|
+
/** Commit composer: pick paths, draft or write the message, commit, optionally push. */
|
|
607
|
+
function CommitDialog(props) {
|
|
608
|
+
const { t, sessionId, status, onClose } = props;
|
|
609
|
+
const rows = status.files;
|
|
610
|
+
const stageable = rows.filter((file) => !file.conflicted);
|
|
611
|
+
const [chosen, setChosen] = (0, react.useState)(() => new Set(stageable.map((file) => file.path)));
|
|
612
|
+
const [message, setMessage] = (0, react.useState)("");
|
|
613
|
+
const [generating, setGenerating] = (0, react.useState)(false);
|
|
614
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
615
|
+
const [language, setLanguage] = (0, react.useState)(readLanguage);
|
|
616
|
+
/** Paths awaiting the destructive-action confirmation. */
|
|
617
|
+
const [discard, setDiscard] = (0, react.useState)(void 0);
|
|
618
|
+
const paths = stageable.filter((file) => chosen.has(file.path)).map((file) => file.path);
|
|
619
|
+
const allSelected = stageable.length > 0 && paths.length === stageable.length;
|
|
620
|
+
const signature = stageable.map((file) => file.path).join("\n");
|
|
621
|
+
(0, react.useEffect)(() => {
|
|
622
|
+
setChosen(new Set(stageable.map((file) => file.path)));
|
|
623
|
+
}, [signature]);
|
|
624
|
+
const chooseLanguage = (next) => {
|
|
625
|
+
setLanguage(next);
|
|
626
|
+
try {
|
|
627
|
+
window.localStorage.setItem(LANGUAGE_KEY, next);
|
|
628
|
+
} catch {}
|
|
629
|
+
};
|
|
630
|
+
const toggle = (path) => {
|
|
631
|
+
setChosen((current) => {
|
|
632
|
+
const next = new Set(current);
|
|
633
|
+
if (next.delete(path)) return next;
|
|
634
|
+
next.add(path);
|
|
635
|
+
return next;
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
const generate = async () => {
|
|
639
|
+
if (paths.length === 0) {
|
|
640
|
+
props.notify(t("error.noFiles"), true);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
setGenerating(true);
|
|
644
|
+
try {
|
|
645
|
+
const draft = await gitApi.generateMessage({
|
|
646
|
+
sessionId,
|
|
647
|
+
files: paths,
|
|
648
|
+
language
|
|
649
|
+
});
|
|
650
|
+
setMessage(draft.message);
|
|
651
|
+
if (draft.language !== language) props.notify(t("commit.staleHost"), true);
|
|
652
|
+
else if (draft.fallback) props.notify(draft.reason === void 0 ? t("commit.fallback") : t("commit.fallbackReason", { detail: draft.reason }), true);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
props.notify(errorText(t, error), true);
|
|
655
|
+
if (isStaleSelection(error)) props.refresh();
|
|
656
|
+
} finally {
|
|
657
|
+
setGenerating(false);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
/** Push what is already committed, without making a new commit. */
|
|
661
|
+
const pushOnly = async () => {
|
|
662
|
+
setBusy(true);
|
|
663
|
+
try {
|
|
664
|
+
await gitApi.push({ sessionId });
|
|
665
|
+
props.notify(t("push.done", { branch: status.head }));
|
|
666
|
+
props.refresh();
|
|
667
|
+
onClose();
|
|
668
|
+
} catch (error) {
|
|
669
|
+
props.notify(errorText(t, error), true);
|
|
670
|
+
} finally {
|
|
671
|
+
setBusy(false);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const submit = async (push) => {
|
|
675
|
+
if (paths.length === 0) {
|
|
676
|
+
props.notify(t("error.noFiles"), true);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (message.trim().length === 0) {
|
|
680
|
+
props.notify(t("commit.needMessage"), true);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
setBusy(true);
|
|
684
|
+
try {
|
|
685
|
+
const committed = await gitApi.commit({
|
|
686
|
+
sessionId,
|
|
687
|
+
files: paths,
|
|
688
|
+
message: message.trim()
|
|
689
|
+
});
|
|
690
|
+
if (!push) {
|
|
691
|
+
props.notify(t("commit.done", { hash: committed.shortHash }));
|
|
692
|
+
props.refresh();
|
|
693
|
+
onClose();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
await gitApi.push({ sessionId });
|
|
698
|
+
props.notify(t("commit.pushDone", { hash: committed.shortHash }));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
props.notify(`${t("commit.pushFail", { hash: committed.shortHash })} · ${errorText(t, error)}`, true);
|
|
701
|
+
}
|
|
702
|
+
props.refresh();
|
|
703
|
+
onClose();
|
|
704
|
+
} catch (error) {
|
|
705
|
+
props.notify(errorText(t, error), true);
|
|
706
|
+
if (isStaleSelection(error)) props.refresh();
|
|
707
|
+
} finally {
|
|
708
|
+
setBusy(false);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
712
|
+
open: true,
|
|
713
|
+
onClose,
|
|
714
|
+
className: "dsh-git-flow-dialog",
|
|
715
|
+
title: t("commit.title"),
|
|
716
|
+
description: t("commit.description"),
|
|
717
|
+
closeLabel: t("commit.cancel"),
|
|
718
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
719
|
+
style: footerStyle,
|
|
720
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
721
|
+
style: metaStyle,
|
|
722
|
+
children: [t("commit.selected", { count: paths.length }), status.upstream === void 0 && ` · ${t("commit.noUpstream")}`]
|
|
723
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
724
|
+
style: actionsStyle$1,
|
|
725
|
+
children: [
|
|
726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
727
|
+
size: "sm",
|
|
728
|
+
variant: "ghost",
|
|
729
|
+
onClick: () => {
|
|
730
|
+
pushOnly();
|
|
731
|
+
},
|
|
732
|
+
disabled: busy || generating,
|
|
733
|
+
children: t("commit.push")
|
|
734
|
+
}),
|
|
735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
736
|
+
size: "sm",
|
|
737
|
+
variant: "outline",
|
|
738
|
+
onClick: () => {
|
|
739
|
+
submit(true);
|
|
740
|
+
},
|
|
741
|
+
disabled: busy || generating,
|
|
742
|
+
children: t("commit.commitPush")
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
745
|
+
size: "sm",
|
|
746
|
+
variant: "primary",
|
|
747
|
+
onClick: () => {
|
|
748
|
+
submit(false);
|
|
749
|
+
},
|
|
750
|
+
disabled: busy || generating,
|
|
751
|
+
children: t("commit.commit")
|
|
752
|
+
})
|
|
753
|
+
]
|
|
754
|
+
})]
|
|
755
|
+
}),
|
|
756
|
+
children: [
|
|
757
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResizeHandle, { label: t("commit.resize") }),
|
|
758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
759
|
+
style: sectionStyle$1,
|
|
760
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
761
|
+
style: rowHeaderStyle,
|
|
762
|
+
children: [
|
|
763
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
764
|
+
style: headingStyle$1,
|
|
765
|
+
children: t("commit.message")
|
|
766
|
+
}),
|
|
767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
768
|
+
label: t("commit.messageLanguage"),
|
|
769
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
+
style: languageStyle,
|
|
771
|
+
children: LANGUAGES.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
772
|
+
active: language === id,
|
|
773
|
+
disabled: generating || busy,
|
|
774
|
+
onClick: () => {
|
|
775
|
+
chooseLanguage(id);
|
|
776
|
+
},
|
|
777
|
+
children: LANGUAGE_LABEL[id]
|
|
778
|
+
}, id))
|
|
779
|
+
})
|
|
780
|
+
}),
|
|
781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
782
|
+
size: "sm",
|
|
783
|
+
variant: "ghost",
|
|
784
|
+
icon: message.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkleRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }),
|
|
785
|
+
onClick: () => {
|
|
786
|
+
generate();
|
|
787
|
+
},
|
|
788
|
+
disabled: generating || busy,
|
|
789
|
+
children: message.length === 0 ? t("commit.generate") : t("commit.regenerate")
|
|
790
|
+
}),
|
|
791
|
+
generating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
792
|
+
style: metaStyle,
|
|
793
|
+
children: t("commit.generating")
|
|
794
|
+
})
|
|
795
|
+
]
|
|
796
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
797
|
+
style: textareaStyle,
|
|
798
|
+
value: message,
|
|
799
|
+
placeholder: t("commit.messagePlaceholder"),
|
|
800
|
+
rows: 5,
|
|
801
|
+
spellCheck: false,
|
|
802
|
+
onChange: (event) => setMessage(event.target.value)
|
|
803
|
+
})]
|
|
804
|
+
}),
|
|
805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
806
|
+
style: sectionStyle$1,
|
|
807
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
808
|
+
style: rowHeaderStyle,
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
811
|
+
style: headingStyle$1,
|
|
812
|
+
children: t("commit.files")
|
|
813
|
+
}),
|
|
814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
815
|
+
style: metaStyle,
|
|
816
|
+
children: t("commit.selected", { count: paths.length })
|
|
817
|
+
}),
|
|
818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
819
|
+
checked: allSelected,
|
|
820
|
+
onChange: (next) => setChosen(next ? new Set(stageable.map((file) => file.path)) : /* @__PURE__ */ new Set()),
|
|
821
|
+
label: t("commit.selectAll", { count: stageable.length }),
|
|
822
|
+
disabled: busy || stageable.length === 0
|
|
823
|
+
})
|
|
824
|
+
]
|
|
825
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
826
|
+
style: listStyle,
|
|
827
|
+
children: [rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
828
|
+
style: emptyListStyle,
|
|
829
|
+
children: t("commit.none")
|
|
830
|
+
}), rows.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileRow, {
|
|
831
|
+
path: filePath(file),
|
|
832
|
+
code: fileCode(file),
|
|
833
|
+
tone: fileTone(file),
|
|
834
|
+
title: file.conflicted ? t("status.conflict") : file.path,
|
|
835
|
+
checked: chosen.has(file.path),
|
|
836
|
+
disabled: file.conflicted || busy,
|
|
837
|
+
discardLabel: t("commit.discardOne", { path: file.path }),
|
|
838
|
+
onToggle: () => {
|
|
839
|
+
toggle(file.path);
|
|
840
|
+
},
|
|
841
|
+
onDiscard: () => {
|
|
842
|
+
setDiscard([file.path]);
|
|
843
|
+
}
|
|
844
|
+
}, file.path))]
|
|
845
|
+
})]
|
|
846
|
+
})
|
|
847
|
+
]
|
|
848
|
+
}), discard !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiscardDialog, {
|
|
849
|
+
t,
|
|
850
|
+
sessionId,
|
|
851
|
+
files: discard,
|
|
852
|
+
notify: props.notify,
|
|
853
|
+
refresh: props.refresh,
|
|
854
|
+
onClose: () => {
|
|
855
|
+
setDiscard(void 0);
|
|
856
|
+
}
|
|
857
|
+
})] });
|
|
858
|
+
}
|
|
859
|
+
/** One changed path: checkbox, single-line path, status badge, discard action. */
|
|
860
|
+
function FileRow(props) {
|
|
861
|
+
const [hover, setHover] = (0, react.useState)(false);
|
|
862
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
863
|
+
className: "dsh-git-flow-file-row",
|
|
864
|
+
style: {
|
|
865
|
+
...fileRowStyle,
|
|
866
|
+
...hover && !props.disabled ? rowHoverStyle : null
|
|
867
|
+
},
|
|
868
|
+
onPointerEnter: () => {
|
|
869
|
+
setHover(true);
|
|
870
|
+
},
|
|
871
|
+
onPointerLeave: () => {
|
|
872
|
+
setHover(false);
|
|
873
|
+
},
|
|
874
|
+
children: [
|
|
875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
876
|
+
className: "dsh-git-flow-file-label",
|
|
877
|
+
checked: props.checked,
|
|
878
|
+
onChange: props.onToggle,
|
|
879
|
+
label: props.path,
|
|
880
|
+
title: props.title,
|
|
881
|
+
disabled: props.disabled
|
|
882
|
+
}),
|
|
883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
884
|
+
style: {
|
|
885
|
+
...badgeStyle,
|
|
886
|
+
color: props.tone
|
|
887
|
+
},
|
|
888
|
+
children: props.code
|
|
889
|
+
}),
|
|
890
|
+
!props.disabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
891
|
+
label: props.discardLabel,
|
|
892
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
893
|
+
type: "button",
|
|
894
|
+
className: "dsh-git-flow-file-action",
|
|
895
|
+
style: iconButtonStyle,
|
|
896
|
+
"aria-label": props.discardLabel,
|
|
897
|
+
onClick: props.onDiscard,
|
|
898
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular, { size: 14 })
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
]
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Right-edge gripper for the dialog card. The card owns its width through the
|
|
906
|
+
* `--dsh-git-flow-dialog-width` custom property (see styles.ts), so the drag
|
|
907
|
+
* only has to write that property; the chosen width is remembered per browser.
|
|
908
|
+
*/
|
|
909
|
+
function ResizeHandle(props) {
|
|
910
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
911
|
+
(0, react.useEffect)(() => {
|
|
912
|
+
const stored = window.localStorage.getItem(DIALOG_WIDTH_KEY);
|
|
913
|
+
if (stored !== null) document.documentElement.style.setProperty(DIALOG_WIDTH_VAR, stored);
|
|
914
|
+
}, []);
|
|
915
|
+
const onPointerDown = (event) => {
|
|
916
|
+
const handle = event.currentTarget;
|
|
917
|
+
const card = handle.closest(".dsh-git-flow-dialog");
|
|
918
|
+
if (!(card instanceof HTMLElement)) return;
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
const startX = event.clientX;
|
|
921
|
+
const startWidth = card.offsetWidth;
|
|
922
|
+
const target = handle;
|
|
923
|
+
target.setPointerCapture(event.pointerId);
|
|
924
|
+
setDragging(true);
|
|
925
|
+
let width = startWidth;
|
|
926
|
+
const move = (moveEvent) => {
|
|
927
|
+
const limit = Math.min(WIDTH_MAX, window.innerWidth - 48);
|
|
928
|
+
width = Math.min(Math.max(startWidth + (moveEvent.clientX - startX), WIDTH_MIN), Math.max(WIDTH_MIN, limit));
|
|
929
|
+
document.documentElement.style.setProperty(DIALOG_WIDTH_VAR, `${width}px`);
|
|
930
|
+
};
|
|
931
|
+
const up = () => {
|
|
932
|
+
target.releasePointerCapture(event.pointerId);
|
|
933
|
+
target.removeEventListener("pointermove", move);
|
|
934
|
+
target.removeEventListener("pointerup", up);
|
|
935
|
+
target.removeEventListener("pointercancel", up);
|
|
936
|
+
setDragging(false);
|
|
937
|
+
window.localStorage.setItem(DIALOG_WIDTH_KEY, `${width}px`);
|
|
938
|
+
};
|
|
939
|
+
target.addEventListener("pointermove", move);
|
|
940
|
+
target.addEventListener("pointerup", up);
|
|
941
|
+
target.addEventListener("pointercancel", up);
|
|
942
|
+
};
|
|
943
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
944
|
+
label: props.label,
|
|
945
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
946
|
+
className: "dsh-git-flow-resize",
|
|
947
|
+
role: "separator",
|
|
948
|
+
"aria-label": props.label,
|
|
949
|
+
"aria-orientation": "vertical",
|
|
950
|
+
"data-dragging": dragging ? "true" : void 0,
|
|
951
|
+
onPointerDown,
|
|
952
|
+
onDoubleClick: () => {
|
|
953
|
+
document.documentElement.style.removeProperty(DIALOG_WIDTH_VAR);
|
|
954
|
+
window.localStorage.removeItem(DIALOG_WIDTH_KEY);
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Confirmation for a destructive discard. Nothing is sent until the user picks
|
|
961
|
+
* the destructive action here.
|
|
962
|
+
*/
|
|
963
|
+
function DiscardDialog(props) {
|
|
964
|
+
const { t, sessionId, files, onClose } = props;
|
|
965
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
966
|
+
const discard = async () => {
|
|
967
|
+
setBusy(true);
|
|
968
|
+
try {
|
|
969
|
+
await gitApi.discard({
|
|
970
|
+
sessionId,
|
|
971
|
+
files
|
|
972
|
+
});
|
|
973
|
+
props.notify(t("commit.discardDone", { count: files.length }));
|
|
974
|
+
props.refresh();
|
|
975
|
+
onClose();
|
|
976
|
+
} catch (error) {
|
|
977
|
+
props.notify(errorText(t, error), true);
|
|
978
|
+
setBusy(false);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
982
|
+
open: true,
|
|
983
|
+
onClose,
|
|
984
|
+
title: t("commit.discardTitle"),
|
|
985
|
+
description: t("commit.discardBody", { count: files.length }),
|
|
986
|
+
closeLabel: t("commit.cancel"),
|
|
987
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
988
|
+
style: actionsStyle$1,
|
|
989
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
990
|
+
size: "sm",
|
|
991
|
+
onClick: onClose,
|
|
992
|
+
disabled: busy,
|
|
993
|
+
children: t("commit.cancel")
|
|
994
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
995
|
+
size: "sm",
|
|
996
|
+
variant: "primary",
|
|
997
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, { size: 14 }),
|
|
998
|
+
onClick: () => {
|
|
999
|
+
discard();
|
|
1000
|
+
},
|
|
1001
|
+
disabled: busy,
|
|
1002
|
+
children: t("commit.discard")
|
|
1003
|
+
})]
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/** Ask for a branch name, create it from the current HEAD, and switch to it. */
|
|
1008
|
+
function CreateBranchDialog(props) {
|
|
1009
|
+
const { t, sessionId, status, onClose } = props;
|
|
1010
|
+
const [name, setName] = (0, react.useState)("");
|
|
1011
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1012
|
+
const create = async () => {
|
|
1013
|
+
setBusy(true);
|
|
1014
|
+
try {
|
|
1015
|
+
await gitApi.createBranch({
|
|
1016
|
+
sessionId,
|
|
1017
|
+
name: name.trim()
|
|
1018
|
+
});
|
|
1019
|
+
props.notify(t("create.done", { branch: name.trim() }));
|
|
1020
|
+
props.refresh();
|
|
1021
|
+
onClose();
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
props.notify(errorText(t, error), true);
|
|
1024
|
+
setBusy(false);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1028
|
+
open: true,
|
|
1029
|
+
onClose,
|
|
1030
|
+
title: t("create.title"),
|
|
1031
|
+
description: t("create.description", { branch: status.head }),
|
|
1032
|
+
closeLabel: t("commit.cancel"),
|
|
1033
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1034
|
+
style: actionsStyle$1,
|
|
1035
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1036
|
+
size: "sm",
|
|
1037
|
+
onClick: onClose,
|
|
1038
|
+
disabled: busy,
|
|
1039
|
+
children: t("commit.cancel")
|
|
1040
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1041
|
+
size: "sm",
|
|
1042
|
+
variant: "primary",
|
|
1043
|
+
onClick: () => {
|
|
1044
|
+
create();
|
|
1045
|
+
},
|
|
1046
|
+
disabled: busy || name.trim().length === 0,
|
|
1047
|
+
children: t("create.submit")
|
|
1048
|
+
})]
|
|
1049
|
+
}),
|
|
1050
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1051
|
+
style: sectionStyle$1,
|
|
1052
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1053
|
+
style: headingStyle$1,
|
|
1054
|
+
children: t("create.name")
|
|
1055
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1056
|
+
value: name,
|
|
1057
|
+
placeholder: t("create.placeholder"),
|
|
1058
|
+
autoFocus: true,
|
|
1059
|
+
style: branchInputStyle,
|
|
1060
|
+
onChange: (event) => setName(event.target.value)
|
|
1061
|
+
})]
|
|
1062
|
+
})
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* A switch that git refused because it would overwrite local edits. The first
|
|
1067
|
+
* attempt runs without `force`; only this second confirmation sends it.
|
|
1068
|
+
*/
|
|
1069
|
+
function ForceCheckoutDialog(props) {
|
|
1070
|
+
const { t, sessionId, branch, onClose } = props;
|
|
1071
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1072
|
+
const force = async () => {
|
|
1073
|
+
setBusy(true);
|
|
1074
|
+
try {
|
|
1075
|
+
await gitApi.checkout({
|
|
1076
|
+
sessionId,
|
|
1077
|
+
name: branch,
|
|
1078
|
+
force: true
|
|
1079
|
+
});
|
|
1080
|
+
props.notify(t("checkout.done", { branch }));
|
|
1081
|
+
props.refresh();
|
|
1082
|
+
onClose();
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
props.notify(errorText(t, error), true);
|
|
1085
|
+
setBusy(false);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1089
|
+
open: true,
|
|
1090
|
+
onClose,
|
|
1091
|
+
title: t("checkout.dirtyTitle"),
|
|
1092
|
+
description: t("checkout.dirtyBody", { branch }),
|
|
1093
|
+
closeLabel: t("commit.cancel"),
|
|
1094
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1095
|
+
style: actionsStyle$1,
|
|
1096
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1097
|
+
size: "sm",
|
|
1098
|
+
onClick: onClose,
|
|
1099
|
+
disabled: busy,
|
|
1100
|
+
children: t("commit.cancel")
|
|
1101
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1102
|
+
size: "sm",
|
|
1103
|
+
variant: "primary",
|
|
1104
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, { size: 14 }),
|
|
1105
|
+
onClick: () => {
|
|
1106
|
+
force();
|
|
1107
|
+
},
|
|
1108
|
+
disabled: busy,
|
|
1109
|
+
children: t("checkout.force")
|
|
1110
|
+
})]
|
|
1111
|
+
})
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/** Display path, showing both sides of a rename. */
|
|
1115
|
+
function filePath(file) {
|
|
1116
|
+
return file.oldPath === void 0 ? file.path : `${file.oldPath} → ${file.path}`;
|
|
1117
|
+
}
|
|
1118
|
+
/** Two-letter porcelain code, collapsed for display. */
|
|
1119
|
+
function fileCode(file) {
|
|
1120
|
+
if (file.conflicted) return "U";
|
|
1121
|
+
if (file.untracked) return "?";
|
|
1122
|
+
const code = `${file.indexStatus}${file.worktreeStatus}`;
|
|
1123
|
+
return code === ".." ? "·" : code;
|
|
1124
|
+
}
|
|
1125
|
+
const sectionStyle$1 = {
|
|
1126
|
+
display: "grid",
|
|
1127
|
+
gap: 8,
|
|
1128
|
+
minWidth: 0
|
|
1129
|
+
};
|
|
1130
|
+
const rowHeaderStyle = {
|
|
1131
|
+
display: "flex",
|
|
1132
|
+
alignItems: "center",
|
|
1133
|
+
gap: 10,
|
|
1134
|
+
minWidth: 0
|
|
1135
|
+
};
|
|
1136
|
+
const languageStyle = {
|
|
1137
|
+
display: "inline-flex",
|
|
1138
|
+
alignItems: "center",
|
|
1139
|
+
gap: 4,
|
|
1140
|
+
flex: "0 0 auto"
|
|
1141
|
+
};
|
|
1142
|
+
const headingStyle$1 = {
|
|
1143
|
+
fontSize: 12,
|
|
1144
|
+
color: "var(--dsw-alias-label-secondary, currentColor)",
|
|
1145
|
+
flex: "0 0 auto"
|
|
1146
|
+
};
|
|
1147
|
+
const metaStyle = {
|
|
1148
|
+
fontSize: 12,
|
|
1149
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)",
|
|
1150
|
+
flex: "0 0 auto"
|
|
1151
|
+
};
|
|
1152
|
+
const listStyle = {
|
|
1153
|
+
display: "grid",
|
|
1154
|
+
gap: 1,
|
|
1155
|
+
maxHeight: 216,
|
|
1156
|
+
overflowY: "auto",
|
|
1157
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.25))",
|
|
1158
|
+
borderRadius: 10,
|
|
1159
|
+
padding: 4
|
|
1160
|
+
};
|
|
1161
|
+
const emptyListStyle = {
|
|
1162
|
+
padding: "10px 8px",
|
|
1163
|
+
fontSize: 12,
|
|
1164
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)"
|
|
1165
|
+
};
|
|
1166
|
+
const fileRowStyle = {
|
|
1167
|
+
display: "flex",
|
|
1168
|
+
alignItems: "center",
|
|
1169
|
+
justifyContent: "space-between",
|
|
1170
|
+
gap: 8,
|
|
1171
|
+
minWidth: 0,
|
|
1172
|
+
padding: "3px 6px",
|
|
1173
|
+
borderRadius: 6
|
|
1174
|
+
};
|
|
1175
|
+
const rowHoverStyle = { background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.10))" };
|
|
1176
|
+
const iconButtonStyle = {
|
|
1177
|
+
display: "inline-flex",
|
|
1178
|
+
alignItems: "center",
|
|
1179
|
+
justifyContent: "center",
|
|
1180
|
+
flex: "0 0 auto",
|
|
1181
|
+
width: 22,
|
|
1182
|
+
height: 22,
|
|
1183
|
+
padding: 0,
|
|
1184
|
+
border: "none",
|
|
1185
|
+
borderRadius: 6,
|
|
1186
|
+
background: "transparent",
|
|
1187
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)",
|
|
1188
|
+
cursor: "pointer"
|
|
1189
|
+
};
|
|
1190
|
+
const badgeStyle = {
|
|
1191
|
+
flex: "0 0 auto",
|
|
1192
|
+
minWidth: 22,
|
|
1193
|
+
padding: "0 5px",
|
|
1194
|
+
border: "1px solid currentColor",
|
|
1195
|
+
borderRadius: 6,
|
|
1196
|
+
fontFamily: "var(--dsw-font-mono, ui-monospace, monospace)",
|
|
1197
|
+
fontSize: 10,
|
|
1198
|
+
lineHeight: "16px",
|
|
1199
|
+
textAlign: "center",
|
|
1200
|
+
opacity: .9
|
|
1201
|
+
};
|
|
1202
|
+
const footerStyle = {
|
|
1203
|
+
display: "flex",
|
|
1204
|
+
alignItems: "center",
|
|
1205
|
+
justifyContent: "space-between",
|
|
1206
|
+
gap: 12,
|
|
1207
|
+
width: "100%"
|
|
1208
|
+
};
|
|
1209
|
+
const actionsStyle$1 = {
|
|
1210
|
+
display: "flex",
|
|
1211
|
+
alignItems: "center",
|
|
1212
|
+
gap: 8,
|
|
1213
|
+
justifyContent: "flex-end"
|
|
1214
|
+
};
|
|
1215
|
+
const branchInputStyle = { width: "100%" };
|
|
1216
|
+
const textareaStyle = {
|
|
1217
|
+
width: "100%",
|
|
1218
|
+
resize: "vertical",
|
|
1219
|
+
minHeight: 96,
|
|
1220
|
+
fontFamily: "var(--dsw-font-mono, ui-monospace, monospace)",
|
|
1221
|
+
fontSize: 13,
|
|
1222
|
+
lineHeight: 1.5,
|
|
1223
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
1224
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
1225
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.25))",
|
|
1226
|
+
borderRadius: 8,
|
|
1227
|
+
padding: "8px 10px",
|
|
1228
|
+
outline: "none"
|
|
1229
|
+
};
|
|
1230
|
+
/** Codes that add content read as success, removals as warning, the rest neutral. */
|
|
1231
|
+
function fileTone(file) {
|
|
1232
|
+
const code = fileCode(file);
|
|
1233
|
+
if (code === "U") return "var(--dsw-alias-state-error-primary, currentColor)";
|
|
1234
|
+
if (code === "?" || code[1] === "A" || code === "AM") return "var(--dsw-alias-state-success-primary, currentColor)";
|
|
1235
|
+
if (code === "D" || code[1] === "D") return "var(--dsw-alias-state-warn-primary, currentColor)";
|
|
1236
|
+
return "var(--dsw-alias-label-tertiary, currentColor)";
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/client/GitFlowDialog.tsx
|
|
1240
|
+
/**
|
|
1241
|
+
* The three git-flow modal surfaces: start a flow branch, guide the first
|
|
1242
|
+
* `develop` into existence, and finish a flow branch.
|
|
1243
|
+
*
|
|
1244
|
+
* The finish dialog is the one destructive surface here, so it lists every step
|
|
1245
|
+
* of the run before the user confirms: merging, tagging, versioning, pushing,
|
|
1246
|
+
* and deleting the branch all happen without a second prompt afterwards.
|
|
1247
|
+
*/
|
|
1248
|
+
/** Flow families in the order the start dialog offers them. */
|
|
1249
|
+
const KINDS = [
|
|
1250
|
+
"feature",
|
|
1251
|
+
"release",
|
|
1252
|
+
"hotfix"
|
|
1253
|
+
];
|
|
1254
|
+
/** Copy key naming one family. */
|
|
1255
|
+
function kindKey(kind) {
|
|
1256
|
+
return `flow.${kind}`;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Name a flow branch and create it from the branch its family starts at.
|
|
1260
|
+
* A missing develop is not an error the user can fix in this dialog, so it
|
|
1261
|
+
* closes and hands off to the guided creation instead.
|
|
1262
|
+
*/
|
|
1263
|
+
function StartFlowDialog(props) {
|
|
1264
|
+
const { t, sessionId, state, status, onClose } = props;
|
|
1265
|
+
const [kind, setKind] = (0, react.useState)("feature");
|
|
1266
|
+
const [name, setName] = (0, react.useState)("");
|
|
1267
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1268
|
+
const prefix = kind === "feature" ? state.branches.featurePrefix : kind === "release" ? state.branches.releasePrefix : state.branches.hotfixPrefix;
|
|
1269
|
+
const base = kind === "hotfix" ? state.branches.production : state.branches.develop;
|
|
1270
|
+
const blank = name.trim().length === 0;
|
|
1271
|
+
const start = async () => {
|
|
1272
|
+
if (blank) {
|
|
1273
|
+
props.notify(t("start.needName"), true);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
setBusy(true);
|
|
1277
|
+
try {
|
|
1278
|
+
const next = await gitApi.startFlow({
|
|
1279
|
+
sessionId,
|
|
1280
|
+
kind,
|
|
1281
|
+
name: name.trim()
|
|
1282
|
+
});
|
|
1283
|
+
props.notify(t("start.done", { branch: next.head }));
|
|
1284
|
+
props.refresh();
|
|
1285
|
+
onClose();
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
if (error instanceof GitApiError && error.code === "git/develop-missing") {
|
|
1288
|
+
onClose();
|
|
1289
|
+
props.onNeedDevelop();
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
props.notify(errorText(t, error), true);
|
|
1293
|
+
setBusy(false);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1297
|
+
open: true,
|
|
1298
|
+
onClose,
|
|
1299
|
+
title: t("start.title"),
|
|
1300
|
+
description: t("start.description", {
|
|
1301
|
+
develop: state.branches.develop,
|
|
1302
|
+
production: state.branches.production
|
|
1303
|
+
}),
|
|
1304
|
+
closeLabel: t("commit.cancel"),
|
|
1305
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1306
|
+
style: actionsStyle,
|
|
1307
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1308
|
+
size: "sm",
|
|
1309
|
+
onClick: onClose,
|
|
1310
|
+
disabled: busy,
|
|
1311
|
+
children: t("commit.cancel")
|
|
1312
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1313
|
+
size: "sm",
|
|
1314
|
+
variant: "primary",
|
|
1315
|
+
onClick: () => {
|
|
1316
|
+
start();
|
|
1317
|
+
},
|
|
1318
|
+
disabled: busy || blank,
|
|
1319
|
+
children: t("start.submit")
|
|
1320
|
+
})]
|
|
1321
|
+
}),
|
|
1322
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1323
|
+
style: sectionStyle,
|
|
1324
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1325
|
+
style: headingStyle,
|
|
1326
|
+
children: t("start.kind")
|
|
1327
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1328
|
+
style: pillRowStyle,
|
|
1329
|
+
children: KINDS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
1330
|
+
active: kind === id,
|
|
1331
|
+
disabled: busy,
|
|
1332
|
+
onClick: () => {
|
|
1333
|
+
setKind(id);
|
|
1334
|
+
},
|
|
1335
|
+
children: t(kindKey(id))
|
|
1336
|
+
}, id))
|
|
1337
|
+
})]
|
|
1338
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1339
|
+
style: sectionStyle,
|
|
1340
|
+
children: [
|
|
1341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1342
|
+
style: headingStyle,
|
|
1343
|
+
children: t("start.name")
|
|
1344
|
+
}),
|
|
1345
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1346
|
+
value: name,
|
|
1347
|
+
placeholder: t("start.placeholder", { prefix }),
|
|
1348
|
+
autoFocus: true,
|
|
1349
|
+
style: inputStyle,
|
|
1350
|
+
disabled: busy,
|
|
1351
|
+
onChange: (event) => setName(event.target.value),
|
|
1352
|
+
onKeyDown: (event) => {
|
|
1353
|
+
if (event.key === "Enter") start();
|
|
1354
|
+
}
|
|
1355
|
+
}),
|
|
1356
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1357
|
+
style: hintStyle,
|
|
1358
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
|
|
1359
|
+
style: codeStyle,
|
|
1360
|
+
children: [prefix, name.trim() || "…"]
|
|
1361
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("start.base", { base }) })]
|
|
1362
|
+
}),
|
|
1363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1364
|
+
style: hintStyle,
|
|
1365
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("start.versionHint") }), status.detached && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["· ", t("error.detached")] })]
|
|
1366
|
+
})
|
|
1367
|
+
]
|
|
1368
|
+
})]
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/** Create the integration branch from production (or HEAD when there is none). */
|
|
1372
|
+
function InitDevelopDialog(props) {
|
|
1373
|
+
const { t, sessionId, state, status, onClose } = props;
|
|
1374
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1375
|
+
const from = state.hasProduction ? state.branches.production : status.head;
|
|
1376
|
+
const create = async () => {
|
|
1377
|
+
setBusy(true);
|
|
1378
|
+
try {
|
|
1379
|
+
await gitApi.initDevelop({ sessionId });
|
|
1380
|
+
props.notify(t("initDevelop.done", { develop: state.branches.develop }));
|
|
1381
|
+
props.refresh();
|
|
1382
|
+
onClose();
|
|
1383
|
+
props.onDone();
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
props.notify(errorText(t, error), true);
|
|
1386
|
+
setBusy(false);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1390
|
+
open: true,
|
|
1391
|
+
onClose,
|
|
1392
|
+
title: t("initDevelop.title", { develop: state.branches.develop }),
|
|
1393
|
+
description: t("initDevelop.description", {
|
|
1394
|
+
develop: state.branches.develop,
|
|
1395
|
+
from
|
|
1396
|
+
}),
|
|
1397
|
+
closeLabel: t("commit.cancel"),
|
|
1398
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1399
|
+
style: actionsStyle,
|
|
1400
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1401
|
+
size: "sm",
|
|
1402
|
+
onClick: onClose,
|
|
1403
|
+
disabled: busy,
|
|
1404
|
+
children: t("commit.cancel")
|
|
1405
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1406
|
+
size: "sm",
|
|
1407
|
+
variant: "primary",
|
|
1408
|
+
onClick: () => {
|
|
1409
|
+
create();
|
|
1410
|
+
},
|
|
1411
|
+
disabled: busy,
|
|
1412
|
+
children: t("initDevelop.submit", { develop: state.branches.develop })
|
|
1413
|
+
})]
|
|
1414
|
+
})
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Confirm and run the finish sequence. The step list is generated from the same
|
|
1419
|
+
* facts the host will act on, so what the user reads is what will happen.
|
|
1420
|
+
*/
|
|
1421
|
+
function FinishFlowDialog(props) {
|
|
1422
|
+
const { t, sessionId, state, status, onClose } = props;
|
|
1423
|
+
const isRelease = (state.kind ?? "feature") !== "feature";
|
|
1424
|
+
const targets = isRelease ? [state.branches.production, state.branches.develop] : [state.branches.develop];
|
|
1425
|
+
const [version, setVersion] = (0, react.useState)(() => state.version ?? "");
|
|
1426
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1427
|
+
const tag = isRelease && version.trim().length > 0 ? `${state.branches.tagPrefix}${version.trim()}` : state.tagName;
|
|
1428
|
+
const finish = async () => {
|
|
1429
|
+
setBusy(true);
|
|
1430
|
+
try {
|
|
1431
|
+
const done = await gitApi.finishFlow({
|
|
1432
|
+
sessionId,
|
|
1433
|
+
version: isRelease && version.trim().length > 0 ? version.trim() : void 0
|
|
1434
|
+
});
|
|
1435
|
+
if (done.failure !== void 0) props.notify(t("finish.partial", {
|
|
1436
|
+
branch: done.branch,
|
|
1437
|
+
detail: done.failure.detail
|
|
1438
|
+
}), true);
|
|
1439
|
+
else {
|
|
1440
|
+
const parts = [t("finish.merged", { targets: done.mergedInto.join(", ") })];
|
|
1441
|
+
if (done.tag !== void 0) parts.push(t("finish.tagCreated", { tag: done.tag }));
|
|
1442
|
+
if (done.deleted) parts.push(t("finish.deleted"));
|
|
1443
|
+
props.notify(`${t("finish.done", { branch: done.branch })} · ${parts.join(" · ")}`);
|
|
1444
|
+
}
|
|
1445
|
+
props.refresh();
|
|
1446
|
+
onClose();
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
props.notify(errorText(t, error), true);
|
|
1449
|
+
setBusy(false);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1453
|
+
open: true,
|
|
1454
|
+
onClose,
|
|
1455
|
+
title: t("finish.title", { branch: state.head }),
|
|
1456
|
+
description: t("finish.description"),
|
|
1457
|
+
closeLabel: t("commit.cancel"),
|
|
1458
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1459
|
+
style: actionsStyle,
|
|
1460
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1461
|
+
size: "sm",
|
|
1462
|
+
onClick: onClose,
|
|
1463
|
+
disabled: busy,
|
|
1464
|
+
children: t("commit.cancel")
|
|
1465
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1466
|
+
size: "sm",
|
|
1467
|
+
variant: "primary",
|
|
1468
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, { size: 14 }),
|
|
1469
|
+
onClick: () => {
|
|
1470
|
+
finish();
|
|
1471
|
+
},
|
|
1472
|
+
disabled: busy || status.files.length > 0 || state.tagExists,
|
|
1473
|
+
children: t("finish.submit")
|
|
1474
|
+
})]
|
|
1475
|
+
}),
|
|
1476
|
+
children: [
|
|
1477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
1478
|
+
style: sectionStyle,
|
|
1479
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
|
|
1480
|
+
style: stepsStyle,
|
|
1481
|
+
children: [
|
|
1482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("finish.mergeInto", { targets: targets.join(", ") }) }),
|
|
1483
|
+
isRelease && tag !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("finish.tag", { tag }) }),
|
|
1484
|
+
isRelease && version.trim().length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("finish.version", { version: version.trim() }) }),
|
|
1485
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("finish.push") }),
|
|
1486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("finish.delete") })
|
|
1487
|
+
]
|
|
1488
|
+
})
|
|
1489
|
+
}),
|
|
1490
|
+
isRelease && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1491
|
+
style: sectionStyle,
|
|
1492
|
+
children: [
|
|
1493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1494
|
+
style: headingStyle,
|
|
1495
|
+
children: t("finish.versionLabel")
|
|
1496
|
+
}),
|
|
1497
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1498
|
+
value: version,
|
|
1499
|
+
placeholder: "0.4.0",
|
|
1500
|
+
style: inputStyle,
|
|
1501
|
+
disabled: busy,
|
|
1502
|
+
onChange: (event) => setVersion(event.target.value)
|
|
1503
|
+
}),
|
|
1504
|
+
state.tagExists && tag !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1505
|
+
style: { ...warnStyle },
|
|
1506
|
+
children: t("finish.tagExists", { tag })
|
|
1507
|
+
})
|
|
1508
|
+
]
|
|
1509
|
+
}),
|
|
1510
|
+
status.files.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1511
|
+
style: warnStyle,
|
|
1512
|
+
children: t("checkout.dirtyTitle")
|
|
1513
|
+
})
|
|
1514
|
+
]
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const sectionStyle = {
|
|
1518
|
+
display: "grid",
|
|
1519
|
+
gap: 8,
|
|
1520
|
+
minWidth: 0
|
|
1521
|
+
};
|
|
1522
|
+
const headingStyle = {
|
|
1523
|
+
fontSize: 12,
|
|
1524
|
+
color: "var(--dsw-alias-label-secondary, currentColor)"
|
|
1525
|
+
};
|
|
1526
|
+
const pillRowStyle = {
|
|
1527
|
+
display: "flex",
|
|
1528
|
+
alignItems: "center",
|
|
1529
|
+
gap: 6,
|
|
1530
|
+
flexWrap: "wrap"
|
|
1531
|
+
};
|
|
1532
|
+
const inputStyle = { width: "100%" };
|
|
1533
|
+
const hintStyle = {
|
|
1534
|
+
display: "flex",
|
|
1535
|
+
flexWrap: "wrap",
|
|
1536
|
+
alignItems: "center",
|
|
1537
|
+
gap: 6,
|
|
1538
|
+
fontSize: 12,
|
|
1539
|
+
color: "var(--dsw-alias-label-tertiary, currentColor)"
|
|
1540
|
+
};
|
|
1541
|
+
const codeStyle = {
|
|
1542
|
+
padding: "0 4px",
|
|
1543
|
+
borderRadius: 4,
|
|
1544
|
+
fontFamily: "var(--dsw-font-mono, ui-monospace, monospace)",
|
|
1545
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.12))"
|
|
1546
|
+
};
|
|
1547
|
+
const stepsStyle = {
|
|
1548
|
+
display: "grid",
|
|
1549
|
+
gap: 6,
|
|
1550
|
+
margin: 0,
|
|
1551
|
+
paddingLeft: 18,
|
|
1552
|
+
fontSize: 13,
|
|
1553
|
+
lineHeight: 1.5,
|
|
1554
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
1555
|
+
};
|
|
1556
|
+
const warnStyle = {
|
|
1557
|
+
padding: "8px 10px",
|
|
1558
|
+
borderRadius: 8,
|
|
1559
|
+
fontSize: 12,
|
|
1560
|
+
color: "var(--dsw-alias-state-warn-primary, #b45309)",
|
|
1561
|
+
background: "var(--dsw-alias-state-warn-bg, rgba(245,158,11,0.12))"
|
|
1562
|
+
};
|
|
1563
|
+
const actionsStyle = {
|
|
1564
|
+
display: "flex",
|
|
1565
|
+
alignItems: "center",
|
|
1566
|
+
gap: 8,
|
|
1567
|
+
justifyContent: "flex-end"
|
|
1568
|
+
};
|
|
1569
|
+
//#endregion
|
|
1570
|
+
//#region src/client/BranchChip.tsx
|
|
1571
|
+
/**
|
|
1572
|
+
* The composer-tool-row entry: owns the session's git status, the branch menu,
|
|
1573
|
+
* the three dialog surfaces, and the toast. Everything below it is presentational.
|
|
1574
|
+
*/
|
|
1575
|
+
/** Branch chip for the session's workspace; renders nothing outside a repository. */
|
|
1576
|
+
function GitFlowChip({ sessionId, t }) {
|
|
1577
|
+
const [status, setStatus] = (0, react.useState)(void 0);
|
|
1578
|
+
const [hidden, setHidden] = (0, react.useState)(false);
|
|
1579
|
+
const [branches, setBranches] = (0, react.useState)(void 0);
|
|
1580
|
+
const [flow, setFlow] = (0, react.useState)(void 0);
|
|
1581
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1582
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1583
|
+
const [dialog, setDialog] = (0, react.useState)(void 0);
|
|
1584
|
+
const [banner, setBanner] = (0, react.useState)(void 0);
|
|
1585
|
+
const notify = (0, react.useCallback)((text, failed = false) => {
|
|
1586
|
+
setBanner({
|
|
1587
|
+
seq: Date.now() + Math.random(),
|
|
1588
|
+
text,
|
|
1589
|
+
failed
|
|
1590
|
+
});
|
|
1591
|
+
}, []);
|
|
1592
|
+
const loadStatus = (0, react.useCallback)(async () => {
|
|
1593
|
+
try {
|
|
1594
|
+
setStatus(await gitApi.status(sessionId));
|
|
1595
|
+
setHidden(false);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
if (isSilentError(error)) {
|
|
1598
|
+
setStatus(void 0);
|
|
1599
|
+
setHidden(true);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
setHidden(false);
|
|
1603
|
+
notify(errorText(t, error), true);
|
|
1604
|
+
}
|
|
1605
|
+
}, [
|
|
1606
|
+
sessionId,
|
|
1607
|
+
t,
|
|
1608
|
+
notify
|
|
1609
|
+
]);
|
|
1610
|
+
(0, react.useEffect)(() => {
|
|
1611
|
+
loadStatus();
|
|
1612
|
+
}, [loadStatus]);
|
|
1613
|
+
(0, react.useEffect)(() => {
|
|
1614
|
+
if (!open) return;
|
|
1615
|
+
loadStatus();
|
|
1616
|
+
(async () => {
|
|
1617
|
+
try {
|
|
1618
|
+
setBranches(await gitApi.branches(sessionId));
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
notify(errorText(t, error), true);
|
|
1621
|
+
}
|
|
1622
|
+
try {
|
|
1623
|
+
setFlow(await gitApi.gitflowState(sessionId));
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
if (!isSilentError(error)) notify(errorText(t, error), true);
|
|
1626
|
+
}
|
|
1627
|
+
})();
|
|
1628
|
+
}, [
|
|
1629
|
+
open,
|
|
1630
|
+
sessionId,
|
|
1631
|
+
t,
|
|
1632
|
+
notify,
|
|
1633
|
+
loadStatus
|
|
1634
|
+
]);
|
|
1635
|
+
const refresh = (0, react.useCallback)(() => {
|
|
1636
|
+
loadStatus();
|
|
1637
|
+
(async () => {
|
|
1638
|
+
try {
|
|
1639
|
+
setFlow(await gitApi.gitflowState(sessionId));
|
|
1640
|
+
} catch {}
|
|
1641
|
+
})();
|
|
1642
|
+
}, [loadStatus, sessionId]);
|
|
1643
|
+
/** Push the current branch's unpushed commits, straight from the menu. */
|
|
1644
|
+
const pushCommits = async () => {
|
|
1645
|
+
setBusy(true);
|
|
1646
|
+
try {
|
|
1647
|
+
await gitApi.push({ sessionId });
|
|
1648
|
+
notify(t("push.done", { branch: status?.head ?? "" }));
|
|
1649
|
+
refresh();
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
notify(errorText(t, error), true);
|
|
1652
|
+
} finally {
|
|
1653
|
+
setBusy(false);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
/** Pull the current branch's unmerged commits, straight from the menu. */
|
|
1657
|
+
const pullRemote = async () => {
|
|
1658
|
+
setBusy(true);
|
|
1659
|
+
try {
|
|
1660
|
+
await gitApi.pull({ sessionId });
|
|
1661
|
+
notify(t("pull.done", { branch: status?.head ?? "" }));
|
|
1662
|
+
refresh();
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
notify(errorText(t, error), true);
|
|
1665
|
+
} finally {
|
|
1666
|
+
setBusy(false);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
const checkout = async (name) => {
|
|
1670
|
+
if (name === status?.head) return;
|
|
1671
|
+
setBusy(true);
|
|
1672
|
+
try {
|
|
1673
|
+
const next = await gitApi.checkout({
|
|
1674
|
+
sessionId,
|
|
1675
|
+
name
|
|
1676
|
+
});
|
|
1677
|
+
setStatus(next);
|
|
1678
|
+
notify(t("checkout.done", { branch: next.head }));
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
if (error instanceof GitApiError && error.code === "git/dirty-worktree") setDialog({
|
|
1681
|
+
kind: "force",
|
|
1682
|
+
branch: name
|
|
1683
|
+
});
|
|
1684
|
+
else notify(errorText(t, error), true);
|
|
1685
|
+
} finally {
|
|
1686
|
+
setBusy(false);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
if (hidden || status === void 0) return null;
|
|
1690
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchMenu, {
|
|
1692
|
+
t,
|
|
1693
|
+
status,
|
|
1694
|
+
branches,
|
|
1695
|
+
busy,
|
|
1696
|
+
open,
|
|
1697
|
+
onOpenChange: setOpen,
|
|
1698
|
+
onSelectBranch: (name) => {
|
|
1699
|
+
checkout(name);
|
|
1700
|
+
},
|
|
1701
|
+
onNewBranch: () => {
|
|
1702
|
+
setDialog({ kind: "new" });
|
|
1703
|
+
},
|
|
1704
|
+
onCommit: () => {
|
|
1705
|
+
refresh();
|
|
1706
|
+
setDialog({ kind: "commit" });
|
|
1707
|
+
},
|
|
1708
|
+
onPush: () => {
|
|
1709
|
+
pushCommits();
|
|
1710
|
+
},
|
|
1711
|
+
onPull: () => {
|
|
1712
|
+
pullRemote();
|
|
1713
|
+
},
|
|
1714
|
+
onRefresh: () => {
|
|
1715
|
+
setBranches(void 0);
|
|
1716
|
+
refresh();
|
|
1717
|
+
},
|
|
1718
|
+
flow,
|
|
1719
|
+
onStartFlow: () => {
|
|
1720
|
+
refresh();
|
|
1721
|
+
setDialog({ kind: "start" });
|
|
1722
|
+
},
|
|
1723
|
+
onFinishFlow: () => {
|
|
1724
|
+
setDialog({ kind: "finish" });
|
|
1725
|
+
}
|
|
1726
|
+
}),
|
|
1727
|
+
dialog?.kind === "commit" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommitDialog, {
|
|
1728
|
+
t,
|
|
1729
|
+
sessionId,
|
|
1730
|
+
status,
|
|
1731
|
+
notify,
|
|
1732
|
+
refresh,
|
|
1733
|
+
onClose: () => {
|
|
1734
|
+
setDialog(void 0);
|
|
1735
|
+
}
|
|
1736
|
+
}),
|
|
1737
|
+
dialog?.kind === "new" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreateBranchDialog, {
|
|
1738
|
+
t,
|
|
1739
|
+
sessionId,
|
|
1740
|
+
status,
|
|
1741
|
+
notify,
|
|
1742
|
+
refresh,
|
|
1743
|
+
onClose: () => {
|
|
1744
|
+
setDialog(void 0);
|
|
1745
|
+
}
|
|
1746
|
+
}),
|
|
1747
|
+
dialog?.kind === "force" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ForceCheckoutDialog, {
|
|
1748
|
+
t,
|
|
1749
|
+
sessionId,
|
|
1750
|
+
branch: dialog.branch,
|
|
1751
|
+
notify,
|
|
1752
|
+
refresh,
|
|
1753
|
+
onClose: () => {
|
|
1754
|
+
setDialog(void 0);
|
|
1755
|
+
}
|
|
1756
|
+
}),
|
|
1757
|
+
dialog?.kind === "start" && flow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StartFlowDialog, {
|
|
1758
|
+
t,
|
|
1759
|
+
sessionId,
|
|
1760
|
+
state: flow,
|
|
1761
|
+
status,
|
|
1762
|
+
notify,
|
|
1763
|
+
refresh,
|
|
1764
|
+
onClose: () => {
|
|
1765
|
+
setDialog(void 0);
|
|
1766
|
+
},
|
|
1767
|
+
onNeedDevelop: () => {
|
|
1768
|
+
setDialog({
|
|
1769
|
+
kind: "init-develop",
|
|
1770
|
+
thenStart: true
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
}),
|
|
1774
|
+
dialog?.kind === "finish" && flow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FinishFlowDialog, {
|
|
1775
|
+
t,
|
|
1776
|
+
sessionId,
|
|
1777
|
+
state: flow,
|
|
1778
|
+
status,
|
|
1779
|
+
notify,
|
|
1780
|
+
refresh,
|
|
1781
|
+
onClose: () => {
|
|
1782
|
+
setDialog(void 0);
|
|
1783
|
+
}
|
|
1784
|
+
}),
|
|
1785
|
+
dialog?.kind === "init-develop" && flow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InitDevelopDialog, {
|
|
1786
|
+
t,
|
|
1787
|
+
sessionId,
|
|
1788
|
+
state: flow,
|
|
1789
|
+
status,
|
|
1790
|
+
notify,
|
|
1791
|
+
refresh,
|
|
1792
|
+
onClose: () => {
|
|
1793
|
+
setDialog(void 0);
|
|
1794
|
+
},
|
|
1795
|
+
onDone: () => {
|
|
1796
|
+
if (dialog.thenStart === true) setDialog({ kind: "start" });
|
|
1797
|
+
}
|
|
1798
|
+
}),
|
|
1799
|
+
banner !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1800
|
+
text: banner.text,
|
|
1801
|
+
icon: banner.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, { size: 14 }) : void 0,
|
|
1802
|
+
onDone: () => {
|
|
1803
|
+
setBanner(void 0);
|
|
1804
|
+
}
|
|
1805
|
+
}, banner.seq)
|
|
1806
|
+
] });
|
|
1807
|
+
}
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/client/locales.ts
|
|
1810
|
+
/**
|
|
1811
|
+
* Browser copy for the git surface. Both dictionaries carry the same keys;
|
|
1812
|
+
* `en` is typed against `zh` so a missing key is a compile error.
|
|
1813
|
+
*/
|
|
1814
|
+
const GIT_FLOW_NS = "gitFlow";
|
|
1815
|
+
/** Both locales, in the shape `ctx.locale.register` validates. */
|
|
1816
|
+
const gitFlowLocale = {
|
|
1817
|
+
zh: {
|
|
1818
|
+
"chip.tooltip": "当前分支 {branch},{count} 个未提交改动",
|
|
1819
|
+
"chip.tooltip.clean": "当前分支 {branch},工作区干净",
|
|
1820
|
+
"chip.aheadBehind": "领先 {ahead},落后 {behind}",
|
|
1821
|
+
"chip.unpushed": "有 {count} 个提交未推送",
|
|
1822
|
+
"menu.search": "搜索分支",
|
|
1823
|
+
"menu.local": "本地分支",
|
|
1824
|
+
"menu.remote": "仅在远端",
|
|
1825
|
+
"menu.empty": "没有匹配的分支",
|
|
1826
|
+
"menu.loading": "读取分支…",
|
|
1827
|
+
"menu.newBranch": "新建分支…",
|
|
1828
|
+
"menu.commit": "提交…",
|
|
1829
|
+
"menu.push": "推送提交",
|
|
1830
|
+
"menu.pull": "拉取远程代码",
|
|
1831
|
+
"menu.refresh": "刷新状态",
|
|
1832
|
+
"menu.startFlow": "启动 Git Flow 分支…",
|
|
1833
|
+
"menu.finishFlow": "完成 Git Flow 分支…",
|
|
1834
|
+
"flow.feature": "功能",
|
|
1835
|
+
"flow.release": "发布",
|
|
1836
|
+
"flow.hotfix": "热修复",
|
|
1837
|
+
"flow.badge": "{kind}",
|
|
1838
|
+
"start.title": "启动 Git Flow 分支",
|
|
1839
|
+
"start.description": "功能与发布分支从 {develop} 切出,热修复从 {production} 切出。",
|
|
1840
|
+
"start.kind": "分支类型",
|
|
1841
|
+
"start.name": "名称",
|
|
1842
|
+
"start.placeholder": "例如 login,将创建 {prefix}login",
|
|
1843
|
+
"start.submit": "创建并切换",
|
|
1844
|
+
"start.done": "已创建并切换到 {branch}",
|
|
1845
|
+
"start.needName": "请先填写名称",
|
|
1846
|
+
"start.base": "将从 {base} 切出",
|
|
1847
|
+
"start.versionHint": "发布与热修复的名称建议带版本号,例如 0.4.0",
|
|
1848
|
+
"initDevelop.title": "还没有 {develop} 分支",
|
|
1849
|
+
"initDevelop.description": "Git Flow 需要一个集成分支。将从 {from} 创建 {develop} 并切换过去。",
|
|
1850
|
+
"initDevelop.submit": "创建 {develop}",
|
|
1851
|
+
"initDevelop.done": "已创建 {develop}",
|
|
1852
|
+
"finish.title": "完成 {branch}",
|
|
1853
|
+
"finish.description": "以下动作会依次执行,合并提交与标签一旦创建就不会回滚。",
|
|
1854
|
+
"finish.mergeInto": "合并到 {targets}(--no-ff)",
|
|
1855
|
+
"finish.tag": "创建标签 {tag}",
|
|
1856
|
+
"finish.version": "把 package.json 版本改为 {version}",
|
|
1857
|
+
"finish.push": "推送分支与标签到 origin",
|
|
1858
|
+
"finish.delete": "删除已完成的本地分支",
|
|
1859
|
+
"finish.versionLabel": "版本号",
|
|
1860
|
+
"finish.submit": "完成并合并",
|
|
1861
|
+
"finish.tagExists": "标签 {tag} 已存在,请先改用其它版本号",
|
|
1862
|
+
"finish.done": "已完成 {branch}",
|
|
1863
|
+
"finish.partial": "{branch} 已在本地完成,但推送失败:{detail}",
|
|
1864
|
+
"finish.merged": "已合并到 {targets}",
|
|
1865
|
+
"finish.tagCreated": "已创建标签 {tag}",
|
|
1866
|
+
"finish.versionWritten": "已提交版本号 {version}",
|
|
1867
|
+
"finish.deleted": "已删除本地分支",
|
|
1868
|
+
"finish.pushFail": "推送失败:{detail}",
|
|
1869
|
+
"checkout.done": "已切换到 {branch}",
|
|
1870
|
+
"checkout.dirtyTitle": "工作区有未提交改动",
|
|
1871
|
+
"checkout.dirtyBody": "切换到 {branch} 会覆盖本地改动。可以先提交,或者强制切换(`git switch --force` 会丢弃全部本地改动)。",
|
|
1872
|
+
"checkout.force": "强制切换",
|
|
1873
|
+
"create.title": "新建分支",
|
|
1874
|
+
"create.description": "基于当前分支 {branch} 创建并切换。",
|
|
1875
|
+
"create.name": "分支名称",
|
|
1876
|
+
"create.placeholder": "例如 feature/git-flow",
|
|
1877
|
+
"create.submit": "创建并切换",
|
|
1878
|
+
"create.done": "已创建并切换到 {branch}",
|
|
1879
|
+
"commit.title": "提交改动",
|
|
1880
|
+
"commit.description": "只提交勾选的文件;提交信息可由模型生成,也可以自己写。",
|
|
1881
|
+
"commit.files": "改动文件",
|
|
1882
|
+
"commit.selectAll": "全部 {count} 个文件",
|
|
1883
|
+
"commit.message": "提交信息",
|
|
1884
|
+
"commit.messageLanguage": "提交信息语言",
|
|
1885
|
+
"commit.messagePlaceholder": "第一行写成 feat: 描述,空一行后可以补充要点",
|
|
1886
|
+
"commit.generate": "生成提交信息",
|
|
1887
|
+
"commit.regenerate": "重新生成",
|
|
1888
|
+
"commit.generating": "正在生成…",
|
|
1889
|
+
"commit.push": "推送",
|
|
1890
|
+
"commit.commit": "提交",
|
|
1891
|
+
"commit.commitPush": "提交并推送",
|
|
1892
|
+
"commit.cancel": "取消",
|
|
1893
|
+
"commit.discard": "放弃更改",
|
|
1894
|
+
"commit.discardTitle": "放弃这些文件的改动?",
|
|
1895
|
+
"commit.discardBody": "将把 {count} 个文件恢复到 HEAD:已跟踪的改动被丢弃,未跟踪的文件会被删除。此操作无法撤销。",
|
|
1896
|
+
"commit.discardDone": "已放弃 {count} 个文件的改动",
|
|
1897
|
+
"commit.discardOne": "放弃「{path}」的改动",
|
|
1898
|
+
"commit.resize": "拖动调整面板宽度",
|
|
1899
|
+
"commit.none": "还没有勾选文件",
|
|
1900
|
+
"commit.needMessage": "请先填写提交信息",
|
|
1901
|
+
"commit.selected": "已选 {count} 个文件",
|
|
1902
|
+
"commit.noUpstream": "当前分支还没有上游,首次推送会自动建立",
|
|
1903
|
+
"commit.fallback": "模型没有给出可用信息,已填入模板,可自行修改",
|
|
1904
|
+
"commit.staleHost": "语言选择没生效:宿主进程还是旧版本,重启 dsh web 后再试",
|
|
1905
|
+
"commit.fallbackReason": "模型没有给出可用信息({detail}),已填入模板,可自行修改",
|
|
1906
|
+
"commit.done": "已提交 {hash}",
|
|
1907
|
+
"commit.pushDone": "已提交并推送 {hash}",
|
|
1908
|
+
"commit.pushFail": "已提交 {hash},但推送失败",
|
|
1909
|
+
"push.done": "已推送 {branch}",
|
|
1910
|
+
"pull.done": "已拉取 {branch}",
|
|
1911
|
+
"status.clean": "工作区干净",
|
|
1912
|
+
"status.conflict": "有冲突文件",
|
|
1913
|
+
"error.notRepository": "当前工作区不是 git 仓库",
|
|
1914
|
+
"error.notInstalled": "本机没有可用的 git 命令",
|
|
1915
|
+
"error.timeout": "git 命令超时",
|
|
1916
|
+
"error.noRoute": "这个会话还没有模型路由,先发出一轮对话再试",
|
|
1917
|
+
"error.detached": "当前处于游离 HEAD,先切到一个分支再操作",
|
|
1918
|
+
"error.noFiles": "请至少勾选一个文件",
|
|
1919
|
+
"error.invalid": "输入不合法:{detail}",
|
|
1920
|
+
"error.failed": "操作失败:{detail}",
|
|
1921
|
+
"error.developMissing": "仓库里还没有 develop 分支,请先创建",
|
|
1922
|
+
"error.mergeConflict": "合并时出现冲突,请先解决冲突再完成分支",
|
|
1923
|
+
"error.tagExists": "标签已经存在:{detail}",
|
|
1924
|
+
"error.gitflowBranchRequired": "当前分支不是功能、发布或热修复分支"
|
|
1925
|
+
},
|
|
1926
|
+
en: {
|
|
1927
|
+
"chip.tooltip": "Branch {branch}, {count} uncommitted changes",
|
|
1928
|
+
"chip.tooltip.clean": "Branch {branch}, working tree clean",
|
|
1929
|
+
"chip.aheadBehind": "{ahead} ahead, {behind} behind",
|
|
1930
|
+
"chip.unpushed": "{count} commits not pushed",
|
|
1931
|
+
"menu.search": "Search branches",
|
|
1932
|
+
"menu.local": "Local branches",
|
|
1933
|
+
"menu.remote": "Remote only",
|
|
1934
|
+
"menu.empty": "No matching branch",
|
|
1935
|
+
"menu.loading": "Loading branches…",
|
|
1936
|
+
"menu.newBranch": "New branch…",
|
|
1937
|
+
"menu.commit": "Commit…",
|
|
1938
|
+
"menu.push": "Push commits",
|
|
1939
|
+
"menu.pull": "Pull from remote",
|
|
1940
|
+
"menu.refresh": "Refresh status",
|
|
1941
|
+
"menu.startFlow": "Start git flow branch…",
|
|
1942
|
+
"menu.finishFlow": "Finish git flow branch…",
|
|
1943
|
+
"flow.feature": "Feature",
|
|
1944
|
+
"flow.release": "Release",
|
|
1945
|
+
"flow.hotfix": "Hotfix",
|
|
1946
|
+
"flow.badge": "{kind}",
|
|
1947
|
+
"start.title": "Start a git flow branch",
|
|
1948
|
+
"start.description": "Features and releases branch off {develop}; hotfixes branch off {production}.",
|
|
1949
|
+
"start.kind": "Branch type",
|
|
1950
|
+
"start.name": "Name",
|
|
1951
|
+
"start.placeholder": "e.g. login, which creates {prefix}login",
|
|
1952
|
+
"start.submit": "Create and switch",
|
|
1953
|
+
"start.done": "Created and switched to {branch}",
|
|
1954
|
+
"start.needName": "Write a name first",
|
|
1955
|
+
"start.base": "branches off {base}",
|
|
1956
|
+
"start.versionHint": "Release and hotfix names should carry a version, e.g. 0.4.0",
|
|
1957
|
+
"initDevelop.title": "No {develop} branch yet",
|
|
1958
|
+
"initDevelop.description": "A git flow needs an integration branch. {develop} will be created from {from} and checked out.",
|
|
1959
|
+
"initDevelop.submit": "Create {develop}",
|
|
1960
|
+
"initDevelop.done": "Created {develop}",
|
|
1961
|
+
"finish.title": "Finish {branch}",
|
|
1962
|
+
"finish.description": "These steps run in order. Once a merge commit or tag exists it is not rolled back.",
|
|
1963
|
+
"finish.mergeInto": "Merge into {targets} (--no-ff)",
|
|
1964
|
+
"finish.tag": "Create tag {tag}",
|
|
1965
|
+
"finish.version": "Set package.json version to {version}",
|
|
1966
|
+
"finish.push": "Push the branches and tag to origin",
|
|
1967
|
+
"finish.delete": "Delete the finished local branch",
|
|
1968
|
+
"finish.versionLabel": "Version",
|
|
1969
|
+
"finish.submit": "Finish and merge",
|
|
1970
|
+
"finish.tagExists": "Tag {tag} already exists; pick another version first",
|
|
1971
|
+
"finish.done": "Finished {branch}",
|
|
1972
|
+
"finish.partial": "{branch} finished locally, but the push failed: {detail}",
|
|
1973
|
+
"finish.merged": "Merged into {targets}",
|
|
1974
|
+
"finish.tagCreated": "Created tag {tag}",
|
|
1975
|
+
"finish.versionWritten": "Committed version {version}",
|
|
1976
|
+
"finish.deleted": "Deleted the local branch",
|
|
1977
|
+
"finish.pushFail": "Push failed: {detail}",
|
|
1978
|
+
"checkout.done": "Switched to {branch}",
|
|
1979
|
+
"checkout.dirtyTitle": "You have uncommitted changes",
|
|
1980
|
+
"checkout.dirtyBody": "Switching to {branch} would overwrite local changes. Commit them first, or force the switch and let `git switch --force` discard every local change.",
|
|
1981
|
+
"checkout.force": "Force switch",
|
|
1982
|
+
"create.title": "New branch",
|
|
1983
|
+
"create.description": "Create from the current branch {branch} and switch to it.",
|
|
1984
|
+
"create.name": "Branch name",
|
|
1985
|
+
"create.placeholder": "e.g. feature/git-flow",
|
|
1986
|
+
"create.submit": "Create and switch",
|
|
1987
|
+
"create.done": "Created and switched to {branch}",
|
|
1988
|
+
"commit.title": "Commit changes",
|
|
1989
|
+
"commit.description": "Only the checked files are committed. The message can be drafted by the model or written by you.",
|
|
1990
|
+
"commit.files": "Changed files",
|
|
1991
|
+
"commit.selectAll": "All {count} files",
|
|
1992
|
+
"commit.message": "Commit message",
|
|
1993
|
+
"commit.messageLanguage": "Commit message language",
|
|
1994
|
+
"commit.messagePlaceholder": "Start with `feat: summary`, optionally add bullets after a blank line",
|
|
1995
|
+
"commit.generate": "Draft message",
|
|
1996
|
+
"commit.regenerate": "Regenerate",
|
|
1997
|
+
"commit.generating": "Drafting…",
|
|
1998
|
+
"commit.push": "Push",
|
|
1999
|
+
"commit.commit": "Commit",
|
|
2000
|
+
"commit.commitPush": "Commit and push",
|
|
2001
|
+
"commit.cancel": "Cancel",
|
|
2002
|
+
"commit.discard": "Discard changes",
|
|
2003
|
+
"commit.discardTitle": "Discard these files?",
|
|
2004
|
+
"commit.discardBody": "This restores {count} files to HEAD: tracked edits are dropped and untracked files are deleted. It cannot be undone.",
|
|
2005
|
+
"commit.discardDone": "Discarded changes in {count} files",
|
|
2006
|
+
"commit.discardOne": "Discard changes in “{path}”",
|
|
2007
|
+
"commit.resize": "Drag to resize the panel",
|
|
2008
|
+
"commit.none": "No file selected",
|
|
2009
|
+
"commit.needMessage": "Write a commit message first",
|
|
2010
|
+
"commit.selected": "{count} files selected",
|
|
2011
|
+
"commit.noUpstream": "No upstream yet; the first push creates one",
|
|
2012
|
+
"commit.fallback": "The model returned nothing usable, so a template was filled in",
|
|
2013
|
+
"commit.staleHost": "The language choice did not apply: the host process is older than this page — restart dsh web",
|
|
2014
|
+
"commit.fallbackReason": "The model returned nothing usable ({detail}), so a template was filled in",
|
|
2015
|
+
"commit.done": "Committed {hash}",
|
|
2016
|
+
"commit.pushDone": "Committed and pushed {hash}",
|
|
2017
|
+
"commit.pushFail": "Committed {hash}, but the push failed",
|
|
2018
|
+
"push.done": "Pushed {branch}",
|
|
2019
|
+
"pull.done": "Pulled {branch}",
|
|
2020
|
+
"status.clean": "Working tree clean",
|
|
2021
|
+
"status.conflict": "Conflicted files",
|
|
2022
|
+
"error.notRepository": "This workspace is not a git repository",
|
|
2023
|
+
"error.notInstalled": "git is not available on this machine",
|
|
2024
|
+
"error.timeout": "The git command timed out",
|
|
2025
|
+
"error.noRoute": "This session has no model route yet; send one turn first",
|
|
2026
|
+
"error.detached": "The worktree has a detached HEAD; switch to a branch first",
|
|
2027
|
+
"error.noFiles": "Select at least one file",
|
|
2028
|
+
"error.invalid": "Invalid input: {detail}",
|
|
2029
|
+
"error.failed": "Operation failed: {detail}",
|
|
2030
|
+
"error.developMissing": "This repository has no develop branch yet; create it first",
|
|
2031
|
+
"error.mergeConflict": "The merge conflicted; resolve the conflicts before finishing",
|
|
2032
|
+
"error.tagExists": "That tag already exists: {detail}",
|
|
2033
|
+
"error.gitflowBranchRequired": "This branch is not a feature, release, or hotfix branch"
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region src/client/styles.ts
|
|
2038
|
+
/**
|
|
2039
|
+
* The one stylesheet this plugin needs. Inline styles cannot reach inside the
|
|
2040
|
+
* primitive components (the checkbox labels its own text, the modal card owns
|
|
2041
|
+
* its width), so the few rules that must cross that boundary live here and are
|
|
2042
|
+
* installed once per client fiber.
|
|
2043
|
+
* @module dsh-git-flow/client/styles
|
|
2044
|
+
*/
|
|
2045
|
+
const STYLE_ID = "dsh-git-flow-styles";
|
|
2046
|
+
/**
|
|
2047
|
+
* Card width comes from a custom property so the drag handle can change it at
|
|
2048
|
+
* runtime; unset, it falls back to the modal primitive's own width.
|
|
2049
|
+
*/
|
|
2050
|
+
const CSS = `
|
|
2051
|
+
.dsh-git-flow-dialog {
|
|
2052
|
+
position: relative;
|
|
2053
|
+
width: var(--dsh-git-flow-dialog-width, min(380px, 100%));
|
|
2054
|
+
}
|
|
2055
|
+
.dsh-git-flow-resize {
|
|
2056
|
+
position: absolute;
|
|
2057
|
+
top: 0;
|
|
2058
|
+
right: 0;
|
|
2059
|
+
bottom: 0;
|
|
2060
|
+
width: 14px;
|
|
2061
|
+
cursor: ew-resize;
|
|
2062
|
+
touch-action: none;
|
|
2063
|
+
z-index: 2;
|
|
2064
|
+
}
|
|
2065
|
+
.dsh-git-flow-resize::after {
|
|
2066
|
+
content: '';
|
|
2067
|
+
position: absolute;
|
|
2068
|
+
top: 50%;
|
|
2069
|
+
right: 4px;
|
|
2070
|
+
width: 3px;
|
|
2071
|
+
height: 32px;
|
|
2072
|
+
margin-top: -16px;
|
|
2073
|
+
border-radius: 2px;
|
|
2074
|
+
background: var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.45));
|
|
2075
|
+
opacity: 0;
|
|
2076
|
+
transition: opacity 0.12s ease;
|
|
2077
|
+
}
|
|
2078
|
+
.dsh-git-flow-resize:hover::after,
|
|
2079
|
+
.dsh-git-flow-resize[data-dragging='true']::after {
|
|
2080
|
+
opacity: 1;
|
|
2081
|
+
}
|
|
2082
|
+
.dsh-git-flow-file-label {
|
|
2083
|
+
flex: 1 1 auto;
|
|
2084
|
+
min-width: 0;
|
|
2085
|
+
}
|
|
2086
|
+
.dsh-git-flow-file-label > span {
|
|
2087
|
+
min-width: 0;
|
|
2088
|
+
overflow: hidden;
|
|
2089
|
+
text-overflow: ellipsis;
|
|
2090
|
+
white-space: nowrap;
|
|
2091
|
+
}
|
|
2092
|
+
.dsh-git-flow-file-action {
|
|
2093
|
+
opacity: 0;
|
|
2094
|
+
transition: opacity 0.12s ease;
|
|
2095
|
+
}
|
|
2096
|
+
.dsh-git-flow-file-row:hover .dsh-git-flow-file-action,
|
|
2097
|
+
.dsh-git-flow-file-action:focus-visible {
|
|
2098
|
+
opacity: 1;
|
|
2099
|
+
}
|
|
2100
|
+
`;
|
|
2101
|
+
/**
|
|
2102
|
+
* Install the stylesheet for this fiber.
|
|
2103
|
+
* @returns a disposer removing the style element.
|
|
2104
|
+
*/
|
|
2105
|
+
function installStyles() {
|
|
2106
|
+
if (document.getElementById(STYLE_ID) !== null) return () => {};
|
|
2107
|
+
const element = document.createElement("style");
|
|
2108
|
+
element.id = STYLE_ID;
|
|
2109
|
+
element.textContent = CSS;
|
|
2110
|
+
document.head.appendChild(element);
|
|
2111
|
+
return () => {
|
|
2112
|
+
element.remove();
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/client/index.ts
|
|
2117
|
+
/** Services this fiber needs before it activates. */
|
|
2118
|
+
const inject = ["slots", "locale"];
|
|
2119
|
+
/**
|
|
2120
|
+
* Mount the branch chip and its copy.
|
|
2121
|
+
* @param ctx - client root context.
|
|
2122
|
+
*/
|
|
2123
|
+
function apply(ctx) {
|
|
2124
|
+
ctx.effect(() => ctx.locale.register(GIT_FLOW_NS, gitFlowLocale), "dsh-git-flow: locale dictionary");
|
|
2125
|
+
ctx.effect(installStyles, "dsh-git-flow: stylesheet");
|
|
2126
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
2127
|
+
name: "conversation.input.left",
|
|
2128
|
+
id: "dsh-git-flow",
|
|
2129
|
+
order: 20,
|
|
2130
|
+
locale: GIT_FLOW_NS
|
|
2131
|
+
}, GitFlowChip));
|
|
2132
|
+
}
|
|
2133
|
+
//#endregion
|
|
2134
|
+
exports.apply = apply;
|
|
2135
|
+
exports.inject = inject;
|
|
2136
|
+
return module.exports;
|
|
2137
|
+
}
|
|
2138
|
+
});
|