@michengai/dsh-codex-ui 0.2.72 → 0.2.74

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 +215 -58
  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);
@@ -1103,6 +1160,17 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1103
1160
  if (!after) return hoveredId;
1104
1161
  return index >= ids.length - 1 ? void 0 : ids[index + 1];
1105
1162
  }
1163
+ /**
1164
+ * 置顶标题区与首项目的上半区都表示“插到第一项之前”,因此必须共用同一条指示线。
1165
+ * 非空列表把指示线贴到首项目顶部;空列表才使用独立的起始占位线。
1166
+ */
1167
+ function pinnedHeaderDropIndicator(ids) {
1168
+ const workspaceId = ids[0];
1169
+ return workspaceId === void 0 ? { kind: "empty" } : {
1170
+ kind: "workspace",
1171
+ workspaceId
1172
+ };
1173
+ }
1106
1174
  /** 按指定 id 顺序取出对应项;未出现在 ids 中的项丢弃。 */
1107
1175
  function orderByIds(items, ids, idOf) {
1108
1176
  const byId = new Map(items.map((item) => [idOf(item), item]));
@@ -1229,6 +1297,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1229
1297
  };
1230
1298
  const [renameDraft, setRenameDraft] = (0, react.useState)("");
1231
1299
  const [busy, setBusy] = (0, react.useState)();
1300
+ const busyRef = (0, react.useRef)();
1232
1301
  const [error, setError] = (0, react.useState)();
1233
1302
  const [workspaceDragId, setWorkspaceDragId] = (0, react.useState)();
1234
1303
  const [workspaceDropId, setWorkspaceDropId] = (0, react.useState)();
@@ -1336,9 +1405,12 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1336
1405
  const pinnedGroups = orderByIds(groups.items, pinnedWorkspaceIds, (workspace) => String(workspace.workspaceId));
1337
1406
  const regularGroups = groups.items.filter((workspace) => !projectPinned(workspace.workspaceId));
1338
1407
  const pinDragActive = workspaceDragId !== void 0;
1408
+ const pinnedHeaderDrop = pinSlot === "header" ? pinnedHeaderDropIndicator(pinnedGroups.map((workspace) => String(workspace.workspaceId))) : void 0;
1339
1409
  const assignedIds = workspaces.items.flatMap((workspace) => workspace.sessionIds.map((id) => String(id)));
1340
1410
  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
1411
  const run = async (key, action) => {
1412
+ if (busyRef.current !== void 0) return;
1413
+ busyRef.current = key;
1342
1414
  setBusy(key);
1343
1415
  setError(void 0);
1344
1416
  try {
@@ -1347,6 +1419,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1347
1419
  } catch (reason) {
1348
1420
  setError(reason instanceof Error ? reason.message : String(reason));
1349
1421
  } finally {
1422
+ busyRef.current = void 0;
1350
1423
  setBusy(void 0);
1351
1424
  }
1352
1425
  };
@@ -1453,6 +1526,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1453
1526
  y: menu.y
1454
1527
  } : void 0;
1455
1528
  const isCurrentWorkspace = sessions.current !== void 0 && workspace.visibleIds.includes(sessions.current);
1529
+ const isPinnedHeaderDrop = zone === "pinned" && pinnedHeaderDrop?.kind === "workspace" && pinnedHeaderDrop.workspaceId === String(workspace.workspaceId);
1456
1530
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1457
1531
  className: `dcu-wb-project${isCurrentWorkspace ? " dcu-wb-project-current" : ""}`,
1458
1532
  onDragOver: (event) => {
@@ -1496,7 +1570,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1496
1570
  },
1497
1571
  children: [
1498
1572
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1499
- className: `dcu-wb-project-head${menuOpen ? " dcu-wb-menu-open" : ""}${workspaceDropId === workspace.workspaceId ? workspaceDropAfter ? " dcu-wb-drop dcu-wb-drop-after" : " dcu-wb-drop" : ""}${workspaceDragId === String(workspace.workspaceId) ? " dcu-wb-dragging" : ""}`,
1573
+ className: `dcu-wb-project-head${menuOpen ? " dcu-wb-menu-open" : ""}${isPinnedHeaderDrop || workspaceDropId === workspace.workspaceId ? workspaceDropAfter && !isPinnedHeaderDrop ? " dcu-wb-drop dcu-wb-drop-after" : " dcu-wb-drop" : ""}${workspaceDragId === String(workspace.workspaceId) ? " dcu-wb-dragging" : ""}`,
1500
1574
  role: "treeitem",
1501
1575
  "aria-expanded": isExpanded,
1502
1576
  tabIndex: 0,
@@ -1868,7 +1942,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1868
1942
  if (!pinDragActive) return;
1869
1943
  event.preventDefault();
1870
1944
  event.dataTransfer.dropEffect = "move";
1871
- if (event.target === event.currentTarget || event.target instanceof Element && event.target.closest(".dcu-wb-section-head") !== null) setPinSlot("header");
1945
+ if (event.target === event.currentTarget || event.target instanceof Element && event.target.closest(".dcu-wb-section-head") !== null) {
1946
+ setPinSlot("header");
1947
+ setWorkspaceDropId(void 0);
1948
+ setWorkspaceDropAfter(false);
1949
+ }
1872
1950
  },
1873
1951
  onDrop: (event) => {
1874
1952
  event.preventDefault();
@@ -1915,10 +1993,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1915
1993
  className: "dcu-wb-section-body",
1916
1994
  "data-open": sectionOpen("pinned"),
1917
1995
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1918
- (pinDragActive || pinSlot === "header") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1919
- className: `dcu-wb-pin-start${pinSlot === "header" ? " dcu-wb-drop" : ""}`,
1996
+ pinDragActive && pinnedGroups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1997
+ className: `dcu-wb-pin-start${pinnedHeaderDrop?.kind === "empty" ? " dcu-wb-drop" : ""}`,
1920
1998
  onDragOver: (event) => {
1921
- if (!pinDragActive) return;
1922
1999
  event.preventDefault();
1923
2000
  event.stopPropagation();
1924
2001
  event.dataTransfer.dropEffect = "move";
@@ -2330,7 +2407,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2330
2407
  function useBusyAction(onSuccess) {
2331
2408
  const [busy, setBusy] = (0, react.useState)();
2332
2409
  const [error, setError] = (0, react.useState)();
2410
+ const busyRef = (0, react.useRef)();
2333
2411
  const run = async (key, action) => {
2412
+ if (busyRef.current !== void 0) return;
2413
+ busyRef.current = key;
2334
2414
  setBusy(key);
2335
2415
  setError(void 0);
2336
2416
  try {
@@ -2339,6 +2419,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2339
2419
  } catch (reason) {
2340
2420
  setError(reason instanceof Error ? reason.message : String(reason));
2341
2421
  } finally {
2422
+ busyRef.current = void 0;
2342
2423
  setBusy(void 0);
2343
2424
  }
2344
2425
  };
@@ -2557,11 +2638,15 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2557
2638
  });
2558
2639
  (0, react.useEffect)(() => {
2559
2640
  let disposed = false;
2560
- let loading = false;
2641
+ let active;
2561
2642
  const load = () => {
2562
- if (loading) return;
2563
- loading = true;
2564
- loadChannelGroups().then((next) => {
2643
+ if (active !== void 0 || document.visibilityState === "hidden") return;
2644
+ const controller = new AbortController();
2645
+ active = controller;
2646
+ const timeout = window.setTimeout(() => {
2647
+ controller.abort();
2648
+ }, 8e3);
2649
+ loadChannelGroups(controller.signal).then((next) => {
2565
2650
  if (!disposed) {
2566
2651
  setGroups(next);
2567
2652
  setPollError(void 0);
@@ -2569,16 +2654,23 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2569
2654
  }).catch(() => {
2570
2655
  if (!disposed) setPollError(t("channels.loadError"));
2571
2656
  }).finally(() => {
2572
- loading = false;
2657
+ window.clearTimeout(timeout);
2658
+ if (active === controller) active = void 0;
2573
2659
  });
2574
2660
  };
2575
2661
  load();
2576
2662
  const timer = window.setInterval(load, 4e3);
2663
+ const onVisibilityChange = () => {
2664
+ if (document.visibilityState === "visible") load();
2665
+ };
2666
+ document.addEventListener("visibilitychange", onVisibilityChange);
2577
2667
  return () => {
2578
2668
  disposed = true;
2669
+ active?.abort();
2579
2670
  window.clearInterval(timer);
2671
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2580
2672
  };
2581
- }, []);
2673
+ }, [t]);
2582
2674
  const banner = pollError ?? error;
2583
2675
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2584
2676
  className: "dcu-wb",
@@ -2875,6 +2967,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2875
2967
  }
2876
2968
  //#endregion
2877
2969
  //#region src/client/CodexSidebar.tsx
2970
+ const subscribeEmptyCompanionTabs = () => () => {};
2971
+ const getEmptyCompanionTabs = () => EMPTY_COMPANION_TABS;
2878
2972
  const stylesheet$3 = `
2879
2973
  .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
2974
  .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 +3026,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2932
3026
  const [searchQuery, setSearchQuery] = (0, react.useState)("");
2933
3027
  const [activeSearchIndex, setActiveSearchIndex] = (0, react.useState)(0);
2934
3028
  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));
3029
+ const companionTabs = (0, react.useSyncExternalStore)(companionSlots?.subscribe ?? subscribeEmptyCompanionTabs, companionSlots?.getSnapshot ?? getEmptyCompanionTabs, companionSlots?.getSnapshot ?? getEmptyCompanionTabs);
2936
3030
  const showChannels = companionTabs.channels;
2937
3031
  const showSchedule = companionTabs.schedule;
2938
3032
  const showCompanionTabs = showChannels || showSchedule;
@@ -3126,14 +3220,17 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3126
3220
  (0, react.useEffect)(() => {
3127
3221
  let startX = 0;
3128
3222
  let dragging = false;
3223
+ let pointerId;
3129
3224
  const onDown = (event) => {
3130
- if (!isSidebarDragHandle(event.target)) return;
3225
+ if (dragging || !isSidebarDragHandle(event.target)) return;
3131
3226
  dragging = true;
3227
+ pointerId = event.pointerId;
3132
3228
  startX = event.clientX;
3133
3229
  };
3134
3230
  const onUp = (event) => {
3135
- if (!dragging) return;
3231
+ if (!dragging || event.pointerId !== pointerId) return;
3136
3232
  dragging = false;
3233
+ pointerId = void 0;
3137
3234
  if (!shouldCollapseOnSidebarDrag(startX, event.clientX)) return;
3138
3235
  window.requestAnimationFrame(() => {
3139
3236
  window.requestAnimationFrame(() => {
@@ -3141,11 +3238,18 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3141
3238
  });
3142
3239
  });
3143
3240
  };
3241
+ const onCancel = (event) => {
3242
+ if (event.pointerId !== pointerId) return;
3243
+ dragging = false;
3244
+ pointerId = void 0;
3245
+ };
3144
3246
  window.addEventListener("pointerdown", onDown, true);
3145
3247
  window.addEventListener("pointerup", onUp, true);
3248
+ window.addEventListener("pointercancel", onCancel, true);
3146
3249
  return () => {
3147
3250
  window.removeEventListener("pointerdown", onDown, true);
3148
3251
  window.removeEventListener("pointerup", onUp, true);
3252
+ window.removeEventListener("pointercancel", onCancel, true);
3149
3253
  };
3150
3254
  }, [toggleSidebar]);
3151
3255
  if (collapsed || width < 80) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
@@ -3607,9 +3711,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3607
3711
  const root = (0, react.useRef)(null);
3608
3712
  const stateRef = (0, react.useRef)("loading");
3609
3713
  const requestId = (0, react.useRef)(0);
3714
+ const installingRef = (0, react.useRef)();
3610
3715
  stateRef.current = state;
3611
- (0, react.useEffect)(() => () => {
3612
- alive.current = false;
3716
+ (0, react.useEffect)(() => {
3717
+ alive.current = true;
3718
+ return () => {
3719
+ alive.current = false;
3720
+ };
3613
3721
  }, []);
3614
3722
  const load = (0, react.useCallback)(async (signal) => {
3615
3723
  const currentRequest = ++requestId.current;
@@ -3639,8 +3747,17 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3639
3747
  if (node === null || typeof IntersectionObserver === "undefined") return () => {
3640
3748
  controller.abort();
3641
3749
  };
3750
+ let initialized = false;
3751
+ let wasVisible = false;
3642
3752
  const observer = new IntersectionObserver((entries) => {
3643
- if (entries.some((entry) => entry.isIntersecting)) refresh();
3753
+ const visible = entries.some((entry) => entry.isIntersecting);
3754
+ if (!initialized) {
3755
+ initialized = true;
3756
+ wasVisible = visible;
3757
+ return;
3758
+ }
3759
+ if (visible && !wasVisible) refresh();
3760
+ wasVisible = visible;
3644
3761
  }, { threshold: .2 });
3645
3762
  observer.observe(node);
3646
3763
  return () => {
@@ -3649,6 +3766,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3649
3766
  };
3650
3767
  }, [load]);
3651
3768
  const install = async (id) => {
3769
+ if (installingRef.current !== void 0) return;
3770
+ installingRef.current = id;
3652
3771
  setInstalling(id);
3653
3772
  setMessage(void 0);
3654
3773
  try {
@@ -3669,6 +3788,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3669
3788
  text: error instanceof Error ? error.message : t("about.installFailed")
3670
3789
  });
3671
3790
  } finally {
3791
+ installingRef.current = void 0;
3672
3792
  if (alive.current) setInstalling(void 0);
3673
3793
  }
3674
3794
  };
@@ -3736,7 +3856,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3736
3856
  (!dependency.installed || dependency.updateAvailable) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3737
3857
  className: "dcu-about-install",
3738
3858
  type: "button",
3739
- disabled: installing === dependency.id,
3859
+ disabled: installing !== void 0,
3740
3860
  onClick: () => {
3741
3861
  install(dependency.id);
3742
3862
  },
@@ -4190,11 +4310,27 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4190
4310
  let applying = false;
4191
4311
  let frame;
4192
4312
  let pending;
4313
+ let frameObserver;
4314
+ const watchFrame = (next) => {
4315
+ if (frame === next) return;
4316
+ frameObserver?.disconnect();
4317
+ frame = next;
4318
+ if (frame === void 0) return;
4319
+ frameObserver = new MutationObserver(schedule);
4320
+ frameObserver.observe(frame, {
4321
+ attributes: true,
4322
+ attributeFilter: [
4323
+ "style",
4324
+ "data-sidebar-collapsed",
4325
+ "data-dragging"
4326
+ ]
4327
+ });
4328
+ };
4193
4329
  const apply = () => {
4194
4330
  if (applying) return;
4195
4331
  applying = true;
4196
4332
  try {
4197
- if (frame === void 0 || !frame.isConnected) frame = findSidebarFrame(document);
4333
+ if (frame === void 0 || !frame.isConnected) watchFrame(findSidebarFrame(document));
4198
4334
  if (frame !== void 0) applySlimSidebar(frame);
4199
4335
  } finally {
4200
4336
  applying = false;
@@ -4208,19 +4344,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4208
4344
  });
4209
4345
  };
4210
4346
  apply();
4211
- const observer = new MutationObserver(schedule);
4347
+ const observer = new MutationObserver(() => {
4348
+ if (frame === void 0 || !frame.isConnected) schedule();
4349
+ });
4212
4350
  observer.observe(document.body, {
4213
- attributes: true,
4214
- attributeFilter: [
4215
- "style",
4216
- "data-sidebar-collapsed",
4217
- "data-dragging"
4218
- ],
4219
4351
  childList: true,
4220
4352
  subtree: true
4221
4353
  });
4222
4354
  return () => {
4223
4355
  observer.disconnect();
4356
+ frameObserver?.disconnect();
4224
4357
  if (pending !== void 0) window.cancelAnimationFrame(pending);
4225
4358
  };
4226
4359
  }
@@ -4234,8 +4367,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4234
4367
  return document.querySelector("[data-conversation-scroll]");
4235
4368
  }
4236
4369
  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;
4370
+ return conversationAnchors(root).get(key) ?? null;
4371
+ }
4372
+ /** 一次扫描生成锚点索引,滚动帧内不得为每个轮次重复遍历 DOM。 */
4373
+ function conversationAnchors(root) {
4374
+ const anchors = /* @__PURE__ */ new Map();
4375
+ for (const anchor of root.querySelectorAll("[data-chat-anchor-key]")) {
4376
+ const key = anchor.dataset.chatAnchorKey;
4377
+ if (key !== void 0 && !anchors.has(key)) anchors.set(key, anchor);
4378
+ }
4379
+ return anchors;
4239
4380
  }
4240
4381
  //#endregion
4241
4382
  //#region src/client/TurnNavigator.tsx
@@ -4337,27 +4478,44 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4337
4478
  };
4338
4479
  }, []);
4339
4480
  (0, react.useEffect)(() => {
4340
- const host = conversationScrollRoot();
4341
- if (host === null || turns.length === 0) return;
4481
+ if (turns.length === 0) return;
4482
+ let host = null;
4342
4483
  let frame = null;
4343
4484
  const update = () => {
4344
4485
  frame = null;
4486
+ if (host === null) return;
4345
4487
  const threshold = host.getBoundingClientRect().top + Math.min(180, host.clientHeight * .35);
4488
+ const anchors = conversationAnchors(host);
4346
4489
  let next = turns[0]?.key ?? null;
4347
4490
  for (const turn of turns) {
4348
- const anchor = conversationAnchor(host, turn.key);
4349
- if (anchor !== null && anchor.getBoundingClientRect().top <= threshold) next = turn.key;
4491
+ const anchor = anchors.get(turn.key);
4492
+ if (anchor !== void 0 && anchor.getBoundingClientRect().top <= threshold) next = turn.key;
4350
4493
  }
4351
4494
  setCurrent((previous) => previous === next ? previous : next);
4352
4495
  };
4353
4496
  const schedule = () => {
4354
4497
  if (frame === null) frame = window.requestAnimationFrame(update);
4355
4498
  };
4356
- host.addEventListener("scroll", schedule, { passive: true });
4499
+ const bindHost = () => {
4500
+ const next = conversationScrollRoot();
4501
+ if (next === host) return;
4502
+ host?.removeEventListener("scroll", schedule);
4503
+ host = next;
4504
+ host?.addEventListener("scroll", schedule, { passive: true });
4505
+ schedule();
4506
+ };
4507
+ const observer = new MutationObserver(() => {
4508
+ if (host === null || !host.isConnected) bindHost();
4509
+ });
4510
+ observer.observe(document.body, {
4511
+ childList: true,
4512
+ subtree: true
4513
+ });
4357
4514
  window.addEventListener("resize", schedule);
4358
- schedule();
4515
+ bindHost();
4359
4516
  return () => {
4360
- host.removeEventListener("scroll", schedule);
4517
+ observer.disconnect();
4518
+ host?.removeEventListener("scroll", schedule);
4361
4519
  window.removeEventListener("resize", schedule);
4362
4520
  if (frame !== null) window.cancelAnimationFrame(frame);
4363
4521
  };
@@ -4412,7 +4570,6 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4412
4570
  index: index + 1,
4413
4571
  summary: turn.summary
4414
4572
  }),
4415
- title: turn.summary,
4416
4573
  style: tickStyle,
4417
4574
  onFocus: () => {
4418
4575
  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.74",
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",