@michengai/dsh-codex-ui 0.2.72 → 0.2.73

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 +194 -53
  2. package/dist/index.mjs +127 -44
  3. package/package.json +3 -2
package/dist/client.js CHANGED
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
18
18
  if (match !== void 0) return match;
19
19
  }
20
20
  }
21
+ let cancelPendingNavigation;
21
22
  function openSettingsSection(root, label, onMissing) {
22
23
  const labels = typeof label === "string" ? [label] : label;
23
24
  const trigger = root?.querySelector("[aria-haspopup=\"dialog\"]");
@@ -25,18 +26,48 @@ window.__ModuleLoader__.load({
25
26
  onMissing?.();
26
27
  return;
27
28
  }
28
- trigger.click();
29
- window.requestAnimationFrame(() => {
30
- window.requestAnimationFrame(() => {
31
- const target = pickSettingsSectionButton([...document.querySelectorAll("[role=\"dialog\"] nav button")], labels);
32
- if (target !== void 0) {
33
- target.click();
34
- return;
35
- }
36
- console.warn(`[michengai-codex-ui] 未找到设置分区:${labels.join(" / ")}`);
37
- onMissing?.();
29
+ const opening = cancelPendingNavigation !== void 0;
30
+ cancelPendingNavigation?.();
31
+ if (!opening && document.querySelector("[role=\"dialog\"]") === null) trigger.click();
32
+ let frame;
33
+ let finished = false;
34
+ const observer = new MutationObserver(() => {
35
+ schedule();
36
+ });
37
+ const cleanup = () => {
38
+ if (finished) return;
39
+ finished = true;
40
+ observer.disconnect();
41
+ window.clearTimeout(timeout);
42
+ if (frame !== void 0) window.cancelAnimationFrame(frame);
43
+ if (cancelPendingNavigation === cleanup) cancelPendingNavigation = void 0;
44
+ };
45
+ const select = () => {
46
+ const target = pickSettingsSectionButton([...document.querySelectorAll("[role=\"dialog\"] nav button")], labels);
47
+ if (target === void 0) return false;
48
+ cleanup();
49
+ target.click();
50
+ return true;
51
+ };
52
+ const schedule = () => {
53
+ if (finished || frame !== void 0) return;
54
+ frame = window.requestAnimationFrame(() => {
55
+ frame = void 0;
56
+ select();
38
57
  });
58
+ };
59
+ const timeout = window.setTimeout(() => {
60
+ if (select()) return;
61
+ cleanup();
62
+ console.warn(`[michengai-codex-ui] 未找到设置分区:${labels.join(" / ")}`);
63
+ onMissing?.();
64
+ }, 1500);
65
+ observer.observe(document.body, {
66
+ childList: true,
67
+ subtree: true
39
68
  });
69
+ cancelPendingNavigation = cleanup;
70
+ schedule();
40
71
  }
41
72
  //#endregion
42
73
  //#region src/client/sidebar-search.ts
@@ -52,6 +83,7 @@ window.__ModuleLoader__.load({
52
83
  channels: false,
53
84
  schedule: false
54
85
  };
86
+ const reportedSlotErrors = /* @__PURE__ */ new WeakSet();
55
87
  /** 插槽声明本身不算占用;只有其他插件 register 后才视为已安装。 */
56
88
  function readSlotEntries(slots, name) {
57
89
  if (slots === void 0) return [];
@@ -59,7 +91,11 @@ window.__ModuleLoader__.load({
59
91
  if (typeof read !== "function") return [];
60
92
  try {
61
93
  return read.call(slots, name) ?? [];
62
- } catch {
94
+ } catch (error) {
95
+ if (!reportedSlotErrors.has(slots)) {
96
+ reportedSlotErrors.add(slots);
97
+ console.warn("[michengai-codex-ui] 无法读取配套插件插槽。", error);
98
+ }
63
99
  return [];
64
100
  }
65
101
  }
@@ -251,11 +287,19 @@ window.__ModuleLoader__.load({
251
287
  });
252
288
  }
253
289
  /** 读取 IM 频道分组;失败时交给界面显示空态或错误。 */
254
- async function loadChannelGroups() {
255
- const response = await fetch(CHANNELS_ENDPOINT, { cache: "no-store" });
256
- const payload = await response.json();
257
- if (!response.ok) throw new Error("无法读取频道会话");
290
+ async function loadChannelGroups(signal) {
291
+ const response = await fetch(CHANNELS_ENDPOINT, {
292
+ cache: "no-store",
293
+ signal
294
+ });
295
+ let payload;
296
+ try {
297
+ payload = await response.json();
298
+ } catch {
299
+ throw new Error(response.ok ? "频道会话数据格式无效" : "无法读取频道会话");
300
+ }
258
301
  const root = payload !== null && typeof payload === "object" ? payload : {};
302
+ if (!response.ok) throw new Error(text(root.error, "无法读取频道会话"));
259
303
  if (root.ok === false) throw new Error(text(root.error, "无法读取频道会话"));
260
304
  return parseChannelGroups(payload);
261
305
  }
@@ -679,7 +723,6 @@ window.__ModuleLoader__.load({
679
723
  card.append(sub);
680
724
  }
681
725
  card.dataset.dcuUserText = text;
682
- card.title = text;
683
726
  }
684
727
  }
685
728
  function ensureUserBubbleStyle(doc) {
@@ -790,19 +833,25 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
790
833
  slider.style.transform = `translateX(${Math.max(0, tabBox.left - listBox.left)}px)`;
791
834
  }
792
835
  function watchTabSelection(tabs) {
793
- if (tabs.dataset.dcuTabWatch === "") return;
794
836
  tabs.dataset.dcuTabWatch = "";
795
837
  const sync = () => {
796
838
  syncTabSlider(tabs);
797
839
  };
798
- new MutationObserver(sync).observe(tabs, {
840
+ const onClick = () => {
841
+ window.requestAnimationFrame(sync);
842
+ };
843
+ const observer = new MutationObserver(sync);
844
+ observer.observe(tabs, {
799
845
  attributes: true,
800
846
  subtree: true,
801
847
  attributeFilter: ["aria-selected", "data-state"]
802
848
  });
803
- tabs.addEventListener("click", () => {
804
- window.requestAnimationFrame(sync);
805
- });
849
+ tabs.addEventListener("click", onClick);
850
+ return () => {
851
+ observer.disconnect();
852
+ tabs.removeEventListener("click", onClick);
853
+ delete tabs.dataset.dcuTabWatch;
854
+ };
806
855
  }
807
856
  function ensureStyle(doc) {
808
857
  if (doc.getElementById("dcu-conversation-header-style") !== null) return;
@@ -818,6 +867,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
818
867
  ensureUserBubbleStyle(doc);
819
868
  let applying = false;
820
869
  let frame;
870
+ let watchedTabs;
871
+ let stopWatchingTabs;
821
872
  const run = () => {
822
873
  frame = void 0;
823
874
  if (applying) return;
@@ -825,7 +876,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
825
876
  try {
826
877
  placeConversationTabs(doc);
827
878
  const tabs = findConversationTablist(doc);
828
- if (tabs !== void 0) watchTabSelection(tabs);
879
+ if (tabs !== watchedTabs) {
880
+ stopWatchingTabs?.();
881
+ watchedTabs = tabs;
882
+ stopWatchingTabs = tabs === void 0 ? void 0 : watchTabSelection(tabs);
883
+ }
829
884
  syncTabSlider(doc);
830
885
  decorateConversationTitle(doc);
831
886
  decorateUserBubbles(doc);
@@ -845,6 +900,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
845
900
  });
846
901
  return () => {
847
902
  observer.disconnect();
903
+ stopWatchingTabs?.();
848
904
  if (frame !== void 0) window.cancelAnimationFrame(frame);
849
905
  };
850
906
  }
@@ -903,6 +959,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
903
959
  window.clearTimeout(showTipTimer.current);
904
960
  showTipTimer.current = void 0;
905
961
  }
962
+ if (hideTipTimer.current !== void 0) window.clearTimeout(hideTipTimer.current);
906
963
  hideTipTimer.current = window.setTimeout(() => {
907
964
  setHoverTip(void 0);
908
965
  }, 120);
@@ -1229,6 +1286,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1229
1286
  };
1230
1287
  const [renameDraft, setRenameDraft] = (0, react.useState)("");
1231
1288
  const [busy, setBusy] = (0, react.useState)();
1289
+ const busyRef = (0, react.useRef)();
1232
1290
  const [error, setError] = (0, react.useState)();
1233
1291
  const [workspaceDragId, setWorkspaceDragId] = (0, react.useState)();
1234
1292
  const [workspaceDropId, setWorkspaceDropId] = (0, react.useState)();
@@ -1339,6 +1397,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1339
1397
  const assignedIds = workspaces.items.flatMap((workspace) => workspace.sessionIds.map((id) => String(id)));
1340
1398
  const recentIds = ungroupedSessionIds(sessions.ids ?? Object.keys(sessions.byId), sessions.byId, assignedIds, workspaces.archivedSessionIds).sort((left, right) => (sessions.byId[right]?.updatedAt ?? 0) - (sessions.byId[left]?.updatedAt ?? 0));
1341
1399
  const run = async (key, action) => {
1400
+ if (busyRef.current !== void 0) return;
1401
+ busyRef.current = key;
1342
1402
  setBusy(key);
1343
1403
  setError(void 0);
1344
1404
  try {
@@ -1347,6 +1407,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1347
1407
  } catch (reason) {
1348
1408
  setError(reason instanceof Error ? reason.message : String(reason));
1349
1409
  } finally {
1410
+ busyRef.current = void 0;
1350
1411
  setBusy(void 0);
1351
1412
  }
1352
1413
  };
@@ -2330,7 +2391,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2330
2391
  function useBusyAction(onSuccess) {
2331
2392
  const [busy, setBusy] = (0, react.useState)();
2332
2393
  const [error, setError] = (0, react.useState)();
2394
+ const busyRef = (0, react.useRef)();
2333
2395
  const run = async (key, action) => {
2396
+ if (busyRef.current !== void 0) return;
2397
+ busyRef.current = key;
2334
2398
  setBusy(key);
2335
2399
  setError(void 0);
2336
2400
  try {
@@ -2339,6 +2403,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2339
2403
  } catch (reason) {
2340
2404
  setError(reason instanceof Error ? reason.message : String(reason));
2341
2405
  } finally {
2406
+ busyRef.current = void 0;
2342
2407
  setBusy(void 0);
2343
2408
  }
2344
2409
  };
@@ -2557,11 +2622,15 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2557
2622
  });
2558
2623
  (0, react.useEffect)(() => {
2559
2624
  let disposed = false;
2560
- let loading = false;
2625
+ let active;
2561
2626
  const load = () => {
2562
- if (loading) return;
2563
- loading = true;
2564
- loadChannelGroups().then((next) => {
2627
+ if (active !== void 0 || document.visibilityState === "hidden") return;
2628
+ const controller = new AbortController();
2629
+ active = controller;
2630
+ const timeout = window.setTimeout(() => {
2631
+ controller.abort();
2632
+ }, 8e3);
2633
+ loadChannelGroups(controller.signal).then((next) => {
2565
2634
  if (!disposed) {
2566
2635
  setGroups(next);
2567
2636
  setPollError(void 0);
@@ -2569,16 +2638,23 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2569
2638
  }).catch(() => {
2570
2639
  if (!disposed) setPollError(t("channels.loadError"));
2571
2640
  }).finally(() => {
2572
- loading = false;
2641
+ window.clearTimeout(timeout);
2642
+ if (active === controller) active = void 0;
2573
2643
  });
2574
2644
  };
2575
2645
  load();
2576
2646
  const timer = window.setInterval(load, 4e3);
2647
+ const onVisibilityChange = () => {
2648
+ if (document.visibilityState === "visible") load();
2649
+ };
2650
+ document.addEventListener("visibilitychange", onVisibilityChange);
2577
2651
  return () => {
2578
2652
  disposed = true;
2653
+ active?.abort();
2579
2654
  window.clearInterval(timer);
2655
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2580
2656
  };
2581
- }, []);
2657
+ }, [t]);
2582
2658
  const banner = pollError ?? error;
2583
2659
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2584
2660
  className: "dcu-wb",
@@ -2875,6 +2951,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2875
2951
  }
2876
2952
  //#endregion
2877
2953
  //#region src/client/CodexSidebar.tsx
2954
+ const subscribeEmptyCompanionTabs = () => () => {};
2955
+ const getEmptyCompanionTabs = () => EMPTY_COMPANION_TABS;
2878
2956
  const stylesheet$3 = `
2879
2957
  .dcu-root{--dcu-font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei UI",sans-serif;--dcu-sidebar-primary:#393d3e;--dcu-sidebar-secondary:#676b6c;--dcu-sidebar-tertiary:#9a9f9f;--dcu-sidebar-navigation:#4e5253;--dcu-sidebar-icon:#4e5253;--dcu-sidebar-hover:#dfe8e5;--dcu-sidebar-border:rgba(37,46,41,.10);--dcu-tip-bg:#ffffff;--dcu-tip-shadow:0 10px 32px rgba(31,39,36,.22);height:100%;min-width:0;box-sizing:border-box;display:flex;flex-direction:column;background:#eef7f5;color:var(--dcu-sidebar-primary);font:14px/20px var(--dcu-font)}body[data-ds-dark-theme] .dcu-root{background:#1d2120;--dcu-sidebar-primary:#b9bab9;--dcu-sidebar-secondary:#909191;--dcu-sidebar-tertiary:#666867;--dcu-sidebar-navigation:#b9bab9;--dcu-sidebar-icon:#afafaf;--dcu-sidebar-hover:#303432;--dcu-sidebar-border:rgba(255,255,255,.08);--dcu-tip-bg:#2a2e2c;--dcu-tip-shadow:0 10px 30px rgba(0,0,0,.28)}
2880
2958
  .dcu-root *{box-sizing:border-box}.dcu-head{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;column-gap:8px;height:60px;padding:8px 8px 8px 12px}.dcu-brand{border:0;background:transparent;color:inherit;padding:0;display:flex;align-items:center;min-width:0;overflow:hidden}.dcu-brand svg{display:block;width:auto;max-width:100%;height:24px;min-width:0}.dcu-head-actions{display:grid;grid-auto-flow:column;grid-auto-columns:28px;align-items:center;column-gap:8px;height:28px}
@@ -2932,7 +3010,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2932
3010
  const [searchQuery, setSearchQuery] = (0, react.useState)("");
2933
3011
  const [activeSearchIndex, setActiveSearchIndex] = (0, react.useState)(0);
2934
3012
  const [imTab, setImTab] = (0, react.useState)("tasks");
2935
- const companionTabs = (0, react.useSyncExternalStore)(companionSlots?.subscribe ?? (() => () => {}), companionSlots?.getSnapshot ?? (() => EMPTY_COMPANION_TABS), companionSlots?.getSnapshot ?? (() => EMPTY_COMPANION_TABS));
3013
+ const companionTabs = (0, react.useSyncExternalStore)(companionSlots?.subscribe ?? subscribeEmptyCompanionTabs, companionSlots?.getSnapshot ?? getEmptyCompanionTabs, companionSlots?.getSnapshot ?? getEmptyCompanionTabs);
2936
3014
  const showChannels = companionTabs.channels;
2937
3015
  const showSchedule = companionTabs.schedule;
2938
3016
  const showCompanionTabs = showChannels || showSchedule;
@@ -3126,14 +3204,17 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3126
3204
  (0, react.useEffect)(() => {
3127
3205
  let startX = 0;
3128
3206
  let dragging = false;
3207
+ let pointerId;
3129
3208
  const onDown = (event) => {
3130
- if (!isSidebarDragHandle(event.target)) return;
3209
+ if (dragging || !isSidebarDragHandle(event.target)) return;
3131
3210
  dragging = true;
3211
+ pointerId = event.pointerId;
3132
3212
  startX = event.clientX;
3133
3213
  };
3134
3214
  const onUp = (event) => {
3135
- if (!dragging) return;
3215
+ if (!dragging || event.pointerId !== pointerId) return;
3136
3216
  dragging = false;
3217
+ pointerId = void 0;
3137
3218
  if (!shouldCollapseOnSidebarDrag(startX, event.clientX)) return;
3138
3219
  window.requestAnimationFrame(() => {
3139
3220
  window.requestAnimationFrame(() => {
@@ -3141,11 +3222,18 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3141
3222
  });
3142
3223
  });
3143
3224
  };
3225
+ const onCancel = (event) => {
3226
+ if (event.pointerId !== pointerId) return;
3227
+ dragging = false;
3228
+ pointerId = void 0;
3229
+ };
3144
3230
  window.addEventListener("pointerdown", onDown, true);
3145
3231
  window.addEventListener("pointerup", onUp, true);
3232
+ window.addEventListener("pointercancel", onCancel, true);
3146
3233
  return () => {
3147
3234
  window.removeEventListener("pointerdown", onDown, true);
3148
3235
  window.removeEventListener("pointerup", onUp, true);
3236
+ window.removeEventListener("pointercancel", onCancel, true);
3149
3237
  };
3150
3238
  }, [toggleSidebar]);
3151
3239
  if (collapsed || width < 80) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
@@ -3607,9 +3695,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3607
3695
  const root = (0, react.useRef)(null);
3608
3696
  const stateRef = (0, react.useRef)("loading");
3609
3697
  const requestId = (0, react.useRef)(0);
3698
+ const installingRef = (0, react.useRef)();
3610
3699
  stateRef.current = state;
3611
- (0, react.useEffect)(() => () => {
3612
- alive.current = false;
3700
+ (0, react.useEffect)(() => {
3701
+ alive.current = true;
3702
+ return () => {
3703
+ alive.current = false;
3704
+ };
3613
3705
  }, []);
3614
3706
  const load = (0, react.useCallback)(async (signal) => {
3615
3707
  const currentRequest = ++requestId.current;
@@ -3639,8 +3731,17 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3639
3731
  if (node === null || typeof IntersectionObserver === "undefined") return () => {
3640
3732
  controller.abort();
3641
3733
  };
3734
+ let initialized = false;
3735
+ let wasVisible = false;
3642
3736
  const observer = new IntersectionObserver((entries) => {
3643
- if (entries.some((entry) => entry.isIntersecting)) refresh();
3737
+ const visible = entries.some((entry) => entry.isIntersecting);
3738
+ if (!initialized) {
3739
+ initialized = true;
3740
+ wasVisible = visible;
3741
+ return;
3742
+ }
3743
+ if (visible && !wasVisible) refresh();
3744
+ wasVisible = visible;
3644
3745
  }, { threshold: .2 });
3645
3746
  observer.observe(node);
3646
3747
  return () => {
@@ -3649,6 +3750,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3649
3750
  };
3650
3751
  }, [load]);
3651
3752
  const install = async (id) => {
3753
+ if (installingRef.current !== void 0) return;
3754
+ installingRef.current = id;
3652
3755
  setInstalling(id);
3653
3756
  setMessage(void 0);
3654
3757
  try {
@@ -3669,6 +3772,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3669
3772
  text: error instanceof Error ? error.message : t("about.installFailed")
3670
3773
  });
3671
3774
  } finally {
3775
+ installingRef.current = void 0;
3672
3776
  if (alive.current) setInstalling(void 0);
3673
3777
  }
3674
3778
  };
@@ -3736,7 +3840,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3736
3840
  (!dependency.installed || dependency.updateAvailable) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3737
3841
  className: "dcu-about-install",
3738
3842
  type: "button",
3739
- disabled: installing === dependency.id,
3843
+ disabled: installing !== void 0,
3740
3844
  onClick: () => {
3741
3845
  install(dependency.id);
3742
3846
  },
@@ -4190,11 +4294,27 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4190
4294
  let applying = false;
4191
4295
  let frame;
4192
4296
  let pending;
4297
+ let frameObserver;
4298
+ const watchFrame = (next) => {
4299
+ if (frame === next) return;
4300
+ frameObserver?.disconnect();
4301
+ frame = next;
4302
+ if (frame === void 0) return;
4303
+ frameObserver = new MutationObserver(schedule);
4304
+ frameObserver.observe(frame, {
4305
+ attributes: true,
4306
+ attributeFilter: [
4307
+ "style",
4308
+ "data-sidebar-collapsed",
4309
+ "data-dragging"
4310
+ ]
4311
+ });
4312
+ };
4193
4313
  const apply = () => {
4194
4314
  if (applying) return;
4195
4315
  applying = true;
4196
4316
  try {
4197
- if (frame === void 0 || !frame.isConnected) frame = findSidebarFrame(document);
4317
+ if (frame === void 0 || !frame.isConnected) watchFrame(findSidebarFrame(document));
4198
4318
  if (frame !== void 0) applySlimSidebar(frame);
4199
4319
  } finally {
4200
4320
  applying = false;
@@ -4208,19 +4328,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4208
4328
  });
4209
4329
  };
4210
4330
  apply();
4211
- const observer = new MutationObserver(schedule);
4331
+ const observer = new MutationObserver(() => {
4332
+ if (frame === void 0 || !frame.isConnected) schedule();
4333
+ });
4212
4334
  observer.observe(document.body, {
4213
- attributes: true,
4214
- attributeFilter: [
4215
- "style",
4216
- "data-sidebar-collapsed",
4217
- "data-dragging"
4218
- ],
4219
4335
  childList: true,
4220
4336
  subtree: true
4221
4337
  });
4222
4338
  return () => {
4223
4339
  observer.disconnect();
4340
+ frameObserver?.disconnect();
4224
4341
  if (pending !== void 0) window.cancelAnimationFrame(pending);
4225
4342
  };
4226
4343
  }
@@ -4234,8 +4351,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4234
4351
  return document.querySelector("[data-conversation-scroll]");
4235
4352
  }
4236
4353
  function conversationAnchor(root, key) {
4237
- for (const anchor of root.querySelectorAll("[data-chat-anchor-key]")) if (anchor.dataset.chatAnchorKey === key) return anchor;
4238
- return null;
4354
+ return conversationAnchors(root).get(key) ?? null;
4355
+ }
4356
+ /** 一次扫描生成锚点索引,滚动帧内不得为每个轮次重复遍历 DOM。 */
4357
+ function conversationAnchors(root) {
4358
+ const anchors = /* @__PURE__ */ new Map();
4359
+ for (const anchor of root.querySelectorAll("[data-chat-anchor-key]")) {
4360
+ const key = anchor.dataset.chatAnchorKey;
4361
+ if (key !== void 0 && !anchors.has(key)) anchors.set(key, anchor);
4362
+ }
4363
+ return anchors;
4239
4364
  }
4240
4365
  //#endregion
4241
4366
  //#region src/client/TurnNavigator.tsx
@@ -4337,27 +4462,44 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4337
4462
  };
4338
4463
  }, []);
4339
4464
  (0, react.useEffect)(() => {
4340
- const host = conversationScrollRoot();
4341
- if (host === null || turns.length === 0) return;
4465
+ if (turns.length === 0) return;
4466
+ let host = null;
4342
4467
  let frame = null;
4343
4468
  const update = () => {
4344
4469
  frame = null;
4470
+ if (host === null) return;
4345
4471
  const threshold = host.getBoundingClientRect().top + Math.min(180, host.clientHeight * .35);
4472
+ const anchors = conversationAnchors(host);
4346
4473
  let next = turns[0]?.key ?? null;
4347
4474
  for (const turn of turns) {
4348
- const anchor = conversationAnchor(host, turn.key);
4349
- if (anchor !== null && anchor.getBoundingClientRect().top <= threshold) next = turn.key;
4475
+ const anchor = anchors.get(turn.key);
4476
+ if (anchor !== void 0 && anchor.getBoundingClientRect().top <= threshold) next = turn.key;
4350
4477
  }
4351
4478
  setCurrent((previous) => previous === next ? previous : next);
4352
4479
  };
4353
4480
  const schedule = () => {
4354
4481
  if (frame === null) frame = window.requestAnimationFrame(update);
4355
4482
  };
4356
- host.addEventListener("scroll", schedule, { passive: true });
4483
+ const bindHost = () => {
4484
+ const next = conversationScrollRoot();
4485
+ if (next === host) return;
4486
+ host?.removeEventListener("scroll", schedule);
4487
+ host = next;
4488
+ host?.addEventListener("scroll", schedule, { passive: true });
4489
+ schedule();
4490
+ };
4491
+ const observer = new MutationObserver(() => {
4492
+ if (host === null || !host.isConnected) bindHost();
4493
+ });
4494
+ observer.observe(document.body, {
4495
+ childList: true,
4496
+ subtree: true
4497
+ });
4357
4498
  window.addEventListener("resize", schedule);
4358
- schedule();
4499
+ bindHost();
4359
4500
  return () => {
4360
- host.removeEventListener("scroll", schedule);
4501
+ observer.disconnect();
4502
+ host?.removeEventListener("scroll", schedule);
4361
4503
  window.removeEventListener("resize", schedule);
4362
4504
  if (frame !== null) window.cancelAnimationFrame(frame);
4363
4505
  };
@@ -4412,7 +4554,6 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4412
4554
  index: index + 1,
4413
4555
  summary: turn.summary
4414
4556
  }),
4415
- title: turn.summary,
4416
4557
  style: tickStyle,
4417
4558
  onFocus: () => {
4418
4559
  setHoverAt(index);
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -54,6 +54,7 @@ function managedDependency(id) {
54
54
  //#region src/dependency-manager.ts
55
55
  const PROFILE_PENDING_UPDATES_FILE = ".dsh-pending-updates.json";
56
56
  const APPLY_PLUGIN_UPDATES_IPC = "apply-plugin-updates";
57
+ const PLUGIN_INSTALL_TIMEOUT_MS = 6e5;
57
58
  function profileDirectory() {
58
59
  if (process.env.DSH_PROFILE_DIR !== void 0) return process.env.DSH_PROFILE_DIR;
59
60
  return resolve(homedir(), ".dsh", "profiles", "web");
@@ -173,7 +174,7 @@ function applyReleaseExclude(source, packageName, version) {
173
174
  }
174
175
  /** 将用户本次确认的精确版本加入 Profile 的 pnpm 发布时间保护例外。 */
175
176
  async function ensureLatestReleaseAllowed(packageName, version) {
176
- if (versionParts(version) === void 0) throw new Error("npm 返回了无法识别的最新版本。");
177
+ if (parseSemver(version) === void 0) throw new Error("npm 返回了无法识别的最新版本。");
177
178
  const path = resolve(profileDirectory(), "pnpm-workspace.yaml");
178
179
  let source;
179
180
  try {
@@ -185,26 +186,40 @@ async function ensureLatestReleaseAllowed(packageName, version) {
185
186
  const next = applyReleaseExclude(source, packageName, version);
186
187
  if (next !== source) await writeFile(path, next, "utf8");
187
188
  }
188
- function versionParts(version) {
189
- const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
190
- return match === null ? void 0 : [
191
- Number(match[1]),
192
- Number(match[2]),
193
- Number(match[3])
194
- ];
189
+ function parseSemver(version) {
190
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
191
+ if (match === null) return void 0;
192
+ return {
193
+ core: [
194
+ Number(match[1]),
195
+ Number(match[2]),
196
+ Number(match[3])
197
+ ],
198
+ prerelease: match[4] === void 0 ? [] : match[4].split(".")
199
+ };
195
200
  }
196
- function prereleaseRank(version) {
197
- const match = /-rc\.(\d+)/i.exec(version);
198
- return match === null ? Number.POSITIVE_INFINITY : Number(match[1]);
201
+ function comparePrerelease(left, right) {
202
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
203
+ const length = Math.max(left.length, right.length);
204
+ for (let index = 0; index < length; index += 1) {
205
+ const a = left[index];
206
+ const b = right[index];
207
+ if (a === void 0 || b === void 0) return a === b ? 0 : a === void 0 ? -1 : 1;
208
+ if (a === b) continue;
209
+ const aNumeric = /^\d+$/.test(a);
210
+ const bNumeric = /^\d+$/.test(b);
211
+ if (aNumeric && bNumeric) return Number(a) > Number(b) ? 1 : -1;
212
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
213
+ return a > b ? 1 : -1;
214
+ }
215
+ return 0;
199
216
  }
200
217
  function newerVersion(installed, latest) {
201
- const current = versionParts(installed);
202
- const candidate = versionParts(latest);
218
+ const current = parseSemver(installed);
219
+ const candidate = parseSemver(latest);
203
220
  if (current === void 0 || candidate === void 0) return false;
204
- if (candidate[0] !== current[0]) return candidate[0] > current[0];
205
- if (candidate[1] !== current[1]) return candidate[1] > current[1];
206
- if (candidate[2] !== current[2]) return candidate[2] > current[2];
207
- return prereleaseRank(latest) > prereleaseRank(installed);
221
+ for (let index = 0; index < current.core.length; index += 1) if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index];
222
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
208
223
  }
209
224
  /** 返回 Web profile 中固定管理插件的实际安装版本与 npm latest 状态。 */
210
225
  async function dependencyStatuses() {
@@ -272,9 +287,31 @@ async function recordPendingUpdate(packageName, version) {
272
287
  });
273
288
  await writeFile(pendingPath, `${JSON.stringify({ packages }, void 0, 2)}\n`, "utf8");
274
289
  }
290
+ async function removePendingUpdate(packageName) {
291
+ const pendingPath = resolve(profileDirectory(), PROFILE_PENDING_UPDATES_FILE);
292
+ try {
293
+ const parsed = JSON.parse(await readFile(pendingPath, "utf8"));
294
+ if (!Array.isArray(parsed.packages)) return;
295
+ const packages = parsed.packages.filter((item) => item === null || typeof item !== "object" || item.packageName !== packageName);
296
+ if (packages.length === parsed.packages.length) return;
297
+ if (packages.length === 0) {
298
+ await unlink(pendingPath);
299
+ return;
300
+ }
301
+ await writeFile(pendingPath, `${JSON.stringify({ packages }, void 0, 2)}\n`, "utf8");
302
+ } catch (error) {
303
+ if (error.code !== "ENOENT") throw error;
304
+ }
305
+ }
275
306
  async function recordDeclaredVersion(packageName, version) {
276
307
  const manifestPath = resolve(profileDirectory(), "package.json");
277
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
308
+ let manifest;
309
+ try {
310
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
311
+ } catch (error) {
312
+ if (error.code !== "ENOENT") throw error;
313
+ manifest = {};
314
+ }
278
315
  manifest.dependencies = {
279
316
  ...manifest.dependencies,
280
317
  [packageName]: version
@@ -293,43 +330,87 @@ function pluginCommandError(stderr) {
293
330
  * 复用启动当前服务的 DSH CLI:它会通过 pnpm 从 npm 安装或更新,并自动维护
294
331
  * dsh.profile.bundles,避免浏览器端直接管理 profile 文件。
295
332
  */
296
- function runDshPlugin(args) {
297
- const entry = resolveDshCliEntry();
298
- return new Promise((resolvePromise, reject) => {
299
- const child = spawn(process.execPath, [
300
- ...process.execArgv,
301
- entry,
302
- "plugin",
303
- "--profile",
304
- "web",
305
- ...args
333
+ function pluginExecArgv(args = process.execArgv) {
334
+ return args.filter((arg) => !/^--(?:inspect|inspect-brk|debug|debug-brk)(?:=|$)/.test(arg));
335
+ }
336
+ const activePluginChildren = /* @__PURE__ */ new Set();
337
+ function terminatePluginChild(child) {
338
+ if (child.exitCode !== null || child.killed) return;
339
+ if (process.platform === "win32" && child.pid !== void 0) {
340
+ const killer = spawn("taskkill", [
341
+ "/pid",
342
+ String(child.pid),
343
+ "/T",
344
+ "/F"
306
345
  ], {
307
- cwd: process.cwd(),
308
- env: {
309
- ...process.env,
310
- CI: "true"
311
- },
312
346
  windowsHide: true,
313
- stdio: [
314
- "ignore",
315
- "pipe",
316
- "pipe"
317
- ]
347
+ stdio: "ignore"
318
348
  });
349
+ killer.once("error", () => {
350
+ child.kill("SIGTERM");
351
+ });
352
+ killer.unref();
353
+ return;
354
+ }
355
+ child.kill("SIGTERM");
356
+ }
357
+ /** 插件停用时终止仍在运行的安装进程,避免热更新后遗留 pnpm。 */
358
+ function disposeDependencyInstaller() {
359
+ for (const child of activePluginChildren) terminatePluginChild(child);
360
+ }
361
+ function monitorPluginChild(child, timeoutMs = PLUGIN_INSTALL_TIMEOUT_MS) {
362
+ return new Promise((resolvePromise, reject) => {
363
+ activePluginChildren.add(child);
319
364
  let output = "";
365
+ let settled = false;
320
366
  const collect = (chunk) => {
321
- output += String(chunk);
367
+ output = `${output}${String(chunk)}`.slice(-65536);
368
+ };
369
+ const finish = (error) => {
370
+ if (settled) return;
371
+ settled = true;
372
+ clearTimeout(timeout);
373
+ activePluginChildren.delete(child);
374
+ error === void 0 ? resolvePromise() : reject(error);
322
375
  };
323
376
  child.stdout?.on("data", collect);
324
377
  child.stderr?.on("data", collect);
325
378
  child.once("error", () => {
326
- reject(/* @__PURE__ */ new Error("无法启动 DSH 插件安装命令。请确认 Node.js 与 pnpm 可用后重试。"));
379
+ finish(/* @__PURE__ */ new Error("无法启动 DSH 插件安装命令。请确认 Node.js 与 pnpm 可用后重试。"));
327
380
  });
328
381
  child.once("exit", (code) => {
329
- code === 0 ? resolvePromise() : reject(pluginCommandError(output));
382
+ finish(code === 0 ? void 0 : pluginCommandError(output));
330
383
  });
384
+ const timeout = setTimeout(() => {
385
+ terminatePluginChild(child);
386
+ finish(/* @__PURE__ */ new Error("插件安装超时,已终止安装进程。请检查网络后重试。"));
387
+ }, timeoutMs);
388
+ timeout.unref?.();
331
389
  });
332
390
  }
391
+ function runDshPlugin(args, timeoutMs = PLUGIN_INSTALL_TIMEOUT_MS) {
392
+ const entry = resolveDshCliEntry();
393
+ return monitorPluginChild(spawn(process.execPath, [
394
+ ...pluginExecArgv(),
395
+ entry,
396
+ "plugin",
397
+ "--profile",
398
+ "web",
399
+ ...args
400
+ ], {
401
+ cwd: process.cwd(),
402
+ env: {
403
+ ...process.env,
404
+ CI: "true"
405
+ },
406
+ windowsHide: true,
407
+ stdio: [
408
+ "ignore",
409
+ "pipe",
410
+ "pipe"
411
+ ]
412
+ }), timeoutMs);
413
+ }
333
414
  /** 并发安装互斥:pnpm 锁文件竞争会触发 EPERM/EBUSY,同一时间只允许一个安装进程。 */
334
415
  let installing = false;
335
416
  /** 仅允许安装固定依赖,避免把浏览器输入转成任意命令。 */
@@ -352,10 +433,10 @@ async function installDependencyLocked(id, requestHotUpdate) {
352
433
  const targetVersion = target === dependency.packageName ? latestVersion : await npmLatestVersion(target);
353
434
  if (targetVersion === void 0) throw new Error("无法获取 npm 最新版本,请检查网络或 npm registry 后重试。");
354
435
  const remove = pluginsToRemoveBeforeInstall(declared, target);
355
- if (remove.length > 0) await runDshPlugin(["remove", ...remove]);
356
436
  await ensureLatestReleaseAllowed(target, targetVersion);
357
437
  if (!isOfficialRuntimePackage(target)) await recordDeclaredVersion(target, targetVersion);
358
438
  await recordPendingUpdate(target, targetVersion);
439
+ if (remove.length > 0) await runDshPlugin(["remove", ...remove]);
359
440
  if (requestHotUpdate()) return dependencyStatuses();
360
441
  try {
361
442
  await runDshPlugin([
@@ -368,6 +449,7 @@ async function installDependencyLocked(id, requestHotUpdate) {
368
449
  throw error;
369
450
  }
370
451
  if (await installedPackageVersion(target) !== targetVersion) return dependencyStatuses();
452
+ await removePendingUpdate(target);
371
453
  return dependencyStatuses();
372
454
  }
373
455
  //#endregion
@@ -419,7 +501,7 @@ function crossSiteRequest(request) {
419
501
  /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
420
502
  function publicDependencyError(error) {
421
503
  const message = error instanceof Error ? error.message : "依赖管理暂不可用。";
422
- if (/[A-Za-z]:[\\/]|\/(?:home|Users|var|tmp)\//.test(message)) return "依赖管理暂不可用,请查看服务端日志。";
504
+ if (/[A-Za-z]:[\\/]|\/(?:home|root|Users|var|tmp)\//.test(message)) return "依赖管理暂不可用,请查看服务端日志。";
423
505
  return message;
424
506
  }
425
507
  const inject = [
@@ -528,6 +610,7 @@ function apply(ctx) {
528
610
  }
529
611
  });
530
612
  return () => {
613
+ disposeDependencyInstaller();
531
614
  disposeConnectors();
532
615
  disposeDependencies();
533
616
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.72",
3
+ "version": "0.2.73",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -53,7 +53,8 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsc --noEmit && tsdown",
56
- "test": "tsx tests/hover-tip.assert.ts && tsx tests/companion-slots.assert.ts && tsx tests/channel-api.assert.ts && tsx tests/schedule-sessions.assert.ts && tsx tests/session-tree.assert.ts && tsx tests/pinned-sessions.assert.ts && tsx tests/session-manager.assert.ts && tsx tests/workspace-browser.assert.ts && tsx tests/sidebar-search.assert.ts && tsx tests/settings-navigation.assert.ts && tsx tests/settings-nav-icons.assert.ts && tsx tests/locales.assert.ts && vitest run tests/client-runtime.integration.spec.ts && tsx tests/settings-integration.assert.ts && tsx tests/about-dependencies.assert.ts && tsx tests/dependency-manager.assert.ts && tsx tests/conversation-bubbles.assert.ts && tsx tests/conversation-header.assert.ts && tsx tests/conversation-visuals.assert.ts && tsdown && tsx tests/client-bundle.assert.ts && tsx tests/codex-suite.assert.ts"
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "pnpm run typecheck && vitest run tests/client-runtime.integration.spec.ts && tsdown && node tests/run-assertions.mjs"
57
58
  },
58
59
  "peerDependencies": {
59
60
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",