dshmarket 1.73.4 → 1.73.5

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/client/client.js CHANGED
@@ -384,6 +384,16 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
384
384
  agentQueueStaleGone: "排队时它还在已安装列表里,现在已不在,因此跳过(没有卸载)。",
385
385
  agentQueueStaleNoUpdate: "排队时它有可用更新,现在没有了,已跳过。",
386
386
  queuedBadge: "排队中",
387
+ agentDrainBadge: "等待 Agent",
388
+ agentDrainWaiting: "等待 {0} 个 Agent 到达安全点…",
389
+ agentDrainShowAgents: "查看正在运行的会话",
390
+ agentDrainVerifying: "正在验证结果…",
391
+ agentDrainCancelled: "已取消。",
392
+ agentDrainRestarted: "宿主在这次操作完成前重启了,它已取消。需要的话请重新发起。",
393
+ agentDrainHistoryDone: "上次排队的操作已完成。",
394
+ agentDrainHistoryFailed: "上次排队的操作失败了({0})。请检查「已安装」或导出的日志。",
395
+ agentDrainHistoryFailedShort: "上次排队的操作失败了。请检查「已安装」或导出的日志。",
396
+ agentDrainHistoryCancelled: "上次排队的操作已取消。",
387
397
  favoriteFailed: "收藏失败,请稍后重试",
388
398
  favoriteUnavailable: "收藏功能暂不可用。请刷新页面或重启 dsh web。",
389
399
  favoriteBusy: "插件操作未结束,收藏未保存,请稍后重试",
@@ -1158,6 +1168,16 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1158
1168
  agentQueueStaleGone: "It was installed when you queued this, and is not now — skipped, nothing was uninstalled.",
1159
1169
  agentQueueStaleNoUpdate: "It had an update available when you queued this, and does not now — skipped.",
1160
1170
  queuedBadge: "Queued",
1171
+ agentDrainBadge: "Waiting for Agents",
1172
+ agentDrainWaiting: "Waiting for {0} Agent(s) to reach a safe point…",
1173
+ agentDrainShowAgents: "Show running sessions",
1174
+ agentDrainVerifying: "Verifying the result…",
1175
+ agentDrainCancelled: "Cancelled.",
1176
+ agentDrainRestarted: "The host restarted before this finished, so the task was cancelled. Start it again if you still need it.",
1177
+ agentDrainHistoryDone: "The last deferred operation completed.",
1178
+ agentDrainHistoryFailed: "The last deferred operation failed ({0}). Check Installed or the exported log.",
1179
+ agentDrainHistoryFailedShort: "The last deferred operation failed. Check Installed or the exported log.",
1180
+ agentDrainHistoryCancelled: "The last deferred operation was cancelled.",
1161
1181
  favoriteFailed: "Could not update favorites. Try again.",
1162
1182
  favoriteUnavailable: "Favorites are unavailable. Refresh the page or restart dsh web.",
1163
1183
  favoriteBusy: "A plugin operation is still running. Favorites were not saved — try again shortly.",
@@ -1932,6 +1952,16 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1932
1952
  agentQueueStaleGone: "キューに入れた時点ではインストール済みでしたが、現在は存在しないためスキップしました(アンインストールはしていません)。",
1933
1953
  agentQueueStaleNoUpdate: "キューに入れた時点では利用できる更新がありましたが、現在はないためスキップしました。",
1934
1954
  queuedBadge: "キュー待ち",
1955
+ agentDrainBadge: "Agent 待ち",
1956
+ agentDrainWaiting: "{0} 件の Agent が安全な地点に到達するのを待っています…",
1957
+ agentDrainShowAgents: "実行中のセッションを表示",
1958
+ agentDrainVerifying: "結果を確認しています…",
1959
+ agentDrainCancelled: "キャンセルしました。",
1960
+ agentDrainRestarted: "この操作が終わる前にホストが再起動したため、キャンセルされました。必要ならもう一度実行してください。",
1961
+ agentDrainHistoryDone: "前回の保留操作は完了しました。",
1962
+ agentDrainHistoryFailed: "前回の保留操作は失敗しました({0})。「インストール済み」またはエクスポートしたログを確認してください。",
1963
+ agentDrainHistoryFailedShort: "前回の保留操作は失敗しました。「インストール済み」またはエクスポートしたログを確認してください。",
1964
+ agentDrainHistoryCancelled: "前回の保留操作はキャンセルされました。",
1935
1965
  favoriteFailed: "お気に入りを更新できませんでした。しばらくしてから再試行してください",
1936
1966
  favoriteUnavailable: "お気に入り機能は現在使えません。ページを更新するか、dsh web を再起動してください。",
1937
1967
  favoriteBusy: "プラグインの操作が終わっていないため、お気に入りは保存されませんでした。しばらくしてから再試行してください",
@@ -5006,7 +5036,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5006
5036
  * that wrote it can still be waiting for that reload.
5007
5037
  */
5008
5038
  function serializeSettled(list) {
5009
- return list.filter(isSettled).slice(-20).map((record) => ({
5039
+ return list.filter(isSettled).filter((record) => record.hostOperationId === void 0).slice(-20).map((record) => ({
5010
5040
  id: record.id,
5011
5041
  kind: record.kind,
5012
5042
  name: record.name,
@@ -5056,6 +5086,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5056
5086
  function restoreSettledRecord(value) {
5057
5087
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
5058
5088
  const row = value;
5089
+ if (asText(row.hostOperationId) !== void 0) return null;
5059
5090
  const id = asText(row.id);
5060
5091
  const name = asText(row.name);
5061
5092
  const kind = row.kind === "install" || row.kind === "update" || row.kind === "uninstall" ? row.kind : void 0;
@@ -5284,6 +5315,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5284
5315
  switch (record.state) {
5285
5316
  case "queued": {
5286
5317
  const blockers = record.blockedBy?.length ?? 0;
5318
+ if (record.hostOperationId !== void 0) {
5319
+ const elapsed = record.waitSince === void 0 ? null : Math.max(0, Math.floor((Date.now() - record.waitSince) / 1e3));
5320
+ const base = blockers > 0 ? t("agentDrainWaiting").replace("{0}", String(blockers)) : t("agentBusyQueued");
5321
+ return elapsed === null ? base : `${base} (${String(elapsed)}s)`;
5322
+ }
5287
5323
  if (blockers > 0) return `${t("opQueued")} · ${t("opQueuedWaitingAgents").replace("{0}", String(blockers))}`;
5288
5324
  return ahead === null || ahead === 0 ? t("opQueued") : `${t("opQueued")} · ${t("opQueuedAhead")} ${String(ahead)}`;
5289
5325
  }
@@ -5300,6 +5336,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5300
5336
  const wrapRef = (0, react.useRef)(null);
5301
5337
  const summary = summarize(records);
5302
5338
  const busy = summary.running + summary.queued > 0;
5339
+ const waitingOnHost = records.some((record) => record.hostOperationId !== void 0 && record.state === "queued" && record.waitSince !== void 0);
5340
+ const [, setClock] = (0, react.useState)(0);
5341
+ (0, react.useEffect)(() => {
5342
+ if (!waitingOnHost) return void 0;
5343
+ const timer = setInterval(() => setClock((value) => value + 1), 1e3);
5344
+ return () => clearInterval(timer);
5345
+ }, [waitingOnHost]);
5303
5346
  (0, react.useEffect)(() => {
5304
5347
  if (!open) return void 0;
5305
5348
  const onKey = (event) => {
@@ -5429,6 +5472,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5429
5472
  className: bucketOf(record.state) === "attention" ? `${Market_module_css_default.opStatus} ${Market_module_css_default.opStatusBad}` : Market_module_css_default.opStatus,
5430
5473
  children: statusLine(t, props.lang, record, ahead)
5431
5474
  }),
5475
+ record.hostOperationId !== void 0 && record.state === "queued" && (record.blockedBy?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("agentDrainShowAgents") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: record.blockedBy.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: id }, id)) })] }),
5432
5476
  needsUser(record) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConflictChoice, {
5433
5477
  t,
5434
5478
  record,
@@ -5448,7 +5492,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5448
5492
  onClick: () => props.onCancel(record),
5449
5493
  children: t("cancelOp")
5450
5494
  }),
5451
- record.state === "queued" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.onRunNow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5495
+ record.state === "queued" && (record.hostOperationId !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5496
+ variant: "outline",
5497
+ size: "sm",
5498
+ onClick: () => props.onCancel(record),
5499
+ children: t("cancelOp")
5500
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.onRunNow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5452
5501
  variant: "primary",
5453
5502
  size: "sm",
5454
5503
  onClick: () => props.onRunNow?.(record),
@@ -5458,7 +5507,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5458
5507
  size: "sm",
5459
5508
  onClick: () => props.onDismiss(record),
5460
5509
  children: t("opDequeue")
5461
- })] }),
5510
+ })] })),
5462
5511
  record.state === "done" && record.needsRefresh === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5463
5512
  variant: "primary",
5464
5513
  size: "sm",
@@ -9597,6 +9646,17 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9597
9646
  return names.length === 0 ? void 0 : names;
9598
9647
  }
9599
9648
  /**
9649
+ * Drop a Host-projected row before the page's own record adopts the same
9650
+ * operation id (#411).
9651
+ *
9652
+ * The status poll can create `host-<id>` while a slow probe is still running;
9653
+ * without this the 202 would leave both rows pointing at one Host operation,
9654
+ * and the panel's first match would keep the synthetic one forever.
9655
+ */
9656
+ function withoutHostProjection(list, operationId, keepId) {
9657
+ return list.filter((record) => record.id === keepId || record.id !== `host-${operationId}` && record.hostOperationId !== operationId);
9658
+ }
9659
+ /**
9600
9660
  * Whether the "compatible with this host" filter starts on, and remembers the
9601
9661
  * answer once the user disagrees with the default (#872).
9602
9662
  *
@@ -9913,7 +9973,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9913
9973
  (0, react.useEffect)(() => {
9914
9974
  if (!queueRestoredRef.current) return;
9915
9975
  try {
9916
- const queued = records.filter((record) => record.state === "queued").map((record) => ({
9976
+ const queued = records.filter((record) => record.state === "queued" && record.hostOperationId === void 0).map((record) => ({
9917
9977
  kind: record.kind,
9918
9978
  name: record.name,
9919
9979
  ...record.url === void 0 ? {} : { url: record.url },
@@ -10438,7 +10498,9 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10438
10498
  */
10439
10499
  const statusRef = (0, react.useRef)({
10440
10500
  busy: false,
10441
- runningAgents: []
10501
+ runningAgents: [],
10502
+ agentDrain: false,
10503
+ boot: null
10442
10504
  });
10443
10505
  /** True while the drain is executing one queued request (no parallel starts). */
10444
10506
  const drainingRef = (0, react.useRef)(false);
@@ -10493,6 +10555,221 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10493
10555
  setSkins,
10494
10556
  setUpdates
10495
10557
  ]);
10558
+ /**
10559
+ * Host-owned deferred operations (#411).
10560
+ *
10561
+ * These rows are projections of `/status`, never entries in the client
10562
+ * queue: the Host owns the maintenance holds, and re-running a row here
10563
+ * would start a second mutation outside that barrier. `dshm-drain-v1`
10564
+ * remembers only which operations this page asked for, so a Host restart can
10565
+ * say "that one was cancelled" instead of leaving the user to guess.
10566
+ */
10567
+ const settledHostRef = (0, react.useRef)(/* @__PURE__ */ new Set());
10568
+ const knownHostIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
10569
+ /** Host operations the user cleared; a status poll must not add them back. */
10570
+ const dismissedHostRef = (0, react.useRef)(/* @__PURE__ */ new Set());
10571
+ const drainMarkerRestoredRef = (0, react.useRef)(false);
10572
+ const clearDrainMarker = (0, react.useCallback)((operationId) => {
10573
+ try {
10574
+ const raw = localStorage.getItem("dshm-drain-v1");
10575
+ if (raw === null) return;
10576
+ const parsed = JSON.parse(raw);
10577
+ if (!Array.isArray(parsed)) return;
10578
+ const kept = parsed.filter((entry) => entry?.id !== operationId);
10579
+ if (kept.length === 0) localStorage.removeItem("dshm-drain-v1");
10580
+ else localStorage.setItem("dshm-drain-v1", JSON.stringify(kept));
10581
+ } catch {}
10582
+ }, []);
10583
+ const rememberDrainMarker = (0, react.useCallback)((operation) => {
10584
+ try {
10585
+ const raw = localStorage.getItem("dshm-drain-v1");
10586
+ const parsed = raw === null ? [] : JSON.parse(raw);
10587
+ const list = Array.isArray(parsed) ? parsed : [];
10588
+ if (list.some((entry) => entry?.id === operation.operationId)) return;
10589
+ list.push({
10590
+ id: operation.operationId,
10591
+ kind: operation.kind,
10592
+ name: operation.name,
10593
+ ...operation.url === void 0 ? {} : { url: operation.url }
10594
+ });
10595
+ localStorage.setItem("dshm-drain-v1", JSON.stringify(list));
10596
+ } catch {}
10597
+ }, []);
10598
+ /** Mark operations this page started in an earlier boot as cancelled by restart. */
10599
+ const restoreDrainMarkers = (0, react.useCallback)((status) => {
10600
+ if (drainMarkerRestoredRef.current) return;
10601
+ const boot = typeof status.boot === "string" ? status.boot : null;
10602
+ if (boot === null) return;
10603
+ drainMarkerRestoredRef.current = true;
10604
+ let saved = null;
10605
+ try {
10606
+ saved = JSON.parse(localStorage.getItem("dshm-drain-v1") ?? "null");
10607
+ } catch {
10608
+ saved = null;
10609
+ }
10610
+ if (!Array.isArray(saved) || saved.length === 0) return;
10611
+ const entries = saved.filter((entry) => {
10612
+ if (entry === null || typeof entry !== "object") return false;
10613
+ return typeof entry.id === "string";
10614
+ });
10615
+ const stale = entries.filter((entry) => !entry.id.startsWith(`${boot}-`));
10616
+ const current = entries.filter((entry) => entry.id.startsWith(`${boot}-`));
10617
+ if (stale.length > 0) {
10618
+ setRecords((previous) => {
10619
+ let next = previous;
10620
+ for (const entry of stale) {
10621
+ if (dismissedHostRef.current.has(entry.id)) continue;
10622
+ if (next.some((record) => record.hostOperationId === entry.id || record.id === `host-${entry.id}`)) continue;
10623
+ const kind = entry.kind === "install" || entry.kind === "update" || entry.kind === "uninstall" ? entry.kind : "update";
10624
+ next = enqueue(next, {
10625
+ id: `host-${entry.id}`,
10626
+ kind,
10627
+ name: typeof entry.name === "string" ? entry.name : "plugin",
10628
+ ...typeof entry.url === "string" ? { url: entry.url } : {},
10629
+ hostOperationId: entry.id,
10630
+ state: "warned",
10631
+ reason: t("agentDrainRestarted")
10632
+ });
10633
+ }
10634
+ return next;
10635
+ });
10636
+ setOperationsOpen(true);
10637
+ }
10638
+ try {
10639
+ if (current.length > 0) localStorage.setItem("dshm-drain-v1", JSON.stringify(current));
10640
+ else localStorage.removeItem("dshm-drain-v1");
10641
+ } catch {}
10642
+ }, [t]);
10643
+ /** Project Host-owned operations into the Tasks panel. */
10644
+ const applyDrainStatus = (0, react.useCallback)((status) => {
10645
+ const operations = Array.isArray(status.drainOperations) ? status.drainOperations : [];
10646
+ const history = Array.isArray(status.drainHistory) ? status.drainHistory : [];
10647
+ if (operations.length === 0 && history.length === 0) return;
10648
+ let opened = false;
10649
+ for (const operation of operations) if (operation.state === "accepted" || operation.state === "draining" || operation.state === "queued-for-mutation" || operation.state === "running" || operation.state === "verifying") {
10650
+ if (!knownHostIdsRef.current.has(operation.operationId)) {
10651
+ knownHostIdsRef.current.add(operation.operationId);
10652
+ opened = true;
10653
+ }
10654
+ }
10655
+ for (const operation of operations) {
10656
+ if (dismissedHostRef.current.has(operation.operationId)) continue;
10657
+ if (operation.state !== "succeeded" && operation.state !== "failed" && operation.state !== "cancelled") continue;
10658
+ clearDrainMarker(operation.operationId);
10659
+ if (settledHostRef.current.has(operation.operationId)) continue;
10660
+ settledHostRef.current.add(operation.operationId);
10661
+ if (operation.state === "succeeded") {
10662
+ refreshInstalled();
10663
+ if (operation.outcome?.restartRequired === true) setRestartNames((names) => names.includes(operation.name) ? names : names.concat(operation.name));
10664
+ if (operation.kind === "update") setUpdatedNames((names) => names.includes(operation.name) ? names : names.concat(operation.name));
10665
+ if (operation.kind === "install" && operation.url !== void 0) setDoneUrls((urls) => urls.includes(operation.url) ? urls : urls.concat(operation.url));
10666
+ } else refreshInstalled();
10667
+ }
10668
+ const hostRecord = (operation) => {
10669
+ const base = {
10670
+ id: `host-${operation.operationId}`,
10671
+ kind: operation.kind,
10672
+ name: operation.name,
10673
+ ...operation.url === void 0 ? {} : { url: operation.url },
10674
+ hostOperationId: operation.operationId
10675
+ };
10676
+ if (operation.state === "accepted" || operation.state === "draining" || operation.state === "queued-for-mutation") return {
10677
+ ...base,
10678
+ state: "queued",
10679
+ ...operation.wait === null ? {} : {
10680
+ blockedBy: operation.wait.activeAgentIds,
10681
+ waitSince: operation.wait.since
10682
+ },
10683
+ reason: operation.wait === null ? t("agentBusyQueued") : t("agentDrainWaiting").replace("{0}", String(operation.wait.activeCount))
10684
+ };
10685
+ if (operation.state === "running") return {
10686
+ ...base,
10687
+ state: "running",
10688
+ blockedBy: void 0,
10689
+ waitSince: void 0,
10690
+ reason: void 0
10691
+ };
10692
+ if (operation.state === "verifying") return {
10693
+ ...base,
10694
+ state: "running",
10695
+ detail: t("agentDrainVerifying"),
10696
+ blockedBy: void 0,
10697
+ waitSince: void 0,
10698
+ reason: void 0
10699
+ };
10700
+ if (operation.state === "succeeded") return {
10701
+ ...base,
10702
+ state: "done",
10703
+ needsRefresh: operation.outcome?.needsRefresh === true,
10704
+ blockedBy: void 0,
10705
+ waitSince: void 0,
10706
+ reason: void 0
10707
+ };
10708
+ if (operation.state === "cancelled") return {
10709
+ ...base,
10710
+ state: "warned",
10711
+ reason: t("agentDrainCancelled"),
10712
+ blockedBy: void 0,
10713
+ waitSince: void 0
10714
+ };
10715
+ return {
10716
+ ...base,
10717
+ state: "failed",
10718
+ reason: operation.failure?.message ?? t("installFail"),
10719
+ blockedBy: void 0,
10720
+ waitSince: void 0
10721
+ };
10722
+ };
10723
+ setRecords((previous) => {
10724
+ let next = previous;
10725
+ for (const operation of operations) {
10726
+ if (dismissedHostRef.current.has(operation.operationId)) continue;
10727
+ const existing = next.find((record) => record.hostOperationId === operation.operationId);
10728
+ const desired = hostRecord(operation);
10729
+ if (existing === void 0) next = enqueue(next, desired);
10730
+ else {
10731
+ const { id: _id, kind: _kind, name: _name, hostOperationId: _host, ...changes } = desired;
10732
+ next = patch(next, existing.id, changes);
10733
+ }
10734
+ }
10735
+ for (const entry of history) {
10736
+ if (dismissedHostRef.current.has(entry.id)) continue;
10737
+ const id = `host-${entry.id}`;
10738
+ const existing = next.find((record) => record.id === id || record.hostOperationId === entry.id);
10739
+ const changes = entry.state === "succeeded" ? existing?.needsRefresh === true ? {
10740
+ state: "done",
10741
+ blockedBy: void 0,
10742
+ waitSince: void 0,
10743
+ reason: void 0
10744
+ } : {
10745
+ state: "done",
10746
+ needsRefresh: false,
10747
+ blockedBy: void 0,
10748
+ waitSince: void 0,
10749
+ reason: void 0
10750
+ } : {
10751
+ state: entry.state === "cancelled" ? "warned" : "failed",
10752
+ reason: entry.state === "cancelled" ? t("agentDrainHistoryCancelled") : t("agentDrainHistoryFailedShort"),
10753
+ blockedBy: void 0,
10754
+ waitSince: void 0
10755
+ };
10756
+ if (existing !== void 0) next = patch(next, existing.id, changes);
10757
+ else next = enqueue(next, {
10758
+ id,
10759
+ kind: entry.kind,
10760
+ name: entry.name,
10761
+ hostOperationId: entry.id,
10762
+ ...changes
10763
+ });
10764
+ }
10765
+ return next;
10766
+ });
10767
+ if (opened) setOperationsOpen(true);
10768
+ }, [
10769
+ clearDrainMarker,
10770
+ refreshInstalled,
10771
+ t
10772
+ ]);
10496
10773
  /** Active Bundles count as installed in Discover without becoming package-manager targets. */
10497
10774
  const catalogInstalled = (0, react.useMemo)(() => installedForCatalog(installed, installedBundles), [installed, installedBundles]);
10498
10775
  (0, react.useMemo)(() => new Set(disabledNames), [disabledNames]);
@@ -10638,8 +10915,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10638
10915
  if (status === null) return;
10639
10916
  statusRef.current = {
10640
10917
  busy: status.busy === true,
10641
- runningAgents: Array.isArray(status.runningAgents) ? status.runningAgents.map(String) : []
10918
+ runningAgents: Array.isArray(status.runningAgents) ? status.runningAgents.map(String) : [],
10919
+ agentDrain: status.agentDrain === true,
10920
+ boot: typeof status.boot === "string" ? status.boot : null
10642
10921
  };
10922
+ applyDrainStatus(status);
10923
+ restoreDrainMarkers(status);
10643
10924
  setEnvReady(status.pnpm !== false);
10644
10925
  applyGithubRouting(status);
10645
10926
  if (typeof status.boot === "string") {
@@ -10657,9 +10938,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10657
10938
  }).catch(() => {});
10658
10939
  refreshInstalled();
10659
10940
  }, [
10941
+ applyDrainStatus,
10660
10942
  refreshInstalled,
10661
10943
  loadCatalog,
10662
- readMarketStatus
10944
+ readMarketStatus,
10945
+ restoreDrainMarkers
10663
10946
  ]);
10664
10947
  (0, react.useEffect)(() => {
10665
10948
  if (bootId === null) return;
@@ -10768,8 +11051,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10768
11051
  if (status === null) return;
10769
11052
  statusRef.current = {
10770
11053
  busy: status.busy === true,
10771
- runningAgents: Array.isArray(status.runningAgents) ? status.runningAgents.map(String) : []
11054
+ runningAgents: Array.isArray(status.runningAgents) ? status.runningAgents.map(String) : [],
11055
+ agentDrain: status.agentDrain === true,
11056
+ boot: typeof status.boot === "string" ? status.boot : null
10772
11057
  };
11058
+ applyDrainStatus(status);
11059
+ restoreDrainMarkers(status);
10773
11060
  setHostBusy(status.busy === true);
10774
11061
  setDebuggerLatch(typeof status.debugger === "string" ? status.debugger : null);
10775
11062
  if (status.active) {
@@ -10853,6 +11140,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10853
11140
  }, 2e3);
10854
11141
  return () => clearInterval(timer);
10855
11142
  }, [
11143
+ applyDrainStatus,
10856
11144
  busyUrl,
10857
11145
  updatingName,
10858
11146
  data,
@@ -10861,6 +11149,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10861
11149
  repoHints,
10862
11150
  refreshInstalled,
10863
11151
  lang,
11152
+ restoreDrainMarkers,
10864
11153
  t
10865
11154
  ]);
10866
11155
  const scrollToTop = () => {
@@ -11166,7 +11455,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11166
11455
  const rest = entries.length > 1 ? ` (+${entries.length - 1})` : "";
11167
11456
  return `${first.name} — ${first.layers.join(" / ")}${rest}`;
11168
11457
  };
11169
- const doInstall = (0, react.useCallback)((plugin, force = false, version) => {
11458
+ const doInstall = (0, react.useCallback)((plugin, force = false, version, preferReject = false) => {
11170
11459
  setBuildsSkipped(null);
11171
11460
  setConfirming(null);
11172
11461
  setInstallError(null);
@@ -11190,7 +11479,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11190
11479
  body: JSON.stringify({
11191
11480
  url: plugin.url,
11192
11481
  ...force ? { force: true } : {},
11193
- ...version !== void 0 ? { version } : {}
11482
+ ...version !== void 0 ? { version } : {},
11483
+ ...statusRef.current.agentDrain && !preferReject ? { busyPolicy: "drain" } : {}
11194
11484
  })
11195
11485
  }).then((res) => res.json().then((body) => ({
11196
11486
  status: res.status,
@@ -11198,6 +11488,26 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11198
11488
  }))).then(({ status, body }) => {
11199
11489
  setBusyUrl(null);
11200
11490
  sessionStorage.removeItem("dshm-pending");
11491
+ if (status === 202 && typeof body.operationId === "string") {
11492
+ const operation = body.operation;
11493
+ setRecords((list) => patch(withoutHostProjection(list, body.operationId, recordId), recordId, {
11494
+ state: "queued",
11495
+ hostOperationId: body.operationId,
11496
+ ...operation?.wait === null || operation?.wait === void 0 ? {} : {
11497
+ blockedBy: operation.wait.activeAgentIds,
11498
+ waitSince: operation.wait.since
11499
+ },
11500
+ reason: operation?.wait === null || operation?.wait === void 0 ? t("agentBusyQueued") : t("agentDrainWaiting").replace("{0}", String(operation.wait.activeCount))
11501
+ }));
11502
+ rememberDrainMarker({
11503
+ operationId: body.operationId,
11504
+ kind: "install",
11505
+ name: plugin.name,
11506
+ url: plugin.url
11507
+ });
11508
+ setOperationsOpen(true);
11509
+ return;
11510
+ }
11201
11511
  if (status === 200 && body.ok && body.hot && pluginCategories(plugin).includes("theme")) {
11202
11512
  sessionStorage.setItem("dshm-toast", JSON.stringify([plugin.name]));
11203
11513
  sessionStorage.setItem("dshm-tab", "themes");
@@ -11241,6 +11551,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11241
11551
  }));
11242
11552
  refreshInstalled();
11243
11553
  } else {
11554
+ if (body.unsupported === true) {
11555
+ setRecords((list) => drop(list, recordId));
11556
+ doInstall(plugin, force, version, true);
11557
+ return;
11558
+ }
11244
11559
  if (body.agentsBusy !== true && body.migration && typeof body.migration === "object") {
11245
11560
  const { from, target } = body.migration;
11246
11561
  if (typeof from === "string" && typeof target === "string") {
@@ -11256,6 +11571,22 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11256
11571
  }
11257
11572
  }
11258
11573
  if (status === 409) {
11574
+ if (body.failureCode === "AGENT_BARRIER_LOST") {
11575
+ setRecords((list) => patch(list, recordId, {
11576
+ state: "failed",
11577
+ reason: String(body.error ?? t("installFail"))
11578
+ }));
11579
+ setOperationsOpen(true);
11580
+ return;
11581
+ }
11582
+ if (body.targetChanged === true) {
11583
+ setRecords((list) => patch(list, recordId, {
11584
+ state: "failed",
11585
+ reason: String(body.error ?? t("installFail"))
11586
+ }));
11587
+ setOperationsOpen(true);
11588
+ return;
11589
+ }
11259
11590
  if (body.agentsBusy === true) {
11260
11591
  setRecords((list) => patch(list, recordId, {
11261
11592
  state: "queued",
@@ -11315,6 +11646,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11315
11646
  }, [
11316
11647
  nextRecordId,
11317
11648
  refreshInstalled,
11649
+ rememberDrainMarker,
11318
11650
  t,
11319
11651
  installed
11320
11652
  ]);
@@ -11529,14 +11861,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11529
11861
  enterRecovery
11530
11862
  ]);
11531
11863
  /** Cancel the running plugin command (#6 by @qichuang321). */
11532
- const doCancel = (0, react.useCallback)(() => {
11864
+ const doCancel = (0, react.useCallback)((record) => {
11865
+ const body = record?.hostOperationId === void 0 ? {} : { operationId: record.hostOperationId };
11533
11866
  fetch(api("/dsh-market/cancel"), {
11534
11867
  method: "POST",
11535
11868
  headers: { "content-type": "application/json" },
11536
- body: "{}"
11869
+ body: JSON.stringify(body)
11537
11870
  }).catch(() => {});
11538
11871
  }, []);
11539
- const doUpdate = (0, react.useCallback)((name, force = false, restore = false, compatVersion, confirmAgentsRunning = false, eligibleVersion) => {
11872
+ const doUpdate = (0, react.useCallback)((name, force = false, restore = false, compatVersion, confirmAgentsRunning = false, eligibleVersion, preferReject = false) => {
11540
11873
  const expectedVersion = compatVersion === void 0 && !restore ? eligibleVersion ?? updates[name]?.eligibleVersion : void 0;
11541
11874
  const updateOptions = {
11542
11875
  force,
@@ -11569,7 +11902,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11569
11902
  ...restore ? { restore: true } : {},
11570
11903
  ...compatVersion !== void 0 ? { compatVersion } : {},
11571
11904
  ...expectedVersion !== void 0 ? { eligibleVersion: expectedVersion } : {},
11572
- ...confirmAgentsRunning ? { confirmAgentsRunning: true } : {}
11905
+ ...confirmAgentsRunning ? { confirmAgentsRunning: true } : {},
11906
+ ...!restore && statusRef.current.agentDrain && !preferReject ? { busyPolicy: "drain" } : {}
11573
11907
  })
11574
11908
  }).then((res) => res.json().then((body) => ({
11575
11909
  status: res.status,
@@ -11577,6 +11911,25 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11577
11911
  }))).then(({ status, body }) => {
11578
11912
  sessionStorage.removeItem("dshm-updating");
11579
11913
  setUpdatingName(null);
11914
+ if (status === 202 && typeof body.operationId === "string") {
11915
+ const operation = body.operation;
11916
+ setRecords((list) => patch(withoutHostProjection(list, body.operationId, updateRecordId), updateRecordId, {
11917
+ state: "queued",
11918
+ hostOperationId: body.operationId,
11919
+ ...operation?.wait === null || operation?.wait === void 0 ? {} : {
11920
+ blockedBy: operation.wait.activeAgentIds,
11921
+ waitSince: operation.wait.since
11922
+ },
11923
+ reason: operation?.wait === null || operation?.wait === void 0 ? t("agentBusyUpdateQueued") : t("agentDrainWaiting").replace("{0}", String(operation.wait.activeCount))
11924
+ }));
11925
+ rememberDrainMarker({
11926
+ operationId: body.operationId,
11927
+ kind: "update",
11928
+ name
11929
+ });
11930
+ setOperationsOpen(true);
11931
+ return;
11932
+ }
11580
11933
  if (body.cancelled === true) {
11581
11934
  setRecords((list) => drop(list, updateRecordId));
11582
11935
  refreshInstalled();
@@ -11613,7 +11966,29 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11613
11966
  if (body.compatibility?.code === "soft-incompatible") setCompatibilityNotice(body.compatibility);
11614
11967
  refreshInstalled();
11615
11968
  } else {
11969
+ if (body.unsupported === true) {
11970
+ setRecords((list) => drop(list, updateRecordId));
11971
+ doUpdate(name, force, restore, compatVersion, confirmAgentsRunning, eligibleVersion, true);
11972
+ return;
11973
+ }
11616
11974
  if (status === 409) {
11975
+ if (body.failureCode === "AGENT_BARRIER_LOST") {
11976
+ setRecords((list) => patch(list, updateRecordId, {
11977
+ state: "failed",
11978
+ reason: String(body.error ?? "An Agent became active before the update; nothing was changed.")
11979
+ }));
11980
+ setOperationsOpen(true);
11981
+ return;
11982
+ }
11983
+ if (body.targetChanged === true) {
11984
+ setRecords((list) => patch(list, updateRecordId, {
11985
+ state: "failed",
11986
+ reason: String(body.error ?? "Update target changed")
11987
+ }));
11988
+ setInstallError(localizeBilingual(String(body.error ?? "Update target changed"), lang));
11989
+ setOperationsOpen(true);
11990
+ return;
11991
+ }
11617
11992
  if (body.agentsBusy === true) {
11618
11993
  setRecords((list) => patch(list, updateRecordId, {
11619
11994
  state: "queued",
@@ -11667,6 +12042,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11667
12042
  }, [
11668
12043
  refreshInstalled,
11669
12044
  installed,
12045
+ rememberDrainMarker,
11670
12046
  t,
11671
12047
  lang,
11672
12048
  updates
@@ -12127,7 +12503,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12127
12503
  setHotNames((names) => names.filter((entry) => entry !== name));
12128
12504
  setRefreshNames((names) => names.filter((entry) => entry !== name));
12129
12505
  }, []);
12130
- const doUninstall = (0, react.useCallback)((name, confirmAgentsRunning = false) => {
12506
+ const doUninstall = (0, react.useCallback)((name, confirmAgentsRunning = false, preferReject = false) => {
12131
12507
  setRemoveConfirm(null);
12132
12508
  setInstallError(null);
12133
12509
  setActivationWarnings([]);
@@ -12144,12 +12520,53 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12144
12520
  headers: { "content-type": "application/json" },
12145
12521
  body: JSON.stringify({
12146
12522
  name,
12147
- ...confirmAgentsRunning ? { confirmAgentsRunning: true } : {}
12523
+ ...confirmAgentsRunning ? { confirmAgentsRunning: true } : {},
12524
+ ...statusRef.current.agentDrain && !preferReject ? { busyPolicy: "drain" } : {}
12148
12525
  })
12149
12526
  }).then((res) => res.json().then((body) => ({
12150
12527
  status: res.status,
12151
12528
  body
12152
12529
  }))).then(({ status, body }) => {
12530
+ if (status === 202 && typeof body.operationId === "string") {
12531
+ const operation = body.operation;
12532
+ setRecords((list) => patch(withoutHostProjection(list, body.operationId, uninstallRecordId), uninstallRecordId, {
12533
+ state: "queued",
12534
+ hostOperationId: body.operationId,
12535
+ ...operation?.wait === null || operation?.wait === void 0 ? {} : {
12536
+ blockedBy: operation.wait.activeAgentIds,
12537
+ waitSince: operation.wait.since
12538
+ },
12539
+ reason: operation?.wait === null || operation?.wait === void 0 ? t("agentBusyUninstallQueued") : t("agentDrainWaiting").replace("{0}", String(operation.wait.activeCount))
12540
+ }));
12541
+ rememberDrainMarker({
12542
+ operationId: body.operationId,
12543
+ kind: "uninstall",
12544
+ name
12545
+ });
12546
+ setOperationsOpen(true);
12547
+ return;
12548
+ }
12549
+ if (body.unsupported === true) {
12550
+ setRecords((list) => drop(list, uninstallRecordId));
12551
+ doUninstall(name, confirmAgentsRunning, true);
12552
+ return;
12553
+ }
12554
+ if (status === 409 && body.failureCode === "AGENT_BARRIER_LOST") {
12555
+ setRecords((list) => patch(list, uninstallRecordId, {
12556
+ state: "failed",
12557
+ reason: String(body.error ?? "An Agent became active before the uninstall; nothing was changed.")
12558
+ }));
12559
+ setOperationsOpen(true);
12560
+ return;
12561
+ }
12562
+ if (status === 409 && body.targetChanged === true) {
12563
+ setRecords((list) => patch(list, uninstallRecordId, {
12564
+ state: "failed",
12565
+ reason: String(body.error ?? t("installFail"))
12566
+ }));
12567
+ setOperationsOpen(true);
12568
+ return;
12569
+ }
12153
12570
  if (status === 409 && body.agentsBusy === true) {
12154
12571
  setRecords((list) => patch(list, uninstallRecordId, {
12155
12572
  state: "queued",
@@ -12187,6 +12604,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12187
12604
  hotNames,
12188
12605
  refreshNames,
12189
12606
  nextRecordId,
12607
+ rememberDrainMarker,
12190
12608
  t
12191
12609
  ]);
12192
12610
  const busyUrlRef = (0, react.useRef)(null);
@@ -12250,19 +12668,24 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12250
12668
  const timer = setInterval(() => {
12251
12669
  if (drainingRef.current) return;
12252
12670
  if (operationInFlight()) return;
12253
- if (!recordsRef.current.some((record) => record.state === "queued")) return;
12671
+ if (!recordsRef.current.some((record) => record.hostOperationId === void 0 ? record.state === "queued" : record.state === "queued" || record.state === "running")) return;
12254
12672
  fetch(api("/dsh-market/status"), { cache: "no-store" }).then(readMarketStatus).then((status) => {
12255
12673
  if (disposed || status === null) return;
12256
12674
  const runningAgents = Array.isArray(status.runningAgents) ? status.runningAgents.map(String) : [];
12257
12675
  const busy = status.busy === true;
12258
12676
  statusRef.current = {
12259
12677
  busy,
12260
- runningAgents
12678
+ runningAgents,
12679
+ agentDrain: status.agentDrain === true,
12680
+ boot: typeof status.boot === "string" ? status.boot : null
12261
12681
  };
12682
+ applyDrainStatus(status);
12683
+ restoreDrainMarkers(status);
12262
12684
  if (busy) return;
12685
+ if (status.drainActive === true) return;
12263
12686
  if (operationInFlight()) return;
12264
- const task = recordsRef.current.find((record) => record.state === "queued" && (record.kind === "install" || runningAgents.length === 0));
12265
- if (task === void 0 || recordsRef.current.some((record) => record.state === "running")) return;
12687
+ const task = recordsRef.current.find((record) => record.state === "queued" && record.hostOperationId === void 0 && (record.kind === "install" || runningAgents.length === 0));
12688
+ if (task === void 0 || recordsRef.current.some((record) => record.state === "running" && record.hostOperationId === void 0)) return;
12266
12689
  setRecords((list) => list.filter((record) => record.id !== task.id));
12267
12690
  drainingRef.current = true;
12268
12691
  try {
@@ -12280,9 +12703,14 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12280
12703
  disposed = true;
12281
12704
  clearInterval(timer);
12282
12705
  };
12283
- }, [readMarketStatus]);
12706
+ }, [
12707
+ applyDrainStatus,
12708
+ readMarketStatus,
12709
+ restoreDrainMarkers
12710
+ ]);
12284
12711
  /** A queued record's "run now": retry immediately instead of waiting for idle. */
12285
12712
  const runQueuedNow = (0, react.useCallback)((record, confirmAgentsRunning = false) => {
12713
+ if (record.hostOperationId !== void 0) return;
12286
12714
  if (!recordsRef.current.some((item) => item.id === record.id && item.state === "queued")) return;
12287
12715
  if (record.kind !== "install" && !confirmAgentsRunning && (statusRef.current.runningAgents.length > 0 || (record.blockedBy?.length ?? 0) > 0)) {
12288
12716
  setAgentsConfirm({
@@ -13252,7 +13680,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13252
13680
  type: "button",
13253
13681
  className: Market_module_css_default.cardBlockedMark,
13254
13682
  onClick: openOperations,
13255
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("queuedBadge")]
13683
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t(record?.hostOperationId !== void 0 && (record.blockedBy?.length ?? 0) > 0 ? "agentDrainBadge" : "queuedBadge")]
13256
13684
  }) : blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13257
13685
  type: "button",
13258
13686
  className: Market_module_css_default.cardBlockedMark,
@@ -13456,7 +13884,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13456
13884
  variant: "outline",
13457
13885
  size: "sm",
13458
13886
  disabled: cancelling,
13459
- onClick: doCancel,
13887
+ onClick: () => doCancel(),
13460
13888
  children: cancelling ? t("cancelling") : t("cancelOp")
13461
13889
  }),
13462
13890
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13595,7 +14023,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13595
14023
  type: "button",
13596
14024
  className: Market_module_css_default.cardBlockedMark,
13597
14025
  onClick: openOperations,
13598
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("queuedBadge")]
14026
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t(record?.hostOperationId !== void 0 && (record.blockedBy?.length ?? 0) > 0 ? "agentDrainBadge" : "queuedBadge")]
13599
14027
  }) : blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13600
14028
  type: "button",
13601
14029
  className: Market_module_css_default.cardBlockedMark,
@@ -13652,7 +14080,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13652
14080
  variant: "outline",
13653
14081
  size: "sm",
13654
14082
  disabled: cancelling,
13655
- onClick: doCancel,
14083
+ onClick: () => doCancel(),
13656
14084
  children: cancelling ? t("cancelling") : t("cancelOp")
13657
14085
  }),
13658
14086
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -14037,9 +14465,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
14037
14465
  onOpenChange: setOperationsOpen,
14038
14466
  replacing,
14039
14467
  envReady,
14040
- onClearSettled: () => setRecords((list) => clearSettled(list)),
14041
- onCancel: () => doCancel(),
14042
- onDismiss: (record) => setRecords((list) => drop(list, record.id)),
14468
+ onClearSettled: () => {
14469
+ for (const record of recordsRef.current) if (record.hostOperationId !== void 0 && (record.state === "done" || record.state === "warned" || record.state === "failed")) dismissedHostRef.current.add(record.hostOperationId);
14470
+ setRecords((list) => clearSettled(list));
14471
+ },
14472
+ onCancel: (record) => doCancel(record),
14473
+ onDismiss: (record) => {
14474
+ if (record.hostOperationId !== void 0) dismissedHostRef.current.add(record.hostOperationId);
14475
+ setRecords((list) => drop(list, record.id));
14476
+ },
14043
14477
  onRefresh: () => location.reload(),
14044
14478
  onResolveConflict: resolveConflict,
14045
14479
  onForceInstall: (record) => {
@@ -15653,7 +16087,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
15653
16087
  variant: "outline",
15654
16088
  size: "sm",
15655
16089
  disabled: cancelling,
15656
- onClick: doCancel,
16090
+ onClick: () => doCancel(),
15657
16091
  children: cancelling ? t("cancelling") : t("cancelOp")
15658
16092
  }),
15659
16093
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {