@dungsil/dsh-plugin-ui-worktree 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ DSH 입력창의 권한 선택 옆에 **Worktree 선택 상자**를 추가합니
8
8
  2. 입력창의 선택 상자에서 현재 체크아웃과 기존 worktree의 이름을 확인합니다. 접두사와 경로는 표시하지 않습니다.
9
9
  3. **새 워크트리**를 선택합니다. 별도 입력 창 없이 **새 워크트리**로 표시하며, 이때는 브랜치나 경로를 만들지 않습니다. 다음 일반 프롬프트를 전송하면 해당 프롬프트로 이름을 정하고 생성·전환합니다. 대화 영역에서 단계별 진행 상태와 소요 시간을 확인할 수 있으며, 생성과 전환이 끝난 뒤에 실제 브랜치 이름을 표시합니다.
10
10
  4. 기존 worktree를 선택하면 같은 저장소에 등록된 경로인지 확인한 뒤 즉시 전환합니다. 생성 예약이 있다면 취소하며, 현재 체크아웃을 다시 선택해도 예약을 취소할 수 있습니다.
11
- 5. 오류가 발생하면 **조회 실패** 또는 **변경 확인 필요** 버튼을 눌러 상세 창을 엽니다. **워크트리 새로고침**으로 실제 상태를 확인한 뒤 기존 worktree를 선택하거나 다시 예약합니다. 생성에 실패하면 모델 작업을 시작하지 않으므로 프롬프트를 다시 전송해야 합니다. 다음 프롬프트나 새로고침만으로 생성을 자동 재시도하지 않습니다.
11
+ 5. 오류가 발생하면 선택 상자 안에 **조회 실패** 또는 **변경 확인 필요**를 표시하며, 선택 상자를 누르면 상세 창을 엽니다. 선택 상자 옆에는 별도 오류 라벨을 표시하지 않습니다. **워크트리 새로고침**으로 실제 상태를 확인한 뒤 기존 worktree를 선택하거나 다시 예약합니다. 생성에 실패하면 모델 작업을 시작하지 않으므로 프롬프트를 다시 전송해야 합니다. 다음 프롬프트나 새로고침만으로 생성을 자동 재시도하지 않습니다.
12
12
 
13
13
  ## 동작 범위
14
14
 
package/dist/client.js CHANGED
@@ -3,6 +3,147 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  let react = require("react");
4
4
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
5
  let react_jsx_runtime = require("react/jsx-runtime");
6
+ // ../../tools/ui/src/client.ts
7
+ function useLocaleSnapshot(locale) {
8
+ const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
9
+ const getSnapshot = (0, react.useCallback)(() => locale.getSnapshot(), [locale]);
10
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
11
+ }
12
+ /** Each effect owns its sheet, even when another plugin installs the same defaults. */
13
+ function installStyles(css) {
14
+ const style = document.createElement("style");
15
+ style.textContent = css;
16
+ document.head.append(style);
17
+ return () => style.remove();
18
+ }
19
+ // ../../tools/ui/src/icons/git-branch.svg
20
+ var git_branch_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"6\" cy=\"5\" r=\"3\"/>\n <circle cx=\"6\" cy=\"19\" r=\"3\"/>\n <circle cx=\"18\" cy=\"6\" r=\"3\"/>\n <path d=\"M6 8v8M6 14h7a5 5 0 0 0 5-5\"/>\n</svg>\n";
21
+ // ../../tools/ui/src/composer.tsx
22
+ const composerStyles = `
23
+ :where(.dsh-plugin-composer-trigger) { display: flex; align-items: center; gap: 4px; min-width: 0; 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; line-height: 20px; cursor: pointer; }
24
+ :where(.dsh-plugin-composer-trigger):disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
25
+ .dsh-plugin-composer-icon, .dsh-plugin-composer-chevron { display: inline-flex; flex: none; }
26
+ .dsh-plugin-composer-label, .dsh-plugin-composer-detail { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
+ .dsh-plugin-composer-chevron, .dsh-plugin-composer-detail { color: var(--dsw-alias-label-caption); }
28
+ .dsh-plugin-composer-trigger[aria-expanded=true] .dsh-plugin-composer-chevron { transform: rotate(180deg); }
29
+ `;
30
+ const ComposerTrigger = (0, react.forwardRef)(function ComposerTrigger({ className = "", icon, label, detail, ...props }, ref) {
31
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32
+ ...props,
33
+ ref,
34
+ type: "button",
35
+ className: `dsh-plugin-composer-trigger ${className}`,
36
+ children: [
37
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
38
+ className: "dsh-plugin-composer-icon",
39
+ "aria-hidden": "true",
40
+ children: icon
41
+ }),
42
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
43
+ className: "dsh-plugin-composer-label",
44
+ children: label
45
+ }),
46
+ detail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47
+ className: "dsh-plugin-composer-detail",
48
+ children: detail
49
+ }),
50
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
51
+ className: "dsh-plugin-composer-chevron",
52
+ "aria-hidden": "true",
53
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})
54
+ })
55
+ ]
56
+ });
57
+ });
58
+ // ../../tools/ui/src/settings.tsx
59
+ /** The rc.5 settings shell has no icon slot, so decorate only the matching navigation rows. */
60
+ function installSettingsIcon(id, labels, svg) {
61
+ const attribute = "data-dsh-settings-icon";
62
+ const selector = `[${attribute}="${id}"]`;
63
+ const removeStyles = installStyles(`
64
+ ${selector} > svg { display: none; }
65
+ ${selector}::before { content: ''; display: block; flex: none; width: 16px; height: 16px; background: currentColor; mask: url("data:image/svg+xml,${encodeURIComponent(svg)}") center / contain no-repeat; }
66
+ `);
67
+ const marked = /* @__PURE__ */ new Set();
68
+ const refresh = () => {
69
+ const rows = new Set(document.querySelectorAll("[data-shortcut-modal=\"settings\"] nav button"));
70
+ for (const row of marked) if (!rows.has(row) || !labels.includes(row.textContent?.trim() ?? "")) {
71
+ row.removeAttribute(attribute);
72
+ marked.delete(row);
73
+ }
74
+ for (const row of rows) if (labels.includes(row.textContent?.trim() ?? "")) {
75
+ row.setAttribute(attribute, id);
76
+ marked.add(row);
77
+ }
78
+ };
79
+ const observer = new MutationObserver(refresh);
80
+ observer.observe(document.body, {
81
+ childList: true,
82
+ subtree: true,
83
+ characterData: true
84
+ });
85
+ refresh();
86
+ return () => {
87
+ observer.disconnect();
88
+ for (const row of marked) row.removeAttribute(attribute);
89
+ removeStyles();
90
+ };
91
+ }
92
+ const settingsStyles = `
93
+ .dsh-plugin-settings-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
94
+ .dsh-plugin-settings-text { flex: 1; min-width: 0; }
95
+ .dsh-plugin-settings-picker { min-width: 0; }
96
+ .dsh-plugin-settings-selector { box-sizing: border-box; display: inline-flex; align-items: center; gap: 12px; max-width: 100%; height: 36px; padding: 0 14px; border: none; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); color: inherit; font: inherit; cursor: pointer; }
97
+ .dsh-plugin-settings-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
98
+ .dsh-plugin-settings-selector svg { flex: none; }
99
+ .dsh-plugin-settings-selector:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
100
+ .dsh-plugin-settings-selector:disabled { opacity: .5; cursor: default; }
101
+ .dsh-plugin-settings-selector:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary-new-colorprimary-new-color); outline-offset: 2px; }
102
+ `;
103
+ function SettingsRow({ label, children }) {
104
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
105
+ className: "dsh-plugin-settings-row",
106
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
107
+ className: "dsh-plugin-settings-text",
108
+ children: label
109
+ }), children]
110
+ });
111
+ }
112
+ function SettingsPicker({ className = "", value, selectedId, items, onSelect, disabled = false, ...buttonProps }) {
113
+ const [open, setOpen] = (0, react.useState)(false);
114
+ (0, react.useEffect)(() => {
115
+ if (disabled) setOpen(false);
116
+ }, [disabled]);
117
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
118
+ className: `dsh-plugin-settings-picker ${className}`,
119
+ open: open && !disabled,
120
+ onClose: () => setOpen(false),
121
+ items,
122
+ selectedId,
123
+ onSelect: (id) => {
124
+ setOpen(false);
125
+ if (!disabled) onSelect(id);
126
+ },
127
+ align: "end",
128
+ portal: true,
129
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
130
+ ...buttonProps,
131
+ type: "button",
132
+ className: "dsh-plugin-settings-selector",
133
+ "aria-haspopup": "menu",
134
+ "aria-expanded": open && !disabled,
135
+ disabled,
136
+ onClick: () => setOpen((current) => !current),
137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
138
+ className: "dsh-plugin-settings-value",
139
+ children: value
140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
141
+ size: 16,
142
+ "aria-hidden": "true"
143
+ })]
144
+ })
145
+ });
146
+ }
6
147
  // src/protocol.ts
7
148
  const channel = "/api";
8
149
  const endpointPrefix = "dungsil-worktree/";
@@ -192,6 +333,9 @@ const copy$1 = {
192
333
  const copy = {
193
334
  ko: {
194
335
  nav: "워크트리",
336
+ defaultSelection: "기본 선택",
337
+ current: "현재 체크아웃",
338
+ new: "새 워크트리",
195
339
  directory: "워크트리 풀 디렉터리",
196
340
  remote: "추적 중인 원격 브랜치 사용",
197
341
  naming: "자동 브랜치 이름 형식",
@@ -209,6 +353,9 @@ const copy = {
209
353
  },
210
354
  zh: {
211
355
  nav: "工作树",
356
+ defaultSelection: "默认选择",
357
+ current: "当前检出",
358
+ new: "新工作树",
212
359
  directory: "工作树池目录",
213
360
  remote: "使用跟踪的远程分支",
214
361
  naming: "自动分支命名格式",
@@ -226,6 +373,9 @@ const copy = {
226
373
  },
227
374
  en: {
228
375
  nav: "Worktrees",
376
+ defaultSelection: "Default selection",
377
+ current: "Current checkout",
378
+ new: "New worktree",
229
379
  directory: "Worktree pool directory",
230
380
  remote: "Use tracked remote branch",
231
381
  naming: "Automatic branch naming",
@@ -249,14 +399,13 @@ const validPrefix = (value) => value.length <= 100 && (value === "" || /^[a-zA-Z
249
399
  const validSettings = (settings) => validDirectory(settings.directory) && validPrefix(settings.prefix) && (settings.naming !== "custom" || !!settings.prefix);
250
400
  const sameSettings = (left, right) => JSON.stringify(left) === JSON.stringify(right);
251
401
  function WorktreeSettingsSection({ ctx }) {
252
- const language = (0, react.useSyncExternalStore)((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getSnapshot().active);
402
+ const { active: language } = useLocaleSnapshot(ctx.locale);
253
403
  const text = copy[languageKey(language)];
254
404
  const id = (0, react.useId)();
255
405
  const connection = ctx.get("connection");
256
406
  const [draft, setDraft] = (0, react.useState)();
257
407
  const [operation, setOperation] = (0, react.useState)("loading");
258
408
  const [notice, setNotice] = (0, react.useState)();
259
- const [namingOpen, setNamingOpen] = (0, react.useState)(false);
260
409
  const saved = (0, react.useRef)(void 0);
261
410
  const latest = (0, react.useRef)(void 0);
262
411
  const pending = (0, react.useRef)(false);
@@ -277,7 +426,6 @@ function WorktreeSettingsSection({ ctx }) {
277
426
  if (!controller || controller.signal.aborted || pending.current) return;
278
427
  pending.current = true;
279
428
  blocked.current = true;
280
- setNamingOpen(false);
281
429
  setOperation("loading");
282
430
  try {
283
431
  await read(controller);
@@ -327,7 +475,6 @@ function WorktreeSettingsSection({ ctx }) {
327
475
  } finally {
328
476
  if (!controller.signal.aborted) {
329
477
  if (blocked.current) {
330
- setNamingOpen(false);
331
478
  setOperation("loading");
332
479
  try {
333
480
  await read(controller);
@@ -383,19 +530,37 @@ function WorktreeSettingsSection({ ctx }) {
383
530
  draft && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
384
531
  disabled,
385
532
  children: [
386
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
387
- className: "dsh-worktree-settings-row",
388
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
389
- className: "dsh-worktree-settings-text",
390
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
391
- htmlFor: `${id}-directory`,
392
- children: text.directory
393
- }), invalidDirectory && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
394
- id: `${id}-directory-error`,
395
- role: "alert",
396
- children: text.invalidDirectory
397
- })]
398
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
534
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
535
+ id: `${id}-default-selection`,
536
+ className: "dsh-worktree-settings-title",
537
+ children: text.defaultSelection
538
+ }),
539
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsPicker, {
540
+ className: "dsh-worktree-settings-picker",
541
+ disabled,
542
+ "aria-labelledby": `${id}-default-selection`,
543
+ value: text[draft.defaultSelection],
544
+ selectedId: draft.defaultSelection,
545
+ items: ["current", "new"].map((id) => ({
546
+ id,
547
+ label: text[id]
548
+ })),
549
+ onSelect: (defaultSelection) => {
550
+ change({ defaultSelection });
551
+ }
552
+ })
553
+ }),
554
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
555
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
556
+ htmlFor: `${id}-directory`,
557
+ children: text.directory
558
+ }), invalidDirectory && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
559
+ id: `${id}-directory-error`,
560
+ role: "alert",
561
+ children: text.invalidDirectory
562
+ })] }),
563
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
399
564
  className: "dsh-worktree-settings-input",
400
565
  id: `${id}-directory`,
401
566
  type: "text",
@@ -407,36 +572,31 @@ function WorktreeSettingsSection({ ctx }) {
407
572
  "aria-invalid": invalidDirectory || void 0,
408
573
  "aria-describedby": invalidDirectory ? `${id}-directory-error` : void 0,
409
574
  onChange: (event) => change({ directory: event.target.value })
410
- })]
575
+ })
411
576
  }),
412
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
413
- className: "dsh-worktree-settings-row",
414
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
415
- className: "dsh-worktree-settings-text",
416
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
417
- className: "dsh-worktree-settings-title",
418
- children: text.remote
419
- })
420
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
578
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
579
+ className: "dsh-worktree-settings-title",
580
+ children: text.remote
581
+ }),
582
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
421
583
  label: text.remote,
422
584
  checked: draft.remote,
423
585
  disabled,
424
586
  onChange: (remote) => change({ remote })
425
- })]
587
+ })
426
588
  }),
427
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
428
- className: "dsh-worktree-settings-row",
429
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
430
- className: "dsh-worktree-settings-text",
431
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
432
- id: `${id}-naming`,
433
- className: "dsh-worktree-settings-title",
434
- children: text.naming
435
- })
436
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
437
- open: namingOpen,
438
- align: "end",
439
- portal: true,
589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
590
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
591
+ id: `${id}-naming`,
592
+ className: "dsh-worktree-settings-title",
593
+ children: text.naming
594
+ }),
595
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsPicker, {
596
+ className: "dsh-worktree-settings-picker",
597
+ disabled,
598
+ "aria-labelledby": `${id}-naming`,
599
+ value: text[draft.naming],
440
600
  selectedId: draft.naming,
441
601
  items: [
442
602
  "semantic",
@@ -446,39 +606,21 @@ function WorktreeSettingsSection({ ctx }) {
446
606
  id,
447
607
  label: text[id]
448
608
  })),
449
- onClose: () => setNamingOpen(false),
450
609
  onSelect: (naming) => {
451
- setNamingOpen(false);
452
- if (!disabled) change({ naming });
453
- },
454
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
455
- type: "button",
456
- className: "dsh-worktree-settings-selector",
457
- disabled,
458
- "aria-labelledby": `${id}-naming`,
459
- "aria-haspopup": "menu",
460
- "aria-expanded": namingOpen,
461
- onClick: () => setNamingOpen((open) => !open),
462
- 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, {
463
- size: 16,
464
- "aria-hidden": "true"
465
- })]
466
- })
467
- })]
610
+ change({ naming });
611
+ }
612
+ })
468
613
  }),
469
- draft.naming === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
470
- className: "dsh-worktree-settings-row",
471
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
472
- className: "dsh-worktree-settings-text",
473
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
474
- htmlFor: `${id}-prefix`,
475
- children: text.prefix
476
- }), invalidPrefix && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
477
- id: `${id}-prefix-error`,
478
- role: "alert",
479
- children: text.invalidPrefix
480
- })]
481
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
614
+ draft.naming === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
615
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
616
+ htmlFor: `${id}-prefix`,
617
+ children: text.prefix
618
+ }), invalidPrefix && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
619
+ id: `${id}-prefix-error`,
620
+ role: "alert",
621
+ children: text.invalidPrefix
622
+ })] }),
623
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
482
624
  className: "dsh-worktree-settings-input",
483
625
  id: `${id}-prefix`,
484
626
  type: "text",
@@ -491,7 +633,7 @@ function WorktreeSettingsSection({ ctx }) {
491
633
  autoComplete: "off",
492
634
  spellCheck: false,
493
635
  onChange: (event) => change({ prefix: event.target.value })
494
- })]
636
+ })
495
637
  })
496
638
  ]
497
639
  }),
@@ -508,30 +650,22 @@ function WorktreeSettingsSection({ ctx }) {
508
650
  });
509
651
  }
510
652
  // src/styles.ts
511
- const styles = `
653
+ const styles = `${settingsStyles}${composerStyles}
512
654
  .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; }
513
655
  .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; }
514
- .dsh-worktree-controls { display: inline-flex; flex-wrap: nowrap; align-items: center; min-width: 0; max-width: 100%; }
515
656
  .dsh-worktree-picker { display: inline-flex; min-width: 0; max-width: 100%; }
516
- .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; }
517
- .dsh-worktree-icon, .dsh-worktree-chevron { display: inline-flex; flex: none; }
518
- .dsh-worktree-icon svg { width: 14px; height: 14px; }
519
- .dsh-worktree-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
520
- .dsh-worktree-chevron { color: var(--dsw-alias-label-caption); }
521
- .dsh-worktree-trigger[aria-expanded=true] .dsh-worktree-chevron { transform: rotate(180deg); }
522
- .dsh-worktree-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
657
+ .dsh-worktree-trigger { display: inline-flex; max-width: 220px; font-weight: 500; }
658
+ .dsh-worktree-trigger .dsh-plugin-composer-icon svg { width: 14px; height: 14px; }
523
659
  .dsh-worktree-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
524
660
  @container (width <= 460px) {
525
- .dsh-worktree-name { display: none; }
661
+ .dsh-worktree-trigger .dsh-plugin-composer-label { display: none; }
526
662
  }
527
663
  @media (prefers-reduced-motion: no-preference) {
528
- .dsh-worktree-chevron { transition: transform .12s; }
664
+ .dsh-worktree-trigger .dsh-plugin-composer-chevron { transition: transform .12s; }
529
665
  }
530
666
  @media (hover: hover) {
531
- .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); }
667
+ .dsh-worktree-trigger:hover:not(:disabled), .dsh-worktree-dialog button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
532
668
  }
533
- .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; }
534
- .dsh-worktree-error-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
535
669
  .dsh-worktree-dialog.dsh-worktree-error-dialog { inset: 0; margin: auto; }
536
670
  .dsh-worktree-error-dialog dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 12px; }
537
671
  .dsh-worktree-error-dialog dt { color: var(--dsw-alias-label-secondary); }
@@ -547,40 +681,35 @@ const styles = `
547
681
  .dsh-worktree-dialog button:disabled { opacity: .45; cursor: not-allowed; }
548
682
  .dsh-worktree-dialog button:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
549
683
  .dsh-worktree-sr { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
550
- .dsh-worktree-progress { margin: 12px 0 20px; color: var(--dsw-alias-label-secondary); font: 13px/1.6 ui-monospace, monospace; }
684
+ .dsh-worktree-progress { margin: 12px 0 20px; color: var(--dsw-alias-label-secondary); font: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); }
551
685
  .dsh-worktree-progress > p { margin: 0 0 8px; overflow-wrap: anywhere; }
552
686
  .dsh-worktree-progress ol { list-style: none; margin: 0; padding: 8px 0 0; border-top: 1px solid var(--dsw-alias-border-l2); }
553
687
  .dsh-worktree-progress li { display: flex; align-items: center; gap: 10px; padding: 3px 4px; }
554
688
  .dsh-worktree-progress li[data-state=pending] { opacity: .4; }
555
- .dsh-worktree-progress-mark { box-sizing: border-box; display: inline-flex; flex: none; align-items: center; justify-content: center; width: 14px; height: 14px; border: 1px solid currentColor; border-radius: 50%; font-size: 11px; line-height: 1; }
556
- .dsh-worktree-progress li[data-state=running] .dsh-worktree-progress-mark { border-right-color: transparent; }
689
+ .dsh-worktree-progress-mark { display: inline-flex; flex: none; align-items: center; justify-content: center; width: calc(16px + var(--dsh-content-font-delta, 0px)); height: calc(16px + var(--dsh-content-font-delta, 0px)); }
690
+ .dsh-worktree-progress-mark svg { width: calc(14px + var(--dsh-content-font-delta, 0px)); height: calc(14px + var(--dsh-content-font-delta, 0px)); }
557
691
  .dsh-worktree-progress li[data-state=failed], .dsh-worktree-progress [role=alert] { color: var(--dsw-alias-state-error-primary); }
558
692
  .dsh-worktree-progress [role=alert] { margin-top: 10px; font: inherit; }
559
- .dsh-worktree-progress-time { margin-inline-start: auto; padding-inline-start: 8px; font-size: 11px; }
693
+ .dsh-worktree-progress-time { margin-inline-start: auto; padding-inline-start: 8px; font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px); font-variant-numeric: tabular-nums; }
560
694
  @keyframes dsh-worktree-spin { to { transform: rotate(360deg); } }
561
695
  @media (prefers-reduced-motion: no-preference) {
562
696
  .dsh-worktree-progress li[data-state=running] .dsh-worktree-progress-mark { animation: dsh-worktree-spin .8s linear infinite; }
563
697
  }
564
698
  .dsh-worktree-settings { width: 100%; color: var(--dsw-alias-label-primary); font: inherit; }
565
699
  .dsh-worktree-settings fieldset { min-width: 0; padding: 0; margin: 0; border: 0; }
566
- .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); }
567
- .dsh-worktree-settings-row:last-child { border-bottom: 0; }
568
- .dsh-worktree-settings-text { flex: 1; min-width: 0; }
700
+ .dsh-worktree-settings .dsh-plugin-settings-row { gap: 24px; }
701
+ .dsh-worktree-settings .dsh-plugin-settings-row:last-child { border-bottom: 0; }
569
702
  .dsh-worktree-settings label, .dsh-worktree-settings-title { display: block; font-size: 14px; font-weight: 400; line-height: 20px; }
570
- .dsh-worktree-settings-text p { margin: 4px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
703
+ .dsh-worktree-settings .dsh-plugin-settings-text p { margin: 4px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
571
704
  .dsh-worktree-settings-input { box-sizing: border-box; width: 220px; max-width: 100%; flex: 0 1 220px; min-width: 0; }
572
- .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; }
573
- .dsh-worktree-settings-selector span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
574
- .dsh-worktree-settings-selector svg { flex: none; }
575
- .dsh-worktree-settings-selector:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
576
- .dsh-worktree-settings-selector:disabled { opacity: .5; cursor: default; }
577
- .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; }
705
+ .dsh-worktree-settings-picker .dsh-plugin-settings-selector { max-width: 220px; color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 22px; }
706
+ .dsh-worktree-settings-picker .dsh-plugin-settings-selector:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
578
707
  .dsh-worktree-settings [role=status], .dsh-worktree-settings [role=alert] { margin: 12px 0; font-size: 12px; line-height: 18px; }
579
708
  .dsh-worktree-settings [role=status]:empty { margin: 0; }
580
709
  .dsh-worktree-settings [role=alert] { color: var(--dsw-alias-state-error-primary); }
581
710
  @media (max-width: 600px) {
582
- .dsh-worktree-settings-row { flex-wrap: wrap; gap: 12px; }
583
- .dsh-worktree-settings-text { flex-basis: 160px; }
711
+ .dsh-worktree-settings .dsh-plugin-settings-row { flex-wrap: wrap; gap: 12px; }
712
+ .dsh-worktree-settings .dsh-plugin-settings-text { flex-basis: 160px; }
584
713
  .dsh-worktree-settings-input { flex-grow: 1; }
585
714
  }
586
715
  `;
@@ -699,7 +828,7 @@ const progressDefinition = {
699
828
  anchorSeq: context.state.anchorSeq,
700
829
  location: context.start.location,
701
830
  visibility: "visible",
702
- groupTree: "turn",
831
+ groupTree: context.state.progress.status === "completed" ? void 0 : "turn",
703
832
  data: context.state.progress
704
833
  };
705
834
  }
@@ -711,7 +840,7 @@ function installProgress(ctx) {
711
840
  key: "dungsil-worktree-progress",
712
841
  inject: () => ({})
713
842
  }, function Progress({ node }) {
714
- const language = (0, react.useSyncExternalStore)((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getSnapshot().active);
843
+ const { active: language } = useLocaleSnapshot(ctx.locale);
715
844
  const text = labels[language.startsWith("ko") ? "ko" : language.startsWith("zh") ? "zh" : "en"];
716
845
  const progress = node.data;
717
846
  const ended = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn.status === "closed" : false;
@@ -741,7 +870,7 @@ function installProgress(ctx) {
741
870
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
742
871
  className: "dsh-worktree-progress-mark",
743
872
  "aria-hidden": "true",
744
- children: state === "completed" ? "✓" : state === "failed" ? "×" : state === "cancelled" ? "−" : ""
873
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(state === "completed" ? _deepseek_ai_dsh_client_ui_primitives.IconCheckCircleOutlineRegular : state === "failed" ? _deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular : state === "cancelled" ? _deepseek_ai_dsh_client_ui_primitives.IconStopFillRegular : state === "running" ? _deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular : _deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, { size: 14 })
745
874
  }),
746
875
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
747
876
  state !== "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -768,11 +897,15 @@ const inject = [
768
897
  "uiConversation"
769
898
  ];
770
899
  function apply(ctx) {
900
+ ctx.effect(() => installSettingsIcon(name, [
901
+ "en",
902
+ "ko",
903
+ "zh"
904
+ ].map(settingsLabel), git_branch_default));
771
905
  const connection = ctx.get("connection");
772
- const subscribeLocale = (listener) => ctx.locale.subscribe(listener);
773
906
  installProgress(ctx);
774
907
  function Picker({ sessionId, useSession }) {
775
- const language = (0, react.useSyncExternalStore)(subscribeLocale, () => ctx.locale.getSnapshot().active);
908
+ const { active: language } = useLocaleSnapshot(ctx.locale);
776
909
  const text = copy$1[language.startsWith("ko") ? "ko" : language.startsWith("zh") ? "zh" : "en"];
777
910
  const unavailable = useSession((s) => s.running || s.removed || s.openState !== "open" || s.pendingSubmissions.length > 0);
778
911
  const running = useSession((s) => s.running);
@@ -788,7 +921,6 @@ function apply(ctx) {
788
921
  const lifetime = (0, react.useRef)(null);
789
922
  const trigger = (0, react.useRef)(null);
790
923
  const errorDialog = (0, react.useRef)(null);
791
- const errorTrigger = (0, react.useRef)(null);
792
924
  const errorTitle = (0, react.useId)();
793
925
  const errorDescription = (0, react.useId)();
794
926
  const status = (0, react.useId)();
@@ -904,7 +1036,7 @@ function apply(ctx) {
904
1036
  if (!detailError && errorDialog.current?.open) errorDialog.current.close();
905
1037
  }, [error, creation?.failed]);
906
1038
  const restoreErrorFocus = () => {
907
- (errorTrigger.current ?? trigger.current)?.focus();
1039
+ trigger.current?.focus();
908
1040
  };
909
1041
  const current = currentWorktree(view);
910
1042
  (0, react.useEffect)(() => {
@@ -916,79 +1048,57 @@ function apply(ctx) {
916
1048
  view
917
1049
  ]);
918
1050
  const branch = creation ? creation.failed ? text.mutationError : text.create : current ? current.branch ?? text.detached : text.unknown;
1051
+ const label = detailError ? errorLabel : view ? branch : loading ? text.loading : text.title;
919
1052
  const notice = loading ? text.loading : busy ? text.busy : error ? errorLabel : unavailable ? text.wait : creation ? creation.failed ? text.creationError : text.reservationHint : changed ? text.changed : view?.worktrees.length === 0 ? text.empty : "";
920
1053
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
921
1054
  className: "dsh-worktree",
922
1055
  children: [
923
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
924
- className: "dsh-worktree-controls",
925
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
926
- className: "dsh-worktree-picker",
927
- open,
928
- side: "top",
929
- portal: true,
930
- selectedId: creation ? "create" : current ? `path:${current.path}` : void 0,
931
- items: [{
932
- id: "create",
933
- label: text.create,
934
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, {})
935
- }, ...view?.worktrees.map((item) => ({
936
- id: `path:${item.path}`,
937
- label: item.branch ?? text.detached,
938
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {})
939
- })) ?? []],
940
- onClose: () => setOpen(false),
941
- onSelect: (id) => {
942
- setOpen(false);
943
- if (disabled || pending.current) return;
944
- if (id === "create") {
945
- if (!creation || creation.failed) change("create");
946
- } else if (id.startsWith("path:") && (creation || id !== `path:${current?.path}`)) change("switch", id.slice(5));
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1057
+ className: "dsh-worktree-picker",
1058
+ open,
1059
+ side: "top",
1060
+ portal: true,
1061
+ selectedId: creation ? "create" : current ? `path:${current.path}` : void 0,
1062
+ items: [{
1063
+ id: "create",
1064
+ label: text.create,
1065
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, {})
1066
+ }, ...view?.worktrees.map((item) => ({
1067
+ id: `path:${item.path}`,
1068
+ label: item.branch ?? text.detached,
1069
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {})
1070
+ })) ?? []],
1071
+ onClose: () => setOpen(false),
1072
+ onSelect: (id) => {
1073
+ setOpen(false);
1074
+ if (disabled || pending.current) return;
1075
+ if (id === "create") {
1076
+ if (!creation || creation.failed) change("create");
1077
+ } else if (id.startsWith("path:") && (creation || id !== `path:${current?.path}`)) change("switch", id.slice(5));
1078
+ },
1079
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerTrigger, {
1080
+ ref: trigger,
1081
+ className: "dsh-worktree-trigger",
1082
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
1083
+ label,
1084
+ disabled: detailError ? loading || busy : disabled,
1085
+ "aria-label": `${text.title}: ${label}`,
1086
+ "aria-haspopup": detailError ? "dialog" : "menu",
1087
+ "aria-expanded": detailError ? void 0 : open,
1088
+ "aria-describedby": status,
1089
+ "aria-busy": loading || busy,
1090
+ title: label,
1091
+ onClick: () => {
1092
+ if (detailError) errorDialog.current?.showModal();
1093
+ else setOpen((value) => !value);
947
1094
  },
948
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
949
- ref: trigger,
950
- type: "button",
951
- className: "dsh-worktree-trigger",
952
- disabled,
953
- "aria-label": view ? `${text.title}: ${branch}` : text.title,
954
- "aria-haspopup": "menu",
955
- "aria-expanded": open,
956
- "aria-describedby": status,
957
- "aria-busy": loading || busy,
958
- title: view ? branch : text.title,
959
- onClick: () => setOpen((value) => !value),
960
- onKeyDown: (event) => {
961
- if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
962
- event.preventDefault();
963
- setOpen(true);
964
- }
965
- },
966
- children: [
967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
968
- className: "dsh-worktree-icon",
969
- "aria-hidden": "true",
970
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { className: "dsh-worktree-branch" })
971
- }),
972
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
973
- className: "dsh-worktree-name",
974
- children: view ? branch : loading ? text.loading : text.title
975
- }),
976
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
977
- className: "dsh-worktree-chevron",
978
- "aria-hidden": "true",
979
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})
980
- })
981
- ]
982
- })
983
- }), detailError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
984
- ref: errorTrigger,
985
- type: "button",
986
- className: "dsh-worktree-error-trigger",
987
- "aria-haspopup": "dialog",
988
- "aria-label": `${errorLabel}: ${text.errorTitle}`,
989
- onClick: () => errorDialog.current?.showModal(),
990
- children: errorLabel
991
- })]
1095
+ onKeyDown: (event) => {
1096
+ if (!detailError && !disabled && !open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1097
+ event.preventDefault();
1098
+ setOpen(true);
1099
+ }
1100
+ }
1101
+ })
992
1102
  }),
993
1103
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
994
1104
  id: status,
@@ -1046,12 +1156,7 @@ function apply(ctx) {
1046
1156
  label: () => settingsLabel(ctx.locale.getSnapshot().active),
1047
1157
  inject: () => ({ ctx })
1048
1158
  }, WorktreeSettingsSection));
1049
- ctx.effect(() => {
1050
- const style = document.createElement("style");
1051
- style.textContent = styles;
1052
- document.head.append(style);
1053
- return () => style.remove();
1054
- }, "ui-worktree: composer styles");
1159
+ ctx.effect(() => installStyles(styles), "ui-worktree: composer styles");
1055
1160
  }
1056
1161
  exports.apply = apply;
1057
1162
  exports.inject = inject;
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
4
4
  import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { SessionId, appendPluginRecord } from "@deepseek-ai/dsh-session";
5
+ import { SessionId, appendPluginRecord, pluginRecordOf } from "@deepseek-ai/dsh-session";
6
6
  // src/protocol.ts
7
7
  const channel = "/api";
8
8
  const endpointPrefix = "dungsil-worktree/";
9
9
  const progressRecord = "plugin:dungsil/ui-worktree/progress";
10
+ const selectionRecord = "plugin:dungsil/ui-worktree/selection";
10
11
  // src/index.ts
11
12
  const name = "dungsil-ui-worktree";
12
13
  const inject = [
@@ -22,6 +23,7 @@ const inject = [
22
23
  "sandboxPolicy"
23
24
  ];
24
25
  const Config = z.object({
26
+ defaultSelection: z.union(["current", "new"]).default("current"),
25
27
  directory: z.string().default(".agents/worktrees"),
26
28
  remote: z.boolean().default(false),
27
29
  naming: z.union([
@@ -37,7 +39,7 @@ const Config = z.object({
37
39
  function validSettings(value) {
38
40
  if (!value || typeof value !== "object") return false;
39
41
  const input = value;
40
- return typeof input.directory === "string" && input.directory.trim().length > 0 && input.directory.length <= 4096 && !/[\0\r\n]/.test(input.directory) && typeof input.remote === "boolean" && [
42
+ return ["current", "new"].includes(input.defaultSelection) && typeof input.directory === "string" && input.directory.trim().length > 0 && input.directory.length <= 4096 && !/[\0\r\n]/.test(input.directory) && typeof input.remote === "boolean" && [
41
43
  "semantic",
42
44
  "dsh",
43
45
  "custom"
@@ -213,13 +215,22 @@ function apply(ctx, config) {
213
215
  for (const record of output.split("\0\0")) {
214
216
  const fields = record.split("\0");
215
217
  const path = fields.find((field) => field.startsWith("worktree "))?.slice(9);
216
- if (!path || fields.includes("bare")) continue;
218
+ if (!path || fields.includes("bare") || fields.some((field) => /^prunable(?: |$)/.test(field))) continue;
217
219
  const branch = fields.find((field) => field.startsWith("branch refs/heads/"))?.slice(18) ?? null;
218
220
  worktrees.push({
219
221
  path,
220
222
  branch
221
223
  });
222
224
  }
225
+ if (!agent.session.snapshotEvents().some((event) => pluginRecordOf(event)?.type === "plugin:dungsil/ui-worktree/selection")) {
226
+ if (config.defaultSelection === "new" && !creations.has(agent) && !agent.session.deriveMessages().some((message) => ["user", "user-rpc"].includes(message.source.kind))) creations.set(agent, {
227
+ cwd,
228
+ name: "",
229
+ failed: false
230
+ });
231
+ appendPluginRecord(agent.session, selectionRecord, { initialized: true });
232
+ await ctx.sessions.flush(agent.session);
233
+ }
223
234
  const creation = creations.get(agent);
224
235
  return {
225
236
  cwd,
@@ -240,6 +251,7 @@ function apply(ctx, config) {
240
251
  return {
241
252
  revision: createHash("sha256").update(JSON.stringify(current ?? {})).digest("hex"),
242
253
  settings: {
254
+ defaultSelection: current?.defaultSelection ?? "current",
243
255
  directory: current?.directory ?? ".agents/worktrees",
244
256
  remote: current?.remote ?? false,
245
257
  naming: current?.naming ?? "semantic",
@@ -274,33 +286,67 @@ function apply(ctx, config) {
274
286
  "--format=%(refname)",
275
287
  `refs/heads/${branch}`
276
288
  ])) throw new Error("Branch already exists");
277
- const pool = ctx.fs.processPath(await ctx.fs.resolve(config.directory.trim(), {
278
- cwd: root,
279
- signal
280
- }));
281
- const path = ctx.fs.processPath(await ctx.fs.resolve(branch, {
282
- cwd: pool,
283
- signal
284
- }));
285
- await run(agent, cwd, [
289
+ const allocate = async (checkOnly = false) => JSON.parse(await run(agent, cwd, [
286
290
  "--input-type=module",
287
291
  "-e",
288
292
  `
289
- import fs from 'node:fs/promises';
290
- import path from 'node:path';
291
- const [root, destination] = process.argv.slice(1);
292
- await fs.mkdir(path.dirname(root), { recursive: true });
293
- try {
294
- await fs.mkdir(root);
295
- await fs.writeFile(path.join(root, '.gitignore'), '*\\n', { flag: 'wx' });
296
- } catch (error) { if (error.code !== 'EEXIST') throw error; }
297
- await fs.mkdir(path.dirname(destination), { recursive: true });
298
- await fs.mkdir(destination);
299
- `,
300
- pool,
301
- path
302
- ], signal, "node");
293
+ import fs from 'node:fs/promises';
294
+ import path from 'node:path';
295
+ const [source, configured, branch, checkOnly] = process.argv.slice(1);
296
+ let pool = path.resolve(source, configured);
297
+ const fromSource = path.relative(source, pool);
298
+ // An explicitly configured external pool may use owner-selected symlinks.
299
+ // Repository-relative pools must be inspected before following links.
300
+ if (fromSource === '..' || fromSource.startsWith('..' + path.sep) || path.isAbsolute(fromSource)) {
301
+ async function canonicalParent(target) {
302
+ try { return await fs.realpath(target); }
303
+ catch (error) {
304
+ if (error.code !== 'ENOENT') throw error;
305
+ const parent = path.dirname(target);
306
+ if (parent === target) throw error;
307
+ return path.join(await canonicalParent(parent), path.basename(target));
308
+ }
309
+ }
310
+ pool = await canonicalParent(pool);
311
+ }
312
+ const destination = path.resolve(pool, branch);
313
+ const relative = path.relative(pool, destination);
314
+ if (!relative || relative.startsWith('..' + path.sep) || path.isAbsolute(relative))
315
+ throw new Error('Invalid destination');
316
+ async function walk(target, create) {
317
+ let current = path.parse(target).root;
318
+ for (const component of target.slice(current.length).split(path.sep).filter(Boolean)) {
319
+ current = path.join(current, component);
320
+ if (create) {
321
+ try { await fs.mkdir(current); }
322
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
323
+ }
324
+ const info = await fs.lstat(current);
325
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Unsafe directory');
326
+ }
327
+ if (await fs.realpath(target) !== target) throw new Error('Directory changed');
328
+ }
329
+ await walk(path.dirname(pool), !checkOnly);
330
+ if (!checkOnly) {
331
+ try {
332
+ await fs.mkdir(pool);
333
+ await fs.writeFile(path.join(pool, '.gitignore'), '*\\n', { flag: 'wx' });
334
+ } catch (error) { if (error.code !== 'EEXIST') throw error; }
335
+ }
336
+ await walk(pool, false);
337
+ await walk(path.dirname(destination), !checkOnly);
338
+ if (!checkOnly) await fs.mkdir(destination);
339
+ await walk(destination, false);
340
+ process.stdout.write(JSON.stringify(destination));
341
+ `,
342
+ root,
343
+ config.directory.trim(),
344
+ branch,
345
+ checkOnly ? "check" : ""
346
+ ], signal, "node"));
347
+ const path = await allocate();
303
348
  report(2);
349
+ await allocate(true);
304
350
  await git([
305
351
  "worktree",
306
352
  "add",
@@ -311,13 +357,15 @@ function apply(ctx, config) {
311
357
  path,
312
358
  commit
313
359
  ]);
314
- const filters = await git([
360
+ const filters = await run(agent, path, [
361
+ "--work-tree",
362
+ path,
315
363
  "config",
316
364
  "--null",
317
365
  "--name-only",
318
366
  "--get-regexp",
319
367
  "^filter\\."
320
- ], void 0, [0, 1]);
368
+ ], signal, "git", {}, [0, 1]);
321
369
  const drivers = new Set(filters.split("\0").flatMap((key) => {
322
370
  const separator = key.lastIndexOf(".");
323
371
  return separator > 7 && [
@@ -338,6 +386,7 @@ function apply(ctx, config) {
338
386
  env[`GIT_CONFIG_KEY_${index}`] = `filter.${driver}.${attribute}`;
339
387
  env[`GIT_CONFIG_VALUE_${index++}`] = attribute === "required" ? "false" : "";
340
388
  }
389
+ await allocate(true);
341
390
  await run(agent, path, [
342
391
  "--work-tree",
343
392
  path,
@@ -371,9 +420,10 @@ function apply(ctx, config) {
371
420
  signal.throwIfAborted();
372
421
  await ctx.configEditor.edit(entry(), (current) => {
373
422
  if (snapshot().revision !== input.revision) throw new Error("conflict");
374
- const { directory, remote, naming, prefix } = input.settings;
423
+ const { defaultSelection, directory, remote, naming, prefix } = input.settings;
375
424
  return {
376
425
  ...current,
426
+ defaultSelection,
377
427
  directory,
378
428
  remote,
379
429
  naming,
@@ -431,7 +481,32 @@ function apply(ctx, config) {
431
481
  });
432
482
  else {
433
483
  if (!(await describe(agent, operationSignal)).worktrees.some((entry) => entry.path === input.path)) return failure("invalid");
434
- await ctx.workingDirectory.set(agent, input.path, operationSignal);
484
+ const targetPath = input.path;
485
+ const resolve = (path, cwd) => ctx.fs.resolve(path, {
486
+ cwd,
487
+ signal: operationSignal
488
+ });
489
+ const sourceCwd = await ctx.workingDirectory.ensure(agent, operationSignal);
490
+ const rev = async (cwd, option) => (await run(agent, cwd, [
491
+ "rev-parse",
492
+ "--path-format=absolute",
493
+ option
494
+ ], operationSignal)).replace(/\r?\n$/, "");
495
+ const target = await resolve(targetPath, sourceCwd);
496
+ const top = await resolve(await rev(targetPath, "--show-toplevel"), targetPath);
497
+ const common = await resolve(await rev(sourceCwd, "--git-common-dir"), sourceCwd);
498
+ const targetCommon = await resolve(await rev(targetPath, "--git-common-dir"), targetPath);
499
+ if (target.targetKey !== top.targetKey || common.targetKey !== targetCommon.targetKey) return failure("invalid");
500
+ const admin = await resolve(await rev(targetPath, "--absolute-git-dir"), targetPath);
501
+ const dotGitInfo = await ctx.fs.lstat(".git", { cwd: targetPath }, operationSignal);
502
+ if (!dotGitInfo || !["file", "directory"].includes(dotGitInfo.type)) return failure("invalid");
503
+ const dotGit = await resolve(".git", targetPath);
504
+ if (admin.targetKey !== common.targetKey) {
505
+ const backlink = await resolve("gitdir", ctx.fs.processPath(admin));
506
+ const bytes = await ctx.fs.readBytes(backlink, operationSignal, 8192);
507
+ if ((await resolve(new TextDecoder().decode(bytes).replace(/\r?\n$/, ""), ctx.fs.processPath(admin))).targetKey !== dotGit.targetKey) return failure("invalid");
508
+ }
509
+ await ctx.workingDirectory.set(agent, ctx.fs.processPath(target), operationSignal);
435
510
  creations.delete(agent);
436
511
  }
437
512
  await ctx.sessions.flush(agent.session);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-worktree",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Automatically name, create, and enter Git worktrees after sending a task, with setup progress in the conversation.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,6 @@
33
33
  "@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
34
34
  "@deepseek-ai/dsh-client-ui-chat": "0.2.1-rc.5",
35
35
  "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
36
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
37
36
  "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
38
37
  "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
39
38
  "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.5",
@@ -50,12 +49,12 @@
50
49
  "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.5",
51
50
  "@deepseek-ai/dsh-working-directory": "0.2.1-rc.5",
52
51
  "@deepseek-ai/schemastery": "~3.18.5",
53
- "@dungsil/dsh-plugin-model-settings": "0.3.0",
52
+ "@dungsil/dsh-plugin-model-settings": "0.4.0",
54
53
  "@dungsil/dsh-tsconfig": "0.0.0",
54
+ "@dungsil/dsh-tsdown": "0.0.0",
55
+ "@dungsil/dsh-ui": "0.0.0",
55
56
  "@types/react": "18.3.31",
56
- "happy-dom": "21.0.4",
57
57
  "react": "18.3.1",
58
- "react-dom": "18.3.1",
59
58
  "tsdown": "0.23.4"
60
59
  },
61
60
  "peerDependencies": {