@dungsil/dsh-plugin-ui-worktree 0.4.0 → 0.6.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
@@ -20,14 +20,14 @@ function installStyles(css) {
20
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
21
  // ../../tools/ui/src/composer.tsx
22
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; }
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: 1.5385; cursor: pointer; }
24
24
  :where(.dsh-plugin-composer-trigger):disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
25
25
  .dsh-plugin-composer-icon, .dsh-plugin-composer-chevron { display: inline-flex; flex: none; }
26
26
  .dsh-plugin-composer-label, .dsh-plugin-composer-detail { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
27
  .dsh-plugin-composer-chevron, .dsh-plugin-composer-detail { color: var(--dsw-alias-label-caption); }
28
28
  .dsh-plugin-composer-trigger[aria-expanded=true] .dsh-plugin-composer-chevron { transform: rotate(180deg); }
29
29
  `;
30
- const ComposerTrigger = (0, react.forwardRef)(function ComposerTrigger({ className = "", icon, label, detail, ...props }, ref) {
30
+ const ComposerTrigger = /* @__PURE__ */ (0, react.forwardRef)(function ComposerTrigger({ className = "", icon, label, detail, ...props }, ref) {
31
31
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32
32
  ...props,
33
33
  ref,
@@ -57,23 +57,37 @@ const ComposerTrigger = (0, react.forwardRef)(function ComposerTrigger({ classNa
57
57
  });
58
58
  // ../../tools/ui/src/settings.tsx
59
59
  /** The rc.5 settings shell has no icon slot, so decorate only the matching navigation rows. */
60
- function installSettingsIcon(id, labels, svg) {
60
+ function installSettingsIcon(id, labels, icon) {
61
61
  const attribute = "data-dsh-settings-icon";
62
62
  const selector = `[${attribute}="${id}"]`;
63
63
  const removeStyles = installStyles(`
64
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; }
65
+ ${typeof icon === "string" ? `${selector}::before { content: ''; display: block; flex: none; width: 16px; height: 16px; background: currentColor; mask: url("data:image/svg+xml,${encodeURIComponent(icon)}") center / contain no-repeat; }` : ""}
66
+ ${selector} > [data-dsh-settings-icon-glyph] { display: inline-flex; flex: none; }
66
67
  `);
67
- const marked = /* @__PURE__ */ new Set();
68
+ const marked = /* @__PURE__ */ new Map();
68
69
  const refresh = () => {
69
70
  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
+ for (const [row, dispose] of marked) if (!rows.has(row) || !labels.includes(row.textContent?.trim() ?? "")) {
71
72
  row.removeAttribute(attribute);
73
+ dispose();
72
74
  marked.delete(row);
73
75
  }
74
- for (const row of rows) if (labels.includes(row.textContent?.trim() ?? "")) {
76
+ for (const row of rows) if (!marked.has(row) && labels.includes(row.textContent?.trim() ?? "")) {
75
77
  row.setAttribute(attribute, id);
76
- marked.add(row);
78
+ let dispose = () => {};
79
+ if (typeof icon !== "string") {
80
+ const glyph = document.createElement("span");
81
+ glyph.setAttribute("data-dsh-settings-icon-glyph", "");
82
+ glyph.setAttribute("aria-hidden", "true");
83
+ row.prepend(glyph);
84
+ const unmount = icon(glyph);
85
+ dispose = () => {
86
+ unmount();
87
+ glyph.remove();
88
+ };
89
+ }
90
+ marked.set(row, dispose);
77
91
  }
78
92
  };
79
93
  const observer = new MutationObserver(refresh);
@@ -85,7 +99,10 @@ ${selector}::before { content: ''; display: block; flex: none; width: 16px; heig
85
99
  refresh();
86
100
  return () => {
87
101
  observer.disconnect();
88
- for (const row of marked) row.removeAttribute(attribute);
102
+ for (const [row, dispose] of marked) {
103
+ row.removeAttribute(attribute);
104
+ dispose();
105
+ }
89
106
  removeStyles();
90
107
  };
91
108
  }
@@ -158,7 +175,10 @@ function isWorktreeProgress(value) {
158
175
  ].includes(progress.status) && Number.isFinite(progress.updatedAt) && Array.isArray(progress.durations) && progress.durations.length === 4 && progress.durations.every((duration) => Number.isFinite(duration) && duration >= 0) && (progress.branch === void 0 || typeof progress.branch === "string");
159
176
  }
160
177
  function comparablePath(path) {
161
- return (/^[a-z]:[\\/]/i.test(path) || path.startsWith("\\\\") || path.startsWith("//") ? path.replaceAll("\\", "/").replace(/^[A-Z]:/, (drive) => drive.toLowerCase()) : path).replace(/\/+$/, "");
178
+ const value = /^[a-z]:[\\/]/i.test(path) || path.startsWith("\\\\") || path.startsWith("//") ? path.replaceAll("\\", "/").replace(/^[A-Z]:/, (drive) => drive.toLowerCase()) : path;
179
+ let end = value.length;
180
+ while (end > 0 && value[end - 1] === "/") end--;
181
+ return value.slice(0, end);
162
182
  }
163
183
  function currentWorktree(view) {
164
184
  if (!view) return void 0;
@@ -168,10 +188,15 @@ function currentWorktree(view) {
168
188
  return cwd === path || cwd.startsWith(path + "/");
169
189
  }).sort((a, b) => comparablePath(b.path).length - comparablePath(a.path).length)[0];
170
190
  }
171
- function isWorktreeView(value) {
191
+ function isWorktreeSelectionView(value) {
172
192
  if (!value || typeof value !== "object") return false;
173
193
  const view = value;
174
- return typeof view.cwd === "string" && (view.pendingCreation === void 0 || view.pendingCreation !== null && typeof view.pendingCreation.name === "string" && typeof view.pendingCreation.failed === "boolean") && Array.isArray(view.worktrees) && view.worktrees.every((item) => item && typeof item.path === "string" && (item.branch === null || typeof item.branch === "string"));
194
+ return typeof view.cwd === "string" && (view.locked === void 0 || typeof view.locked === "boolean") && (view.pendingCreation === void 0 || view.pendingCreation !== null && typeof view.pendingCreation.name === "string" && typeof view.pendingCreation.failed === "boolean");
195
+ }
196
+ function isWorktreeView(value) {
197
+ if (!isWorktreeSelectionView(value)) return false;
198
+ const view = value;
199
+ return (view.needsInitialization === void 0 || typeof view.needsInitialization === "boolean") && Array.isArray(view.worktrees) && view.worktrees.every((item) => item && typeof item.path === "string" && (item.branch === null || typeof item.branch === "string"));
175
200
  }
176
201
  // src/errors.ts
177
202
  const failureCodes = [
@@ -206,17 +231,22 @@ function failureCode(error) {
206
231
  // src/requests.ts
207
232
  const copy$1 = {
208
233
  zh: {
209
- title: "Git 工作树",
234
+ title: "下一任务的位置",
235
+ choose: "选择工作树",
236
+ savingSelection: "正在保存下一任务的位置…",
237
+ selectionError: "请确认工作树选择后再发送提示。",
238
+ locked: "任务开始后无法更改此会话的工作树。请在其他工作树中新建会话。",
210
239
  detached: "分离的 HEAD",
211
240
  unknown: "无法获取分支",
212
241
  create: "新工作树",
213
- reservationHint: "发送下一条提示后,将先创建并切换到新工作树再开始任务。选择现有工作树可取消。",
214
- creationError: "无法确认创建结果,尚未开始任务。请刷新列表,选择工作树或重新预约,然后重新发送提示。",
242
+ primaryBranch: "当前分支",
243
+ reservationHint: "发送下一条提示后,将先创建并切换到新工作树再开始任务。选择当前分支可取消。",
244
+ creationError: "无法确认创建结果,尚未开始任务。请选择当前分支或重新预约,然后重新发送提示。",
215
245
  close: "关闭",
216
246
  refresh: "刷新工作树",
217
247
  loading: "正在加载工作树…",
218
248
  busy: "正在切换工作树…",
219
- changed: "已切换工作树。",
249
+ changed: "已选择下一任务的位置。",
220
250
  empty: "未找到 Git 工作树。",
221
251
  wait: "请等待会话准备就绪且当前任务结束。",
222
252
  error: "无法确认当前状态。请刷新工作树以检查状态后再重试。",
@@ -228,12 +258,13 @@ const copy$1 = {
228
258
  errorOperation: "请求",
229
259
  operations: {
230
260
  describe: "查询工作树",
261
+ initialize: "初始化工作树选择",
231
262
  create: "创建工作树",
232
- switch: "切换工作树"
263
+ select: "保存下一任务的位置"
233
264
  },
234
265
  reasons: {
235
266
  busy: "会话中有正在执行的任务。请在任务结束后重新查询。",
236
- conflict: "当前工作目录已更改。请刷新列表后再进行更改。",
267
+ conflict: "当前工作目录已更改或工作树选择已锁定。请重新查询状态。",
237
268
  invalid: "请求无效。请刷新列表并检查分支名称或工作树。",
238
269
  operation_failed: "主机无法完成操作。请检查 Git 和工作树配置。",
239
270
  session_not_found: "会话不可用。请重新打开会话后重试。",
@@ -241,23 +272,28 @@ const copy$1 = {
241
272
  auth_required: "连接需要身份验证。请重新打开应用后重试。",
242
273
  endpoint_missing: "主机没有工作树接口。请确认插件已启用并重启应用。",
243
274
  client_unavailable: "连接缺少工作树 RPC 客户端。请刷新页面或更新应用。",
244
- invalid_response: "主机返回的工作树列表无效。请检查应用和插件版本。",
275
+ invalid_response: "主机返回的工作树状态无效。请检查应用和插件版本。",
245
276
  connection_error: "收到有效响应前连接失败。请检查连接后重新查询。",
246
277
  timeout: "请求超时。请在当前任务结束后重新查询。"
247
278
  }
248
279
  },
249
280
  en: {
250
- title: "Git worktree",
281
+ title: "Next task location",
282
+ choose: "Choose worktree",
283
+ savingSelection: "Saving next task location…",
284
+ selectionError: "Confirm the worktree selection before sending your prompt.",
285
+ locked: "The worktree cannot change after work starts. Create a new session in another worktree.",
251
286
  detached: "Detached HEAD",
252
287
  unknown: "Branch unavailable",
253
288
  create: "New worktree",
254
- reservationHint: "The next prompt will create and enter a new worktree before starting work. Select an existing worktree to cancel.",
255
- creationError: "Creation could not be confirmed, so work did not start. Refresh the list, select a worktree or schedule a new one, then resend your prompt.",
289
+ primaryBranch: "Current branch",
290
+ reservationHint: "The next prompt will create and enter a new worktree before starting work. Select the current branch to cancel.",
291
+ creationError: "Creation could not be confirmed, so work did not start. Select the current branch or schedule a new worktree, then resend your prompt.",
256
292
  close: "Close",
257
293
  refresh: "Refresh worktrees",
258
294
  loading: "Loading worktrees…",
259
295
  busy: "Changing worktree…",
260
- changed: "Worktree changed.",
296
+ changed: "Next task location selected.",
261
297
  empty: "No Git worktrees found.",
262
298
  wait: "Wait until the session is ready and idle.",
263
299
  error: "Could not confirm the current state. Refresh worktrees to check before trying again.",
@@ -269,12 +305,13 @@ const copy$1 = {
269
305
  errorOperation: "Request",
270
306
  operations: {
271
307
  describe: "List worktrees",
308
+ initialize: "Initialize worktree selection",
272
309
  create: "Create worktree",
273
- switch: "Switch worktree"
310
+ select: "Save next task location"
274
311
  },
275
312
  reasons: {
276
313
  busy: "The session has an active task. Retry after the task stops.",
277
- conflict: "The current directory changed. Reload the list before another change.",
314
+ conflict: "The current directory changed or the worktree selection is locked. Reload to check the state.",
278
315
  invalid: "The request is invalid. Reload the list and check the branch name or worktree.",
279
316
  operation_failed: "The host could not complete the operation. Check Git and the worktree configuration.",
280
317
  session_not_found: "The session is unavailable. Reopen the session and retry.",
@@ -282,23 +319,28 @@ const copy$1 = {
282
319
  auth_required: "The connection requires authentication. Reopen the application and retry.",
283
320
  endpoint_missing: "The host has no worktree endpoint. Check that the plugin is active and restart the application.",
284
321
  client_unavailable: "The connection has no worktree RPC client. Reload the page or update the application.",
285
- invalid_response: "The host returned an invalid worktree list. Check the application and plugin versions.",
322
+ invalid_response: "The host returned an invalid worktree state. Check the application and plugin versions.",
286
323
  connection_error: "The connection failed before a valid response. Check the connection and retry.",
287
324
  timeout: "The request timed out. Retry after the current task stops."
288
325
  }
289
326
  },
290
327
  ko: {
291
- title: "Git worktree",
328
+ title: "다음 작업 위치",
329
+ choose: "워크트리 선택",
330
+ savingSelection: "다음 작업 위치를 저장하는 중…",
331
+ selectionError: "워크트리 선택을 확인한 뒤 프롬프트를 전송하세요.",
332
+ locked: "작업을 시작한 세션에서는 워크트리를 바꿀 수 없습니다. 다른 워크트리에서 새 세션을 만드세요.",
292
333
  detached: "분리된 HEAD",
293
334
  unknown: "브랜치 확인 불가",
294
335
  create: "새 워크트리",
295
- reservationHint: "다음 프롬프트를 전송하면 새 워크트리를 생성·전환한 뒤 작업을 시작합니다. 기존 워크트리를 선택하면 예약을 취소합니다.",
296
- creationError: "생성 결과를 확인하지 못해 작업을 시작하지 않았습니다. 목록을 새로고침하고 워크트리를 선택하거나 다시 예약한 뒤 프롬프트를 다시 전송하세요.",
336
+ primaryBranch: "현재 브랜치",
337
+ reservationHint: "다음 프롬프트를 전송하면 새 워크트리를 생성·전환한 뒤 작업을 시작합니다. 현재 브랜치를 선택하면 예약을 취소합니다.",
338
+ creationError: "생성 결과를 확인하지 못해 작업을 시작하지 않았습니다. 현재 브랜치를 선택하거나 다시 예약한 뒤 프롬프트를 다시 전송하세요.",
297
339
  close: "닫기",
298
340
  refresh: "워크트리 새로고침",
299
341
  loading: "워크트리를 불러오는 중…",
300
342
  busy: "워크트리 변경 중…",
301
- changed: "워크트리를 변경했습니다.",
343
+ changed: "다음 작업 위치를 선택했습니다.",
302
344
  empty: "Git worktree가 없습니다.",
303
345
  wait: "세션이 준비되고 실행 중인 작업이 끝나면 사용할 수 있습니다.",
304
346
  error: "현재 상태를 확인하지 못했습니다. 다시 시도하기 전에 새로고침으로 상태를 확인하세요.",
@@ -310,12 +352,13 @@ const copy$1 = {
310
352
  errorOperation: "요청",
311
353
  operations: {
312
354
  describe: "워크트리 조회",
355
+ initialize: "워크트리 선택 초기화",
313
356
  create: "워크트리 생성",
314
- switch: "워크트리 전환"
357
+ select: "다음 작업 위치 저장"
315
358
  },
316
359
  reasons: {
317
360
  busy: "세션에서 작업을 실행 중입니다. 작업이 끝난 뒤 다시 조회하세요.",
318
- conflict: "현재 작업 경로가 바뀌었습니다. 목록을 새로고침한 뒤 변경하세요.",
361
+ conflict: "현재 작업 경로가 바뀌었거나 워크트리 선택이 고정됐습니다. 다시 조회해 상태를 확인하세요.",
319
362
  invalid: "요청이 올바르지 않습니다. 목록을 새로고침하고 브랜치 이름이나 워크트리를 확인하세요.",
320
363
  operation_failed: "호스트가 작업을 완료하지 못했습니다. Git과 워크트리 설정을 확인하세요.",
321
364
  session_not_found: "세션을 찾지 못했습니다. 세션을 다시 열고 조회하세요.",
@@ -323,7 +366,7 @@ const copy$1 = {
323
366
  auth_required: "연결 인증이 필요합니다. 앱을 다시 열고 조회하세요.",
324
367
  endpoint_missing: "호스트에 워크트리 API가 없습니다. 플러그인을 활성화했는지 확인하고 앱을 다시 시작하세요.",
325
368
  client_unavailable: "연결에 워크트리 RPC 기능이 없습니다. 페이지를 새로고침하거나 앱을 업데이트하세요.",
326
- invalid_response: "호스트가 올바르지 않은 워크트리 목록을 반환했습니다. 앱과 플러그인 버전을 확인하세요.",
369
+ invalid_response: "호스트가 올바르지 않은 워크트리 상태를 반환했습니다. 앱과 플러그인 버전을 확인하세요.",
327
370
  connection_error: "정상 응답을 받기 전에 연결이 실패했습니다. 연결을 확인하고 다시 조회하세요.",
328
371
  timeout: "요청 시간이 초과됐습니다. 실행 중인 작업이 끝난 뒤 다시 조회하세요."
329
372
  }
@@ -338,6 +381,8 @@ const copy = {
338
381
  new: "새 워크트리",
339
382
  directory: "워크트리 풀 디렉터리",
340
383
  remote: "추적 중인 원격 브랜치 사용",
384
+ deleteOnArchive: "보관 시 워크트리 삭제",
385
+ deleteOnArchiveDescription: "이 플러그인이 만든 워크트리만 삭제합니다. 미커밋·미추적·무시된 파일이나 다른 세션이 있으면 유지하며, 브랜치는 삭제하지 않습니다.",
341
386
  naming: "자동 브랜치 이름 형식",
342
387
  semantic: "시멘틱 (feat/… 또는 fix/…)",
343
388
  dsh: "DSH (dsh/*)",
@@ -358,6 +403,8 @@ const copy = {
358
403
  new: "新工作树",
359
404
  directory: "工作树池目录",
360
405
  remote: "使用跟踪的远程分支",
406
+ deleteOnArchive: "归档时删除工作树",
407
+ deleteOnArchiveDescription: "仅删除此插件创建的工作树。有未提交、未跟踪、被忽略的文件或其他会话使用时保留,不删除分支。",
361
408
  naming: "自动分支命名格式",
362
409
  semantic: "语义化(feat/… 或 fix/…)",
363
410
  dsh: "DSH(dsh/*)",
@@ -378,6 +425,8 @@ const copy = {
378
425
  new: "New worktree",
379
426
  directory: "Worktree pool directory",
380
427
  remote: "Use tracked remote branch",
428
+ deleteOnArchive: "Delete worktree on archive",
429
+ deleteOnArchiveDescription: "Only removes worktrees created by this plugin. Keeps worktrees with uncommitted, untracked or ignored files, or other sessions. Branches are kept.",
381
430
  naming: "Automatic branch naming",
382
431
  semantic: "Semantic (feat/… or fix/…)",
383
432
  dsh: "DSH (dsh/*)",
@@ -586,6 +635,18 @@ function WorktreeSettingsSection({ ctx }) {
586
635
  onChange: (remote) => change({ remote })
587
636
  })
588
637
  }),
638
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
639
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
640
+ className: "dsh-worktree-settings-title",
641
+ children: text.deleteOnArchive
642
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.deleteOnArchiveDescription })] }),
643
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
644
+ label: text.deleteOnArchive,
645
+ checked: draft.deleteOnArchive,
646
+ disabled,
647
+ onChange: (deleteOnArchive) => change({ deleteOnArchive })
648
+ })
649
+ }),
589
650
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
590
651
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
591
652
  id: `${id}-naming`,
@@ -652,10 +713,17 @@ function WorktreeSettingsSection({ ctx }) {
652
713
  // src/styles.ts
653
714
  const styles = `${settingsStyles}${composerStyles}
654
715
  .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; }
716
+ [data-row-key]:has(.dsh-vcs-session-request) > .dsh-worktree-sidebar-name { display: none; }
655
717
  .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; }
656
718
  .dsh-worktree-picker { display: inline-flex; min-width: 0; max-width: 100%; }
719
+ .dsh-worktree-pinned { position: sticky; top: 0; z-index: 1; background: var(--dsw-alias-bg-layer-1); }
720
+ .dsh-worktree-option { display: flex; align-items: center; gap: 8px; min-width: 0; }
721
+ .dsh-worktree-option > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
722
+ .dsh-worktree-option > svg { flex: none; width: 14px; height: 14px; margin-inline-start: auto; color: var(--dsw-alias-label-primary); }
723
+ .dsh-worktree-badge { flex: none; padding: 1px 5px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; font-weight: 400; }
657
724
  .dsh-worktree-trigger { display: inline-flex; max-width: 220px; font-weight: 500; }
658
725
  .dsh-worktree-trigger .dsh-plugin-composer-icon svg { width: 14px; height: 14px; }
726
+ .dsh-worktree-trigger:not([aria-haspopup]) .dsh-plugin-composer-chevron { display: none; }
659
727
  .dsh-worktree-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
660
728
  @container (width <= 460px) {
661
729
  .dsh-worktree-trigger .dsh-plugin-composer-label { display: none; }
@@ -894,7 +962,8 @@ const inject = [
894
962
  "slots",
895
963
  "locale",
896
964
  "connection",
897
- "uiConversation"
965
+ "uiConversation",
966
+ "conversation"
898
967
  ];
899
968
  function apply(ctx) {
900
969
  ctx.effect(() => installSettingsIcon(name, [
@@ -904,19 +973,47 @@ function apply(ctx) {
904
973
  ].map(settingsLabel), git_branch_default));
905
974
  const connection = ctx.get("connection");
906
975
  installProgress(ctx);
907
- function Picker({ sessionId, useSession }) {
976
+ const synced = /* @__PURE__ */ new Map();
977
+ const ownedBlocks = /* @__PURE__ */ new Map();
978
+ const blocks = ctx.conversation.blocks;
979
+ ctx.effect(() => () => {
980
+ const owned = [...ownedBlocks];
981
+ ownedBlocks.clear();
982
+ for (const [id, block] of owned) if (blocks.storeFor(id).getSnapshot() === block) blocks.set(id, void 0);
983
+ });
984
+ function Picker({ sessionId, useSession, useProjection, useInput }) {
908
985
  const { active: language } = useLocaleSnapshot(ctx.locale);
909
986
  const text = copy$1[language.startsWith("ko") ? "ko" : language.startsWith("zh") ? "zh" : "en"];
910
- const unavailable = useSession((s) => s.running || s.removed || s.openState !== "open" || s.pendingSubmissions.length > 0);
911
- const running = useSession((s) => s.running);
912
- const wasRunning = (0, react.useRef)(running);
913
- const [view, setView] = (0, react.useState)();
914
- const [loading, setLoading] = (0, react.useState)(true);
915
- const [busy, setBusy] = (0, react.useState)(false);
987
+ const inputBusy = useInput((input) => input.phase !== "plain");
988
+ const unavailable = useSession((s) => s.running || s.removed || s.openState !== "open" || s.pendingSubmissions.length > 0) || inputBusy;
989
+ const unavailableRef = (0, react.useRef)(unavailable);
990
+ unavailableRef.current = unavailable;
991
+ const unreadable = useSession((s) => s.removed || s.openState !== "open");
992
+ const unreadableRef = (0, react.useRef)(unreadable);
993
+ unreadableRef.current = unreadable;
994
+ const cwd = useProjection("workingDirectory");
995
+ const started = useProjection("sessionListMetadata")?.blank === false;
996
+ const cached = synced.get(sessionId);
997
+ const initialView = cached?.cwd === cwd ? cached : void 0;
998
+ const previousReadiness = (0, react.useRef)({
999
+ unreadable: true,
1000
+ unavailable,
1001
+ cwd
1002
+ });
1003
+ const [view, setView] = (0, react.useState)(initialView);
1004
+ const [loading, setLoading] = (0, react.useState)(!initialView);
1005
+ const viewRef = (0, react.useRef)(view);
1006
+ viewRef.current = view;
1007
+ const selected = (0, react.useRef)();
1008
+ const saving = (0, react.useRef)(false);
1009
+ const version = (0, react.useRef)(0);
916
1010
  const [error, setError] = (0, react.useState)();
917
1011
  const [changed, setChanged] = (0, react.useState)(false);
918
1012
  const [open, setOpen] = (0, react.useState)(false);
1013
+ const needsSync = (0, react.useRef)(true);
1014
+ needsSync.current = !view || !view.locked && (!!view.needsInitialization || !!view.pendingCreation);
919
1015
  const pending = (0, react.useRef)(false);
1016
+ const reloadRequested = (0, react.useRef)(false);
920
1017
  const needsRefresh = (0, react.useRef)(false);
921
1018
  const lifetime = (0, react.useRef)(null);
922
1019
  const trigger = (0, react.useRef)(null);
@@ -924,52 +1021,112 @@ function apply(ctx) {
924
1021
  const errorTitle = (0, react.useId)();
925
1022
  const errorDescription = (0, react.useId)();
926
1023
  const status = (0, react.useId)();
927
- const disabled = unavailable || loading || busy || !!error || !view;
928
1024
  const creation = view?.pendingCreation;
1025
+ const locked = !!view?.locked || started && !creation;
1026
+ const disabled = locked || unavailable || loading || !view;
929
1027
  const detailError = error ?? (creation?.failed ? {
930
1028
  operation: "create",
931
1029
  code: "operation_failed",
932
1030
  uncertain: true
933
1031
  } : void 0);
934
1032
  const errorLabel = detailError?.uncertain ? text.mutationError : text.lookupError;
1033
+ const gate = (reason) => {
1034
+ const store = blocks.storeFor(sessionId);
1035
+ const owned = ownedBlocks.get(sessionId);
1036
+ const current = store.getSnapshot();
1037
+ if (!reason) {
1038
+ ownedBlocks.delete(sessionId);
1039
+ if (owned && current === owned) blocks.set(sessionId, void 0);
1040
+ } else {
1041
+ synced.delete(sessionId);
1042
+ const block = owned?.reason === reason ? owned : { reason };
1043
+ ownedBlocks.set(sessionId, block);
1044
+ if (!current || current === owned) blocks.set(sessionId, block);
1045
+ }
1046
+ };
935
1047
  const recordFailure = (operation, cause) => {
936
1048
  const code = failureCode(cause);
937
1049
  needsRefresh.current = true;
1050
+ const uncertain = operation !== "describe" || ownedBlocks.has(sessionId);
1051
+ if (uncertain) gate(text.selectionError);
938
1052
  setError((previous) => ({
939
1053
  operation,
940
1054
  code,
941
- uncertain: operation !== "describe" || !!previous?.uncertain
1055
+ uncertain: uncertain || !!previous?.uncertain
942
1056
  }));
943
1057
  console.warn("[dungsil-ui-worktree]", {
944
1058
  operation,
945
1059
  code
946
1060
  });
947
1061
  };
948
- const request = async (operation, payload, signal) => {
1062
+ const call = async (operation, payload, signal) => {
949
1063
  if (typeof connection?.rpc?.call !== "function") throw { code: "client_unavailable" };
950
1064
  const result = await connection.rpc.call(channel, endpointPrefix + operation, payload, signal);
951
1065
  signal.throwIfAborted();
952
1066
  if (!result.ok) throw result.error;
953
- if (!isWorktreeView(result.value)) throw { code: "invalid_response" };
954
1067
  return result.value;
955
1068
  };
1069
+ const request = async (operation, payload, signal) => {
1070
+ const value = await call(operation, payload, signal);
1071
+ if (!isWorktreeView(value)) throw { code: "invalid_response" };
1072
+ return value;
1073
+ };
1074
+ const updateView = (next) => {
1075
+ viewRef.current = next;
1076
+ setView(next);
1077
+ };
1078
+ const mergeSelection = (next) => {
1079
+ const previous = viewRef.current;
1080
+ if (!previous) return;
1081
+ updateView({
1082
+ ...previous,
1083
+ cwd: next.cwd,
1084
+ pendingCreation: next.pendingCreation,
1085
+ locked: next.locked,
1086
+ needsInitialization: false
1087
+ });
1088
+ };
956
1089
  const load = async (explicit = false) => {
957
1090
  const controller = lifetime.current;
958
- if (!controller || controller.signal.aborted || pending.current || needsRefresh.current && !explicit) return;
1091
+ if (!controller || controller.signal.aborted || unreadableRef.current || saving.current || needsRefresh.current && !explicit) return;
1092
+ if (pending.current) {
1093
+ reloadRequested.current = true;
1094
+ return;
1095
+ }
1096
+ reloadRequested.current = false;
959
1097
  pending.current = true;
960
1098
  setLoading(true);
1099
+ let operation = "describe";
961
1100
  try {
962
1101
  const view = await request("describe", { sessionId }, controller.signal);
963
1102
  if (controller.signal.aborted) return;
964
- setView(view);
1103
+ updateView(view);
1104
+ if (view.needsInitialization && !view.locked && !unavailableRef.current) {
1105
+ operation = "initialize";
1106
+ gate(text.savingSelection);
1107
+ const initialized = await request("initialize", {
1108
+ sessionId,
1109
+ cwd: view.cwd
1110
+ }, controller.signal);
1111
+ if (controller.signal.aborted) return;
1112
+ updateView(initialized);
1113
+ }
965
1114
  needsRefresh.current = false;
966
1115
  setError(void 0);
1116
+ gate();
967
1117
  } catch (cause) {
968
- if (!controller.signal.aborted) recordFailure("describe", cause);
1118
+ if (!controller.signal.aborted) {
1119
+ if (failureCode(cause) !== "busy") recordFailure(operation, cause);
1120
+ else if (!needsRefresh.current && operation === "initialize") gate();
1121
+ }
969
1122
  } finally {
970
1123
  if (!controller.signal.aborted) {
971
1124
  pending.current = false;
972
1125
  setLoading(false);
1126
+ if (reloadRequested.current) {
1127
+ reloadRequested.current = false;
1128
+ load();
1129
+ }
973
1130
  }
974
1131
  }
975
1132
  };
@@ -977,9 +1134,15 @@ function apply(ctx) {
977
1134
  const controller = new AbortController();
978
1135
  lifetime.current = controller;
979
1136
  pending.current = false;
980
- load();
1137
+ const store = blocks.storeFor(sessionId);
1138
+ const restoreGate = () => {
1139
+ const block = ownedBlocks.get(sessionId);
1140
+ if (block && !store.getSnapshot()) blocks.set(sessionId, block);
1141
+ };
1142
+ restoreGate();
1143
+ const unsubscribe = store.subscribe(restoreGate);
981
1144
  const focus = () => {
982
- load();
1145
+ if (needsSync.current && !pending.current) load();
983
1146
  };
984
1147
  const resize = () => {
985
1148
  setOpen(false);
@@ -988,50 +1151,94 @@ function apply(ctx) {
988
1151
  window.addEventListener("resize", resize);
989
1152
  return () => {
990
1153
  controller.abort();
1154
+ unsubscribe();
991
1155
  window.removeEventListener("focus", focus);
992
1156
  window.removeEventListener("resize", resize);
993
1157
  };
994
1158
  }, []);
995
1159
  (0, react.useEffect)(() => {
996
- if (wasRunning.current && !running) load();
997
- wasRunning.current = running;
998
- }, [running]);
1160
+ const previous = previousReadiness.current;
1161
+ previousReadiness.current = {
1162
+ unreadable,
1163
+ unavailable,
1164
+ cwd
1165
+ };
1166
+ if (!unreadable && ((previous.unreadable || previous.unavailable && !unavailable) && needsSync.current || previous.cwd !== cwd)) load();
1167
+ }, [
1168
+ unreadable,
1169
+ unavailable,
1170
+ cwd
1171
+ ]);
1172
+ (0, react.useEffect)(() => {
1173
+ if (locked && view && !creation && view.cwd === cwd && !error && !ownedBlocks.has(sessionId)) synced.set(sessionId, {
1174
+ ...view,
1175
+ locked: true,
1176
+ needsInitialization: false
1177
+ });
1178
+ else synced.delete(sessionId);
1179
+ }, [
1180
+ sessionId,
1181
+ view,
1182
+ locked,
1183
+ creation,
1184
+ cwd,
1185
+ error
1186
+ ]);
999
1187
  (0, react.useEffect)(() => {
1000
1188
  if (disabled) setOpen(false);
1001
1189
  }, [disabled]);
1002
- const change = async (endpoint, path) => {
1190
+ const change = async (choice) => {
1003
1191
  const controller = lifetime.current;
1004
- if (disabled || pending.current || needsRefresh.current || !view || !controller || controller.signal.aborted) return;
1005
- pending.current = true;
1006
- setBusy(true);
1007
- setChanged(false);
1192
+ const currentView = viewRef.current;
1193
+ if (locked || unavailableRef.current || loading || pending.current || !currentView || !controller || controller.signal.aborted) return;
1194
+ const intent = {
1195
+ version: ++version.current,
1196
+ cwd: currentView.cwd,
1197
+ choice
1198
+ };
1199
+ selected.current = intent;
1200
+ mergeSelection({
1201
+ cwd: currentView.cwd,
1202
+ pendingCreation: choice === "new" ? {
1203
+ name: text.create,
1204
+ failed: false
1205
+ } : void 0
1206
+ });
1207
+ setError(void 0);
1208
+ setChanged(true);
1209
+ needsRefresh.current = false;
1210
+ gate(text.savingSelection);
1211
+ if (saving.current) return;
1212
+ saving.current = true;
1008
1213
  try {
1009
- const payload = endpoint === "create" ? {
1010
- sessionId,
1011
- cwd: view.cwd
1012
- } : {
1013
- sessionId,
1014
- cwd: view.cwd,
1015
- path
1016
- };
1017
- const nextView = await request(endpoint, payload, controller.signal);
1018
- if (controller.signal.aborted) return;
1019
- setView(nextView);
1020
- setChanged(true);
1021
- pending.current = false;
1022
- await load();
1023
- } catch (cause) {
1024
- if (!controller.signal.aborted) recordFailure(endpoint, cause);
1025
- } finally {
1026
- if (!controller.signal.aborted) {
1027
- pending.current = false;
1028
- setBusy(false);
1214
+ while (selected.current && !controller.signal.aborted) {
1215
+ const next = selected.current;
1216
+ try {
1217
+ const value = await call("select", {
1218
+ sessionId,
1219
+ cwd: next.cwd,
1220
+ choice: next.choice
1221
+ }, controller.signal);
1222
+ if (!isWorktreeSelectionView(value)) throw { code: "invalid_response" };
1223
+ if (selected.current?.version !== next.version) continue;
1224
+ mergeSelection(value);
1225
+ selected.current = void 0;
1226
+ gate();
1227
+ } catch (cause) {
1228
+ if (controller.signal.aborted) return;
1229
+ if (selected.current?.version !== next.version) continue;
1230
+ selected.current = void 0;
1231
+ recordFailure("select", cause);
1232
+ return;
1233
+ }
1029
1234
  }
1235
+ } finally {
1236
+ saving.current = false;
1030
1237
  }
1031
1238
  };
1032
1239
  (0, react.useEffect)(() => {
1033
- if (!busy && changed) trigger.current?.focus();
1034
- }, [busy, changed]);
1240
+ if (changed) trigger.current?.focus();
1241
+ }, [changed]);
1035
1242
  (0, react.useEffect)(() => {
1036
1243
  if (!detailError && errorDialog.current?.open) errorDialog.current.close();
1037
1244
  }, [error, creation?.failed]);
@@ -1047,9 +1254,14 @@ function apply(ctx) {
1047
1254
  current,
1048
1255
  view
1049
1256
  ]);
1050
- const branch = creation ? creation.failed ? text.mutationError : text.create : current ? current.branch ?? text.detached : text.unknown;
1257
+ const selectCurrent = () => {
1258
+ setOpen(false);
1259
+ if (disabled) return;
1260
+ if (viewRef.current?.pendingCreation || needsRefresh.current) change("current");
1261
+ };
1262
+ const branch = creation ? text.create : current ? current.branch ?? text.detached : text.unknown;
1051
1263
  const label = detailError ? errorLabel : view ? branch : loading ? text.loading : text.title;
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 : "";
1264
+ const notice = loading ? text.loading : error ? errorLabel : unavailable ? text.wait : locked ? text.locked : creation ? creation.failed ? text.creationError : text.reservationHint : changed ? text.changed : view?.worktrees.length === 0 ? text.empty : "";
1053
1265
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1054
1266
  className: "dsh-worktree",
1055
1267
  children: [
@@ -1058,39 +1270,23 @@ function apply(ctx) {
1058
1270
  open,
1059
1271
  side: "top",
1060
1272
  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
- })) ?? []],
1273
+ listClassName: "dsh-worktree-menu",
1071
1274
  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
1275
  anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerTrigger, {
1080
1276
  ref: trigger,
1081
1277
  className: "dsh-worktree-trigger",
1082
1278
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
1083
1279
  label,
1084
- disabled: detailError ? loading || busy : disabled,
1280
+ disabled: detailError ? loading : disabled,
1085
1281
  "aria-label": `${text.title}: ${label}`,
1086
- "aria-haspopup": detailError ? "dialog" : "menu",
1087
- "aria-expanded": detailError ? void 0 : open,
1282
+ "aria-haspopup": detailError ? "dialog" : locked ? void 0 : "menu",
1283
+ "aria-expanded": detailError || locked ? void 0 : open,
1088
1284
  "aria-describedby": status,
1089
- "aria-busy": loading || busy,
1090
- title: label,
1285
+ "aria-busy": loading,
1286
+ title: locked ? text.locked : `${text.title}: ${branch}${creation ? ` — ${text.reservationHint}` : ""}`,
1091
1287
  onClick: () => {
1092
1288
  if (detailError) errorDialog.current?.showModal();
1093
- else setOpen((value) => !value);
1289
+ else if (!disabled) setOpen((value) => !value);
1094
1290
  },
1095
1291
  onKeyDown: (event) => {
1096
1292
  if (!detailError && !disabled && !open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
@@ -1098,6 +1294,37 @@ function apply(ctx) {
1098
1294
  setOpen(true);
1099
1295
  }
1100
1296
  }
1297
+ }),
1298
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1299
+ className: "dsh-worktree-pinned",
1300
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
1301
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, {}),
1302
+ current: !!creation,
1303
+ onSelect: () => {
1304
+ setOpen(false);
1305
+ const latest = viewRef.current?.pendingCreation;
1306
+ if (!disabled && (!latest || latest.failed || needsRefresh.current)) change("new");
1307
+ },
1308
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1309
+ className: "dsh-worktree-option",
1310
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text.create }), creation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})]
1311
+ })
1312
+ }), current && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
1313
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
1314
+ current: !creation,
1315
+ onSelect: selectCurrent,
1316
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1317
+ className: "dsh-worktree-option",
1318
+ children: [
1319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: current.branch ?? text.detached }),
1320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1321
+ className: "dsh-worktree-badge",
1322
+ children: text.primaryBranch
1323
+ }),
1324
+ !creation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})
1325
+ ]
1326
+ })
1327
+ }, current.path)]
1101
1328
  })
1102
1329
  }),
1103
1330
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1132,13 +1359,22 @@ function apply(ctx) {
1132
1359
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.errorCode }),
1133
1360
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: detailError.code }) })
1134
1361
  ] })] }),
1135
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", { children: [view && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1363
+ type: "button",
1364
+ "data-worktree-choose": true,
1365
+ disabled,
1366
+ onClick: () => {
1367
+ errorDialog.current?.close();
1368
+ setOpen(true);
1369
+ },
1370
+ children: text.choose
1371
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1136
1372
  type: "button",
1137
1373
  "data-worktree-retry": true,
1138
- disabled: loading || busy,
1374
+ disabled: loading,
1139
1375
  onClick: () => void load(true),
1140
1376
  children: loading ? text.loading : text.refresh
1141
- }) })
1377
+ })] })
1142
1378
  ]
1143
1379
  })
1144
1380
  ]