@dungsil/dsh-plugin-ui-worktree 0.5.0 → 0.6.1
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 +7 -7
- package/dist/client.js +237 -109
- package/dist/index.js +255 -75
- package/package.json +43 -41
package/dist/client.js
CHANGED
|
@@ -20,7 +20,7 @@ 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:
|
|
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; }
|
|
@@ -56,12 +56,13 @@ const ComposerTrigger = /* @__PURE__ */ (0, react.forwardRef)(function ComposerT
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
// ../../tools/ui/src/settings.tsx
|
|
59
|
-
/**
|
|
59
|
+
/** Decorate matching navigation rows in both the rc.5 and 0.2.2 settings shells. */
|
|
60
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
|
-
${selector} > svg
|
|
64
|
+
${selector} > svg,
|
|
65
|
+
${selector} > [aria-hidden="true"]:not([data-dsh-settings-icon-glyph]) { display: none; }
|
|
65
66
|
${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
67
|
${selector} > [data-dsh-settings-icon-glyph] { display: inline-flex; flex: none; }
|
|
67
68
|
`);
|
|
@@ -188,10 +189,15 @@ function currentWorktree(view) {
|
|
|
188
189
|
return cwd === path || cwd.startsWith(path + "/");
|
|
189
190
|
}).sort((a, b) => comparablePath(b.path).length - comparablePath(a.path).length)[0];
|
|
190
191
|
}
|
|
191
|
-
function
|
|
192
|
+
function isWorktreeSelectionView(value) {
|
|
192
193
|
if (!value || typeof value !== "object") return false;
|
|
193
194
|
const view = value;
|
|
194
|
-
return typeof view.cwd === "string" && (view.
|
|
195
|
+
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");
|
|
196
|
+
}
|
|
197
|
+
function isWorktreeView(value) {
|
|
198
|
+
if (!isWorktreeSelectionView(value)) return false;
|
|
199
|
+
const view = value;
|
|
200
|
+
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"));
|
|
195
201
|
}
|
|
196
202
|
// src/errors.ts
|
|
197
203
|
const failureCodes = [
|
|
@@ -226,18 +232,22 @@ function failureCode(error) {
|
|
|
226
232
|
// src/requests.ts
|
|
227
233
|
const copy$1 = {
|
|
228
234
|
zh: {
|
|
229
|
-
title: "
|
|
235
|
+
title: "下一任务的位置",
|
|
236
|
+
choose: "选择工作树",
|
|
237
|
+
savingSelection: "正在保存下一任务的位置…",
|
|
238
|
+
selectionError: "请确认工作树选择后再发送提示。",
|
|
239
|
+
locked: "任务开始后无法更改此会话的工作树。请在其他工作树中新建会话。",
|
|
230
240
|
detached: "分离的 HEAD",
|
|
231
241
|
unknown: "无法获取分支",
|
|
232
242
|
create: "新工作树",
|
|
233
243
|
primaryBranch: "当前分支",
|
|
234
|
-
reservationHint: "
|
|
235
|
-
creationError: "
|
|
244
|
+
reservationHint: "发送下一条提示后,将先创建并切换到新工作树再开始任务。选择当前分支可取消。",
|
|
245
|
+
creationError: "无法确认创建结果,尚未开始任务。请选择当前分支或重新预约,然后重新发送提示。",
|
|
236
246
|
close: "关闭",
|
|
237
247
|
refresh: "刷新工作树",
|
|
238
248
|
loading: "正在加载工作树…",
|
|
239
249
|
busy: "正在切换工作树…",
|
|
240
|
-
changed: "
|
|
250
|
+
changed: "已选择下一任务的位置。",
|
|
241
251
|
empty: "未找到 Git 工作树。",
|
|
242
252
|
wait: "请等待会话准备就绪且当前任务结束。",
|
|
243
253
|
error: "无法确认当前状态。请刷新工作树以检查状态后再重试。",
|
|
@@ -251,11 +261,11 @@ const copy$1 = {
|
|
|
251
261
|
describe: "查询工作树",
|
|
252
262
|
initialize: "初始化工作树选择",
|
|
253
263
|
create: "创建工作树",
|
|
254
|
-
|
|
264
|
+
select: "保存下一任务的位置"
|
|
255
265
|
},
|
|
256
266
|
reasons: {
|
|
257
267
|
busy: "会话中有正在执行的任务。请在任务结束后重新查询。",
|
|
258
|
-
conflict: "
|
|
268
|
+
conflict: "当前工作目录已更改或工作树选择已锁定。请重新查询状态。",
|
|
259
269
|
invalid: "请求无效。请刷新列表并检查分支名称或工作树。",
|
|
260
270
|
operation_failed: "主机无法完成操作。请检查 Git 和工作树配置。",
|
|
261
271
|
session_not_found: "会话不可用。请重新打开会话后重试。",
|
|
@@ -263,24 +273,28 @@ const copy$1 = {
|
|
|
263
273
|
auth_required: "连接需要身份验证。请重新打开应用后重试。",
|
|
264
274
|
endpoint_missing: "主机没有工作树接口。请确认插件已启用并重启应用。",
|
|
265
275
|
client_unavailable: "连接缺少工作树 RPC 客户端。请刷新页面或更新应用。",
|
|
266
|
-
invalid_response: "
|
|
276
|
+
invalid_response: "主机返回的工作树状态无效。请检查应用和插件版本。",
|
|
267
277
|
connection_error: "收到有效响应前连接失败。请检查连接后重新查询。",
|
|
268
278
|
timeout: "请求超时。请在当前任务结束后重新查询。"
|
|
269
279
|
}
|
|
270
280
|
},
|
|
271
281
|
en: {
|
|
272
|
-
title: "
|
|
282
|
+
title: "Next task location",
|
|
283
|
+
choose: "Choose worktree",
|
|
284
|
+
savingSelection: "Saving next task location…",
|
|
285
|
+
selectionError: "Confirm the worktree selection before sending your prompt.",
|
|
286
|
+
locked: "The worktree cannot change after work starts. Create a new session in another worktree.",
|
|
273
287
|
detached: "Detached HEAD",
|
|
274
288
|
unknown: "Branch unavailable",
|
|
275
289
|
create: "New worktree",
|
|
276
290
|
primaryBranch: "Current branch",
|
|
277
|
-
reservationHint: "The next prompt will create and enter a new worktree before starting work. Select
|
|
278
|
-
creationError: "Creation could not be confirmed, so work did not start.
|
|
291
|
+
reservationHint: "The next prompt will create and enter a new worktree before starting work. Select the current branch to cancel.",
|
|
292
|
+
creationError: "Creation could not be confirmed, so work did not start. Select the current branch or schedule a new worktree, then resend your prompt.",
|
|
279
293
|
close: "Close",
|
|
280
294
|
refresh: "Refresh worktrees",
|
|
281
295
|
loading: "Loading worktrees…",
|
|
282
296
|
busy: "Changing worktree…",
|
|
283
|
-
changed: "
|
|
297
|
+
changed: "Next task location selected.",
|
|
284
298
|
empty: "No Git worktrees found.",
|
|
285
299
|
wait: "Wait until the session is ready and idle.",
|
|
286
300
|
error: "Could not confirm the current state. Refresh worktrees to check before trying again.",
|
|
@@ -294,11 +308,11 @@ const copy$1 = {
|
|
|
294
308
|
describe: "List worktrees",
|
|
295
309
|
initialize: "Initialize worktree selection",
|
|
296
310
|
create: "Create worktree",
|
|
297
|
-
|
|
311
|
+
select: "Save next task location"
|
|
298
312
|
},
|
|
299
313
|
reasons: {
|
|
300
314
|
busy: "The session has an active task. Retry after the task stops.",
|
|
301
|
-
conflict: "The current directory changed
|
|
315
|
+
conflict: "The current directory changed or the worktree selection is locked. Reload to check the state.",
|
|
302
316
|
invalid: "The request is invalid. Reload the list and check the branch name or worktree.",
|
|
303
317
|
operation_failed: "The host could not complete the operation. Check Git and the worktree configuration.",
|
|
304
318
|
session_not_found: "The session is unavailable. Reopen the session and retry.",
|
|
@@ -306,24 +320,28 @@ const copy$1 = {
|
|
|
306
320
|
auth_required: "The connection requires authentication. Reopen the application and retry.",
|
|
307
321
|
endpoint_missing: "The host has no worktree endpoint. Check that the plugin is active and restart the application.",
|
|
308
322
|
client_unavailable: "The connection has no worktree RPC client. Reload the page or update the application.",
|
|
309
|
-
invalid_response: "The host returned an invalid worktree
|
|
323
|
+
invalid_response: "The host returned an invalid worktree state. Check the application and plugin versions.",
|
|
310
324
|
connection_error: "The connection failed before a valid response. Check the connection and retry.",
|
|
311
325
|
timeout: "The request timed out. Retry after the current task stops."
|
|
312
326
|
}
|
|
313
327
|
},
|
|
314
328
|
ko: {
|
|
315
|
-
title: "
|
|
329
|
+
title: "다음 작업 위치",
|
|
330
|
+
choose: "워크트리 선택",
|
|
331
|
+
savingSelection: "다음 작업 위치를 저장하는 중…",
|
|
332
|
+
selectionError: "워크트리 선택을 확인한 뒤 프롬프트를 전송하세요.",
|
|
333
|
+
locked: "작업을 시작한 세션에서는 워크트리를 바꿀 수 없습니다. 다른 워크트리에서 새 세션을 만드세요.",
|
|
316
334
|
detached: "분리된 HEAD",
|
|
317
335
|
unknown: "브랜치 확인 불가",
|
|
318
336
|
create: "새 워크트리",
|
|
319
337
|
primaryBranch: "현재 브랜치",
|
|
320
|
-
reservationHint: "다음 프롬프트를 전송하면 새 워크트리를 생성·전환한 뒤 작업을 시작합니다.
|
|
321
|
-
creationError: "생성 결과를 확인하지 못해 작업을 시작하지 않았습니다.
|
|
338
|
+
reservationHint: "다음 프롬프트를 전송하면 새 워크트리를 생성·전환한 뒤 작업을 시작합니다. 현재 브랜치를 선택하면 예약을 취소합니다.",
|
|
339
|
+
creationError: "생성 결과를 확인하지 못해 작업을 시작하지 않았습니다. 현재 브랜치를 선택하거나 다시 예약한 뒤 프롬프트를 다시 전송하세요.",
|
|
322
340
|
close: "닫기",
|
|
323
341
|
refresh: "워크트리 새로고침",
|
|
324
342
|
loading: "워크트리를 불러오는 중…",
|
|
325
343
|
busy: "워크트리 변경 중…",
|
|
326
|
-
changed: "
|
|
344
|
+
changed: "다음 작업 위치를 선택했습니다.",
|
|
327
345
|
empty: "Git worktree가 없습니다.",
|
|
328
346
|
wait: "세션이 준비되고 실행 중인 작업이 끝나면 사용할 수 있습니다.",
|
|
329
347
|
error: "현재 상태를 확인하지 못했습니다. 다시 시도하기 전에 새로고침으로 상태를 확인하세요.",
|
|
@@ -337,11 +355,11 @@ const copy$1 = {
|
|
|
337
355
|
describe: "워크트리 조회",
|
|
338
356
|
initialize: "워크트리 선택 초기화",
|
|
339
357
|
create: "워크트리 생성",
|
|
340
|
-
|
|
358
|
+
select: "다음 작업 위치 저장"
|
|
341
359
|
},
|
|
342
360
|
reasons: {
|
|
343
361
|
busy: "세션에서 작업을 실행 중입니다. 작업이 끝난 뒤 다시 조회하세요.",
|
|
344
|
-
conflict: "현재 작업 경로가
|
|
362
|
+
conflict: "현재 작업 경로가 바뀌었거나 워크트리 선택이 고정됐습니다. 다시 조회해 상태를 확인하세요.",
|
|
345
363
|
invalid: "요청이 올바르지 않습니다. 목록을 새로고침하고 브랜치 이름이나 워크트리를 확인하세요.",
|
|
346
364
|
operation_failed: "호스트가 작업을 완료하지 못했습니다. Git과 워크트리 설정을 확인하세요.",
|
|
347
365
|
session_not_found: "세션을 찾지 못했습니다. 세션을 다시 열고 조회하세요.",
|
|
@@ -349,7 +367,7 @@ const copy$1 = {
|
|
|
349
367
|
auth_required: "연결 인증이 필요합니다. 앱을 다시 열고 조회하세요.",
|
|
350
368
|
endpoint_missing: "호스트에 워크트리 API가 없습니다. 플러그인을 활성화했는지 확인하고 앱을 다시 시작하세요.",
|
|
351
369
|
client_unavailable: "연결에 워크트리 RPC 기능이 없습니다. 페이지를 새로고침하거나 앱을 업데이트하세요.",
|
|
352
|
-
invalid_response: "호스트가 올바르지 않은 워크트리
|
|
370
|
+
invalid_response: "호스트가 올바르지 않은 워크트리 상태를 반환했습니다. 앱과 플러그인 버전을 확인하세요.",
|
|
353
371
|
connection_error: "정상 응답을 받기 전에 연결이 실패했습니다. 연결을 확인하고 다시 조회하세요.",
|
|
354
372
|
timeout: "요청 시간이 초과됐습니다. 실행 중인 작업이 끝난 뒤 다시 조회하세요."
|
|
355
373
|
}
|
|
@@ -706,6 +724,7 @@ const styles = `${settingsStyles}${composerStyles}
|
|
|
706
724
|
.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; }
|
|
707
725
|
.dsh-worktree-trigger { display: inline-flex; max-width: 220px; font-weight: 500; }
|
|
708
726
|
.dsh-worktree-trigger .dsh-plugin-composer-icon svg { width: 14px; height: 14px; }
|
|
727
|
+
.dsh-worktree-trigger:not([aria-haspopup]) .dsh-plugin-composer-chevron { display: none; }
|
|
709
728
|
.dsh-worktree-trigger:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 0; }
|
|
710
729
|
@container (width <= 460px) {
|
|
711
730
|
.dsh-worktree-trigger .dsh-plugin-composer-label { display: none; }
|
|
@@ -944,7 +963,8 @@ const inject = [
|
|
|
944
963
|
"slots",
|
|
945
964
|
"locale",
|
|
946
965
|
"connection",
|
|
947
|
-
"uiConversation"
|
|
966
|
+
"uiConversation",
|
|
967
|
+
"conversation"
|
|
948
968
|
];
|
|
949
969
|
function apply(ctx) {
|
|
950
970
|
ctx.effect(() => installSettingsIcon(name, [
|
|
@@ -954,25 +974,45 @@ function apply(ctx) {
|
|
|
954
974
|
].map(settingsLabel), git_branch_default));
|
|
955
975
|
const connection = ctx.get("connection");
|
|
956
976
|
installProgress(ctx);
|
|
957
|
-
|
|
977
|
+
const synced = /* @__PURE__ */ new Map();
|
|
978
|
+
const ownedBlocks = /* @__PURE__ */ new Map();
|
|
979
|
+
const blocks = ctx.conversation.blocks;
|
|
980
|
+
ctx.effect(() => () => {
|
|
981
|
+
const owned = [...ownedBlocks];
|
|
982
|
+
ownedBlocks.clear();
|
|
983
|
+
for (const [id, block] of owned) if (blocks.storeFor(id).getSnapshot() === block) blocks.set(id, void 0);
|
|
984
|
+
});
|
|
985
|
+
function Picker({ sessionId, useSession, useProjection, useInput }) {
|
|
958
986
|
const { active: language } = useLocaleSnapshot(ctx.locale);
|
|
959
987
|
const text = copy$1[language.startsWith("ko") ? "ko" : language.startsWith("zh") ? "zh" : "en"];
|
|
960
|
-
const
|
|
988
|
+
const inputBusy = useInput((input) => input.phase !== "plain");
|
|
989
|
+
const unavailable = useSession((s) => s.running || s.removed || s.openState !== "open" || s.pendingSubmissions.length > 0) || inputBusy;
|
|
961
990
|
const unavailableRef = (0, react.useRef)(unavailable);
|
|
962
991
|
unavailableRef.current = unavailable;
|
|
963
992
|
const unreadable = useSession((s) => s.removed || s.openState !== "open");
|
|
964
993
|
const unreadableRef = (0, react.useRef)(unreadable);
|
|
965
994
|
unreadableRef.current = unreadable;
|
|
995
|
+
const cwd = useProjection("workingDirectory");
|
|
996
|
+
const started = useProjection("sessionListMetadata")?.blank === false;
|
|
997
|
+
const cached = synced.get(sessionId);
|
|
998
|
+
const initialView = cached?.cwd === cwd ? cached : void 0;
|
|
966
999
|
const previousReadiness = (0, react.useRef)({
|
|
967
1000
|
unreadable: true,
|
|
968
|
-
unavailable
|
|
1001
|
+
unavailable,
|
|
1002
|
+
cwd
|
|
969
1003
|
});
|
|
970
|
-
const [view, setView] = (0, react.useState)();
|
|
971
|
-
const [loading, setLoading] = (0, react.useState)(
|
|
972
|
-
const
|
|
1004
|
+
const [view, setView] = (0, react.useState)(initialView);
|
|
1005
|
+
const [loading, setLoading] = (0, react.useState)(!initialView);
|
|
1006
|
+
const viewRef = (0, react.useRef)(view);
|
|
1007
|
+
viewRef.current = view;
|
|
1008
|
+
const selected = (0, react.useRef)();
|
|
1009
|
+
const saving = (0, react.useRef)(false);
|
|
1010
|
+
const version = (0, react.useRef)(0);
|
|
973
1011
|
const [error, setError] = (0, react.useState)();
|
|
974
1012
|
const [changed, setChanged] = (0, react.useState)(false);
|
|
975
1013
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1014
|
+
const needsSync = (0, react.useRef)(true);
|
|
1015
|
+
needsSync.current = !view || !view.locked && (!!view.needsInitialization || !!view.pendingCreation);
|
|
976
1016
|
const pending = (0, react.useRef)(false);
|
|
977
1017
|
const reloadRequested = (0, react.useRef)(false);
|
|
978
1018
|
const needsRefresh = (0, react.useRef)(false);
|
|
@@ -982,38 +1022,74 @@ function apply(ctx) {
|
|
|
982
1022
|
const errorTitle = (0, react.useId)();
|
|
983
1023
|
const errorDescription = (0, react.useId)();
|
|
984
1024
|
const status = (0, react.useId)();
|
|
985
|
-
const disabled = unavailable || loading || busy || !!error || !view;
|
|
986
1025
|
const creation = view?.pendingCreation;
|
|
1026
|
+
const locked = !!view?.locked || started && !creation;
|
|
1027
|
+
const disabled = locked || unavailable || loading || !view;
|
|
987
1028
|
const detailError = error ?? (creation?.failed ? {
|
|
988
1029
|
operation: "create",
|
|
989
1030
|
code: "operation_failed",
|
|
990
1031
|
uncertain: true
|
|
991
1032
|
} : void 0);
|
|
992
1033
|
const errorLabel = detailError?.uncertain ? text.mutationError : text.lookupError;
|
|
1034
|
+
const gate = (reason) => {
|
|
1035
|
+
const store = blocks.storeFor(sessionId);
|
|
1036
|
+
const owned = ownedBlocks.get(sessionId);
|
|
1037
|
+
const current = store.getSnapshot();
|
|
1038
|
+
if (!reason) {
|
|
1039
|
+
ownedBlocks.delete(sessionId);
|
|
1040
|
+
if (owned && current === owned) blocks.set(sessionId, void 0);
|
|
1041
|
+
} else {
|
|
1042
|
+
synced.delete(sessionId);
|
|
1043
|
+
const block = owned?.reason === reason ? owned : { reason };
|
|
1044
|
+
ownedBlocks.set(sessionId, block);
|
|
1045
|
+
if (!current || current === owned) blocks.set(sessionId, block);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
993
1048
|
const recordFailure = (operation, cause) => {
|
|
994
1049
|
const code = failureCode(cause);
|
|
995
1050
|
needsRefresh.current = true;
|
|
1051
|
+
const uncertain = operation !== "describe" || ownedBlocks.has(sessionId);
|
|
1052
|
+
if (uncertain) gate(text.selectionError);
|
|
996
1053
|
setError((previous) => ({
|
|
997
1054
|
operation,
|
|
998
1055
|
code,
|
|
999
|
-
uncertain:
|
|
1056
|
+
uncertain: uncertain || !!previous?.uncertain
|
|
1000
1057
|
}));
|
|
1001
1058
|
console.warn("[dungsil-ui-worktree]", {
|
|
1002
1059
|
operation,
|
|
1003
1060
|
code
|
|
1004
1061
|
});
|
|
1005
1062
|
};
|
|
1006
|
-
const
|
|
1063
|
+
const call = async (operation, payload, signal) => {
|
|
1007
1064
|
if (typeof connection?.rpc?.call !== "function") throw { code: "client_unavailable" };
|
|
1008
1065
|
const result = await connection.rpc.call(channel, endpointPrefix + operation, payload, signal);
|
|
1009
1066
|
signal.throwIfAborted();
|
|
1010
1067
|
if (!result.ok) throw result.error;
|
|
1011
|
-
if (!isWorktreeView(result.value)) throw { code: "invalid_response" };
|
|
1012
1068
|
return result.value;
|
|
1013
1069
|
};
|
|
1070
|
+
const request = async (operation, payload, signal) => {
|
|
1071
|
+
const value = await call(operation, payload, signal);
|
|
1072
|
+
if (!isWorktreeView(value)) throw { code: "invalid_response" };
|
|
1073
|
+
return value;
|
|
1074
|
+
};
|
|
1075
|
+
const updateView = (next) => {
|
|
1076
|
+
viewRef.current = next;
|
|
1077
|
+
setView(next);
|
|
1078
|
+
};
|
|
1079
|
+
const mergeSelection = (next) => {
|
|
1080
|
+
const previous = viewRef.current;
|
|
1081
|
+
if (!previous) return;
|
|
1082
|
+
updateView({
|
|
1083
|
+
...previous,
|
|
1084
|
+
cwd: next.cwd,
|
|
1085
|
+
pendingCreation: next.pendingCreation,
|
|
1086
|
+
locked: next.locked,
|
|
1087
|
+
needsInitialization: false
|
|
1088
|
+
});
|
|
1089
|
+
};
|
|
1014
1090
|
const load = async (explicit = false) => {
|
|
1015
1091
|
const controller = lifetime.current;
|
|
1016
|
-
if (!controller || controller.signal.aborted || unreadableRef.current || needsRefresh.current && !explicit) return;
|
|
1092
|
+
if (!controller || controller.signal.aborted || unreadableRef.current || saving.current || needsRefresh.current && !explicit) return;
|
|
1017
1093
|
if (pending.current) {
|
|
1018
1094
|
reloadRequested.current = true;
|
|
1019
1095
|
return;
|
|
@@ -1025,20 +1101,25 @@ function apply(ctx) {
|
|
|
1025
1101
|
try {
|
|
1026
1102
|
const view = await request("describe", { sessionId }, controller.signal);
|
|
1027
1103
|
if (controller.signal.aborted) return;
|
|
1028
|
-
|
|
1029
|
-
if (view.needsInitialization && !unavailableRef.current) {
|
|
1104
|
+
updateView(view);
|
|
1105
|
+
if (view.needsInitialization && !view.locked && !unavailableRef.current) {
|
|
1030
1106
|
operation = "initialize";
|
|
1107
|
+
gate(text.savingSelection);
|
|
1031
1108
|
const initialized = await request("initialize", {
|
|
1032
1109
|
sessionId,
|
|
1033
1110
|
cwd: view.cwd
|
|
1034
1111
|
}, controller.signal);
|
|
1035
1112
|
if (controller.signal.aborted) return;
|
|
1036
|
-
|
|
1113
|
+
updateView(initialized);
|
|
1037
1114
|
}
|
|
1038
1115
|
needsRefresh.current = false;
|
|
1039
1116
|
setError(void 0);
|
|
1117
|
+
gate();
|
|
1040
1118
|
} catch (cause) {
|
|
1041
|
-
if (!controller.signal.aborted
|
|
1119
|
+
if (!controller.signal.aborted) {
|
|
1120
|
+
if (failureCode(cause) !== "busy") recordFailure(operation, cause);
|
|
1121
|
+
else if (!needsRefresh.current && operation === "initialize") gate();
|
|
1122
|
+
}
|
|
1042
1123
|
} finally {
|
|
1043
1124
|
if (!controller.signal.aborted) {
|
|
1044
1125
|
pending.current = false;
|
|
@@ -1054,8 +1135,15 @@ function apply(ctx) {
|
|
|
1054
1135
|
const controller = new AbortController();
|
|
1055
1136
|
lifetime.current = controller;
|
|
1056
1137
|
pending.current = false;
|
|
1138
|
+
const store = blocks.storeFor(sessionId);
|
|
1139
|
+
const restoreGate = () => {
|
|
1140
|
+
const block = ownedBlocks.get(sessionId);
|
|
1141
|
+
if (block && !store.getSnapshot()) blocks.set(sessionId, block);
|
|
1142
|
+
};
|
|
1143
|
+
restoreGate();
|
|
1144
|
+
const unsubscribe = store.subscribe(restoreGate);
|
|
1057
1145
|
const focus = () => {
|
|
1058
|
-
load();
|
|
1146
|
+
if (needsSync.current && !pending.current) load();
|
|
1059
1147
|
};
|
|
1060
1148
|
const resize = () => {
|
|
1061
1149
|
setOpen(false);
|
|
@@ -1064,6 +1152,7 @@ function apply(ctx) {
|
|
|
1064
1152
|
window.addEventListener("resize", resize);
|
|
1065
1153
|
return () => {
|
|
1066
1154
|
controller.abort();
|
|
1155
|
+
unsubscribe();
|
|
1067
1156
|
window.removeEventListener("focus", focus);
|
|
1068
1157
|
window.removeEventListener("resize", resize);
|
|
1069
1158
|
};
|
|
@@ -1072,46 +1161,85 @@ function apply(ctx) {
|
|
|
1072
1161
|
const previous = previousReadiness.current;
|
|
1073
1162
|
previousReadiness.current = {
|
|
1074
1163
|
unreadable,
|
|
1075
|
-
unavailable
|
|
1164
|
+
unavailable,
|
|
1165
|
+
cwd
|
|
1076
1166
|
};
|
|
1077
|
-
if (!unreadable && (previous.unreadable || previous.unavailable && !unavailable)) load();
|
|
1078
|
-
}, [
|
|
1167
|
+
if (!unreadable && ((previous.unreadable || previous.unavailable && !unavailable) && needsSync.current || previous.cwd !== cwd)) load();
|
|
1168
|
+
}, [
|
|
1169
|
+
unreadable,
|
|
1170
|
+
unavailable,
|
|
1171
|
+
cwd
|
|
1172
|
+
]);
|
|
1173
|
+
(0, react.useEffect)(() => {
|
|
1174
|
+
if (locked && view && !creation && view.cwd === cwd && !error && !ownedBlocks.has(sessionId)) synced.set(sessionId, {
|
|
1175
|
+
...view,
|
|
1176
|
+
locked: true,
|
|
1177
|
+
needsInitialization: false
|
|
1178
|
+
});
|
|
1179
|
+
else synced.delete(sessionId);
|
|
1180
|
+
}, [
|
|
1181
|
+
sessionId,
|
|
1182
|
+
view,
|
|
1183
|
+
locked,
|
|
1184
|
+
creation,
|
|
1185
|
+
cwd,
|
|
1186
|
+
error
|
|
1187
|
+
]);
|
|
1079
1188
|
(0, react.useEffect)(() => {
|
|
1080
1189
|
if (disabled) setOpen(false);
|
|
1081
1190
|
}, [disabled]);
|
|
1082
|
-
const change = async (
|
|
1191
|
+
const change = async (choice) => {
|
|
1083
1192
|
const controller = lifetime.current;
|
|
1084
|
-
|
|
1085
|
-
pending.current
|
|
1086
|
-
|
|
1087
|
-
|
|
1193
|
+
const currentView = viewRef.current;
|
|
1194
|
+
if (locked || unavailableRef.current || loading || pending.current || !currentView || !controller || controller.signal.aborted) return;
|
|
1195
|
+
const intent = {
|
|
1196
|
+
version: ++version.current,
|
|
1197
|
+
cwd: currentView.cwd,
|
|
1198
|
+
choice
|
|
1199
|
+
};
|
|
1200
|
+
selected.current = intent;
|
|
1201
|
+
mergeSelection({
|
|
1202
|
+
cwd: currentView.cwd,
|
|
1203
|
+
pendingCreation: choice === "new" ? {
|
|
1204
|
+
name: text.create,
|
|
1205
|
+
failed: false
|
|
1206
|
+
} : void 0
|
|
1207
|
+
});
|
|
1208
|
+
setError(void 0);
|
|
1209
|
+
setChanged(true);
|
|
1210
|
+
needsRefresh.current = false;
|
|
1211
|
+
gate(text.savingSelection);
|
|
1212
|
+
if (saving.current) return;
|
|
1213
|
+
saving.current = true;
|
|
1088
1214
|
try {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1215
|
+
while (selected.current && !controller.signal.aborted) {
|
|
1216
|
+
const next = selected.current;
|
|
1217
|
+
try {
|
|
1218
|
+
const value = await call("select", {
|
|
1219
|
+
sessionId,
|
|
1220
|
+
cwd: next.cwd,
|
|
1221
|
+
choice: next.choice
|
|
1222
|
+
}, controller.signal);
|
|
1223
|
+
if (!isWorktreeSelectionView(value)) throw { code: "invalid_response" };
|
|
1224
|
+
if (selected.current?.version !== next.version) continue;
|
|
1225
|
+
mergeSelection(value);
|
|
1226
|
+
selected.current = void 0;
|
|
1227
|
+
gate();
|
|
1228
|
+
} catch (cause) {
|
|
1229
|
+
if (controller.signal.aborted) return;
|
|
1230
|
+
if (selected.current?.version !== next.version) continue;
|
|
1231
|
+
selected.current = void 0;
|
|
1232
|
+
recordFailure("select", cause);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1109
1235
|
}
|
|
1236
|
+
} finally {
|
|
1237
|
+
saving.current = false;
|
|
1110
1238
|
}
|
|
1111
1239
|
};
|
|
1112
1240
|
(0, react.useEffect)(() => {
|
|
1113
|
-
if (
|
|
1114
|
-
}, [
|
|
1241
|
+
if (changed) trigger.current?.focus();
|
|
1242
|
+
}, [changed]);
|
|
1115
1243
|
(0, react.useEffect)(() => {
|
|
1116
1244
|
if (!detailError && errorDialog.current?.open) errorDialog.current.close();
|
|
1117
1245
|
}, [error, creation?.failed]);
|
|
@@ -1127,13 +1255,18 @@ function apply(ctx) {
|
|
|
1127
1255
|
current,
|
|
1128
1256
|
view
|
|
1129
1257
|
]);
|
|
1130
|
-
const
|
|
1258
|
+
const selectCurrent = () => {
|
|
1259
|
+
setOpen(false);
|
|
1260
|
+
if (disabled) return;
|
|
1261
|
+
if (viewRef.current?.pendingCreation || needsRefresh.current) change("current");
|
|
1262
|
+
};
|
|
1263
|
+
const branch = creation ? text.create : current ? current.branch ?? text.detached : text.unknown;
|
|
1131
1264
|
const label = detailError ? errorLabel : view ? branch : loading ? text.loading : text.title;
|
|
1132
|
-
const notice = loading ? text.loading :
|
|
1265
|
+
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 : "";
|
|
1133
1266
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1134
1267
|
className: "dsh-worktree",
|
|
1135
1268
|
children: [
|
|
1136
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
1137
1270
|
className: "dsh-worktree-picker",
|
|
1138
1271
|
open,
|
|
1139
1272
|
side: "top",
|
|
@@ -1145,16 +1278,16 @@ function apply(ctx) {
|
|
|
1145
1278
|
className: "dsh-worktree-trigger",
|
|
1146
1279
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
|
|
1147
1280
|
label,
|
|
1148
|
-
disabled: detailError ? loading
|
|
1281
|
+
disabled: detailError ? loading : disabled,
|
|
1149
1282
|
"aria-label": `${text.title}: ${label}`,
|
|
1150
|
-
"aria-haspopup": detailError ? "dialog" : "menu",
|
|
1151
|
-
"aria-expanded": detailError ? void 0 : open,
|
|
1283
|
+
"aria-haspopup": detailError ? "dialog" : locked ? void 0 : "menu",
|
|
1284
|
+
"aria-expanded": detailError || locked ? void 0 : open,
|
|
1152
1285
|
"aria-describedby": status,
|
|
1153
|
-
"aria-busy": loading
|
|
1154
|
-
title:
|
|
1286
|
+
"aria-busy": loading,
|
|
1287
|
+
title: locked ? text.locked : `${text.title}: ${branch}${creation ? ` — ${text.reservationHint}` : ""}`,
|
|
1155
1288
|
onClick: () => {
|
|
1156
1289
|
if (detailError) errorDialog.current?.showModal();
|
|
1157
|
-
else setOpen((value) => !value);
|
|
1290
|
+
else if (!disabled) setOpen((value) => !value);
|
|
1158
1291
|
},
|
|
1159
1292
|
onKeyDown: (event) => {
|
|
1160
1293
|
if (!detailError && !disabled && !open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
@@ -1163,51 +1296,37 @@ function apply(ctx) {
|
|
|
1163
1296
|
}
|
|
1164
1297
|
}
|
|
1165
1298
|
}),
|
|
1166
|
-
children:
|
|
1299
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1167
1300
|
className: "dsh-worktree-pinned",
|
|
1168
1301
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
|
|
1169
1302
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, {}),
|
|
1170
1303
|
current: !!creation,
|
|
1171
1304
|
onSelect: () => {
|
|
1172
1305
|
setOpen(false);
|
|
1173
|
-
|
|
1306
|
+
const latest = viewRef.current?.pendingCreation;
|
|
1307
|
+
if (!disabled && (!latest || latest.failed || needsRefresh.current)) change("new");
|
|
1174
1308
|
},
|
|
1175
1309
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1176
1310
|
className: "dsh-worktree-option",
|
|
1177
1311
|
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, {})]
|
|
1178
1312
|
})
|
|
1179
|
-
}),
|
|
1313
|
+
}), current && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
|
|
1180
1314
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
|
|
1181
|
-
current: !creation
|
|
1182
|
-
onSelect:
|
|
1183
|
-
setOpen(false);
|
|
1184
|
-
if (creation || current !== item) change("switch", item.path);
|
|
1185
|
-
},
|
|
1315
|
+
current: !creation,
|
|
1316
|
+
onSelect: selectCurrent,
|
|
1186
1317
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1187
1318
|
className: "dsh-worktree-option",
|
|
1188
1319
|
children: [
|
|
1189
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children:
|
|
1320
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: current.branch ?? text.detached }),
|
|
1190
1321
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1191
1322
|
className: "dsh-worktree-badge",
|
|
1192
1323
|
children: text.primaryBranch
|
|
1193
1324
|
}),
|
|
1194
|
-
!creation &&
|
|
1325
|
+
!creation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})
|
|
1195
1326
|
]
|
|
1196
1327
|
})
|
|
1197
|
-
},
|
|
1198
|
-
})
|
|
1199
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
|
|
1200
|
-
current: !creation && current === item,
|
|
1201
|
-
separatorBefore: index === 0,
|
|
1202
|
-
onSelect: () => {
|
|
1203
|
-
setOpen(false);
|
|
1204
|
-
if (creation || current !== item) change("switch", item.path);
|
|
1205
|
-
},
|
|
1206
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1207
|
-
className: "dsh-worktree-option",
|
|
1208
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.branch ?? text.detached }), !creation && current === item && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})]
|
|
1209
|
-
})
|
|
1210
|
-
}, item.path))]
|
|
1328
|
+
}, current.path)]
|
|
1329
|
+
})
|
|
1211
1330
|
}),
|
|
1212
1331
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1213
1332
|
id: status,
|
|
@@ -1241,13 +1360,22 @@ function apply(ctx) {
|
|
|
1241
1360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.errorCode }),
|
|
1242
1361
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: detailError.code }) })
|
|
1243
1362
|
] })] }),
|
|
1244
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", { children: [view && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1364
|
+
type: "button",
|
|
1365
|
+
"data-worktree-choose": true,
|
|
1366
|
+
disabled,
|
|
1367
|
+
onClick: () => {
|
|
1368
|
+
errorDialog.current?.close();
|
|
1369
|
+
setOpen(true);
|
|
1370
|
+
},
|
|
1371
|
+
children: text.choose
|
|
1372
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1245
1373
|
type: "button",
|
|
1246
1374
|
"data-worktree-retry": true,
|
|
1247
|
-
disabled: loading
|
|
1375
|
+
disabled: loading,
|
|
1248
1376
|
onClick: () => void load(true),
|
|
1249
1377
|
children: loading ? text.loading : text.refresh
|
|
1250
|
-
}) })
|
|
1378
|
+
})] })
|
|
1251
1379
|
]
|
|
1252
1380
|
})
|
|
1253
1381
|
]
|