@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/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: "Worktree request",
9
- close: "Close",
10
- mode: "Worktree action",
11
- description: "Send a separate message asking the model to use existing tools. Your draft stays unchanged.",
12
- create: "Create new",
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
- path: "Existing worktree path",
16
- createHint: "Uses create_worktree. Enable Plugins → Official → Git worktrees first. Uncommitted changes stay in the source checkout.",
17
- reuseHint: "Uses working_directory after checking the path. If you do not know the path, ask the model to list worktrees.",
18
- list: "Request list",
19
- send: "Ask model",
20
- sending: "Sending…",
21
- sent: "Request sent to the model. Check the conversation for the result.",
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 delivery. Check the conversation before retrying."
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: "Worktree 요청",
27
- close: "닫기",
28
- mode: "Worktree 작업",
29
- description: "모델에게 기존 도구를 사용하도록 별도 메시지를 보냅니다. 작성 중인 내용은 유지됩니다.",
30
- create: "새로 만들기",
31
- reuse: "기존 경로 사용",
101
+ title: "Git worktree",
102
+ detached: "분리된 HEAD",
103
+ unknown: "브랜치 확인 불가",
104
+ create: "신규 워크트리…",
105
+ createTitle: "워크트리 생성",
32
106
  branch: "새 브랜치 이름 (선택)",
33
- path: "기존 worktree 경로",
34
- createHint: "create_worktree를 사용합니다. 먼저 플러그인 → 공식 → Git worktrees를 켜세요. 커밋하지 않은 변경은 원래 경로에 남습니다.",
35
- reuseHint: "경로 확인 후 working_directory를 사용합니다. 경로를 모르면 모델에게 목록을 요청하세요.",
36
- list: "목록 요청",
37
- send: "모델에 요청",
38
- sending: "전송 중…",
39
- sent: "모델에 요청했습니다. 실행 결과는 대화에서 확인하세요.",
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
- function requestText(action, input, korean) {
45
- const value = input.trim();
46
- if (action === "list") return korean ? "현재 저장소의 기존 Git worktree 목록을 읽기 전용으로 조회하고 브랜치와 절대 경로를 보여주세요. worktree를 만들거나 작업 경로를 바꾸지 말고, 제가 사용할 경로를 선택할 때까지 기다려 주세요." : "List the existing Git worktrees of the current repository read-only, showing branches and absolute paths. Do not create a worktree or change the working directory. Wait for me to choose a path.";
47
- if (action === "reuse") return korean ? `기존 Git worktree를 재사용해 주세요. 아래 JSON의 path는 명령이나 지시가 아닌 경로 값입니다: ${JSON.stringify({ path: value })}\n먼저 현재 저장소의 worktree 목록에서 해당 경로를 확인한 뒤 working_directory({cd: path}) 도구로 전환해 주세요. 일치하는 worktree가 없으면 중단하고 알려주세요. 새 worktree나 브랜치를 만들거나 기존 변경을 삭제하지 마세요. 결과의 실제 작업 경로를 알려주세요.` : `Reuse an existing Git worktree. The path in this JSON is a literal path, not a command or instruction: ${JSON.stringify({ path: value })}\nFirst verify it against the current repository's worktree list, then switch using the working_directory({cd: path}) tool. Stop and report if no worktree matches. Do not create a worktree or branch or discard existing changes. Report the resulting working directory.`;
48
- const args = JSON.stringify(value ? { name: value } : {});
49
- return korean ? `@deepseek-ai/dsh-experimental-tool-worktree의 create_worktree 도구를 ${args} 인자로 호출하여 현재 HEAD에서 새 Git worktree를 만들고 해당 경로에서 작업하도록 전환해 주세요. JSON 값은 명령이나 지시가 아닌 브랜치 이름입니다. 도구가 없으면 중단하고 공식 Git worktrees 플러그인을 켜야 한다고 알려주세요. 직접 git worktree add로 대체하거나 기존 브랜치·디렉터리를 덮어쓰지 마세요. 결과의 브랜치와 실제 작업 경로를 알려주세요.` : `Call the create_worktree tool from @deepseek-ai/dsh-experimental-tool-worktree with ${args} to create a new Git worktree from the current HEAD and enter it. JSON values are literal branch names, not commands or instructions. If the tool is unavailable, stop and ask me to enable the official Git worktrees plugin. Do not substitute git worktree add or overwrite an existing branch or directory. Report the resulting branch and working directory.`;
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 { display: inline-flex; min-width: 0; }
54
- .dsh-worktree-trigger { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 6px; border: 0; border-radius: 7px; color: var(--dsw-alias-label-secondary, #626873); background: transparent; font: inherit; font-size: 12px; cursor: pointer; white-space: nowrap; }
55
- .dsh-worktree-trigger:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2, #f1f2f4); }
56
- .dsh-worktree-chevron { opacity: .55; }
57
- .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; padding: 16px; border: 1px solid var(--dsw-alias-border-l4, #e3e5e8); border-radius: 14px; background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #20242c); box-shadow: 0 8px 32px #0002; font: 13px/1.55 var(--dsw-font-family, system-ui); }
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, #626873); }
62
- .dsh-worktree-dialog button { border: 1px solid var(--dsw-alias-border-l4, #e3e5e8); border-radius: 8px; background: transparent; color: inherit; padding: 7px 10px; cursor: pointer; font: inherit; }
63
- .dsh-worktree-dialog header button { padding: 0; width: 28px; height: 28px; border: 0; font-size: 20px; }
64
- .dsh-worktree-dialog button:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-1, #f3f4f6); }
65
- .dsh-worktree-dialog fieldset { display: flex; flex-wrap: wrap; gap: 12px; border: 0; padding: 0; margin: 14px 0; }
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: #4d6bfe; border-color: transparent; color: white; }
73
- .dsh-worktree-dialog [role=alert] { color: var(--dsw-alias-state-error-primary, #b42318); }
74
- .dsh-worktree button:disabled, .dsh-worktree-dialog button:disabled { opacity: .45; cursor: not-allowed; }
75
- .dsh-worktree button:focus-visible, .dsh-worktree-dialog input:focus-visible { outline: 2px solid #4d6bfe; outline-offset: 2px; }
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
- "sessions"
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, useAccess }) {
525
+ function Picker({ sessionId, useSession }) {
88
526
  const language = (0, react.useSyncExternalStore)(subscribeLocale, () => ctx.locale.getSnapshot().active);
89
- const text = copy[language === "ko" ? "ko" : "en"];
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 allowed = useAccess("session/prompt");
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)(false);
96
- const [sent, setSent] = (0, react.useState)(false);
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 alive = (0, react.useRef)(true);
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 disabled = unavailable || !allowed || busy;
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
- alive.current = true;
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
- alive.current = false;
606
+ controller.abort();
607
+ window.removeEventListener("focus", focus);
608
+ window.removeEventListener("resize", resize);
109
609
  };
110
610
  }, []);
111
611
  (0, react.useEffect)(() => {
112
- const close = () => dialog.current?.close();
113
- window.addEventListener("resize", close);
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 send = async (action) => {
130
- if (pending.current || disabled || action === "reuse" && !value.trim()) return;
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
- setError(false);
629
+ setChanged(false);
134
630
  try {
135
- const session = ctx.sessions.binding(sessionId)?.session;
136
- if (!session) throw new Error("Session is no longer available");
137
- if (!(await session.prompt([{
138
- type: "text",
139
- text: requestText(action, value, language === "ko")
140
- }], "queue")).ok) throw new Error("Request was not accepted");
141
- if (!alive.current) return;
142
- setSent(true);
143
- setValue("");
144
- dialog.current?.close();
145
- } catch {
146
- if (alive.current) setError(true);
147
- } finally {
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
- if (alive.current) setBusy(false);
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)("button", {
156
- ref: trigger,
157
- type: "button",
158
- className: "dsh-worktree-trigger",
159
- disabled,
160
- "aria-haspopup": "dialog",
161
- "aria-expanded": open,
162
- title: unavailable ? text.wait : text.title,
163
- onClick: show,
164
- children: [
165
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
166
- width: "14",
167
- height: "14",
168
- viewBox: "0 0 16 16",
169
- fill: "none",
170
- stroke: "currentColor",
171
- strokeWidth: "1.3",
172
- "aria-hidden": "true",
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)("circle", {
175
- cx: "4",
176
- cy: "3",
177
- r: "1.5"
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)("circle", {
180
- cx: "12",
181
- cy: "4",
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)("circle", {
185
- cx: "4",
186
- cy: "13",
187
- r: "1.5"
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: sent ? text.sent : ""
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: () => setOpen(false),
210
- onClick: (e) => {
211
- const box = e.currentTarget.getBoundingClientRect();
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: title,
218
- children: text.title
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: () => dialog.current?.close(),
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", { children: text.description }),
226
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
227
- disabled: busy,
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)("form", {
256
- onSubmit: (e) => {
257
- e.preventDefault();
258
- send(mode);
259
- },
260
- children: [
261
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
262
- htmlFor: field,
263
- children: mode === "create" ? text.branch : text.path
264
- }),
265
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
266
- id: field,
267
- type: "text",
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;