@dungsil/dsh-plugin-ui-worktree 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -16
- package/dist/client.js +786 -218
- package/dist/index.js +408 -2
- package/locale/en.json +1 -1
- package/locale/ko.json +1 -1
- package/locale/zh.json +6 -0
- package/package.json +37 -6
package/dist/client.js
CHANGED
|
@@ -1,307 +1,868 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-worktree", factory(require) { const module = { exports: {} }; const exports = module.exports;
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
let react = require("react");
|
|
4
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
4
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
// src/protocol.ts
|
|
7
|
+
const channel = "/api";
|
|
8
|
+
const endpointPrefix = "dungsil-worktree/";
|
|
9
|
+
function comparablePath(path) {
|
|
10
|
+
return (/^[a-z]:[\\/]/i.test(path) || path.startsWith("\\\\") || path.startsWith("//") ? path.replaceAll("\\", "/").replace(/^[A-Z]:/, (drive) => drive.toLowerCase()) : path).replace(/\/+$/, "");
|
|
11
|
+
}
|
|
12
|
+
function currentWorktree(view) {
|
|
13
|
+
if (!view) return void 0;
|
|
14
|
+
const cwd = comparablePath(view.cwd);
|
|
15
|
+
return view.worktrees.filter((item) => {
|
|
16
|
+
const path = comparablePath(item.path);
|
|
17
|
+
return cwd === path || cwd.startsWith(path + "/");
|
|
18
|
+
}).sort((a, b) => comparablePath(b.path).length - comparablePath(a.path).length)[0];
|
|
19
|
+
}
|
|
20
|
+
function isWorktreeView(value) {
|
|
21
|
+
if (!value || typeof value !== "object") return false;
|
|
22
|
+
const view = value;
|
|
23
|
+
return typeof view.cwd === "string" && Array.isArray(view.worktrees) && view.worktrees.every((item) => item && typeof item.path === "string" && (item.branch === null || typeof item.branch === "string"));
|
|
24
|
+
}
|
|
25
|
+
// src/errors.ts
|
|
26
|
+
const failureCodes = [
|
|
27
|
+
"busy",
|
|
28
|
+
"conflict",
|
|
29
|
+
"invalid",
|
|
30
|
+
"operation_failed",
|
|
31
|
+
"session_not_found",
|
|
32
|
+
"forbidden",
|
|
33
|
+
"auth_required",
|
|
34
|
+
"endpoint_missing",
|
|
35
|
+
"client_unavailable",
|
|
36
|
+
"invalid_response",
|
|
37
|
+
"connection_error",
|
|
38
|
+
"timeout"
|
|
39
|
+
];
|
|
40
|
+
function failureCode(error) {
|
|
41
|
+
if (error instanceof Error && error.name === "TimeoutError") return "timeout";
|
|
42
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
43
|
+
const code = error.code === "session/not-found" ? "session_not_found" : error.code;
|
|
44
|
+
if (failureCodes.includes(code)) return code;
|
|
45
|
+
return "operation_failed";
|
|
46
|
+
}
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
const status = error.message.match(/\bHTTP (\d{3})\b/)?.[1];
|
|
49
|
+
if (status === "401") return "auth_required";
|
|
50
|
+
if (status === "403") return "forbidden";
|
|
51
|
+
if (status === "404") return "endpoint_missing";
|
|
52
|
+
}
|
|
53
|
+
return "connection_error";
|
|
54
|
+
}
|
|
5
55
|
// src/requests.ts
|
|
6
|
-
const copy = {
|
|
56
|
+
const copy$1 = {
|
|
7
57
|
en: {
|
|
8
|
-
title: "
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
reuse: "Reuse existing",
|
|
58
|
+
title: "Git worktree",
|
|
59
|
+
detached: "Detached HEAD",
|
|
60
|
+
unknown: "Branch unavailable",
|
|
61
|
+
create: "New worktree…",
|
|
62
|
+
createTitle: "Create worktree",
|
|
14
63
|
branch: "New branch name (optional)",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
64
|
+
hint: "Leave the name empty for model naming. Uncommitted changes stay in the source checkout.",
|
|
65
|
+
close: "Close",
|
|
66
|
+
submit: "Create",
|
|
67
|
+
refresh: "Refresh worktrees",
|
|
68
|
+
loading: "Loading worktrees…",
|
|
69
|
+
busy: "Changing worktree…",
|
|
70
|
+
changed: "Worktree changed.",
|
|
71
|
+
empty: "No Git worktrees found.",
|
|
22
72
|
wait: "Wait until the session is ready and idle.",
|
|
23
|
-
error: "Could not confirm
|
|
73
|
+
error: "Could not confirm the current state. Refresh worktrees to check before trying again.",
|
|
74
|
+
lookupError: "Lookup failed",
|
|
75
|
+
mutationError: "Check worktree state",
|
|
76
|
+
errorTitle: "Worktree error details",
|
|
77
|
+
readError: "Could not load the worktree list. Retry the lookup to check the current branch.",
|
|
78
|
+
errorCode: "Error code",
|
|
79
|
+
errorOperation: "Request",
|
|
80
|
+
operations: {
|
|
81
|
+
describe: "List worktrees",
|
|
82
|
+
create: "Create worktree",
|
|
83
|
+
switch: "Switch worktree"
|
|
84
|
+
},
|
|
85
|
+
reasons: {
|
|
86
|
+
busy: "The session has an active task. Retry after the task stops.",
|
|
87
|
+
conflict: "The current directory changed. Reload the list before another change.",
|
|
88
|
+
invalid: "The request is invalid. Reload the list and check the branch name or worktree.",
|
|
89
|
+
operation_failed: "The host could not complete the operation. Check Git and the worktree configuration.",
|
|
90
|
+
session_not_found: "The session is unavailable. Reopen the session and retry.",
|
|
91
|
+
forbidden: "This operation requires the owner connection. Reopen the application as the owner.",
|
|
92
|
+
auth_required: "The connection requires authentication. Reopen the application and retry.",
|
|
93
|
+
endpoint_missing: "The host has no worktree endpoint. Check that the plugin is active and restart the application.",
|
|
94
|
+
client_unavailable: "The connection has no worktree RPC client. Reload the page or update the application.",
|
|
95
|
+
invalid_response: "The host returned an invalid worktree list. Check the application and plugin versions.",
|
|
96
|
+
connection_error: "The connection failed before a valid response. Check the connection and retry.",
|
|
97
|
+
timeout: "The request timed out. Retry after the current task stops."
|
|
98
|
+
}
|
|
24
99
|
},
|
|
25
100
|
ko: {
|
|
26
|
-
title: "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
reuse: "기존 경로 사용",
|
|
101
|
+
title: "Git worktree",
|
|
102
|
+
detached: "분리된 HEAD",
|
|
103
|
+
unknown: "브랜치 확인 불가",
|
|
104
|
+
create: "신규 워크트리…",
|
|
105
|
+
createTitle: "워크트리 생성",
|
|
32
106
|
branch: "새 브랜치 이름 (선택)",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
107
|
+
hint: "이름을 비우면 모델이 이름을 정합니다. 커밋하지 않은 변경은 원래 체크아웃에 남습니다.",
|
|
108
|
+
close: "닫기",
|
|
109
|
+
submit: "생성",
|
|
110
|
+
refresh: "워크트리 새로고침",
|
|
111
|
+
loading: "워크트리를 불러오는 중…",
|
|
112
|
+
busy: "워크트리 변경 중…",
|
|
113
|
+
changed: "워크트리를 변경했습니다.",
|
|
114
|
+
empty: "Git worktree가 없습니다.",
|
|
40
115
|
wait: "세션이 준비되고 실행 중인 작업이 끝나면 사용할 수 있습니다.",
|
|
41
|
-
error: "
|
|
116
|
+
error: "현재 상태를 확인하지 못했습니다. 다시 시도하기 전에 새로고침으로 상태를 확인하세요.",
|
|
117
|
+
lookupError: "조회 실패",
|
|
118
|
+
mutationError: "변경 확인 필요",
|
|
119
|
+
errorTitle: "워크트리 오류 상세",
|
|
120
|
+
readError: "워크트리 목록을 불러오지 못했습니다. 다시 조회해 현재 브랜치를 확인하세요.",
|
|
121
|
+
errorCode: "오류 코드",
|
|
122
|
+
errorOperation: "요청",
|
|
123
|
+
operations: {
|
|
124
|
+
describe: "워크트리 조회",
|
|
125
|
+
create: "워크트리 생성",
|
|
126
|
+
switch: "워크트리 전환"
|
|
127
|
+
},
|
|
128
|
+
reasons: {
|
|
129
|
+
busy: "세션에서 작업을 실행 중입니다. 작업이 끝난 뒤 다시 조회하세요.",
|
|
130
|
+
conflict: "현재 작업 경로가 바뀌었습니다. 목록을 새로고침한 뒤 변경하세요.",
|
|
131
|
+
invalid: "요청이 올바르지 않습니다. 목록을 새로고침하고 브랜치 이름이나 워크트리를 확인하세요.",
|
|
132
|
+
operation_failed: "호스트가 작업을 완료하지 못했습니다. Git과 워크트리 설정을 확인하세요.",
|
|
133
|
+
session_not_found: "세션을 찾지 못했습니다. 세션을 다시 열고 조회하세요.",
|
|
134
|
+
forbidden: "소유자 연결에서만 사용할 수 있습니다. 소유자로 앱을 다시 여세요.",
|
|
135
|
+
auth_required: "연결 인증이 필요합니다. 앱을 다시 열고 조회하세요.",
|
|
136
|
+
endpoint_missing: "호스트에 워크트리 API가 없습니다. 플러그인을 활성화했는지 확인하고 앱을 다시 시작하세요.",
|
|
137
|
+
client_unavailable: "연결에 워크트리 RPC 기능이 없습니다. 페이지를 새로고침하거나 앱을 업데이트하세요.",
|
|
138
|
+
invalid_response: "호스트가 올바르지 않은 워크트리 목록을 반환했습니다. 앱과 플러그인 버전을 확인하세요.",
|
|
139
|
+
connection_error: "정상 응답을 받기 전에 연결이 실패했습니다. 연결을 확인하고 다시 조회하세요.",
|
|
140
|
+
timeout: "요청 시간이 초과됐습니다. 실행 중인 작업이 끝난 뒤 다시 조회하세요."
|
|
141
|
+
}
|
|
42
142
|
}
|
|
43
143
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
144
|
+
// src/settings.tsx
|
|
145
|
+
const copy = {
|
|
146
|
+
ko: {
|
|
147
|
+
nav: "워크트리",
|
|
148
|
+
directory: "워크트리 풀 디렉터리",
|
|
149
|
+
directoryHint: "원본 저장소 루트 기준 상대 경로 또는 절대 경로를 입력하세요.",
|
|
150
|
+
remote: "추적 중인 원격 브랜치 사용",
|
|
151
|
+
remoteHint: "로컬에 가져온 원격 참조를 사용합니다. 자동으로 fetch하지 않습니다.",
|
|
152
|
+
naming: "자동 브랜치 이름 형식",
|
|
153
|
+
namingHint: "워크트리 생성 시 브랜치 입력을 비운 경우에만 적용합니다.",
|
|
154
|
+
semantic: "시멘틱 (feat/… 또는 fix/…)",
|
|
155
|
+
dsh: "DSH (dsh/*)",
|
|
156
|
+
custom: "사용자 지정 접두사",
|
|
157
|
+
prefix: "브랜치 접두사",
|
|
158
|
+
prefixHint: "영문·숫자·하이픈·밑줄과 /를 사용하세요. 예: team/, task-",
|
|
159
|
+
save: "저장",
|
|
160
|
+
refresh: "설정 새로고침",
|
|
161
|
+
loading: "설정을 불러오는 중입니다…",
|
|
162
|
+
saving: "저장 중…",
|
|
163
|
+
saved: "설정을 저장했습니다. 다음 워크트리 생성부터 적용합니다.",
|
|
164
|
+
error: "설정을 확인하지 못했습니다. 다시 저장하기 전에 설정을 새로고침하세요.",
|
|
165
|
+
conflict: "다른 곳에서 설정을 변경했습니다. 설정을 새로고침한 뒤 다시 저장하세요."
|
|
166
|
+
},
|
|
167
|
+
en: {
|
|
168
|
+
nav: "Worktrees",
|
|
169
|
+
directory: "Worktree pool directory",
|
|
170
|
+
directoryHint: "Enter a path relative to the source repository root, or an absolute path.",
|
|
171
|
+
remote: "Use tracked remote branch",
|
|
172
|
+
remoteHint: "Uses the locally fetched remote reference. Does not fetch automatically.",
|
|
173
|
+
naming: "Automatic branch naming",
|
|
174
|
+
namingHint: "Applies only when the branch input is left empty when creating a worktree.",
|
|
175
|
+
semantic: "Semantic (feat/… or fix/…)",
|
|
176
|
+
dsh: "DSH (dsh/*)",
|
|
177
|
+
custom: "Custom prefix",
|
|
178
|
+
prefix: "Branch prefix",
|
|
179
|
+
prefixHint: "Use letters, digits, hyphens, underscores and /. For example: team/, task-",
|
|
180
|
+
save: "Save",
|
|
181
|
+
refresh: "Reload settings",
|
|
182
|
+
loading: "Loading settings…",
|
|
183
|
+
saving: "Saving…",
|
|
184
|
+
saved: "Settings saved. Applies to the next worktree creation.",
|
|
185
|
+
error: "Could not confirm settings. Reload settings before saving again.",
|
|
186
|
+
conflict: "Settings changed elsewhere. Reload settings before saving again."
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const settingsLabel = (language) => copy[language.startsWith("ko") ? "ko" : "en"].nav;
|
|
190
|
+
function WorktreeSettingsSection({ ctx }) {
|
|
191
|
+
const language = (0, react.useSyncExternalStore)((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getSnapshot().active);
|
|
192
|
+
const text = copy[language.startsWith("ko") ? "ko" : "en"];
|
|
193
|
+
const id = (0, react.useId)();
|
|
194
|
+
const connection = ctx.get("connection");
|
|
195
|
+
const [view, setView] = (0, react.useState)();
|
|
196
|
+
const [draft, setDraft] = (0, react.useState)();
|
|
197
|
+
const [operation, setOperation] = (0, react.useState)("loading");
|
|
198
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
199
|
+
const [namingOpen, setNamingOpen] = (0, react.useState)(false);
|
|
200
|
+
const pending = (0, react.useRef)(false);
|
|
201
|
+
const needsReload = (0, react.useRef)(false);
|
|
202
|
+
const lifetime = (0, react.useRef)(null);
|
|
203
|
+
const request = async (save = false) => {
|
|
204
|
+
const controller = lifetime.current;
|
|
205
|
+
if (!controller || controller.signal.aborted || pending.current) return;
|
|
206
|
+
if (save && (!view || !draft || needsReload.current)) return;
|
|
207
|
+
pending.current = true;
|
|
208
|
+
setNamingOpen(false);
|
|
209
|
+
setOperation(save ? "saving" : "loading");
|
|
210
|
+
setNotice(void 0);
|
|
211
|
+
try {
|
|
212
|
+
const result = await connection.rpc.call(channel, `${endpointPrefix}${save ? "save-settings" : "settings"}`, save ? {
|
|
213
|
+
revision: view.revision,
|
|
214
|
+
settings: { ...draft }
|
|
215
|
+
} : {}, controller.signal);
|
|
216
|
+
if (controller.signal.aborted) return;
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
needsReload.current = true;
|
|
219
|
+
setNotice(result.error.code === "conflict" ? "conflict" : "error");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const next = result.value;
|
|
223
|
+
setView(next);
|
|
224
|
+
setDraft({ ...next.settings });
|
|
225
|
+
needsReload.current = false;
|
|
226
|
+
if (save) setNotice("saved");
|
|
227
|
+
} catch {
|
|
228
|
+
if (!controller.signal.aborted) {
|
|
229
|
+
needsReload.current = true;
|
|
230
|
+
setNotice("error");
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
if (!controller.signal.aborted) {
|
|
234
|
+
pending.current = false;
|
|
235
|
+
setOperation(void 0);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
(0, react.useEffect)(() => {
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
lifetime.current = controller;
|
|
242
|
+
pending.current = false;
|
|
243
|
+
request();
|
|
244
|
+
return () => controller.abort();
|
|
245
|
+
}, []);
|
|
246
|
+
const busy = operation !== void 0;
|
|
247
|
+
const dirty = !!draft && JSON.stringify(draft) !== JSON.stringify(view?.settings);
|
|
248
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
249
|
+
className: "dsh-worktree-settings",
|
|
250
|
+
"aria-label": text.nav,
|
|
251
|
+
"aria-busy": busy,
|
|
252
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
253
|
+
onSubmit: (event) => {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
request(true);
|
|
256
|
+
},
|
|
257
|
+
children: [
|
|
258
|
+
draft && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
259
|
+
disabled: busy,
|
|
260
|
+
children: [
|
|
261
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
262
|
+
className: "dsh-worktree-settings-row",
|
|
263
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
264
|
+
className: "dsh-worktree-settings-text",
|
|
265
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
266
|
+
htmlFor: `${id}-directory`,
|
|
267
|
+
children: text.directory
|
|
268
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
269
|
+
id: `${id}-directory-hint`,
|
|
270
|
+
children: text.directoryHint
|
|
271
|
+
})]
|
|
272
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
273
|
+
className: "dsh-worktree-settings-input",
|
|
274
|
+
id: `${id}-directory`,
|
|
275
|
+
type: "text",
|
|
276
|
+
value: draft.directory,
|
|
277
|
+
required: true,
|
|
278
|
+
maxLength: 4096,
|
|
279
|
+
autoComplete: "off",
|
|
280
|
+
spellCheck: false,
|
|
281
|
+
"aria-describedby": `${id}-directory-hint`,
|
|
282
|
+
onChange: (event) => setDraft({
|
|
283
|
+
...draft,
|
|
284
|
+
directory: event.target.value
|
|
285
|
+
})
|
|
286
|
+
})]
|
|
287
|
+
}),
|
|
288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
289
|
+
className: "dsh-worktree-settings-row",
|
|
290
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
291
|
+
className: "dsh-worktree-settings-text",
|
|
292
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
293
|
+
className: "dsh-worktree-settings-title",
|
|
294
|
+
children: text.remote
|
|
295
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.remoteHint })]
|
|
296
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
297
|
+
label: text.remote,
|
|
298
|
+
checked: draft.remote,
|
|
299
|
+
disabled: busy,
|
|
300
|
+
onChange: (remote) => setDraft({
|
|
301
|
+
...draft,
|
|
302
|
+
remote
|
|
303
|
+
})
|
|
304
|
+
})]
|
|
305
|
+
}),
|
|
306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
307
|
+
className: "dsh-worktree-settings-row",
|
|
308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
309
|
+
className: "dsh-worktree-settings-text",
|
|
310
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
311
|
+
id: `${id}-naming`,
|
|
312
|
+
className: "dsh-worktree-settings-title",
|
|
313
|
+
children: text.naming
|
|
314
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
315
|
+
id: `${id}-naming-hint`,
|
|
316
|
+
children: text.namingHint
|
|
317
|
+
})]
|
|
318
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
319
|
+
open: namingOpen,
|
|
320
|
+
align: "end",
|
|
321
|
+
portal: true,
|
|
322
|
+
selectedId: draft.naming,
|
|
323
|
+
items: [
|
|
324
|
+
"semantic",
|
|
325
|
+
"dsh",
|
|
326
|
+
"custom"
|
|
327
|
+
].map((id) => ({
|
|
328
|
+
id,
|
|
329
|
+
label: text[id]
|
|
330
|
+
})),
|
|
331
|
+
onClose: () => setNamingOpen(false),
|
|
332
|
+
onSelect: (naming) => {
|
|
333
|
+
setNamingOpen(false);
|
|
334
|
+
if (!busy) setDraft({
|
|
335
|
+
...draft,
|
|
336
|
+
naming
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
340
|
+
type: "button",
|
|
341
|
+
className: "dsh-worktree-settings-selector",
|
|
342
|
+
disabled: busy,
|
|
343
|
+
"aria-labelledby": `${id}-naming`,
|
|
344
|
+
"aria-describedby": `${id}-naming-hint`,
|
|
345
|
+
"aria-haspopup": "menu",
|
|
346
|
+
"aria-expanded": namingOpen,
|
|
347
|
+
onClick: () => setNamingOpen((open) => !open),
|
|
348
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text[draft.naming] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
|
|
349
|
+
size: 16,
|
|
350
|
+
"aria-hidden": "true"
|
|
351
|
+
})]
|
|
352
|
+
})
|
|
353
|
+
})]
|
|
354
|
+
}),
|
|
355
|
+
draft.naming === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
356
|
+
className: "dsh-worktree-settings-row",
|
|
357
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
358
|
+
className: "dsh-worktree-settings-text",
|
|
359
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
360
|
+
htmlFor: `${id}-prefix`,
|
|
361
|
+
children: text.prefix
|
|
362
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
363
|
+
id: `${id}-prefix-hint`,
|
|
364
|
+
children: text.prefixHint
|
|
365
|
+
})]
|
|
366
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
367
|
+
className: "dsh-worktree-settings-input",
|
|
368
|
+
id: `${id}-prefix`,
|
|
369
|
+
type: "text",
|
|
370
|
+
value: draft.prefix,
|
|
371
|
+
required: true,
|
|
372
|
+
maxLength: 100,
|
|
373
|
+
pattern: "[a-zA-Z0-9][a-zA-Z0-9_\\-]*(/[a-zA-Z0-9][a-zA-Z0-9_\\-]*)*/?",
|
|
374
|
+
"aria-describedby": `${id}-prefix-hint`,
|
|
375
|
+
autoComplete: "off",
|
|
376
|
+
spellCheck: false,
|
|
377
|
+
onChange: (event) => setDraft({
|
|
378
|
+
...draft,
|
|
379
|
+
prefix: event.target.value
|
|
380
|
+
})
|
|
381
|
+
})]
|
|
382
|
+
})
|
|
383
|
+
]
|
|
384
|
+
}),
|
|
385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
386
|
+
role: "status",
|
|
387
|
+
children: operation ? text[operation] : notice === "saved" ? text.saved : ""
|
|
388
|
+
}),
|
|
389
|
+
(notice === "error" || notice === "conflict") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
390
|
+
role: "alert",
|
|
391
|
+
children: text[notice]
|
|
392
|
+
}),
|
|
393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
394
|
+
type: "button",
|
|
395
|
+
variant: "outline",
|
|
396
|
+
disabled: busy,
|
|
397
|
+
onClick: () => void request(),
|
|
398
|
+
children: text.refresh
|
|
399
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
400
|
+
type: "submit",
|
|
401
|
+
variant: "primary",
|
|
402
|
+
disabled: busy || !view || !dirty || needsReload.current,
|
|
403
|
+
children: operation === "saving" ? text.saving : text.save
|
|
404
|
+
})] })
|
|
405
|
+
]
|
|
406
|
+
})
|
|
407
|
+
});
|
|
50
408
|
}
|
|
51
409
|
// src/styles.ts
|
|
52
410
|
const styles = `
|
|
53
|
-
.dsh-worktree {
|
|
54
|
-
.dsh-worktree
|
|
55
|
-
.dsh-worktree-
|
|
56
|
-
.dsh-worktree-
|
|
57
|
-
.dsh-worktree-
|
|
411
|
+
.dsh-worktree-sidebar-name { flex: 0 1 auto; min-width: 0; max-width: 88px; margin-inline: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 16px; }
|
|
412
|
+
.dsh-worktree { display: inline-flex; flex-wrap: wrap; align-items: center; min-width: 0; max-width: 100%; color: var(--dsw-alias-label-secondary); font: inherit; }
|
|
413
|
+
.dsh-worktree-controls { display: inline-flex; flex-wrap: nowrap; align-items: center; min-width: 0; max-width: 100%; }
|
|
414
|
+
.dsh-worktree-picker { display: inline-flex; min-width: 0; max-width: 100%; }
|
|
415
|
+
.dsh-worktree-trigger { display: inline-flex; align-items: center; gap: 4px; min-width: 0; max-width: 220px; height: 28px; padding: 0 4px 0 8px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; }
|
|
416
|
+
.dsh-worktree-icon, .dsh-worktree-chevron { display: inline-flex; flex: none; }
|
|
417
|
+
.dsh-worktree-icon svg { width: 14px; height: 14px; }
|
|
418
|
+
.dsh-worktree-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
419
|
+
.dsh-worktree-chevron { color: var(--dsw-alias-label-caption); }
|
|
420
|
+
.dsh-worktree-trigger[aria-expanded=true] .dsh-worktree-chevron { transform: rotate(180deg); }
|
|
421
|
+
.dsh-worktree-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
422
|
+
.dsh-worktree-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
|
|
423
|
+
@container (width <= 460px) {
|
|
424
|
+
.dsh-worktree-name { display: none; }
|
|
425
|
+
}
|
|
426
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
427
|
+
.dsh-worktree-chevron { transition: transform .12s; }
|
|
428
|
+
}
|
|
429
|
+
@media (hover: hover) {
|
|
430
|
+
.dsh-worktree-trigger:hover:not(:disabled), .dsh-worktree-error-trigger:hover, .dsh-worktree-dialog button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
431
|
+
}
|
|
432
|
+
.dsh-worktree-error-trigger { flex: 0 0 auto; min-height: 28px; padding: 2px 6px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 20px; white-space: nowrap; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
433
|
+
.dsh-worktree-error-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
|
|
434
|
+
.dsh-worktree-dialog.dsh-worktree-error-dialog { inset: 0; margin: auto; }
|
|
435
|
+
.dsh-worktree-error-dialog dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 12px; }
|
|
436
|
+
.dsh-worktree-error-dialog dt { color: var(--dsw-alias-label-secondary); }
|
|
437
|
+
.dsh-worktree-error-dialog dd { margin: 0; overflow-wrap: anywhere; }
|
|
438
|
+
.dsh-worktree-dialog { box-sizing: border-box; position: fixed; margin: 0; width: 360px; max-width: calc(100vw - 16px); max-height: calc(100dvh - 16px); overflow: auto; overscroll-behavior: contain; padding: 16px; border: 1px solid transparent; border-radius: var(--dsw-radius-lg); background: var(--dsw-specific-menu); backdrop-filter: var(--dsw-menu-backdrop-filter); color: var(--dsw-alias-label-primary); --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-elevation-prominent); font: inherit; font-size: 13px; font-weight: 400; line-height: 1.55; }
|
|
58
439
|
.dsh-worktree-dialog::backdrop { background: transparent; }
|
|
59
440
|
.dsh-worktree-dialog header { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
|
60
441
|
.dsh-worktree-dialog h2 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
61
|
-
.dsh-worktree-dialog p { margin: 10px 0; color: var(--dsw-alias-label-secondary
|
|
62
|
-
.dsh-worktree-dialog button { border: 1px solid var(--dsw-alias-border-
|
|
63
|
-
.dsh-worktree-dialog header button { padding: 0; width: 28px; height: 28px; border: 0;
|
|
64
|
-
.dsh-worktree-dialog
|
|
65
|
-
.dsh-worktree-dialog
|
|
66
|
-
.dsh-worktree-dialog fieldset label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
|
|
67
|
-
.dsh-worktree-dialog input[type=radio] { accent-color: #4d6bfe; margin: 0; }
|
|
68
|
-
.dsh-worktree-dialog form > label { display: block; font-weight: 500; margin-bottom: 6px; }
|
|
69
|
-
.dsh-worktree-dialog input[type=text] { box-sizing: border-box; width: 100%; padding: 9px 10px; border: 1px solid var(--dsw-alias-border-l4, #dfe2e8); border-radius: 8px; background: var(--dsw-alias-bg-layer-1, #f7f8fa); color: inherit; font: inherit; }
|
|
442
|
+
.dsh-worktree-dialog p { margin: 10px 0; color: var(--dsw-alias-label-secondary); }
|
|
443
|
+
.dsh-worktree-dialog button { border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; color: inherit; padding: 7px 10px; cursor: pointer; font: inherit; }
|
|
444
|
+
.dsh-worktree-dialog header button { display: inline-flex; align-items: center; justify-content: center; padding: 0; width: 28px; height: 28px; border: 0; }
|
|
445
|
+
.dsh-worktree-dialog form > label { display: block; font-weight: 500; margin: 14px 0 6px; }
|
|
446
|
+
.dsh-worktree-dialog input[type=text] { box-sizing: border-box; width: 100%; padding: 9px 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-1); color: inherit; font: inherit; }
|
|
70
447
|
.dsh-worktree-dialog .dsh-worktree-hint { font-size: 12px; }
|
|
71
|
-
.dsh-worktree-dialog footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
72
|
-
.dsh-worktree-dialog .dsh-worktree-primary, .dsh-worktree-dialog .dsh-worktree-primary:hover:not(:disabled) { background:
|
|
73
|
-
.dsh-worktree-dialog [role=alert] { color: var(--dsw-alias-state-error-primary
|
|
74
|
-
.dsh-worktree
|
|
75
|
-
.dsh-worktree button:focus-visible, .dsh-worktree-dialog input:focus-visible { outline: 2px solid
|
|
448
|
+
.dsh-worktree-dialog footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
449
|
+
.dsh-worktree-dialog .dsh-worktree-primary, .dsh-worktree-dialog .dsh-worktree-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-fill); border-color: transparent; color: var(--dsw-alias-label-primary-foreground); }
|
|
450
|
+
.dsh-worktree-dialog [role=alert] { color: var(--dsw-alias-state-error-primary); }
|
|
451
|
+
.dsh-worktree-dialog button:disabled { opacity: .45; cursor: not-allowed; }
|
|
452
|
+
.dsh-worktree-dialog button:focus-visible, .dsh-worktree-dialog input:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
|
|
76
453
|
.dsh-worktree-sr { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
454
|
+
.dsh-worktree-settings { width: 100%; color: var(--dsw-alias-label-primary); font: inherit; }
|
|
455
|
+
.dsh-worktree-settings fieldset { min-width: 0; padding: 0; margin: 0; border: 0; }
|
|
456
|
+
.dsh-worktree-settings-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
457
|
+
.dsh-worktree-settings-row:last-child { border-bottom: 0; }
|
|
458
|
+
.dsh-worktree-settings-text { flex: 1; min-width: 0; }
|
|
459
|
+
.dsh-worktree-settings label, .dsh-worktree-settings-title { display: block; font-size: 14px; font-weight: 400; line-height: 20px; }
|
|
460
|
+
.dsh-worktree-settings-text p { margin: 4px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
|
|
461
|
+
.dsh-worktree-settings-input { box-sizing: border-box; width: 220px; max-width: 100%; flex: 0 1 220px; min-width: 0; }
|
|
462
|
+
.dsh-worktree-settings-selector { display: inline-flex; align-items: center; gap: 12px; max-width: 220px; height: 36px; padding: 0 14px; border: 0; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
|
|
463
|
+
.dsh-worktree-settings-selector span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
464
|
+
.dsh-worktree-settings-selector svg { flex: none; }
|
|
465
|
+
.dsh-worktree-settings-selector:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
466
|
+
.dsh-worktree-settings-selector:disabled { opacity: .5; cursor: default; }
|
|
467
|
+
.dsh-worktree-settings-selector:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
|
|
468
|
+
.dsh-worktree-settings [role=status], .dsh-worktree-settings [role=alert] { margin: 12px 0; font-size: 12px; line-height: 18px; }
|
|
469
|
+
.dsh-worktree-settings [role=status]:empty { margin: 0; }
|
|
470
|
+
.dsh-worktree-settings footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
|
471
|
+
.dsh-worktree-settings [role=alert] { color: var(--dsw-alias-state-error-primary); }
|
|
472
|
+
@media (max-width: 600px) {
|
|
473
|
+
.dsh-worktree-settings-row { flex-wrap: wrap; gap: 12px; }
|
|
474
|
+
.dsh-worktree-settings-text { flex-basis: 160px; }
|
|
475
|
+
.dsh-worktree-settings-input { flex-grow: 1; }
|
|
476
|
+
}
|
|
77
477
|
`;
|
|
478
|
+
// src/sidebar.ts
|
|
479
|
+
/** Add passive metadata before the native trailing controls without replacing React-owned rows. */
|
|
480
|
+
function installSidebarLabel(sessionId, name, path) {
|
|
481
|
+
const labels = /* @__PURE__ */ new Map();
|
|
482
|
+
const refresh = () => {
|
|
483
|
+
for (const [row, label] of labels) if (!row.isConnected || row.getAttribute("data-row-key") !== `session:${sessionId}`) {
|
|
484
|
+
label.remove();
|
|
485
|
+
labels.delete(row);
|
|
486
|
+
}
|
|
487
|
+
for (const row of document.querySelectorAll("[data-row-key]")) {
|
|
488
|
+
if (row.dataset.rowKey !== `session:${sessionId}`) continue;
|
|
489
|
+
const actions = row.lastElementChild;
|
|
490
|
+
if (!actions || actions === labels.get(row)) continue;
|
|
491
|
+
let label = labels.get(row);
|
|
492
|
+
if (!label) {
|
|
493
|
+
label = document.createElement("span");
|
|
494
|
+
label.className = "dsh-worktree-sidebar-name";
|
|
495
|
+
label.textContent = name;
|
|
496
|
+
label.title = path;
|
|
497
|
+
labels.set(row, label);
|
|
498
|
+
}
|
|
499
|
+
if (label.nextElementSibling !== actions) row.insertBefore(label, actions);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const observer = new document.defaultView.MutationObserver(refresh);
|
|
503
|
+
observer.observe(document.body, {
|
|
504
|
+
childList: true,
|
|
505
|
+
subtree: true,
|
|
506
|
+
attributes: true,
|
|
507
|
+
attributeFilter: ["data-row-key"]
|
|
508
|
+
});
|
|
509
|
+
refresh();
|
|
510
|
+
return () => {
|
|
511
|
+
observer.disconnect();
|
|
512
|
+
for (const label of labels.values()) label.remove();
|
|
513
|
+
};
|
|
514
|
+
}
|
|
78
515
|
// src/client.tsx
|
|
79
516
|
const name = "dungsil-ui-worktree";
|
|
80
517
|
const inject = [
|
|
81
518
|
"slots",
|
|
82
519
|
"locale",
|
|
83
|
-
"
|
|
520
|
+
"connection"
|
|
84
521
|
];
|
|
85
522
|
function apply(ctx) {
|
|
523
|
+
const connection = ctx.get("connection");
|
|
86
524
|
const subscribeLocale = (listener) => ctx.locale.subscribe(listener);
|
|
87
|
-
function Picker({ sessionId, useSession
|
|
525
|
+
function Picker({ sessionId, useSession }) {
|
|
88
526
|
const language = (0, react.useSyncExternalStore)(subscribeLocale, () => ctx.locale.getSnapshot().active);
|
|
89
|
-
const text = copy[language
|
|
527
|
+
const text = copy$1[language.startsWith("ko") ? "ko" : "en"];
|
|
90
528
|
const unavailable = useSession((s) => s.running || s.removed || s.openState !== "open" || s.pendingSubmissions.length > 0);
|
|
91
|
-
const
|
|
92
|
-
const [mode, setMode] = (0, react.useState)("create");
|
|
529
|
+
const [view, setView] = (0, react.useState)();
|
|
93
530
|
const [value, setValue] = (0, react.useState)("");
|
|
531
|
+
const [loading, setLoading] = (0, react.useState)(true);
|
|
94
532
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
95
|
-
const [error, setError] = (0, react.useState)(
|
|
96
|
-
const [
|
|
533
|
+
const [error, setError] = (0, react.useState)();
|
|
534
|
+
const [changed, setChanged] = (0, react.useState)(false);
|
|
97
535
|
const [open, setOpen] = (0, react.useState)(false);
|
|
98
536
|
const pending = (0, react.useRef)(false);
|
|
99
|
-
const
|
|
537
|
+
const needsRefresh = (0, react.useRef)(false);
|
|
538
|
+
const lifetime = (0, react.useRef)(null);
|
|
100
539
|
const trigger = (0, react.useRef)(null);
|
|
101
540
|
const dialog = (0, react.useRef)(null);
|
|
541
|
+
const errorDialog = (0, react.useRef)(null);
|
|
542
|
+
const errorTrigger = (0, react.useRef)(null);
|
|
102
543
|
const title = (0, react.useId)();
|
|
544
|
+
const errorTitle = (0, react.useId)();
|
|
545
|
+
const errorDescription = (0, react.useId)();
|
|
103
546
|
const field = (0, react.useId)();
|
|
104
|
-
const
|
|
547
|
+
const status = (0, react.useId)();
|
|
548
|
+
const disabled = unavailable || loading || busy || !!error || !view;
|
|
549
|
+
const errorLabel = error?.uncertain ? text.mutationError : text.lookupError;
|
|
550
|
+
const recordFailure = (operation, cause) => {
|
|
551
|
+
const code = failureCode(cause);
|
|
552
|
+
needsRefresh.current = true;
|
|
553
|
+
setError((previous) => ({
|
|
554
|
+
operation,
|
|
555
|
+
code,
|
|
556
|
+
uncertain: operation !== "describe" || !!previous?.uncertain
|
|
557
|
+
}));
|
|
558
|
+
console.warn("[dungsil-ui-worktree]", {
|
|
559
|
+
operation,
|
|
560
|
+
code
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
const request = async (operation, payload, signal) => {
|
|
564
|
+
if (typeof connection?.rpc?.call !== "function") throw { code: "client_unavailable" };
|
|
565
|
+
const result = await connection.rpc.call(channel, endpointPrefix + operation, payload, signal);
|
|
566
|
+
signal.throwIfAborted();
|
|
567
|
+
if (!result.ok) throw result.error;
|
|
568
|
+
if (!isWorktreeView(result.value)) throw { code: "invalid_response" };
|
|
569
|
+
return result.value;
|
|
570
|
+
};
|
|
571
|
+
const load = async (explicit = false) => {
|
|
572
|
+
const controller = lifetime.current;
|
|
573
|
+
if (!controller || controller.signal.aborted || pending.current || needsRefresh.current && !explicit) return;
|
|
574
|
+
pending.current = true;
|
|
575
|
+
setLoading(true);
|
|
576
|
+
try {
|
|
577
|
+
const view = await request("describe", { sessionId }, controller.signal);
|
|
578
|
+
if (controller.signal.aborted) return;
|
|
579
|
+
setView(view);
|
|
580
|
+
needsRefresh.current = false;
|
|
581
|
+
setError(void 0);
|
|
582
|
+
} catch (cause) {
|
|
583
|
+
if (!controller.signal.aborted) recordFailure("describe", cause);
|
|
584
|
+
} finally {
|
|
585
|
+
if (!controller.signal.aborted) {
|
|
586
|
+
pending.current = false;
|
|
587
|
+
setLoading(false);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
105
591
|
(0, react.useEffect)(() => {
|
|
106
|
-
|
|
592
|
+
const controller = new AbortController();
|
|
593
|
+
lifetime.current = controller;
|
|
594
|
+
pending.current = false;
|
|
595
|
+
load();
|
|
596
|
+
const focus = () => {
|
|
597
|
+
load();
|
|
598
|
+
};
|
|
599
|
+
const resize = () => {
|
|
600
|
+
setOpen(false);
|
|
601
|
+
dialog.current?.close();
|
|
602
|
+
};
|
|
603
|
+
window.addEventListener("focus", focus);
|
|
604
|
+
window.addEventListener("resize", resize);
|
|
107
605
|
return () => {
|
|
108
|
-
|
|
606
|
+
controller.abort();
|
|
607
|
+
window.removeEventListener("focus", focus);
|
|
608
|
+
window.removeEventListener("resize", resize);
|
|
109
609
|
};
|
|
110
610
|
}, []);
|
|
111
611
|
(0, react.useEffect)(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return () => window.removeEventListener("resize", close);
|
|
115
|
-
}, []);
|
|
612
|
+
if (disabled) setOpen(false);
|
|
613
|
+
}, [disabled]);
|
|
116
614
|
const show = () => {
|
|
117
615
|
const element = dialog.current;
|
|
118
616
|
const button = trigger.current;
|
|
119
|
-
if (!element || !button) return;
|
|
120
|
-
setError(false);
|
|
121
|
-
setSent(false);
|
|
617
|
+
if (disabled || pending.current || !element || !button) return;
|
|
122
618
|
element.showModal();
|
|
123
619
|
const anchor = button.getBoundingClientRect();
|
|
124
620
|
const box = element.getBoundingClientRect();
|
|
125
621
|
element.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - box.width - 8))}px`;
|
|
126
622
|
element.style.top = `${Math.max(8, anchor.top - box.height - 8)}px`;
|
|
127
|
-
setOpen(true);
|
|
128
623
|
};
|
|
129
|
-
const
|
|
130
|
-
|
|
624
|
+
const change = async (endpoint, path) => {
|
|
625
|
+
const controller = lifetime.current;
|
|
626
|
+
if (disabled || pending.current || needsRefresh.current || !view || !controller || controller.signal.aborted) return;
|
|
131
627
|
pending.current = true;
|
|
132
628
|
setBusy(true);
|
|
133
|
-
|
|
629
|
+
setChanged(false);
|
|
134
630
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
631
|
+
const payload = endpoint === "create" ? {
|
|
632
|
+
sessionId,
|
|
633
|
+
cwd: view.cwd,
|
|
634
|
+
...value.trim() ? { name: value.trim() } : {}
|
|
635
|
+
} : {
|
|
636
|
+
sessionId,
|
|
637
|
+
cwd: view.cwd,
|
|
638
|
+
path
|
|
639
|
+
};
|
|
640
|
+
const nextView = await request(endpoint, payload, controller.signal);
|
|
641
|
+
if (controller.signal.aborted) return;
|
|
642
|
+
setView(nextView);
|
|
643
|
+
setChanged(true);
|
|
644
|
+
if (endpoint === "create") {
|
|
645
|
+
setValue("");
|
|
646
|
+
dialog.current?.close();
|
|
647
|
+
}
|
|
148
648
|
pending.current = false;
|
|
149
|
-
|
|
649
|
+
await load();
|
|
650
|
+
} catch (cause) {
|
|
651
|
+
if (!controller.signal.aborted) recordFailure(endpoint, cause);
|
|
652
|
+
} finally {
|
|
653
|
+
if (!controller.signal.aborted) {
|
|
654
|
+
pending.current = false;
|
|
655
|
+
setBusy(false);
|
|
656
|
+
}
|
|
150
657
|
}
|
|
151
658
|
};
|
|
659
|
+
(0, react.useEffect)(() => {
|
|
660
|
+
if (!busy && changed && !dialog.current?.open) trigger.current?.focus();
|
|
661
|
+
}, [busy, changed]);
|
|
662
|
+
(0, react.useEffect)(() => {
|
|
663
|
+
if (!error && errorDialog.current?.open) errorDialog.current.close();
|
|
664
|
+
}, [error]);
|
|
665
|
+
const restoreErrorFocus = () => {
|
|
666
|
+
(errorTrigger.current ?? trigger.current)?.focus();
|
|
667
|
+
};
|
|
668
|
+
const current = currentWorktree(view);
|
|
669
|
+
(0, react.useEffect)(() => {
|
|
670
|
+
if (!current || current === view?.worktrees[0]) return;
|
|
671
|
+
return installSidebarLabel(sessionId, current.branch ?? current.path.split(/[\\/]/).filter(Boolean).at(-1) ?? current.path, current.path);
|
|
672
|
+
}, [
|
|
673
|
+
sessionId,
|
|
674
|
+
current,
|
|
675
|
+
view
|
|
676
|
+
]);
|
|
677
|
+
const branch = current ? current.branch ?? text.detached : text.unknown;
|
|
678
|
+
const notice = loading ? text.loading : busy ? text.busy : error ? errorLabel : unavailable ? text.wait : changed ? text.changed : view?.worktrees.length === 0 ? text.empty : "";
|
|
152
679
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
153
680
|
className: "dsh-worktree",
|
|
154
681
|
children: [
|
|
155
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
683
|
+
className: "dsh-worktree-controls",
|
|
684
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
685
|
+
className: "dsh-worktree-picker",
|
|
686
|
+
open,
|
|
687
|
+
side: "top",
|
|
688
|
+
portal: true,
|
|
689
|
+
selectedId: current ? `path:${current.path}` : void 0,
|
|
690
|
+
items: [{
|
|
691
|
+
id: "create",
|
|
692
|
+
label: text.create,
|
|
693
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, {})
|
|
694
|
+
}, ...view?.worktrees.map((item) => ({
|
|
695
|
+
id: `path:${item.path}`,
|
|
696
|
+
label: item.branch ?? text.detached,
|
|
697
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {})
|
|
698
|
+
})) ?? []],
|
|
699
|
+
onClose: () => setOpen(false),
|
|
700
|
+
onSelect: (id) => {
|
|
701
|
+
setOpen(false);
|
|
702
|
+
if (disabled || pending.current) return;
|
|
703
|
+
if (id === "create") show();
|
|
704
|
+
else if (id.startsWith("path:") && id !== `path:${current?.path}`) change("switch", id.slice(5));
|
|
705
|
+
},
|
|
706
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
707
|
+
ref: trigger,
|
|
708
|
+
type: "button",
|
|
709
|
+
className: "dsh-worktree-trigger",
|
|
710
|
+
disabled,
|
|
711
|
+
"aria-label": view ? `${text.title}: ${branch}` : text.title,
|
|
712
|
+
"aria-haspopup": "menu",
|
|
713
|
+
"aria-expanded": open,
|
|
714
|
+
"aria-describedby": status,
|
|
715
|
+
"aria-busy": loading || busy,
|
|
716
|
+
title: view ? branch : text.title,
|
|
717
|
+
onClick: () => setOpen((value) => !value),
|
|
718
|
+
onKeyDown: (event) => {
|
|
719
|
+
if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
720
|
+
event.preventDefault();
|
|
721
|
+
setOpen(true);
|
|
722
|
+
}
|
|
723
|
+
},
|
|
173
724
|
children: [
|
|
174
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
725
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
726
|
+
className: "dsh-worktree-icon",
|
|
727
|
+
"aria-hidden": "true",
|
|
728
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { className: "dsh-worktree-branch" })
|
|
178
729
|
}),
|
|
179
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
r: "1.5"
|
|
730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
731
|
+
className: "dsh-worktree-name",
|
|
732
|
+
children: view ? branch : loading ? text.loading : text.title
|
|
183
733
|
}),
|
|
184
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
})
|
|
189
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4.5v7M12 5.5v1A3.5 3.5 0 0 1 8.5 10H4" })
|
|
734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
735
|
+
className: "dsh-worktree-chevron",
|
|
736
|
+
"aria-hidden": "true",
|
|
737
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})
|
|
738
|
+
})
|
|
190
739
|
]
|
|
191
|
-
}),
|
|
192
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Worktree" }),
|
|
193
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
194
|
-
"aria-hidden": "true",
|
|
195
|
-
className: "dsh-worktree-chevron",
|
|
196
|
-
children: "⌄"
|
|
197
740
|
})
|
|
198
|
-
|
|
741
|
+
}), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
742
|
+
ref: errorTrigger,
|
|
743
|
+
type: "button",
|
|
744
|
+
className: "dsh-worktree-error-trigger",
|
|
745
|
+
"aria-haspopup": "dialog",
|
|
746
|
+
"aria-label": `${errorLabel}: ${text.errorTitle}`,
|
|
747
|
+
onClick: () => errorDialog.current?.showModal(),
|
|
748
|
+
children: errorLabel
|
|
749
|
+
})]
|
|
199
750
|
}),
|
|
200
751
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
752
|
+
id: status,
|
|
201
753
|
className: "dsh-worktree-sr",
|
|
202
754
|
role: "status",
|
|
203
|
-
children:
|
|
755
|
+
children: notice
|
|
204
756
|
}),
|
|
205
757
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
206
758
|
ref: dialog,
|
|
207
759
|
className: "dsh-worktree-dialog",
|
|
208
760
|
"aria-labelledby": title,
|
|
209
|
-
onClose: () =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const outside = e.clientX < box.left || e.clientX > box.right || e.clientY < box.top || e.clientY > box.bottom;
|
|
213
|
-
if (e.target === e.currentTarget && outside && !pending.current) e.currentTarget.close();
|
|
761
|
+
onClose: () => trigger.current?.focus(),
|
|
762
|
+
onCancel: (event) => {
|
|
763
|
+
if (pending.current) event.preventDefault();
|
|
214
764
|
},
|
|
765
|
+
onClick: (event) => {
|
|
766
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
767
|
+
const outside = event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
|
|
768
|
+
if (event.target === event.currentTarget && outside && !pending.current) event.currentTarget.close();
|
|
769
|
+
},
|
|
770
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
771
|
+
id: title,
|
|
772
|
+
children: text.createTitle
|
|
773
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
774
|
+
type: "button",
|
|
775
|
+
"aria-label": text.close,
|
|
776
|
+
disabled: busy,
|
|
777
|
+
onClick: () => dialog.current?.close(),
|
|
778
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, { "aria-hidden": "true" })
|
|
779
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
780
|
+
onSubmit: (event) => {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
change("create");
|
|
783
|
+
},
|
|
784
|
+
children: [
|
|
785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
786
|
+
htmlFor: field,
|
|
787
|
+
children: text.branch
|
|
788
|
+
}),
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
790
|
+
id: field,
|
|
791
|
+
type: "text",
|
|
792
|
+
value,
|
|
793
|
+
disabled: busy,
|
|
794
|
+
maxLength: 200,
|
|
795
|
+
autoComplete: "off",
|
|
796
|
+
spellCheck: false,
|
|
797
|
+
placeholder: "fix-login",
|
|
798
|
+
onChange: (event) => setValue(event.target.value),
|
|
799
|
+
onKeyDown: (event) => {
|
|
800
|
+
if (event.key === "Enter" && (event.nativeEvent.isComposing || event.keyCode === 229)) event.preventDefault();
|
|
801
|
+
}
|
|
802
|
+
}),
|
|
803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
804
|
+
className: "dsh-worktree-hint",
|
|
805
|
+
children: text.hint
|
|
806
|
+
}),
|
|
807
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
808
|
+
role: "alert",
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: error.uncertain ? text.error : text.readError }),
|
|
811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.reasons[error.code] }),
|
|
812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: error.code })
|
|
813
|
+
]
|
|
814
|
+
}),
|
|
815
|
+
unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
816
|
+
role: "status",
|
|
817
|
+
children: text.wait
|
|
818
|
+
}),
|
|
819
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
820
|
+
type: "button",
|
|
821
|
+
disabled: loading || busy,
|
|
822
|
+
onClick: () => void load(true),
|
|
823
|
+
children: text.refresh
|
|
824
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
825
|
+
type: "submit",
|
|
826
|
+
className: "dsh-worktree-primary",
|
|
827
|
+
disabled,
|
|
828
|
+
children: busy ? text.busy : text.submit
|
|
829
|
+
})] })
|
|
830
|
+
]
|
|
831
|
+
})]
|
|
832
|
+
}),
|
|
833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
834
|
+
ref: errorDialog,
|
|
835
|
+
className: "dsh-worktree-dialog dsh-worktree-error-dialog",
|
|
836
|
+
"aria-labelledby": errorTitle,
|
|
837
|
+
"aria-describedby": errorDescription,
|
|
838
|
+
onClose: restoreErrorFocus,
|
|
215
839
|
children: [
|
|
216
840
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
217
|
-
id:
|
|
218
|
-
children: text.
|
|
841
|
+
id: errorTitle,
|
|
842
|
+
children: text.errorTitle
|
|
219
843
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
220
844
|
type: "button",
|
|
221
845
|
"aria-label": text.close,
|
|
222
|
-
onClick: () =>
|
|
223
|
-
children: "
|
|
846
|
+
onClick: () => errorDialog.current?.close(),
|
|
847
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, { "aria-hidden": "true" })
|
|
224
848
|
})] }),
|
|
225
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
children: [
|
|
229
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
|
|
230
|
-
className: "dsh-worktree-sr",
|
|
231
|
-
children: text.mode
|
|
232
|
-
}),
|
|
233
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
234
|
-
type: "radio",
|
|
235
|
-
name: field,
|
|
236
|
-
checked: mode === "create",
|
|
237
|
-
onChange: () => {
|
|
238
|
-
setMode("create");
|
|
239
|
-
setValue("");
|
|
240
|
-
setError(false);
|
|
241
|
-
}
|
|
242
|
-
}), text.create] }),
|
|
243
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
244
|
-
type: "radio",
|
|
245
|
-
name: field,
|
|
246
|
-
checked: mode === "reuse",
|
|
247
|
-
onChange: () => {
|
|
248
|
-
setMode("reuse");
|
|
249
|
-
setValue("");
|
|
250
|
-
setError(false);
|
|
251
|
-
}
|
|
252
|
-
}), text.reuse] })
|
|
253
|
-
]
|
|
849
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
850
|
+
id: errorDescription,
|
|
851
|
+
children: error?.uncertain ? text.error : text.readError
|
|
254
852
|
}),
|
|
255
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
value,
|
|
269
|
-
disabled: busy,
|
|
270
|
-
required: mode === "reuse",
|
|
271
|
-
maxLength: mode === "create" ? 200 : 4096,
|
|
272
|
-
autoComplete: "off",
|
|
273
|
-
spellCheck: false,
|
|
274
|
-
placeholder: mode === "create" ? "fix-login" : "/path/to/worktree",
|
|
275
|
-
onChange: (e) => setValue(e.target.value),
|
|
276
|
-
onKeyDown: (e) => {
|
|
277
|
-
if (e.key === "Enter" && (e.nativeEvent.isComposing || e.keyCode === 229)) e.preventDefault();
|
|
278
|
-
}
|
|
279
|
-
}),
|
|
280
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
281
|
-
className: "dsh-worktree-hint",
|
|
282
|
-
children: mode === "create" ? text.createHint : text.reuseHint
|
|
283
|
-
}),
|
|
284
|
-
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
285
|
-
role: "alert",
|
|
286
|
-
children: text.error
|
|
287
|
-
}),
|
|
288
|
-
unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
289
|
-
role: "status",
|
|
290
|
-
children: text.wait
|
|
291
|
-
}),
|
|
292
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", { children: [mode === "reuse" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
293
|
-
type: "button",
|
|
294
|
-
disabled,
|
|
295
|
-
onClick: () => void send("list"),
|
|
296
|
-
children: text.list
|
|
297
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
298
|
-
type: "submit",
|
|
299
|
-
className: "dsh-worktree-primary",
|
|
300
|
-
disabled: disabled || mode === "reuse" && !value.trim(),
|
|
301
|
-
children: busy ? text.sending : text.send
|
|
302
|
-
})] })
|
|
303
|
-
]
|
|
304
|
-
})
|
|
853
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.reasons[error.code] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
854
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.errorOperation }),
|
|
855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: text.operations[error.operation] }),
|
|
856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.errorCode }),
|
|
857
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: error.code }) })
|
|
858
|
+
] })] }),
|
|
859
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
860
|
+
type: "button",
|
|
861
|
+
"data-worktree-retry": true,
|
|
862
|
+
disabled: loading || busy,
|
|
863
|
+
onClick: () => void load(true),
|
|
864
|
+
children: loading ? text.loading : text.refresh
|
|
865
|
+
}) })
|
|
305
866
|
]
|
|
306
867
|
})
|
|
307
868
|
]
|
|
@@ -312,6 +873,13 @@ function apply(ctx) {
|
|
|
312
873
|
id: "dungsil-ui-worktree",
|
|
313
874
|
order: -100
|
|
314
875
|
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, { ...props }, props.sessionId)));
|
|
876
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
877
|
+
name: "settings.section",
|
|
878
|
+
id: name,
|
|
879
|
+
order: 16,
|
|
880
|
+
label: () => settingsLabel(ctx.locale.getSnapshot().active),
|
|
881
|
+
inject: () => ({ ctx })
|
|
882
|
+
}, WorktreeSettingsSection));
|
|
315
883
|
ctx.effect(() => {
|
|
316
884
|
const style = document.createElement("style");
|
|
317
885
|
style.textContent = styles;
|