@dungsil/dsh-plugin-ui-vcs 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/dist/client.js +103 -88
  2. package/dist/index.js +182 -109
  3. package/package.json +2 -1
package/dist/client.js CHANGED
@@ -23,6 +23,10 @@ var git_pull_request_draft_default = "<svg xmlns=\"http://www.w3.org/2000/svg\"
23
23
  var git_pull_request_closed_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=\"5\" cy=\"5\" r=\"2\"/>\n <circle cx=\"5\" cy=\"19\" r=\"2\"/>\n <circle cx=\"19\" cy=\"19\" r=\"2\"/>\n <path d=\"M5 7v10M19 17v-5M16 3l6 6m0-6-6 6\"/>\n</svg>\n";
24
24
  // ../../tools/ui/src/icons/git-merge.svg
25
25
  var git_merge_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=\"5\" cy=\"5\" r=\"2\"/>\n <circle cx=\"5\" cy=\"19\" r=\"2\"/>\n <circle cx=\"19\" cy=\"19\" r=\"2\"/>\n <path d=\"M5 7v10M5 7a12 12 0 0 0 12 12\"/>\n</svg>\n";
26
+ // ../../tools/ui/src/icons/circle-x.svg
27
+ var circle_x_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=\"12\" cy=\"12\" r=\"9\"/>\n <path d=\"m9 9 6 6m0-6-6 6\"/>\n</svg>\n";
28
+ // ../../tools/ui/src/icons/circle-minus.svg
29
+ var circle_minus_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=\"12\" cy=\"12\" r=\"9\"/>\n <path d=\"M9 12h6\"/>\n</svg>\n";
26
30
  // ../../tools/ui/src/client.ts
27
31
  function useLocaleSnapshot(locale) {
28
32
  const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
@@ -57,6 +61,37 @@ function SettingsRow({ label, children }) {
57
61
  }), children]
58
62
  });
59
63
  }
64
+ // ../../tools/ui/src/button-picker.tsx
65
+ function ButtonPicker({ label, value, items, onChange, disabled = false, menuClassName, compact, ...buttonProps }) {
66
+ const [open, setOpen] = (0, react.useState)(false);
67
+ (0, react.useEffect)(() => {
68
+ if (disabled) setOpen(false);
69
+ }, [disabled]);
70
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
71
+ open: open && !disabled,
72
+ portal: true,
73
+ compact,
74
+ className: menuClassName,
75
+ selectedId: value,
76
+ items,
77
+ onClose: () => setOpen(false),
78
+ onSelect: (id) => {
79
+ setOpen(false);
80
+ const item = items.find((item) => item.id === id && !item.disabled);
81
+ if (!disabled && item) onChange(item.id);
82
+ },
83
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
84
+ ...buttonProps,
85
+ type: "button",
86
+ "aria-label": label,
87
+ "aria-haspopup": "menu",
88
+ "aria-expanded": open && !disabled,
89
+ disabled,
90
+ onClick: () => setOpen((current) => !current),
91
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: items.find((item) => item.id === value)?.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { "aria-hidden": "true" })]
92
+ })
93
+ });
94
+ }
60
95
  // locale/en.json
61
96
  var en_default = {
62
97
  meta: {
@@ -13235,8 +13270,15 @@ const styles = `
13235
13270
  .dsh-pr-loading{display:inline-flex;flex-shrink:0;width:16px;height:16px;vertical-align:middle;color:var(--dsw-alias-label-secondary)}
13236
13271
  .dsh-pr-loading svg{width:100%;height:100%;animation:dsh-pr-spin .8s linear infinite}
13237
13272
  .dsh-pr-initial-loading{display:flex;justify-content:center;padding:24px 16px}
13273
+ .dsh-vcs-check-status{display:inline-flex;flex:none;width:14px;height:14px;align-items:center;justify-content:center;vertical-align:middle;color:var(--dsw-alias-label-tertiary)}
13274
+ .dsh-vcs-check-status>span{display:contents}
13275
+ .dsh-vcs-check-status svg{width:100%;height:100%}
13276
+ .dsh-vcs-check-status[data-state=done]{color:var(--dsw-alias-state-success-primary)}
13277
+ .dsh-vcs-check-status[data-state=error]{color:var(--dsw-alias-state-error-primary)}
13278
+ .dsh-vcs-check-status:is([data-state=ongoing],[data-state=warning]){color:var(--dsw-alias-state-warn-primary)}
13279
+ .dsh-vcs-check-status[data-state=ongoing] svg{animation:dsh-pr-spin 1.5s linear infinite}
13238
13280
  @keyframes dsh-pr-spin{to{transform:rotate(360deg)}}
13239
- @media(prefers-reduced-motion:reduce){.dsh-pr-loading svg{animation:none}}
13281
+ @media(prefers-reduced-motion:reduce){.dsh-pr-loading svg,.dsh-vcs-check-status[data-state=ongoing] svg{animation:none}}
13240
13282
  .dsh-pr-panel :is(a,textarea,summary,[role=tabpanel]):focus-visible{outline:2px solid var(--dsw-focus-ring-color);outline-offset:-2px}
13241
13283
  .dsh-pr-content{overflow:auto;flex:1;min-height:0}
13242
13284
  .dsh-pr-content>p{padding:20px 16px;color:var(--dsw-alias-label-secondary);line-height:1.6}
@@ -13356,6 +13398,7 @@ function bindPullRequestLinks(root, open) {
13356
13398
  }
13357
13399
  // src/pull-requests-client.tsx
13358
13400
  var pull_requests_client_exports = /* @__PURE__ */ __exportAll({
13401
+ CheckStatusIcon: () => CheckStatusIcon,
13359
13402
  apply: () => apply$2,
13360
13403
  checksState: () => checksState,
13361
13404
  inject: () => inject$1,
@@ -13398,6 +13441,14 @@ function checksState(item) {
13398
13441
  ].includes(state))) return "ongoing";
13399
13442
  return states.every((state) => ["success", "passed"].includes(state)) ? "done" : "idle";
13400
13443
  }
13444
+ function CheckStatusIcon({ state }) {
13445
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13446
+ className: "dsh-vcs-check-status",
13447
+ "data-state": state,
13448
+ "aria-hidden": "true",
13449
+ children: state === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckCircleOutlineRegular, {}) : state === "ongoing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular, {}) : state === "warning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutlineRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { dangerouslySetInnerHTML: { __html: state === "error" ? circle_x_default : circle_minus_default } })
13450
+ });
13451
+ }
13401
13452
  function Loading({ label }) {
13402
13453
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13403
13454
  className: "dsh-pr-loading",
@@ -13430,37 +13481,6 @@ function Description({ text, labels }) {
13430
13481
  variant: "compact"
13431
13482
  }), render(sections)] });
13432
13483
  }
13433
- function Choice({ label, value, items, onChange, disabled }) {
13434
- const [open, setOpen] = (0, react.useState)(false);
13435
- (0, react.useEffect)(() => {
13436
- if (disabled) setOpen(false);
13437
- }, [disabled]);
13438
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
13439
- open,
13440
- portal: true,
13441
- compact: true,
13442
- className: "dsh-pr-choice",
13443
- selectedId: value,
13444
- items,
13445
- onClose: () => setOpen(false),
13446
- onSelect: (id) => {
13447
- const item = items.find((item) => item.id === id && !item.disabled);
13448
- if (item) onChange(item.id);
13449
- setOpen(false);
13450
- },
13451
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
13452
- type: "button",
13453
- size: "sm",
13454
- className: "dsh-pr-picker",
13455
- "aria-label": label,
13456
- "aria-haspopup": "menu",
13457
- "aria-expanded": open,
13458
- disabled,
13459
- onClick: () => setOpen(!open),
13460
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: items.find((item) => item.id === value)?.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { "aria-hidden": "true" })]
13461
- })
13462
- });
13463
- }
13464
13484
  function apply$2(ctx) {
13465
13485
  const connection = ctx.get("connection");
13466
13486
  const sessions = ctx.get("sessions");
@@ -13474,6 +13494,7 @@ function apply$2(ctx) {
13474
13494
  function SessionRequest({ sessionId }) {
13475
13495
  (0, react.useSyncExternalStore)(subscribe, () => ctx.locale.getSnapshot().active);
13476
13496
  const copy = text();
13497
+ const current = (0, react.useSyncExternalStore)((listener) => ctx.sidebarRight.mounted.subscribe(listener), () => ctx.sidebarRight.mounted.getSnapshot() === sessionId);
13477
13498
  const running = (0, react.useSyncExternalStore)((listener) => sessions.list.subscribe(listener), () => sessions.list.getSnapshot().byId[sessionId]?.running ?? false);
13478
13499
  const cwd = (0, react.useSyncExternalStore)((listener) => sessions.list.subscribe(listener), () => sessions.list.getSnapshot().byId[sessionId]?.cwd);
13479
13500
  const anchor = (0, react.useRef)(null);
@@ -13491,12 +13512,21 @@ function apply$2(ctx) {
13491
13512
  return () => node.remove();
13492
13513
  }, [sessionId]);
13493
13514
  (0, react.useEffect)(() => {
13494
- let active;
13495
13515
  setView(void 0);
13516
+ }, [
13517
+ sessionId,
13518
+ current,
13519
+ cwd
13520
+ ]);
13521
+ (0, react.useEffect)(() => {
13522
+ if (!current || running) return;
13523
+ let disposed = false;
13524
+ let active;
13496
13525
  const refresh = async () => {
13497
- active?.abort();
13498
- active = new AbortController();
13499
- const { signal } = active;
13526
+ if (disposed || active || document.visibilityState === "hidden") return;
13527
+ const controller = new AbortController();
13528
+ active = controller;
13529
+ const { signal } = controller;
13500
13530
  try {
13501
13531
  const result = await connection.rpc.call(channel$1, method, {
13502
13532
  sessionId,
@@ -13505,22 +13535,28 @@ function apply$2(ctx) {
13505
13535
  if (!signal.aborted) setView(result.ok ? result.value : void 0);
13506
13536
  } catch {
13507
13537
  if (!signal.aborted) setView(void 0);
13538
+ } finally {
13539
+ if (active === controller) active = void 0;
13508
13540
  }
13509
13541
  };
13510
13542
  refresh();
13511
13543
  window.addEventListener("focus", refresh);
13544
+ document.addEventListener("visibilitychange", refresh);
13512
13545
  const interval = window.setInterval(refresh, 6e4);
13513
13546
  return () => {
13547
+ disposed = true;
13514
13548
  active?.abort();
13515
13549
  window.removeEventListener("focus", refresh);
13550
+ document.removeEventListener("visibilitychange", refresh);
13516
13551
  window.clearInterval(interval);
13517
13552
  };
13518
13553
  }, [
13519
13554
  sessionId,
13555
+ current,
13520
13556
  running,
13521
13557
  cwd
13522
13558
  ]);
13523
- const item = view?.request;
13559
+ const item = current ? view?.request : void 0;
13524
13560
  const state = item?.state === "open" && item.draft ? "draft" : item?.state;
13525
13561
  const icon = state === "merged" ? git_merge_default : state === "closed" ? git_pull_request_closed_default : state === "draft" ? git_pull_request_draft_default : git_pull_request_default;
13526
13562
  const label = item && state ? `${copy.title} ${view?.provider === "gitlab" ? "!" : "#"}${item.number}: ${copy[state]}` : "";
@@ -13894,10 +13930,7 @@ function apply$2(ctx) {
13894
13930
  onClick: () => setChecksOpen(!checksOpen),
13895
13931
  "aria-busy": loading,
13896
13932
  children: [
13897
- loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Loading, { label: copy.loading }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
13898
- state,
13899
- appearance: "step"
13900
- }),
13933
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Loading, { label: copy.loading }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckStatusIcon, { state }),
13901
13934
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.headSha ? checksSummary : copy.checks }),
13902
13935
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { "aria-hidden": "true" })
13903
13936
  ]
@@ -13913,10 +13946,7 @@ function apply$2(ctx) {
13913
13946
  className: "dsh-pr-checks",
13914
13947
  children: item.checks.map((check, index) => {
13915
13948
  const status = checkStatus(check.status);
13916
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
13917
- state: status.state,
13918
- appearance: "step"
13919
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
13949
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckStatusIcon, { state: status.state }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
13920
13950
  className: "dsh-pr-check-name",
13921
13951
  children: [check.url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
13922
13952
  href: check.url,
@@ -14074,7 +14104,11 @@ function apply$2(ctx) {
14074
14104
  })] }),
14075
14105
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14076
14106
  className: "dsh-pr-actions",
14077
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Choice, {
14107
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonPicker, {
14108
+ size: "sm",
14109
+ className: "dsh-pr-picker",
14110
+ menuClassName: "dsh-pr-choice",
14111
+ compact: true,
14078
14112
  label: copy.review,
14079
14113
  value: reviewAction,
14080
14114
  disabled: pending,
@@ -14279,7 +14313,6 @@ const copy$1 = {
14279
14313
  invalid: "이름과 올바른 이메일을 함께 입력하거나 두 항목을 모두 비우세요.",
14280
14314
  cli: "CLI 인증 상태",
14281
14315
  refresh: "새로고침",
14282
- selectSession: "CLI 상태를 확인하려면 세션을 먼저 여세요.",
14283
14316
  authenticated: "인증됨",
14284
14317
  unauthenticated: "인증되지 않음",
14285
14318
  unavailable: "설치되지 않음",
@@ -14303,7 +14336,6 @@ const copy$1 = {
14303
14336
  invalid: "Enter both a name and a valid email, or leave both fields empty.",
14304
14337
  cli: "CLI authentication",
14305
14338
  refresh: "Refresh",
14306
- selectSession: "Open a session to check CLI status.",
14307
14339
  authenticated: "Authenticated",
14308
14340
  unauthenticated: "Not authenticated",
14309
14341
  unavailable: "Not installed",
@@ -14327,7 +14359,6 @@ const copy$1 = {
14327
14359
  invalid: "请输入姓名和有效邮箱,或将两个字段都留空。",
14328
14360
  cli: "CLI 认证状态",
14329
14361
  refresh: "刷新",
14330
- selectSession: "请先打开会话以检查 CLI 状态。",
14331
14362
  authenticated: "已认证",
14332
14363
  unauthenticated: "未认证",
14333
14364
  unavailable: "未安装",
@@ -14343,7 +14374,6 @@ function VcsSettings({ ctx }) {
14343
14374
  const text = localized(active);
14344
14375
  const id = (0, react.useId)();
14345
14376
  const connection = ctx.get("connection");
14346
- const sessionId = (0, react.useSyncExternalStore)((listener) => ctx.sidebarRight.mounted.subscribe(listener), () => ctx.sidebarRight.mounted.getSnapshot());
14347
14377
  const [view, setView] = (0, react.useState)();
14348
14378
  const [draft, setDraft] = (0, react.useState)();
14349
14379
  const [busy, setBusy] = (0, react.useState)(false);
@@ -14378,21 +14408,14 @@ function VcsSettings({ ctx }) {
14378
14408
  const controller = new AbortController();
14379
14409
  lifetime.current = controller;
14380
14410
  load(controller.signal);
14411
+ check(controller.signal);
14381
14412
  return () => controller.abort();
14382
14413
  }, []);
14383
- (0, react.useEffect)(() => {
14384
- const controller = new AbortController();
14385
- setCli(void 0);
14386
- setCliError(false);
14387
- setChecking(false);
14388
- if (sessionId) check(controller.signal);
14389
- return () => controller.abort();
14390
- }, [sessionId]);
14391
14414
  const check = async (signal) => {
14392
14415
  setChecking(true);
14393
14416
  setCliError(false);
14394
14417
  try {
14395
- const result = await connection.rpc.call(channel, "dungsil-vcs/cli-status", { sessionId }, signal);
14418
+ const result = await connection.rpc.call(channel, "dungsil-vcs/cli-status", {}, signal);
14396
14419
  if (signal.aborted) return;
14397
14420
  if (!result.ok) throw new Error("unavailable");
14398
14421
  setCli(result.value);
@@ -14452,7 +14475,6 @@ function VcsSettings({ ctx }) {
14452
14475
  className: "dsh-vcs-settings",
14453
14476
  "aria-label": text.nav,
14454
14477
  children: [
14455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.scope }),
14456
14478
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
14457
14479
  onSubmit: (event) => {
14458
14480
  event.preventDefault();
@@ -14463,6 +14485,7 @@ function VcsSettings({ ctx }) {
14463
14485
  disabled: blocked,
14464
14486
  children: [
14465
14487
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.author }),
14488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.scope }),
14466
14489
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.authorHint }),
14467
14490
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14468
14491
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
@@ -14545,31 +14568,26 @@ function VcsSettings({ ctx }) {
14545
14568
  ]
14546
14569
  }),
14547
14570
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.cli }),
14548
- !sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14571
+ checking && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14549
14572
  role: "status",
14550
- children: text.selectSession
14551
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14552
- checking && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14553
- role: "status",
14554
- children: text.loading
14555
- }),
14556
- cliError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14557
- role: "alert",
14558
- children: text.error
14559
- }),
14560
- cli?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14561
- className: "dsh-vcs-settings-cli",
14562
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14563
- label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.cli }),
14564
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text[item.status === "error" ? "errorStatus" : item.status] })
14565
- }), item.accounts.map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
14566
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: account.host }),
14567
- " · ",
14568
- account.login,
14569
- account.method && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", text[account.method]] })
14570
- ] }, index))]
14571
- }, item.cli))
14572
- ] })
14573
+ children: text.loading
14574
+ }),
14575
+ cliError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14576
+ role: "alert",
14577
+ children: text.error
14578
+ }),
14579
+ cli?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14580
+ className: "dsh-vcs-settings-cli",
14581
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14582
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.cli }),
14583
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text[item.status === "error" ? "errorStatus" : item.status] })
14584
+ }), item.accounts.map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
14585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: account.host }),
14586
+ " · ",
14587
+ account.login,
14588
+ account.method && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", text[account.method]] })
14589
+ ] }, index))]
14590
+ }, item.cli))
14573
14591
  ]
14574
14592
  });
14575
14593
  }
@@ -14823,10 +14841,7 @@ function apply(ctx) {
14823
14841
  className: "dsh-vcs-trigger",
14824
14842
  title: `${labels.checks}: ${ciLabel}`,
14825
14843
  onClick: () => ctx.sidebarRight.openTab("dungsil-vcs-pull-requests", { params: { section: "checks" } }),
14826
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
14827
- state: ciState,
14828
- appearance: "step"
14829
- }), `${labels.checks}: ${ciLabel}`]
14844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckStatusIcon, { state: ciState }), `${labels.checks}: ${ciLabel}`]
14830
14845
  })]
14831
14846
  });
14832
14847
  }
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
3
- import { SessionId } from "@deepseek-ai/dsh-session";
3
+ import { Session, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Buffer as Buffer$1 } from "node:buffer";
5
5
  import { Context, Service } from "@deepseek-ai/cordis";
6
6
  import { createHash } from "node:crypto";
7
+ import { homedir } from "node:os";
7
8
  // \0rolldown/runtime.js
8
9
  var __defProp = Object.defineProperty;
9
10
  var __exportAll = (all, no_symbols) => {
@@ -900,6 +901,69 @@ defineMethod("transform", [
900
901
  "callback",
901
902
  "preserve"
902
903
  ], ({ inner }, isInner) => inner.toString(isInner));
904
+ // src/inspect-session.ts
905
+ async function inspectSession(ctx, id, signal) {
906
+ signal.throwIfAborted();
907
+ const live = ctx.get("agents")?.get(id);
908
+ if (live) return live.session;
909
+ const inspection = await ctx.sessionController.inspect(id, signal);
910
+ signal.throwIfAborted();
911
+ return Session.create(id, inspection.events, inspection.meta, inspection.inheritedEventCount);
912
+ }
913
+ // ../../tools/async/src/index.ts
914
+ function createTaskQueue({ concurrency, maxPending }) {
915
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || !Number.isSafeInteger(maxPending) || maxPending < 0) throw new RangeError("Invalid task queue limits");
916
+ let active = 0;
917
+ const pending = /* @__PURE__ */ new Set();
918
+ const drain = () => {
919
+ while (active < concurrency && pending.size) pending.values().next().value.start();
920
+ };
921
+ return (signal, task) => {
922
+ if (signal.aborted) return Promise.reject(signal.reason);
923
+ if (active >= concurrency && pending.size >= maxPending) return Promise.reject(/* @__PURE__ */ new Error("busy"));
924
+ return new Promise((resolve, reject) => {
925
+ const entry = {
926
+ start() {
927
+ pending.delete(entry);
928
+ signal.removeEventListener("abort", entry.cancel);
929
+ active++;
930
+ (async () => {
931
+ try {
932
+ signal.throwIfAborted();
933
+ resolve(await task());
934
+ } catch (error) {
935
+ reject(error);
936
+ } finally {
937
+ active--;
938
+ drain();
939
+ }
940
+ })();
941
+ },
942
+ cancel() {
943
+ pending.delete(entry);
944
+ signal.removeEventListener("abort", entry.cancel);
945
+ reject(signal.reason);
946
+ }
947
+ };
948
+ pending.add(entry);
949
+ signal.addEventListener("abort", entry.cancel, { once: true });
950
+ drain();
951
+ });
952
+ };
953
+ }
954
+ // src/read-queue.ts
955
+ const queues = /* @__PURE__ */ new WeakMap();
956
+ function readQueue(provider) {
957
+ let queue = queues.get(provider);
958
+ if (!queue) {
959
+ queue = createTaskQueue({
960
+ concurrency: 2,
961
+ maxPending: 128
962
+ });
963
+ queues.set(provider, queue);
964
+ }
965
+ return queue;
966
+ }
903
967
  // src/pull-requests-protocol.ts
904
968
  const channel$1 = "/api";
905
969
  const method = "dungsil-vcs/pull-requests/list";
@@ -975,9 +1039,10 @@ function actionInput$1(value) {
975
1039
  function apply$3(ctx, config) {
976
1040
  const lifetime = new AbortController();
977
1041
  const actionsInFlight = /* @__PURE__ */ new Set();
1042
+ const read = readQueue(ctx.subprocess);
978
1043
  ctx.effect(() => () => lifetime.abort());
979
- const list = async (agent, signal, action, requested, summary = false) => {
980
- const cwd = ctx.workingDirectory.get(agent.session);
1044
+ const list = async (session, signal, action, requested, summary = false) => {
1045
+ const cwd = ctx.workingDirectory.get(session);
981
1046
  const run = async (executable, args, failure = "command_failed", allowed = [0], trim = true) => {
982
1047
  let resolved;
983
1048
  try {
@@ -986,7 +1051,7 @@ function apply$3(ctx, config) {
986
1051
  throw new Error(executable === "git" ? "repository" : "cli_missing");
987
1052
  }
988
1053
  const argv = [resolved, ...args];
989
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
1054
+ const policy = ctx.sandboxPolicy.resolve({ session });
990
1055
  const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
991
1056
  ...policy,
992
1057
  mode: policy.mode
@@ -1119,7 +1184,7 @@ function apply$3(ctx, config) {
1119
1184
  if (requested.origin !== new URL(origin).origin || provider === "github" && repository.split("/").length !== 2 || !match || !Number.isSafeInteger(Number(match[2]))) throw new Error("unsupported");
1120
1185
  if (provider === "github" ? match[1].toLowerCase() !== prefix.toLowerCase() : match[1] !== prefix) {
1121
1186
  if (provider === "gitlab") throw new Error("unsupported");
1122
- const associated = await list(agent, signal, void 0, void 0, summary);
1187
+ const associated = await list(session, signal, void 0, void 0, summary);
1123
1188
  const linkedUrl = `${requested.origin}${match[1]}/${provider === "github" ? "pull" : forge ? "pulls" : "-/merge_requests"}/${match[2]}`;
1124
1189
  if ("request" in associated && associated.request && (provider === "github" ? associated.request.url.toLowerCase() === linkedUrl.toLowerCase() : associated.request.url === linkedUrl)) return associated;
1125
1190
  throw new Error("unsupported");
@@ -1714,14 +1779,24 @@ function apply$3(ctx, config) {
1714
1779
  inFlight = action.sessionId;
1715
1780
  }
1716
1781
  const requested = route === "dungsil-vcs/pull-requests/list" && input.requestUrl !== void 0 ? lookupUrl(input.requestUrl) : void 0;
1717
- const resolved = await ctx.sessionController.resolveAgent(SessionId(input.sessionId));
1718
- if ("error" in resolved) throw new Error("unavailable");
1719
1782
  const signal = AbortSignal.any([
1720
1783
  request.signal,
1721
1784
  lifetime.signal,
1722
1785
  AbortSignal.timeout(2e4)
1723
1786
  ]);
1724
- const value = await list(resolved.agent, signal, action, requested, input.summary === true);
1787
+ const sessionId = SessionId(input.sessionId);
1788
+ const execute = async () => {
1789
+ signal.throwIfAborted();
1790
+ let session;
1791
+ if (action) {
1792
+ const resolved = await ctx.sessionController.resolveAgent(sessionId);
1793
+ if ("error" in resolved) throw new Error("unavailable");
1794
+ signal.throwIfAborted();
1795
+ session = resolved.agent.session;
1796
+ } else session = await inspectSession(ctx, sessionId, signal);
1797
+ return list(session, signal, action, requested, input.summary === true);
1798
+ };
1799
+ const value = action ? await execute() : await read(signal, execute);
1725
1800
  if (action && !("completed" in value)) throw new Error("stale_request");
1726
1801
  return reply({
1727
1802
  ok: true,
@@ -5062,8 +5137,6 @@ const name$1 = "dungsil-vcs-settings";
5062
5137
  const inject$1 = [
5063
5138
  "connection",
5064
5139
  "configEditor",
5065
- "sessionController",
5066
- "workingDirectory",
5067
5140
  "subprocess",
5068
5141
  "sandbox",
5069
5142
  "sandboxPolicy"
@@ -5143,12 +5216,6 @@ function apply$1(ctx) {
5143
5216
  value: snapshot()
5144
5217
  };
5145
5218
  } else {
5146
- const sessionId = body.payload?.sessionId;
5147
- if (typeof sessionId !== "string" || !sessionId || sessionId.length > 256) throw new Error("invalid");
5148
- const resolved = await ctx.sessionController.resolveAgent(SessionId(sessionId));
5149
- if ("error" in resolved) throw new Error("unavailable");
5150
- const { agent } = resolved;
5151
- const cwd = await ctx.workingDirectory.ensure(agent, signal);
5152
5219
  const value = await inspectCliStatus(async (executable, args) => {
5153
5220
  const bounded = AbortSignal.any([signal, AbortSignal.timeout(1e4)]);
5154
5221
  let path;
@@ -5159,14 +5226,14 @@ function apply$1(ctx) {
5159
5226
  throw error;
5160
5227
  }
5161
5228
  const argv = [path, ...args];
5162
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
5229
+ const policy = ctx.sandboxPolicy.resolve();
5163
5230
  const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5164
5231
  ...policy,
5165
5232
  mode: policy.mode
5166
5233
  }, bounded)).argv;
5167
5234
  const child = ctx.subprocess.spawn({
5168
5235
  argv: command,
5169
- cwd,
5236
+ cwd: homedir(),
5170
5237
  signal: bounded,
5171
5238
  graceMs: 1e3,
5172
5239
  env: {
@@ -5205,7 +5272,6 @@ function apply$1(ctx) {
5205
5272
  }
5206
5273
  });
5207
5274
  signal.throwIfAborted();
5208
- if (ctx.workingDirectory.get(agent.session) !== cwd) throw new Error("conflict");
5209
5275
  result = {
5210
5276
  ok: true,
5211
5277
  value
@@ -5241,6 +5307,9 @@ const inject = [
5241
5307
  "workingDirectory"
5242
5308
  ];
5243
5309
  function apply(ctx, config) {
5310
+ const lifetime = new AbortController();
5311
+ const read = readQueue(ctx.subprocess);
5312
+ ctx.effect(() => () => lifetime.abort(), "ui-vcs: cancel context reads");
5244
5313
  ctx.plugin(pull_requests_exports, config);
5245
5314
  ctx.plugin(commands_exports, config);
5246
5315
  ctx.plugin(settings_exports);
@@ -5265,100 +5334,104 @@ function apply(ctx, config) {
5265
5334
  if (body.method !== "dungsil-vcs/describe") throw new Error("Invalid method");
5266
5335
  const sessionId = body.payload?.sessionId;
5267
5336
  if (typeof sessionId !== "string" || !sessionId) throw new Error("Invalid session");
5268
- const resolved = await ctx.sessionController.resolveAgent(SessionId(sessionId));
5269
- if ("error" in resolved) throw new Error("Session unavailable");
5270
- const { agent } = resolved;
5271
- const signal = AbortSignal.any([request.signal, AbortSignal.timeout(1e4)]);
5272
- const cwd = await ctx.workingDirectory.ensure(agent, signal);
5273
- const git = async (args) => {
5274
- const argv = [
5275
- await ctx.subprocess.resolveExecutable("git", void 0, signal),
5276
- "-c",
5277
- "core.fsmonitor=false",
5278
- "-c",
5279
- "core.hooksPath=/dev/null",
5280
- ...args
5281
- ];
5282
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
5283
- const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5284
- ...policy,
5285
- mode: policy.mode
5286
- }, signal)).argv;
5287
- const child = ctx.subprocess.spawn({
5288
- argv: command,
5289
- cwd,
5290
- signal,
5291
- graceMs: 1e3,
5292
- env: {
5293
- ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
5294
- GIT_TERMINAL_PROMPT: "0",
5295
- GIT_OPTIONAL_LOCKS: "0",
5296
- GIT_NO_LAZY_FETCH: "1"
5297
- },
5298
- stdio: {
5299
- stdin: "ignore",
5300
- stdout: { maxBytes: 65536 },
5301
- stderr: { maxBytes: 4096 }
5302
- }
5303
- });
5304
- try {
5305
- const outcome = await child.done;
5306
- signal.throwIfAborted();
5307
- const output = child.collected.stdout.readFrom(0);
5308
- if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
5309
- return output.text.replace(/\r?\n$/, "");
5310
- } finally {
5311
- child.terminate();
5312
- await child.waitForExit();
5313
- }
5314
- };
5315
- const gitDir = await git([
5316
- "rev-parse",
5317
- "--path-format=absolute",
5318
- "--git-dir"
5319
- ]);
5320
- const commonDir = await git([
5321
- "rev-parse",
5322
- "--path-format=absolute",
5323
- "--git-common-dir"
5337
+ const signal = AbortSignal.any([
5338
+ request.signal,
5339
+ lifetime.signal,
5340
+ AbortSignal.timeout(1e4)
5324
5341
  ]);
5325
- let operation = "pr";
5326
- if (gitDir === commonDir) {
5327
- const branch = await git([
5328
- "symbolic-ref",
5329
- "--quiet",
5330
- "--short",
5331
- "HEAD"
5342
+ result = await read(signal, async () => {
5343
+ const session = await inspectSession(ctx, SessionId(sessionId), signal);
5344
+ const cwd = ctx.workingDirectory.get(session);
5345
+ const git = async (args) => {
5346
+ const argv = [
5347
+ await ctx.subprocess.resolveExecutable("git", void 0, signal),
5348
+ "-c",
5349
+ "core.fsmonitor=false",
5350
+ "-c",
5351
+ "core.hooksPath=/dev/null",
5352
+ ...args
5353
+ ];
5354
+ const policy = ctx.sandboxPolicy.resolve({ session });
5355
+ const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5356
+ ...policy,
5357
+ mode: policy.mode
5358
+ }, signal)).argv;
5359
+ const child = ctx.subprocess.spawn({
5360
+ argv: command,
5361
+ cwd,
5362
+ signal,
5363
+ graceMs: 1e3,
5364
+ env: {
5365
+ ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
5366
+ GIT_TERMINAL_PROMPT: "0",
5367
+ GIT_OPTIONAL_LOCKS: "0",
5368
+ GIT_NO_LAZY_FETCH: "1"
5369
+ },
5370
+ stdio: {
5371
+ stdin: "ignore",
5372
+ stdout: { maxBytes: 65536 },
5373
+ stderr: { maxBytes: 4096 }
5374
+ }
5375
+ });
5376
+ try {
5377
+ const outcome = await child.done;
5378
+ signal.throwIfAborted();
5379
+ const output = child.collected.stdout.readFrom(0);
5380
+ if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
5381
+ return output.text.replace(/\r?\n$/, "");
5382
+ } finally {
5383
+ child.terminate();
5384
+ await child.waitForExit();
5385
+ }
5386
+ };
5387
+ const gitDir = await git([
5388
+ "rev-parse",
5389
+ "--path-format=absolute",
5390
+ "--git-dir"
5332
5391
  ]);
5333
- const upstream = await git([
5334
- "for-each-ref",
5335
- "--format=%(upstream:remotename)",
5336
- `refs/heads/${branch}`
5392
+ const commonDir = await git([
5393
+ "rev-parse",
5394
+ "--path-format=absolute",
5395
+ "--git-common-dir"
5337
5396
  ]);
5338
- const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5339
- const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5340
- if (!remote) throw new Error("Default remote unknown");
5341
- operation = await git([
5342
- "symbolic-ref",
5343
- "--quiet",
5344
- `refs/remotes/${remote}/HEAD`
5345
- ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5346
- }
5347
- const headSha = await git(["rev-parse", "HEAD"]);
5348
- const dirty = !!await git([
5349
- "status",
5350
- "--porcelain",
5351
- "--untracked-files=normal"
5352
- ]);
5353
- result = {
5354
- ok: true,
5355
- value: {
5356
- operation,
5357
- headSha,
5358
- dirty
5397
+ let operation = "pr";
5398
+ if (gitDir === commonDir) {
5399
+ const branch = await git([
5400
+ "symbolic-ref",
5401
+ "--quiet",
5402
+ "--short",
5403
+ "HEAD"
5404
+ ]);
5405
+ const upstream = await git([
5406
+ "for-each-ref",
5407
+ "--format=%(upstream:remotename)",
5408
+ `refs/heads/${branch}`
5409
+ ]);
5410
+ const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5411
+ const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5412
+ if (!remote) throw new Error("Default remote unknown");
5413
+ operation = await git([
5414
+ "symbolic-ref",
5415
+ "--quiet",
5416
+ `refs/remotes/${remote}/HEAD`
5417
+ ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5359
5418
  }
5360
- };
5361
- if (ctx.workingDirectory.get(agent.session) !== cwd) throw new Error("Working directory changed");
5419
+ const headSha = await git(["rev-parse", "HEAD"]);
5420
+ const dirty = !!await git([
5421
+ "status",
5422
+ "--porcelain",
5423
+ "--untracked-files=normal"
5424
+ ]);
5425
+ if (ctx.workingDirectory.get(session) !== cwd) throw new Error("Working directory changed");
5426
+ return {
5427
+ ok: true,
5428
+ value: {
5429
+ operation,
5430
+ headSha,
5431
+ dirty
5432
+ }
5433
+ };
5434
+ });
5362
5435
  } catch {
5363
5436
  result = failure;
5364
5437
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-vcs",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Git actions and PR/MR review for GitHub, GitLab, Gitea and Forgejo.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,7 @@
52
52
  "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.5",
53
53
  "@deepseek-ai/dsh-working-directory": "0.2.1-rc.5",
54
54
  "@deepseek-ai/schemastery": "~3.18.5",
55
+ "@dungsil/dsh-async": "0.0.0",
55
56
  "@dungsil/dsh-tsconfig": "0.0.0",
56
57
  "@dungsil/dsh-ui": "0.0.0",
57
58
  "@types/react": "18.3.31",